Andrey Epifancev 10317ae4d3 docs: итоги исполнения единой нормализации ББ/ОФР (#124, #116)
Решения дорожек (планы 3А, 3Б, 4, 5, 6) перенесены в design.md change'а,
добавлен раздел «Открытые вопросы исполнения»; гайд фронта по API приведён
к arm-core main 35a24e7a; уточнена строка про зависимости в dev/checks.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:57:03 +04:00

373 KiB
Raw Blame History

Единый алгоритм нормализации ББ/ОФР (scan-gateway и tabular-parser) — дизайн

Issue: arm-ka-docs#116. Статус: финальная версия — целевая картина по шагам 1–9 согласована с владельцем (решения Р-1…Р-19, 17.10.2026) и прошла три раунда независимого ревью (review-2026-10-17.md, review-2026-10-17-closure.md, review-2026-10-17-blind.md, review-2026-10-17-closure-2.md, review-2026-10-17-blind-2.md). Спека (дельты OpenSpec, контракт v2.0, §7/§9/§11 в 05-domain-rules.md, DBML v5.19) написана 18.10.2026 (решения S-1…S-25), прошла ревью вслепую (review-2026-10-18-spec.md, находки K-1…K-25) и доработана по нему (решения владельца Р-20…Р-22, решения S-26…S-52 ниже), прошла повторное ревью вслепую (review-2026-10-18-spec-2.md, находки L-1…L-30) и доработана по нему (решения владельца Р-23…Р-26, решения S-53…S-74 ниже). Где текст дизайна выше расходится с решениями Р-20…Р-26 и S-n, действуют они и спека.

Цель

Нормализация результата распознавания форм ББ/ОФР в scan-gateway (PDF → ИС Сканирование) и в tabular-parser (Excel) должна идти по одной цепочке шагов. У каждого шага — определённый результат, который передаётся в следующий шаг; результаты между шагами одинаковы для обоих сервисов (где это возможно). Все нестыковки собраны в таблицы различий по каждому шагу.

Зачем: нормализовать каждый шаг; убрать бойлерплейт в arm-core (повторная дедупликация, пересчёт проверок, статус, который ядро не может взять из результата); улучшить поддерживаемость (один словарь — одно место); улучшить пользовательский опыт заказчика (старые ошибки распознавания AURORA-1406, 1462, 1511, 1517, 1519, 1522, 1523, 1524, 1634 — свойство конвейера, а не отдельные баги, см. issue #116).

Как работает сейчас: цепочка шагов

Каждый шаг описан так: вход → что делает scan-gateway → что делает tabular-parser → выход → различия. Код: scan-gateway api/src/core/worker.py, gateway/, normalizer/ocr/; tabular-parser api/src/core/worker.py, parser/book.py, parser/readers/, parser/statements/. Правила: 05-domain-rules.md §7, §11, §11.1, §12.

Шаг 0. Загрузка и доставка до сервиса

Вход: файл пользователя.

Общая часть (arm-core): файл принимается в карточку ЮЛ или в консолидацию (primary_files.upload_one): тип — по расширению и сигнатуре, дедуп по (ЮЛ, sha256), файл — в S3, запись stored_file и recognition_job (queued). Конвейер: xls / xlsx / xlsb → tabular, PDF / PNG / JPG → image. Публикатор outbox собирает recognition-request (job_id, file_id, pipeline, file_type, original_name, ссылка S3, reply_deadline, attempt) и публикует в subject конвейера (files.tabular_uploaded / files.img_uploaded). Срок ответа по умолчанию: OCR — 20 мин, парсер — 5 мин; нет ack 30 с — повтор, после трёх повторов — dlq, файл ERROR.

В сервисе (одинаково): запрос проверяется (JSON-объект, MAJOR версии 1, свой pipeline, схема; иначе SCHEMA_MISMATCH без ack); повтор job_id в работе — только повторный ack; ack processing до чтения S3; файл читается из S3 (не прочитан — FILE_READ_ERROR). scan-gateway дополнительно: бакет должен быть его единственным; reply_deadline истёк до отправки — TIMEOUT.

Выход: байты исходного файла + запрос (file_type, original_name, reply_deadline).

Различия: нет, кроме сроков ответа и проверки бакета.

Шаг 1. Подготовка данных

Вход: байты файла.

1.1 Разбор и чтение сырых данных

scan-gateway — разбор делает ИС Сканирование:

  1. Файл пишется во «Входящие» горячей папки как <job_id>.<ext> (нет расширения / папка недоступна — RECOGNITION_FAILED).
  2. ИС кладёт в «Исходящие» <job_id>/ один или несколько json — по одному на распознанный комплект отчётности.
  3. Опрос каждые SCAN_POLL_SECONDS (5 с); набор готов, когда есть *.json и он не менялся SCAN_SETTLE_SECONDS (2 с); срок — min(reply_deadline, запись + SCAN_TIMEOUT_SECONDS) (1200 с), иначе TIMEOUT.
  4. Склейка: каждый json разбирается независимо, по возрастанию имени → {job_id, files: [{name, response, parse_error}]}; сохраняется в S3 raw/{job_id}.json до нормализации; затем (только при SCAN_CLEANUP) папка очищается.

Содержимое одного json (по fixtures/ocr/*.json, §11): корень {file_uid, error {code, message}, data_ocr}, успех — error.code == ""; data_ocr: поля шапки label, organization, inn, units, date — каждое {form_name, confidence} (сырой OCR-текст), одна шапка на json; table_structure[] — таблицы форм {label {form_name, confidence}, rows[{row_index, cells[{column_index, value, confidence}]}]}, column_index не обязательно подряд. Наблюдается: json с таблицами содержит ББ и ОФР одного периода под общей шапкой. Допущение владельца (17.10.2026): несколько периодов в одном файле ИС отдаёт разными json; образца нет — проверить на первом таком файле.

tabular-parser — чтение делает парсер:

  1. Файл больше PARSE_MAX_BYTES (50 МБ) — UNSUPPORTED_FORMAT; PARSE_TIMEOUT_SECONDS (120 с) — от начала разбора.
  2. Формат — по сигнатуре байтов (OLE2 → XLS, zip с xl/workbook.bin → XLSB, иной zip → XLSX, прочее — UNSUPPORTED_FORMAT); несовпадение с file_type — предупреждение; лимиты распакованного zip и площади объединённых ячеек.
  3. Книга → листы Sheet: непустые ячейки (текст, значение, числовой формат), объединённые диапазоны, уровни группировки, габариты. Библиотека упала — UNREADABLE; xlsb без стилей и группировки — предупреждение.

1.2 Базовая разметка

scan-gateway — разметку сделала ИС, шлюз её принимает: ошибочный json (parse_error, непустой error.code, корень не объект) — предупреждение <имя>: <код>: <сообщение> и пропуск; все json ошибочны — error с кодом первого. Шапка json: ИНН — только цифры, организация, дата (двойники + разбор), единицы; уверенность ИС у каждого поля; предупреждения уровня файла «ИНН не распознан» / «ИНН распознан неуверенно» / «дата отчёта не разобрана». Каждая таблица table_structure[i] — кандидат в документ; тип — по label (rules/statement_labels.yaml).

tabular-parser — разметка по содержимому (§12 п. 1, 6): вид листа (признак ведомости в первых 30 строках — ОСВ, отдельный путь; заголовок формы или ОКУД — форма; маркер «Форма … с.2», шапка «Код» с периодами, колонка кодов или 1–2 кода сразу после листа формы — продолжение; иначе прочее с предупреждением). Формы на листе: строки с заголовком («Бухгалтерский баланс», «Отчет о финансовых результатах», «…о прибылях и убытках») или ОКУД — кандидаты; без кодов между ними сливаются; заголовок после кодов — новая форма. Тип по ОКУД (0710001 → BALANCE, 0710002 → INCOME, прочие 0710xxx и «Пояснения» → DISCLOSURE), заголовок спорит с ОКУД — ОКУД и предупреждение. Шапка своя у каждой формы в границах блока: организация, ИНН (без ИНН аудитора), ОКПО, ОКОПФ, ОКФС, единицы, дата, адрес; найдено — 100, нет — null; предупреждения «в шапке не найдена организация» / «…единица измерения». Листы-продолжения присоединяются к последней открытой форме — но решение требует раскладки колонок (шаг 2).

Выход: список кандидатов в документы форм — признак типа, шапка, сырые строки таблицы (строки × ячейки).

Различия (шаг 1):

№ scan-gateway tabular-parser
1 Кто размечает ИС Сканирование парсер, по содержимому
2 Единица разметки json (комплект одного периода) → таблицы книга → листы → блоки форм
3 Шапка одна на json, общая для его таблиц своя у каждой формы
4 Тип формы label таблицы (уточнение по кодам — шаг 6) ОКУД, иначе заголовок
5 Уверенность у каждого поля шапки и каждой ячейки, 0–100 нет (найдено — 100)
6 Многостраничная форма одна таблица = одна форма, склейки нет листы-продолжения присоединяются (шаг 6)
7 Ячейка сырой OCR-текст + confidence текст, значение и числовой формат

Шаг 2. Раскладка таблицы

Вход: сырые строки таблицы одной формы.

scan-gateway (layout.py, rows.parse_table_rows, §11.1 п. 3): шапка — строка с ячейкой, начинающейся на «код»; наименование — ячейка «наименование», иначе ближайшая заполненная слева от кода; колонки значений — правее кода, где в тексте четыре цифры года или текст начинается на «на » / «за ». Повторная шапка внутри таблицы перезаписывает раскладку (число колонок изменилось — предупреждение; значения строки берутся по позиции, лишние отбрасываются, недостающие — «ячейки нет»). Без шапки раскладка выводится по данным: код — столбец с наибольшим числом значений из справочника кодов, наименование — левее с наибольшим числом текстов, значения — правее, где суммы не реже мусора; шапка синтезируется, колонки без дат, предупреждение. Ни шапки, ни столбца кода — таблица пропускается («колонка кодов не найдена»). Колонка: label — текст шапки, period_date — разбор даты после замены букв-двойников, только если текст «размером с дату»; нет даты — «колонка N без даты».

tabular-parser (columns.find_layout, §12 п. 2–4): колонка кодов — столбец с наибольшим числом кодов (4–5 цифр, первые четыре в диапазоне формы), кодов ≥ 3 (у продолжения ≥ 1) и доля среди непустых ≥ порога; нет — документ без строк, «колонка кодов не найдена». Строка заголовка — последняя над первым кодом с подписью «Код», иначе последняя с двумя и более периодами правее кодов (или одним не голым годом); двухстрочный заголовок склеивается; строка нумерации «1 2 3 4 5» — часть заголовка. Колонки-периоды — правее кодов с распознанным периодом по словарю statement_periods.yaml; подпись без периода («Изменение», «%») пропускается, одно предупреждение со списком. Наименование — левее кодов с наибольшим числом текстов, кроме «Пояснения». Без заголовка (продолжение): колонки значений — где в строках с кодами есть числа, подпись пустая, дата null.

Выход: раскладка формы — столбец кода, столбец наименования, колонки значений [{label, period_date}], первая строка данных, строки, которые не данные (шапка, нумерация).

Различия (шаг 2):

№ scan-gateway tabular-parser
1 Что считается кодом при поиске столбца столбец — по справочнику кодов (infer_layout), строка — любое целое (int(text)) 4–5 цифр в диапазоне формы (словарь)
2 Повторная шапка внутри таблицы перезаписывает раскладку не ищется (одна раскладка на блок)
3 Дата колонки «размером с дату» + двойники + parse_date словарь периодов period_end
4 Без шапки синтез шапки, колонки без дат, предупреждение колонки по числам, подпись пустая
5 Строка нумерации «1 2 3 4 5» не распознаётся (становится строкой данных, AURORA-1517) часть заголовка, в данных пропускается
6 Подпись без периода колонка значений, «без даты» колонка пропускается, предупреждение

Шаг 3. Отбор строк данных

Вход: раскладка + сырые строки.

scan-gateway (rows.py, §11.1 п. 4): склеенная строка («1500\n1700» с N частями в суммах) разлепляется на N (OQ-156, предупреждение-счётчик). Классификация по ячейке кода: пустой код — пропуск (есть суммы — счётчик «строк без кода с суммами пропущено»); нечисловой код («4 400\nI #4У», «ИЧ,250») — пропуск (счётчик «строк с нечисловым кодом пропущено»); целое число — строка данных (вид ROW / TOTAL / BALANCE по наименованию — дальше не используется). Повтор кода — остаётся первая, предупреждение «код N повторяется: оставлена первая строка». Пропущенные строки не восстанавливаются.

tabular-parser (lines.collect_lines, §12 п. 5): пустые строки пропускаются; код — 4–5 цифр в диапазоне, иначе строка «без кода»: нумерация «1 2 3 4 5» — пропуск; без кода с ненулевыми суммами — счётчик «строк без кода с суммами пропущено»; повтор кода — остаётся первая, код в duplicates.

Выход: строки данных формы — сырой код, сырое наименование, ячейки значений по колонкам; счётчики отброшенных.

Различия (шаг 3):

№ scan-gateway tabular-parser
1 Код-мусор отдельный счётчик «нечисловой код» не отличается от «без кода»
2 Склейка двух строк в одну разлепляется (OQ-156) нет такого случая
3 Повтор кода предупреждение текстом checks.duplicate_line_codes
4 Отброшенная строка только счётчик; строка исчезает (AURORA-1634, 1462) только счётчик; строка исчезает

Шаг 4. Значения ячеек

Вход: ячейки значений строки.

scan-gateway (amounts.parse_amount, document._value, §11.1 п. 7): пробелы удаляются первыми; запятая / точка как разделитель тысяч по шаблонам (4,200, 837.994); запятая → точка; скобки → минус; из строки выбрасывается всё, кроме цифр, +, -, .; больше одной точки — все удаляются; больше двух знаков после точки — amount = null, текст в string_value. Прочерк / пусто — null. is_recognized = confidence == 100 и при разборе не выбрасывались символы. Ячейки под колонкой нет — amount = null, confidence = 0, is_recognized = false.

tabular-parser (lines._value, statement_money, number_format.display_amount, §12 п. 5): число с числовым форматом — как показывает формат ячейки (масштабирующие запятые → тысячи, число знаков, секции и условия, округление half-up; формат не разобран — сырое значение и одно предупреждение на документ); иначе to_money: Decimal, скобки → минус, прочерк / пусто → null, нечисловой текст → string_value, is_recognized = false. confidence всегда 100. Счётчики дробных значений: при «тыс.» и ≥ 10 % дробных — предупреждение.

Выход: StatementValue {column_index, amount, string_value, confidence, is_recognized}.

Различия (шаг 4):

№ scan-gateway tabular-parser
1 Предобработка текста OCR-мусор: пробелы, двойники, тысячи, скобки } / ] числовой формат ячейки Excel
2 Смысл is_recognized уверенность 100 ∧ ничего не выброшено текст разобрался как число
3 «42831 1» пробел удалён → 428311, «чистое» (AURORA-1511) не бывает
4 Единицы не проверяются предупреждение о дробных при «тыс.»
5 Пустая ячейка там, где была колонка null, confidence ИС null, 100

Шаг 5. Код, наименование, редакция строки

Вход: строка данных с сырым кодом и наименованием.

scan-gateway (document._line, _choose_edition, §11.1 п. 6): редакция формы — по дате отчёта, иначе по самой поздней дате колонок, иначе по кодам (предупреждение); код очищается до букв, цифр, пробела и точки; наименование — из справочника редакции по коду (api/rules/statement_lines.yaml шлюза), уверенность кода и имени = уверенность ячейки кода, если текст чистый и код в справочнике, иначе min(conf, 99); кода нет в справочнике — имя из OCR со своей уверенностью, код min(conf, 99), предупреждение «код N из редакции …», если он есть в другой.

tabular-parser (lines.collect_lines, _line_name): код как найден (4–5 цифр); наименование — из файла: ячейка колонки наименований, если пуста — ближайшая текстовая левее кодов, кроме «Пояснения»; уверенность кода 100, имени 100 (пусто — null). Редакции и справочника нет.

Выход: StatementLine {line_code, line_code_confidence, line_name, line_name_confidence, values}.

Различия (шаг 5):

№ scan-gateway tabular-parser
1 Источник наименования справочник по коду (AURORA-1406, 1522) файл
2 Редакция (66н / ФСБУ 4) определяется, влияет на имена не определяется
3 Справочник строк свой statement_lines.yaml (не совпадает с ядром) нет
4 Уверенность кода от ИС, срезается до 99, если не в справочнике всегда 100

Шаг 6. Сборка документа

Вход: раскладка, строки, шапка кандидата.

scan-gateway (document.build_document, _resolve_type): тип — по label; все коды одной группы и противоречат label — тип по кодам с предупреждением; label не сопоставлен — по большинству кодов. Колонки — из первой шапки. locator = file:<json>;table:<i>. Шапка — поля json (общая).

tabular-parser (statement._document, merge): лист-продолжение присоединяется к последней открытой форме, если число колонок сумм совпало и тип в маркере не противоречит (слабое продолжение с 1–2 кодами — ещё и коды из диапазона формы); не присоединился — одинокое продолжение с пустой шапкой и предупреждение. locator = sheet:<A>,<B>. DISCLOSURE и форма без колонки кодов — без columns / lines.

Выход: StatementDocument {document_type, locator, header, columns, lines} (без checks).

Различия (шаг 6):

№ scan-gateway tabular-parser
1 Тип формы label, сверка с кодами ОКУД / заголовок, с кодами не сверяется
2 Склейка страниц нет листы-продолжения
3 locator file:…;table:i sheet:A,B

Шаг 7. Самопроверки

Вход: документ.

scan-gateway: не делает; checks отсутствует («не проверялось»).

tabular-parser (statements/checks.run_statement_checks, §12 п. 7): соотношения §7 по каждой колонке из statement_checks.yaml: жёсткие → relations_failed, мягкие → soft_relations_failed; допуск ±1, округление 0,001 % уводит жёсткое в мягкие; abs_terms; «родитель = Σ подстрок» только при непустой детализации (AURORA-1527); повторы кодов → duplicate_line_codes; предупреждения текстом на каждое нарушение.

Выход: StatementChecks или ничего.

Различия (шаг 7): есть только у парсера; словарь §7 у парсера и ядра — две копии; ядро результат парсера не читает, считает заново.

Шаг 8. Статус и предупреждения

Вход: документы и накопленные предупреждения.

scan-gateway (_Build.fully_recognized, _normalize_file, normalize): документ «полностью распознан», если ни одного предупреждения, все ячейки is_recognized, уверенность кода и имени 100, ИНН и организация с уверенностью 100, дата распознана. Файл — все документы распознаны ∧ нет предупреждений файла. Конверт done ⇔ все файлы распознаны ∧ ни один json не ошибочен; иначе needs_check; ни одной таблицы — error RECOGNITION_FAILED. Предупреждения — плоские строки <json>: table:<i>: <текст> в metrics.warnings.

tabular-parser (statement._is_ok, book.parse): документ ok ⇔ колонка кодов найдена ∧ не одинокое продолжение ∧ есть колонки ∧ в шапке организация или ИНН ∧ checks есть ∧ нет жёстких нарушений ∧ нет повторов кодов; DISCLOSURE всегда ok. Книга done ⇔ все документы ok ∧ ни один лист не потерян; книга без листов ОСВ и форм — INVALID_DOCUMENT. Предупреждения — строки sheet=<имя> <текст> в metrics.warnings; на статус не влияют.

Выход: ResultStatus конверта + metrics.warnings[].

Различия (шаг 8):

№ scan-gateway tabular-parser
1 Критерий done всё с уверенностью 100 и без предупреждений структура найдена, проверки сошлись
2 Влияние предупреждений любое → needs_check не влияют
3 Статус документа нет (только конверт) внутренний ok, в контракт не попадает
4 Формат предупреждений <json>: table:<i>: … sheet=<имя> …
5 Привязка предупреждения к файлу к документу (по префиксу)

Общее для обоих: статус один на конверт; предупреждения — свободный текст без привязки к строке или ячейке; ядро и экран не могут показать, что именно и где не так.

Шаг 9. Результат

Одинаково: RecognitionResult (schema_version, job_id, file_id, status, documents, metrics) → S3 results/{file_id}/{job_id}/result.json → конверт в files.tabular_parsed с Nats-Msg-Id = result:<job_id>:<attempt>. У шлюза в конверте ещё raw_s3_key.

Что с результатом делает ядро (для полноты картины)

  • result_mapping.map_file → statement_mapping.build_statement: повторно отбрасывает повторы колонок, кодов и значений (защита от обоих производителей), пишет financial_statement_* как пришло.
  • statement_status (D-6): RECOGNIZED ⇔ конверт done ∧ проверки §7 пересчитаны ядром и сошлись ∧ ИНН не MISMATCH ∧ период известен ∧ все ячейки с уверенностью 100 и is_recognized ∧ есть строки ∧ нет предупреждений маппинга; иначе NEEDS_CHECK. checks парсера не читаются; metrics.warnings сервиса на статус не влияют. RECOGNIZED сразу автоподтверждается.
  • GET …/statement: §7 считается заново по словарю ядра (копия словаря парсера); недостающие строки достраиваются из statement_lines.yaml ядра (не совпадает со справочником шлюза); предупреждения в ответе отсутствуют.
  • Экран: ячейка подсвечивается только по confidence < 100 или is_recognized = false; нарушения §7 — списком без привязки к ячейкам; предупреждения сервиса и маппинга нигде не показываются.

Бойлерплейт и потери, которые порождены различиями шагов 2–8: три копии словарей (§7 в парсере и ядре, строки в шлюзе и ядре); двойная дедупликация; пересчёт проверок; статус, который ядро собирает заново из косвенных признаков; три несвязанных канала качества (уверенность ячейки, проверки §7, предупреждения), из которых пользователь видит полтора.

Целевая картина

Каждый шаг ниже: вход = выход предыдущего шага, единый выход для обоих сервисов, список «различие → к чему приводим → что меняется в scan-gateway → что меняется в tabular-parser». Решения владельца собраны в конце (Р-1…Р-19, все приняты 17.10.2026). Текст прошёл три раунда независимого ревью (review-2026-10-17*.md рядом с этим файлом); находки внесены. Имена полей — рабочие, уточняются при написании спеки.

Принципы, из которых выведены решения по строкам:

  1. Один выход на шаг. Код, зависящий от источника, живёт только в шаге 1 (адаптеры «json ИС → кандидат», «лист Excel → кандидат»). Шаги 2–8 — общий код с общими словарями (форма общего кода — Р-1). Единственное место, где общий код смотрит на источник, — разбор запятой и точки в сумме (шаг 4).
  2. Ничего не выбрасывать молча. Строка, колонка или ячейка, которую не удалось истолковать, идёт дальше со статусом, а не превращается в счётчик; пользователь её видит и исправляет. Положение в источнике (ref), порядок строк (row_index) и сырой текст ячейки (text) доезжают до базы и экрана. Подтвердить документ, не разобрав строки с суммами без кода, нельзя (Р-13).
  3. Качество — структурно и с привязкой к месту: уверенность ячейки (от источника), is_recognized (разбор числа), статус строки, нарушения §7 по ячейкам, предупреждения с уровнем и привязкой. Уверенность, is_recognized и §7 сервис сводит в один признак needs_review у значения, строки и поля шапки (шаг 8) и из них выводит статус документа. Ядро при загрузке не пересчитывает ни проверки, ни статус, ни needs_review: хранит их как пришли и добавляет только предупреждения info по своему словарю имён. После правок аналитика ядро считает тем же общим кодом, добавляя своё знание: правка или подтверждение снимает needs_review, акцепт снимает жёсткие нарушения, квартал периода, сверка ИНН и наименований с справочником — условия RECOGNIZED.
  4. Один словарь — одно место: проверки §7 (полный и упрощённый наборы), периоды, заголовки форм, упорядоченные перечни кодов форм по редакциям и вариантам (без наименований), диапазоны кодов — в общем коде сервисов; соответствие «код → наименование» — только в ядре, сервисы его не знают и отдают распознанный текст (Р-3). Порядок и состав редакций ядро берёт из своей копии общего кода, словарь ядра хранит только имена; sync-statements --check сверяет, что все коды словаря ядра есть в перечне.
  5. Очистка OCR (§11.1) — не отдельный шаг, а первая стадия типовых разборов текста в общем коде, там, где уже известен смысл ячейки: дата шапки (шаг 1), подписи колонок (шаг 2), код строки и разлепление склеенных строк (шаг 3), сумма (шаг 4). Буквы-двойники («О» → 0, «З» → 3, «б» → 6, «S» → 5, «I», «l», «|» → 1) заменяются в датах, подписях, кодах и суммах (Р-11, отменяет §11.1 п. 1 и решение № 22 среза 3a «только даты»): сумма с заменой получает is_recognized = false, код с заменой считается неуверенным (эффективная уверенность ниже 100), а дальше их судьбу решают контрольные соотношения §7 и структурная проверка кода. В суммах крайние «|» (след границы таблицы: «| : 122477») убираются, а не заменяются. ИНН двойники не трогают: только цифры, как сейчас — иначе «3|1 21 2|» из aep_3 даёт лишние единицы и MISMATCH с ЮЛ. Разбор вызывается для любой ячейки без value; у Excel он срабатывает только на текстовых ячейках и почти всегда ничего не меняет. Из normalizer/ocr/ шлюза text, dates, amounts становятся общими разборами, layout, rows, header, document — шагами 2–6 общего кода.

Шаг 1 — Подготовка данных

Решения владельца: 1) кто размечает — различие остаётся; 2) на выходе одинаковый контракт; 3) шапка своя у каждой таблицы; 4) тип формы — единый подход; 5) уверенность есть и у парсера, всегда 100; 6) парсер склеивает продолжения в шаге 1; 7) ячейка — один формат.

Вход: байты файла (шаг 0).

Единый выход: список кандидатов в форму на файл, FormCandidate:

  • source — ocr | excel;
  • locator — откуда взят: file:<имя json>;table:<i> (как в §11 и golden-эталонах) / sheet:<A>[,<B>];
  • type_hint — BALANCE | INCOME | DISCLOSURE | null, по единому правилу (п. 4);
  • evidence — ОКУД и заголовок как ячейки (для сверки с кодами в шаге 6);
  • header — поля header контракта (organization_name, inn, report_date, units, okud, okpo, okopf, okfs, address), каждое — ячейка или null;
  • rows — плотная сетка строк × ячеек; все страницы и листы формы уже в одной сетке; пустая позиция — null;
  • segment_breaks — номера строк сетки, с которых начинается новый лист Excel (у OCR пусто): граница листа — всегда граница сегмента раскладки (шаг 2);
  • warnings — предупреждения шага (формат — шаг 8).

Ячейка Cell одна для шапки и таблицы: text (как показывает источник), value (число или дата, если источник их знает; у OCR — null), confidence 0–100 (у Excel — 100), ref — позиция в источнике: OCR — row:<row_index>, Excel — <лист>!<A1>. Уникальность в документе требуется от ref строки (шаг 3), не ячейки.

  • 1. Кто размечает — остаётся, фиксируется полем source
    • scan-gateway: без изменений
    • tabular-parser: без изменений
  • 2. Единица разметки — файл → список кандидатов; у OCR каждая таблица ИС — кандидат, у Excel — каждый блок формы на листе; шаги 2–8 работают только с кандидатом (Р-1 — форма общего кода)
    • scan-gateway: адаптер «таблица json → кандидат»
    • tabular-parser: адаптер «блок формы на листе → кандидат»
  • 3. Шапка — у каждого кандидата своя; поля = поля header контракта, каждое — Cell; разбор текста поля — общий код по text: ИНН → только цифры, без двойников; дата → словарь периодов (шаг 2 п. 3), который включает все шаблоны сегодняшнего dates.py шлюза; единицы → словарь. Уверенность и сырой текст каждого поля доезжают до контракта (*_confidence, *_text)
    • scan-gateway: шапку json копирует в каждую свою таблицу
    • tabular-parser: добавляет сырой текст и уверенность 100
  • 4. Тип формы — одно правило: ОКУД → заголовок по одному словарю → null; окончательно — по кодам строк в шаге 6, у обоих
    • scan-gateway: label таблицы становится заголовком; statement_labels.yaml уходит в общий словарь
    • tabular-parser: добавляет сверку по кодам (шаг 6)
  • 5. Уверенность — поле Cell.confidence; Excel всегда 100
    • scan-gateway: без изменений
    • tabular-parser: добавляет поле
  • 6. Многостраничная форма — одна форма = одна сетка на выходе шага 1; строки продолжения дописываются в конец сетки по маркеру «Форма … с.2» или по позиции листа, граница листа запоминается в segment_breaks; тип из маркера сверяется с type_hint, расхождение — отдельный кандидат + предупреждение error. Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы (как _weak_blocker сейчас) и, при трёх и более кодах, число колонок сумм совпадает с формой; иначе — отдельный кандидат с предупреждением error. Раскладка у каждого сегмента своя (шаг 2 п. 2), поэтому разная ширина колонок или объединённые ячейки на листе-продолжении без шапки не сдвигают значения
    • scan-gateway: без изменений
    • tabular-parser: склейка переезжает до раскладки; join_blocker сохраняет проверки по типу, диапазону кодов и числу колонок
  • 7. Ячейка — один формат Cell; Excel: text — как показывает числовой формат, value — число по формату (tp#42; формат не разобран — сырое значение + предупреждение); OCR: text сырой, value null, confidence от ИС
    • scan-gateway: обёртка над value + confidence ИС
    • tabular-parser: отображаемый текст и значение считаются здесь, а не в шаге 4

Шаг 2 — Раскладка таблицы

Вход: FormCandidate (сетка, segment_breaks, type_hint).

Единый выход: TableLayout — сегменты сетки (участки между повторными шапками и границами листов), у каждого: столбец кода, столбец наименования, колонки значений [{col, label, label_confidence, period_end, period_kind}], сопоставленные с первым сегментом по порядку; роль каждой строки сетки (title | header | numbering | data).

Диапазоны кодов (словарь, границы уточняются в спеке): ББ 1100–1799, ОФР 2100–2999, отчёт об изменениях капитала 3100–3699, отчёт о движении денежных средств 4100–4499, отчёт о целевом использовании 6100–6499, пояснения 5000–5999. «Кодовые диапазоны» для поиска столбца — объединение этих отрезков (годы 2000–2099 и четырёхзначные суммы на 1–6 вне отрезков кодами не считаются). «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца; им проверяются разлепление строк (шаг 3), слабые продолжения и наследование столбца в сегментах.

  • 1. Столбец кодов — одно правило: код — текст без пробелов, переносов и краевой пунктуации («1220 ,» → 1220), после замены двойников (порядок: убрать пробелы и переносы → двойники → проверка; «|» → 1 только если результат попадает в кодовый диапазон: «| 140» → 1140), 4–5 цифр (допускается .0), первые четыре в кодовых диапазонах. Многострочная ячейка — код, если каждая её часть код. (а) Столбец, над данными которого стоит подпись «Код» / «Коды» / «Код строки» (словарь подписей) и под которой доля кодов среди непустых ячеек ≥ 0,6 или кодов ≥ 3; несколько кандидатов — столбец с наибольшим числом кодов; берётся последняя такая подпись над первой строкой с кодом. Защита обязательна: в выгрузках 1С под подписью «Коды» титульного блока стоят ОКУД, дата, ОКПО, ИНН, ОКОПФ (12267 у АО, 12300 у ООО — по первым четырём цифрам это «коды баланса»), ОКЕИ: один код из девяти, доля 0,11. Документ из двух строк остаётся документом: два из двух, доля 1,0. (б) Подписи нет → столбец с наибольшим числом ячеек-кодов, кодов ≥ 3, доля ≥ 0,6 (так парсер сейчас); нет — «колонка кодов не найдена». Статусы строк с нечитаемым или пустым кодом — новое поведение для обоих: шлюз сегодня такие строки пропускает со счётчиком
    • scan-gateway: столбец по подписи получает защиту по доле; запасной подсчёт — по кодовым диапазонам, как сейчас; классификация строк int(text) → статусы
    • tabular-parser: добавляет проход по подписи до подсчёта; диапазон формы → кодовые диапазоны
  • 2. Повторная шапка и границы листов — одно правило: новый сегмент открывает строка с признаком шапки («Код» / та же сигнатура) или граница листа из segment_breaks; у сегмента своя раскладка: по его шапке, а без шапки — столбец кодов по правилу п. 1, и если сегмент не первый и кодов в нём меньше трёх, ослабленный поиск: столбец с наибольшим числом кодов из диапазона столбца первого сегмента, минимум один (так relaxed + _weak_blocker парсера сейчас: лист «_2» НПЗ с единственной строкой 2400, у которого столбец кодов в другой колонке, чем на листе 1); колонки значений без шапки — по числам в строках с кодами, сопоставление с первым сегментом по порядку; число не совпало — предупреждение error, значения по позиции; столбец кодов не найден ни так, ни так — предупреждение error сегмента, строки сегмента не теряются (роль data, статусы по шагу 3)
    • scan-gateway: как сейчас, через общее правило
    • tabular-parser: бывший «лист-продолжение со своей раскладкой» становится сегментом; бывшая проверка числа колонок в join_blocker — предупреждение сегмента
  • 3. Дата колонки — один словарь периодов (statement_periods.yaml), объединяющий сегодняшний yaml парсера и шаблоны dates.py шлюза: «На 31 марта 2025 г.», «За январь – март 2025 г.», «31.12.2024», «30/09/25», «30092025», «За 9 месяцев 2025 г.», «за I квартал 2025 г.», «1 полугодие 2026», «с 01.01.2025 по 31.03.2025», «2024», двузначный год, хвост «0:00:00», клетки «2|0|2|4|»; строки test_dates.py::test_parses_real_forms шлюза — обязательные тесты словаря. Результат — period_end и period_kind: абсолютные подписи → absolute; относительные («На отчётную дату», «На 31 декабря предыдущего года», «На 31 декабря года, предшествующего предыдущему», «За отчётный период», «За аналогичный период прошлого года») → relative:<вид>, дата считается от эффективной даты документа: дата отчёта из шапки, иначе самая поздняя абсолютная дата колонок; нет ни той ни другой → period_end = null, period_kind остаётся относительным, ядро досчитает после правки даты отчёта; подписи нет → period_kind = none. Подпись OCR с уверенностью ниже 100, разобранная в дату, подтверждается, если дата равна одной из ожидаемых (эффективная дата, минус год, минус два года) — иначе error «дата колонки неуверенна». Перед разбором — очистка двойников OCR. Относительные подписи — Р-6: это бланки по приказу 66н без впечатанных дат, у шлюза сейчас «колонка 0 без даты: На отчетную дату»
    • scan-gateway: parse_date → словарь, его шаблоны переезжают в словарь
    • tabular-parser: добавляет очистку двойников (no-op), относительные подписи и шаблоны шлюза
  • 4. Строка заголовка и её отсутствие — одно правило (обе ветки парсера сохраняются): строка заголовка — последняя над первой строкой с кодом, где в столбце кодов стоит подпись «Код»; иначе последняя над первой строкой с кодом с двумя и более подписями-периодами правее кодов (или одной, не голым годом); двухстрочный заголовок склеивается. Роль title — только строки выше найденной строки заголовка (титульный блок «Дата 30 | 9 | 2025» Межениновской); в сегменте без заголовка роли title нет, строки выше первого кода с содержимым идут в данные (шаг 3). Заголовка нет ни у одного сегмента документа → колонки значений первого сегмента = столбцы правее кода, где в строках с кодами стоят числа; label пустая, period_end null, period_kind = none; одно предупреждение error «строка заголовка не найдена». Заголовок есть хотя бы у одного сегмента — остальные сегменты берут его колонки (п. 2) без предупреждения: листы 2–5 ББ НПЗ без шапки остаются done
    • scan-gateway: синтез шапки уходит (он эквивалентен); добавляет ветку «заголовок по периодам»
    • tabular-parser: добавляет предупреждение для документа без единого заголовка
  • 5. Строка нумерации «1 2 3 4 5» — одно правило is_numbering_row → роль numbering, в данные не попадает
    • scan-gateway: добавляет (AURORA-1517)
    • tabular-parser: как сейчас
  • 6. Подпись без периода — Р-6: подпись — период по словарю (абсолютный или относительный, п. 3) → колонка с датой; подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») → пропуск с предупреждением info; иначе, если в столбце есть числа в строках с кодами (OCR-мусор вроде «| | г» с уверенностью 17 в aep_3, обрезанная или самодельная подпись) — колонка остаётся с подписью как есть, period_end = null, period_kind = none, предупреждение error с привязкой к колонке, документ needs_check; аналитик ставит дату колонке через PATCH или подтверждает после сверки с источником; иначе пропуск. Ядру колонка без даты не мешает для периода формы (эффективная дата, п. 3), но межформенная проверка и гудвилл «на отчётную дату» по такой колонке не считаются, пока дата не проставлена. Пропуск числовой колонки с нераспознанной подписью отвергнут: для OCR это тихая потеря периода с возможным автоподтверждением
    • scan-gateway: добавляет словарь исключений и относительные периоды
    • tabular-parser: перестаёт молча пропускать неизвестные числовые колонки; добавляет относительные периоды
  • 7. Столбец наименования — одно правило: подпись «Наименование (показателя)» в строке заголовка → этот столбец; иначе столбец левее кодов с наибольшим числом текстов, кроме столбца «Пояснения»; наименование строки — ячейка этого столбца, пусто → ближайшая текстовая ячейка левее кода, кроме «Пояснения»
    • scan-gateway: «наименование», иначе слева от кода → общее правило
    • tabular-parser: _name_column / _line_name → общее правило

Шаг 3 — Отбор строк данных

Вход: FormCandidate + TableLayout.

Единый выход: RawLine[] — по строке на каждую строку сетки с ролью data, у которой есть содержимое: код, число или непустой текст в колонке значений с уверенностью ниже 100 (признак потерянной суммы: aep_3 row 8 «[190 :» с «Ё», «" :»). У строки: row_index (порядковый номер в документе), ref (уникален в документе; у разлепленных строк row:<i>#<k>), ячейка кода, code_status, ячейка наименования, ячейки значений по колонкам раскладки сегмента, parent_ref (для detail). Статусы (ok | unreadable | missing | detail):

  • ok — код разобран;
  • unreadable — ячейка кода содержит хотя бы одну цифру, но кодом не разобралась («ИЧ,250», «изо :», «[190 :»); ячейка кода без единой цифры («}», «:») считается пустой;
  • detail — Р-8: строка без кода с содержимым, стоящая под строкой с кодом (в том же сегменте или в предыдущих сегментах документа — родитель продолжается через разрыв страницы), — расшифровка («в том числе: …», «из них …», «Оборудование к установке», «Товар (41.01)»); родитель — ближайшая строка с кодом выше. Сервисы её отдают, ядро хранит, в проверки §7, агрегаты и критерий done она не входит, экран по умолчанию не показывает. Отменяет OQ-36 / OQ-141 («строки без кода не отдаются»); OQ-149 («восстановление строк по наименованию») остаётся открытым;
  • missing — строка без кода с содержимым, над которой во всём документе нет строки с кодом (кода не было или он потерян в самом начале таблицы: «Выручка» aep_3, где 2110 уехал в ячейку значения); строки роли title сюда не попадают.

Строка без кода и без содержимого (заголовок раздела «АКТИВ», «в том числе:» без сумм) — не строка. Счётчиков отброшенных нет — есть статусы. Повтор кода — не статус: по правилам отчётности один код может стоять у нескольких строк с разными наименованиями, все они ok. Строка формы с пустой ячейкой кода под другой строкой с кодом попадёт в detail и будет скрыта; её значения не войдут в сумму раздела, нарушение §7 подсветит родителя (цель соотношения отсутствует при непустых членах — нарушение, шаг 7), аналитик раскроет расшифровки и назначит код (Р-13) — осознанная цена Р-8.

  • 1. Код-мусор — статус unreadable, строка сохраняется с сырым текстом кода (AURORA-1462: «ИЧ,250» видна, аналитик назначает 1250). Разбор кода — по правилу шага 2 п. 1
    • scan-gateway: счётчик → статус
    • tabular-parser: отличает «нет кода» от «не разобран»
  • 2. Склейка двух строк в одну — одно правило, выполняется до разбора кода: ячейка кода содержит N частей через перевод строки и ячейки значений — N частей → N строк (OQ-156) с ref row:<i>#<k>; разлепление делается, если хотя бы одна часть кода разобралась как код диапазона столбца («44 30\nI |» aep_6 не множится: 4430 не в диапазоне ББ), нечитаемая часть становится строкой unreadable; число частей не совпало или ни одна часть не код — одна строка, многострочное значение → amount = null, string_value = текст, is_recognized = false. Значения разлепленных строк получают is_recognized = false и нуждаются в покрытии проверками §7 (Р-17: сошлись — подсветки нет, иначе needs_review); у Excel не срабатывает
    • scan-gateway: как сейчас, через общее правило; разлепление при нечитаемой части — новое; статус документа после разлепления — по общему правилу, а не безусловный needs_check
    • tabular-parser: правило общее, no-op
  • 3. Повтор кода — Р-4: повтор кода — законный случай (один код, разные наименования), не ошибка и не предупреждение. Все строки сохраняются как есть со статусом ok; текстовое предупреждение и checks.duplicate_line_codes уходят. Для проверок §7, подтверждения и всего, что идёт после, значения одного кода идут суммой (см. «Что уходит из ядра»; договорённость ДБД в AURORA-1431; закрывает AURORA-1406). Неверно прочитанный код, случайно совпавший с существующим, ловит структурная проверка кода: повтор, у которого хотя бы одна копия с уверенностью ниже 100, — needs_review у этой строки (Р-16)
    • scan-gateway: «оставлена первая» → все строки
    • tabular-parser: duplicates уходит
  • 4. Отброшенная строка — строки не отбрасываются: unreadable / missing / detail идут дальше со статусом, row_index и ref (AURORA-1634, 1462); ядро не достраивает вместо них пустую шаблонную строку молча; подтвердить документ, пока у unreadable / missing есть суммы, нельзя (Р-13). Отмена OQ-36 / OQ-141 — Р-8
    • scan-gateway: счётчики → статусы
    • tabular-parser: счётчики → статусы; строки без кода с суммами начинают отдаваться

Шаг 4 — Значения ячеек

Вход: ячейки значений RawLine по колонкам раскладки (Cell или null).

Единый выход: StatementValue {column_index, amount, string_value, text, confidence, is_recognized}: text — сырой текст источника всегда, как Cell.text; string_value — как сейчас, только когда сумма не разобрана (amount = null, непустой текст); needs_review досчитывается в шаге 8.

  • 1. Предобработка текста — у ячейки после шага 1 либо есть value, либо только text. value — число → amount = value, квантование до 2 знаков half-up, не больше 16 целых цифр (numeric(18,2)), иначе amount = null, string_value = текст, is_recognized = false и предупреждение; value — дата или логическое → amount = null, string_value = текст, is_recognized = false; value нет → один parse_amount(text, source) для обоих: двойники (Р-11), пробелы и тонкие пробелы, минус «−» (U+2212) как знак, скобки → минус, причём } и ] принимаются как закрывающая скобка (OQ-147), прочерк → null; разделители: у ocr запятая — тысячи при группе из трёх цифр (AURORA-1405, sg#35) и точка — тысячи при группах из трёх цифр («837.994», sg#37), текст с тремя и более знаками после разделителя — amount = null, string_value = текст (решение № 30 среза 3a сохраняется); у excel запятая и точка — десятичный разделитель («4,200» = 4,20, «1.500» = 1,5, как to_money сейчас); «лучшая догадка» из цифр ограничена 16 цифрами, иначе null; многострочное значение в строке с однострочным кодом → amount = null, string_value = текст, is_recognized = false; Excel-ячейки «число текстом» идут тем же путём. Разбор ОСВ (to_money в classify.py) общим parse_amount не заменяется — он остаётся за пределами этой работы
    • scan-gateway: parse_amount переезжает в общий код и получает квантование, предел длины, } / ], «−», тонкие пробелы
    • tabular-parser: to_money для форм заменяется общим parse_amount с source = excel
  • 2. Смысл is_recognized — один: текст разобран как число без выброшенных и исправленных символов (замена двойника — исправление; } / ] вместо скобки — тоже); confidence — отдельно, от источника; оба признака сервис сводит в needs_review (шаг 8, «Подтверждение проверкой»), экран подсвечивает по нему
    • scan-gateway: отвязывает от confidence
    • tabular-parser: как сейчас
  • 3. «42831 1» — в parse_amount пробел допустим только как разделитель тысяч (группы ровно по три цифры); иначе amount — лучшая догадка (цифры подряд), is_recognized = false, сырой текст — в text (AURORA-1511, 1519)
    • scan-gateway: меняется через общий код
    • tabular-parser: то же правило, у Excel почти не срабатывает
  • 4. Единицы — одно правило: единицы «тыс. руб.» и дробных ≥ 10 % → предупреждение документа info (как у парсера сейчас, документ остаётся done); то же сочетание у книги xlsb без числовых форматов — error: вероятен масштаб ×1000 (рубли с копейками, которые формат переводил в тысячи), а §7 сходится при любом масштабе
    • scan-gateway: добавляет
    • tabular-parser: как сейчас; xlsb-сочетание — новое
  • 5. Пустая ячейка — Р-2: позиции нет, текст пуст или прочерк → amount = null, string_value = null, is_recognized = true, confidence ячейки (нет ячейки — 100), собственной подсветки нет. По fixtures/ocr ИС отдаёт ячейку на каждую позицию каждой строки: пустые — пустой текст с уверенностью 100; непрочитанные — пустой текст с уверенностью 0, и такие нуждаются в покрытии по общему правилу шага 8 (снимается покрытием или подтверждением ячейки аналитиком); позиция отсутствует только при несовпадении колонок сегмента, что покрыто предупреждением error шага 2 п. 2
    • scan-gateway: «ячейки нет → 0, не распознано» уходит
    • tabular-parser: как сейчас

Шаг 5 — Код, наименование, редакция

Вход: RawLine (статус, row_index, ref, ячейки кода и наименования) + StatementValue[].

Единый выход: StatementLine {row_index, ref, line_status, line_code, line_code_text, line_code_confidence, line_name, line_name_confidence, parent_ref, values}: line_code — разобранный код (4–5 цифр) или null при unreadable / missing / detail; line_code_text — сырой текст ячейки кода; line_code_confidence — от источника, а у кода с заменой двойника — ниже 100 (неуверенный); line_status = code_status шага 3; parent_ref — у detail. Несколько строк с одним кодом — обычные строки ok, порядок как в файле.

  • 1. Источник наименования — Р-3: сервисы отдают распознанный текст наименования как есть, с уверенностью источника; соответствие «код → наименование» — только в ядре и только когда нужно (показ справочного имени, достройка шаблонных строк, сверка для неуверенных кодов — Р-16). Экран показывает справочное имя, файловое — рядом (AURORA-1406, 1522)
    • scan-gateway: имя из statement_lines.yaml → имя из OCR
    • tabular-parser: как сейчас
  • 2. Редакция (66н / ФСБУ 4) — в сервисах редакция нужна только для упорядоченного перечня кодов (подтверждение кода структурой, шаг 8) и выбирается по эффективной дате документа (шаг 2 п. 3), без даты — проверяются обе редакции. Наименования и достройка шаблонных строк — ядро по его словарю имён; проверка «код не из редакции формы» — ядро по своей копии перечня общего кода
    • scan-gateway: _choose_edition → выбор редакции перечня кодов в общем коде
    • tabular-parser: добавляет выбор редакции перечня кодов
  • 3. Справочник строк — только в ядре; в общий код сервисов не входит; копия в шлюзе удаляется
    • scan-gateway: удаляет api/rules/statement_lines.yaml
    • tabular-parser: без изменений
  • 4. Уверенность кода — от источника (ИС / 100), без срезания до 99 по справочнику; «код не в справочнике» — проверка ядра, предупреждение info к строке, не сервисов
    • scan-gateway: убирает min(conf, 99)
    • tabular-parser: как сейчас

Шаг 6 — Сборка документа

Вход: FormCandidate (type_hint, evidence, header) + TableLayout + StatementLine[].

Единый выход: StatementDocument {document_type, form_variant, locator, header, columns, lines} без checks.

  • 1. Тип формы — одно правило: большинство кодов в диапазоне ББ → BALANCE, ОФР → INCOME, прочих форм и пояснений → DISCLOSURE; совпало с type_hint или type_hint null — тип по кодам; противоречит — тип по кодам + предупреждение error; кодов нет — type_hint. Без колонки кодов: type_hint есть → документ без строк (DISCLOSURE — done, иначе needs_check с предупреждением error); type_hint нет: у ocr каждая таблица ИС с содержимым — потерянный документ, предупреждение error файла (сегодня шлюз: «нет шапки и столбца кода» → needs_check), у excel лист «прочее» — info. DISCLOSURE по type_hint — шаги 2–7 не выполняются; DISCLOSURE по кодам — шаги 2–5 выполнены, строки и колонки отбрасываются (ядро строки пояснений не хранит), проверки не считаются, документ done. Вариант формы form_variant — full | simplified, только по структуре кодов (ОКУД для этого не годится: 0710096 — код КНД комплекта упрощённой отчётности, на бланках стоят 0710001 / 0710002): ББ — есть 1600 и нет ни 1100, ни 1200; ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300; шаг 7 и перечень кодов берут упрощённый набор. Образец упрощённой формы — в fixtures при первой возможности
    • scan-gateway: как сейчас, через общее правило; добавляет упрощённый вариант и error за потерянную таблицу
    • tabular-parser: добавляет сверку с кодами и упрощённый вариант
  • 2. Склейка страниц — решена в шаге 1, различия нет
    • scan-gateway: без изменений
    • tabular-parser: без изменений
  • 3. locator — одна грамматика «пары ключ:значение через ;», набор ключей по источнику: file:<имя json>;table:<i> / sheet:<A>[,<B>] — ключи как сейчас, golden-эталоны и §11 не меняются
    • scan-gateway: без изменений
    • tabular-parser: без изменений

Шапка документа — поля header кандидата как есть (text → *_text, value → поле, confidence → *_confidence у всех полей).

Шаг 7 — Самопроверки

Вход: StatementDocument.

Единый выход: StatementChecks у обоих: relations_failed[], soft_relations_failed[] (каждое нарушение — с кодами слагаемых и column_index, чтобы экран подсветил ячейки). Правила счёта:

  • значение кода в колонке — сумма по всем строкам ok с этим кодом (повтор кода — законный случай, шаг 3 п. 3); строки detail, unreadable, missing в соотношения не входят; duplicate_line_codes уходит; нарушение подсвечивает все строки кода;
  • соотношение считается посчитанным, если непуста цель или хотя бы один член; пропускается, только когда пусто всё (ни строки ok с кодом цели, ни одного непустого члена). Пустая цель при непустых членах — жёсткое нарушение («БАЛАНС» без кода, ушедший в detail, делает 1600 невидимым, и это должно подсветить родителя); непустая цель при пустых членах — тоже нарушение (лист только с активом: «1600 = 1700» при отсутствующем 1700 — needs_check, как сегодня у трёх файлов манифеста парсера). Сегодня оба сервиса и ядро соотношение с отсутствующей целью пропускают — поведение меняется; посчитанное соотношение из всех null ничего не подтверждает (то же правило, что для «родитель = Σ подстрок» по AURORA-1527);
  • «сошлось» / «не сошлось»: жёсткое соотношение не сошлось, если расхождение больше допуска ±1 и больше округления 0,001 % — только тогда оно в relations_failed; расхождение в пределах округления и мягкие соотношения — в soft_relations_failed, это info, на статус и подсветку не влияют (OQ-138 сохраняется);
  • соотношение с членами по модулю (abs_terms: 2120, 2210, 2220, 2330, 2350, 2410) сходится при любом знаке такого члена и потому подтверждает только величину, не знак — см. «Подтверждение проверкой»;
  • набор соотношений выбирается по form_variant: полный или упрощённый (нет подытогов 1100 / 1200 / 1300 у ББ, 2100 / 2200 / 2300 у ОФР, свои равенства), оба — в одном словаре statement_checks.yaml;
  • родитель пятизначной подстроки — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код из первых четырёх цифр (subline_parents, §12 п. 7: 12310 под 1230).

Проверки по словарю имён (код не в словаре) и по перечню кодов (код не из редакции формы по эффективной дате) — в ядре, как предупреждения info к строке; сервисы словаря имён не знают (Р-3, Р-12).

  • 1. Проверки только у парсера; словарь §7 в двух копиях; ядро результат не читает — проверки — общий код с одним словарём statement_checks.yaml у обоих сервисов; для OCR нарушение §7 — главный признак неверно прочитанной цифры; ядро читает checks из результата, хранит их (document_issue, см. «Что уходит из ядра») и пересчитывает тем же кодом только после правок аналитика (PATCH)
    • scan-gateway: добавляет шаг
    • tabular-parser: run_statement_checks → общий код

Шаг 8 — Статус и предупреждения

Вход: StatementDocument + StatementChecks + предупреждения шагов 1–7.

Единый выход: у каждого документа status (done | needs_check), warnings[], needs_review у каждого значения, каждой строки и каждого поля шапки; у файла metrics.warnings[]; статус конверта — агрегат. Предупреждение — структура {code, severity: error | info, message, line_refs?: [], column_index?, field?}, а не строка; line_refs — список, потому что нарушение подсвечивает все строки кода.

  • 1. Критерий done — одна функция общего кода с входами «раскладка, шапка, статусы строк, needs_review, нарушения, предупреждения, подтверждения и акцепт аналитика»: раскладка найдена ∧ (организация или ИНН) ∧ период определён (дата отчёта или самая поздняя абсолютная дата колонок — statement_period ядра; у ИС поле date часто мусор: у aep_6 и «БО 1 кв 2025» там название организации) ∧ все строки ok или detail ∧ нет значений, строк и полей шапки с needs_review (см. «Подтверждение проверкой» ниже) ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет предупреждений уровня error. DISCLOSURE — done без условий о раскладке, строках и проверках (как сейчас). Excel выполняет условия уверенности автоматически; структурная проверка кодов к Excel не применяется (Р-12). Квартал периода, статус конверта, сверка ИНН и наименований — знание ядра, в done не входят
    • scan-gateway: «ничего не выброшено и уверенность 100» → общий критерий
    • tabular-parser: _is_ok → общий критерий; добавляются строки и значения
  • 2. Влияние предупреждений — по уровню: error → needs_check, info — не влияет; у обоих одинаково. Таблица уровней, полная версия — в спеке. error: ошибочный json набора при успешном соседе (уровень файла; решение № 28 среза 3a), BACKEND_PARSING_ERROR таблицы, таблица ИС с содержимым без колонки кодов и без типа (уровень файла), сегмент с содержимым без колонки кодов (в том числе потерянный лист-продолжение), лист с тремя и более кодами и другим числом колонок, не присоединившийся к форме, колонки сегмента не совпали, колонка без даты, дата колонки неуверенна, заголовка нет ни у одного сегмента, тип по кодам противоречит заголовку, тип маркера продолжения противоречит форме, ни организации, ни ИНН в шапке, период не определён (нет ни даты отчёта, ни дат колонок), дата отчёта неуверенна и не совпала с датами колонок, xlsb без форматов при «тыс. руб.» и дробных. info: ИНН не найден или неуверен при найденной организации (сверку с ЮЛ делает ядро — MISMATCH; НПЗ ОФР без ИНН остаётся done), организация не найдена или неуверенна при найденном ИНН, дата отчёта не разобрана при наличии дат колонок, единицы не найдены (аналитик правит в шапке), формат файла не совпал с file_type (файл прочитан верно), подпись из словаря исключений пропущена, дробные при «тыс. руб.» (кроме xlsb без форматов), xlsb без числовых форматов само по себе (tp#42, документ остаётся done), лист Excel «прочее» без type_hint и кодов, разлеплена склеенная строка, код не в словаре имён или не из редакции (ядро). Принцип: error — только то, что меняет данные или их полноту
    • scan-gateway: «любое → needs_check» уходит
    • tabular-parser: предупреждения начинают влиять
  • 3. Статус документа — в контракте: document.status у каждого документа, включая ОСВ (у ОСВ done, когда ведомость разобрана, как сейчас; её предупреждения — info); конверт done ⇔ все документы done ∧ нет предупреждений файла уровня error; иначе needs_check; документов нет — error с кодом по источнику: RECOGNITION_FAILED у шлюза, INVALID_DOCUMENT у парсера — это различие остаётся, коды описывают разные причины. Статус конверта в условие RECOGNIZED ядра не входит, туда входят предупреждения error уровня файла (Р-19)
    • scan-gateway: добавляет
    • tabular-parser: внутренний ok → в контракт
  • 4. Формат предупреждений — структура (см. выше) вместо строк <json>: table:<i>: … / sheet=<имя> … у всех документов результата, включая ОСВ; место — в locator документа, а не в тексте
    • scan-gateway: меняет
    • tabular-parser: меняет, включая путь ОСВ
  • 5. Привязка — к документу, строкам (line_refs), ячейкам (line_refs + column_index) или полю шапки (field); ref строки уникален в документе и хранится в ядре вместе с row_index, поэтому экран показывает предупреждение у места и после сохранения
    • scan-gateway: меняет
    • tabular-parser: меняет

Подтверждение проверкой. Проверка §7 — арбитр, уверенность — подсказка там, где проверки нет. По каждому значению сервис считает needs_review:

  • значение нуждается в покрытии, если confidence ниже порога (Р-5: 100) или is_recognized = false (число собрано из грязного текста, с заменой двойников или из разлепленной строки — Р-7, Р-11, Р-17); чистые значения (уверенность 100 и is_recognized = true) в покрытии не нуждаются: правило покрытия и правило знака к ним не применяются;
  • значение покрыто, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано и сошлось (в пределах допуска или округления), и amount самого значения не null (мусор, давший null, покрытым не бывает);
  • знак: для члена соотношения с abs_terms покрытие подтверждает только величину. Знак подтверждён, если форма записи знака у ячейки (скобки, минус, без знака — по text) совпадает с конвенцией колонки — формой записи у большинства членов abs_terms этой колонки с непустым amount, is_recognized = true и без правки аналитика (правленые ячейки в голосование не входят); не совпадает или сравнивать не с чем — needs_review = true независимо от соотношения. Так «(7744536}» с } как закрывающей скобкой даёт −7 744 536 и подтверждается, если 2210 и 2220 в той же колонке тоже в скобках; «{38378)» при остальных в скобках — подсвечивается;
  • needs_review = true, если значение нуждается в покрытии и не покрыто (или не подтверждён знак);
  • true у всех ячеек жёсткого соотношения в колонке, где оно не сошлось вне допуска и округления, независимо от уверенности и чистоты; мягкие соотношения и расхождения в пределах округления подсветки не дают;
  • иначе false: значение подтверждено проверкой, не подсвечивается и статус не трогает; сырой текст (text) и уверенность остаются в подсказке ячейки.

Экран подсвечивает ячейки только по needs_review. Ядро хранит needs_review и text как пришли и пересчитывает needs_review вместе с checks общим кодом после PATCH (конвенция знака считается от сохранённого text), добавляя своё: правленое значение (is_modified) или подтверждённое аналитиком без изменения (is_confirmed, хранится) — needs_review = false; акцепт нарушений — жёсткие нарушения не считаются; предупреждения error документа сохраняются и после правки, кроме снимаемых соответствующей правкой (таблица — в «Что уходит из ядра»). Не покрыто проверками и остаётся на уверенности: справочные строки ОФР 25xx / 29xx, детализация без проверки родителя, DISCLOSURE, колонки, где есть только мягкие соотношения. Риск — две взаимно компенсирующие ошибки в одном соотношении; принимается (перекрёстные соотношения баланса его снижают). Закрывает AURORA-1524 без ущерба для 1519. Сошедшееся посчитанное равенство подтверждает и догадку из грязного текста (Р-7): случайное совпадение требует второй, компенсирующей ошибки в том же равенстве.

Поля шапки. У каждого поля шапки свой needs_review: дата отчёта с уверенностью ниже 100 подтверждена, если совпала с самой поздней абсолютной датой колонок, иначе needs_review = true и предупреждение error «дата отчёта неуверенна» — иначе неверно прочитанный год давал бы неверный период и автоподтверждение; ИНН и организация с уверенностью ниже 100 — info, их сверяет ядро (ИНН — MISMATCH с ЮЛ, организация — по алиасам); единицы и прочие поля на статус не влияют. Сегодня шлюз требует уверенность 100 у ИНН и организации и распознанную дату — правило становится точнее, не слабее.

Код строки (Р-5, Р-12, Р-16). Уверенность кода подтверждается не §7 (перепутанные соседи 1220 и 1230 дают ту же сумму раздела), а структурой и наименованием; правило применяется только к неуверенным кодам — уверенность ниже 100 или замена двойника (Р-12): у Excel и чистых OCR-кодов расхождение с перечнем даёт предупреждение info ядра «код не из редакции», а не needs_check — иначе пятизначные подстроки 1С (11101, 23201) и нестандартные строки клиента (1231) блокировали бы автоподтверждение. В общем коде лежит упорядоченный перечень кодов каждой формы по редакциям (66н, ФСБУ 4) и вариантам (полная, упрощённая) без наименований — порядок бланка, где итоги стоят после слагаемых (1110 … 1190, 1100; 2110, 2120, 2100), а не по возрастанию. Источник перечня — объединение кодов шаблонов банка (словарь ядра) и кодов печатных форм 1С по редакции: 1105, 1530, 2420, 2460, 2930 и другие, которых в словаре ядра нет. Алгоритм: четырёхзначные коды строк документа образуют последовательность; для каждой проверяемой редакции берётся наибольшая подпоследовательность, согласованная с порядком бланка; редакция — та, где она длиннее (при равенстве — по эффективной дате); если таких подпоследовательностей несколько, подтверждены только коды, общие для всех; коды с уверенностью 100 участвуют в последовательности как подтверждённые якоря. Пятизначный код подтверждён, если подтверждён его родитель (правило шага 7) и подстрока стоит после него. Равные соседние коды допустимы, но повтор кода, у которого хотя бы одна копия неуверенна, — needs_review у неуверенной строки (Р-16). Неподтверждённый неуверенный код — needs_review у строки (StatementLine.needs_review), документ — needs_check. Структура не ловит подмену неуверенного кода на незапечатанного соседа того же раздела (1110 → 1120, когда 1120 на бланке пропущен): это закрывает ядро — для строк с неуверенным кодом условие RECOGNIZED включает совпадение распознанного наименования со справочным по коду (Р-16; порог сходства — в спеке), расхождение подсвечивает строку через document_issue. По fixtures/ocr коды часто приходят с уверенностью 50 и 25, поэтому требовать 100, как шлюз сейчас, нельзя. Перечень кодов — не словарь строк: наименований в нём нет, Р-3 не нарушается; ядро пользуется своей копией перечня для проверки «код не из редакции», словарь ядра хранит только имена. Уверенность наименования на статус и подсветку в сервисах не влияет: сервисы отдают текст как есть.

Шаг 9 — Результат

Одинаково, как сейчас; контракт recognition-result.schema.json → v2.0 (Р-9: по правилам контракта смена типа поля — MAJOR). Что меняется: line_code nullable, line_status, line_code_text, row_index, ref, parent_ref, needs_review у значения, строки и полей шапки, values[].text, header.*_confidence и *_text у всех полей; document.status у всех документов, включая ОСВ, document.form_variant; document.warnings[] и metrics.warnings[] — структуры с уровнем и привязкой у всех документов, включая ОСВ; columns[].period_kind и label_confidence; checks обязателен для BALANCE / INCOME со строками у обоих конвейеров (у документа без строк отсутствует), нарушения с кодами слагаемых, без duplicate_line_codes; service.statements_version — коммит копии общего кода, которым посчитан результат. Выкладка: ядро поднимает SUPPORTED_MAJOR и на время перехода принимает 1.x и 2.0 (два маппера, ack тоже 1.x и 2.0; при приёме 1.x статус документа считается по прежнему правилу D-6 — job_done ∧ проверки ∧ values_ok ∧ …, новые поля заполняются по умолчанию: line_status = ok, row_index по порядку, text = string_value), сервисы переходят на 2.0 после ядра — порядок «сначала ядро» обязателен, иначе первый же результат с null в line_code даёт файлу ERROR. Запрос recognition-request остаётся 1.x. Путь ОСВ в парсере меняется только в части структуры предупреждений и document.status.

Что уходит из ядра и что в нём меняется

  • build_statement: повторная дедупликация колонок, кодов и значений (контракт гарантирует уникальность колонок; повторы кодов — законные строки, хранятся все как пришли).
  • DBML v5.19 и одна миграция: financial_statement_line — снять уникальность (meta_id, line_code), добавить row_index (у строк из источника; у шаблонных и созданных правкой — null), source_ref (частичный уникальный индекс по (meta_id, source_ref) там, где не null), line_status, line_code_text, parent_row_index (родитель detail), needs_review, is_confirmed (подтверждение кода аналитиком); financial_statement_value — source_text (из values[].text), needs_review (как пришло от сервиса, пересчитывается ядром после PATCH; пересматривает решение v5.13 «считается, не хранится»), is_confirmed (подтверждено без изменения, хранится вместе с confirmed_by / at); financial_statement_column.period_kind, label_confidence; financial_statement_meta.form_variant, needs_review полей шапки; таблица document_issue (вид warning | relation | soft, код, уровень, сообщение, список line_id, column_id?, field?, источник service | core) — хранит предупреждения и нарушения как пришли; при перезагрузке файла очищается; при пересчёте после PATCH записи relation / soft заменяются целиком независимо от источника, записи warning источника service сохраняются, кроме снимаемых правкой; recognition_job.statements_version; document — снять уникальность (stored_file_id, document_type, osv_account_code) только для BALANCE / INCOME (частичный индекс: ОСВ по счёту и DISCLOSURE / OTHER остаются «один на тип на файл», §9.5 п. 2) и добавить порядковый номер формы в файле (Р-10); в ту же миграцию — crm_id ГСК / ГСЗ (AURORA-1619, arm-core#112). Заполнение при миграции: row_index существующих строк — по порядку кода, line_status = ok, needs_review значений — по прежнему values_ok, source_text = string_value.
  • Повторы кодов (Р-4): для проверок §7, подтверждения и всего, что идёт после (КФО, выгрузки, сверки), документ читается через одну функцию «агрегат формы по коду» — сумма значений строк ok по колонке, список line_id, справочное имя — чистой функцией, без хранения (DECISION-5). На неё переводятся все точки, где сегодня «одна строка на код»: _line_amount (межформенная проверка 1370 / 2400), statement_amounts, subline_parents / parent_code, formLayout (дети под родителем), _resolve_cells, metrics.fields_*.
  • statement_status (D-6) при приёме 2.0: RECOGNIZED ⇔ document.status = done ∧ нет предупреждений error уровня файла (потерянный json или таблица ИС, решение № 28 среза 3a; needs_check соседнего документа само по себе не блокирует — Р-19) ∧ ИНН ≠ MISMATCH ∧ период — конец квартала (period_known: иначе документ RECOGNIZED, но не подтверждаемый, 409 PERIOD_REQUIRED) ∧ у строк с неуверенным кодом распознанное наименование совпало со справочным (Р-16) ∧ в файле нет второй формы того же типа и периода (Р-18: при двух и более — ни одна не автоподтверждается). Остальные признаки D-6 не пересчитываются.
  • §7 при загрузке: checks, needs_review и предупреждения берутся из результата и хранятся (document_issue); ядро добавляет только info по словарю имён и перечню кодов и свою сверку наименований для неуверенных кодов. Пересчёт общим кодом — только после PATCH, с правилами is_modified или is_confirmed ⇒ needs_review = false у значения; line_code_modified или is_confirmed ⇒ needs_review строки = false; акцепт ⇒ жёсткие нарушения сняты. Версия общего кода из результата (statements_version) пишется в recognition_job; при пересчёте ядро сравнивает её со своей копией, расхождение — предупреждение info к документу (на окне выкладки «сначала ядро» расхождение штатно).
  • Какая правка какое error снимает: PATCH header.report_date — «период не определён», «дата отчёта не разобрана», «дата отчёта неуверенна»; PATCH header.inn или organization_name — «ни организации, ни ИНН»; PATCH columns[].period_date — «колонка без даты», «дата колонки неуверенна»; назначение кода или отметка detail — нарушения, пересчитываемые §7. Остальные error сервиса (колонки сегмента не совпали, заголовка нет, тип противоречит, потерянный json, лист или таблица, xlsb с масштабом) снимаются только перезагрузкой файла.
  • PATCH …/statement (Р-13): помимо header и values принимает values[].confirmed (подтвердить ячейку без изменения значения: is_confirmed, needs_review = false, is_modified не ставится, аудит), columns: [{column_id, period_date}] (дата колонке без подписи или с неуверенной подписью; пересчитывает is_current; report_date: null обнуляет period_end относительных колонок) и lines: «назначить код» (line_id, line_code), «отметить расшифровкой» (line_id, line_status = detail), «подтвердить код» (line_id, confirmed). Переходы: unreadable / missing / detail → ok назначением кода; unreadable / missing / ok → detail отметкой; ok → ok с другим кодом (смена кода, прежний в line_code_original_value); ok неуверенный → подтверждён. Правила: код 4–5 цифр, может совпадать с существующим (Р-4); незнакомый словарю — info; назначенный или подтверждённый код снимает needs_review строки, line_code_confidence становится 100, line_code_modified, line_code_original_value; у detail родитель пересчитывается как ближайшая строка ok выше по row_index; шаблонная строка с таким кодом, если она уже создана правкой аналитика с суммами, — 409 LINE_EXISTS с подсказкой (аналитик сначала очищает её суммы или выбирает другой код), без сумм — удаляется, её место занимает настоящая; пара «line_status = ok без кода» — 422; удаления строк нет (первичка не мутируется, detail — способ вывести строку из сумм). Строки, созданные правкой: row_index = null, source_ref = null, line_status = ok. Аудит RECOGNIZED_DATA_EDIT с before / after по коду, статусу, родителю и подтверждению. Один PATCH — один пересчёт и одна запись.
  • Подтверждение документа (Р-13, Р-18): порядок проверок 409 по §9.5 п. 4: PERIOD_REQUIRED → LINES_UNRESOLVED (есть строки unreadable / missing с непустым amount; строки без сумм не мешают) → DUPLICATE_FORM (в файле уже подтверждена другая форма того же типа и периода; 409 со ссылкой на неё; между файлами работает автозамена §9.5 п. 3) → жёсткие нарушения без акцепта. Автоподтверждения LINES_UNRESOLVED не касается (done таких строк не допускает). detail в агрегат, КФО и снимок ЕХ не попадают — цена Р-8.
  • Правка даты отчёта (PATCH шапки): для колонок с period_kind = relative:<вид> ядро досчитывает period_end общим кодом от новой даты.
  • Словарь §7: единственный экземпляр в общем коде (Р-1), копия ядра удаляется. Словарь имён строк (код → наименование по редакциям) остаётся только в ядре, копия шлюза удаляется; порядок и состав редакций ядро берёт из своей копии перечня общего кода.
  • Достройка шаблонных строк: по словарю ядра; код, у которого есть хотя бы одна строка ok из файла, шаблоном не достраивается.
  • Экран (Р-14): раскладка остаётся «как на бумаге» — разделы, внутри строки ok по возрастанию кода, несколько строк одного кода в порядке файла, шаблонные строки на своих местах, пятизначные подстроки под родителем. Строка unreadable / missing ставится под ближайшей предыдущей по row_index строкой, которая разместилась по коду, а без такой — в начало первого раздела; подсвечена целиком, в ячейке кода серым сырой текст с подсказкой. Строки detail скрыты под переключателем «Показать расшифровки (N)», как пустые строки детализации, показываются под родителем с отступом. Подсветка ячеек только по needs_review (уверенность, is_recognized и нарушения §7 уже сведены сервисом), в подсказке — text и уверенность; поля шапки с needs_review подсвечены; предупреждения и нарушения — из document_issue, с привязкой к месту. В режиме «Редактировать»: ячейка кода у unreadable / missing — поле ввода с подсказкой из справочника (код → справочное имя), меню строки «Это расшифровка»; у detail — «Назначить код»; у обычной строки — «Сделать расшифровкой», смена кода и «Подтвердить код» у неуверенного; подтверждение ячейки без правки; дата у колонки без подписи. Кнопка «Подтвердить данные» при неразобранных строках с суммами отключена с подсказкой «Разберите строки без кода: N» и переходом к первой; сервер отвечает тем же 409.
  • Открытый вопрос: склейка двух половинок формы из одного файла не предоставляется, файл перезагружают — в 07-open-questions.md (Р-15). Вопрос о сопоставлении файлового наименования со справочным закрыт Р-16: для неуверенных кодов это условие RECOGNIZED, для остальных — только показ справочного имени.

Решения владельца (Р-1…Р-19 — 17.10.2026, Р-20…Р-44 — 18.10.2026)

  • Р-1 (шаг 1, п. 2) — копия по скрипту. Платформа не собирает Python-пакеты и не кладёт их в свой Nexus, поэтому общий код — dataclass-ы шага 1 (сериализуемые в json), шаги 2–8, словари §7, периодов, заголовков форм, перечни и диапазоны кодов (без словаря имён) со своим загрузчиком — живёт в отдельном репозитории arm-statements со своими тестами и оформлен как пакет arm_statements/ с pyproject. Границы пакета: пакет владеет своими типами входа и выхода, хост конвертирует выход в свою модель контракта (у ядра на переходе их две); golden-тесты пакета — внутри пакета на синтетических и обезличенных данных, тесты на реальных фикстурах остаются в scan-gateway и tabular-parser и соседний репозиторий не читают. В tabular-parser, scan-gateway и arm-core пакет лежит копией вместе со своими тестами и конфигом линтеров (конфиг пакета подключается к pytest и ruff хоста как include, чтобы правила не конфликтовали), импортируется по имени пакета arm_statements (путь добавляется в pyproject), поэтому замена копии на зависимость не трогает импорты; маркер коммита источника попадает в результат как service.statements_version; раскладывает и проверяет свежесть скрипт sync-statements в docs по образцу sync-web.sh (--check, код 20 при прямых правках), вызывается из make check-full перед PR локально — не из make check и не из пайплайна: тест, читающий соседний репозиторий, уже ломал main (arm-core #33, пайплайн платформы вне репозиториев). Прямые правки копии запрещены. Срочное исправление общего кода: PR в arm-statements → sync-statements → PR в каждом потребителе, порядок выкладки «сначала ядро».

  • Р-2 (шаг 4, п. 5) — отсутствующая позиция под колонкой — чистый null без собственной подсветки. Основание: по fixtures ИС отдаёт ячейку на каждую позицию (пустая — уверенность 100, непрочитанная — 0, и её ловит общее правило needs_review); отсутствие позиции бывает только при несовпадении колонок сегмента и покрыто предупреждением шага 2 п. 2.

  • Р-3 (шаг 5, п. 1) — сервисы отдают распознанный текст наименования; соответствие «код → наименование» — только в ядре, при необходимости (= решение № 2 по спринту 2, AURORA-1406 / 1522; договорённость ДБД «из справочника» выполняется на экране).

  • Р-4 (шаг 3, п. 3) — повторы кодов законны; все строки сохраняются и показываются, для проверок и всех операций после подтверждения или автоподтверждения — суммой по коду. Следствие: снять уникальность (meta_id, line_code) в DBML; все чтения «одна строка на код» в ядре переводятся на агрегат.

  • Р-5 (шаг 8, «Подтверждение проверкой») — порог для значений — 100 (у ИС уверенность дискретная: 100, ~80, 71, 67, 60, 50, 25, 0, промежутка 90–99 нет; в группе 70–89 есть настоящие ошибки, например «11 30» с уверенностью 80); ложных подсветок становится меньше за счёт подтверждения проверкой §7, а не порога. Код строки подтверждается структурой — упорядоченным перечнем кодов формы по редакции, не уверенностью; область применения — Р-12, дополнение — Р-16 (= решение № 7 по спринту 2, AURORA-1519 / 1524).

  • Р-6 (шаг 2, п. 6) — числовая колонка с нераспознанной подписью остаётся в документе без даты с предупреждением error (needs_check, подтверждение руками); пропуск отвергнут как тихая потеря периода. Относительные подписи бланка 66н («На отчётную дату», «На 31 декабря предыдущего года», …) добавляются в словарь периодов и считаются от эффективной даты документа; после правки даты отчёта ядро досчитывает их; дату колонке можно поставить руками.

  • Р-7 (шаг 8, «Подтверждение проверкой») — да: сошедшееся жёсткое соотношение §7 снимает подсветку и с is_recognized = false; такой документ может получить done и автоподтвердиться, сырой текст остаётся в подсказке ячейки. Оговорки по ревью: соотношение должно быть посчитанным, а не «0 = 0»; для членов по модулю знак подтверждается конвенцией колонки, а не соотношением; правило знака — только для сомнительных ячеек. Варианты «всегда подсвечивать» и «снимать подсветку, но не автоподтверждать» отвергнуты: первый возвращает ложные подсветки AURORA-1524, второй даёт статус «требует проверки» без подсветок.

  • Р-8 (шаг 3) — расшифровки без кода («в том числе …») отдаются сервисами со статусом detail и хранятся ядром; в проверки, агрегаты и done не входят; экран по умолчанию их не показывает (переключатель, как у пустых строк детализации). Отменяет OQ-36 / OQ-141.

  • Р-9 (шаг 9) — контракт v2.0, а не аддитивный 1.3; ядро на время перехода принимает 1.x и 2.0; порядок выкладки — сначала ядро.

  • Р-10 (ядро) — снять уникальность document по (stored_file_id, document_type, osv_account_code) для форм ББ / ОФР, вторая форма того же типа в файле хранится; в ту же миграцию DBML v5.19 — crm_id ГСК / ГСЗ из AURORA-1619 (arm-core#112).

  • Р-11 (принцип 5) — буквы-двойники заменяются и в суммах, и в кодах; результат — is_recognized = false у суммы и неуверенный код, дальше их проверяют контрольные соотношения §7 и структура, и если всё сошлось, подсвечивать не нужно (Р-7 с оговорками). Отменяет §11.1 п. 1 «только даты» и решение № 22 среза 3a. ИНН двойники не трогают. Данные: в четырёх json ИС двойников в суммах и кодах нет вовсе — правило на вырост, не под текущие сканы.

  • Р-12 (шаг 8, «Код строки») — структурное подтверждение кода применяется только к неуверенным кодам (уверенность ниже 100 или замена двойника); у Excel и чистых OCR-кодов расхождение с перечнем даёт info от ядра, не needs_check.

  • Р-13 (подтверждение документа) — подтверждение документа блокируется (409 LINES_UNRESOLVED), пока есть строки unreadable / missing с суммами; аналитик назначает код или отмечает строку расшифровкой в режиме правки, через PATCH …/statement lines (детали — в «Что уходит из ядра»).

  • Р-14 (экран) — раскладка по коду «как на бумаге» остаётся (просьба владельца 09.10.2026); строки со статусами ставятся под ближайшей предыдущей размещённой строкой по row_index, расшифровки под родителем и скрыты по умолчанию; переключатель «как в файле» — не в этой работе.

  • Р-15 (ядро) — вторая форма того же типа и периода у одного ЮЛ из того же файла хранится; склейка половинок формы из одного файла не предоставляется (файл перезагружают), вопрос — в 07-open-questions.md. От ИС Сканирование такой случай не ожидается (владелец), основной источник — Excel с листом-продолжением другого типа или двойной печатью. Уточнено Р-18.

  • Р-16 (шаг 8, «Код строки»; ядро) — защита от подмены неуверенного кода на соседний: повтор кода, у которого хотя бы одна копия неуверенна, — needs_review у этой строки (сервис); для строк с неуверенным кодом условие RECOGNIZED в ядре включает совпадение распознанного наименования со справочным по коду, расхождение подсвечивает строку. Закрывает открытый вопрос о сопоставлении наименований: для неуверенных кодов — условие автоподтверждения, для остальных — только показ справочного имени.

  • Р-17 (шаг 3, п. 2) — разлепленные строки OQ-156 не держат документ в needs_check безусловно (решение 08.10.2026 заменяется): их значения нуждаются в покрытии, как грязные, и при сошедшихся соотношениях §7 не подсвечиваются; само разлепление — предупреждение info.

  • Р-18 (ядро) — при двух и более формах одного типа и периода в одном файле ни одна не автоподтверждается; подтверждение второй при подтверждённой первой — 409 DUPLICATE_FORM только внутри файла (между файлами работает автозамена §9.5 п. 3).

  • Р-19 (ядро) — RECOGNIZED не зависит от статуса конверта: достаточно document.status = done и отсутствия предупреждений error уровня файла (потерянный json или таблица ИС блокируют, needs_check соседнего документа из-за одной ячейки — нет).

  • Р-20 (18.10.2026; ревью спеки K-4, повторы кодов) — законные повторы кодов (в реальных ББ 2025 — 1220 «НДС, начисленный по отгрузке» и 1520 «Обязательство по аренде») допускаются в упорядоченном перечне кодов: структура подтверждает обе копии. Правило Р-16 «повтор с неуверенной копией — needs_review» применяется только к повтору, которого нет в перечне. Словарь имён ядра — список имён (синонимов) на код; загрузчик перестаёт запрещать повтор кода в этом смысле (сверка наименования — с любым из имён). В спеке: перечень contracts/statement-code-orders.yaml (repeats: у fsbu4 — 1220, 1520, 2410 печатной формы 1С; у 66n — раздел III НКО, см. S-28), §11.9, §7 «Полный состав формы», спеки statement-normalization и arm-core.

  • Р-21 (18.10.2026; ревью спеки K-4, мягкие соотношения ОФР) — неуверенное значение подтверждается и мягким соотношением, если оно сошлось точно (расхождение 0 или в пределах допуска ±1; не «в пределах округления»). Применяется к 2400, 2410, 2411, 2412, 2460 и прочим строкам, у которых есть только мягкие соотношения. Знак для abs_terms — по прежнему правилу конвенции колонки. В спеке: §7, §11.9, требование «Шаг 8 — needs_review значения».

  • Р-22 (18.10.2026; решения писателя S-1…S-25) — приняты все, с оговорками ревьюера из таблицы S-n в review-2026-10-18-spec.md: S-4 — синонимы на код и исключение «итого по разделу» из правила вхождения; S-5 — в упрощённом ОФР 2400 мягкое, как в полном, ББ — жёсткие; S-7 — OQ-148 закрывается этим решением; S-10 / S-22 — одна формулировка; S-13 — двойники внутри детектора нумерации и допуск одного пропуска; S-15 — экранирование имени листа в Excel-ref; S-17 — доопределить по K-23; S-19 — codes и column_index у document_issue; S-21 — по K-2 / K-21. Явно утверждены владельцем S-4, S-5, S-7, S-10, S-12, S-13, S-14, S-20, S-22, S-25 (отмечены ниже).

  • Р-23 (18.10.2026; повторное ревью спеки L-1) — значения строк detail в needs_review и в статус документа не входят: у них needs_review = false, функция статуса их не учитывает; подсказка на экране по text и confidence остаётся. Следствие: на реальных сканах РБ и Счастливочной за 1 кв. 2025 формы могут стать done (по прогону ревьюера держали их только ячейки расшифровок). В спеке: statement-normalization («Шаг 8 — needs_review значения», функция статуса, сценарий на РБ), arm-core (пересчёт после правки, согласованность), arm-web, §11.9, proposal; S-51 пересмотрено.

  • Р-24 (18.10.2026; L-13 и упрощение S-36) — перед выкладкой ядра база очищается от форм ББ/ОФР (документы BALANCE / INCOME, их financial_statement_*, комментарии, привязки к версиям «в работе»; файлы, где кроме форм ничего нет, — целиком с заданиями; ОСВ, консолидации, справочники не трогаются; детали и отказы — S-73). Поэтому миграция v5.19 только добавляет колонки, индексы и значения по умолчанию; §7 для существующих форм не пересчитывается, импорта копии arm_statements в ревизии Alembic нет. Миграция обязана проходить и на непустой базе (колонки по умолчанию, file_ordinal по порядку, document_issue пуст), но корректность нарушений у старых форм не гарантируется — сказано явно. Для приёма 1.x после выкладки (окно перехода) §7 по-прежнему считается ядром копией пакета при приёме (остаётся из S-36). В спеке: proposal (порядок выкладки), arm-core («Миграция DBML v5.19», «Очистка форм перед выкладкой», «Формы результата 1.x — §7 при приёме»), §7 «Ядро», §9.5 п. 2, DBML v5.19 (шапка).

  • Р-25 (18.10.2026) — утверждены владельцем решения писателя S-26, S-34, S-36 (в редакции Р-24), S-37, S-41, S-47 (подтверждение без правки не сбрасывает акцепт), S-48 (в редакции L-12: ядро вызывает общую функцию статуса). Отмечены ниже.

  • Р-26 (18.10.2026; OQ-168) — коды без образца в fixtures (2421 / 2430 / 2450 в 66н, 1120 и другие в ФСБУ 4) в перечень кодов не входят до появления образцов; неуверенный такой код на скане — needs_review строки. Временное правило, принятое владельцем; OQ-168 остаётся открытым до образцов.

  • Р-27 (18.10.2026; S-73, открытый вопрос доработки) — смешанные файлы «ОСВ + формы» при очистке перед выкладкой: удалять целиком (--include-mixed) или только формы — владелец решает по отчёту purge_statements.py --dry-run в момент очистки; до этого скрипт по умолчанию удаляет только формы.

  • Р-28 (18.10.2026; S-55) — принято: при приёме 2.0 ОСВ получает RECOGNIZED по своему osvDocument.status и отсутствию предупреждений error уровня файла, независимо от статуса соседней формы в той же книге (как Р-19 для форм).

  • Р-29 (18.10.2026; S-67) — расхождение «шаблонные строки экрана по редакции по дате, проверки ядра — по редакции по структуре» (бланк 66н, сданный в 2025 году) в этой работе не сводится; отдельная задача после реализации.

  • Р-30 (18.10.2026; S-59) — принято: вариант формы документа, принятого по контракту 1.x, определяется по структуре кодов (как у 2.0), а не остаётся пустым.

Решения владельца по открытым вопросам планов реализации (18.10.2026, plans/):

  • Р-31 (план 4 В-2, план 5 Q-3) — если на прогоне реальных файлов (РБ, Счастливочная, НПЗ, Межениновская) форма получает needs_check по правилу, которое спека считает законным, исполнитель останавливается и поднимает вопрос владельцу по конкретному случаю; адаптер под файл не подгоняется.
  • Р-44 (план 4, ответ на Р-31; 18.10.2026) — числа правее таблицы формы не колонки сумм: при наличии колонки с подписью периода столбец без текста подписи правее последней такой колонки в значения не попадает, вместо COLUMN_WITHOUT_DATE — NUMBERS_OUTSIDE_TABLE (info, адреса ячеек), статус не меняется (пример — Межениновская 2025-Q3, формулы аналитика в U–W). Уточняет P1-17 (№ 120). Цена ошибки: настоящая колонка сумм с потерянной подписью будет отброшена с info; поэтому подписанный столбец без чисел границей не служит. Единые для Excel и OCR.
  • Р-32 (план 5 Q-1) — успешный json ИС без таблиц даёт JSON_FAILED, а не молчаливый пропуск.
  • Р-33 (план 5 Q-2) — если все json набора ошибочны, конверт error перечисляет JSON_FAILED каждого json; код ошибки файла — от первого.
  • Р-34 (план 3 O-2) — подтверждение кода строки уверенность кода не меняет: ставится флаг is_confirmed, line_code_confidence остаётся как пришло, line_code_modified не ставится; снятие подтверждения возвращает needs_review по прежнему правилу. Спека arm-core приводится к этому.
  • Р-35 (план 3 O-5, P3-12) — очистка форм перед выкладкой отказывает только при привязке формы к версии консолидации в статусе «Верифицирована» или дальше; версии в работе, на согласовании, отклонённые и отменённые не мешают. На стенде 18.10.2026 версий дальше «в работе» нет.
  • Р-36 (план 0 O-1) — в окне перехода (до перехода сервисов на 2.0) новые примеры контракта верхнего уровня в docs не добавляются.
  • Р-37 (план 6 В-1) — экран принимается по подстановке ответа 2.0 из фикстуры и регрессии на 1.x; прогон на aep_3 — follow-up после перехода scan-gateway на 2.0.
  • Р-38 (план 6 В-3) — подтверждённая ячейка помечается серой галочкой.
  • Р-39 (план 4 В-1) — коды предупреждений ОСВ — по источнику (OSV_*), без дробления.
  • Р-40 (план 3 O-3) — предупреждения CODE_NOT_IN_DICTIONARY / CODE_NOT_IN_EDITION пишутся только для четырёхзначных кодов.
  • Р-41 (план 3 O-6) — смена crm_id ГСК / ГСЗ пишется в аудит как изменение группы.
  • Р-42 (план 2 O-3) — sync-statements --check добавляется в сборку снимка для контура (prepare-contour-snapshot.sh), по аналогии с sync-web.
  • Р-43 (план 2 O-4) — репозиторий arm-statements вносится в таблицу workspace (CLAUDE.md workspace и context/00-workspace-layout.md) в рамках плана 1.

Decisions made without discussion

Решения агента 17.10.2026, принятые при оформлении целевой картины без отдельного обсуждения с владельцем; все обратимы правкой спеки или словаря.

  1. Уровни предупреждений (шаг 8, п. 2): error — только то, что меняет данные или их полноту; полный список — в шаге 8 п. 2, таблица «какая правка какое error снимает» — в «Что уходит из ядра». Цена ошибки: лишний или пропущенный needs_check, правится таблицей уровней в спеке.
  2. DISCLOSURE всегда done без условий о раскладке, строках и проверках; по type_hint шаги 2–7 не выполняются, по кодам прочих форм строки отбрасываются — сохранено текущее поведение парсера. Цена: пояснения и формы 3, 4, 6 не проверяются; так и сейчас.
  3. Столбец кодов — подпись «Код» принимается при доле кодов под ней ≥ 0,6 или кодах ≥ 3, при нескольких кандидатах — больше кодов; запасной подсчёт «кодов ≥ 3 и доля ≥ 0,6» взят у парсера; сегмент не первый с кодами < 3 — ослабленный поиск по диапазону столбца первого сегмента (как relaxed парсера). Цена: таблица без подписи и с одним-двумя кодами в первом сегменте → документ без строк с needs_check.
  4. Диапазоны кодов — явные отрезки по формам (шаг 2), объединение для поиска столбца, «диапазон столбца» для разлепления и продолжений; границы форм 3, 4, 6 уточняются в спеке. Цена: код вне отрезка не распознаётся как код; правится словарём.
  5. Начальный словарь исключений подписей («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») — по тестам парсера; пополняется словарём, не кодом.
  6. Грамматика locator — пары ключ:значение через ;, ключи по источнику, как сейчас. Цена: нулевая.
  7. Грамматика ref — OCR row:<row_index>[#<k>], Excel <лист>!<A1>; ref строки уникален в документе.
  8. value, который не число (дата, логическое) в колонке сумм — amount = null, string_value = текст, is_recognized = false. Цена: минимальная.
  9. Предел и квантование суммы — 16 целых цифр, 2 знака, half-up — только для числового value Excel; текст OCR с тремя и более знаками после разделителя остаётся null + string_value (решение № 30 среза 3a); «догадка из цифр» длиннее 16 — null.
  10. parse_amount знает источник: запятая и точка у Excel — десятичные, у OCR — тысячи при группах из трёх цифр. Единственное место, где шаги 2–8 смотрят на source. Разбор ОСВ не затрагивается.
  11. checks отсутствует у документа без строк, обязателен у BALANCE / INCOME со строками.
  12. Правило detail: строка без кода с содержимым под строкой с кодом (родитель продолжается через границу сегмента); ячейка кода без единой цифры — пустая. Цена: строка формы с потерянным кодом под другой строкой скрывается до нарушения §7 у родителя (цель отсутствует — нарушение); лечится назначением кода (Р-13).
  13. Посчитанное соотношение — непуста цель или хотя бы один член; пустая цель при непустых членах и непустая цель при пустых членах — нарушение; сегодня соотношение с отсутствующей целью пропускается у обоих сервисов и ядра. Цена: формы без подытогов — только с упрощённым набором соотношений, иначе needs_check, как и сегодня.
  14. Упрощённая форма определяется только по структуре кодов (ББ: есть 1600, нет 1100 и 1200; ОФР: есть 2400 или 2110, нет 2100, 2200, 2300) и получает свой набор соотношений и перечень кодов; ОКУД 0710096 признаком не служит. Цена: ошибка определения → не те соотношения → needs_check; образца упрощённой формы в fixtures пока нет.
  15. Конвенция знака для членов abs_terms: форма записи у большинства членов abs_terms колонки с непустым amount, is_recognized = true и без правки; проверяется только у ячеек, нуждающихся в покрытии. Цена: ОФР, где сомнительные расходы записаны не так, как остальные, подсвечивает их; это и нужно.
  16. Подтверждение даты отчёта структурой: неуверенная дата отчёта подтверждается совпадением с самой поздней датой колонок, неуверенная подпись колонки — попаданием в ожидаемые даты; иначе error. Цена: скан с верной, но неуверенной датой без колонок с датами — needs_check, снимается правкой даты.
  17. Хранить needs_review, text, подтверждения, нарушения и предупреждения в ядре (financial_statement_value, financial_statement_line, financial_statement_meta, document_issue) вместо пересчёта на каждом GET — пересматривает решение v5.13 «считается, не хранится». Цена: ещё одна таблица и миграция; взамен ядро при чтении ничего не считает, а подтверждения аналитика переживают пересчёт.
  18. Эффективная дата документа = дата отчёта, иначе самая поздняя абсолютная дата колонок — та же формула, что statement_period ядра; от неё считаются относительные подписи и выбирается редакция перечня кодов.
  19. document_issue: очищается при перезагрузке файла, записи relation / soft заменяются целиком при пересчёте, line_id — список; error сервиса снимаются только правкой из таблицы соответствия.
  20. Границы пакета arm_statements: свои типы входа и выхода, конвертация в модель контракта на стороне хоста, golden внутри пакета на синтетике, тесты на реальных фикстурах — только в сервисах; расхождение версий копий — info.
  21. Алгоритм структурного подтверждения кода: наибольшая подпоследовательность по порядку бланка, редакция с длиннейшей, пересечение при неоднозначности, уверенные коды — якоря, пятизначные — через родителя. Цена: при двух переставленных неуверенных соседях оба needs_review; это и нужно.
  22. Слияние шаблонной строки с назначенным кодом: шаблонная строка с суммами аналитика — 409 LINE_EXISTS, без сумм — заменяется настоящей. Цена: лишний шаг у аналитика в редком случае; двойного счёта нет.
  23. Рабочие имена FormCandidate, TableLayout, RawLine, needs_review, line_status, line_code_text, row_index, source_ref, parent_ref, period_kind, form_variant, document_issue, statements_version, is_confirmed, LINES_UNRESOLVED, DUPLICATE_FORM, arm-statements, sync-statements — по образцу существующих (sync-web.sh); меняются при написании спеки без последствий.

Решения при написании спеки (18.10.2026)

Решения агента при оформлении спеки, контракта v2.0, правил §11 и DBML v5.19 там, где дизайн оставлял «уточняется в спеке» или не давал значения. Маркер S-n — в отчёте спеки; все обратимы правкой спеки, словаря или DBML до реализации.

  1. S-1. Имя способности контракта — дельта к существующей recognition-contract, а не новая recognition-result-contract. Основание: спека с этим смыслом уже есть в openspec/specs/. Цена: нулевая.
  2. S-2. Конверт recognition-result-message тоже v2.0: metrics.warnings в конверте — те же структуры, что в файле результата; производитель пишет в конверт тот же MAJOR. Основание: «ack тоже 1.x и 2.0» (шаг 9) и одна форма предупреждений. Цена: ещё одна схема поднимает MAJOR; ядро и так принимает оба.
  3. S-3. Диапазоны кодов — как в дизайне, без изменений (3100–3699, 4100–4499, 5000–5999, 6100–6499). Цена: код вне отрезка не код; правится словарём.
  4. S-4. Совпадение наименований (Р-16): нижний регистр, ё → е, всё кроме букв и цифр — пробел; совпало при равенстве, вхождении более короткой строки (≥ 5 символов) или difflib.SequenceMatcher ≥ 0,8 с любым справочным именем кода в любой редакции; нет кода в словаре имён — не совпало. Цена: ложное несовпадение — NEEDS_CHECK и ручное подтверждение; ложное совпадение маловероятно у разных статей одного раздела. Утверждено владельцем 18.10.2026 (Р-22) с оговоркой: у кода несколько имён (синонимы, Р-20); если одно из имён начинается с «итого по разделу», совпадение — только равенство (S-49).
  5. S-5. Формулы упрощённого набора по бланку приказа 66н, прил. 5 (ББ: 1600 = 1150 + 1170 + 1210 + 1230 + 1250, 1700 = 1300 + 1350 + 1360 + 1410 + 1450 + 1510 + 1520 + 1550, 1600 = 1700; ОФР: 2400 = 2110 − 2120 − 2330 + 2340 − 2350 − 2410), все жёсткие; ASSUMPTION до образца (OQ-166). Цена: неверная формула — needs_check упрощённых форм. Утверждено владельцем 18.10.2026 (Р-22) с изменением: ОФР 2400 = … упрощённого набора — мягкое, как 2400 полного (знак 2410, OQ-12; ревью K-11); соотношения ББ — жёсткие.
  6. S-6. Словари подписей: кода — «код», «коды», «код строки», «код показателя»; наименования — подпись, начинающаяся с «наименование». Цена: пополняется словарём.
  7. S-7. Прочерк (-, –, —, − без цифр) — пустое значение, is_recognized = true (как пустая ячейка, Р-2); OQ-148 — допущение. Цена: тире-мусор на месте цифры не подсвечивается сам по себе, его ловит §7 (пустой член). Утверждено владельцем 18.10.2026 (Р-22): OQ-148 закрыт этим решением.
  8. S-8. Каталог кодов предупреждений (таблица §11.9 05-domain-rules.md); code в контракте — строка ^[A-Z][A-Z0-9_]*$, не перечисление (новый код — не MAJOR); VALUE_OUT_OF_RANGE — info (значение и так needs_review). Цена: опечатка кода не ловится схемой — ловится тестами пакета.
  9. S-9. Предупреждение уровня файла имеет locator (json набора ИС, лист). Цена: нулевая.
  10. S-10. needs_review полей шапки — массив header.needs_review_fields в контракте и jsonb в financial_statement_meta; сейчас туда попадает только report_date: сомнения в ИНН и организации — info по дизайну, needs_review у них дал бы needs_check вопреки «НПЗ ОФР без ИНН остаётся done». Цена: экран подсвечивает ИНН и организацию по issues с field, а не по needs_review. Утверждено владельцем 18.10.2026 (Р-22); формулировка одна во всех документах: needs_review_fields — поля, которые держат документ в needs_check; сырой текст и уверенность всех полей шапки хранятся (header_source, S-33).
  11. S-11. Значения period_kind: absolute, relative:current, relative:prev_year_end, relative:prev_prev_year_end, relative:prev_period, none; «За аналогичный период» — эффективная дата − 1 год (29.02 → 28.02). Цена: новая относительная подпись — новое значение (MINOR).
  12. S-12. Ожидаемые даты неуверенной подписи колонки — эффективная дата, она же − 1 и − 2 года и 31 декабря предыдущего и предшествующего ему года. Основание: у квартального ББ колонки «на 31.12 прошлого года», и набор дизайна (дата, − 1 год, − 2 года) давал бы error на каждой такой форме. Цена: чуть шире окно подтверждения. Утверждено владельцем 18.10.2026 (Р-22).
  13. S-13. Строка нумерации — не меньше трёх непустых ячеек, все целые, подряд с 1 слева направо. Цена: нумерация с пропуском не распознаётся (строка уйдёт в данные со статусом). Утверждено владельцем 18.10.2026 (Р-22) с оговоркой: двойники внутри детектора и допуск одного пропуска — уточнено S-40.
  14. S-14. Признак повторной шапки — подпись кода в строке или ≥ 2 непустых ячеек, совпавших с шапкой первого сегмента. Цена: шапка без «Код» и с другими подписями не начнёт сегмент. Утверждено владельцем 18.10.2026 (Р-22).
  15. S-15. ref и row_index: row_index с 0; Excel ref строки — адрес ячейки столбца кодов (без столбца — первой непустой ячейки); разлепленные row:<i>#<k> с k от 1; у разлепленных строк line_code_text и text — k-е части. Цена: нулевая. Оговорка Р-22: имя листа в Excel-ref экранируется по правилу Excel (кавычки, удвоенный апостроф), примеры контракта исправлены.
  16. S-16. Уверенность кода с заменой двойника — не выше 99 (так «неуверенный» виден из одного поля). Цена: нулевая.
  17. S-17. Формат PATCH lines — один элемент = одно действие (line_code | line_status: "detail" | confirmed); коды 422 INVALID_LINE_CODE, LINE_CODE_REQUIRED, INVALID_LINE_PATCH, UNKNOWN_LINE, UNKNOWN_COLUMN; правка суммы по line_code при наличии строки ok — 422 UNKNOWN_CELL. Цена: имена кодов меняются до реализации без последствий. Оговорка Р-22: доопределено по K-23 (S-47); 409 LINE_EXISTS переименован в LINE_CODE_TAKEN (S-43).
  18. S-18. Детали 409: LINES_UNRESOLVED — detail.lines[] {line_id, row_index, line_code_text}; DUPLICATE_FORM — detail.document_id. Цена: нулевая.
  19. S-19. Имена DBML v5.19: document.file_ordinal (с 0, уникален в файле), financial_statement_meta.needs_review_fields (jsonb), document_issue.line_ids (jsonb — сверка ORM↔DBML не знает массивов), enum'ы statement_line_status, statement_form_variant, document_issue_kind, issue_severity, issue_source; индекс (meta_id, line_code) остаётся неуникальным; у строки is_confirmed без confirmed_by/at (строго по дизайну, кто — в аудите). Новые колонки — последними в таблицах (как их добавит миграция). Цена: нулевая до миграции. Оговорка Р-22: у document_issue добавлены codes и column_index (S-31, S-35); висячий line_id удалённой шаблонной строки убирается из line_ids.
  20. S-20. related_group.crm_id уникален (как у ГК и ЮЛ), повтор — 409 CRM_ID_EXISTS. Цена: две группы с одним ID CRM завести нельзя; так и должно быть. Утверждено владельцем 18.10.2026 (Р-22).
  21. S-21. Значения по умолчанию при приёме 1.x: ref = null, needs_review строки false, period_kind — absolute при дате иначе none, form_variant = null, строки-предупреждения — LEGACY_WARNING info. Цена: на окне выкладки документы 1.x без новых сигналов качества. Оговорка Р-22: по K-2 / K-21 — S-36.
  22. S-22. Статус документа ОСВ — done, когда контрольные суммы §1.4 листа сошлись, иначе needs_check (прежнее правило конверта, применённое к листу). Цена: нулевая — конверт ведёт себя как сейчас. Утверждено владельцем 18.10.2026 (Р-22); формулировка «контрольные суммы §1.4 листа сошлись» теперь одна в контракте, §11, спеках recognition-contract и tabular-recognition-service.
  23. S-23. service.statements_version обязателен, если в результате есть statementDocument (схема проверяет). Цена: результат только с ОСВ может его не нести.
  24. S-24. Редакция перечня кодов в сервисах — fsbu4 при эффективной дате после 2024-12-31 (то же правило, что edition ядра, OQ-145). Цена: досрочный ФСБУ 4/2023 — проверяются обе через «длиннейшую подпоследовательность». Уточнено S-26: проверяются всегда обе, дата решает только ничью.
  25. S-25. Квартал периода (условие RECOGNIZED): дата не конец квартала → NEEDS_CHECK (как D-6), ручное подтверждение — 409 PERIOD_REQUIRED. Основание: оговорка дизайна «иначе документ RECOGNIZED, но не подтверждаемый» противоречит автоподтверждению RECOGNIZED. Цена: нулевая — поведение как сейчас. Утверждено владельцем 18.10.2026 (Р-22).

Решения при доработке спеки по ревью K-1…K-25 (18.10.2026)

Решения агента при закрытии находок review-2026-10-18-spec.md там, где ни ревьюер, ни владелец не задали значение, или где агент отступил от предложения ревьюера. Маркер S-n продолжает нумерацию; все обратимы правкой спеки, словаря или DBML до реализации.

  1. S-26. Редакции перечня кодов — всегда обе (K-5): для неуверенного кода считаются наибольшие подпоследовательности по 66n и fsbu4, берётся длиннейшая, эффективная дата решает только ничью. Основание: Межениновская за 3 кв. 2025 сдана на бланке 66н — выбор только по дате (S-24) не подтвердил бы её 1120; формула дизайна «редакция — где длиннее, при равенстве — по дате» иначе не имеет смысла. Цена: на ББ с кодами, общими для обеих редакций, выбор безразличен; неверный выбор возможен только при равной длине — тогда решает дата, как в S-24. Уточнено S-65 (лучшая подпоследовательность — сначала по числу уверенных кодов). Утверждено владельцем 18.10.2026 (Р-25).
  2. S-27. Перечень кодов — данные в docs (K-5): источник — contracts/statement-code-orders.yaml (обращение «как контракт»: общие данные сервисов и ядра), в пакете — байтовая копия rules/statement_code_orders.yaml, идентичность проверяет sync-statements --check; форма файла — scripts/validate_contracts.py, для этого в make venv docs добавлен PyYAML. Цена: ещё одна зависимость docs-проверок (make venv нужно перезапустить); перенос источника в arm-statements — правкой одного требования.
  3. S-28. Состав перечня — только подтверждённые образцом коды (K-5): шаблоны банка + коды печатных форм 1С и сканов из fixtures/ (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; раздел III НКО (1310 «Паевой фонд» … 1370, 1300 — aep_3) — законный повтор 66n; клиентский 1231 — в 66n (Унагранде 2024). Коды без образца (1120 у fsbu4, 2421 / 2430 / 2450 у 66n, упрощённые перечни) — OQ-168, OQ-166 (временное правило утверждено владельцем — Р-26). Все 12 последовательностей кодов из fixtures (Excel и OCR) укладываются в свою редакцию целиком. Цена: на скане с неподтверждённым кодом и неуверенностью ниже 100 — needs_review строки; у Excel и уверенного кода — только info.
  4. S-29. Окно перехода контракта (K-8): схемы 1.2 / 1.0 лежат в docs как *.v1.schema.json (байтовые копии, сервисы сверяются с ними без правки копий), примеры — examples/v1/; тест идентичности сервиса выбирает файл по $id копии (…/v1 → *.v1.schema.json) — это шаг 0 выкладки в tabular-parser и scan-gateway до слияния docs. Отступление от предложения ревьюера «тест сверяет с MAJOR, который сервис пишет»: признак — $id копии, а не настройка сервиса, чтобы правило работало без знания кода сервиса. Цена: маленький PR в двух сервисах до слияния docs; без него их make check красный на окне. Заменено S-53 (повторное ревью L-2: файла .v1 у запроса нет, примеры и ядро не были покрыты).
  5. S-30. Механика sync-statements (K-9): копия — api/arm_statements/ (импорт как src), тесты пакета в testpaths хоста, mypy src arm_statements; маркер — файл api/arm_statements/VERSION с полным хэшем; --check: 0 синхронно, 10 отстала (зелёный с предупреждением — иначе любой посторонний PR в сервис требовал бы подтягивать чужие изменения пакета), 20 прямая правка / чужая версия / расхождение перечня кодов / код словаря ядра вне перечня, 2 нет checkout или аргумент. Без checkout arm-statements check-full красный (код 2): это проверка перед PR, а не make check. Цена: отставшая копия проходит check-full; видна в описании PR.
  6. S-31. column_index — номер столбца сетки кандидата (K-6): у OCR — column_index ИС как есть (разреженный), у Excel — номер столбца листа первого сегмента с 0; у document_issue — column_id для колонки формы и column_index для столбца вне колонок; line_refs — только строки lines[]. Отступление от варианта «порядковый номер колонки значений»: иначе пропущенная колонка (COLUMN_SKIPPED) не имела бы номера. Примеры контракта приведены к правилу (Excel — 3, 4; OCR — 2). Цена: номера не с 0 и не подряд — экран и ядро не должны считать их позициями. Дополнено S-56 (колонки сегментов со своей шапкой).
  7. S-32. TABLE_PARSE_ERROR и SHEET_OTHER — предупреждения файла (K-7): с locator; документа нет; TABLE_PARSE_ERROR (error) блокирует RECOGNIZED всех документов файла, как JSON_FAILED. Цена: один упавший разбор таблицы держит весь файл на ручном подтверждении.
  8. S-33. Шапка: header_source jsonb + прежние *_original_value (K-3): сырой текст и уверенность всех десяти полей — одной колонкой {поле: {text, confidence}}; inn_original_value, report_date_original_value, units_original_value заполняются как прежде, inn_confidence остаётся. Отступление от варианта «колонки на каждое поле»: 20 колонок ради подсказки на экране. Цена: поиск по сырому тексту поля шапки в SQL — через jsonb.
  9. S-34. organization_name в PATCH шапки (K-12): непустая строка до 1000 символов, organization_name_modified, organization_name_original_value (новые колонки DBML), пересчёт counterparty_check, аудит; снимает NO_ORGANIZATION_NO_INN. Выбран вариант «добавить», а не «убрать из правила снятия»: иначе форма без шапки снималась бы только перезагрузкой. Цена: правка наименования влияет на сверку контрагента по алиасам. Утверждено владельцем 18.10.2026 (Р-25).
  10. S-35. Коды нарушений в document_issue (K-13): relation → RELATION_FAILED / error, soft → RELATION_SOFT / info, колонка codes jsonb, line_ids — все строки ok кодов. Цена: нулевая.
  11. S-36. Формы без checks (K-2, K-21): у результата 1.x и у форм до миграции ядро один раз считает §7 копией пакета (form_variant = null — полный набор) и пишет document_issue (source = core): при приёме 1.x и в ревизии миграции (ревизия импортирует копию arm_statements). D-6 «проверки сошлись» для 1.x — из этого расчёта; GET и confirm читают нарушения только из document_issue. Предупреждения маппера 1.x по документу — MAPPING_WARNING info; строки metrics.warnings 1.x — LEGACY_WARNING в recognition_job.metrics (уровень файла, не document_issue); STATEMENTS_VERSION_MISMATCH при statements_version = null не пишется. Цена: ревизия миграции зависит от кода пакета на момент миграции (старая ревизия при смене пакета считает по новым правилам — для разовой миграции приемлемо); отказ от миграционного расчёта дал бы потерю нарушений у существующих форм (K-2). Утверждено владельцем 18.10.2026 (Р-25) в редакции Р-24: миграционного расчёта нет — база перед выкладкой очищается от форм; расчёт при приёме 1.x остаётся (вариант формы — по структуре кодов, S-59).
  12. S-37. Признак строки правки — row_index IS NULL (K-1): маппер 1.x и 2.0 всегда пишет row_index; миграция ставит NULL строкам с line_code_original_value IS NULL; line_code_original_value — код из источника, правками не меняется. Отступление от дизайна («смена кода — прежний в line_code_original_value»): при первой смене это то же значение, а при повторной правке исходный код источника не теряется. Цена: «предыдущий код» после двух правок — только в аудите. Утверждено владельцем 18.10.2026 (Р-25).
  13. S-38. Функция статуса и «содержимое» строки (K-14): вход — данные без типов источника (layout_found, у ядра — есть строка из источника; has_organization_or_inn; period_known; статусы и needs_review; число жёстких нарушений; accepted; уровни предупреждений). Содержимое строки — цифра в ячейке кода, сумма в значениях или непустая ячейка значений с уверенностью ниже 100; строка unreadable без значений («44 30\nI |» aep_6) — строка данных: документ needs_check, подтверждение не блокирует. Цена: такая строка требует от аналитика «Это расшифровка» или кода, хотя сумм в ней нет.
  14. S-39. Столбец кодов ищется всегда, и у DISCLOSURE (K-10): type_hint = DISCLOSURE при кодах ББ/ОФР → тип по кодам + TYPE_CONFLICT; у DISCLOSURE шаги 3–7 не выполняются, columns / lines — пустые массивы. Цена: лишний проход поиска столбца по пояснениям.
  15. S-40. Детектор нумерации (S-13 с оговоркой Р-22, K-15): ≥ 3 непустых ячеек, каждая после замены двойников — целое; возрастают на 1, начинаются с 1 или 2, допускается один пропуск. Цена: настоящая строка данных из трёх маленьких целых подряд («1 | 2 | 3» в суммах) станет нумерацией — на формах ББ/ОФР не встречалась.
  16. S-41. LINES_UNRESOLVED — сумма или текст (K-16): блокирует строка unreadable / missing, у которой хотя бы в одной ячейке amount или string_value не null. Цена: строка с мусором вместо суммы требует разбора. Утверждено владельцем 18.10.2026 (Р-25).
  17. S-42. Подтверждение неуверенной даты отчёта (K-17): PATCH header.report_date тем же значением у поля из needs_review_fields снимает его и REPORT_DATE_UNCERTAIN; у бланка 66н только с относительными подписями это единственный путь — признано в §11.9. Цена: аналитик должен явно подтвердить дату на каждом таком бланке.
  18. S-43. Имена (K-18): 409 LINE_EXISTS → LINE_CODE_TAKEN (имя занято сообщением 422 UNKNOWN_CELL в коде ядра); period_end колонки → period_date во всех документах (как в контракте и DBML); порядок lines в API — «по возрастанию кода, как сейчас», «как на бумаге» — раскладка экрана; DISCLOSURE — пустые массивы. Цена: нулевая.
  19. S-44. Откат миграции (K-19): гарантирован на пустой и совместимой с v5.18 БД; на несовместимых данных downgrade останавливается с ошибкой и ничего не удаляет. Цена: откат после появления данных v2.0 — только восстановлением из резервной копии.
  20. S-45. «Того же периода» (K-20) — та же пара period_year, period_quarter (как автозамена §9.5 п. 3); формы без квартала не считаются (их и так держит PERIOD_REQUIRED). Цена: нулевая.
  21. S-46. Единицы и ОКЕИ (K-22): словарь statement_units.yaml в пакете (написания и коды ОКЕИ 383 / 384 / 385); единицы по тексту, иначе по ОКЕИ; okei (+ _text, _confidence) — в header контракта v2.0 и headerField. Цена: ОКЕИ, противоречащий тексту единиц, не ловится (текст главнее).
  22. S-47. Пробелы PATCH (K-23, S-17): confirmed: false снимает подтверждение ячейки и кода; line_status принимает только "detail" ("ok" — 422 LINE_CODE_REQUIRED); обратного detail → unreadable / missing нет (исправляется назначением кода, исходное — перезагрузкой); columns[].period_date = null — 422 валидации; акцепт сбрасывает правка данных (сумма, код, статус строки, дата колонки, поле шапки), подтверждения (confirmed) — нет. Последнее сужает правило Среза 3b «акцепт сбрасывается любой правкой формы» — вынесено владельцу (отчёт O-n). Цена: если владелец считает подтверждение правкой — одна строка спеки. Утверждено владельцем 18.10.2026 (Р-25): подтверждение без правки акцепт не сбрасывает.
  23. S-48. Страховка done (K-24): done из результата при строке unreadable / missing со значением, needs_review, relations_failed или error-предупреждении документа → NEEDS_CHECK и RESULT_STATUS_INCONSISTENT info. Цена: проверка на каждом маппинге — линейная по строкам. Утверждено владельцем 18.10.2026 (Р-25) в редакции L-12: согласованность — вызов общей функции статуса по входу из результата (не список признаков).
  24. S-49. Словарь имён ядра (Р-20, S-4): повтор кода в редакции разрешён — все имена кода синонимы; шаблонная строка и reference_name берут первое; «итого по разделу» — только равенство. Цена: справочное имя законного повтора на экране — первое имя кода, файловое — рядом.
  25. S-50. Толкование Р-21: мягкое соотношение, сошедшееся в пределах ±1, покрывает значения любого своего кода; у кода в несошедшемся жёстком соотношении подсветка остаётся (правило «все ячейки несошедшегося жёсткого»), поэтому на практике Р-21 действует на строки без жёстких соотношений, как сформулировал владелец. Цена: нулевая.
  26. S-51. Ожидания на реальных сканах признаны в §11.9 и proposal (K-4): большинство из шести OCR-документов останутся needs_check; формулировка гайда фронта смягчена («может стать RECOGNIZED»). Цена: нулевая. Пересмотрено Р-23: значения detail статус не держат, ББ и ОФР РБ и Счастливочной за 1 кв. 2025 могут стать done; держат needs_check aep_3 и aep_6.
  27. S-52. Манифест tabular-parser (K-25): в спеке tabular-recognition-service перечислены записи, получающие 1600 = 1700 (страницы 2 баланса ×3), и записи, чьи статусы done меняться не должны. Цена: нулевая.

Решения при доработке спеки по повторному ревью L-1…L-30 (18.10.2026)

Решения агента при закрытии находок review-2026-10-18-spec-2.md там, где ни ревьюер, ни владелец (Р-23…Р-26) не задали значение, или где агент отступил от предложения ревьюера. Маркер S-n продолжает нумерацию; все обратимы правкой спеки, словаря или DBML до реализации.

  1. S-53. Правило теста идентичности копии контракта (L-2; заменяет S-29): схема копии с $id …/v<M> сверяется с <имя>.v<M>.schema.json docs, если такой файл есть, иначе с основным; пример — с examples/v<M>/<имя>.json, если он есть, иначе с examples/<имя>.json (M — MAJOR копии схемы вида примера, вид — по префиксу имени); прочие файлы копии (.v<N>, examples/v<N>/ у ядра) — с одноимёнными; файлы docs другого MAJOR сервис не сверяет. Правило вводится в шаг 0 у всех трёх потребителей, включая ядро. Основание: существующие тесты (arm-core/api/tests/ test_contract.py, tabular-parser и scan-gateway api/tests/unit/test_contract.py) сравнивают весь набор *.json через rglob, включая examples/; правило «файл, если есть» проходит до слияния docs (файлов .v1 нет), на окне и после, у запроса без .v1. Цена: три маленьких PR до слияния docs; удаление MAJOR 1 — в порядке «сначала ядро убирает копии .v1, затем docs».
  2. S-54. file_ordinal у всех документов (L-3): индекс документа в documents[] результата при любом MAJOR, у ОСВ, форм, DISCLOSURE и OTHER; несозданный документ (второй лист ОСВ с тем же счётом) номер не занимает и других не сдвигает — номера с пропусками. Цена: номер не «порядковый без дыр»; экран его не показывает.
  3. S-55. Предупреждения файла и ОСВ в ядре (L-4): recognition_job.metrics.warnings и JobBrief.warnings — структуры fileWarning; строки старых заданий конвертируются при чтении в LEGACY_WARNING (данные не переписываются); JobBrief.mapping_warnings — строки, как были. Предупреждения листа ОСВ 2.0 — document_issue документа ОСВ, отдаются в новом DocumentOut.issues[]. osvDocument.status используется: ОСВ 2.0 RECOGNIZED ⇔ status = done ∧ checks чисты ∧ нет error файла ∧ контрагент не MISMATCH ∧ нет предупреждений маппинга листа; статус конверта не входит (как Р-19 для форм). Отступление от гайда «поля только добавляются»: тип JobBrief.warnings меняется — записано в гайде фронта. Цена: фронт обязан перейти на объекты в той же выкладке, что и ядро; ОСВ в книге с сомнительной ОФР теперь автоподтверждается (раньше держалась статусом задания).
  4. S-56. Колонки сегмента со своей шапкой (L-6): columns[] задаёт первый сегмент; колонки следующего сегмента со своей шапкой сопоставляются с ними по period_date (если у каждой колонки сегмента дата есть и совпадает ровно с одной), иначе по порядку; при сопоставлении по порядку другое число колонок — SEGMENT_COLUMNS_MISMATCH, лишние столбцы сегмента в columns[] не попадают, их номера — в message. Цена: сегмент с переставленными колонками без дат сопоставится по порядку неверно — §7 это поймает; НПЗ (лист 4 ББ со своей шапкой) остаётся done.
  5. S-57. Явное правило мусора в parse_amount (L-7): «ядро числа» от первого «цифра, (, знак» до последнего «цифра, ), }, ]»; всё вне ядра — краевой мусор, выбрасывается с is_recognized = false; в ядре — только цифры, пробелы, ,, ., знак в начале и пара скобок вокруг числа, иначе amount = null + string_value (буква в ядре, непарная скобка, символ между цифрами); крайние | удаляются до замены двойников; { и [ открывающей скобкой не считаются. Так 211(1 | и (Ю4) дают null, : : 2856983 = ■ — 2856983, {38378) — null (сценарий «знак вне конвенции» переписан на значение без знака). Отступление от сегодняшнего amounts.py (выбрасывал всё, кроме цифр): осознанно. Цена: «12:34» даёт null и требует внимания аналитика; «1 234 руб.» даёт 1234 с is_recognized = false (буквы после последней цифры — краевой мусор, значение нуждается в покрытии §7).
  6. S-58. Дрейф перечня кодов — код 10, не 20 (L-8): sync-statements --check в хосте сверяет копию перечня с docs, расхождение — 10 (предупреждение), а не 20; 20 — только прямая правка копии, чужая версия, недостающая зависимость пакета и (в ядре) код словаря имён вне перечня копии. Порядок правки перечня: docs → arm-statements → хосты. Цена: копия с устаревшим перечнем проходит check-full с предупреждением; видна в описании PR.
  7. S-59. Точка входа пакета для пересчёта (L-10): StoredForm (без сетки) → {form_variant, edition, checks, needs_review строк и значений, родители detail, status}; признаки code_fixed / value_fixed, строки row_index = null участвуют в §7, needs_review_fields — вход (пересчёт шапку не переоценивает). Ядро вызывает её после PATCH и при приёме 1.x; вариант формы при приёме 1.x — по структуре кодов, а не null (уточняет S-36: иначе упрощённая форма 1.x получила бы нарушения полного набора до первой правки, а после правки — другой набор). Цена: у формы 1.x form_variant может отличаться от «полного по умолчанию»; образца упрощённой формы нет (OQ-166).
  8. S-60. «Итого по разделу» — сравнение номеров раздела (L-11): номер — начальные буквы i/v/x первого слова после фразы (хвост OCR «V1», «V /» отбрасывается), strip явно; номера равны ⇔ совпало; номер не выделен — равенство строк. Отступление от S-49 («только равенство»): ломалось на «Итого по разделу V1» Счастливочной. Цена: «II», прочитанное как «I1», даст I ≠ II — ложное NAME_MISMATCH (безопасная сторона).
  9. S-61. «изо :» — пустой код, правило не меняется (L-14): двойники цифрами в ячейке кода без цифр не считаются (иначе слова, затёкшие в столбец кодов, стали бы unreadable и блокировали подтверждение); пример в §11.4 исправлен. Цена: потерянная строка 1130 aep_3 («изо :», '3' с уверенностью 0) уходит в detail и скрыта; её сумма не входит в 1100, и 1100 = … подсвечивает раздел, если сумма больше допуска ±1.
  10. S-62. Родитель detail (L-15): ближайшая строка ok выше (unreadable / missing пропускаются; нет ok выше — строка missing); после каждого PATCH ядро пересчитывает родителей всех строк detail; нет ok выше — родитель null, статус остаётся detail. Цена: строка detail без родителя на экране — в начале раздела.
  11. S-63. Дата колонки руками (L-16, L-17): ручная дата ставит period_kind = absolute; дата, равная текущей, у колонки с COLUMN_DATE_UNCERTAIN / COLUMN_WITHOUT_DATE — подтверждение (аналог S-42), снимает предупреждение и акцепт не сбрасывает. Цена: после ручной даты относительная колонка не следует за правкой даты отчёта — так и нужно.
  12. S-64. Подтверждение дат только уверенными источниками (L-18): для неуверенной подписи колонки эффективная дата — дата отчёта с уверенностью 100, иначе самая поздняя абсолютная дата колонок с подписью уверенности 100; неуверенная дата отчёта подтверждается только датами уверенных подписей. Для периода и относительных подписей эффективная дата — по всем источникам. Цена: скан, где и дата отчёта, и все подписи неуверенны, — всегда needs_check с подтверждением даты руками.
  13. S-65. Структурная проверка кода — уточнения (L-19): «лучшая» подпоследовательность — сначала по числу уверенных кодов (якоря), затем по длине; коды вне перечня редакции в её последовательность не входят; противоречащие якоря — входит наибольшее согласованное их множество; неуверенный код подтверждён, если входит во все лучшие; вариант формы — по всем строкам ok и пересчитывается ядром после правки кода. Цена: при перепутанных уверенных соседях неуверенный код между ними может остаться неподтверждённым — needs_review, безопасная сторона.
  14. S-66. Форма знака (L-20): по результату разбора (скобки, включая } / ]; минус; без знака); сама ячейка в голосование не входит; нужно строгое большинство, равенство — не подтверждён. Цена: колонка с двумя голосующими разной формы не подтверждает знак ни одной сомнительной ячейки.
  15. S-67. Редакция для проверок ядра (L-21): CODE_NOT_IN_EDITION и CODE_NOT_IN_DICTIONARY — по редакции точки входа (лучшая подпоследовательность по всем строкам ok, ничья — по дате); edition ответа API и шаблонные строки — по дате, как сейчас. Цена: на бланке 66н, сданном в 2025 году, шаблонные строки на экране — по fsbu4, а проверки — по 66n; сведение — отдельной задачей при необходимости (O-1 отчёта).
  16. S-68. OTHER удалён из v2.0, form_variant обязателен (L-22): в схеме v2.0 statementDocument.document_type — BALANCE | INCOME | DISCLOSURE, form_variant — в required (null у DISCLOSURE); OTHER остаётся в схеме 1.x и в document_type ядра. Цена: нулевая — производителей OTHER в 2.0 нет.
  17. S-69. Откат миграции — точная формулировка (L-24; уточняет S-44): downgrade останавливается только на данных, которые v5.18 не может представить без потери первичных данных и ручного ввода (повтор кода, строка без кода, две формы одного типа в файле, непустой crm_id групп, правленое наименование организации); производные и сигнальные колонки v5.19 и document_issue удаляет с записью числа удалённых значений в журнал миграции (подтверждения — в аудите). Цена: откат после работы аналитиков теряет подтверждения ячеек и кодов как признаки (история — в аудите).
  18. S-70. Механика копии пакета — уточнения (L-25): VERSION из сравнения исключён (в источнике его нет); зависимости пакета из его pyproject обязаны быть в зависимостях хоста (иначе --check = 20); пакет проходит самый строгий конфиг ruff / mypy среди хостов; тесты пакета со своим conftest, autouse-фикстуры хоста ограничены его каталогом tests/. Цена: пакет живёт по самым строгим правилам всех хостов.
  19. S-71. Содержимое таблицы ИС и TABLE_SKIPPED (L-27): содержимое — хотя бы одна ячейка с цифрой или «цифрой под видом буквы»; таблица только из текста без столбца кодов и типа — новое предупреждение файла TABLE_SKIPPED (info), а не TABLE_LOST (error). Цена: текстовая таблица ИС не блокирует автоподтверждение; если в ней была форма без единой цифры — такой формы не бывает.
  20. S-72. «Цифра под видом буквы» — содержимое строки (L-30): ячейка значений, текст которой после замены двойников без условия соседства содержит цифру и не содержит других букв ('I', '| О'), делает строку строкой данных; текст из настоящих букв при уверенности 100 (подписи под формой Excel: «Руководитель … Косарев», «(расшифровка подписи)» у Межениновской) — нет: иначе каждая книга получала бы мусорные строки detail. Признано осознанным исключением из «ничего не выбрасывать молча». Цена: текстовая строка без цифр с уверенностью 100 по-прежнему теряется молча.
  21. S-73. Очистка форм перед выкладкой — детали (Р-24): скрипт ядра scripts/purge_statements.py, --dry-run по умолчанию, --apply одной транзакцией; отказ без изменений, если форма упомянута в ex_snapshot_item или привязана к версии не «в работе»; файлы только с формами удаляются целиком (как DELETE /files/{id}, без запрета FILE_HAS_CONFIRMED_DOCUMENTS, задания — тоже); в смешанных файлах «ОСВ + формы» удаляются только формы, файл остаётся — повторно его не загрузить (дедуп sha256), поэтому --dry-run перечисляет их и владелец решает про --include-mixed; системный аудит FILE_DELETE. Цена: подтверждённые формы и их привязки к версиям «в работе» исчезают до повторной загрузки; КФО этих версий пересчитывается.
  22. S-74. needs_review значений после выхода строки из detail (Р-23): строка, которой назначили код, получает needs_review значений по общему правилу (покрытие §7); строка, отмеченная detail, — false; значения unreadable / missing считаются по общему правилу (документ и так needs_check по статусу строки). Цена: нулевая.

Решения при планировании и исполнении плана 0 (18.10.2026)

  1. P0-1. Правило теста идентичности — копией в трёх репозиториях: правило живёт файлом api/tests/contract_identity.py + сценарный тест, побайтно одинаковыми в arm-core, tabular-parser и scan-gateway, а не общим пакетом и не скриптом docs; эталон — копия arm-core, расхождение ловит shasum (план 0, задача 5) и ревью; соседние репозитории в make check не читаются. Цена: правка правила — три PR; разошедшиеся копии до сверки не видны.
  2. P0-2. «Копия держит MAJOR N»: в копии есть хотя бы один файл *.v<N>.schema.json или examples/v<N>/*.json; тогда все такие файлы docs обязательны в копии. Цена: ядро, положившее один файл .v1, обязано положить все.
  3. P0-3. Неопределимая пара — ошибка: пример с неизвестным префиксом, схема без $id вида …/v<M>, пример вида, схемы которого нет в копии — ошибка теста с именем файла, а не пропуск. Цена: новый вид примера в docs требует правки правила.
  4. P0-4. ARM_KA_DOCS в scan-gateway: тест идентичности scan-gateway читает ARM_KA_DOCS (спека называет переменную); поиск fixtures не меняется. Цена: расхождение в dev/checks.md сужается до fixtures.
  5. P0-5. OpenSpec change contract-identity-window в tabular-parser и scan-gateway: MODIFIED требования «Копия контракта идентична источнику» (прежний текст «байт в байт равны ../docs/contracts/» на окне неверен); arm-core change не получает — правило описано в спеке docs recognition-contract. Цена: у arm-core при закрытии нет openspec archive.
  6. R-1 (план 0). Дорожку плана 0 исполняет фоновый агент на sonnet: а не сама сессия: сессия координирует четыре параллельные дорожки (планы 0, 1, 3А, 6). Метод — executing-plans (Native, ≤5 задач), ревью всей ветки — opus. Цена: слабее контекст сессии при финальных рулингах; страхует ревью и таблица находок.
  7. P1-1. Конфиг линтеров пакета — только в корневом pyproject.toml репозитория arm-statements (в дерево arm_statements/ не входит и в хост не копируется): ruff.toml внутри arm_statements/ по иерархии ruff перекрыл бы конфиг хоста, а спека требует «конфиг хоста применяется к копии». Код пакета написан под самый строгий конфиг, так что он проходит любой хост. Цена: «конфиг линтеров вместе с копией» (формулировка спеки) читается как «код, проходящий конфиг», а не как файл; см. O-1.
  8. P1-2. Версии инструментов пакета — пины scan-gateway (ruff==0.16.10, mypy==2.4.0, pytest==9.1.1): это единственный хост с явными пинами; у tabular-parser и arm-core >=, в их окружениях встречаются ruff 0.16.10–0.17.1. Цена: новая версия ruff в хосте может найти новое замечание в копии; см. O-2.
  9. P1-3. PLR2004 в тестах пакета не отключается: per-file-ignores хостов (tests/** у scan-gateway) на копию api/arm_statements/tests/ не распространяются. Цена: числа в тестах — кортежами и D("…").
  10. P1-4. Ничья редакций при effective = None — edition = None, неуверенный код подтверждён только при подтверждении в обеих редакциях. Цена: лишняя подсветка кода у формы без даты (она и так needs_check — PERIOD_UNKNOWN).
  11. P1-5. «Книга xlsb без числовых форматов» шаг 4 узнаёт по предупреждению адаптера XLSB_NO_FORMATS в FormCandidate.warnings — отдельного поля у кандидата спека не задаёт. Цена: адаптер Excel обязан класть это предупреждение в кандидат каждой формы книги xlsb.
  12. P1-6. У DISCLOSURE предупреждения шапки не выдаются, статус всегда done (спека: «DISCLOSURE — done без условий»). Цена: пустая шапка пояснений не видна предупреждением.
  13. P1-7. Код Excel из текстовой ячейки с заменой двойника — неуверенный (≤ 99) и проходит структурную проверку, как OCR; «у Excel needs_review по структуре не ставится» прочитано как «у чистых кодов Excel» (уверенность 100). Цена: редкий текстовый код «I1О0» в Excel может подсветиться.
  14. P1-8. Строка правки (row_index = None) у ядра участвует в §7 и в выборе варианта, только если у неё есть сумма; в структурной последовательности и как родитель detail — нет (позиции в файле нет). Цена: код правки не служит якорем для подтверждения соседних неуверенных кодов.
  15. P1-9. Двухстрочный заголовок склеивается, когда подпись верхней строки отдельно — не период, а «верхняя + нижняя» — период («На 30 сентября» + «2025 г.»; нижняя «2025 г.» сама по себе дала бы ошибочное 31.12.2025). Цена: заголовок в два ряда без даты не склеивается (колонка всё равно остаётся по числам); если над строкой заголовка стоит посторонний текст, который вместе с нижней подписью случайно читается как период, подпись склеится.
  16. P1-10. Текстовая сумма без пробелов длиннее 16 целых цифр — amount = null, string_value = текст, is_recognized = false, без VALUE_OUT_OF_RANGE (предупреждение — только для числового value Excel, как в спеке).
  17. P1-11. COLUMN_DATE_UNCERTAIN проверяется только у абсолютных подписей: относительные даты вычисляются от эффективной даты и в ожидаемые входят по построению.
  18. P1-12. Поля шапки, правила которых спека не задаёт: okpo, okopf, okfs — только цифры; okud — первое совпадение 0710\d{3}; organization_name, address — текст без пробелов по краям.
  19. P1-13. В словарь пакета statement_headers.yaml переезжают и метки шапки Excel (labels, auditor, legal_form) — proposal: «statement_headers.yaml переезжает в пакет»; у tabular-parser не остаётся своего словаря форм. Цена: в общем словаре есть данные, которыми пользуется один адаптер.
  20. P1-14. «Под подписью» в правиле (а) столбца кодов — ячейки столбца ниже подписи до конца сегмента.
  21. P1-15. FailedRelation несёт и column_index, и column_key — чтобы ядро получало нарушения в своих ключах колонок без обратного сопоставления.
  22. P1-16. Перечень кодов копируется из ветки docs feat/116-unified-normalization до слияния docs PR #117.
  23. P1-17. Столбец правее кода без подписи, но с числами, в сегменте с заголовком — колонка с label = "", period_kind = none и COLUMN_WITHOUT_DATE (error): «числовая колонка без разобранной подписи» по таблице уровней.
  24. P1-18. Заголовок есть у одного из сегментов, но не у первого — колонки документа задаёт первый сегмент с заголовком (спека: «остальные сегменты берут его колонки»), column_index — номера столбцов этого сегмента.
  25. P1-19. Документ BALANCE / INCOME без строк (CODE_COLUMN_NOT_FOUND) получает form_variant = full: в схеме v2.0 form_variant обязателен и null только у DISCLOSURE.
  26. P1-20. При accepted (у ядра) правило «needs_review у всех ячеек несошедшегося жёсткого соотношения» не применяется: иначе акцепт снимал бы нарушение со статуса, а подсвеченные им ячейки держали бы needs_check через условие «нет значений с needs_review» (спека: «accepted ⇒ жёсткие нарушения статус не держат»). У сервиса accepted всегда ложно. Цена: после акцепта ячейки несошедшегося соотношения не подсвечиваются (нарушение видно в checks).
  27. P1-21. FailedRelation.expected / actual (и RelationOutcome) — Decimal | None: None — цель или все члены пусты; такое посчитанное соотношение — всегда нарушение (допуск не применяется). Контракт v2.0 так и задаёт (expected — money, «null — цели нет»; actual — money), агрегат ядра тоже не превращает пустое в 0 (план 3, Review Focus 2). Цена: хосты и ядро обрабатывают None в тексте нарушения («пусто»); при ошибке — 0 вместо пустоты прятал бы «пустую цель» в допуск ±1 (1600 = 0 при пустом 1700 считалось бы сошедшимся).
  28. P1-22. Две публичные нормализации текста: normalize_label (дословно normalize_text tabular-parser — пунктуация сохраняется; под неё записаны перенесённые из tabular-parser метки шапки, заголовки, ОКУД, маркер продолжения) и normalize_caption (подписи колонок, единицы, относительные периоды). Таблица «ключ словаря → нормализация» — в разделе API; type_hint и is_continuation пакета пользуются normalize_label. Цена: новый синоним пишется под нормализацию своего ключа; ошибка — синоним молча не срабатывает (ловят тесты пакета на синтетике и покрытие словарей в tabular-parser, O-8 плана 4).
  29. P1-23. Новый код XLS_NO_CODEPAGE (info, файл): книга xls без кодовой страницы — заметка читателя, относящаяся ко всей книге (и к формам, и к ОСВ). Строки добавлены в таблицу спеки «Шаг 8 — предупреждения» и §11.9 05-domain-rules.md (правило спеки: «новый код вводится словарём пакета и таблицей §11»). Цена: tabular-parser выдаёт его в metrics.warnings вместо OSV_XLS_NO_CODEPAGE в каждом документе ОСВ (план 4, P4-13); экран показывает его в карточке файла, а не у листа ОСВ.
  30. P1-24. resolve_relative(ABSOLUTE | NONE, …) → None, а не исключение: у таких колонок дата не вычисляется, а ядро перебирает колонки формы одним циклом. Цена: вызов с неверным видом не заметен (ядро зовёт функцию только для relative:*, план 3 B2).
  31. P1-25. choose_edition(document_type, codes: Sequence[tuple[str, bool]], effective, rules, *, variant=None) вместо запрошенной choose_edition(effective, codes, rules): перечень кодов зависит от формы и варианта, а «лучшая подпоследовательность» по спеке сравнивает сначала число уверенных кодов — без типа и признака уверенности результат расходился бы с recompute. confirm_codes и recompute берут редакцию этой же функцией. Цена: ядро передаёт пары (код, уверенный), собранные из строк результата (line_code_confidence == 100).
  32. P1-26. Перевод выхода в форму контракта v2.0 (to_contract, warning_to_contract) — в пакете, а не в каждом сервисе: одно уплощение шапки, одно правило «необязательные ключи без null», один отброс column_key. Пакет теперь знает форму statementDocument v2.0. Цена: новый MAJOR контракта меняет и пакет (синхронизация копий — план 2), а модель контракта хоста валидирует dict пакета — расхождение видно тестом схемы хоста, а не пакета.
  33. P1-27. Статус документа — функция документа: status_input(StatementDocument) (конвейер считает им status) и status_input_from_contract(dict) для ядра (страховка document.status результата 2.0); свойство status_input_from_contract(to_contract(d)) == status_input(d) закреплено тестами задач 19 и 20. layout_found выводится из «строки есть» (строк нет ⇔ столбец кодов не найден, задача 17 п. 2). Цена: если в будущем появится документ со столбцом кодов, но без строк, функции нужен явный признак раскладки.
  34. P1-28. code_column(rows, rules, *, relaxed) и amount_columns(rows, code_col, rules, *, source) — публичные обёртки шага 2 над сеткой Cell (адаптер Excel строит её из листа); минимум ослабленного поиска и слабого продолжения — данные словаря (min_codes_continuation: 1, был литерал «минимум один» в коде шага 2 и min_codes_continuation tabular-parser). relaxed задаётся типом формы, а не внутренним CodeSection. Цена: адаптеру нужна сетка Cell листа, чтобы спросить столбец кодов (у tabular-parser она уже есть — grid_rows плана 4).
  35. P1-29. form_name — поле StatementHeader (в контракте он в header), а не StatementDocument; ставит конвейер из evidence.title.text как есть, parse_header оставляет None. Цена: хост, собирающий шапку parse_header сам (без конвейера), заголовка не получит — таких вызовов в планах нет.
  36. P1-30. Коды предупреждений — WarningCode (StrEnum) в issues.py; загрузчик требует равенства множества кодов словаря и перечисления; make_warning с неизвестным кодом — KeyError. Цена: новый код — две правки в пакете (словарь + член перечисления) и строка §11.9 docs; опечатку в коде хоста ловит mypy (член перечисления), а строковый литерал — только KeyError при вызове.
  37. P1-31. Rules.code_orders.sequence(…, DISCLOSURE, …) → (), а не KeyError: у пояснений перечня нет, и структурная проверка, и проверки ядра по редакции к ним не применяются. Цена: вызов для пояснений молча даёт пустой перечень.
  38. R-1 (план 1). Дорожка плана 1 — фоновый SDD-контроллер на sonnet: дорожки волны 1 (планы 0, 1, 3А, 6) идут параллельно четырьмя фоновыми агентами; исполнители задач — sonnet, финальное ревью ветки — opus. Цена: слабее контекст сессии; страхуют пошаговые ревью и таблица находок.
  39. R-2 (план 1). Р-34 и Р-43 учтены в брифах: подтверждение кода — флаг code_fixed (is_confirmed), уверенность не меняется; arm-statements внесён в таблицу workspace (CLAUDE.md workspace, context/00-workspace-layout.md, коммит e6eef27 ветки feat/116). Цена: нет.
  40. R-3 (план 1). parse_code: краевая | — как в спеке: | → 1, если результат в кодовом диапазоне (строка 178 spec.md), допустимы 4–5 цифр: «1220|» → 12201, «|1220» → 11220 с fixed=True (подсвечивается). Не менялось: буквальное чтение спеки. Цена: код с краевой чертой OCR становится пятизначным с подсветкой; вопрос владельцу спеки (кандидат в follow-up). Ledger I-2 (T4).
  41. R-4 (план 1). Тест плана test_sublines исправлен по спеке: тест плана (1230 = 10 против 4 + 5 = 9) противоречил допуску ±1 мягких соотношений (расхождение 1 — в допуске); оставлено поведение по спеке, тест заменён парой (4+4 → нарушение, 4+5 → нет). Цена: если для подстрок хотели строже — допуск подстрок 0 вместо ±1.
  42. R-5 (план 1). Неуверенный код вне перечня редакции не подтверждается структурой: спека явного правила не даёт; по общему «подтверждён ⇔ входит во все лучшие подпоследовательности», а код вне перечня в последовательность не входит. Цена: неуверенный нестандартный код из OCR получает needs_review (безопасная сторона).
  43. R-6 (план 1). История ветки feat/1-arm-statements-package переписана git filter-branch --msg-filter: один коммит имел трейлер «Claude Sonnet 5.5» вместо требуемого «Claude Opus 5.5»; ветка не публиковалась. Цена: SHA коммитов в ledger/отчётах до переписи устарели.
  44. R-7 (план 1). Решения исполнителей задач при пробелах спеки: T6.2 (смешанные/повторные разделители суммы → цифры подряд ≤ 16 и is_recognized=false; excel допускает ≥ 3 знака после разделителя); T10.1–T10.4 (SEGMENT_WITHOUT_CODES только если столбец кодов найден хотя бы у одного сегмента; ослабленный поиск включается, когда строгий не нашёл и отрезок известен; сегмент двухстрочной повторной шапки открывается строкой с подписью кода; сопоставление по датам требует разных целевых колонок); T11.2 (сегмент без столбца кодов — наименование слева, значения по value_cols или правее наименования); T14.3–T14.4 (ничья без даты — edition=None, подтверждено в обеих; родитель пятизначного — ближайшая выше строка с нужным кодом, уверенная или подтверждённая); T15.2 («строгое большинство» знака = более 50 %). Ревью задач: со спекой не расходятся. Цена: перечисленные трактовки при пробелах спеки; пересмотр — правкой кода пакета и словарей.
  45. R-8 (план 1). Расширение публичного API сверх раздела плана: в __all__ добавлены Rules (нужен хостам в сигнатурах) и RulesError (обработка load_rules); абзац про них — в README пакета. Цена: хосты могут зависеть от двух лишних имён.

Решения плана 2 (sync-statements), 18.10.2026 (дорожка плана 2, ветка docs feat/120-sync-statements, arm-ka-docs#120; решения P2-n — автор плана, R-n — контроллер):

  1. P2-1 (O-1 плана 1). Файл конфига линтеров в копию не кладётся; «конфиг пакета подключён к хосту» — это: ruff хоста уже совпадает с пакетом (line-length = 120, py311, E F I UP B PL), в isort known-first-party добавлен arm_statements; в mypy хоста модули arm_statements.* входят в строгий блок (те же флаги, что strict = true пакета). Цена: блок флагов повторён в трёх хостах; если хост когда-нибудь станет строже пакета, копия покраснеет в хосте — чинить в arm-statements.
  2. P2-2 (O-2). Пины ruff==0.16.10, mypy==2.4.0, types-PyYAML==6.0.12.20260906 во всех хостах (у tabular-parser и arm-core были >=; в их окружениях уже стоят эти версии — пины ничего не переформатируют). pytest не закрепляется: тесты пакета не зависят от его версии. Цена: обновление ruff/mypy — согласованно, сначала пакет.
  3. P2-3 (O-3). testpaths = tests arm_statements/tests в pytest.ini хостов; conftest хостов не меняется: у них нет api/conftest.py, а tests/conftest.py действует только на tests/; env, asyncio_mode, --strict-markers тестам пакета не мешают. Проверка — равное число собранных тестов пакета в хосте и в arm-statements.
  4. P2-4 (O-4). Dockerfile хостов не меняется: COPY api/ ./ уже кладёт api/arm_statements/rules/*.yaml, .dockerignore их не исключает; платформа уже возит api/rules/*.yaml и api/contracts/ (сервисы читают их с диска в контуре). Проверка — запуск default_rules() в собранном образе (задачи 7–9).
  5. P2-5. Подключение хостов — три bounded PR этого плана (задачи 7–9), а не части планов 3/4/5: правки одинаковые и механические, план 3 уже ждёт «PR плана 2 в arm-core», первая раскладка проверяет скрипт на настоящих хостах, а планы 4/5 стартуют от main с копией. Цена: ещё три PR и копия в main хостов до того, как её вызывает код сервиса.
  6. P2-6. Режим --in-place (раскладка в существующий worktree без issue, коммита и PR) — для подключения хоста и для PR фичи, которой нужна новая версия пакета; спека дополнена (задача 6). Цена: второй путь раскладки, мимо отдельного PR синхронизации.
  7. P2-7. Скрипт — bash по образцу sync-web.sh (spec называет sync-statements.sh), разбор TOML/YAML — помощник statements_check.py (Python 3.11 + PyYAML); интерпретатор — STATEMENTS_PY → docs/.venv → api/.venv хоста → python3; подходящего нет — код 2. Цена: make venv docs ставит PyYAML (как и в PR #117).
  8. P2-8. В --check отсутствие копии — 20 («копии нет»): хост, в котором вызывают проверку, обязан держать копию. В обычном режиме хост без копии — 2 с указанием на --in-place: подключение хоста — ручная задача с правкой конфигов.
  9. P2-9. Нет contracts/statement-code-orders.yaml в docs — 10 (docs старше пакета; так до слияния PR #117).
  10. P2-10. Зависимости сверяются по имени (PEP 503) с api/requirements.txt хоста (его ставит образ); разные версии — примечание, код не меняют. Цена: несовпадение версии PyYAML ловят только тесты пакета в хосте.
  11. P2-11. «Копия отстала» — буквально по спеке: VERSION ≠ --ref, даже если новые коммиты источника не трогают arm_statements/ (README, Makefile). Цена: лишние предупреждения 10 после таких коммитов (см. O-1).
  12. P2-12. Ошибка fetch в --check не фатальна (предупреждение, сравнение с локальными ссылками): проверка перед PR не должна краснеть без сети, если копия на месте.
  13. P2-13. Сравнение — по файлам рабочего дерева (diff -rq), без VERSION и кэшей (__pycache__, *.pyc, .pytest_cache, .mypy_cache, .ruff_cache, .DS_Store); биты режима не сравниваются. Незакоммиченная правка — тоже 20.
  14. P2-14. Обычный режим перед коммитом гоняет make venv и make check хоста в новом worktree (как sync-web.sh — проверки фронта). Цена: минута на окружение.
  15. P2-15. «Сначала ядро» при синхронизации сервиса — предупреждение (в выводе и в описании PR), не отказ: порядок важен для смены формата результата, а не для каждой правки пакета.
  16. P2-16. make -C docs statements-check — обзор по трём хостам рядом (0/10 — зелёные); в make check docs не входит: читает соседние репозитории.
  17. P2-17. Имя хоста — --host или последний компонент origin (arm-core, arm-tabular-parser, arm-scan-gateway), иначе имя каталога; сверка словаря имён — только у arm-core.
  18. P2-18. Раскладка отказывает (код 1), если файлы копии игнорируются .gitignore хоста или содержат то, на чём остановится снимок для контура (/Users/, /home/…, git.aepif.ru, uv venv, uv pip).
  19. P2-19. Строки make venv docs — дословно как в ветке feat/116-unified-normalization, строки таблицы dev/checks.md, которые меняет PR #117, не трогаются: два PR сливаются в любом порядке без конфликта.
  20. P2-20. В CLAUDE.md каждого хоста — правило «копию не править, обновлять скриптом»: агенты читают его первым. Шаблоны docs/repo-templates/ не меняются (разошлись с репозиториями давно).
  21. P2-21. Ветка docs плана — от origin/main (шаг 1 выкладки раньше PR #117); спека, design.md и раздел «Копия» dev/checks.md правятся в feat/116-unified-normalization (задача 6).
  22. R-1 (план 2). make check в docs красный по validate-openspec: в Makefile CHANGE ?= slice-0-skeleton, change в архиве (O-1 плана 0); план 2 это не чинит. Цели проверялись по отдельности (validate-contracts, test-snapshot, test-sync-web, test-sync-statements — зелёные), в PR красный validate-openspec назван. Цена: make check остаётся красным до отдельной правки Makefile.
  23. R-2 (план 2). Р-42 исполнен в этом плане (задача 5b): prepare-contour-snapshot.sh для каждого из трёх сервисов запускает sync-statements.sh --check по дереву api коммита $REF (git archive во временный каталог, --host <имя>): 0 — тихо; 10 — предупреждение и абзац в SNAPSHOT.md; 20 — стоп, код 1; 2 (нет источника или интерпретатора) — предупреждение, снимок делается; если в $REF нет api/arm_statements — проверка пропускается (пакет в хост ещё не подключён: иначе 20 «копии нет» блокировал бы снимок до задач 7–9). Цена: пока хост без копии, снимок о ней не напомнит; тест снимка требует Python ≥ 3.11 с PyYAML (STATEMENTS_PY=$(PY) в test-snapshot).
  24. R-3 (план 2). Спека: «и его конфигом линтеров» убрано: предложение спеки о копии «вместе с … конфигом линтеров» противоречило P2-1 (конфиг пакета в копию не кладётся, применяется конфиг хоста); исправлена спека и раздел «Копия общего кода» dev/checks.md. Цена: нет.
  25. R-4 (план 2). Защиты раскладки липкие, копия при отказе защиты остаётся в дереве: игнорируемые .gitignore файлы и пути контура (P2-18) проверяются и после раскладки, и при повторе --in-place с той же версией («раскладывать нечего» код 1, если копию нельзя коммитить); при коде 1 копия остаётся в дереве хоста, об этом сказано в сообщении. После make check хоста в обычном режиме evaluate повторяется (форматтеры могли переписать копию) — 20 блокирует коммит. Цена: после отказа защиты оператор сам убирает копию (git checkout/clean).
  26. R-5 (план 2). --in-place требует --host-dir и не работает на основном checkout и ветке main/master (финальное ревью ветки docs, I-1): --in-place --host <имя> разворачивался в основной checkout на main и стирал там api/arm_statements; теперь код 2. Цена: --in-place только с --host-dir внутри worktree фичи.

Решения при исполнении (18.10.2026)

Перенос из design.md change'ов исполнения (разделы «Decisions made without discussion»): текст решений, обоснований и цен ошибки — как в источнике, без сокращений; маркеры источника сохранены дословно, нумерация docs продолжает общую (по подразделам ниже — по дорожкам). Номер в начале пункта — номер в этом файле. Решения владельца (Р-n) — в разделе «Решения владельца» выше; решения плана 0–2 и Р-44 перенесены раньше.

План 3, часть А (arm-core, change unified-normalization-core-a)

Источник: openspec/changes/unified-normalization-core-a/design.md в репозитории arm-core, коммит c2ee31d (ветка исполнения; полный текст — там). Перенесено пунктов: 107.

  1. P3-1 (план 3А): Миграция пишет header_source с ключами inn, report_date, units, organization_name: text — из inn_original_value, report_date_original_value, units_original_value, organization_name; confidence — inn_confidence у inn, у остальных null. Обоснование: спека: «из *_original_value и inn_confidence»; прочих полей в v5.18 сырыми нет. Цена ошибки: подсказка у поля старой формы беднее; формы старых времён на контуре удалены очисткой.
  2. P3-2 (план 3А): organization_name_modified — false у всех форм (миграция и оба маппера), не NULL. Обоснование: как inn_modified в маппере 1.x; API отдаёт bool. Цена ошибки: одна строка UPDATE.
  3. P3-3 (план 3А): needs_review_fields: маппер 1.x пишет [], миграция оставляет NULL, чтение NULL = []. Обоснование: спека миграции поле не заполняет. Цена ошибки: нет.
  4. P3-4 (план 3А): Маппер 1.x тоже хранит вторую форму того же типа (Р-10); формы одного типа и одной пары period_year, period_quarter получают MAPPING_WARNING «в файле есть другая форма <тип> за тот же период» и по D-6 — NEEDS_CHECK (аналог Р-18). Обоснование: частичный индекс v5.19 больше не заставляет пропускать; одно правило хранения для обоих MAJOR. Цена ошибки: O-1: если владелец хочет прежний пропуск — вернуть ветку continue.
  5. P3-5 (план 3А): Предупреждения маппинга листа ОСВ (повтор счёта, строки без родителя, нарастающая по данным Q1) пишутся ещё и в document_issue документа листа (warning, MAPPING_WARNING, info, core) при любом MAJOR; строки в metrics.mapping_warnings остаются. Обоснование: спека: DocumentOut.issues ОСВ — «вместе с предупреждениями маппинга ядра этого листа». Цена ошибки: нет.
  6. P3-6 (план 3А): file_ordinal объединённого документа DISCLOSURE / OTHER — индекс первого такого документа в documents[]. Обоснование: несозданный документ «номер не занимает». Цена ошибки: нет.
  7. P3-7 (план 3А): Переопределено Р-40 (владелец, 18.10.2026): CODE_NOT_IN_DICTIONARY / CODE_NOT_IN_EDITION пишутся только для четырёхзначных кодов (пятизначные подстроки Excel не дают info; закрывает O-3). Остальное — как ниже. CODE_NOT_IN_DICTIONARY и CODE_NOT_IN_EDITION — одна запись на код документа (line_ids — все строки ok с кодом), пятизначные коды тоже проверяются; при приёме 1.x проверки ядра уровня info не пишутся. Обоснование: спека для 1.x перечисляет только §7 и MAPPING_WARNING. Цена ошибки: O-3 (поток info у пятизначных).
  8. P3-8 (план 3А): После правки записи core CODE_NOT_IN_DICTIONARY, CODE_NOT_IN_EDITION, NAME_MISMATCH и STATEMENTS_VERSION_MISMATCH пересчитываются (заменяются); NAME_MISMATCH не ставится строке с code_fixed; MAPPING_WARNING, RESULT_STATUS_INCONSISTENT, LEGACY_WARNING сохраняются. Обоснование: код строки меняется правкой; страховка статуса — факт приёма. Цена ошибки: O-4.
  9. P3-9 (план 3А): Результат 2.0: значение с column_index вне колонок формы и повтор column_index колонки — первое остаётся, MAPPING_WARNING. Обоснование: FK значения на колонку; уникальность (meta_id, column_index). Цена ошибки: нет.
  10. P3-10 (план 3А): Части PATCH применяются в порядке lines → values → columns → header; вся адресация (422) проверяется до первой записи; UNKNOWN_CELL по line_code — по кодам строк ok с учётом назначений lines этого запроса; один line_id дважды в lines — 422 INVALID_LINE_PATCH. Обоснование: атомарность и предсказуемость. Цена ошибки: нет.
  11. P3-11 (план 3А): reference_name — первое имя кода в редакции ответа (edition по дате); нет редакции или кода — null. Обоснование: спека: «первое имя кода в словаре»; edition ответа — по дате. Цена ошибки: нет (Р-29).
  12. P3-12 (план 3А): (= Р-35, решение владельца.) Очистка отказывает, только если документ формы упомянут в ex_snapshot_item или привязан к версии в статусе «Верифицирована» или дальше: VERIFIED или ARCHIVED с ex_snapshot.source_version_id = версия; привязки к версиям в работе, на согласовании, REJECTED, CANCELLED (и архивированным отменённым) снимаются, статус версий не меняется. Обоснование: решение владельца 18.10.2026 (Р-35): верифицированная версия read-only и заморожена в ЕХ, прочие — нет; на стенде версий дальше «в работе» нет. Цена ошибки: архивированная версия без снимка ЕХ (из CANCELLED) очистке не мешает — если бы мешала, владелец увидел бы лишний отказ.
  13. P3-13 (план 3А): Коды выхода purge_statements.py: 0 — отчёт или выполнено, 1 — отказ (п. 2 спеки), 2 — ошибка аргументов. Обоснование: как у check_dbml_vs_orm.py. Цена ошибки: нет.
  14. P3-14 (план 3А): До части Б §7 считает ядро на лету (statement_checks.yaml) по агрегату; checks.failed[].codes берутся из правила, line_ids — по агрегату. Обоснование: часть А должна быть самосогласованной. Цена ошибки: временный код удаляется в B6.
  15. P3-15 (план 3А): Для условия RECOGNIZED 2.0 предупреждения файла берутся из metrics.warnings файла результата; в recognition_job.metrics.warnings — из конверта (inbox). Обоснование: маппер читает файл результата; по контракту наборы одинаковы. Цена ошибки: нет.
  16. P3-16 (план 3А): (= Р-34, решение владельца.) Подтверждение кода (lines[] {confirmed: true}) ставит is_confirmed = true и needs_review = false, line_code_confidence и line_code_modified не меняет; назначение кода — confidence = 100, line_code_modified = true. Обоснование: иначе исходная уверенность теряется, а снятие подтверждения не может вернуть needs_review (гайд §5.6.1: «при false — пересчитывается»); аналогия с ячейкой (is_modified не ставится); подтверждено владельцем 18.10.2026 (Р-34), спека arm-core, §9.5 п. 9 и гайд §5.6.1 приведены к этому. Цена ошибки: нет.
  17. P3-17 (план 3А): Порядок строк ответа: строки ok (с шаблонными) — по коду, затем row_index (NULL последними), затем id; за каждой строкой ok источника — её «вложения» по row_index: строки detail с этим родителем и строки unreadable / missing / detail без родителя, у которых она — ближайшая предыдущая по row_index строка ok источника; вложения без такой строки — в начале. Обоснование: спека «Ответ GET …/statement». Цена ошибки: нет.
  18. P3-18 (план 3А): Уточнено Р-41 (владелец): смена crm_id пишется в аудит как изменение группы (GROUP_CHANGE) — закрывает O-6. Изменение crm_id ГСК/ГСЗ пишет одно событие GROUP_CHANGE с before/after всех изменённых полей вместо MEMBERSHIP_CHANGE. Обоснование: спека: «изменение — аудит GROUP_CHANGE». Цена ошибки: O-6.
  19. P3-19 (план 3А): Ядро зовёт у пакета choose_edition (engine.edition) и status_input_from_contract (engine.contract_document_status) вместо сборки StoredForm ради редакции и своего status_input_from_v2; to_contract ядру не нужен. Обоснование: одно правило с сервисом и с recompute (план 1, P1-25, P1-27); пары (код, уверенный) — из строк ok результата по row_index. Цена ошибки: при ошибке сборки пар редакция приёма разойдётся с редакцией после правки — тест B1 сравнивает их.
  20. P3-20 (план 3А): header.source.<поле>.confidence — строка Decimal с двумя знаками ("61.54"), как прочие уверенности API; в JSONB header_source остаётся число как пришло. Обоснование: фронт читает все уверенности одним способом (план 6, A8). Цена ошибки: округление до сотых в подсказке; исходное число — в БД.
  21. P3-21 (план 3А): StatementHeaderOut.okei — из financial_statement_meta.okei (колонка есть с v5.18), только чтение; в PATCH header не входит. Обоснование: спека header контракта несёт okei, экран показывает issues с field = "okei" у поля. Цена ошибки: нет.
  22. P3-22 (план 3А): Справочник строк для подсказки кода — поле StatementOut.edition_lines [{code, name}], а не отдельный эндпоинт: данные уже загружены (LineCatalog, A6) при сборке ответа, нет нового маршрута, права, кэша и запроса на фронте; около 80 пар на форму (≈ 4 КБ) против строк формы с значениями; у формы без даты — коды всех редакций, по одному. Обоснование: наименьший объём реализации в ядре и во фронте; закрывает пробел «у формы без даты подсказки нет» (план 6, O-3). Цена ошибки: ответ GET тяжелее на ≈ 4 КБ; если понадобится справочник вне формы — отдельный эндпоинт поверх того же LineCatalog.edition_lines.
  23. P3-23 (план 3А): DocumentBrief.issue_counts {error, info} — все записи document_issue документа (и relation / soft, в том числе акцептованные) по severity; один групповой запрос на файл. Обоснование: панели файлов нужен признак «есть что смотреть» без запроса на каждый документ (план 6, O-4). Цена ошибки: акцептованное нарушение продолжает помечать документ в панели — подробности на экране документа.
  24. P3-24 (план 3А): Ошибки элемента lines[] (409 LINE_CODE_TAKEN, 422 LINE_CODE_REQUIRED, INVALID_LINE_CODE, INVALID_LINE_PATCH, UNKNOWN_LINE) несут в detail line_id и line_code первого неверного элемента; LINE_CODE_REQUIRED добавлен к четырём кодам запроса фронта. Обоснование: экран ставит ошибку у поля нужной строки, не угадывая (план 6, O-2); одно правило для всех ошибок элемента. Цена ошибки: при нескольких неверных элементах фронт видит только первый — как и прежде.
  25. R-1 (план 3А): Порядок исполнения части А: A0–A4, затем A6–A13; A5 (контракт 2.0, recognition_exchange.py) — последней, после слияния плана 0 (в origin/main нет api/tests/contract_identity.py). Обоснование: A5 зависит от теста идентичности плана 0, остальные задачи его результат не потребляют. Цена ошибки: возможные конфликты слияния в documents.py / recognition_exchange.py при A5 в конце.
  26. R-2 (план 3А): Исполнители задач и их ревьюеры — sonnet; финальное ревью ветки — opus. Обоснование: план даёт прозу, а не готовый код; ревью ветки ловит сквозные расхождения. Цена ошибки: более слабая модель на задаче — лишние итерации ревью.
  27. R-3 (план 3А): Тесты с БД идут на scratch-БД armcore_pytest_* compose-Postgres :5432 (устройство conftest); контейнеры не останавливаются и не пересоздаются. Обоснование: общий стенд разработки не должен падать из-за прогонов. Цена ошибки: недоступный Postgres — задача BLOCKED, а не пропуск тестов.
  28. R-4 (план 3А): A0 (только артефакты OpenSpec) без отдельного ревью сабагентом: достаточно openspec validate --strict. Обоснование: кода в задаче нет. Цена ошибки: неточность формулировок дельт; поймало финальное ревью.
  29. R-5 (план 3А): Ревью A1 и A2 объединено в один пакет после A2. Обоснование: downgrade-тесты A1 проверяемы только с A2. Цена ошибки: находки по A1 пришли на один раунд позже.
  30. R-6 (план 3А): Ревью малых задач объединялось парами (A3+A4, A8+A9, A10+A11, A12+A13). Обоснование: экономия раундов ревью. Цена ошибки: находки по первой задаче пары приходят на шаг позже.
  31. R-7 (план 3А): Change unified-normalization-core-a архивируется только вместе с unified-normalization-core-b; тексты поведения части Б в дельтах А остаются. Обоснование: разбор дельт на два change дороже и не нужен до выкладки обеих частей. Цена ошибки: если А заархивировать отдельно, openspec/specs опишет нереализованное.
  32. R-8 (план 3А): Одна волна правок по итогам финального ревью (sonnet), затем один scoped re-review; M-6, M-8, M-13 не правятся (deferred / rejected). Обоснование: замечания малы; M-13 (advisory-блокировки purge пофайлово) — окно обслуживания. Цена ошибки: M-6 (отдельный SUM для code_amount) и M-8 остаются техдолгом.
  33. A1-1 (план 3А): result_mapping.map_file до A5/A9 присваивает file_ordinal порядковым номером создания документов. Обоснование: иначе uq_document_stored_file_id_file_ordinal ломает файл с 2+ документами. Цена ошибки: номера не равны индексу в documents[]; заменено в A9/A5.
  34. A1-2 (план 3А): organization_name_modified — nullable boolean (как в DBML); миграция пишет false. Обоснование: соответствие DBML v5.19. Цена ошибки: нет.
  35. A1-3 (план 3А): Тест-фабрика tests/factories.py и tests/test_models_part_04_06._document нумеруют file_ordinal. Обоснование: уникальность (stored_file_id, file_ordinal). Цена ошибки: нет.
  36. A1-4 (план 3А): Старый тест «две BALANCE с NULL -> IntegrityError» переписан под DISCLOSURE. Обоснование: по v5.19 для форм это больше не нарушение. Цена ошибки: нет.
  37. A1-5 (план 3А): Имена FK/PK новых объектов (fk_financial_statement_value_confirmed_by_app_user, fk_document_issue_*, pk_document_issue) — по NAMING_CONVENTION. Обоснование: совпадают с create_all, тест схемы зелёный. Цена ошибки: нет.
  38. A2-1 (план 3А): line_status считается потерянным при <> 'ok' (колонка NOT NULL default 'ok'). Обоснование: IS NOT NULL всегда истинно. Цена ошибки: шум в журнале отката.
  39. A2-2 (план 3А): Булевы счётчики и organization_name_modified считаются как IS TRUE; причина отказа «наименование правилось» — organization_name_modified IS TRUE. Обоснование: NULL не считается правкой. Цена ошибки: нет.
  40. A2-3 (план 3А): Причина «строки формы без кода» — по line_code IS NULL (любые), «повтор» — только по ненулевым кодам. Обоснование: точность причин отказа отката. Цена ошибки: нет.
  41. A2-4 (план 3А): Сообщение журнала склеивает счётчики через ; . Обоснование: читаемость. Цена ошибки: нет.
  42. A3-1 (план 3А): Только PATCH, меняющий crm_id, пишет GROUP_CHANGE (before/after всех изменённых полей запроса); PATCH только имени/даты остаётся MEMBERSHIP_CHANGE; создание — MEMBERSHIP_CHANGE с crm_id в payload. Обоснование: P3-18 / Р-41 про смену crm_id; минимум изменения поведения. Цена ошибки: фильтр аудита по действию для правок групп расходится; правка в одну строку.
  43. A3-2 (план 3А): Предпроверка уникальности crm_id не учитывает видимость (глобальная уникальность), как для ГК. Обоснование: единый подход. Цена ошибки: crm_id невидимой группы раскрывает существование через 409 (как ГК/ЮЛ).
  44. A3-3 (план 3А): Before-payload удаления группы включает crm_id. Обоснование: полнота аудита. Цена ошибки: нет.
  45. A4-1 (план 3А): run принимает подменяемую фабрику транзакции (по умолчанию session_transaction). Обоснование: тесты идут на db_session. Цена ошибки: нет (боевой путь — по умолчанию).
  46. A4-2 (план 3А): PurgePlan несёт include_mixed; BlockingVersion по ex_snapshot_item имеет статус EX_SNAPSHOT_ITEM и version_id = ex_snapshot.source_version_id. Обоснование: сигнатура apply_purge без флага. Цена ошибки: нет.
  47. A4-3 (план 3А): Проверка блокировки покрывает ВСЕ документы удаляемых целиком файлов (в том числе OSV/DISCLOSURE; OSV смешанных файлов с --include-mixed). Обоснование: иначе удаление сломало бы FK/заморозку. Цена ошибки: лишний отказ, если OSV удаляемого файла в верифицированной версии (корректно: она заморожена).
  48. A4-4 (план 3А): apply_purge блокирует все файлы плана (jobs -> file -> documents, по id файла), перепланирует под блокировками и отказывает до первой мутации (PurgeRefusedError); версии блокируются позже (file -> doc -> version). Обоснование: защита от гонки с загрузкой. Цена ошибки: окно между проверкой и блокировкой версий (T4.3, rejected).
  49. A4-5 (план 3А): Dry-run с блокировкой печатает блок отказа, но выходит с 0; --apply с блокировкой: отчёт в stdout, сообщение в stderr, код 1. Обоснование: P3-13: 0 = отчёт. Цена ошибки: нет.
  50. A4-6 (план 3А): FILE_DELETE.after_value: для целого файла {unbound_version_ids}, для смешанного {deleted_documents, unbound_version_ids}. Обоснование: трассируемость очистки. Цена ошибки: нет.
  51. A4-7 (план 3А): superseded_with_osv — файлы, у которых superseded_by_file_id указывает на удаляемый файл и которые содержат OSV. Обоснование: сохранение замещения ОСВ. Цена ошибки: нет.
  52. A6-1 (план 3А): section_number возвращает строчные буквы (работает по нормализованному тексту). Обоснование: внутренний формат. Цена ошибки: нет.
  53. A6-2 (план 3А): names() — имена без повторов в порядке редакций и появления; edition_codes — по всем записям. Обоснование: стабильность подсказки. Цена ошибки: нет.
  54. A6-3 (план 3А): Если «итого по разделу» только у одного из имён, номер у другого None — сравнение только по равенству строк. Обоснование: спека. Цена ошибки: нет.
  55. A6-4 (план 3А): Пустые справочные имена пропускаются. Обоснование: шум. Цена ошибки: нет.
  56. A5-1 (план 3А): FileWarning наследует DocumentWarning (есть также line_refs/column_index/field): схема fileWarning = warning + locator. Обоснование: надмножество брифа, без дублирования полей. Цена ошибки: нет.
  57. A5-2 (план 3А): MetricsV2 наследует Metrics (переопределяет warnings); ветвление в _metrics_json идёт по классу конверта, не метрик. Обоснование: изоляция v1/v2 при общей базе метрик. Цена ошибки: v2-конверт обернул бы уже готовые структуры; покрыто test_inbox_v2_warnings_kept.
  58. A5-3 (план 3А): parse_message/parse_result: не-2 MAJOR отдаётся модели v1, чей шаблон ^1\.\d+$ даёт ValidationError (3.0, «banana», нет поля); нечитаемый JSON в parse_result — json.JSONDecodeError (ValueError), не ValidationError. Обоснование: единая точка отказа для неподдерживаемых версий. Цена ошибки: вызывающий код, ждущий только ValidationError, пропустит ошибку чтения JSON (см. открытый вопрос).
  59. A5-4 (план 3А): У v2-предупреждения без locator в метрики пишется как пришло (ключа нет), у LEGACY — locator: None. Обоснование: не домысливать данные, не менять пришедшее. Цена ошибки: потребитель обязан читать locator как необязательный (file_warnings так и делает).
  60. A5-5 (план 3А): RecognitionResultMessageV2 не наследует v1; общие поля вынесены в _MessageBase. Обоснование: изоляция isinstance между версиями. Цена ошибки: нет.
  61. A5-6 (план 3А): file_warnings: locator не-строка/не-null -> объект пропускается. Обоснование: защита от мусора во входящем конверте. Цена ошибки: потеря предупреждения с некорректным locator.
  62. A6-5 (план 3А): Старые тесты API обновлены под код 1105 в fsbu4 (yaml дополнен 1105/1130/1140/1530/2460 и вторыми именами). Обоснование: данные docs. Цена ошибки: для ГСК-кода 1105 на 66н с датой fsbu4 появляется пустая строка «Гудвил» (O-2 A6).
  63. A7-1 (план 3А): FailedCheck.codes — поле с default (), участвует в сравнении. Обоснование: P3-14. Цена ошибки: низкая.
  64. A7-2 (план 3А): insert_statement до маппера 2.0 пишет row_index = порядковый номер строки черновика. Обоснование: иначе все строки источника стали бы origin = template. Цена ошибки: заменено в A9/части Б.
  65. A7-3 (план 3А): checks_input принимает объекты-колонки (id, column_index) — протокол, без импорта моделей в домен. Обоснование: чистый домен. Цена ошибки: нет.
  66. A7-4 (план 3А): code_amount: колонка — первая по column_index, затем SUM по ней; нет колонки или непустых — None. Обоснование: определённость суммы. Цена ошибки: M-6: отдельный SUM, deferred в часть Б.
  67. A7-5 (план 3А): В API checks.failed[].codes и line_ids не выведены в A7 (только домен). Обоснование: не в брифе A7. Цена ошибки: закрыто в A8.
  68. A7-6 (план 3А): Порядок строк ответа в A7 не менялся. Обоснование: P3-17 — задача A8. Цена ошибки: нет.
  69. A8-1 (план 3А): insert_statement временно пишет period_kind (absolute при дате, иначе none). Обоснование: иначе тест брифа на 1.x не проходит до A9. Цена ошибки: заменено в A9; совпадает с backfill миграции.
  70. A8-2 (план 3А): FailedCheckOut.line_ids — объединение line_ids агрегата по codes правила; у межформенной проверки — строки только читаемой формы. Обоснование: кода второй формы в агрегате нет. Цена ошибки: фронт не подсветит строку другой формы.
  71. A8-3 (план 3А): header.source: запись с text: null отдаётся как есть, нечисловая уверенность -> null, не-dict записи пропускаются. Обоснование: T1.1: «нет подсказки». Цена ошибки: нет.
  72. A8-4 (план 3А): order_lines: detail с неизвестным родителем ведёт себя как detail без родителя; вложения по row_index; равные ключи — порядок входа (по id). Обоснование: P3-17. Цена ошибки: нет.
  73. A8-5 (план 3А): IssueCounts — dataclass репозитория, сервис переводит в IssueCountsOut. Обоснование: слои. Цена ошибки: нет.
  74. A9-1 (план 3А): ColumnDraft вместо кортежа в StatementDraft.columns (несёт period_kind / label_confidence). Обоснование: нужны поля колонки. Цена ошибки: правка 4 утверждений в тестах.
  75. A9-2 (план 3А): header_source.*.confidence в БД — число как пришло из 1.x (float), не Decimal. Обоснование: JSONB; P3-20. Цена ошибки: нет.
  76. A9-3 (план 3А): Повтор формы определяется только при известных годе и квартале у обеих; текст issue без префикса locator, в metrics.mapping_warnings — с префиксом. Обоснование: P3-4. Цена ошибки: нет.
  77. A9-4 (план 3А): Пустая ячейка (нет значения) -> needs_review = false (value_clean(None, None) истинно). Обоснование: формула приёма 1.x. Цена ошибки: нет.
  78. A9-5 (план 3А): У листа ОСВ issue пишутся и для предупреждения о повторе счёта (относится к первому листу). Обоснование: P3-5. Цена ошибки: нет.
  79. A10-1 (план 3А): Контрагент UNKNOWN трактуется как None, блокирует только MISMATCH. Обоснование: спека: counterparty_check != MISMATCH. Цена ошибки: низкая.
  80. A10-2 (план 3А): name_mismatches: строка ok с line_code_confidence = None считается уверенной (не проверяется); line_status сравнивается со StatementLineStatus.ok (StrEnum). Обоснование: спека: «confidence < 100» (T10.1: покрыто комментарием и тестом). Цена ошибки: строка без уверенности не даёт NAME_MISMATCH.
  81. A10-3 (план 3А): has_error_warning: строго severity == "error" (регистр важен), не список -> False. Обоснование: строгий контракт. Цена ошибки: нет.
  82. A10-4 (план 3А): noqa PLR0913 на form_status_v2 и тестовом хелпере. Обоснование: сигнатура задана планом. Цена ошибки: нет.
  83. A11-1 (план 3А): Тест test_disclosure_is_confirmable удалён: form_blocker принимает meta, у расшифровки её нет; не-форма проверяется вызывающими (FORM_TYPES) — тест возвращён в волне правок (T11.1) на уровне сервиса. Обоснование: покрытие подтверждения DISCLOSURE/OTHER. Цена ошибки: нет.
  84. A11-2 (план 3А): confirmed_twin при пустом периоде возвращает None (PERIOD_REQUIRED сработает раньше); первым выбирается близнец с меньшим file_ordinal. Обоснование: детерминизм. Цена ошибки: нет.
  85. A11-3 (план 3А): Дубль ищется только среди CONFIRMED; второй NEEDS_CHECK-близнец не блокирует. Обоснование: бриф. Цена ошибки: нет.
  86. A11-4 (план 3А): line_id в extra — строка (str(uuid)), как document_id. Обоснование: единообразие. Цена ошибки: нет.
  87. A12-1 (план 3А): Новый код 422 DUPLICATE_COLUMN (колонка дважды в columns[]). Обоснование: в брифе не было. Цена ошибки: низкая.
  88. A12-2 (план 3А): Подтверждение даты отчёта требует непустого report_date (null == null не подтверждение). Обоснование: смысл подтверждения. Цена ошибки: низкая.
  89. A12-3 (план 3А): Равное текущему organization_name — no-op. Обоснование: подтверждать нечего. Цена ошибки: нет.
  90. A12-4 (план 3А): Снятие подтверждения ячейки и возврат суммы к исходной: needs_review = не(правлена | подтверждена | строка detail | value_clean) — формула приёма 1.x. Обоснование: для 2.0 точное значение даст часть Б. Цена ошибки: ячейка 2.0 с иным сомнением (знак) может показаться чистой до Б.
  91. A12-5 (план 3А): recompute дополнительно сбрасывает needs_review у правленых/подтверждённых значений и значений строк detail (settle_edited_values). Обоснование: согласованность флагов. Цена ошибки: T13.3: флаг навсегда false у значений detail (часть Б).
  92. A12-6 (план 3А): Подтверждение ячейки без записи значения в БД — 422 UNKNOWN_CELL до записей; колонка не этой формы в values[] — UNKNOWN_CELL, UNKNOWN_COLUMN — только для columns[]. Обоснование: существующий тест. Цена ошибки: нет.
  93. A12-7 (план 3А): is_current пересчитывается только при смене дат колонок (самая поздняя дата, меньший column_index). Обоснование: как при приёме. Цена ошибки: нет.
  94. A12-8 (план 3А): Ключи аудита подтверждений: value:<id>:confirmed, column:<id>:date_confirmed, needs_review_fields; смена даты колонки — column:<id>. Обоснование: единый формат. Цена ошибки: нет.
  95. A12-9 (план 3А): Смена report_date убирает report_date из needs_review_fields без отдельного ключа аудита. Обоснование: минимум шума. Цена ошибки: нет.
  96. A12-10 (план 3А): lines[] в StatementPatch нет в A12 (добавлено в A13). Обоснование: разбиение задач. Цена ошибки: нет.
  97. A13-1 (план 3А): Тесты используют код 1215 вместо 1250 из брифа. Обоснование: 1250 есть в источнике ББ Межениновской, а нужен «только шаблонный» код. Цена ошибки: нет.
  98. A13-2 (план 3А): tests/test_api_statements.py::insert_line кладёт StatementLineStatus, а не строку. Обоснование: identity map сервиса. Цена ошибки: нет.
  99. A13-3 (план 3А): Пометка шаблонной строки (row_index IS NULL) как detail — 422 INVALID_LINE_PATCH; подтверждение кода у строки не ok — тоже. Обоснование: спека такого перехода не перечисляет; у detail нужен row_index для родителя. Цена ошибки: низкая, фронт такого не шлёт.
  100. A13-4 (план 3А): Назначение того же кода строке ok с этим кодом — no-op; detail -> detail — no-op. Обоснование: нечего менять, аудита нет. Цена ошибки: T13.1: no-op элемент может дать ложный 409 — недостижимо.
  101. A13-5 (план 3А): Порядок проверок по элементам: действие, повтор, UNKNOWN_LINE, LINE_CODE_REQUIRED, INVALID_LINE_CODE, допустимость; затем отдельным проходом LINE_CODE_TAKEN (409 после всех 422). Обоснование: предсказуемость. Цена ошибки: нет.
  102. A13-6 (план 3А): needs_review при снятии подтверждения: ok и не line_code_modified и уверенность < 100. Обоснование: правило приёма 1.x; для 2.0 уточнит часть Б. Цена ошибки: нет.
  103. A13-7 (план 3А): Назначение кода: confidence = Decimal("100.00") (ответ отдаёт объект сессии). Обоснование: без перечитывания. Цена ошибки: нет.
  104. A13-8 (план 3А): Удаление пустой шаблонной строки пропускается, если она сама — цель элемента запроса. Обоснование: избежать удаления цели. Цена ошибки: нет.
  105. A13-9 (план 3А): Отступление от P3-10: 422 в values / columns после применённых lines откатывает всю транзакцию запроса; отдельной предпроверки адресации values для lines нет. UNKNOWN_CELL по line_code видит назначения lines этого запроса (они применены и flush-нуты раньше); 409 приоритетнее такого 422. Обоснование: эквивалентное наблюдаемое поведение при меньшем коде (T13.5). Цена ошибки: при ошибке в откате частичная запись — тест M-10 фиксирует отсутствие записей и аудита.
  106. A13-10 (план 3А): user_id в apply_line_edits принят по интерфейсу, не используется; аудит строк — ключ line:<id> с {line_code, line_status, parent_row_index, is_confirmed}, в него попадают и detail со сменой только родителя. Обоснование: у строки нет автора правки. Цена ошибки: нет.
  107. A13-11 (план 3А): Пометка шаблонной строки как detail = 422 INVALID_LINE_PATCH. Обоснование: см. A13-3. Цена ошибки: низкая.

План 3, часть Б (arm-core, change unified-normalization-core-b)

Источник: openspec/changes/unified-normalization-core-b/design.md в репозитории arm-core, коммит 0f84088 (ветка исполнения; полный текст — там). Перенесено пунктов: 40.

  1. R-1 (Р-40) (план 3Б): CODE_NOT_IN_DICTIONARY / CODE_NOT_IN_EDITION пишутся только для кодов из ровно четырёх цифр; пятизначные подстроки Excel не проверяются. P3-7 плана в этой части заменён (запись на код документа сохраняется, пятизначные коды — нет). Обоснование: решение владельца 18.10.2026 (Р-40) перед исполнением Б; закрывает O-3 (поток info у пятизначных). Цена ошибки: пропущенный info у нестандартного пятизначного кода; восстановление — снять условие длины кода в маппере 2.0 и пересчёте.
  2. R-2 (Р-34) (план 3Б): Подтверждение кода строки — флаг is_confirmed; line_code_confidence и line_code_modified не меняются (P3-16 части А). Обоснование: решение владельца 18.10.2026; в пересчёте Б правило is_confirmed ⇒ needs_review строки = false берётся из P3-16. Цена ошибки: нет — поведение уже реализовано в части А.
  3. R-3 (план 3Б): Удаление копии словаря §7 в дельте оформлено как MODIFIED «Соотношения §7 по колонкам», а не REMOVED (см. D-4); «Ответ GET …/statement» и порядок 409 оформлены новыми ADDED-требованиями, так как в openspec/specs/ и дельтах части А одноимённых нет. Обоснование: openspec archive требует, чтобы имя REMOVED/MODIFIED существовало; иначе архив упадёт. Цена ошибки: при архивации — правка имён заголовков дельт.
  4. P3-25 (B1) (план 3Б): Тип формы во входе пересчёта — document.document_type (BALANCE / INCOME / DISCLOSURE), а не meta.statement_type (только balance / income); план писал «тип формы» без источника. Обоснование: DISCLOSURE не представим в statement_type; тип документа — единственный источник для всех трёх типов. Цена ошибки: форма DISCLOSURE собралась бы с ошибкой KeyError / потерей типа; правка — одна строка в build_stored_form.
  5. P3-26 (B1, O-3 часть А) (план 3Б): Копия statement-code-orders.yaml в api/contracts/ не заводится: перечень кодов читает пакет из своей байтовой копии api/arm_statements/rules/statement_code_orders.yaml (loader._ORDERS), ядро файла не открывает; идентичность копии пакета с docs/contracts/statement-code-orders.yaml проверяет sync-statements --check; тест идентичности контракта сверяет только *.json. Сейчас копия пакета побайтно равна файлу docs. Обоснование: второй экземпляр файла в ядре никто бы не читал и он мог бы разойтись с пакетом. Цена ошибки: нет; если sync-statements когда-либо перестанет сверять YAML — добавить сверку в contract_identity.py.
  6. P3-27 (B1) (план 3Б): edition_codes берёт строки ok результата 2.0, сортирует по row_index и помечает код уверенным при line_code_confidence == 100 (ровно), None — неуверенный. Обоснование: как в разделе «Что ядро вызывает» и в recompute пакета (код правленый/подтверждённый там — code_fixed). Цена ошибки: редакция приёма разойдётся с редакцией пересчёта; сверку делает test_edition_codes_and_engine_edition.
  7. P3-28 (B2) (план 3Б): StatementService получает StatementsEngine через зависимость get_statements_engine (тесты подменяют её FakeEngine); FailedRelation, StatementChecks, PeriodKind реэкспортируются из services/statements_engine.py, чтобы import arm_statements оставался только в двух файлах. Обоснование: единственная точка импорта пакета (common.md); подмена без патчей. Цена ошибки: нет.
  8. P3-29 (B2) (план 3Б): Статус после правки — form_status_v2: service_done = (result.status == DONE), file_error_warning — metrics.warnings задания, names_ok — нет NAME_MISMATCH пересчёта, duplicate_in_file — duplicate_period_forms по формам файла (file_form_periods); statement_status D-6 с пути правки убран. Обоснование: один источник статуса с приёмом 2.0 (Р-16, Р-18, Р-19). Цена ошибки: форма с повтором периода в файле после правки остаётся NEEDS_CHECK — как при приёме.
  9. P3-30 (B2) (план 3Б): Пустая цель при непустых членах — нарушение с expected = NULL (P1-21); тесты части А «очищенная 1700 не цель» и «цель потерянной строки не проверяется» переписаны под это; потерявшая строку 1700 форма блокирует подтверждение до правки или акцепта. Обоснование: следствие P1-21 пакета; спека («посчитанное соотношение»). Цена ошибки: форма, потерявшая итоговую строку, теперь требует действия аналитика — если владелец против, правится правило пакета (план 1), не ядро.
  10. P3-31 (B2) (план 3Б): NOTHING_TO_ACCEPT и CHECKS_NOT_PASSED читают записи relation; акцептованная форма не считается нарушенной (statement_hard_failures пуст при checks_accepted_at), поэтому повторный акцепт — 409 NOTHING_TO_ACCEPT, как в части А. Обоснование: сохранение поведения А; буква брифа («нет записей relation») допускает оба прочтения. Цена ошибки: повторный акцепт с новым комментарием станет возможен, если снять условие.
  11. P3-32 (B2) (план 3Б): «Эффективная дата» для relative:* колонок при правке header.report_date — новое значение meta.report_date (в том числе NULL ⇒ period_date = NULL), не document.reporting_date; изменение дат колонок идёт в аудит ключом column:<id> без признака правки данных. Обоснование: период документа сам выводится из даты отчёта и дат колонок (§9.5 п. 2), поэтому циклическая зависимость недопустима. Цена ошибки: колонка при пустой дате отчёта теряет дату; аналитик ставит её вручную.
  12. P3-33 (B2) (план 3Б): Запись relation / soft несёт и column_id (колонка найдена по column_key: str(column_id), затем str(column_index)), и column_index нарушения; ответ берёт индекс из колонки, иначе из записи, иначе -1. Обоснование: ключ колонки B4 для 2.0 — индекс. Цена ошибки: нет.
  13. P3-34 (B2) (план 3Б): NAME_MISMATCH пересчитывается для всех кодов (R-1 ограничивает только CODE_NOT_IN_*); CODE_NOT_IN_* не пишутся, если пакет не определил редакцию (а CODE_NOT_IN_EDITION — и вариант); словарь имён берётся по редакции пакета, не по дате. Обоснование: R-1 и спека (редакция документа — edition пакета). Цена ошибки: пятизначная подстрока с уверенностью < 100 даёт NAME_MISMATCH (кода нет в словаре) — если поток окажется шумным, ограничить name_mismatches четырёхзначными.
  14. P3-35 (B2) (план 3Б): Временный код A12/A13 удалён: тело StatementService.recompute, settle_edited_values, has_review_values, has_source_lines, _value_needs_review, nearest_ok_parents и _recompute_parents; parent_row_index detail пишет только пересчёт пакета. Обоснование: задание B2; T13.3 (флаги detail больше не затираются навсегда). Цена ошибки: нет.
  15. P3-36 (B3) (план 3Б): checks_on_intake_1x живёт в statement_recompute.py; движок передаётся в _insert_statements необязательным параметром (по умолчанию default_engine()), map_file сигнатуру не меняет. Приём 1.x пишет meta.form_variant и записи relation / soft (core), но не CODE_NOT_IN_* / NAME_MISMATCH (P3-7) и не трогает needs_review (A9); условие D-6 «проверки сошлись» — not result.checks.relations_failed (нарушение с expected = NULL — жёсткое). Обоснование: задание B3, спека «Пересчёт при приёме»; расхождение с прежним run_checks (его failed) возможно только на формах с пустой целью, где пакет строже. Цена ошибки: форма 1.x с пустой целью получит NEEDS_CHECK вместо RECOGNIZED.
  16. P3-37 (fix1, T2.1) (план 3Б): Аудит lines[] довносит смену parent_row_index после пересчёта: у правленных строк значение заменяется итоговым, у остальных появляется запись line:<id> с одним ключом parent_row_index ({"parent_row_index": старый} → {…: новый}). Обоснование: родителей пишет recompute_form уже после снимка строк правки; полный снимок для не правленных строк не нужен. Цена ошибки: в событии RECOGNIZED_DATA_EDIT у не правленных строк неполный набор ключей.
  17. P3-38 (fix1, T1.1/T1.2/T2.4/T2.6/T2.8) (план 3Б): Паритет редакции приёма и пересчёта для строки с line_code_confidence < 100 и code_fixed подтверждён тестом (расхождения нет, код не менялся); edition_codes пропускает строку ok без кода; column_index нарушения — общим помощником issue_column_index (запись, иначе колонка по column_id) в ответе и в detail отказа; _unconfirmed_needs_review и присвоения needs_review в _apply_item удалены (флаг пишет только пересчёт, он всегда идёт после правки строк); неизвестный relative:* в _resolve_relative_columns пропускается без ошибки. Обоснование: замечания ревью B1+B2. Цена ошибки: колонка с неизвестным relative:* не следует за датой отчёта.
  18. P3-39 (B4) (план 3Б): Проверки ядра info (CODE_NOT_IN_DICTIONARY, CODE_NOT_IN_EDITION, NAME_MISMATCH, STATEMENTS_VERSION_MISMATCH, RESULT_STATUS_INCONSISTENT, code_issues, name_issues, version_issue, name_check_lines) вынесены из services/statement_recompute.py в domain/statement_issues.py; единая функция core_info_issues(lines, *, form, edition: str | None, order: Collection[str] | None, catalog) -> CoreInfo(drafts, mismatched) используется приёмом 2.0 и recompute_form (поведение пересчёта не изменилось, P3-34). Домен пакет arm_statements не импортирует (исключение — domain/stored_form.py по D-1): редакцию (engine.edition) и перечень кодов (engine.code_order) считает сервис; statement_recompute реэкспортирует прежние имена. Вместо сигнатуры плана core_info_issues(*, lines, edition, form_variant, catalog, engine). Обоснование: домен не должен знать StatementsEngine/arm_statements (D-1); одно правило для приёма и пересчёта. Цена ошибки: нет; импортёры прежних имён из statement_recompute работают.
  19. P3-40 (B4) (план 3Б): Приём форм 2.0 — отдельный сервис services/statement_intake_v2.py (map_statement_docs_v2, insert_forms_v2); map_file выбирает маппер по типу результата через _plan_forms / _insert_forms (иначе функция превышала лимит ruff по операторам) и получил необязательный параметр engine. _load_result и тип в process_once остаются под 1.x до B5. Обоснование: размер result_mapping.py и лимиты линтера; граница B4/B5 из брифов. Цена ошибки: нет.
  20. P3-41 (B4) (план 3Б): DISCLOSURE в 2.0 — по-прежнему один документ на файл со статусом RECOGNIZED (D-3), file_ordinal первого; предупреждения всех объединённых документов результата пишутся в его document_issue источником service. Статус сервиса и предупреждения файла уровня error на него не влияют. Обоснование: спека условие RECOGNIZED относится к формам ББ/ОФР; у DISCLOSURE нет данных. Цена ошибки: DISCLOSURE с needs_check сервиса автоподтверждается (данных нет — подтверждать нечего).
  21. P3-42 (B4) (план 3Б): STATEMENTS_VERSION_MISMATCH пишется и при приёме 2.0 (version_issue(job.statements_version, engine.version)), а не только при пересчёте после правки. Обоснование: иначе запись появлялась бы только после первой правки; один набор записей core у приёма и пересчёта. Цена ошибки: лишний info на формах, принятых копией иной версии.
  22. P3-43 (B4) (план 3Б): Результат с повтором row_index или ref у строк: вторая и далее строка пропускается с MAPPING_WARNING; detail с parent_ref, которого нет, хранится с parent_row_index = null и тем же предупреждением. Обоснование: уникальность (meta_id, source_ref) и ключа строки при вставке иначе роняет приём (файл ERROR после попыток); схема уникальности ref/row_index не проверяет. Цена ошибки: потерянная строка дубля видна только в предупреждении.
  23. P3-44 (B4) (план 3Б): Запись relation / soft из checks результата несёт column_id (колонка с таким column_index, если есть) и всегда column_index (как P3-33); запись warning с column_index — либо column_id (колонка есть), либо column_index (колонки нет). Обоснование: спека «Пропущенная колонка» и ответ API, читающий индекс из записи. Цена ошибки: нет.
  24. P3-45 (B4) (план 3Б): is_current колонок 2.0 — как у 1.x (колонка с самой поздней датой, при равенстве меньший column_index; нет дат — NULL), в том числе у колонок relative:* с датой; значение, которого нет в результате, пишется с is_recognized = NULL, остальными полями пустыми и needs_review = false. Обоснование: §9.5 п. 2; спека «Хранение результата формы v2.0». Цена ошибки: колонка relative:* без даты не станет текущей.
  25. P3-46 (B5) (план 3Б): _load_result выбирает модель по MAJOR через contract.versions.parse_result; неизвестный MAJOR, неверный формат версии и любое нарушение схемы — MappingFatal с итогом проверки схемы (результат не соответствует схеме recognition-result: ошибок N, первая: …), файл ERROR, без повтора. Обоснование: спека «Приём файла результата»; D-2. Цена ошибки: файл с MAJOR 3 не мапится, а не обрабатывается молча как 1.x.
  26. P3-47 (B5, T5.2 части А) (план 3Б): Нечитаемый JSON (JSONDecodeError, не UTF-8) в _load_result — тоже MappingFatal с текстом «результат не соответствует схеме recognition-result: файл не является JSON» (текст исключения парсера пользователю не показывается; M-1); не-объект ([], null) отсекается моделью как нарушение схемы. Обоснование: иначе ValueError из parse_result уходил бы в повторяемые ошибки и файл ERROR получал бы только после MAPPING_MAX_ATTEMPTS попыток. Цена ошибки: ошибка JSON не отличается в тексте от прочих ошибок схемы.
  27. P3-48 (B5) (план 3Б): Статус ОСВ 2.0 — osv_status_v2(osv_done = sheet.status == "done", checks_ok, file_error_warning, counterparty, sheet_mapping_warnings); «предупреждения маппинга листа» = повтор счёта + сироты + «нарастающая по данным Q1» (вход document_status 1.x тот же); у 1.x статус как прежде (§9.3, статус задания). Предупреждение уровня error ищется в metrics.warnings результата (file_warnings), а не в recognition_job.metrics. Обоснование: Р-28; единый источник с формами. Цена ошибки: ОСВ с error в метриках файла — NEEDS_CHECK.
  28. P3-49 (B5) (план 3Б): osvDocument.warnings пишутся в document_issue как warning / service с severity из предупреждения (по контракту info, error не отбрасывается); line_refs у ОСВ не сохраняются (строк формы нет), column_index и field — как пришли. Записи сервиса идут раньше записей маппинга ядра. Дубль листа (повтор счёта) своих предупреждений/статуса не вносит — документом остаётся первый лист. Обоснование: у ОСВ нет line_ids/колонок формы; повтор — D-7. Цена ошибки: error-предупреждение листа ОСВ в issues не влияет на статус само по себе (статус — по status листа).
  29. P3-50 (B6) (план 3Б): FailedCheck.codes в ядре остаётся (вопреки плану, который удалял поле): оно нужно межформенной проверке 1370↔2400 (codes = ("1370", "2400")) для codes / line_ids в checks.cross_form ответа; нарушения §7 по колонкам читаются из document_issue, где коды хранятся сами. Обоснование: межформенную проверку пакет не считает (она по двум документам), без codes подсветка строк в ответе пропала бы. Цена ошибки: у cross_form не было бы codes/line_ids, фронт не подсвечивал бы строки 1370 и 2400.
  30. P3-51 (B6) (план 3Б): checks_report уже брал допуск у engine.tolerance() (с B2), поэтому правки сервиса в B6 нет; константа допуска ядра удалена вместе с load_check_rules. Состав и порядок редакций справочника имён statement_lines.yaml не переписывается на engine.editions() в коде: согласованность проверяет тест test_name_dictionary_codes_in_code_orders (редакции справочника = редакции пакета в том же порядке; каждый код справочника есть в перечне редакции пакета, любой вариант формы). Обоснование: справочник остаётся данными имён и состава экрана; перечни кодов — единственный источник в пакете, расхождение ловит тест, а не молчаливая подмена в рантайме. Цена ошибки: при добавлении редакции в пакет без справочника имён тест красный; в рантайме новая редакция дала бы форму без шаблонных строк.
  31. P3-52 (B6) (план 3Б): edition_for (граница 66n/fsbu4 по дате для шаблона экрана) в domain/statement_mapping.py не тронут: переход на engine.edition(...) требует кодов формы и варианта и относится к ответу формы (B7/доработка), а не к удалению копии словаря. Обоснование: не расширять B6 за рамки брифа. Цена ошибки: ответ формы и приём могут расходиться в редакции на формах без даты; существующее поведение сохранено.
  32. P3-53 (fix-волна, I-3, R-11) (план 3Б): Источник «ошибки файла» единый: при приёме результата 2.0 map_file сохраняет result.metrics.warnings структурами в recognition_job.metrics.file_warnings (рядом с mapping_warnings, прочие ключи не затираются, _mapped_metrics); recompute_form читает его через file_error_in_metrics(metrics), при отсутствии ключа (результат 1.x, задания до части Б) — прежний metrics.warnings конверта. Приём (insert_forms_v2, ОСВ) по-прежнему читает результат напрямую. Обоснование: раньше приём смотрел предупреждения файла результата, а пересчёт — конверта: статус формы мог измениться от несвязанной правки. Цена ошибки: при расхождении конверта и результата форма менялась бы NEEDS_CHECK ↔ RECOGNIZED без причины; задание без ключа читается по-старому.
  33. P3-54 (fix-волна, I-2) (план 3Б): build_stored_form передаёт пакету is_recognized = value.is_recognized is not False: NULL (заглушки маппера 1.x/2.0, ячейки, созданные правкой) — не сомнение, как в value_clean ядра и в спеке пакета («позиции нет → is_recognized = true»). Прежнее bool(...) превращало NULL в False, и после любой правки шапки значения без сомнений получали needs_review, а форма — NEEDS_CHECK. Обоснование: is_recognized = NULL означает «признака нет», а не «не распознано». Цена ошибки: форма из заглушек или с созданной ячейкой уходила бы в NEEDS_CHECK от правки шапки.
  34. P3-55 (fix-волна, T4.2) (план 3Б): checks_on_intake_1x(..., now) принимает момент приёма параметром (из map_file), а не utc_now(): записи relation / soft формы 1.x получают тот же created_at, что и остальные записи приёма. Обоснование: единое «сейчас» приёма, воспроизводимость тестов. Цена ошибки: различие created_at на миллисекунды между записями одного приёма; порядок записей в ответе мог бы нарушиться.
  35. P3-56 (fix-волна, M-4) (план 3Б): statement_mapping_v2._lines_and_values строит ref → row_index только по принятым строкам (первый вход побеждает; ссылка вперёд на ещё не встреченную принятую строку допустима): parent_ref строки, чей ref повторяет прежний, указывает на первую принятую строку, а не на пропущенный дубль (раньше побеждал последний). Обоснование: пропущенная строка не должна становиться родителем; детализация висела бы на несохранённом row_index. Цена ошибки: расшифровка привязана не к той строке; пересчёт detail_parents всё равно исправляет её после первой правки.
  36. P3-57 (fix-волна, T4.4, T6.1) (план 3Б): Закрыты тестами ветки приёма 2.0: пропуск дубля row_index / ref, потерянный parent_ref, STATEMENTS_VERSION_MISMATCH (и его отсутствие при совпадении версии), DISCLOSURE с предупреждением error (документ RECOGNIZED, запись сохранена; вопрос владельцу P3-41 остаётся открытым), edition is None (нет CODE_NOT_IN_*, NAME_MISMATCH остаётся), предупреждение листа ОСВ error не блокирует подтверждение (P3-49). Обоснование: покрытие веток без изменения поведения. Цена ошибки: нет.
  37. P3-58 (fix-волна, I-4) (план 3Б): Тесты правки и акцепта формы, принятой маппером 2.0 (tests/test_statements_v2_edit.py): записи relation / soft источника service после первой правки заменяются записями core; warning сервиса уровня error и RESULT_STATUS_INCONSISTENT сохраняются и держат NEEDS_CHECK; RECOGNIZED не меняется от правки шапки; акцепт нарушения, найденного ядром. Наблюдение: «чистая» форма с фабрики balance_lines() по реальному пакету не сходится (1300 без членов) — для тестов пересчёта берётся согласованная форма. Обоснование: тест должен ловить регрессию, а не проходить на форме, у которой пересчёт и так находит нарушение (мутационная проверка I-2 подтверждает). Цена ошибки: тесты на несогласованной форме проходили бы вхолостую.
  38. P3-59 (fix-волна, M-5, M-6, M-7, T6.3) (план 3Б): Документация: CLAUDE.md репозитория исправлен под P3-30 (пустая цель при заполненных членах — нарушение с expected = NULL), «сбрасывает акцепт» — только правка данных (Р-25), добавлено описание приёма результата 2.0; README — формы, принятые кодом части А, теряют проверку CHECKS_NOT_PASSED до первой правки (§7 в document_issue у них нет; обход — очистка форм purge_statements.py). P3-39 уточнён: домен не импортирует arm_statements, исключение — domain/stored_form.py (D-1). Обоснование: документация должна совпадать с кодом. Цена ошибки: читатель CLAUDE.md/README получал бы неверные гарантии о проверках и акцепте.
  39. P3-60 (fix-волна, I-5) (план 3Б): В ответах нарушение §7 может иметь expected / actual = null (пустая цель при непустых членах, P1-21). Индекс колонки при её отсутствии различается: checks.failed[] ответа GET …/statement отдаёт -1 (FailedCheckOut.column_index: int, межформенная cross_form всегда -1), а detail.failures[] отказа 409 CHECKS_NOT_PASSED — null (issue_column_index). Спека описывает поведение как есть; код не правился. Вопрос на выравнивание (-1 или null) — план 6 / фронт (arm-web). Обоснование: фиксация фактического контракта до решения владельца; тип поля у фронта (arm-web) нужно сверить. Цена ошибки: фронт, ожидающий число, получит null в detail.failures[].column_index (только в 409, когда у записи нет ни column_id, ни column_index).
  40. P3-61 (fix-волна, C-1, M-1) (план 3Б): Дельты B переписаны под openspec archive: MODIFIED «Пересчёт после правки», «Словари ядра и достройка шаблонных строк», «Формы результата 1.x — §7 при приёме» теперь несут полный текст требования части А со всеми сценариями плюс уточнения Б (иначе архивация падает: «scenario(s) not present in the modified block»); добавлен MODIFIED «Хранение результата формы v2.0 (DBML v5.19)» и «Форма ББ/ОФР для экрана проверки» (сценарий «Строка, потерянная источником» приведён к P3-30). ADDED «Маппер результата 2.0 для форм» заменён MODIFIED «Хранение…» (устранено противоречие «первое остаётся» vs «без повторной дедупликации») и ADDED «Проверки ядра при приёме результата 2.0»; ADDED «CHECKS_NOT_PASSED из document_issue» и «Блок checks…» оставлены только с тем, чего нет в А (форма элемента нарушения, null), дубль порядка 409 убран. Проверено архивацией А, затем Б на копии openspec/. Дополняет R-3 и D-4. Обоснование: архивация обязана проходить; дубли и противоречия между А и Б вводят в заблуждение. Цена ошибки: если архивация А и Б падает — править заголовки и сценарии дельт Б; при будущей правке требований А в самой А тексты MODIFIED Б нужно обновить синхронно.

План 4 (tabular-parser, change unified-normalization-tp)

Источник: openspec/changes/unified-normalization-tp/design.md в репозитории tabular-parser, коммит b583b04 (ветка исполнения; полный текст — там). Перенесено пунктов: 30.

  1. P4-1 (план 4): Переход на общий код и на контракт v2.0 — одна задача (7): модели, копия схем, сборка книги, путь ОСВ и воркер меняются вместе; «контракт v2.0 в копии» отдельной последней задачей не вынесен — модели 2.0 без копии 2.0 (или наоборот) валят result_contract и тест идентичности. Golden и манифест — сразу за переключением (задача 8), до удаления старого кода; между задачами 7 и 8 проверка идёт с ARM_KA_FIXTURES=/nonexistent. Цена: одна крупная задача и окно в одну задачу без проверки на реальных файлах.
  2. P4-2 (план 4): Адаптер пишется новыми модулями рядом со старыми (cells, forms, header_cells, adapter, contract, pipeline); старые statements/* не правятся и удаляются целиком задачей 9. Цена: временное дублирование логики поиска блоков и шапки.
  3. P4-3 (план 4): Cell.text числа Excel — как в русской локали: группы по три через пробел, если в формате есть разделитель групп; десятичная запятая; литералы секции (скобки) сохраняются; «-» добавляется только у односекционного формата; General/неразобранный формат/xlsb — число без групп и экспоненты, целое без дробной части; дата — dd.mm.yyyy; логическое — «ИСТИНА»/«ЛОЖЬ». Цена: у экзотических форматов текст может отличаться от экрана Excel; value и статус от этого не зависят (у Excel значение берётся из value). Совпадает с примером docs result-statement-excel.json ("586 260,26").
  4. P4-4 (план 4): Показываемое число вне numeric(18,2) больше не делает формат «неразобранным»: адаптер отдаёт Decimal, а VALUE_OUT_OF_RANGE и amount = null выдаёт пакет (спека «Шаг 4»). Цена: правка поведения #42 для чисел больше 16 целых цифр (на реальных файлах их нет).
  5. P4-5 (план 4): Сетка кандидата: строки блока целиком, включая пустые; объединённая ячейка — только в позиции левой верхней, прочие позиции None; ширина — наибольшая среди листов кандидата. Цена: подпись, объединённая на два столбца, видна пакету только в левом.
  6. P4-6 (план 4): Шапка ищется в строках блока выше первой строки с кодом его столбца кодов (столбец — code_column пакета: то же правило (а)/(б) шага 2 с защитой от блока «Коды», своего правила у адаптера нет), без кодов — во всём блоке: так титульный блок «Коды» (ОКОПФ 12267) не мешает, а подписи под формой (ИНН и ФИО в блоке подписей) не попадают в шапку. Цена: метка шапки ниже начала таблицы не найдётся.
  7. P4-7 (план 4): Шаблоны словаря пакета адаптер применяет к тексту, нормализованному функцией пакета по таблице «Словари» раздела API плана 1: метки шапки, заголовки, ОКУД, auditor, legal_form — normalize_label (= прежний normalize_text tabular-parser, P1-22); подпись «Код» и относительные периоды — normalize_caption; маркер продолжения — is_continuation. Цена: нет — смена нормализации в пакете меняет и type_hint, и адаптер.
  8. P4-8 (план 4): Склейка: открытая форма — последний кандидат с type_hint BALANCE/INCOME; лист с маркером сверяется только по типу; лист без маркера (сильное продолжение — столбец кодов по правилу шага 2 или своя строка заголовка; слабое — столбец кодов ослабленным поиском code_column(…, relaxed=<тип формы>), минимум min_codes_continuation кодов, сразу после листа формы) — sheet_joins с числом колонок сумм len(amount_columns(...)) пакета («столбцы правее столбца кодов, где в строках с кодами есть число»); открытой формы нет — отдельный кандидат без предупреждения (пакет даст NO_ORGANIZATION_NO_INN). Цена: «Изменение, %» с числами считается колонкой сумм с обеих сторон сравнения; лист с иной шапкой, но тем же числом колонок, присоединится и будет сопоставлен пакетом по датам.
  9. P4-9 (план 4): SHEET_OTHER для листов «прочее» выдаёт книга сама (документа и кандидата нет), а не пакет по кандидату без type_hint. Цена: два места порождения одного кода (у пакета — для кандидатов, не нашедших ни типа, ни кодов).
  10. P4-10 (план 4): XLSB_NO_FORMATS — у каждого кандидата книги xlsb (P1-5); NUMBER_FORMAT_UNPARSED — одно на кандидата с перечнем форматов всех его листов. Цена: у пояснений xlsb тоже есть XLSB_NO_FORMATS (info).
  11. P4-11 (план 4): Книга вызывает normalize_candidate по одному кандидату, а не normalize: так проверяется дедлайн между кандидатами (header.form_name пакет ставит сам из evidence.title, пара «кандидат → документ» не нужна). Исключение пакета на кандидате — RECOGNITION_FAILED всей книги, как сейчас (TABLE_PARSE_ERROR — код таблицы ИС). Цена: одна сломанная форма роняет книгу с ОСВ.
  12. P4-12 (план 4): Коды ОСВ — по источнику предупреждения, не по тексту: OSV_HIERARCHY_BY_<СТРАТЕГИЯ>, OSV_HEADER, OSV_ROWS, OSV_INN_ROWS, OSV_NO_DATA_ROWS, OSV_BALANCE_IDENTITY, OSV_PARENT_SUM, OSV_TOTAL, OSV_XLSB_NO_OUTLINE; тексты сообщений прежние, кроме иерархии. Цена: внутри OSV_HEADER и OSV_ROWS разные случаи различаются только текстом.
  13. P4-13 (план 4): Заметки читателя: несовпадение формата — FILE_TYPE_MISMATCH файла; «xls без кодовой страницы» — XLS_NO_CODEPAGE файла (info, код пакета, P1-23) в любой книге xls — касается и форм, и ОСВ; «xlsb без outline» — в каждый документ ОСВ книги (OSV_XLSB_NO_OUTLINE), в книге без ОСВ только логируется (иерархия строк — только у ОСВ). Цена: предупреждение о кодировке видно в карточке файла, а не у листа ОСВ.
  14. P4-14 (план 4): Предупреждение «форма не разобрана: книга завершилась ошибкой листа ОСВ» снято: в v2.0 для него нет кода, а текст ошибки и так называет лист ОСВ. Цена: аналитик не видит списка листов форм, оставшихся неразобранными.
  15. P4-15 (план 4): Статус документа ОСВ — прежние условия листа: контрольные суммы §1.4 сошлись и строки ИНН без нарушений (glue.ok, #35). Цена: формулировка спеки «когда контрольные суммы сошлись» читается шире.
  16. P4-16 (план 4): Нет api/arm_statements/VERSION — сервис не стартует (RuntimeError), как при ошибке словаря; в golden service.statements_version — volatile-поле. Цена: копия без VERSION не запустится даже локально.
  17. P4-17 (план 4): Новые синтетические случаи (форма xlsb, книга из пояснений, конфликты продолжений) — в памяти в юнит-тестах (sheet_from_rows, write_xlsb), новых файлов в fixtures/statements-xlsx/synthetic нет; PR fixtures — только golden.
  18. P4-18 (план 4): Манифест получает необязательное поле error_warnings (точное множество кодов error документа); проверка уникальности кодов строк снята (повторы законны, Р-4). Цена: при правке пакета, меняющей набор предупреждений, манифест правится вместе с golden.
  19. P4-19 (план 4): Тест покрытия словарей tabular-parser больше не охватывает словари форм — они в пакете. Цена: «мёртвый» синоним заголовка или метки шапки на реальных файлах не ловится (O-8).
  20. P4-20 (план 4): В tabular-parser — свой change unified-normalization-tp с дельтами statement-parsing, tabular-recognition-service, osv-parsing (по образцу P0-5); требование «Копия контракта идентична источнику» правит план 0, здесь не трогается.
  21. P4-21 (план 4): Golden — отдельные issue, ветка и PR в RSHB/arm-ka-fixtures (один PR на репозиторий); сливается сразу за PR tabular-parser. Цена: короткое окно, когда make check main одного из них красный на golden.
  22. P4-22 (план 4): Две формы на одном листе дают два документа с одинаковым locator (спека уникальности locator не требует; ref строк различаются).
  23. R-2 (план 4): Предупреждение tp #47 «xls без кодовой страницы…» — XLS_NO_CODEPAGE пакета (info, файл, без locator); нового OSV_*-кода нет; текст сообщения reader'а tp #47 сохранён; передаётся флагом BookRead.no_codepage. Цена: правка book.py и golden.
  24. R-5 (план 4): Проверка диапазона numeric(18,2) в display_amount не возвращается (P4-4); диапазон и VALUE_OUT_OF_RANGE выдаёт пакет; старый конвейер удалён задачей 9. Цена: нет после удаления.
  25. R-6 (план 4): Для |raw| ≥ 1e15 текст/значение числа без округления до 15 значащих цифр (иначе число из 17 цифр теряет цифры); пробел групп — обычный U+0020 как в примере docs result-statement-excel.json. Цена: расхождение с NBSP русского Excel, на value/статус не влияет.
  26. R-7 (план 4): Ячейки из одних цифр не считаются подзаголовком даты report_date (иначе год из тройки «число|месяц|год» принимается за подзаголовок), как в прежнем header.py. Цена: подзаголовок из голого числа «2025» не распознаётся.
  27. R-8 (план 4): Ячейка в строке-шапке таблицы (в строке есть подпись «Код» по словарю пакета code_caption) не заголовок формы: подпись колонки «Пояснения» ('^пояснени' в form_titles пакета) делала лист-продолжение НПЗ ещё одной формой DISCLOSURE. Дефект адаптера (спека называет «Пояснения» подписью колонки), пакет не правится. Решение владельца: оставить, посмотреть позже. Цена: компактная шапка, где заголовок формы и «Код» в одной строке, потеряет заголовок.
  28. R-9 (план 4): Предупреждения адаптера при кандидате без документа — предупреждения файла со своим locator; порядок metrics.warnings — сначала reader, затем по номеру первого листа; XLS_NO_CODEPAGE и OSV_XLSB_NO_OUTLINE — флаги BookRead (no_codepage, no_outline). Цена: правка book.py и golden.
  29. R-10 (план 4): НПЗ, ОФР, строки «Реализация иного имущества, прав требования, прочих работ и услуг» (F34, F44): значения 0 с форматом [=0]"-" — только прочерки, суммы нет; по спеке «строка без кода и без содержимого — не строка данных», пакет её не выдаёт; значение прочерка не превращаем в 0 (сломало бы различие null/0 и соотношения §7). test_npz_documents не требует detail/parent_ref 2340. Вопрос владельцу O-8.2: сценарий спеки «Реализация иного имущества… с суммами» не соответствует файлу. Цена: строки из прочерков не видны аналитику.
  30. R-13 (план 4): Ложная склейка/дедлайн: plan_sheets получает deadline (финальное ревью I-1, регрессия таймаута против main). Цена: нет.

План 5 (scan-gateway, change statements-v2)

Источник: openspec/changes/statements-v2/design.md в репозитории scan-gateway, коммит c5e4d0f (ветка исполнения; полный текст — там). Перенесено пунктов: 27.

  1. P5-1 (план 5): Сетка OCR — одна строка сетки на строку ИС по возрастанию row_index, пропуски row_index пустыми строками не заполняются; ширина строк таблицы — max(column_index) + 1. «Плотная по row_index» прочитано как «по порядку row_index», ref всё равно несёт номер ИС. Цена: шаг 2 не видит «дыры» между строками (в реальных json их нет).
  2. P5-2 (план 5): Нарушенная структура таблицы ИС (повтор row_index, повтор column_index в строке, нецелые индексы, rows не список) — исключение адаптера → TABLE_PARSE_ERROR всей таблицы. Сегодня такие ячейки молча пропускались или перезаписывались; повтор row_index сломал бы уникальность ref. Цена: одна битая ячейка теряет таблицу (видно — error).
  3. P5-3 (план 5): value ячейки: None → "", не строка → str(value), без strip; confidence: число (не bool) → [0, 100], иначе 0 (как clamp_confidence, §11.1 «NaN → 0»). Цена: числовой value ИС (не встречался) даст текст "1234.0".
  4. P5-4 (план 5): Ячейка ИС с пустым текстом — Cell("", …), а не None: ИС отдаёт ячейку на каждую позицию строки, None — только там, где ячейки нет. Цена: нет.
  5. P5-5 (план 5): Шапка: поле ИС отсутствует, не объект или form_name пустой → None (а не Cell("", 0)); ref полей шапки — data_ocr.<поле ИС> (пакет ref шапки в выход не переносит — у HeaderField его нет, план 1, раздел API); data_ocr.label (заголовок комплекта) не используется. Цена: <поле>_text пустого поля — null, а не "".
  6. P5-6 (план 5): evidence.title = Cell(label.form_name, …, ref="table:<i>.label"); header.form_name контракта — document.header.form_name пакета (= evidence.title.text как в источнике, пустой — null, план 1 P1-29); шлюз form_name сам не ставит и пару «кандидат → документ» не хранит.
  7. P5-7 (план 5): Успешный json без data_ocr, без table_structure или с пустым table_structure — JSON_FAILED (error): «ничего не выбрасывать молча» (§11, принцип 2). Цена: если ИС вернёт успешный пустой json рядом с нормальными, конверт будет needs_check. Q-1 закрыт решением владельца Р-32 (подтверждает выбор плана).
  8. P5-8 (план 5): Все json набора ошибочны — конверт error с кодом первого, а metrics.warnings всё равно перечисляют JSON_FAILED каждого json (как строки Среза 0): иначе коды остальных json теряются. Цена: спека называет JSON_FAILED «при успешном соседе»; на error-конверт ядро RECOGNIZED не считает, вреда нет. Q-2 закрыт решением владельца Р-33 (подтверждает выбор плана).
  9. P5-9 (план 5): TABLE_PARSE_ERROR.message = BACKEND_PARSING_ERROR: <тип>: <текст> (текст до 300 символов) — код старого §11.1 п. 9 сохранён в тексте для поиска по логам и golden. Цена: нет.
  10. P5-10 (план 5): Предел metrics.warnings (100 структур, message до 1000 символов) сохранён; при переполнении остаются все error, затем info; строки-сводки нет — новый код предупреждения вводится только словарём пакета. Список в конверте и в файле результата одинаков (спека). Цена: при > 100 предупреждениях часть info не видна ни там, ни там (сырой ответ — в raw/).
  11. P5-11 (план 5): statements_version читается при создании OcrNormalizer; нет VERSION — сервис не стартует. Версия едет в NormalizeOutcome.statements_version и в service.statements_version файла результата; в конверт не пишется (в схеме конверта поля нет). Цена: dev-checkout без VERSION не стартует (копию всегда раскладывает скрипт).
  12. P5-12 (план 5): api/rules/ удалён целиком, SCAN_RULES_DIR убран из настроек; словари — default_rules() пакета. Цена: переопределить словарь без релиза нельзя (и не нужно — словари меняются через arm-statements).
  13. P5-13 (план 5): CONTRACT_VERSION = "2.0" — для файла результата и всех конвертов, включая ack; MAJOR запроса — отдельная константа (SUPPORTED_MAJOR = 1).
  14. P5-14 (план 5): Golden v2 — {schema_version, status, error, metrics, documents} файла результата без job_id, file_id, service, raw_s3_key (statements_version меняется при каждой раскладке копии); успешный golden дополнительно проходит схему v2.0. Цена: golden не ловит регресс поля service.
  15. P5-15 (план 5): Перевод в контракт — to_contract / warning_to_contract пакета (план 1, P1-26): у предупреждений документа нет locator и пустых необязательных ключей (схема не допускает null у column_index и field), у нарушений §7 нет column_key, у предупреждений файла locator есть; шлюз только валидирует dict своими моделями. Цена: правка правил перевода — в arm-statements, а не здесь.
  16. P5-16 (план 5): Модули: OcrNormalizer остаётся в src/normalizer/ocr/__init__.py (меньше правок app.py и тестов), адаптер — ocr/adapter.py, обход набора — ocr/answer_set.py, ошибки json — normalizer/files.py (снимает цикл импортов src.normalizer ↔ src.normalizer.ocr); своего конвертера в контракт (contract_map.py) нет.
  17. P5-17 (план 5): Review Focus 3 утверждает done для обоих документов «БО 1 кв 2025» и «Бухгалтерская отчетность 1 кв.25» (proposal, Р-23). Падение при верном кандидате — находка для пакета, а не правка шлюза; ослабить утверждение может только контроллер рулингом.
  18. P5-18 (план 5): metrics.rows — число строк lines всех документов (как в v1.2), теперь включая строки без кода.
  19. P5-19 (план 5): NullNormalizer сохраняется для тестов воркера и выдаёт JSON_FAILED через make_warning пакета.
  20. P5-20 (план 5): Обход набора зовёт normalize_candidate на каждую таблицу, а не normalize: исключение одной таблицы — TABLE_PARSE_ERROR с locator таблицы, остальные таблицы разбираются (план 1: normalize исключения кандидата не перехватывает). AnswerSetResult.documents — документы пакета как есть (заголовок — в header.form_name).
  21. R-1 (план 5): Коммиты ветки несут трейлеры Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (бриф владельца) и Claude-Session: … (план) — совпадает с обоими источниками. Цена ошибки: нет.
  22. R-2 (Р-36) (план 5): Задача 4 копирует из docs/contracts/examples только существующие файлы верхнего уровня как есть, новых примеров контракта верхнего уровня не добавляет (решение владельца Р-36). Цена ошибки: нет.
  23. R-3 (план 5): Усечение до 300 символов (P5-9) применяется к тексту исключения, а не ко всей строке Тип: текст. Цена ошибки: сообщение чуть длиннее 300 символов при длинном имени типа.
  24. R-4 (T4) (план 5): NormalizeOutcome вынесен в src/normalizer/outcome.py с реэкспортом из src.normalizer — иначе цикл импортов. Цена ошибки: лишний модуль; импорт из src.normalizer работает как раньше.
  25. R-5 (T4) (план 5): app.py вызывает OcrNormalizer() (правила default_rules() внутри), а не OcrNormalizer(default_rules()): импорт arm_statements разрешён только в src/normalizer/** и tests/**. Цена ошибки: нет, эквивалентно.
  26. R-6 (T4) (план 5): Сравнение с эталонами в test_golden временно стояло под pytest.skip до задачи 6 (эталоны fixtures были старого формата); в задаче 6 skip снят. Цена ошибки: окно без golden-проверки на ветке между задачами 4 и 6.
  27. R-7 (план 5): Набор «ошибочный json + успешный json без таблиц / без документов» даёт RECOGNITION_FAILED / «ни одной таблицы не разобрано», а код первого ошибочного — только если успешных json нет (чтение Р-32 + Р-33; исправление T3.I-1, commit 3b9e1f6). Цена ошибки: при другом чтении код ошибки конверта в смешанном случае будет другим; ядро на error-конверт RECOGNIZED не считает.

План 6 (arm-web, change statements-v2)

Источник: openspec/changes/statements-v2/design.md в репозитории arm-web, коммит b4b8c04 (ветка исполнения; полный текст — там). Перенесено пунктов: 73.

  1. P6-1 (план 6): Подсказка кода — StatementOut.edition_lines (справочник строк редакции формы, у формы без даты — всех редакций; план 3 P3-22), затем коды строк ok файла, которых в справочнике нет (имя — reference_name ?? line_name); отдельного запроса нет. Обоснование: справочник приходит с ответом формы. Цена ошибки: нет.
  2. P6-2 (план 6): Якорь строки unreadable / missing — строка ok из источника (row_index не null) с наибольшим row_index меньше своего; якорь detail — строка ok из источника с row_index = parent_row_index; нет якоря (в том числе detail с parent_row_index = null или указывающим не на ok) — в начало первого раздела (после ведущих заголовков разделов; у ОФР — в начало таблицы). Вложение встаёт сразу после строки-якоря; несколько вложений одного якоря — по row_index. Обоснование: спека arm-web «Раскладка», S-62 («строка detail без родителя на экране — в начале раздела»), P3-17 (тот же порядок вложений в ответе ядра). Цена ошибки: нет.
  3. P6-3 (план 6): Скрытие (пустые подстроки, расшифровки) применяется после раскладки, фильтром. Обоснование: иначе скрытый якорь сдвинул бы вложения к соседу. Цена ошибки: нет.
  4. P6-4 (план 6): Справочное имя (reference_name) — основное; файловое (line_name, после срезания приклеенных заголовков разделов) показывается вторым серым текстом, только если отличается после нормализации: нижний регистр, ё → е, оставлены только буквы и цифры. Обоснование: переносы OCR («задолжен-ность») не должны давать ложного «отличается». Цена ошибки: лишнее или пропущенное второе имя — только показ.
  5. P6-5 (план 6): Маркеры «у места» строятся из всех записей issues (в том числе relation / soft — у них line_ids + column_id); список IssuesPanel показывает только kind = warning; нарушения §7 остаются списком в ChecksPanel (из checks, с акцептом) и получают ссылку «Показать» по line_ids[0]. Запись с column_id без line_ids (например, COLUMN_WITHOUT_DATE) показывается у заголовка колонки; поле шапки, которого нет в блоке шапки, и строки, которых нет в ответе, — у документа. Обоснование: одна запись не дублируется в двух списках; заголовок колонки — естественное место для предупреждения о дате колонки. Цена ошибки: нет.
  6. P6-6 (план 6): Подтверждение ячейки — в черновике; доступно, только если у строки есть id и у значения есть id (у достроенных ячеек — нет, ядро ответит UNKNOWN_CELL); сумма в черновике той же ячейки отменяет её подтверждение. Обоснование: PATCH: в элементе values ровно одно из amount / confirmed; правка суммы и так снимает needs_review. Цена ошибки: нет.
  7. P6-7 (план 6): Одно действие на строку в черновике (code / detail / confirm), новое заменяет прежнее; пустое поле кода и код, равный текущему, — не действие; у достроенных шаблонных строк (id = null) действий строки нет. Обоснование: 422 INVALID_LINE_PATCH при двух действиях; lines[] адресуется только line_id. Цена ошибки: нет.
  8. P6-8 (план 6): Сумма в шаблонную строку по line_code X и назначение кода X другой строке в одном сохранении — ошибка черновика «Код X назначается строке, а в шаблонную строку X введены суммы — сохраните по очереди», запрос не уходит. Обоснование: ядро ответит 422 UNKNOWN_CELL (P3-10), а сообщение ядра не говорит, что делать. Цена ошибки: нет.
  9. P6-9 (план 6): Ошибка элемента lines[] (409 LINE_CODE_TAKEN, 422 INVALID_LINE_CODE, INVALID_LINE_PATCH, UNKNOWN_LINE, LINE_CODE_REQUIRED) показывается у поля кода строки detail.line_id (текст сервера в подсказке поля и message.error); строки с этим id на экране нет — текстом над таблицей. Обоснование: ядро отдаёт detail.line_id и detail.line_code (план 3 P3-24). Цена ошибки: нет.
  10. P6-10 (план 6): Строка «неразобранная» для кнопки «Подтвердить данные» — line_status ∈ {unreadable, missing} и хотя бы у одного значения amount !== null или string_value !== null (ровно условие ядра для 409 LINES_UNRESOLVED). Обоснование: кнопка и сервер считают одинаково. Цена ошибки: нет.
  11. P6-11 (план 6): Подтверждение даты отчёта — кнопка «Подтвердить дату» у поля из needs_review_fields (уходит header.report_date тем же значением); дата колонки — поле ГГГГ-ММ-ДД у заголовка колонки (как поле даты отчёта) с кнопкой «Подтвердить дату», если дата есть и у колонки COLUMN_DATE_UNCERTAIN / COLUMN_WITHOUT_DATE. Обоснование: перепечатывать ту же дату ради подтверждения неочевидно; формат ввода — как у существующего поля даты. Цена ошибки: нет.
  12. P6-12 (план 6): Панель файлов: у строки документа — значок с числом «предупреждения файла (job.warnings) + issue_counts.error + issue_counts.info» документа, красный при хотя бы одном error (в файле или issue_counts.error > 0); у строки файла без документов — только job.warnings; нуль — значка нет. Клик открывает список: предупреждения файла (message и locator), job.mapping_warnings (как info) и DocumentOut.issues документа, загружаемые при открытии (GET /documents/{id}). Обоснование: DocumentBrief.issue_counts (план 3 P3-23) помечает документ без запроса на каждый; подробности — по клику. Цена ошибки: акцептованное нарушение тоже даёт значок — подробности в списке.
  13. P6-13 (план 6): job.warnings на клиенте нормализуется так же, как ядро: строка → {code: "LEGACY_WARNING", severity: "info", message, locator: null}, объект с code, severity ∈ {error, info}, message — как есть, прочее пропускается, не список → []. Обоснование: экран не падает при ошибке порядка выкладки. Цена ошибки: нет.
  14. P6-14 (план 6): Сценарий T11 — на локальном стенде; строки unreadable / missing / detail и записи issues всех видов подставляются перехватом GET и PATCH …/statement (page.route) из фикстуры плана, пока сервисы шлют 1.x; на реальных формах 1.x — регрессия. Обоснование: до шага 5 выкладки 2.0-данных на стенде нет. Цена ошибки: повторный прогон на aep_3 после шага 5 — follow-up.
  15. P6-15 (план 6): Цвета: ячейка needs_review — #fff7e6, исправлено — #e6f4ff, строка unreadable / missing целиком — #fff1f0, строка ok с needs_review — фон ячеек наименования и кода #fff7e6; подтверждённая ячейка — маленькая серая галочка перед суммой; уровень error — красный значок ExclamationCircleOutlined #cf1322, info — синий InfoCircleOutlined #1677ff. Обоснование: палитра уже используется на экране и в панели файлов. Цена ошибки: только вид.
  16. P6-16 (план 6): Ячейка кода: ok — код; unreadable — серым line_code_text (пусто — «—»), подсказка «Код не распознан: «<текст>»»; missing — серым «—», подсказка «Кода в строке нет»; detail — пусто, подсказка «Расшифровка — в суммы формы не входит»; у ok в подсказке «Уверенность кода: N%» (если меньше 100 и код не правлен), «Код подтверждён», «Код назначен аналитиком». Обоснование: спека: «в ячейке кода серым — сырой текст с подсказкой». Цена ошибки: только вид.
  17. P6-17 (план 6): header.source.<поле>.confidence — string | null (строка Decimal, как прочие уверенности API, план 3 P3-20), в подсказке — Number() и округление до целого. Обоснование: одно правило для всех уверенностей. Цена ошибки: нет.
  18. P6-18 (план 6): node_modules worktree — симлинк на установленный каталог с eslint, prettier и vitest 3.1.4; поэтому вместо make check (он начинается с npm install и поставил бы пакеты в чужой каталог) гоняется make lint typecheck test build — те же проверки без установки. Обоснование: память владельца: симлинк, как в прежних срезах. Цена ошибки: нет; в описании PR сказано, что install пропущен.
  19. P6-19 (план 6): Предупреждения ОСВ-документа (DocumentOut.issues, OSV_*) — блок IssuesPanel над таблицей экрана ОСВ, все записи — у документа (строки ОСВ не адресуются line_ids). Обоснование: спека: «предупреждения листа ОСВ … из DocumentOut.issues[]»; экран документа уже загружает DocumentOut. Цена ошибки: нет.
  20. R-1 (план 6): Шаги «сверка с макетом / прогон в браузере» (T3 s5, T4 s4, T7–T10) перенесены в T11 (дата 2026-10-18). Обоснование: нет стенда ядра с v5.19, общий Playwright-браузер делят параллельные дорожки. Цена ошибки: визуальные дефекты найдутся позже, в T11.
  21. R-2 (план 6): Ссылка «Показать» также у проверок со статусом soft_failed (дата 2026-10-18). Обоснование: согласованное расширение: нарушение так же адресуется строкой. Цена ошибки: только показ.
  22. R-3 (план 6): Компонент меню строки назван LineMenuButton.tsx, а не LineMenu.tsx (дата 2026-10-18). Обоснование: на регистронезависимой ФС (macOS) LineMenu.tsx и lineMenu.ts дают TS1149. Цена ошибки: нет.
  23. O-1 (план 6): Два MODIFIED-требования («Форма ББ/ОФР — просмотр и правка», «Подтверждение документа») составлены исполнителем из текущей спеки: в docs-спеке arm-web все требования ADDED. «Предупреждения ОСВ-документа» (P6-19) в docs-спеке нет; написано по плану. Сверить в arm-ka-docs#116 (дата 2026-10-18). Обоснование: иначе change нельзя было бы описать дельтой. Цена ошибки: расхождение формулировок со спекой docs при архивации.
  24. T1.D-1 (план 6): cellFlags возвращает ещё и confirmed; двум старым ожиданиям добавлен confirmed: false; тест «внимание по уверенности» заменён на «внимание только по needs_review» (дата 2026-10-18). Обоснование: toEqual падает на новом ключе; так велит бриф. Цена ошибки: только тесты.
  25. T1.D-2 (план 6): В тестовый хелпер doc() добавлен issue_counts: {error: 0, info: 0} (дата 2026-10-18). Обоснование: поле обязательно в DocumentBrief, иначе падает tsc. Цена ошибки: нет.
  26. T1.D-3 (план 6): Значения фикстуры statement-v2.json, которых нет в брифе, придуманы исполнителем (форматы сумм, уверенности, text, UUID, codes, сообщения, is_current, пустая колонка c2, порядок edition_lines) (дата 2026-10-18). Обоснование: фикстура должна быть полной. Цена ошибки: тесты завязаны на выдуманные значения; реальные данные — в T11.
  27. T1.D-4 (план 6): Фикстура приведена к StatementOut через as в тесте (дата 2026-10-18). Обоснование: импорт JSON расширяет литералы до string; полноту проверяет рантайм-тест пар «строка × колонка». Цена ошибки: слабая проверка типов фикстуры.
  28. T1.D-5 (план 6): Тест «переносит поля v2» проверяет ключи по l-u7, code-1250, l-1230, l-d2 (дата 2026-10-18). Обоснование: ровно как в брифе. Цена ошибки: нет.
  29. T3.D-1 (план 6): Несколько строк подсказки кода склеиваются переводом строки; у codeModified уверенность не показывается (дата 2026-10-18). Обоснование: читаемость подсказки. Цена ошибки: только вид.
  30. T3.D-2 (план 6): Подсказка сумм принимает необязательный issues (добавляет message) (дата 2026-10-18). Обоснование: задел для T4. Цена ошибки: нет.
  31. T3.D-3 (план 6): Уверенность округляется Math.round(Number()) (P6-17) (дата 2026-10-18). Обоснование: одно правило для всех уверенностей. Цена ошибки: нет.
  32. T3.D-4 (план 6): В режиме правки у подтверждённой ячейки галочка — prefix поля ввода (дата 2026-10-18). Обоснование: ячейка в режиме правки — Input. Цена ошибки: наложение значков, см. T8.D-3.
  33. T3.D-5 (план 6): id строки row-<key> выставляется только на строках-линиях, не на заголовках разделов (дата 2026-10-18). Обоснование: якорь прокрутки нужен только строкам. Цена ошибки: нет.
  34. T4.D-1 (план 6): Подписи мест полей шапки: organization_name — «Организация», inn — «ИНН», report_date — «Дата отчёта», units — «Единица измерения»; неизвестное поле — своим кодом, запись уходит в document (дата 2026-10-18). Обоснование: P6-5. Цена ошибки: только вид.
  35. T4.D-2 (план 6): Запись с field из блока шапки всегда в byField, независимо от line_ids (дата 2026-10-18). Обоснование: поле шапки — самостоятельное место. Цена ошибки: нет.
  36. T4.D-3 (план 6): warning с line_ids без column_id — место «строка …», с column_id — «<строка>, колонка «…»»; relation / soft в список не входят (дата 2026-10-18). Обоснование: P6-5. Цена ошибки: только вид.
  37. T4.D-5 (план 6): onShow панелей передаёт ключ строки; для достроенных строк (id = null) перехода нет — line_ids их не содержат (дата 2026-10-18). Обоснование: нет id — нет адреса. Цена ошибки: нет ссылки «Показать» у достроенной строки.
  38. T4.D-6 (план 6): Значок у наименования строки — только по byRow; записи byCell дают значок только в ячейке (дата 2026-10-18). Обоснование: не дублировать. Цена ошибки: только вид.
  39. T5.D-1 (план 6): Кнопка «Подтвердить дату» рисуется только при переданном onConfirmDate (дата 2026-10-18). Обоснование: иначе мёртвая кнопка. Цена ошибки: нет.
  40. T5.D-2 (план 6): У организации в подсказке при modified — «Распознано: «…»»; нет source — подсказки нет (дата 2026-10-18). Обоснование: нечего показывать. Цена ошибки: нет.
  41. T5.D-3 (план 6): Уверенность в подсказке шапки через Number() ('67.00' → 67%) (дата 2026-10-18). Обоснование: P6-17. Цена ошибки: нет.
  42. T5.D-4 (план 6): Placeholder организации — «Наименование организации» (текст не из плана) (дата 2026-10-18). Обоснование: поле в режиме правки должно быть подписано. Цена ошибки: только вид.
  43. T6.D-1 (план 6): buildStatementPatch для суммы в неизвестную строку возвращает ошибку amount (раньше бросалось исключение) (дата 2026-10-18). Обоснование: экран не должен падать. Цена ошибки: нет.
  44. T6.D-2 (план 6): Подтверждение ячейки и действие строки у строк без id молча пропускаются (дата 2026-10-18). Обоснование: UI их не создаёт (P6-6, P6-7). Цена ошибки: молчаливая потеря действия при ошибке UI.
  45. T6.D-3 (план 6): Код сравнивается после trim; пустой или равный текущему — не действие и не ошибка, проверка до формата (дата 2026-10-18). Обоснование: P6-7. Цена ошибки: нет.
  46. T6.D-4 (план 6): Конфликт P6-8 ищется по сумме в шаблонную строку (id = null) с тем же кодом; key ошибки — lineId строки с действием (дата 2026-10-18). Обоснование: P6-8. Цена ошибки: нет.
  47. T6.D-5 (план 6): Тексты ошибок: «Наименование организации — не длиннее 1000 символов» (длина организации); «Исправьте суммы с неверным форматом» (суммы, как раньше) (дата 2026-10-18). Обоснование: тексты не из плана. Цена ошибки: только вид.
  48. T6.D-6 (план 6): confirm_report_date при отсутствии текущей даты ничего не отправляет (дата 2026-10-18). Обоснование: нечего подтверждать. Цена ошибки: пустое тело PATCH, «Сохранить» молчит.
  49. T6.D-7 (план 6): Ошибка amount подсвечивает ячейку красной рамкой и сама по себе (дата 2026-10-18). Обоснование: раньше подсветка шла только по разбору формата. Цена ошибки: только вид.
  50. T7.D-2 (план 6): Подпись пункта меню: «Это расшифровка» для unreadable / missing, «Сделать расшифровкой» для ok (дата 2026-10-18). Обоснование: разные смыслы действия. Цена ошибки: только вид.
  51. T7.D-3 (план 6): Меню строки у ok — только при origin = 'source'; шаблонные строки с id меню не получают (дата 2026-10-18). Обоснование: бриф. Цена ошибки: нет.
  52. T7.D-4 (план 6): lineErrorTarget читает ApiError.body.detail (отдельного поля detail у ApiError нет) (дата 2026-10-18). Обоснование: иначе деталей ошибки не достать. Цена ошибки: нет.
  53. T7.D-5 (план 6): Ошибка сервера сбрасывается любой правкой черновика (в setDraft) (дата 2026-10-18). Обоснование: иначе устаревшая ошибка висит после правки. Цена ошибки: нет.
  54. T7.D-6 (план 6): У unreadable / missing с действием detail / confirm поле кода остаётся, рядом тег (дата 2026-10-18). Обоснование: бриф: поле всегда. Цена ошибки: см. T7.2: очистка поля теряет действие.
  55. T8.D-1 (план 6): Подтверждение ячейки без id значения молча пропускается в buildStatementPatch; кнопка не рисуется (cellConfirmState → null) (дата 2026-10-18). Обоснование: P6-6. Цена ошибки: нет.
  56. T8.D-2 (план 6): Ожидающая кнопка даты — disabled с текстом «Дата подтверждена (до сохранения)»; без ожидания «Подтвердить дату»; отменить подтверждение даты можно только отменой всей правки. Для колонки ожидание = черновая дата равна period_date. Затем текст подсказки при нажатой кнопке подтверждения ячейки: «Подтверждение будет сохранено — нажмите, чтобы убрать» (дата 2026-10-18). Обоснование: пользователь должен видеть, что подтверждение в черновике. Цена ошибки: текст не из плана; ширина колонки (T8.2) проверяется в T11.
  57. T8.D-3 (план 6): При значке записи у Input paddingLeft: 14, чтобы значок не перекрывал галочку (дата 2026-10-18). Обоснование: без браузера не проверено. Цена ошибки: визуальное наложение; проверка в T11.
  58. T8.D-4 (план 6): cellConfirmState возвращает pending-unconfirm при confirms[key] === false (дата 2026-10-18). Обоснование: нужно третье состояние. Цена ошибки: нет.
  59. T8.D-5 (план 6): Подтверждение ячейки только строкам с lineId (дата 2026-10-18). Обоснование: P6-6, P6-7. Цена ошибки: нет.
  60. T9.D-1 (план 6): «Значения есть» = amount !== null || string_value !== null по любой ячейке; detail не блокирует независимо от значений (дата 2026-10-18). Обоснование: P6-10. Цена ошибки: расхождение с условием ядра.
  61. T9.D-2 (план 6): Ключ ссылки в 409 LINES_UNRESOLVED — line_id (дата 2026-10-18). Обоснование: ключ GridRow для строк из БД. Цена ошибки: ссылка молчит, если строки нет в таблице (T9.6).
  62. T9.D-3 (план 6): Описание Alert подтверждения собирается императивно (confirmDescription); приоритет: failures > lines > duplicate (дата 2026-10-18). Обоснование: линтер запрещает вложенные тернарники. Цена ошибки: только вид.
  63. T10.D-1 (план 6): toFileWarnings отбрасывает объекты без code / message или с severity не error / info (дата 2026-10-18). Обоснование: P6-13. Цена ошибки: нет.
  64. T10.D-2 (план 6): В режиме document у IssuesPanel колонка «место» не показывается; OsvDocument кладёт place '' / targetKey null (дата 2026-10-18). Обоснование: у документа нет места. Цена ошибки: нет.
  65. T10.D-3 (план 6): Подробности документа в поповере грузятся при открытии (trigger=click), состояние сбрасывается при закрытии (дата 2026-10-18). Обоснование: P6-12. Цена ошибки: лишний запрос при повторном открытии.
  66. T10.D-4 (план 6): Значок — <button> под плашкой статуса; без documentId раздел «Предупреждения документа» не показывается (дата 2026-10-18). Обоснование: правила a11y. Цена ошибки: нет.
  67. T10.D-5 (план 6): Пустой список в разделе «Предупреждения документа» — «Нет» (дата 2026-10-18). Обоснование: текст не из плана. Цена ошибки: только вид.
  68. UI-1 (план 6): Тексты, которых нет в плане: «Дата подтверждена (до сохранения)», «Подтверждение будет сохранено — нажмите, чтобы убрать», «Строка не найдена в форме», «Наименование организации — не длиннее 1000 символов» (дата 2026-10-18). Обоснование: нужны для состояний, плана для них не было. Цена ошибки: только формулировки.
  69. T11.D-1 (план 6): Колонки значений 128px вместо 112 (поле даты из 10 символов и сумма с галочкой обрезались), tableLayout='fixed' (дата 2026-10-18). Обоснование: замечание T11.1: колонка «прыгала» при появлении кнопки даты. Цена ошибки: только вид.
  70. T11.D-2 (план 6): Кнопка подтверждения даты — короткая «Подтвердить» / «Подтверждено», длинный текст «Подтвердить дату» / «Дата подтверждена (до сохранения)» в подсказке и aria-label (дата 2026-10-18). Обоснование: фиксированная ширина колонки; отклонение от формулировки «Подтвердить дату» в P6-11. Цена ошибки: только подпись.
  71. T11.D-3 (план 6): Ссылка «Разберите строки без кода: N» — Button type=link отдельной строкой над шапкой карточки, видна и в режиме правки (дата 2026-10-18). Обоснование: клавиатурный доступ; строки нужно разбирать именно при правке. Цена ошибки: только вид.
  72. T11.D-4 (план 6): Список предупреждений документа показывает первые 3 записи (ошибки первыми) и «Показать все (N)»; то же на экране ОСВ (дата 2026-10-18). Обоснование: таблица уезжала ниже сгиба. Цена ошибки: только вид.
  73. T11.D-5 (план 6): Значение со string_value без суммы показывается серым текстом, а не «-» (дата 2026-10-18). Обоснование: аналитик должен видеть нераспознанный текст. Цена ошибки: только вид.

Открытые вопросы исполнения

Вопросы, поднятые при исполнении 18.10.2026 и не закрытые решениями владельца. Ответов здесь нет; источник — где лежит полный текст вопроса. Номера пунктов — из подраздела «Решения при исполнении» выше (где пункт перенесён).

  • O-2 / P3-30 (план 3Б, arm-core). Пустая цель соотношения при непустых членах блокирует подтверждение формы, потерявшей итоговую строку (следствие P1-21 пакета). Источник: arm-core, openspec/changes/unified-normalization-core-b/design.md, P3-30; O-2 — в ledger плана 3.
  • P3-34 (план 3Б). NAME_MISMATCH пересчитывается для всех кодов, а R-1 (Р-40) ограничивает только CODE_NOT_IN_*: пятизначная подстрока с уверенностью < 100 даёт NAME_MISMATCH. Источник: тот же design.md части Б, P3-34.
  • P3-36 (план 3Б). Приём 1.x пишет form_variant и записи relation / soft, но не CODE_NOT_IN_* / NAME_MISMATCH и не трогает needs_review; условие D-6 «проверки сошлись» для 1.x. Источник: design.md части Б, P3-36.
  • P3-41: DISCLOSURE 2.0 (план 3Б). DISCLOSURE в 2.0 — один документ на файл со статусом RECOGNIZED, предупреждения объединённых документов пишутся в его document_issue; уровень error на статус не влияет. Источник: design.md части Б, P3-41.
  • P3-52 (план 3Б). edition_for (граница 66n/fsbu4 по дате для шаблона экрана) не переведён на engine.edition(...): ответ формы и приём могут расходиться в редакции на формах без даты. Источник: design.md части Б, P3-52.
  • O-8.2 плана 4 (tabular-parser). Строки НПЗ ОФР «Реализация иного имущества, прав требования…» состоят из прочерков ([=0]"-"): сценарий спеки «…с суммами» не соответствует файлу, пакет строки не выдаёт (R-10). Источник: ledger плана 4 (.superpowers/sdd/2026-10-18-plan-4-tabular-parser/progress.md), R-10 в design.md unified-normalization-tp.
  • R-8 плана 4. «Пояснения» в адаптере: ячейка в строке-шапке с подписью «Код» не заголовок формы; шапка, где заголовок и «Код» в одной строке, теряет заголовок (T5.3). Владелец: оставить, посмотреть позже. Источник: design.md unified-normalization-tp, R-8.
  • Остаток I-2 правила Р-44. Колонка OCR с потерянной подписью объединённой ячейки: настоящая колонка сумм без подписи может не считаться колонкой сумм. Источник: Р-44 выше; arm-statements #6 (3390f92), docs #123.
  • R-7 плана 5 (scan-gateway). Смешанный набор json («ошибочный + успешный без таблиц / без документов») даёт RECOGNITION_FAILED «ни одной таблицы не разобрано», код первого ошибочного — только если успешных json нет. Источник: scan-gateway, openspec/changes/statements-v2/design.md, R-7 (3b9e1f6).
  • O-3 плана 5. Одиночный суррогат в тексте ячейки OCR не чистится ни пакетом, ни шлюзом (было и раньше). Источник: ledger плана 5 (.superpowers/sdd/2026-10-18-plan-5-scan-gateway/progress.md), O-3, deferred.
  • R-3 и M-5 плана 1 (arm-statements). R-3 (= I-2 задачи 4): parse_code("1220|") → 12201, "|1220" → 11220, результат fixed=True; M-5: смешанные разделители суммы дают «лучшую догадку» ×100. Оба — вопрос владельцу спеки. Источник: ledger плана 1 (arm-statements/.superpowers/sdd/2026-10-18-plan-1-arm-statements/progress.md).
  • M-7 плана 1. spec.md:13 («конвертацию делает хост») против P1-26 (пакет). Источник: ledger плана 1, M-7 (правка спеки в docs PR #117).
  • В-1 плана 6 (arm-web). Приёмка экрана — по перехвату с фикстурой (P6-14) и регрессии на формах 1.x; проверка на реальных 2.0-данных (aep_3, РБ 1 кв. 2025) — позже, после перехода сервисов. Источник: план 6 (plans/2026-10-18-plan-6-arm-web.md, В-1).
  • В-3 плана 6. Подтверждённая ячейка помечается маленькой серой галочкой (P6-15) — в спеке и макете вида нет. Источник: план 6, В-3.
  • P6-11 / T11.D-2 (план 6). Короткая подпись кнопки даты «Подтвердить» / «Подтверждено» отклоняется от P6-11 («Подтвердить дату»). Источник: arm-web, openspec/changes/statements-v2/design.md, T11.D-2; ledger плана 6 (P11.6).
  • T11.9 / T10.2 (план 6). mapping_warnings не входят в значок предупреждений панели файлов (deferred, P6-12). Источник: ledger плана 6 (.superpowers/sdd/2026-10-18-statements-v2/progress.md).
  • P6-19 (план 6). Требования «Предупреждения ОСВ-документа» (блок IssuesPanel на экране ОСВ) нет в спеке docs. Источник: arm-web, design.md statements-v2, P6-19.