Skip to content

Latest commit

 

History

History
207 lines (183 loc) · 116 KB

File metadata and controls

207 lines (183 loc) · 116 KB

Принятые решения

  1. Монорепозиторий. Next.js и FastAPI версионируются вместе, но собираются отдельными контейнерами.
  2. PostgreSQL — единственный обязательный аналитический storage MVP. Интерфейс AnalyticsSink оставляет путь к ClickHouse без двойной записи сейчас.
  3. Воспроизводимая демо-база. Seed 20260717 генерирует 180 дней метрик, вакансии и пользователей; повторный запуск идемпотентен.
  4. Публичные данные ограничены на сервере. Анонимный/free получает не более 30 дней истории, Premium-профессии возвращают teaser без метрик, ranking ограничен top-3. CSS не участвует в защите.
  5. Cookie-сессия. JWT хранится в HttpOnly/SameSite=Lax cookie; изменяющие авторизованные запросы используют double-submit CSRF token.
  6. Демо-платежи не имитируют реальный acquiring. DemoPaymentProvider создаёт проверяемые идемпотентные события. Реальный provider добавляется через интерфейс.
  7. HH выключен по умолчанию. Одновременно обязательны feature flag, письменно подтверждённое право коммерческого использования, app name и contact email. Используется только официальный API, без HTML, CAPTCHA/proxy/rate-limit обходов.
  8. AI необязателен. Объяснимый rule-based classifier — основной. Ollama может разбирать только неопределённые записи через отдельный профиль/флаг; GPU не требуется.
  9. Достоверность важнее красивого числа. Midpoint рассчитывается только при двух границах, gross/net группируются раздельно, минимум по умолчанию — 20 наблюдений.
  10. Ресурсный бюджет. Compose-лимиты суммарно ниже 8 ГБ в local-dev и не резервируют GPU.
  11. Проверки в текущей машине. Системные node, docker и ollama первоначально не обнаружены в PATH; для Node/Python используются runtime Codex, фактический Docker-запуск зависит от доступности Docker Desktop.
  12. Локальная Ollama проверена 2026-07-17. API 127.0.0.1:11434 доступен, установлена mistral:7b (~4,37 ГБ), inference завершился. Однако модель не выполнила простую constrained-инструкцию и сгенерировала нерелевантный текст; её нельзя включать в классификацию без отдельного quality benchmark, JSON-schema validation и rule-based confidence gate.
  13. Demo-аккаунты используют example.com. Адреса @demo.local отклоняются серверным EmailStr, поэтому воспроизводимые пользователи имеют email free@example.com, premium@example.com и admin@example.com. Для уже созданной локальной demo-базы успешный вход по новому адресу безопасно и однократно мигрирует соответствующую legacy-запись.
  14. Первоначальный mailto-подход личного ведения заменён. Ранний MVP открывал почтовое приложение посетителя; решение №29 заменяет его отдельной серверной формой по явному запросу владельца.
  15. Происхождение данных раскрывается без навязчивой маркировки каждого экрана. Главные продуктовые страницы используют нейтральные статусы работы сервиса, а описание подготовленного набора, провайдеров и ограничений остаётся на страницах «Источники» и «Методология», в документации и машинных описаниях. Значение live допустимо только после успешной загрузки законного официального источника, проверки условий использования и сохранения provenance; свежая дата сама по себе не превращает подготовленные данные в реальные.
  16. Стек профессии является публичным справочным контентом. Для всех 50 slug хранится отдельный набор языков, фреймворков, программ и платформ. Он доступен даже на Premium-teaser странице, потому что не содержит платных метрик и улучшает полезность индексируемой landing page.
  17. Недельное изменение в рейтинге означает динамику спроса, а не смену номера строки. Сравниваются средние количества вакансий за два соседних семидневных окна по России; это позволяет показать воспроизводимый процент и стрелку без создания фиктивной истории score.
  18. Машиночитаемость не гарантирует позиции или цитирование. Публичные страницы содержат JSON-LD Occupation, Dataset, WebSite, Organization, canonical URL, sitemap и /llms.txt. Premium-значения в эти представления не добавляются; решение об индексации и цитировании остаётся за поисковой системой или LLM.
  19. Референсы используются только как паттерны взаимодействия. В редизайне объединены последовательная анимация слоёв, кинематографичная иерархия и jobs-first поиск, но не копируются изображения, логотипы, тексты, шрифты, фирменная композиция или товарные знаки Apple, Netflix и hh.ru.
  20. Карьерная сцена является оригинальной и code-native. После недоступности генератора изображений персонаж, деньги, пиджак и сумка реализованы собственными HTML/CSS-слоями без внешних ассетов. Hover имеет эквивалент через focus и tap, а prefers-reduced-motion показывает статичную композицию.
  21. Состояния карьерной сцены должны читаться без инструкции. Начальное состояние показывает сутулую посадку, худи, кресло и обеспокоенное выражение; итоговое - прямую осанку, рубашку, галстук, пиджак, рабочую сумку и позитивное выражение. Подписи «наведите» и «интерактивная сцена» убраны как избыточные.
  22. Светлая тема меняет не только токены карточек. В ней осветляются шапка и главный экран, усиливается белый фон и снижается визуальный вес теней; тёмная тема сохраняет кинематографичную чёрно-графитовую подачу.
  23. Dev и production используют разные каталоги Next.js. Compose задаёт NEXT_DIST_DIR=.next-dev, а production build продолжает собираться в .next. Это исключает повреждение dev route manifest после локальной production-проверки и предотвращает массовый 404 динамических страниц /professions/[slug].
  24. Техподдержка отделена от формы личного ведения. /support отправляет заявку в собственный backend-контур: CSRF и Origin check, Redis rate limit, PostgreSQL, Celery и заменяемый EmailProvider. Если SMTP временно не настроен, заявка не теряется и остаётся в статусе pending_configuration; фактическая доставка включается только секретом окружения.
  25. Почтовые учётные данные не автоматизируются через интерфейс аккаунта. Получатель фиксируется конфигурацией, а SMTP использует отдельный пароль приложения. Обычный пароль пользователя не запрашивается сайтом и не хранится в репозитории.
  26. Максимальная локальная модель отделена от критического пути сайта. На RTX 3070 Ti 8 ГБ + 32 ГБ RAM установлена qwen3.6:27b Q4_K_M (17 ГБ). Она прошла короткую 4K structured-output проверку, но работала примерно 70% CPU / 30% GPU, заняла почти всю RAM/VRAM и выдала около 1,46 токена/с после холодной загрузки. После отдельной Docker-provider проверки AI-assist разрешён только для максимум трёх неопределённых записей в ночном запуске, со schema validation и confidence cap; критический путь остаётся rule-based.
  27. Установка модели не означает её постоянную загрузку. После benchmark выполнен ollama stop qwen3.6:27b: RAM и VRAM освобождены, Docker-сервисы продолжают работать независимо, а 17-гигабайтная модель остаётся на диске.
  28. CSP различает development и production. Next.js development получает unsafe-eval, необходимый только React dev-tools для отладочных стеков; production build его не добавляет. Остальные ограничения CSP одинаковы, а браузерный smoke после изменения не показывает console errors.
  29. Поддержка и личное ведение являются двумя разными функциями. У них отдельные API, CSRF cookies, таблицы (support_requests, mentorship_requests), Celery tasks и темы писем [TechRole Support] / [TechRole Mentorship]. Они разделяют только проверенный SMTP provider и антиспам-инфраструктуру.
  30. Неудачная SMTP-аутентификация автоматически не повторяется бесконечно. После первоначального ответа Яндекса 535 заявки были сохранены без циклических попыток. После включения доступа почтовых программ 2026-07-19 SMTP активирован: Support, Mentorship и Nightly доставлены с разными темами, сохранённые заявки повторно отправлены, наличие пяти писем подтверждено через IMAP. Секрет остаётся только в локальном .env.
  31. Черновой реальный контур использует только официальный open-data API «Работа России». TrudvsemOpenDataProvider не скрейпит HTML, ограничивает число запросов и сохраняет только allowlist без контактов/адресов. HH по-прежнему выключен и не вызывался.
  32. Неизвестный gross/net не смешивается с публичными числами. Реальные записи «Работы России» сохраняются как provenance и salary observations с gross = unknown; nightly job намеренно не перезаписывает текущую gross-витрину до появления корректного совместимого преобразования.
  33. Dagster отвечает за рыночный nightly orchestration. Schedule techrole_midnight_moscow работает в 00:00 Europe/Moscow, имеет persistent run history, отдельный webserver/daemon и email-отчёт. Celery остаётся очередью форм сайта и прикладных задач.
  34. Qwen подключена только как ограниченный AI-assist. Docker успешно вызвал qwen3.6:27b через host.docker.internal; модель возвращает только разрешённые slug/seniority, имеет confidence cap 0.79, максимум три вызова на nightly-run и выгружается после обработки. Rule-based результат остаётся главным.
  35. LLM-доступность строится на открытых стандартах, а не скрытом тексте. Публичные SSR-страницы дополнены llms.txt, llms-full.txt, ai-index.json, Occupation/Dataset/DataCatalog/FAQPage JSON-LD, glossary, editorial policy, sitemap и явными crawler rules. Эти меры улучшают извлекаемость и корректное цитирование, но не гарантируют выдачу конкретной LLM.
  36. Chromium не входит в обычный frontend runtime. Playwright запускается отдельным Compose-профилем на официальном version-matched образе; зависимости и результаты живут в отдельных volumes. Внутренний hostname frontend разрешён только как Next development origin, production security policy не ослабляется.
  37. «Работа России» не считается зеркалом hh.ru. Используется самостоятельный официальный open-data API портала Роструда. Source конкретной записи сохраняется, но запись не приписывается hh.ru без проверяемого provenance.
  38. Официальный 180-дневный слой измеряет публикации. API не предоставляет достаточной истории статуса для восстановления одновременно активных вакансий. Поэтому серия строится по creation-date, старые пробные записи вне окна удалены, а UI и JSON-LD содержат явную оговорку.
  39. Реальные грязные данные не должны останавливать ingestion. Длинные TSV-фрагменты в job-name очищаются до первой ячейки и лимита базы с сохранением оригинала в raw_payload; навыки дедуплицируются по фактически сохраняемым 120 символам. Оба случая покрыты тестами.
  40. Удалённый доступ не публикует RDP в интернете. Tailscale установлен на ПК и iPhone, NLA включена, firewall разрешает 3389 только на Tailscale-интерфейсе из 100.64.0.0/10. Сценарий теперь перезапускает TermService и проверяет listener; после изменения требуется один повторный запуск от администратора. Сон/гибернация ПК от сети отключены.
  41. Машиночитаемые страницы являются открытыми, но не обходят paywall. open-data.json, ai-index.json, RSS, llms.txt и публичный JSON-LD содержат открытые описания, агрегаты публикаций и отдельные зарплатные вилки официального источника с неизвестным gross/net. Premium gross-метрики, score breakdown и длинная история туда не включаются.
  42. Официальная зарплата публикуется отдельным контуром. Для «Работы России» midpoint, median, average и P25/P75 считаются только по RUB-записям с двумя границами и отдельно для Junior/Middle/Senior. Первоначальный порог MIN_SALARY_SAMPLE=20 заменён решением 122; принцип «не публиковать ноль или синтетическое число вместо отсутствующего среза» сохраняется.
  43. История официальной зарплаты использует устойчивые окна. Точки строятся раз в семь дней по предшествующему 30-дневному окну. Это снижает шум редких ежедневных публикаций и не смешивает неизвестный налоговый статус с gross-витриной.
  44. Незавершённый ingestion восстанавливается как ошибка. При следующем запуске состояния running старше часа переводятся в failed с причиной interrupted_before_completion; два следа прерванных перезагрузкой запусков 2026-07-19 исправлены в локальной БД.
  45. Свободный HTTPS не равен production-развёртыванию. Tailscale Funnel доступен на всех тарифах и даёт HTTPS *.ts.net, но остаётся beta, зависит от домашнего ПК и не заменяет стабильный домен, production-секреты и мониторинг. Dev-сервер публично не открыт.
  46. Поисковая регистрация выполняется после появления канонического публичного host. Sitemap, robots, RSS, JSON-LD, /.well-known/llms.txt и IndexNow-compatible данные подготовлены. Отправлять localhost или временный URL в Search Console/Webmaster бессмысленно; внешние ссылки не создаются спамом.
  47. Git baseline исключает локальные зависимости и runtime. Репозиторий инициализирован на ветке main; .env, .pnpm-store, node_modules, Next dist, TypeScript cache, Celery schedule и PostgreSQL backups не отслеживаются. Первый commit фиксирует проверенное исходное состояние до продолжения разработки.
  48. Публичный preview использует только production standalone. Dev-сервер не открывается наружу. public-frontend работает на loopback 3100, получает backend через внутренний Compose DNS и считается healthy только при наличии 50 сущностей в ai-index.json. Новая сборка создаётся в отдельном .next-public-live, чтобы не обрывать туннель на время компиляции.
  49. Для AI-поиска не создаётся псевдоспециальная разметка. Официальные рекомендации Google сводят AI-функции к обычным требованиям индексируемости, полезному видимому тексту, внутренним ссылкам и согласованным structured data. llms.txt остаётся дополнительным открытым указателем, а не обещанием индексации.
  50. Цитирование поддерживается переносимыми стандартами. Публичная страница /citation связана с CSL-JSON, BibTeX, RIS, Frictionless Data Package и Link-отношениями open-data ответа. Форматы используют один canonical host, дату ingestion, методологию и provenance; Premium-поля в них не добавляются.
  51. Исследовательский обзор рассчитывается из открытого API. /research и /research.json динамически суммируют публикации, роли с данными, честные нули, salary-ready выборки и топ профессий. Текст явно называет показатель публикациями по creation date, а не одновременно активными вакансиями.
  52. Auth rate limit хранится в Redis. Login и register разделяют окно 10 попыток на HMAC-хеш IP за 10 минут между backend workers. Превышение возвращает 429; недоступность Redis в production закрывает вход с 503, а не включает бесконтрольный fail-open.
  53. Официальные валютные курсы отделены от demo-нормализации. CbrCurrencyRateProvider читает датированный XML только с HTTPS-host Банка России, сохраняет requested/effective date и нормализует номинал. Миграция 0004 добавляет идемпотентные snapshots USD/EUR/KZT; отдельный Dagster op работает только при CBR_CURRENCY_ENABLED=true. Это ещё не разрешает смешивать неизвестный gross/net с gross-витриной.
  54. IndexNow защищён от временных host. Key-файл появляется только из server-side INDEXNOW_KEY; submit-скрипт сверяет его с публичным host, берёт URL только из sitemap и отклоняет localhost, *.lhr.life и *.trycloudflare.com.
  55. CI и backup являются частью baseline эксплуатации. GitHub Actions выполняет backend, frontend, Compose и Chromium E2E, Dependabot готов для Python/npm/actions. Локальный PostgreSQL custom backup получает SHA-256 manifest, проверку pg_restore --list и retention, но не выдаётся за шифрованную off-host копию.
  56. Минимальная observability не требует передачи пользовательских данных. Backend принимает только безопасный X-Request-ID или генерирует UUID, возвращает его клиенту и пишет JSON log с method/path/status/duration. Query string, IP, email и body не логируются; health checks исключены из обычного access log.
  57. Machine-specific remote-access данные не входят в tracked head. Tailscale IP, MagicDNS и Windows username перенесены в игнорируемый LOCAL_OPERATIONS.md. Старый baseline commit всё ещё содержит прежнюю документацию, поэтому публичный remote должен начинаться с нового очищенного history, а не с публикации текущей истории как есть.
  58. Статус слоя является частью публичного контракта. /data-status, /data-status.json и страницы профессий называют базовую gross-витрину prepared_baseline, официальный 180-дневный ряд - observed_historical, а current_market_claim для обоих равен false. Дата подготовленной метрики не выдаётся за проверенную свежесть рынка; публикация не выдаётся за одновременно активную вакансию.
  59. Открытый CSV имеет длинный seniority-формат. /open-data.csv создаёт по строке на сочетание профессия × Junior/Middle/Senior и обязательно включает период, n, confidence, currency, tax status, canonical URL, source и methodology. Отсутствующее значение остаётся пустым, честный ноль остаётся нулём, текстовые formula-префиксы нейтрализуются для spreadsheet-клиентов.
  60. Метрики имеют ограниченную cardinality и не несут пользовательские данные. Внутренний /metrics экспортирует Prometheus counter/histogram только с labels method, восстановленный route template и status class. Фактический slug/path, query, IP, request ID, email, cookie и body исключены; неизвестный маршрут получает label unmatched. Два production workers используют multiprocess directory в tmpfs и Gunicorn cleanup hook. Optional Prometheus profile хранит 15 дней на локальном volume и содержит alerts для down/5xx/p95, но внешняя доставка без выбранного Alertmanager-канала не включается.
  61. Cache не является границей доступа. Catalog/detail кэшируются только после расчёта entitlement; key parts содержат отдельный public/premium tier и фактически разрешённую глубину истории, затем целиком хешируются SHA-256. TTL равен 120 секундам. Redis failure для cache работает fail-open к PostgreSQL, но не меняет fail-closed политику auth rate limit; hit/miss/error имеют только bounded Prometheus labels.
  62. Временный туннель подключается к постоянному local proxy, а не к пересоздаваемому standalone. public-proxy держит loopback listener 3199 и проксирует в production frontend на 3100. Переключение immutable slot пересоздаёт только upstream с --no-deps, поэтому SSH не теряет локальный origin. Это не гарантирует hostname: анонимный localhost.run может выдать новый URL внутри того же PID, поэтому refresh-скрипт выбирает последнюю запись лога и хранит previous URLs. Proxy удаляет client-IP forwarding headers, имеет независимый noindex health endpoint и не используется как замена production ingress.
  63. Editorial growth ограничен самостоятельными проверяемыми материалами. /insights публикует двенадцать уникальных разборов из утверждённого backlog, каждый имеет четыре и более смысловых раздела, checklist, ссылки на основание, citation text, canonical и TechArticle JSON-LD. Полный текст доступен в insights.json, summaries - в RSS/AI/LLM indexes. Короткие doorway-вариации и массовое размножение одинакового текста не создаются.
  64. Доступность и crawlability являются проверяемыми контрактами. Playwright проверяет representative public pages на один main/h1, уникальные id, подписи form controls, имена интерактивных элементов, alt-атрибуты, keyboard skip-link, отсутствие mobile overflow и фактическое подавление повторяющейся анимации при prefers-reduced-motion. Отдельный dependency-free crawl требует не менее 60 sitemap URL и проверяет каждый canonical HTML URL: HTTP/content type, уникальные title/description, canonical, lang=ru, один h1, отсутствие noindex и синтаксис всего JSON-LD. Временные network/429/5xx ошибки получают максимум пять bounded attempts. Оба слоя запускаются в CI; результат не считается полевым Core Web Vitals замером.
  65. Standalone считается готовым только вместе с browser assets. Next output: standalone не копирует <dist>/static и optional public автоматически. prepare:standalone переносит их внутрь immutable bundle с учётом custom NEXT_DIST_DIR, ограничивает путь frontend-каталогом и проверяет ненулевой asset count. Внешний smoke запрашивает все CSS/JS главной страницы; один только HTTP 200 HTML больше не считается достаточным deployment check.
  66. Lab performance budgets являются regression guardrail, а не полевыми Core Web Vitals. На прогретой повторной навигации representative страниц Playwright требует TTFB ≤ 2000 мс, FCP ≤ 3000 мс, LCP ≤ 4000 мс, CLS ≤ 0.1 и event duration темы ≤ 300 мс. Метрики прикладываются к test result и запускаются в общем E2E profile. Они ловят отсутствующий CSS/hydration и крупные layout shifts, но реальные p75 LCP/CLS/INP после стабильного домена всё равно должны измеряться по RUM/Search Console.
  67. Утверждённый editorial backlog закрывается глубиной, а не количеством URL. Добавлены самостоятельные материалы о 180-дневном ряде, seniority-сигналах, нулевых совпадениях узких ролей, индексе 0–100, server-side paywall и LLM-friendly публикации. Вместе с первыми шестью это покрывает все 12 тем backlog. Новая тема появляется только при отдельном проверяемом вопросе; программное размножение комбинаций профессия×город×уровень не допускается.
  68. Каждая editorial статья цитируется как самостоятельный Web resource. Article page объявляет Highwire-style citation meta и MIME-specific alternates, видимо связывает CSL-JSON, BibTeX и RIS, а insights.json/ai-index.json публикуют те же URL. CSL type равен webpage, author/publisher — TechRole Index, issued берётся из publishedAt, URL всегда указывает на canonical Article page. Extensionless /cite/* paths выбраны после live-проверки: Next 16 непоследовательно регистрировал вложенные dynamic folders с .json/.ris в имени.
  69. Observed publication transforms не используют prepared metric table. Миграция 0005 создаёт observed_publication_metrics_daily с source/date/profession/seniority/region/tax-status lineage. PostgreSQL refresh группирует published_at в UTC, а не snapshot active-state, делает idempotent upsert с run-token, удаляет stale slices в той же транзакции и проверяет суммы/count/coverage до commit. gross, net и unknown разделены; зарплатные значения ниже sample gate остаются NULL. Dagster запускает materialization только после полностью успешного ingestion.
  70. Daily export публикует агрегаты, а не provider payload. /open-data-daily.json и /open-data-daily.csv берут только строки observed_publication_metrics_daily, добавляют source/field dictionary/methodology/citation/transform version и в каждой записи сохраняют current_market_claim=false. Raw payload, пользовательские данные и Premium prepared metrics отсутствуют. API передаёт фактический salary_minimum_sample; недоступность backend не маскируется пустым набором, а даёт 503, Retry-After и no-store. CSV различает пустое значение и наблюдённый ноль и нейтрализует formula-префиксы, в том числе после начальных пробельных и управляющих символов.
  71. HTML-страница набора является описанием, а не копией 691 строк. Канонический /open-data-daily показывает grain, период, агрегаты, полный словарь 27 JSON-полей, ограничения интерпретации и ссылки на JSON/CSV; сами записи остаются в переносимых форматах. Dataset JSON-LD повторяет только видимое и проверяемое, указывает применимые к исходному государственному набору типовые условия открытых данных без самовольной перелицензии в CC BY и сохраняет current_market_claim=false. Summary вычисляется независимо от порядка строк, а machine endpoints связываются со страницей через describedby.
  72. Публичное 180-дневное окно состоит из полных UTC-дней. Research catalog и data provenance используют один helper и полуоткрытый интервал [00:00 date_from, 00:00 после date_to). Текущая секунда больше не отрезает начало первого дня, а now + 1 day не захватывает записи следующего календарного дня. Provenance schema 1.2 явно публикует UTC_calendar_days, начало и exclusive end; накопительная materialized history при этом может быть шире текущего rolling window и сохраняет собственный диапазон.
  73. Daily dataset имеет исполняемый публичный контракт. /open-data-daily.schema.json публикует строгий JSON Schema Draft 2020-12: запрещает неизвестные свойства, требует все 27 полей записи, фиксирует допустимые enum/const, null-safe зарплатные значения, диапазоны долей и current_market_claim=false. JSON export сам ссылается на schema; HTML, Data Package, citation, AI/LLM indexes и provenance дают тот же canonical URL. Unit, Playwright и внешний smoke сверяют контракт с каждой фактической строкой, поэтому документация не может незаметно разойтись с выдачей.
  74. Публичные daily representations поддерживают стандартную условную загрузку. JSON, CSV и JSON Schema получают strong SHA-256 ETag по фактическим байтам; изменяемые JSON/CSV дополнительно публикуют одинаковый Last-Modified по последней материализации. If-None-Match использует weak comparison для GET, понимает список и * и имеет приоритет над If-Modified-Since; совпадение возвращает пустой 304 с cache/link validators. Заголовки открыты через CORS. Ошибки backend остаются 503, no-store и без ETag, чтобы недоступность не кэшировалась как датасет.
  75. Production-конфигурация fail closed и не переиспользует demo volume. Settings отклоняет production с demo mode, localhost/HTTP origin, известным/коротким secret или default/коротким PostgreSQL password. Compose получает согласованные POSTGRES_*/DATABASE_URL, передаёт canonical URL в frontend build и runtime, передаёт SITE_ADDRESS Caddy, очищает base ports/source mounts через !reset/!override и оставляет наружу только 80/443. Rendered JSON проверяется отдельным dependency-free валидатором в CI. На пустой БД seed создаёт только reference catalog; существующие demo accounts или enabled demo source блокируют production bootstrap вместо молчаливой публикации synthetic baseline.
  76. Читаемый backup ещё не считается проверенным восстановлением. Backup script использует фактические POSTGRES_USER/POSTGRES_DB и уникальный container path. Отдельный restore-test сверяет manifest SHA-256, разворачивает custom archive только в БД с генерируемым префиксом techrole_restore_, проверяет Alembic revision, каталог и ключевые таблицы, затем удаляет временную БД. Основная БД не очищается и сервисы не останавливаются.
  77. Бесплатный публичный адрес разделён с production-host. Tailscale Funnel публикует только loopback production-proxy 127.0.0.1:3199 и даёт стабильный preview hostname, но остаётся beta, имеет bandwidth limit и зависит от Windows-host. OCI Always Free Ampere A1 выбран как возможный $0 host полного Compose после регистрации владельца; Render/Koyeb free отвергнуты для основной системы из-за истечения/нестойкости БД, scale-to-zero, недостатка ресурсов и отсутствия worker/persistent volume. Cloudflare named Tunnel предпочтителен после появления собственного домена.
  78. Daily dataset публикуется в Croissant 1.1 как live dataset. /open-data-daily.croissant.json использует профиль http://mlcommons.org/croissant/1.1, описывает все 30 фактических CSV-колонок, их типы и составной ключ, связывает source/provenance/citation и намеренно не фиксирует SHA-256 изменяемого CSV. В HTML встраивается та же JSON-LD модель; AI/LLM indexes, robots, Data Package и Link headers ведут на неё. license ссылается только на федеральные типовые условия, на которые указывает «Работа России»; отдельная лицензия кода или дополнительных прав владельца требует самостоятельного решения. Официальный mlcroissant 1.1.0 обязан успешно выполнить и validate, и загрузку пробных записей.
  79. Публичный Git remote проверяется по всей истории до push. Gitleaks сканирует все commits с redaction. Единственное старое совпадение — синтетический app_secret_key из production-settings test — исключается экспериментальным механизмом .gitleaksignore только по полному fingerprint публичного commit/path/rule/line; целые commits, пути и rules не разрешаются. Staged diff и вся очищенная public-main дают no leaks found; отдельный SHA-pinned GitHub workflow повторяет проверку на push, PR и еженедельно.
  80. Каталог данных дополнительно публикуется в W3C DCAT 3. /catalog.jsonld описывает daily dataset отдельно от его JSON/CSV distributions, указывает прямые download URL, DataService/OpenAPI, temporal coverage, источник, федеральные условия и Croissant/JSON Schema conformance. Endpoint использует тот же materialized observed layer, conditional SHA-256 ETag и fail-closed 503; Data Package, HTML, robots и AI/LLM indexes связывают каталог без создания дублирующей landing page.
  81. Next dev assets разрешены только ожидаемым локальным origin. allowedDevOrigins включает loopback 127.0.0.1, localhost и Compose hostname frontend, чтобы локальный и контейнерный Playwright получали hydration/HMR вместо server-only HTML. Настройка применяется только к dev-resource protection; production CSP и публичный proxy не ослабляются.
  82. CSV получает стандартное W3C CSVW-описание по discoverable URL. /open-data-daily.csv-metadata.json следует стандартному правилу <csv-url>-metadata.json, использует обязательный CSVW context, описывает все 30 фактических колонок, их datatypes/nullability и составной primary key. CSV связывает metadata последним подходящим Link: rel="describedby"; type="application/csvm+json", а metadata обратно объявляет rel="describes". Column definitions разделяются с Croissant, чтобы два контракта не расходились.
  83. Canonical Dataset JSON-LD является центром, а форматы — его документацией. Landing публикует стабильный identifier, alternate name, creator/publisher, datePublished, spatial/temporal coverage и keywords. JSON Schema, CSVW, Croissant и DCAT связаны через subjectOf, а не sameAs: они описывают или представляют один набор, но не являются четырьмя независимыми каноническими копиями.
  84. Schema.org Dataset.citation не используется для самоцитирования набора. По контракту Google Dataset Search это поле предназначено для связанной академической публикации. Сам набор идентифицируют name, identifier, creator, publisher и canonical url; человеко- и машиночитаемые форматы цитирования остаются на /citation, но не маскируются под стороннюю научную работу.
  85. CFF-разметка проверяется по схеме, а не только визуально. Верхнеуровневый GitHub-тип остаётся dataset, но preferred-citation.type использует разрешённое CFF 1.2 значение data. Файл обязан проходить cffconvert 2.0.0 --validate до публикации remote и после добавления постоянных repository/DOI identifiers.
  86. AI-assist получает двойной structured-output guard и измеримый domain gate. Ollama принимает JSON Schema с enum всех 50 slug, additionalProperties=false, nullable seniority и confidence 0..1; Pydantic повторно валидирует фактический ответ, после чего прежний cap 0.79 остаётся обязательным. Проверяемый 20-case synthetic benchmark требует 100% schema validity/abstention и пороги по slug/seniority/exact, выгружает модель в finally и не содержит provider payload. Результат 20/20 не заменяет будущую размеченную holdout-выборку и не делает AI критическим путём.
  87. Backend tests не наследуют mutable Compose .env. conftest.py до импорта приложения принудительно задаёт test app env, SQLite, localhost origins и отключает cache/AI/HH/Trudvsem/CBR/email integrations. setdefault для критических настроек недостаточен: он позволял Compose PostgreSQL и временным public origins менять смысл unit-тестов и вызывать ложный CSRF 403.
  88. Любой публичный tunnel завершается на одном production-like proxy. И Tailscale Funnel, и Cloudflare Quick Tunnel направляются только на loopback 127.0.0.1:3199, где frontend и разрешённые /api/* маршруты объединены одним ingress. Прямой quick tunnel на standalone frontend 3100 был бы неполным: health/API smoke и формы снаружи не работали бы.
  89. Фактический Funnel status важнее exit code команды настройки. После one-time approval CLI может одновременно настроить background Funnel и вернуть ненулевой код исходному ожидавшему процессу. Скрипт считает запуск успешным только если tailscale funnel status --json подтверждает включённый *.ts.net host и точный root proxy на 127.0.0.1:3199; текстовый баннер и один exit code не являются достаточной проверкой.
  90. Эксплуатируемые уязвимости раскрываются приватно. Security-проблемы не направляются в публичный issue вместе с proof of concept или данными. Основной канал после публикации — GitHub Private Vulnerability Reporting, резервный — отдельная тема письма владельцу; data-quality сообщения без security-влияния остаются публичными и используют отдельный шаблон.
  91. Первый публичный snapshot имеет версию 0.1.0. Backend/frontend manifests, CFF и changelog используют одну версию и дату 2026-07-21. Release notes фиксируют проверенные contracts и известные ограничения; динамический период/число наблюдений остаются свойствами dataset provenance, а не SemVer приложения. Ссылки changelog получают фактического GitHub owner до публикации.
  92. После canonical cutover временный tunnel закрывается по проверенной identity. Stop-сценарий не завершает процесс только по сохранённому PID: он дополнительно требует точного полного пути %LOCALAPPDATA%\TechRoleIndex\bin\cloudflared.exe, затем отмечает runtime status остановленным. Tailscale Funnel и production proxy при этом не изменяются.
  93. Все сторонние GitHub Actions закреплены на commit SHA. Официальные major refs проверены через GitHub API 2026-07-21 и заменены неизменяемыми digest с читаемым major-комментарием. Dependabot остаётся каналом обновления digest; плавающий @v4/@v5 больше не исполняется в CI с доступом к repository token.
  94. Связи dataset публикуются стандартным RFC 9264 Linkset. Landing обнаруживает /.well-known/linkset.json через HTTP rel="linkset"; документ связывает canonical dataset, JSON/CSV content resources, JSON Schema, CSVW, Croissant, DCAT, Data Package, CSL и provenance. cite-as указывает на сам стабильный canonical landing, а не выдаёт инструкцию /citation за persistent identifier. Linkset получает registered media type, CORS, strong ETag и условный 304.
  95. Статические Article pages и вложенные citation handlers имеют разные dynamic semantics. dynamicParams=false сохраняется у самой страницы для SSG только 12 утверждённых slugs. Три вложенных route handler явно разрешают dynamic params и сами проверяют slug через getInsight; иначе свежий Next dev cache возвращал 404 даже для существующей статьи, хотя её page была сгенерирована.
  96. Hosted monitor повторяет полный внешний contract smoke раз в сутки. GitHub Actions с минимальным contents: read запускает существующий PowerShell-сценарий на Ubuntu runner против canonical Funnel URL; manual dispatch разрешает другой HTTPS origin. Красный run при выключенном Windows-host считается честным сигналом host-dependent preview, а не дефектом dataset contract.
  97. ЮKassa — основной платёжный адаптер, Robokassa — резерв. Решение принято по официальным материалам на 2026-07-21: у ЮKassa зрелый test shop/REST/idempotency/refund/autopayment contract, а подлинность неподписанного webhook безопасно устанавливается авторизованным чтением объекта. Для владельца на НПД выбор повторно проверяется до live: ЮKassa больше не автоматизирует чеки самозанятых, тогда как Robokassa публично предлагает Roboчеки.
  98. Browser никогда не определяет сумму или статус заказа. Он передаёт только allowlisted product code, текущую версию условий и факт явного принятия. Amount/currency/description/access duration создаёт серверный catalog; любой webhook повторно сверяется с локальным order, test/live mode и provider object.
  99. Webhook authenticity зависит от провайдера, но fail closed одинакова. Demo использует HMAC raw body и constant-time compare. ЮKassa не подписывает уведомления, поэтому тело считается лишь сигналом: backend с Basic auth запрашивает текущий payment/refund по id и обрабатывает только ответ API. Payload событий хранится без карточных/персональных данных, повторы дедуплицируются уникальным provider event id.
  100. Реальные списания требуют нескольких независимых guard. Одного production key недостаточно: обязательны отдельные PAYMENTS_LIVE_CONFIRMED, PAYMENTS_LEGAL_APPROVED, утверждённая не-draft версия оферты, статус продавца, совместимая фискализация и при онлайн-кассе явный актуальный VAT code. НПД и ИП/компания не могут использовать одну и ту же фиктивную кассовую настройку.
  101. Tailscale Funnel остаётся sandbox-preview, а не платёжным hosting. Он доступен по HTTPS, но webhook зависит от включённого ПК и локального Docker. Live требует постоянного домена/host, мониторинга и резервного восстановления; до этого UI обязан явно маркировать оплату тестовой и не запрашивать реальные реквизиты.
  102. Фактические доходы специалистов являются третьим слоем данных. Публичные исследования не заполняют NULL в vacancy metrics и не участвуют в score. Профессия получает точный срез, затем технологический/смежный, а при отсутствии — явно категорийный fallback; P10/P90 не интерпретируются как Junior/Senior.
  103. Главный salary benchmark зафиксирован на отчёте Хабр Карьеры I полугодия 2026. Публичный срез n=45 226 хранит период, source URL, методологию, net basis и salary_plus_bonus. Для грейдов допускаются дополнительные публичные источники, но их малые n, unknown tax status и замечания об аномалиях обязаны оставаться видимыми.
  104. Provenance schema 1.3 публикует три несмешиваемых слоя. prepared_analytics, official_publications и salary_benchmarks имеют собственные статусы и current_market_claim=false. Для 50 профессий контракт фиксирует покрытие: direct, related или category-only; обновление чисел требует изменения версионированного source dataset и тестов полного покрытия.
  105. rules-v2 расширяет покрытие только однозначными сигналами. Skills участвуют лишь для обобщённого software-заголовка и только когда после подавления специфичных ролей остаётся одна профессия; явный title alias имеет приоритет, неоднозначность означает abstention. Повторная классификация затрагивает только null/rules-*, поэтому две AI-классификации сохранены. На текущей БД rule-managed покрытие выросло с 1 050 до 1 130 строк без очисток, а число представленных профессий — с 33 до 36.
  106. Подтверждённый НПД меняет основной платёжный вариант на Robokassa. Решение 97 сохраняет историю выбора до ответа владельца, но с 2026-07-22 его supersede-ит Robokassa: официально поддерживаются самозанятые без ИП и автоматические «Робочеки СМЗ». ЮKassa остаётся резервом с сильным REST contract, но её автоматизация чеков НПД прекращена. Публичный калькулятор Robokassa на дату проверки показывал для стартового НПД карты РФ 3,9% и СБП 2,7%; договор в кабинете всегда приоритетнее.
  107. Robokassa ResultURL считается подлинным только после полной серверной сверки. Browser передаёт лишь product code; сервер формирует сумму, описание, чек, числовой InvId, Success/Fail URL и Shp_order_id, подписывает Паролем №1, а ResultURL принимает form-urlencoded POST только с корректной constant-time подписью Паролем №2. Сумма, order/payment id и test/live режим повторно сверяются с БД, повторы дедуплицируются, а успешный запрос всегда получает требуемый OK<InvId>.
  108. Возврат Robokassa в test и live имеет разные проверяемые контуры. Официальный OpStateExt не видит test payments, поэтому безденежные возвраты остаются в demo и MockTransport contract tests. Live сначала получает подписанный OpKey, затем отправляет HS256 JWT по отдельному Password3; pending-запросы сверяются фоново, и entitlement отзывается только после подтверждённого finished. Production принимает только официальные API endpoints, чтобы секреты нельзя было вывести через изменённую конфигурацию.
  109. Постоянный host выбирается по воспроизводимому бюджету, а домен — только на владельца. На 2026-07-22 techrole.ru был свободен; официальные страницы показывали .ru за 200 ₽ с продлением 399 ₽ у Timeweb и 199/420 ₽ у Beget. Основной экономный вариант — Timeweb Cloud 2 vCPU/4 ГБ/50 ГБ от 721 ₽/месяц; Funnel остаётся preview. Покупка домена/аккаунта через FunPay отвергнута из-за отсутствия подтверждённого выигрыша и риска чужой регистрационной ownership.
  110. Публичная SEO-медиана допустима, скрытые данные калькулятора — нет. Для семи точных ролей Хабр Карьера открыто публикует медиану общего дохода в title/description отфильтрованной страницы. Снимок 2026-07-22 хранит только это число и прямую ссылку; значения распределений/грейдов, скрытые входом, не извлекаются. Поскольку публичная методология не подтверждает для этих страниц n и gross/net, оба поля остаются неизвестными. Покрытие меняется с 29/12/9 на 36 прямых, 12 смежных и 2 только категорийных роли без влияния на vacancy metrics или score.
  111. Датированный зарплатный снимок проверяется, но не обновляется автоматически. Точная публичная медиана ручного тестирования расширяет покрытие до 37/11/2. Опциональный Dagster op делает максимум три ограниченные попытки и разбирает только общедоступные title/meta description восьми официальных страниц Хабр Карьеры. Совпадение даёт verified, транспортная ошибка — отдельный unavailable, а изменившееся число останавливает op до ручной проверки; скрытые входом данные и автоматическая перезапись запрещены.
  112. Результат аудита источника должен переживать процесс Dagster. Каждый включённый запуск записывает безопасный итог и публичные детали проверки в существующий PostgreSQL audit_logs до обработки changed; /status раскрывает только время и агрегированные счётчики. Это делает последнюю фактическую проверку видимой после рестарта без новой таблицы, без доверия браузеру и без автоматического изменения зарплатного снимка.
  113. Зарплатный справочный слой публикуется отдельно от vacancy datasets. /salary-benchmarks, JSON и CSV экспортируют 50 профессий с direct/related/category coverage, географией, метрикой, периодом, n, tax status и первичным URL. Они сохраняют current_market_claim=false, не содержат Premium-полей, не участвуют в score и не входят в Data Package официальных данных «Работы России», чтобы его федеральная лицензия ошибочно не распространялась на сторонние исследования; условия дальнейшего использования остаются source-specific.
  114. Пустые ячейки не заполняются переименованными данными. Для каждой профессии точное число публикаций сохраняется отдельно от суммы направления; оба ряда показываются по неделям. Для зарплат официальный midpoint публикуется при n≥3; выбор единого комплекта уровней уточнён решением 122. Только отсутствие подходящего уровневого источника закрывает явно подписанная общерыночная медиана опроса Профсоюза работников ИТ (n=1 539). Так UI получает 150 из 150 Junior/Middle/Senior карточек без выдуманных ролевых значений, скрытых источников и подмены нулей.
  115. На странице профессии существует один комплект Junior/Middle/Senior. Комплект выбирается по качеству provenance и согласованности уровней согласно решению 122. Наибольшая сумма сама по себе не выигрывает. Повторные карточки official/prepared слоёв удалены; n, период, scope и tax status остаются видимыми, а динамические графики сохраняют свои независимые слои.
  116. Category-only покрытие снимается только честным смежным источником. Публичные SEO-медианы калькулятора Хабр Карьеры дают ML-разработчика 230 083 ₽ и ERP-программиста 147 500 ₽ на 2026-07-22. Они закрывают контекст NLP и SAP как related_role, а не exact_role; n и gross/net остаются неизвестными. Покрытие становится 37 direct / 13 related / 0 category-only, при этом категорийные региональные значения продолжают существовать только как подписанный контекст.
  117. Стоимость личного ведения согласуется по заявке. Вместо неподтверждённого фиксированного тарифа страница предлагает кандидату назвать комфортную стоимость в отдельном поле. Формат, объём работы и итоговые условия рассматриваются индивидуально; это не включает оплату и не обходит отдельные legal/payment guards.
  118. Предлагаемая стоимость хранится как часть обращения, а не заказа. Форма получает целое число рублей в диапазоне 1 000–1 000 000, сохраняет его отдельно и включает в письмо владельцу. Это значение не создаёт платёж, не меняет серверный каталог товаров и не может стать суммой списания без отдельного согласования и серверного заказа.
  119. Готовность платежей проверяется без выдачи credentials. Admin-only endpoint и карточка /admin показывают отдельные checklist test/live и вычисленный ResultURL, но сериализуют только булевы признаки. Live-конфигурация дополнительно требует явный PAYMENTS_STABLE_HTTPS_CONFIRMED=true; его можно выставить только после проверки постоянного HTTPS-домена и круглосуточного host, поэтому зависимый от ПК Tailscale Funnel не снимает блокировку.
  120. SEO-метаданные категорий не зависят от повторного API-вызова. Восемь стабильных названий и описаний каталога продублированы в маленьком frontend reference-модуле. generateMetadata читает их локально, а SSR использует как fallback при временной недоступности backend; это исключает пустой description и повторяющийся generic title, не подменяя данные профессий.
  121. Production standalone получает внутренний API-адрес при сборке и запуске. Next.js фиксирует destination для /api/* во время build, поэтому одного runtime environment недостаточно. Production Compose передаёт http://backend:8000 в оба этапа, а fail-closed validator проверяет их совпадение; публичный Caddy остаётся единственной точкой входа.

2026-07-22 — Selectel permanent domain

techrole.ru is the canonical public host on the existing Selectel VDS. DNS stays with Selectel (A @ to 94.102.88.123, CNAME www to the apex) and Caddy terminates HTTPS. The legacy sslip.io URL is retained only as a redirect for continuity. Live payments remain disabled until DNS/TLS and Robokassa production prerequisites are independently verified.

2026-07-23 — Search, resilience and Premium price

The catalog keeps server-side filtering as the source of truth while adding native Russian/English suggestions and a combined category selector. Profession pages expose section anchors, compact non-duplicating facts and confidence labels derived from the underlying layer; temporary API failures have a bounded timeout and friendly recovery links. The only Premium product remains a one-time 30-day access package priced at 290 RUB in the server catalog; payment mode stays disabled until all live guards pass.

2026-07-23 — Public distribution and legal configuration

Seller identity is server-only configuration and is never committed. Legal pages render complete operational text from those variables without public owner-only review notes; the remaining activation checklist lives in the protected admin/payment documentation. Every profession gets a generated OG card and share/citation actions; comparisons use allowlisted canonical query links. The weekly report and RSS are public. IndexNow runs only after a successful materialization and fails the Dagster step on a rejected submission; the weekly Telegram job is opt-in and skips without credentials. None of these paths exposes Premium fields or enables real charges.

2026-07-23 — Salary coherence and Premium account UX

  1. Три полные вилки достаточны для публикации зарплатного среза. Значение MIN_SALARY_SAMPLE по умолчанию снижено до 3; n=3 в интерфейсе называется базовой выборкой и не обозначается ненадёжным. Комплект Junior/Middle/Senior берётся из официального слоя только когда все три медианы присутствуют и не убывают с ростом уровня. При пропуске или инверсии все три видимых значения берутся из одного наиболее точного открытого исследования, а официальный n остаётся рядом для проверки. Это не меняет первичные данные и не повышает зарплату искусственно.

  2. Premium-статус показывается без изменения существующих кнопок шапки. Отдельный клиентский индикатор появляется только после подтверждённого /auth/me, работает в обеих темах и не делает публичные страницы зависимыми от серверных cookies. Личный кабинет и тарифы явно показывают уже активный Premium; правила уведомлений можно поставить на паузу, возобновить и удалить через owner-scoped API с CSRF.

  3. В публичном интерфейсе нет редакторских и инфраструктурных пометок для владельца. Удалены объяснение внутренней коррекции перевёрнутых зарплатных грейдов, pre-live предупреждения над юридическими документами, ссылка на администрирование в общем footer и подробности о контейнерах, Redis, Dagster и runtime-флагах на публичной странице статуса. Пользователь видит итоговые значения, источники, необходимые методологические ограничения и понятный статус свежести данных; технические checklist остаются только в защищённой админ-панели и документации.

  4. Robokassa с фискальной номенклатурой открывается только через POST. Сервер по-прежнему вычисляет продукт, сумму, Receipt, InvId, Return URL и подпись; браузер лишь превращает подписанный набор параметров в скрытую HTML-форму к https://auth.robokassa.ru. CSP разрешает form-action только собственному origin и этому официальному host. Для возвратного чека используется исходное наименование купленной услуги, а не административная причина возврата. Активация магазина сама по себе не снимает live guard: отдельно нужны credentials и подтверждённый статус «Робочеки СМЗ».

  5. Канонический endpoint формы Robokassa — https://auth.robokassa.ru/Merchant/Index.aspx. Первый официальный test checkout обнаружил 404 на альтернативном URL из quick-start. Адрес заменён во всех runtime defaults, production allowlist и frontend contract tests на URL из актуальной документации интерфейса оплаты; выбор POST для запроса с Receipt сохранён.

  6. Сравнение не заканчивается пустой ячейкой и не выдаёт смешанное направление за зарплату роли. Для Junior/Middle/Senior сначала используется полный непротиворечивый 180-дневный срез профессии, затем подписанный уровневый ориентир открытого исследования и только при его отсутствии — непротиворечивый срез направления. Такой порядок не позволяет узкой профессии наследовать медиану направления, фактически сформированную другой массовой ролью: например, срез Data & AI в «Работе России» преимущественно состоит из администраторов баз данных и не является зарплатой Analytics Engineer. Публикации и удалённая работа могут использовать exact → category, но рядом всегда показываются источник и фактические числитель/знаменатель.

  7. Выбор направления в каталоге применяет фильтр сразу. Категория остаётся серверным параметром и дополнительно проверяется перед SSR-рендерингом карточек. Изменение select отправляет GET-форму без отдельного клика; кнопка поиска расположена перед выбором направления. Premium-карточка сохраняет серверную цену 290 ₽, показывает прежний ориентир 1 349 ₽ зачёркнутым и визуально приглушается для уже активного доступа.

  8. Форматы цитирования разборов имеют отдельные стабильные файловые URL. CSL-JSON, BibTeX и RIS публикуются через /insight-citations/<slug>.*; прежние вложенные URL перенаправляются постоянно. Это обходит неоднозначность вложенного route handler в Next dev/standalone и сохраняет уже опубликованные ссылки.

  9. Зарплатный график показывает накопительную 180-дневную медиану и честный fallback направления. Решение 43 о скользящем 30-дневном окне заменено: оно оставляло пустой график даже при достаточной общей выборке, если три полные вилки не попадали в одно короткое окно, а последний день периода иногда вообще не становился точкой. Теперь недельная точка включает все совместимые RUB-вилки уровня от начала периода до своей даты, конец периода добавляется обязательно, а при слишком редком точном ряде целиком выбирается более наполненный ряд направления с явной подписью scope. Синтетические значения не создаются, порог n=3 сохраняется.

  10. В шапке существует одна Premium-кнопка с двумя состояниями. Решение 123 об отдельном индикаторе заменено: гостю та же кнопка ведёт к тарифу и показывает Premium, а после подтверждения /auth/me она меняет состояние на Premium активен и ведёт в кабинет. Дублирующий пункт навигации удалён; кнопка остаётся читаемой в светлой и тёмной темах.

  11. Публичные machine endpoints fail closed при недоступном источнике. open-data, answer-first, AI index, research, full LLM context и RSS больше не возвращают правдоподобный пустой 200. Они отвечают 503/no-store/Retry-After, а успешный контент получает SHA-256 ETag, Last-Modified при наличии даты и 304. Статические форматы связываются явными suffix URL и Link/alternate; зависимость от неявного Accept не вводится.

  12. Answer-first слой содержит только воспроизводимые публичные срезы. /answers и /answers.json показывают top публикаций, salary by level с фактическим n, регионы, соседние недельные окна и ограничения со стабильными fragment URL. JSON-LD совпадает с видимым текстом, current_market_claim=false; Premium и подготовленные значения не попадают в слой.

  13. Каталог платежей является server-side registry с immutable order snapshot. Каждый SKU задаёт server price, результат/срок услуги, fulfillment, entitlement, receipt nomenclature и refund policy. Browser передаёт только allowlisted product code. Исторический заказ и возврат используют сохранённый snapshot даже после изменения текущей цены/каталога.

  14. Test и live Robokassa credentials не взаимозаменяемы. Test использует отдельные Password №1/№2 с временным legacy fallback только для старых sandbox environments; live принимает только explicit live Password №1/№2/№3. Для НПД live дополнительно требует owner-only PAYMENTS_ROBOCHEKI_SMZ_CONFIRMED=true; без него backend не стартует в live.

  15. Возврат продления уменьшает цепочку доступа на срок возвращённого SKU. Новые entitlement продления начинаются после текущего конца, account показывает полный effective expiry. Полный refund отзывает entitlement заказа и сдвигает последующие payment-entitlements, поэтому возврат ранней покупки не оставляет бесплатные 30 дней.

  16. Аналитика является consent-based, first-party и честно ограниченной. Встроенный admin dashboard хранит HMAC browser ID, публичный path/target и hostname referrer без IP, email, raw User-Agent, query/forms/payment data; admin и automation исключаются. Crawler request считается только по заявленному официальному User-Agent и не доказывает личность. Фактическое число внешних AI-цитат недоступно; показываются только copy actions, AI referrals и declared crawler requests.

  17. Live сохраняется только после расчётного control payment. 24 июля 2026 года readiness и официальный sandbox были зелёными, но InvoiceID 30–32 из-за недостаточного остатка остались в Robokassa State=10; деньги, ResultURL и entitlement отсутствовали, а локальные заказы не получили выдуманный terminal status. После пополнения InvoiceID 33 на 290 ₽ получил расчётное состояние, обработанный подписанный ResultURL и отдельный Premium entitlement ровно на 30 дней. Фискальный чек подтверждён владельцем одновременно в Robokassa и «Мой налог», поэтому production разрешено оставить в robokassa/live.

  18. Плотность публикаций, полнота зарплат и исследовательский ориентир показываются раздельно. Число публикаций не считается числом одновременно активных вакансий и не обещает большую зарплатную выборку. API и страница отдельно показывают дневные/недельные полные RUB-вилки и их долю. На странице остаётся один официальный график зарплаты: сплошные линии — накопительные наблюдения профессии или явно подписанного направления по опубликованному правилу отбора, пунктир — статичный ориентир одного непротиворечивого исследования для отсутствующего уровня. Карьерные карточки используют полный неубывающий комплект одного источника. Дублирующий график зарплаты подготовленной витрины удалён, а её vacancy_count переименован в расчётный объём и не смешивается с официальным слоем.

  19. Карточка профессии следует пользовательскому сценарию, а не внутренней модели данных. После hero первым содержательным блоком идёт стек, затем доходы, динамика, подготовленный расчётный ряд, компактный период, provenance и индекс. Прежняя верхняя сводка из четырёх карточек удалена; таблицы индекса/навыков/регионов и блок смежных ролей сохранены.

  20. Редкая профессия получает подписанный контекст, а не пустой или смешанный график. Поток публикаций и полнота используют точную профессию при total_publications >= 20, иначе одно явно названное направление. Зарплата сохраняет per-level exact-first n>=3; инверсии не переставляются, а непротиворечивые карьерные карточки остаются отдельным представлением.

  21. Росстат 57-Т — широкий официальный фон occupation_group. Средняя начисленная gross-зарплата группы ОКЗ не считается медианой, ролевым значением или размером выборки. Selecty, Antal и SuperJob не импортируются без ясного права повторной коммерческой публикации.

  22. Индекс v1.1.0 использует публичный salary input и больший вес качества. Веса: demand 28%, salary 24%, growth 16%, junior 12%, remote 10%, quality 10%. Остальные компоненты остаются prepared; старые строки версионируются, а API присоединяет только активную scoring version.

  23. Динамика зарплаты использует явную reference-фильтрацию, не меняя исходный слой. Базовая национальная медиана выбирается из уже опубликованных ориентиров в порядке exact_role → technology → related_role → category. В динамический midpoint-ряд входят вилки не ниже 40% базы для Junior, 70% для Middle и 100% для Senior; после этого применяется fallback profession → category → статичный market reference. База, scope, коэффициенты и денежные пороги входят в публичный API и методологию. Счётчики публикаций, полнота, raw observations, salary benchmark cards и scoring не фильтруются и не пересчитываются этим правилом.

  24. Ручной Premium — отдельный от платежа административный override. Переключатель /admin создаёт бессрочный entitlement admin_override; режим Free отзывает только admin_override и прежние admin_grant. Entitlement с источником payment:* не затрагивается: оплаченный доступ прекращается только штатным refund-процессом. Mutation требует admin RBAC + CSRF и фиксируется в audit log; список пользователей никогда не сериализует password hash, provider IDs или credentials.

  25. Главный зарплатный график показывает дневное скользящее среднее, а не накопительную медиану. Решение 130 заменено для этого представления: накопление с начала периода превращало линии в почти неизменные ступени и скрывало движение новых наблюдений. Каждая точка теперь является средним midpoint прошедших reference-фильтр полных RUB-вилок за предшествующие 30 дней при n≥3. Динамический scope выбирается только при наблюдениях минимум с трёх разных дат в порядке profession → category → market; scope всегда подписан. Пороги 40%/70%/100%, сами базовые медианы, raw observations, полнота и карточки уровней не меняются. Статичный пунктир исследования остаётся только последним fallback без синтетической истории.

  26. Верхний зарплатный ориентир — одно значение и одна сравнительная шкала. Для каждой профессии выбирается ровно одна национальная медиана без грейда в порядке technology → exact_role → related_role → category → market_level; fallback остаётся явно подписанным своим scope. Справа круговая диаграмма сравнивает её с максимальной медианой среди этих же 50 выбранных ориентиров, поэтому знаменатель воспроизводим и обновляется вместе с серверным каталогом. Источник, период и ограничения сохраняются ниже в provenance-карточках, но не дублируются в главной карточке. Логика и значения Junior/Middle/Senior не меняются; удалён только длинный поясняющий абзац перед ними.

  27. ФИО исполнителя публикуется только в оферте. Server-side реквизит остаётся обязательным и отображается в разделе реквизитов публичной оферты. Политика обработки данных и тексты согласий используют обозначение сервиса TechRole Index и дают ссылку на оферту, не дублируя ФИО на других страницах.

  28. График полноты зарплатных вилок удалён из всех карточек профессий. Машинные данные и проверяемые счётчики полных вилок сохраняются в открытом наборе, но отдельный смешанный график публикаций, вилок и процентов больше не занимает место в пользовательском сценарии. Мобильная карточка использует однострочное прокручиваемое оглавление, компактные панели и KPI без горизонтального переполнения.

  29. Недельные исследования — неизменяемые редакционные снимки открытого daily-набора. Раздел /insights объединяет датированные исследования и методические разборы, а видимая ссылка «Исследования» добавлена в основную навигацию. Каждый недельный материал фиксирует два полных семидневных UTC-окна, dataModifiedAt, арифметику и оговорку current_market_claim=false; динамическая перезапись исторической статьи новыми данными запрещена. Исследования получают Report JSON-LD и отдельные CSL-JSON/BibTeX/RIS, входят в RSS, sitemap, AI/LLM indexes и ссылаются на исходный набор вместо публикации непроверяемых выводов.

  30. Одобренный HH API остаётся отдельным ограниченным снимком. Приложение TechRole Index №26065 одобрено для заявленной публичной коммерческой агрегированной аналитики. Application token хранится только в окружении. Поиск выполняется по роли и алиасам в документированной глубине до 2 000 результатов на запрос, затем глобально дедуплицируется по vacancy id; обход лимитов, scraping и прокси запрещены. Raw payload ограничен allowlist без описаний, работодателей и контактов. Gross, net и неизвестный basis не смешиваются, а публично показываются только source-isolated агрегаты с явной оговоркой, что это не полная историческая база.

  31. Истёкший application token HH обновляется только официальным client_credentials flow. Основной token можно задать напрямую; серверная пара client id/secret является необязательным production fallback. Провайдер обновляет token только при отсутствии token или документированной OAuth-ошибке истечения, повторяет запрос один раз и не публикует credentials, token response или секреты в API и логах. Это обеспечивает ночное обновление без ручного вмешательства и не меняет лимиты поиска.

  32. Подробные карточки HH обогащают только используемый публичный ролевой слой и публикуются агрегированно. После поиска возобновляемый detail-проход запрашивает GET /vacancies/{id} только для вакансий последнего успешного query-match run, соблюдает задержку, Retry-After и пакетные commit. Allowlist включает ключевые навыки, языки, опыт, занятость, рабочие форматы/графики, профессиональные роли и публичные id/названия работодателей; описания, контакты, адреса, response URL и персональные данные исключены. Решение 151 уточнено: работодатели допустимы только как публичный топ-5 по профессии, весь оставшийся хвост объединяется в «Другие компании». Полные карточки и списки всех работодателей не публикуются.

  33. Индекс v1.2.0 использует собственный HH-срез каждой профессии. Веса остаются сопоставимыми с v1.1.0, но demand/growth/junior/remote/quality больше не берутся из prepared-витрины. Salary использует только exact-profession полные RUB gross-вилки при n≥5; меньшая выборка получает нейтральную peer-медиану и низкий quality, а не категорийное значение. HH-график публикаций переключается на точную профессию при n≥5, зарплатная история HH не заимствует направление. Все 50 строк пересчитываются в новой version, старая история не переписывается.

  34. Каталог сохраняет 50 ролей, но заменяет десять с n<5 более наблюдаемыми профессиями. Cloud, Analytics Engineer, PostgreSQL DBA, Firmware, Unreal, SOC, NLP, Ruby, generic JavaScript/TypeScript и SAP не теряются: их алиасы переходят к DevOps, Data Engineer, DBA, Embedded, Game Developer, информационной безопасности, AI Engineer, Backend, Frontend и ERP. Освободившиеся позиции занимают Technical Support Specialist, Product Manager, IT Project Manager, 1C Analyst, QA Engineer, System Engineer, UX/UI Designer, AI Engineer, ERP Specialist и Technical Writer. Старые публичные URL получают постоянные семантические redirect, а не ведут на случайную новую роль из той же строки миграции.

  35. Порог пригодности профессии для отдельной карточки — пять собственных HH-вакансий. На контрольном снимке rules-v3 классифицирует 11 132 из 42 153 уникальных вакансий, и минимальная выборка среди 50 ролей равна шести. Новые высокосигнальные роли дают от 59 до 523 точных совпадений. Длиннейший алиас имеет приоритет, явные отрицательные шаблоны отсеивают не-IT product/system/QA/technical-writer вакансии, а неоднозначность по-прежнему означает abstention.

  36. Расчётный ряд становится точной rolling-моделью HH, а не наследием демонстрационной витрины. Для каждой профессии и уровня сохраняется ежедневный объём её собственных публикаций в предшествующем 30-дневном окне; явный уровень заголовка приоритетнее документированного кода опыта HH. Nightly пересобирает этот слой после materialization, затем v1.2.0 повторно рассчитывает все 50 оценок. Данные направления не заполняют отсутствующую профессию.

  37. Карточка профессии показывает пользовательские ответы и не визуализирует внутреннюю схему данных. Скрыты provenance-блок «Слои, которые нельзя смешивать», служебная финальная строка, смежные роли, форма занятости, рабочие дни/часы, профессиональные роли, ГПД и инклюзивность. Навыки, полезные условия и регионы объединены в один рыночный блок. Удалены отдельные графики HH-зарплаты и интенсивности; основной salary-график доступен всем максимум за 30 дней, его 90/180 кнопки заблокированы без Premium, а другие временные графики доступны только Premium и занимают половину desktop-ширины. API обрезает сами временные массивы, поэтому ограничение не является только CSS. Employer dashboard сохраняет топ-5 плюс «Другие компании», но не кладёт текст поверх кольца: все подписи и числа вынесены в отдельный список.

  38. Поисковое совпадение HH и каноническая классификация — разные отношения. Для каждой профессии и алиаса HH ищется документированной точной фразой только в поле названия вакансии (search_field=name). Все выданные vacancy id сохраняются в отдельном many-to-many слое последнего полностью успешного запуска и дедуплицируются внутри профессии; одна вакансия может честно относиться к нескольким поисковым ролям. Публичные HH-счётчики, работодатели, навыки, rolling-метрики и score используют этот слой, а консервативный Vacancy.profession_id остаётся независимым и не переписывается результатом поискового запроса. Материализация выполняет полную замену снимка: связи и дневные срезы, исчезнувшие из нового успешного запуска, удаляются, поэтому устаревшие совпадения не накапливаются.

  39. Карточки Junior/Middle/Senior повторяют актуальные KPI главного зарплатного графика. Для каждого доступного уровня берутся та же последняя ненулевая точка скользящего 30-дневного среднего, тот же n и тот же scope; сумма показывается в тех же округлённых тысячах рублей. Исследовательский ориентир используется только там, где у главного графика нет устойчивой точки. Отдельная строка «Вилки „Работы России“» удалена, потому что она создавала второе конкурирующее представление той же выборки. Допустимый scope после решения 161 — только profession.

  40. Главная зарплата карточки профессии переходит на точные gross-вилки HH. И график, и Junior/Middle/Senior используют только query-match самой профессии, полную вилку from+to, currency=RUR, gross=true, rolling window 30 дней и n≥5 на точку; category/market fallback запрещён. Открытое исследование остаётся только явно подписанным статичным резервом отсутствующего уровня. Бесплатный API физически ограничивает массив 30 днями, Premium — 180 уже накопленными днями; UI не синтезирует прошлое до начала HH-наблюдений. «Работа России» остаётся отдельным provenance-слоем публикаций и больше не конкурирует с HH в основной зарплатной визуализации.

  41. Главная метафора пути реализуется кодом, а не тяжёлым циклическим видео. Детерминированные Canvas-частицы собираются в человеческий силуэт и реагируют на указатель, SVG-лента Мёбиуса связывает Junior/Middle/Senior с пульсирующим офером. DPR и число частиц ограничены, mobile получает более лёгкую вертикальную композицию, prefers-reduced-motion — статичный кадр. Цвета вычисляются из текущей светлой/тёмной темы; переключение темы не требует отдельного медиафайла и не создаёт чёрной или белой подложки.

  42. Новый query-match снимок HH переключается только после полностью успешного поиска. Во время 270 запросов пары vacancy/profession и их алиасы накапливаются в памяти процесса, а прежний last_run_id остаётся доступным публичному API. Массовое обновление связей выполняется одной финальной транзакцией только при нуле query/AI errors; partial run не делает карточки пустыми и не публикует смесь старых и ещё не законченных профессий. Устаревшие связи остаются с прежним run id и не входят в новый снимок.

  43. 180-дневная Premium-история HH накапливается, а не выдумывается из текущей выдачи. Ежедневная materialization пересчитывает доступное поисковое окно, сохраняет уже закрытые более ранние дни и удаляет только записи старше 180 дней. Salary midpoint проходит тот же ролевой нижний порог, полную gross RUB-вилку и n≥5; публичный главный график читает максимум последние 30 дней накопленного точного ряда, Premium — максимум 180. Если интеграция работает меньше 180 дней, API возвращает только фактически накопленный период.

  44. Ночной HH-конвейер завершает публикацию пересчётом всех оценок. После успешных поиска, detail-enrichment, materialization и построения 180-дневных окон Dagster пересчитывает v1.2.0 для всех 50 профессий. Неуспешный или частичный этап не запускает публикацию новых score, поэтому карточки не смешивают новый спрос со старой оценкой.

  45. Машинный каталог не строит невидимую историю зарплат повторно. /open-data/publications запрашивает только поля своего компактного контракта, а тяжёлые salary-history ряды остаются detail endpoint. Валидированный JSON каталога кешируется в Redis с календарной датой и порогом выборки в ключе: холодный расчёт остаётся воспроизводимым, повторные AI/health запросы не сканируют весь HH snapshot.

  46. Три карточки грейдов выбирают одну согласованную дату главного HH-графика. Решение 160 уточнено: берётся самая свежая дата, где у всех Junior/Middle/Senior есть собственная точка профессии с n≥5, Middle не ниже Junior × 1,4, а Senior не ниже Middle × 1,3. Последние точки разных дат не смешиваются; category/market scope не допускается. Если такого среза ещё нет, исходный график остаётся без коррекции, а карточки честно используют подписанный комплект открытого исследования — значения HH не переставляются и не дорисовываются.

  47. Главный карьерный образ использует отдельные адаптивные художественные кадры и настоящий интерфейс поверх. Решение 162 заменено по выбранному визуальному направлению: световой Möbius-маршрут из листов данных имеет широкую и вертикальную композиции, а заголовок, поиск, источник и статус офера остаются доступным HTML. На мобильном загружается отдельный портретный кадр, обе темы используют собственные CSS-свет/затемнение, движение ограничено лёгким transform, а prefers-reduced-motion останавливает его. Сгенерированное изображение не содержит текста, логотипов или UI и не является источником данных.

  48. Зарплатные Junior/Middle/Senior — исчерпывающие ранговые сегменты всей совместимой HH-выборки. Решения 161 и 167 заменены: полные и односторонние вилки поддерживаемых валют после перевода по сохранённому курсу ЦБ, а gross и математически нормализованный net участвуют в одной сопоставимой gross RUB-модели. Размеры трёх сегментов выводятся из собственных требований роли к опыту со слабым prior 15/45/40 и минимумом пять записей при общем n≥15; отсортированные значения делятся без пропусков, поэтому сумма трёх n равна полной выборке и шкала монотонна. Это расчётные рыночные сегменты, а не заявленные работодателями грейды; unknown tax basis исключается.

  49. Десять оставшихся малоданных карточек освобождают места массовым IT/digital-профессиям. Android, iOS, Flutter и React Native объединяются в Mobile Developer; SDET — в QA Automation, SRE и Platform Engineer — в DevOps, MLOps — в Machine Learning Engineer, Penetration Tester — в Information Security, Unity — в Game Developer. Освободившиеся позиции занимают администратор информационных систем, менеджер IT-продаж, специалист по автоматизации, специалист по внедрению, веб-дизайнер, SEO-специалист, интернет-маркетолог, оператор баз данных и менеджер по развитию. Старые URL перенаправляются на семантически ближайшую сохранённую роль.

  50. Статичный художественный кадр не является главным карьерным эффектом. Решение 168 заменено реализацией из решения 162: Canvas в реальном времени собирает человеческий силуэт из сотен частиц, реагирует на указатель, а SVG/CSS ведут поток данных через три уровня к пульсирующему оферу. Композиция, плотность частиц и DPR адаптированы под mobile и обе темы; offscreen-анимация останавливается, reduced-motion получает готовый статичный кадр без загрузки растрового фона.

  51. Публичная динамика использует ограниченную нормализованную шкалу. Для спроса и зарплаты по-прежнему сравниваются средние двух соседних окон, но разность делится на большее из двух средних, а итог ограничивается диапазоном от −100% до +100%. Это устраняет значения в тысячи процентов при маленьком предыдущем окне, сохраняет направление изменения и единый порог ±3% во всех карточках, рейтингах, ответах и расчётах индекса. Кэш карточки профессии получает новую версию схемы, поэтому старые проценты не переживают релиз.

  52. Планета и зарплатные уровни используют разные, явно проверяемые desktop/mobile-композиции. На экранах от 768 px три флага закреплены на соседних плитах на один шаг левее, а мобильные якоря не меняются. На телефоне Junior, Middle и Senior одновременно помещаются в компактную трёхколоночную строку вместо скрытого горизонтального продолжения. WebGL-сцена начинает загружаться в первом кадре после гидратации без искусственной паузы, а плитки получают тематическое emissive-свечение без снижения геометрической детализации, DPR или качества теней.

  53. Слово «Офер» является гравировкой ядра, а не отдельной золотой надписью. В обеих темах текст использует тёмную бордово-обсидиановую заливку, вдавленный контур, противоположные тень и блик и лёгкий наклон по поверхности; отдельный фон, рамка и внешний золотой glow отсутствуют. HTML-слой сохраняет читаемость и доступность, а WebGL-геометрия, DPR и скорость загрузки не усложняются.

  54. Вся карточка популярной профессии является одной ссылкой. Заголовок больше не создаёт растянутую псевдоссылку поверх карточки: один доступный Link оборачивает карточку целиком, поэтому область стрелки, сенсорное нажатие и клавиатурный focus ведут на одну и ту же профессию без зависимости от hover-анимации.