Skip to content

Latest commit

 

History

History
646 lines (506 loc) · 78.9 KB

ARCHITECTURE.md

File metadata and controls

646 lines (506 loc) · 78.9 KB

pal-norms: архитектура

Срез 11.10.2026. Репо pal-norms, префикс pn, порт 8701, на Pages /pal-norms/. Название для людей «Нормы и сроки», иконка scale

Основа: ../PLAN.md (D1-D10), ../PLATFORM.md (стандарт), ../UX_MAP.md (адреса, тексты, переходы), итоговый план проекта (идеи 5 и 7). Здесь только решения поверх них. Где этот файл молчит, действует PLATFORM.md

Уже лежит в репо (пишет ARCH, дальше владеет DATA):

  • data/schema/*.schema.json: 8 схем публикуемых датасетов. Это контракт между DATA и SITE
  • tests/fixtures/*.json: синтетические конверты по этим схемам, все id с префиксом demo-. На них SITE строит сайт, пока нет настоящих данных

0. Коротко

  • Сайт открывается от задачи, а не от номера акта: «Посчитать срок», «Если не ответили», «Показания», «Все документы». Номер акта везде мелкой строкой под человеческим названием
  • Факты (реквизиты, eoNumber, статус по тексту отмены, дословные цитаты) живут без юриста. Толкования (с какого события считать, кто обязан, куда жаловаться) публикуются только с пометкой «Толкование не проверено юристом в РФ»
  • Два уровня проверки вместо одного: verified значит «текст сверен со сканом официальной публикации», legal_review значит «толкование проверил юрист». Иначе fail closed парализует потребителей: юриста в РФ пока нет
  • Наблюдатель сам коммитит только служебный снимок data/snapshots/. Всё, что видят семьи, приходит через PR или issue
  • Календарь рабочих дней ведём сами из постановлений о переносе выходных с eoNumber (PLATFORM, добавка 2). prodcalendar только сверяет 2025-2026 в тестах
  • Своего OCR-корпуса не строим (решение итогового плана): полные тексты OCR лежат только в raw/cache/ (в .gitignore), в репо попадают цитаты, вырезки скана под ними и sha256 PDF

Поправки к брифу и к предложениям архитекторов

Что Как на самом деле Решение
«Приложение 2 к 208н/243н = показания детям» В OCR скана 0001202505290031 приложение 2 описывает кабинет паллиативной помощи. Показания детям даны в п. 5 Положения (стр. 4 скана). У взрослых: п. 4 Положения (стр. 3-4) и приложение 1 (стр. 10-13) indications: взрослые = п. 4 + прил. 1, дети = п. 5. Кто выдаёт заключение: п. 12, 13, 15 (стр. 6)
Фильтр «2 и более общих» как отбор ПП 1684 и идея 7 итогового плана: без чекбоксов, баллов и вывода «положено» Правило п. 2 крупно сверху, текст групп в <details>. Выбор группы только прячет остальные группы, ничего не считает и не попадает в URL
Карточка «Кому положена паллиативная помощь» Слово «положено» запрещено (идея 7 итогового плана) «Показания к паллиативной помощи» и подпись «Дословный текст приказа»
«До 2011 года актов на портале нет» 502н (2012) нет, 187н (2015) есть Источник текста записываем по факту поиска в API, а не по году
changes.yaml и OCR коммитятся сами PLATFORM 6.3 п. 4: что видят семьи, само не коммитится Сам коммитится только data/snapshots/watch-state.json. Записи ленты приходят PR
Опрос регионов (Block=subjects) Против D8 и рамок наблюдателя Только чтение блоков разрешённых регионов: названия и сканы официальных публикаций о нерабочих праздничных днях (pipeline/regional.py, data/calendar/regional.yaml). Блоки region80, region81, region84, region85, region91, region92 и их публикации не запрашиваются никогда, клиент отказывает на уровне кода (pipeline/pravo.py, тест с поддельной сетью). Исключённых территорий в данных pal-norms нет и быть не может
OCR первых страниц каждого нового акта Правительства Это сотни сканов в месяц и тот же корпус Только заголовки и поиск от ДД.ММ.ГГГГ № N по актам из acts.yaml
/check/ как главный вход для семей Это расширение C03, у ЧекДок есть платный аналог Вторичный вход «Для НКО и координаторов». Находка формулируется как факт с основанием, а не как «замените на»
/indications/ публично до ревью Идея 7 итогового плана: до ревью медика и юриста публикуется только датасет Локально страница полная. На Pages страница-заглушка со ссылкой на датасет, пока content/gates.yaml не разрешит (раздел 2.4). Строка в «Решить Кириллу»

1. MVP и позже

1.1 MVP (один прогон)

  • Акты. Не меньше 15 записей с реквизитами из API и verified:true, всего до 30. Список кандидатов в разделе 3.3
  • Цитаты. Только пункты, на которые опираются статусы, сроки, показания, ссылки на отменённые акты и правила проверки текста. Примерно 40-70 цитат, каждая с вырезкой скана
  • Сроки. 8-12 правил. В данные идёт только правило с дословной цитатой, где стоит число и единица. Правило без найденной цитаты остаётся в data/candidates.yaml и не публикуется
  • Эскалация. 3 ступени: «срок прошёл, ответа нет», «жалобу переслали тем, на кого жаловались», «есть угроза жизни: подать параллельно». Все с legal_review: none
  • Показания. Взрослые (п. 4, прил. 1 целиком) и дети (п. 5), плюс п. 2, «перечень не является исчерпывающим», кто выдаёт заключение (п. 12, 13, 15)
  • Календарь. Базовые праздники по ст. 112 ТК и переносы на 2025, 2026, 2027 годы из ПП 1335, 1466, 1187
  • Проверка текста. 6 правил: 345н/372н, 187н и 193н → 208н/243н; 348н → 398н; 502н → 180н (если в 180н найдена отмена); ПФР и ФСС → СФР; «30 рабочих» рядом с «18 лет» → п. 23. Один эталонный корпус для lib/textcheck.js и tools/pack_lint.py
  • Лента изменений. Только реальные события с eoNumber, все в состоянии accepted: ПП 1187 о переносах на 2027 год, вступление 208н/243н, 398н, 180н, публикация 692н и другие найденные по API
  • Наблюдатель. Код, тесты на записанных ответах API, watch.yml. В CI в этом цикле не запускается (ничего не пушим)
  • Сайт. 13 видов страниц из раздела 2, калькулятор срока, проверка текста, печать A4, работа без JS, светлая и тёмная темы

1.2 Позже

  • Наблюдатель regulation.gov.ru (проекты) и apicr (клинические рекомендации старше 3 лет)
  • review-sheet (печатный лист для юриста: цитата, вырезка скана, вопросы «верно / неверно / не знаю») и review-ingest
  • Atom-лента /changes/changes.xml
  • Релизы SQLite и Zenodo с DOI
  • Таблица бланков рецептов и сроков (C31), реестр законов о возмещении (C14), домены bereavement, social, links.yaml
  • Правила проверки текста, которые сами собираются из aliases и superseded_by (в MVP правила явные, а тест следит, чтобы у каждого отменённого акта с aliases было правило)
  • Региональный слой: только PR от физлиц со ссылкой на официальную публикацию, после решения мейнтейнера в РФ

2. Страницы и маршруты

2.1 Карта

Пути от /pal-norms/. Шапка, подвал, op-now-ссылка, «Вид» и крошки из платформы

Адрес H1 Источник данных Черновик Печать Без JS
`` Нормы и сроки entrypoints, счётчики из всех датасетов нет нет всё
deadlines/ Посчитать срок deadline-rules, calendar, quotes legal результат на 1 лист таблица правил
deadlines/<rule_id>/ situation правила то же legal 1 лист всё, кроме расчёта
escalation/ Если не ответили или ответили не по существу escalation, quotes, acts legal да всё
indications/ Показания к паллиативной помощи indications both да все группы раскрыты
acts/ Документы acts нет нет полный список
acts/<norm_id>/ short_title акта acts, quotes, deadline-rules нет, у толкований статус да всё
calendar/ Праздники и выходные calendar нет нет список на год сборки
calendar/<год>/ Нерабочие дни в 2027 году calendar, quotes нет 1 лист всё
changes/ Что изменилось changes, watch-state.json нет нет всё
check/ Проверить памятку или шаблон lint-rules, acts, quotes legal нет текст «нужен JavaScript» и список правил
data/ Данные для разработчиков index.json нет нет всё
404.html из платформы

site.nav (UX_MAP 5.2): Сроки (deadlines/), Если не ответили (escalation/), Показания (indications/), Документы (acts/)

2.2 Что на каждой странице

Корень /pal-norms/

  • H1 «Нормы и сроки», строка из UX_MAP 7: «Дословные цитаты из официальных публикаций, сроки ответа и куда обращаться дальше. У каждой цитаты ссылка на публикацию и дата сверки»
  • Четыре крупные карточки op-card с иконкой: «Посчитать срок ответа» (calendar-clock), «Если не ответили» (file-text), «Показания к паллиативной помощи» (book-open), «Все документы» (scale). Подпись у каждой одной строкой
  • Ниже H2 «Для НКО и координаторов»: «Проверить памятку или шаблон» (file-check), «Что изменилось» (history), «Праздники и выходные» (calendar-days), «Данные для разработчиков» (code-xml)
  • op-data-date и строка «Последняя проверка новых публикаций: 12.10.2026» ниже сгиба
  • Первый экран 360×640: шапка в две строки, H1, одна фраза, карточки 1-3 целиком, четвёртая начинается

deadlines/: калькулятор <pn-deadline>

  • Шаг 1 «Что произошло?»: fieldset с крупными радиокнопками от 56px, сгруппированными подзаголовками: «Обращения и жалобы», «Медицинские документы», «Медицинские изделия», «Ребёнку скоро 18». Текст радио = situation правила. Не select
  • Шаг 2: подпись = from_event_label правила («Когда обращение получили», «Дата, когда исполнится 18»), <input type="date">. Если браузер не знает type=date, три поля «день, месяц, год». Ошибка под полем с иконкой и словом «Ошибка»
  • Кнопка «Посчитать» (op-btn--primary)
  • Результат в <section aria-live="polite">:
    • result_label и дата крупно, Onest 700, 28px: «Ответ должен прийти не позже» / «14 ноября 2026, суббота»
    • если ambiguous: статус info «Важно. Ориентировочно» и ambiguity_note. Если due_alt есть, вторая дата: «Если срок переносится с выходного: 16 ноября 2026, понедельник»
    • если calendar_verified:false: статус unverified с текстом UX_MAP «Календарь на [год] ещё не утверждён. Дата может сдвинуться на несколько дней». Для обратного счёта ещё «Мы взяли запас 5 рабочих дней»
    • если правило verified:false: статус unverified «Текст нормы не сверен с официальной публикацией» и откуда он взят
    • всегда строка «Толкование не проверено юристом в РФ», пока legal_review.status не ok
    • «Как считали»: шаги steps с датами списком
    • <details> «Что сказано в норме»: op-quote с цитатами правила
    • кнопки «Добавить в календарь» (.ics через ui/js/ics.js, событие в день срока, напоминание за 1 день, в тексте только «Срок: ...» без имён), «Отправить» (op-share, только текст: срок, дата, как посчитано; без адреса с датой), «Распечатать или сохранить PDF»
    • если срок уже прошёл: статус deadline «Срок. Ответ не пришёл в срок. Это повод для следующего шага» и ссылки «Пожаловаться на нарушение срока» → ok /case/#new=1&s=no-answer&rule=<id>&from=<дата> (UX_MAP 4.3) и «Куда обращаться дальше» → escalation/
    • ссылка «Откуда этот срок» → deadlines/<rule_id>/
  • Ниже калькулятора H2 «Все сроки»: таблица op-table (ситуация, срок, с какого события, норма через norm()). Без JS видна только она, форма скрыта до готовности компонента
  • Принимает фрагмент #rule=<id>&from=<дата>&back=<ситуация> через readParams, сверяет с deadline-rules, предзаполняет и стирает фрагмент. Для правила перехода в 18 лет from из адреса не принимается: дата 18-летия почти равна дате рождения
  • Ничего не хранит: ни дату, ни выбор, ни в URL, ни в localStorage
  • Первый экран: H1, плашка черновика компактно, «Шаг 1. Что произошло?», первые 3-4 варианта
  • Печать: только результат, формула, цитата, плашка черновика, .op-sheet-footer. Ровно 1 лист A4

deadlines/<rule_id>/

  • H1 = situation. Блок «Срок» крупно: «30 дней со дня регистрации, 3 дня на регистрацию», кто обязан (who), с какого события
  • Цитаты правила, ambiguity_note, статус проверки
  • Тот же <pn-deadline> с одним правилом (без шага 1)
  • «Если срок нарушен»: ступени из escalation_ids ссылками на escalation/#<id>
  • Якорь страницы принимает #from= из ok (UX_MAP 4.3), кроме правила перехода в 18

escalation/

  • H1 «Если не ответили или ответили не по существу», строка «Что можно сделать дальше и на какую норму опереться»
  • Ступени списком <ol>, каждая op-card с id для якоря: заголовок title («Прошёл срок, а ответа нет»), что сделать (what), куда (addressee_kind словами: «Прокуратура района или города», «Территориальный орган Росздравнадзора», «Центральный аппарат Росздравнадзора», «Орган управления здравоохранением региона», «Руководитель организации»), пометка «Можно одновременно с другими шагами» при parallel
  • В <details> «Основание»: цитаты, norm()
  • Кнопки «Собрать документ» → ok (ok_doc, если есть, иначе страница сценария из content/escalation-links.yaml) и «Посчитать срок» → deadlines/<rule_id>/
  • Статус unverified «Толкование не проверено юристом в РФ» у каждой ступени
  • Конкретных адресов и телефонов нет. Номера региона даёт pal-directory, ссылка «Номера и адреса вашего региона» → pd /
  • Первый экран: H1, строка, плашка черновика, заголовок первой ступени

indications/

  • draft: both, noindex: true
  • H1 «Показания к паллиативной помощи», строка «Дословный текст приказа. Решение принимает врачебная комиссия. Этот текст помогает говорить с врачом»
  • Блок «Главное правило» крупно, op-quote: п. 2 прил. 1 (при 2 и более общих показаниях оцениваются показания по группам) и строка «перечень ... не является исчерпывающим»
  • H2 «Взрослым»: п. 4 Положения (кому оказывается), «Общие показания» списком, «Показания по группам заболеваний» каждой группой в <details open> с group_title в <summary>
  • <pn-indications>: над группами поле select «Показать одну группу» с вариантом «Все группы». Выбор только скрывает остальные <details>, адрес и история не меняются, в хранилище ничего не пишется. Без JS все группы раскрыты
  • H2 «Детям»: п. 5 Положения дословно
  • H2 «Кто решает»: п. 12, 13, 15 дословно
  • «Что дальше»: «Врачебную комиссию не созывают» → ok /scenarios/vk-refused/, «Распечатать лист для разговора с врачом» → hc /conclusion/ (UX_MAP 4.3)
  • Каждая строка: <blockquote data-verbatim>, под группой op-cite с пунктом, страницей скана, eoNumber. Строки с verified:false со статусом «Не проверено»
  • Печать: выбранная группа или все. Плашка черновика на листе
  • Первый экран: H1, плашка черновика, строка, начало блока п. 2

acts/

  • H1 «Документы», строка «Приказы, законы и постановления, на которые опираются сроки и показания. У каждого статус и ссылка на официальную публикацию»
  • Поле поиска op-search без action и без ?q=: фильтрует список на месте по short_title, title, number, aliases
  • Фильтр радиокнопками: «Все», «Действуют», «Утратили силу», «Не проверено»
  • Группы по topics: «Паллиативная помощь», «Обращения и жалобы», «Медицинские изделия», «Врачебная комиссия и документы», «Обезболивание», «МСЭ», «Календарь», «Ведомства»
  • Строка акта: short_title ссылкой, ниже мелко вид, орган, дата и номер из данных; бейдж статуса pn-act-status (иконка и слово: circle-check «Действует», circle-alert «Утратил силу», clock «Ещё не вступил», circle-help «Статус не проверен»); «Заменён: [ссылка]»; бейдж «Не проверено», если verified:false
  • Первый экран: H1, строка, поле поиска, фильтр, первые 2 акта

acts/<norm_id>/

  • Крошки «openpalliative › Нормы и сроки › Документы › [short_title]»
  • H1 short_title, ниже официальное название title
  • Блок статуса: слово и иконка, даты (in_force_from, valid_until как «Действует до 01.09.2031», repealed_at), основание статуса цитатой (status_basis)
  • «Реквизиты» списком <dl>: вид, орган, дата, номер, номер публикации (eoNumber, моноширинно, кнопка op-copy), дата публикации, страниц. Ссылки «Официальная публикация» и «Скан PDF» с иконкой external-link и текстом «откроется сайт publication.pravo.gov.ru»
  • Для text_source: ips_archive: статус unverified «Текста этого акта нет на портале официальной публикации. Цитата взята из ИПС «Законодательство России» по снимку web.archive.org от [дата] и с официальным текстом не сверена»
  • «Ключевые пункты»: цитаты с id="p-23" (поле anchor), чтобы работали ссылки #p-23 из ok и dv
  • «Связи»: «Заменяет», «Заменён», «Изменения» (amended_by с датой и ссылкой на публикацию), «Ссылается на документ, который утратил силу» (cites_repealed, как факт текста, с цитатой; пример: п. 25 Положения 208н/243н указывает на 348н)
  • «Используется в сроках»: rule_ids ссылками
  • «Проверка юристом: не было» или дата и роль из review
  • Первый экран: крошки, H1, официальное название в 2-3 строки, блок статуса

calendar/ и calendar/<год>/

  • calendar/: H1 «Праздники и выходные», строка «Нерабочие дни по Трудовому кодексу и постановлениям Правительства о переносе выходных». Блок «Ближайшие нерабочие дни» (<pn-holidays> считает от сегодняшней даты; без JS список на год сборки). Ссылки на годы со статусом года
  • calendar/<год>/: H1 «Нерабочие дни в 2027 году», по месяцам: даты словами («1-8 января», «23 февраля»), переносы («суббота 4 января перенесена на пятницу 2 мая»), рабочие субботы. Источник: постановление через norm() и цитата. Печать на 1 лист
  • Год без постановления: статус unverified «Календарь на [год] ещё не утверждён»

changes/

  • H1 «Что изменилось», строка «Новые и изменённые документы на портале официальной публикации, которые касаются паллиативной помощи. Запись появляется после проверки»
  • Список по published_at убыванием: дата, вид словами («Новый документ», «Изменения», «Утратил силу», «Календарь на следующий год», «Скоро истекает срок действия», «Вступил в силу»), title, summary, ссылка на публикацию, связанные акты
  • Строка «Последняя проверка новых публикаций: [checked_at]» из data/snapshots/watch-state.json
  • Короткий абзац, как работает проверка: раз в неделю, что ищем, что не трогаем

check/: <pn-check>

  • draft: legal. H1 «Проверить памятку или шаблон», строка «Вставьте текст. Проверка идёт на этом устройстве, текст никуда не отправляется и не сохраняется»
  • textarea 10 строк, autocomplete="off", кнопки «Проверить» и «Очистить» (вторичная)
  • Итог: «Найдено: 3» или статус done «Готово. Ссылок на документы, которые утратили силу, мы не нашли. Мы проверяем только [N] правил, список ниже»
  • Находка: номер строки, фрагмент с <mark>, сообщение-факт («Упомянут приказ, который утратил силу 01.09.2025»), статус error для severity: error, info для warning, «Основание» цитатой из quote_ids, «Действующий документ: [ссылка]» при suggest_act_id
  • Внизу «Словарь от [data_date], правил: 6» и список правил в <details>
  • Текст только в DOM. Ни localStorage, ни IndexedDB, ни URL

data/

  • H1 «Данные для разработчиков». Таблица датасетов из index.json: название, что внутри, записей, непроверенных, версия, ссылки на JSON и схему
  • Как подключить: lib/deadlines.js, lib/textcheck.js, tools/pack_lint.py с короткими примерами
  • Лицензия: данные CC BY 4.0 с атрибуцией «publication.pravo.gov.ru», код MIT. Как предложить правку (PR в YAML, что попадает на ревью)

2.3 Переходы

  • Принимаем из белого списка UX_MAP 4.1: rule, from, back на deadlines/ и deadlines/<id>/; back на всех страницах. Обычные якоря #p-23 на acts/<id>/
  • Отдаём: в ok (/case/#new=1&s=no-answer&rule=..&from=.., /scenarios/vk-refused/, документы по ok_doc), в hc (/conclusion/), в pd (/ и /federal/). Только через xurl()
  • Внизу страниц «Что ещё может пригодиться» по back= или по situations страницы (UX_MAP 5.3)

2.4 Ворота публикации

content/gates.yaml (SITE):

indications_public: false

site_hooks.py читает переменную окружения OP_TARGET. Если она pages и ворота закрыты, indications/ собирается заглушкой: H1 тот же, текст «Страница ждёт проверки медиком и юристом в РФ. Дословный текст есть в датасете и в официальной публикации», ссылки на data/v1/indications.json и публикацию. Локально (OP_TARGET не задан) страница полная. pages.yml выставляет OP_TARGET=pages

2.5 Состояния

Тексты из UX_MAP 6. Свои для pn:

Состояние Где Что видно
Правило без проверенной цитаты калькулятор, страница правила статус «Не проверено», дата считается, но подписана «ориентировочно»
Толкование без ревью сроки, эскалация строка «Толкование не проверено юристом в РФ»
Акт без текста на портале акт, цитаты статус «Не проверено» с источником ИПС и датой снимка
Срок действия акта истекает меньше чем через 120 дней акт статус info «Важно. Срок действия этого приказа истекает [дата]»
Давно не проверялось review.status: expired статус stale «Проверка юристом устарела»

3. Данные

3.1 Файлы data/ (канон, правят люди)

YAML читается BaseLoader, пустое значение = null, типы приводит схема (PLATFORM 4.5). Даты YYYY-MM-DD

Файл Что Публикуется как
meta.yaml version, data_date, примечания поля конвертов
acts.yaml граф актов acts
quotes/<act_id>.yaml дословные цитаты одного акта quotes
deadline-rules.yaml правила сроков deadline-rules
candidates.yaml правила и акты без найденной цитаты или основания: что ищем и где не публикуется
calendar/holidays.yaml базовые праздники ст. 112 ТК, правило переноса ч. 2 ст. 112 calendar (item baseline)
calendar/transfers.yaml по году: постановление, переносы {from, to} calendar (items по годам, off и work вычисляет export)
escalation.yaml ступени escalation
indications.yaml строки показаний indications
lint-rules.yaml правила проверки текста lint-rules
changes.yaml принятые события ленты changes
entrypoints.yaml входы для хаба entrypoints
review/*.yaml ревью толкований: target, role, reviewed_at, expires_after, verdict сливается в review / legal_review
snapshots/watch-state.json служебное состояние наблюдателя не публикуется, читает site_hooks

3.2 Схемы и поля

Точные схемы лежат в data/schema/<name>.schema.json (JSON Schema 2020-12, без $ref, чтобы работало opsite.data.coerce). Ниже смысл полей

acts

  • id: стабильный слаг (208n-243n, 59-fz, pp-1187). Год в конце (505n-2019) только если номер повторяется внутри acts.yaml
  • kind: federal_law | code | government_decree | government_order | ministry_order | joint_order
  • authority, number, doc_date, title (поле name из API), short_title (человеческое название, его выводит norm()), topics[]
  • eoNumber (16 цифр или пусто), publish_date, pages, url (http://publication.pravo.gov.ru/document/<eo>), pdf_url (http://publication.pravo.gov.ru/file/pdf?eoNumber=<eo>), pdf_sha256
  • text_source: official_pdf | ips_archive | none; alt_source для ИПС: nd, snapshot_url, sha256, retrieved_at
  • status: in_force | not_yet_in_force | repealed | draft | unknown; status_basis {act_id, point, quote_id} обязателен у repealed
  • in_force_from, valid_until (срок действия по «гильотине», только с цитатой), repealed_at
  • supersedes[], superseded_by[] (симметричны), amended_by[{eoNumber, doc_date, title}]
  • cites[{act_id, point, quote_id}]: ссылки, видимые в тексте. cites_repealed и rule_ids вычисляет export
  • aliases[]: написания для поиска и проверки текста («345н/372н», «345н»)
  • quote_ids[], review {status, role, reviewed_at, expires_at, ref}, notes
  • общие: verified, verified_at, source_ref

quotes: id (208n-243n-p23), act_id, point («п. 23», «прил. 1, п. 2», «ч. 1 ст. 12»), anchor (p-23, p-a1-2, p-12-1), page (страница PDF), text дословно, text_source, ocr {passes, diff_lines, dpi[]}, scan_checked {at, method, crop}

deadline-rules: situation, result_label, from_event (код) и from_event_label, who, value, unit (calendar_days | working_days | within_24h), direction (forward | backward), pre {value, unit, label} (3 дня на регистрацию), extension (продление по ч. 2 ст. 12 59-ФЗ), shift_if_nonworking (yes | no | unknown), margin_working_days (запас для обратного счёта, 5 у п. 23), norm_ids[], quote_ids[] (минимум одна), ambiguous, ambiguity_note, escalation_ids[], audience[], legal_review

calendar: item baseline (holidays_mmdd[], carry_over, carry_exclude_mmdd[] для январских, quote_ids) и item на год (year, decree_act_id, decree_eoNumber, transfers[], off[] все нерабочие дни кроме обычных суббот и воскресений, work[] рабочие субботы и воскресенья, quote_ids). Год verified только если проверены постановление, его цитата и базовые праздники

escalation: order, trigger (no_answer_after_due | redirected_to_accused | answer_not_on_merits | threat_to_life), title, what, addressee_kind (только тип органа), parallel, norm_ids[], quote_ids[], rule_ids[], ok_doc, legal_review

indications: audience (adults | children), section (scope п. 4 и п. 5 Положения, general, rule п. 2, group, decision п. 12, 13, 15), group_id, group_title (дословный заголовок пункта группы), order, act_id, point, anchor, page, text, text_source, ocr, scan_checked

lint-rules: kind (repealed_act | agency | deadline_mismatch), patterns[], flags (i или пусто), context {pattern, window} (находка засчитывается, только если в окне window символов вокруг есть context.pattern), act_id, suggest_act_id, message (факт, без «замените»), severity (error | warning), quote_ids, since

Диалект регулярных выражений общий для JS (флаг u) и Python re: классы символов, альтернативы, квантификаторы, просмотр назад только фиксированной длины. Без \b (в JS он не видит кириллицу) и без \p{..}. Границы слова пишем так: (?<![0-9A-Za-zА-Яа-яЁё]) и (?![0-9A-Za-zА-Яа-яЁё]). Перед проверкой текст нормализуется одинаково: NFC, неразрывный пробел и табуляция в пробел, № с пробелом и без

changes: published_at, detected_at, eoNumber, title (из API), kind (new_act | amendment | repeal | transfer_decree | sunset_soon | in_force), related_act_ids[], summary (факт одной фразой), state: accepted, review_ref (адрес PR или issue)

entrypoints: схема платформы opsite/schema/entrypoints.schema.json. Входы:

id path situations audience printable maturity
deadlines deadlines/ refused, turning-18 family, org true beta
escalation escalation/ refused, conclusion, discharge family, org true beta
indications indications/ conclusion family, medic true demo
calendar calendar/ night-pain family true beta
acts acts/ org, lawyer false beta
check check/ org false demo
changes changes/ org, lawyer false beta
data data/ developer false beta

offline: false у всех. Заголовки и подписи берутся из UX_MAP (например «Посчитать срок ответа»)

3.3 Источники

Все запросы к официальному порталу публикации. Ни сайтов НКО, ни сайтов учреждений. Региональные блоки портала только на чтение и только разрешённые (см. §0), для справки о региональных нерабочих днях

Что Адрес
метаданные акта http://publication.pravo.gov.ru/api/Document?eoNumber=<eo>
поиск http://publication.pravo.gov.ru/api/Documents?PageSize=30&Index=1&Name=<слова>&Block=<блок>, ещё Number=, SignatoryAuthorityId=, PublishDateFrom=, PublishDateTo=
блоки president (в том числе федеральные законы), government, federal_authorities. subjects не используем
скан http://publication.pravo.gov.ru/file/pdf?eoNumber=<eo> (без текстового слоя, /api/DocumentText отдаёт false)
страница публикации http://publication.pravo.gov.ru/document/<eo>
запасной канал http://publication.pravo.gov.ru/api/Rss?Block=federal_authorities
описание API http://publication.pravo.gov.ru/swagger/v1/swagger.json
ИПС через архив https://web.archive.org/cdx/search/cdx?url=pravo.gov.ru/proxy/ips/*&filter=urlkey:.*nd=<nd>.*&output=json, затем снимок https://web.archive.org/web/<ts>id_/http://pravo.gov.ru/proxy/ips/?doc_itself=&backlink=1&nd=<nd>&page=1&rdk=<rdk>, кодировка cp1251. Для 59-ФЗ: nd=102106413, снимок 20260626091943 (отвечает 200, 67 КБ, проверено 11.10.2026)
перекрёстная проверка календаря npm prodcalendar@0.1.2, только в тестах

Подводные камни API (проверено из AT 11.10.2026): PageSize только 10, 30, 100, 200, иначе пустой ответ без ошибки. Name ищет по complexName целыми словами: «паллиатив» даёт 0, «паллиативной» 303 (федеральных 14). Номера повторяются: «100н» 32 совпадения, «345н» 6 разных актов, поэтому акт находим по тройке «номер, дата, орган», а храним по eoNumber. Номер, дату и орган берём из метаданных API, не из OCR (шапка 208н распознаётся мусором). При параллельных запросах сервер отдаёт пустые тела и таймауты: строго последовательно

Кандидаты в acts.yaml. eoNumber с пометкой «API» сверены запросом /api/Document 11.10.2026 в этой архитектуре, остальные сверили архитекторы в тот же день. DATA перепроверяет каждый

id Акт eoNumber Источник текста
208n-243n Положение о паллиативной помощи (Минздрав и Минтруд) 0001202505290031 (196 стр.) official_pdf
345n-372n прежнее Положение от 31.05.2019, отменено 208н/243н 0001201906270031 API (108 стр.) official_pdf
187n, 193n паллиативная помощь взрослым и детям, 2015 0001201505120026, 0001201505150003 official_pdf
398n перечень медизделий для дома от 09.07.2025 0001202508130021 API official_pdf
348n прежний перечень от 31.05.2019 0001201907010015 API official_pdf
505n передача медизделий пациенту от 10.07.2019 0001201909250013 API official_pdf
180n врачебная комиссия от 10.04.2025 0001202505160009 API official_pdf
502n прежний порядок ВК, 2012 нет на портале none, статус по цитате 180н
789n копии медицинских документов от 31.07.2020 0001202009240027 API official_pdf
100n правила отпуска лекарств от 07.03.2025 0001202504100006 API official_pdf, только процедурные пункты
1093n прежние правила отпуска, 2021 искать статус только по цитате 100н
1094n назначение лекарств и бланки от 24.11.2021 0001202111300115 API official_pdf, только процедурные пункты
692n МСЭ от 09.12.2025 0001202604140027 API official_pdf
pp-588 признание лица инвалидом 0001202204080035 official_pdf
pp-2188 программа госгарантий 0001202512300036 official_pdf
1583-r распоряжение от 23.06.2026, тема по метаданным 0001202606260118 official_pdf
236-fz О Фонде пенсионного и социального страхования 0001202207140012 API official_pdf, основание правила ПФР/ФСС → СФР
59-fz о порядке рассмотрения обращений граждан, 2006 нет на портале ips_archive, verified:false
547-fz, 480-fz, 355-fz поправки к 59-ФЗ 0001202412280052, 0001202308040085, 0001201711270077 official_pdf, только amended_by у 59-fz
koap КоАП, ст. 5.59 нет на портале ips_archive, verified:false
35-fz поправки в ТК 2012 (ст. 112) 0001201204240009 API (4 стр.) official_pdf, основание базовых праздников
pp-240 ТСР, 2008 нет на портале ips_archive или в candidates.yaml
pp-1335, pp-1466, pp-1187 переносы выходных на 2025, 2026, 2027 0001202410040023, 0001202509240023, 0001202609180037 official_pdf

Не вносим без eoNumber и цитаты-основания: 356н, «срок действия до 01.09.2031» у 180н (пока не найден пункт), 23н, ПП 2351

Кандидаты правил сроков (id, что ищем в тексте). Значения в данных только из найденной цитаты

id Что Где искать
59fz-answer ответ на письменное обращение, регистрация + 30 дней, продление ч. 2 ст. 8, ч. 1-2 ст. 12 59-ФЗ (ИПС)
59fz-redirect перенаправление по компетенции, 7 дней ч. 3 ст. 8 59-ФЗ
208n-p23-transition передача сведений о ребёнке во взрослую сеть, не менее чем за 90 рабочих дней до 18 лет, обратный счёт п. 23, стр. 8 скана
505n-vk-decision, 505n-transfer решение ВК о передаче изделия, передача по договору 505н
180n-protocol-extract выписка из протокола ВК по запросу 180н п. 15-16
789n-copies, 789n-inpatient копии и выписки, в стационаре в суточный срок 789н
100n-deferred отсроченное обслуживание рецепта 100н, только сроки
692n-mse, pp-588-mse сроки МСЭ 692н, ПП 588
pp-240-tsr сроки по ТСР ПП 240 (ИПС) или candidates.yaml

3.4 Парсинг (pipeline/)

  • pravo.py: клиент API. urllib.request, User-Agent: openpalliative-watch (+https://github.com/openpalliative), один запрос за раз, пауза 2 с между запросами, таймаут 30 с, 4 попытки с паузами 2, 4, 8, 16 с. PageSize проверяется по списку 10, 30, 100, 200. Разбор ответа в dataclass, неизвестная форма ответа = исключение SourceFormatError
  • fetch_pravo.py: uv run python -m pipeline.fetch_pravo <act_id>|--all. Метаданные в raw/meta/<eo>.json (коммитятся, около 2 КБ), PDF в raw/cache/<eo>.pdf (не коммитится), строка в raw/SOURCES.tsv: eoNumber, url, sha256, bytes, retrieved_at
  • ocr.py: только нужные страницы. Проход A: pdftoppm -r 300 -gray и tesseract -l rus --psm 4. Проход B: -r 400 и --psm 6. Нормализация для сравнения (NFC, пробелы, перенос слова через дефис в конце строки). Разница строк в raw/ocr-diff/<quote_id>.diff. Вырезка скана под цитатой по координатам из tesseract ... tsv в raw/crops/<quote_id>.png (оттенки серого, до 150 КБ). Полный OCR страниц только в raw/cache/ocr/
  • quotes.py: uv run python -m pipeline.quotes add <act_id> <page> "<начало>" "<конец>" вырезает фрагмент между маркерами из прохода A, пишет черновик цитаты в data/quotes/<act_id>.yaml с verified:false, делает вырезку и diff
  • fetch_ips.py: CDX web.archive по nd, последний снимок с id_, cp1251, вырезка статьи по заголовку, sha256 снимка в alt_source. Всегда verified:false
  • calendar.py: из holidays.yaml и transfers.yaml строит off и work по годам. Правило ч. 2 ст. 112: праздник в субботу или воскресенье переносится на следующий рабочий день, кроме январских, которые переносит постановление
  • export.py: datasets(root) -> list[dict]. Собирает конверты через opsite.data.make_envelope, считает cites_repealed, rule_ids, off/work, сливает review/. С PN_FIXTURES=1 возвращает tests/fixtures/*.json как есть (для SITE до данных и для e2e на синтетике)
  • watch.py: раздел 5

Что уже есть локально во временной папке сборки $OP_SCRATCH (путь даёт ведущий, в репо его не пишем; это не репо, нужное переносить в raw/ и pipeline/tests/fixtures/)

  • pn/208n.pdf (8,8 МБ, 196 стр.) и pn/p208/ (PNG 171 страницы), o208/p-001.txt ... p-171.txt (один проход tesseract). Ориентиры: п. 4 стр. 3-4, п. 5 стр. 4, п. 12-15 стр. 6, п. 23 и п. 25 (ссылка на 348н) стр. 8, прил. 1 стр. 10-13, прил. 2 (кабинет) с стр. 14. Страницы 172-196 не отрендерены
  • dev/208ocr.txt (стр. 1-44), pn/n398.pdf и pn/n398.txt
  • pnarch/: ответы API для фикстур наблюдателя: pal.json (Name=паллиативной, 303), tr.json (14 постановлений о переносах, 2014-2027), q.json, m.json, f.json (пустой ответ), rss.xml, swagger.json, od30.csv
  • pn/swagger.json

3.5 Проверка и пометки

Уровень Что значит Кто ставит Условие
цитата verified текст совпадает со сканом официальной публикации DATA при подготовке, позже мейнтейнер text_source: official_pdf, два прохода OCR, расхождения разобраны по вырезке, scan_checked записан, вырезка лежит в raw/crops/
акт verified реквизиты совпадают с API, статус подтверждён DATA метаданные из API, поиск от ДД.ММ.ГГГГ № N выполнен в дату verified_at, у repealed проверенная цитата-основание. Акты ips_archive и none (кроме отменённых по цитате другого акта) всегда false
правило срока verified число и единица стоят в проверенной цитате DATA все quote_ids проверены, value встречается в тексте цитаты (pytest), нормы in_force
год календаря verified постановление и праздники сверены DATA цитаты ПП о переносах и 35-ФЗ проверены, совпадение с prodcalendar за 2025-2026
строка показаний verified как цитата DATA как цитата
legal_review.status: ok толкование проверил юрист в РФ только мейнтейнер через review/*.yaml и CODEOWNERS в MVP везде none

Чего нельзя: verified:true у цитаты из ИПС, у акта без eoNumber (кроме отменённого по проверенной цитате), у правила без цитаты. Если расхождение OCR не удаётся разобрать по вырезке, цитата остаётся verified:false. Медицинские пороги из приказа (например, «потеря массы тела более чем на 10%») показываются только дословно в <blockquote data-verbatim>, без пересказа. Из 100н и 1094н не берём перечни препаратов и бланков, только сроки и порядок

4. Снимки для соседей

4.1 Публикуем (_site/data/v1/)

Конверт PLATFORM 4.1, лицензия CC-BY-4.0, source с eoNumber. stale_after_days: 90

Датасет Кто берёт Зачем
acts все четыре инструмента norm(), ссылки acts/<id>/#p-N, fail closed
quotes ok, hc op-quote в документах и на листах
deadline-rules ok, dv сроки дела, ссылка «Сколько ждать»
calendar ok, hc рабочие дни, праздники на карте на ночь
escalation ok следующий шаг после срока
indications hc лист /conclusion/
lint-rules ok pack_lint.py при сборке пакета
changes хаб (позже)
entrypoints хаб ситуации и /tools/

Код: lib/deadlines.js, lib/textcheck.js (в _site/lib/), tools/pack_lint.py (в _site/tools/, только стандартная библиотека Python)

Строки для deps.yaml потребителей (их правят сами потребители):

  • ok: datasets: [acts, quotes, deadline-rules, calendar, escalation, lint-rules], lib: [deadlines.js, textcheck.js], tools: [pack_lint.py]
  • hc: datasets: [acts, quotes, calendar, indications, deadline-rules], lib: [deadlines.js]
  • dv: datasets: [acts, deadline-rules]

Версии: правка текста цитаты или записи = новый version (CalVer). Новое необязательное поле = минор schema_version. Переименование или удаление поля = v2/, v1/ живёт ещё 12 месяцев

4.2 Берём

Данных соседей не берём, pal-norms первый в графе. Из хаба через scripts/sync-ui: opsite/ (сборщик, UI, situations.json для «Вернуться»)

4.3 Интерфейсы кода

// lib/deadlines.js, без DOM и сети
computeDeadline({ rule, start, calendar })
// rule: элемент deadline-rules; start: "YYYY-MM-DD"; calendar: конверт calendar
// -> { rule_id, due, due_alt, latest, formula, steps: [{ label, date }],
//      norm_ids, ambiguous, verified, calendar_verified, margin_applied }
isWorkingDay(iso, calendar)            // -> { working, verified }
addWorkingDays(iso, n, calendar)       // n может быть отрицательным
nonWorkingDays(fromIso, toIso, calendar) // -> [{ date, kind: "weekend" | "holiday" | "transfer" }]
calendarYears(calendar)                // -> { 2027: { verified } }
  • Календарные дни считаются со следующего дня после события. pre прибавляется первым. within_24h даёт следующий календарный день
  • shift_if_nonworking: yes переносит срок на ближайший рабочий день, unknown оставляет due без переноса и кладёт перенесённую дату в due_alt, latest = более поздняя из двух
  • Обратный счёт: due = рабочий день, до которого от него до события (не включая день события) ровно value рабочих дней. Если хоть один год в диапазоне не verified, вычитается ещё margin_working_days, margin_applied > 0
  • Вне известных лет: выходные и праздники baseline с переносом по ч. 2 ст. 112, calendar_verified:false
  • formula: строка по-русски («30 дней со дня регистрации, 3 дня на регистрацию»), без номеров актов
// lib/textcheck.js
normalize(text)                       // NFC, пробелы, «№»
checkText(text, rules, { acts })
// -> [{ rule_id, kind, severity, line, col, start, end, fragment, context, message,
//       act_id, suggest_act_id, quote_ids }]
# tools/pack_lint.py
python3 tools/pack_lint.py <файлы или папки> --rules lint-rules.json --acts acts.json [--json]
# выход 1, если есть находка severity: error или norm('id') на неизвестный, не in_force или не verified акт

check_text в Python повторяет checkText построчно. Совпадение двух реализаций держит общий эталон tests/golden/lint/ (раздел 7)

5. Автоматизация

5.1 Файлы .github/workflows/

Файл Когда Права Что
pages.yml push в main, вручную, запуск из watch.yml build contents: read, deploy pages: write, id-token: write шаблон PLATFORM 6.1, плюс env: OP_TARGET: pages
checks.yml PR, push в main, вручную (так его запускает watch.yml на ветке черновика) contents: read шаблон PLATFORM 6.2, плюс actionlint и uv run python -m pipeline.watch --fixtures pipeline/tests/fixtures/pravo
watch.yml cron: "17 4 * * 1" и cron: "17 4 * 9 *" (в сентябре ежедневно, окно постановления о переносах), вручную contents: write, pull-requests: write, issues: write, actions: write раздел 5.2
release.yml тег v* contents: write шаблон PLATFORM 6.5 с OP_TARGET: pages: zip _site, zip data/v1/*.json, те же наборы в SQLite (.github/scripts/datasets_sqlite.py), SHA256SUMS

watch.yml запускается на ${{ fromJSON(vars.WATCH_RUNNER || '"ubuntu-24.04"') }} и на Linux ставит poppler-utils tesseract-ocr tesseract-ocr-rus, если их нет, для шага OCR постановления о переносах

5.2 Наблюдатель pipeline/watch.py

uv run python -m pipeline.watch [--fixtures DIR] [--report _reports/watch.json] [--apply]

  1. Канарейка. GET /api/Document?eoNumber=0001202505290031: есть ключи eoNumber, number, documentDate, pagesCount, name, pagesCount == 196. Documents?PageSize=30&Name=паллиативной даёт itemsTotalCount > 0. Не прошла: ничего не меняем, в отчёт source_format. Сеть не ответила: consecutive_failures += 1
  2. Отслеживаемые акты. Для каждого акта с eoNumber: Name=от ДД.ММ.ГГГГ № N по блоку органа. Найденный eoNumber, которого нет ни в acts.yaml, ни в amended_by, ни в seen, даёт review вида amendment_or_repeal
  3. Тематический поиск. Name по словоформам «паллиативной», «паллиативная», «паллиативную», «паллиативных» в блоках president, government, federal_authorities, PublishDateFrom = last_ok_at - 14 дней. Новое даёт new_act
  4. Переносы. Name=О переносе выходных дней, Block=government. Новый eoNumber даёт transfer_decree: скачать PDF, OCR, черновик года в data/calendar/transfers.yaml с verified:false и запись в data/changes.yaml
  5. Срок действия. valid_until ближе 120 дней даёт sunset_soon
  6. Снимок. data/snapshots/watch-state.json: checked_at, last_ok_at, consecutive_failures, seen {eo: {pagesCount, pdfFileLength, publishDateShort}}, queries {имя: {total, last_eo}}. Изменение pagesCount или pdfFileLength у известного акта даёт review вида republished

С --apply (только в Actions или у мейнтейнера):

  • Коммитится само: только data/snapshots/watch-state.json, сообщение data: снимок pravo.gov.ru <дата>. После коммита gh workflow run pages.yml: пуш токеном Actions сам Pages не запускает. Расписание коммит не держит, поэтому последний шаг с if: always() делает PUT .../actions/workflows/watch.yml/enable
  • PR (peter-evans/create-pull-request, ветка watch/<дата>): черновик года календаря и запись ленты (метка review:data), затем gh workflow run checks.yml --ref watch/<дата>. Мерж PR = событие принято
  • Issue (gh issue create, заголовок [watch:<sha8>] ..., где sha8 от kind + eoNumber; дубли ищутся по заголовку среди открытых и закрытых): new_act, amendment_or_repeal, republished, sunset_soon, source_format, и «Наблюдатель не достучался до pravo.gov.ru» при consecutive_failures >= 3
  • Никогда само: status, superseded_by, цитаты, правила, показания, эскалация, правила проверки текста. Только PR с меткой review:legal и ревью человеком

Запасной путь, если pravo.gov.ru не пускает раннеры GitHub: scripts/watch-local раз в месяц у мейнтейнера (с его компьютера API отвечает), тот же отчёт и те же PR через gh. В README: «6 месяцев без ревьюера - архив», релиз zip для зеркала

data_date в meta.yaml правит человек после сверки записей. Дата последнего прохода наблюдателя показывается отдельно и data_date не обновляет

6. Дерево и владение

6.1 Дерево

pal-norms/
├── ARCHITECTURE.md                ARCH
├── README.md LICENSE LICENSE-DATA DATA (позже INT)
├── site.yaml                      SITE
├── pyproject.toml uv.lock .python-version              DATA
├── package.json pnpm-lock.yaml .node-version            DATA (prodcalendar) + SITE (просит в STATUS.md)
├── playwright.config.js           SITE
├── .gitignore                     DATA: _site/ _reports/ .venv/ node_modules/ raw/cache/
├── data/                          DATA
│   ├── meta.yaml acts.yaml deadline-rules.yaml candidates.yaml escalation.yaml
│   ├── indications.yaml lint-rules.yaml changes.yaml entrypoints.yaml
│   ├── quotes/<act_id>.yaml
│   ├── calendar/holidays.yaml calendar/transfers.yaml
│   ├── review/ snapshots/watch-state.json
│   └── schema/*.schema.json       (написал ARCH)
├── pipeline/                      DATA
│   ├── __init__.py pravo.py fetch_pravo.py fetch_ips.py ocr.py quotes.py calendar.py export.py watch.py
│   └── tests/ test_*.py, fixtures/pravo/*.json (ответы API из pnarch), fixtures/ips/*.html
├── raw/                           DATA: SOURCES.tsv, meta/<eo>.json, crops/*.png, ocr-diff/*.diff; cache/ в .gitignore
├── lib/deadlines.js lib/textcheck.js   DATA
├── tools/pack_lint.py             DATA
├── content/                       SITE: pages/*.md, home.yaml, gates.yaml, escalation-links.yaml, labels.yaml
├── templates/pages/               SITE: index, deadlines, deadline-rule, escalation, indications, acts, act, calendar, calendar-year, changes, check, data (.html.j2)
├── templates/partials/            SITE: pn-act-row, pn-act-status, pn-rule-table, pn-quote-list, pn-step
├── assets/css/pal-norms.css       SITE, классы pn-*
├── assets/js/                     SITE: pn-deadline.js pn-check.js pn-acts.js pn-indications.js pn-holidays.js
├── site_hooks.py                  SITE: страницы актов, правил, лет; карты id → запись; чтение watch-state.json
├── opsite/                        SYNC (scripts/sync-ui)
├── scripts/                       DATA: sync-ui, sync-data, serve-one, gate.sh, watch-local
├── tests/unit/                    DATA: deadlines.test.js, textcheck.test.js, calendar.test.js
├── tests/golden/                  DATA: deadlines.yaml, lint/*.txt + *.expected.json
├── tests/fixtures/*.json          DATA (синтетика, написал ARCH)
├── tests/e2e/                     SITE: pages.json, *.spec.js
└── .github/workflows/             DATA: pages.yml checks.yml watch.yml release.yml

6.2 Правила совместной работы

  • DATA не трогает content/, templates/, assets/, site_hooks.py, site.yaml, tests/e2e/. SITE не трогает остальное
  • Связь только через: схемы data/schema/, конверты (pipeline.export или фикстуры), API lib/*.js из 4.3, поля watch-state.json (checked_at, last_ok_at), файл data/meta.yaml
  • SITE начинает сразу: PN_FIXTURES=1 uv run python -m opsite build. Пока DATA не написал export.py, SITE может временно положить свой pipeline/export.py только с веткой фикстур и сказать об этом в STATUS.md. DATA заменяет его первым делом
  • Изменить схему может только DATA. Если SITE нужно поле, строка в STATUS.md раздел pal-norms, DATA добавляет поле и фикстуру
  • Порядок DATA: 1) sync-ui, pyproject, export.py с фикстурами, lib/deadlines.js на синтетике; 2) акты и метаданные; 3) цитаты 208н/243н (п. 2 прил. 1, п. 4, 5, 12, 13, 15, 23, 25, прил. 1 целиком); 4) календарь и golden-тесты; 5) остальные цитаты и правила; 6) lint-rules, textcheck.js, pack_lint.py, эталонный корпус; 7) наблюдатель и workflows
  • Порядок SITE: 1) site.yaml, корень, acts/, acts/<id>/; 2) deadlines/ и <pn-deadline>; 3) indications/, escalation/; 4) calendar/, changes/, check/, data/; 5) e2e, печать, скриншоты

6.3 Синтетический пример для старта

Полные конверты в tests/fixtures/: 3 акта (действующий, отменённый, закон из ИПС), 4 цитаты, 3 правила (вперёд календарные с неоднозначностью, назад рабочие с запасом, в течение суток), календарь (baseline и 2026 без постановления), 3 ступени, 9 строк показаний, 3 правила проверки, 2 события. Все тексты начинаются со слова «Пример», id с demo-, ссылки пустые или на example.invalid. Фрагмент:

{ "id": "demo-turning-18", "verified": true, "verified_at": "2026-10-11", "source_ref": "demo-source",
  "situation": "Пример: ребёнку скоро 18", "result_label": "Сведения должны передать не позже",
  "from_event": "turns-18", "from_event_label": "Дата, когда исполнится 18",
  "who": "детская медицинская организация", "value": 90, "unit": "working_days", "direction": "backward",
  "pre": null, "extension": null, "shift_if_nonworking": "no", "margin_working_days": 5,
  "norm_ids": ["demo-polozhenie"], "quote_ids": ["demo-polozhenie-p23"], "ambiguous": false,
  "ambiguity_note": null, "escalation_ids": [], "audience": ["family", "org"],
  "legal_review": { "status": "none", "role": null, "reviewed_at": null, "expires_at": null, "ref": null } }

Фикстуры проходят opsite/schema/envelope.schema.json и свои схемы (проверено при записи). Тест сборки: без PN_FIXTURES в _site/data/v1/ нет ни одного id demo-

7. Тесты и приёмка

7.1 Команды (из корня репо)

sh scripts/sync-ui
uv sync --frozen
uv run pytest -q
PN_FIXTURES=1 uv run python -m opsite build && uv run python -m opsite check
uv run python -m opsite build && uv run python -m opsite check
pnpm install --frozen-lockfile
pnpm test:unit
pnpm test:e2e
pnpm shots
sh scripts/gate.sh
uv run python -m pipeline.watch --fixtures pipeline/tests/fixtures/pravo --report _reports/watch.json

7.2 Что проверяют

pytest (DATA)

  • каждый файл data/ проходит свою схему после coerce, все id уникальны, ссылки на акты, цитаты, правила и ступени существуют
  • supersedes и superseded_by симметричны, циклов замен нет
  • у repealed есть status_basis с проверенной цитатой, у in_force пусто repealed_at
  • eoNumber 16 цифр, у official_pdf он есть, url и pdf_url собраны из него
  • verified:true у цитаты только с official_pdf, scan_checked и файлом raw/crops/<id>.png; при diff_lines > 0 есть raw/ocr-diff/<id>.diff
  • у правила срока value встречается в тексте одной из его цитат; ambiguous с ambiguity_note
  • у каждого отменённого акта с aliases есть правило repealed_act
  • нет строк про исключённые территории и названий из opsite/lint/drugs.txt в data/
  • watch на записанных ответах: канарейка, пагинация, пустой ответ при неверном PageSize, новый акт, поправка, постановление о переносах, дубль issue не создаётся, при сбое сети ничего не меняется
  • fetch_ips на сохранённом снимке cp1251
  • tools/pack_lint.py на tests/golden/lint/ даёт ровно *.expected.json

node --test (DATA)

  • tests/golden/deadlines.yaml: не меньше 30 строк «правило, дата события → due, due_alt, formula» на 2025-2027: 31 декабря, январские, майские, 12 июня, 4 ноября, рабочие субботы, обратный счёт п. 23, 59-ФЗ с переносом и без, суточный срок, год вне календаря с запасом
  • calendar.json совпадает с prodcalendar@0.1.2 по каждому дню 2025 и 2026 годов
  • textcheck.js на tests/golden/lint/: синтетическая памятка ловит все 6 правил и не трогает «208н/243н», «398н», «180н», «СФР»; «30 рабочих» без «18 лет» рядом не срабатывает

Playwright (SITE), tests/e2e/pages.json: home, deadlines, deadline-208n-p23-transition, escalation, indications (nojs), acts (nojs), act-208n-243n, act-345n-372n, calendar, calendar-2027 (print_pages: 1), changes, check, data. Плюс сценарии:

  • deadlines.spec.js: выбрать правило, ввести дату, увидеть дату и формулу; неоднозначное правило показывает две даты; прошедший срок даёт ссылку в ok с rule и from; фрагмент #rule=..&from=.. предзаполняет форму и исчезает из адреса; .ics скачивается; печать результата 1 лист A4; на 390px нет горизонтальной прокрутки
  • check.spec.js: вставить синтетическую памятку, 6 находок; сторож сети пуст; localStorage и IndexedDB после проверки пусты
  • indications.spec.js: без JS видны все группы; выбор группы не меняет location.href и history.length; плашка «Черновик» видна; с OP_TARGET=pages и закрытыми воротами страница-заглушка
  • acts.spec.js: фильтр «Утратили силу» оставляет отменённые; acts/208n-243n/#p-23 прокручивает к пункту; ссылки на публикацию ведут на publication.pravo.gov.ru
  • axe без serious и critical, один h1, lang="ru", ноль внешних запросов, ни одной ошибки в консоли

7.3 Приёмка MVP

  1. Все команды 7.1 дают 0, сборка до 15 секунд
  2. В acts не меньше 15 записей verified:true с eoNumber, сверенным через API; в deadline-rules не меньше 6 правил verified:true; 59-ФЗ виден со статусом «Не проверено» и источником ИПС
  3. indications: прил. 1 целиком, п. 4, п. 5, п. 2, п. 12, 13, 15; доля verified:true не меньше 90%, остальные видны со статусом
  4. Календарь 2025-2027 verified:true, совпадает с prodcalendar за 2025-2026
  5. 6 правил проверки текста, JS и Python дают одинаковый результат на эталоне
  6. Наблюдатель проходит тесты на фикстурах, _reports/watch.json по схеме PLATFORM 6.3
  7. Скриншоты desktop и mobile, светлая и тёмная тема, печать просмотрены глазами, найденное исправлено
  8. На http://localhost:8080/pal-norms/ работают ссылки из хаба (/s/refused/, /s/conclusion/, /s/turning-18/, /s/night-pain/) и обратные ссылки в ok, hc, pd
  9. Ни длинного, ни среднего тире вне дословных цитат, ни эмодзи, ни номеров актов литералом в content/ и templates/

8. Риски

Риск Мера
Нет официального текста 59-ФЗ, КоАП, ПП 240, а 59-ФЗ нужен ok каждый день ips_archive с verified:false и честной подписью. Сверка по живой ИПС мейнтейнером в РФ (scan_checked.method: ips-live). Не утверждаем, что 547-ФЗ и 480-ФЗ не меняли ст. 8 и 12, пока не прочитан их текст
OCR ошибается одинаково в двух проходах (н/и, цифры, даты) Вырезка скана под каждой цитатой и сверка глазами, без неё verified:false
Семья прочтёт показания как «вам положено» П. 2 сверху, «решает врачебная комиссия», без выводов, черновик для медика и юриста, ворота на Pages
Дата срока читается как гарантия «Ориентировочно» и две даты у неоднозначных, строка про юриста, обратный счёт с запасом
Ложные находки «30 рабочих» Только при «18 лет» рядом, уровень «Важно», в находке цитата п. 23
API меняет формат или режет раннеры GitHub Канарейка, «при сомнении ничего не менять», RSS как второй канал, WATCH_RUNNER, scripts/watch-local
Нет ревьюера Факты живут сами, толкования помечены, expires_after гасит старые ревью, «6 месяцев без ревьюера - архив»

9. Строки для STATUS.md

В платформу

  • Два уровня проверки: verified (текст сверен со сканом) и legal_review (толкование). norm() уже требует verified и in_force. ok и hc нужно отдельно проверять deadline-rules.verified и показывать «Толкование не проверено юристом в РФ», пока legal_review.status != ok
  • В deps.yaml ok и hc добавить датасет quotes, в ok ещё lint-rules и lib/textcheck.js (раздел 4.1)
  • Бейдж статуса акта (pn-act-status: «Действует», «Утратил силу», «Ещё не вступил», «Статус не проверен») пока локальный в pn. Если понадобится ok и dv, поднять в op-status
  • Иконки circle-x в стартовом наборе нет, pn обходится circle-alert

Решить Кириллу

  • Публиковать ли indications/ на Pages до ревью медика и юриста в РФ (итоговый план по идее 7 говорит «только датасет»). По умолчанию ворота закрыты, датасет публикуется
  • 59-ФЗ, КоАП и ПП 240 есть только в ИПС, а из AT она открывается лишь через снимки web.archive.org. До мейнтейнера в РФ правило ответа на обращение остаётся «Не проверено», и ok в документы его не возьмёт