2026-10-18 01:42:57 +04:00

41 KiB
Raw Blame History

Ревью спеки вслепую (18.10.2026, Opus)

Ревьюер: отдельный агент (модель Opus), без права правки, режим «чёрный ящик» — не открывал design.md и прошлые отчёты; решения писателя S-1…S-25 получил отдельным файлом. Маркеры находок — K-n.

Сверка перед передачей владельцу (сессия): подтверждены по коду K-1 (arm-core/api/src/schemas/statements.py:155-157 — origin по line_code_original_value IS NULL), K-4 (запрет повтора кода в загрузчике словаря имён, arm-core/api/src/domain/statement_lines.py:47-49; повторы 1220/1520 с уверенностью 50 в OCR-фикстурах подтверждались и во втором раунде ревью дизайна), K-12 (HeaderPatch — только inn, report_date, units, extra="forbid").

Отчёт агента ниже без сокращений.


Ревью спеки unified-statement-normalization (режим «чёрный ящик», 18.10.2026)

design.md и review-2026-10-17*.md я не открывал. Файлы не правил, ничего не коммитил.

1. Вердикт

Спека подробная и в основном согласованная. openspec validate unified-statement-normalization --strict проходит, все 10 примеров контракта валидны по схемам v2.0 (scripts/validate_contracts.py). Шаги 2–8 цепочки описаны так, что исполнитель arm-statements может писать код и тесты почти без догадок.

К плану спека пока не готова: блокируют две critical-находки в arm-core.

  • K-1. Признак «строка не из источника» сейчас — line_code_original_value IS NULL. В v2.0 он ломается, а миграция проставляет row_index строкам, созданным правкой. В итоге исходная строка может быть удалена или заблокирована как «шаблонная».
  • K-2. Не сказано, как в v2.0 живут документы, принятые по 1.x, и уже существующие документы. Копия словаря §7 у ядра удаляется, form_variant = null, document_issue пуст. Экран и подтверждение для всех существующих форм потеряют нарушения §7.

До плана нужно также закрыть important-находки:

  • хранение *_text и *_confidence шапки в DBML (K-3);
  • данные упорядоченного перечня кодов (K-5);
  • смысл column_index (K-6);
  • уровень TABLE_PARSE_ERROR и SHEET_OTHER (K-7);
  • окно перехода против теста идентичности копий схем (K-8);
  • механика sync-statements (K-9).

Прогон правил на фикстурах ниже, в K-4 и K-25: сегодняшние done у Excel не ломаются. Но ни один из 6 реальных OCR-документов не станет done, и спека этого не признаёт.

2. Находки

Critical

K-1. Признак «строка создана правкой» и row_index в миграции.

  • Где. specs/arm-core/spec.md: требования «PATCH формы», «Миграция DBML v5.19», «Словари ядра и достройка шаблонных строк». DBML v5.19 financial_statement_line.
  • Как сейчас. origin вычисляется как "template" if line.line_code_original_value is None else "source" (arm-core/api/src/schemas/statements.py:157-159). add_line ставит line_code_original_value=None как признак «не из источника» (arm-core/api/src/repositories/statements.py:223-229).
  • Что меняет v2.0.
    • У строк unreadable / missing / detail из источника line_code пуст. Спека не говорит, что маппер пишет в line_code_original_value.
    • Сценарий «Назначение кода строке-мусору» прямо требует line_code_original_value = null.
    • Правила 409 LINE_EXISTS и «без сумм — удаляется» опираются на понятие «шаблонная строка, созданная правкой аналитика», но признак не определён.
    • Миграция заполняет row_index «по возрастанию line_code внутри формы» у всех строк, включая созданные правкой. Это противоречит «Строки, созданные правкой: row_index = null».
  • Сценарий. Аналитик назначил строке unreadable код 1250 → у неё line_code_original_value = null → для ядра она «шаблонная». Затем он назначает 1250 другой строке:
    • если у первой нет сумм, ядро её удалит — потеря строки источника; это нарушает «первичка не мутируется» и «удаления строк нет»;
    • если суммы есть — ложный 409 LINE_EXISTS.
  • Предложение. Явно определить признак в спеке и DBML, например «создана правкой ⇔ row_index IS NULL». Миграция ставит row_index = NULL строкам с line_code_original_value IS NULL. Маппер 1.x и 2.0 всегда пишет row_index. Отдельно определить, что хранится в line_code_original_value у строк источника, например line_code из результата (null у статусов кроме ok). Сценарий назначения кода переписать без опоры на этот столбец.

K-2. Документы, принятые по 1.x, и мигрированные документы после удаления словаря §7 ядра.

  • Где. specs/arm-core/spec.md («Хранение результата», «Пересчёт после правки», «Словари ядра»), specs/recognition-contract/spec.md («Переход на v2.0»), 05-domain-rules.md §7 «Ядро» и §9.3.
  • Что сказано. Статус 1.x-документа считается «по прежнему правилу D-6». D-6 включает checks_passed, который ядро считает своим словарём (load_check_rules → api/rules/statement_checks.yaml), а этот словарь удаляется. В §7 одновременно сказано: «при приёме 1.x — прежним правилом D-6», «после правки — копией arm_statements».
  • Что не определено:
    • каким набором соотношений считать при form_variant = null (так у 1.x и у мигрированных документов);
    • пишутся ли document_issue (relation / soft) для 1.x при загрузке;
    • заполняет ли миграция document_issue для существующих форм;
    • откуда GET …/statement и POST …/confirm (CHECKS_NOT_PASSED) берут нарушения у документа без записей document_issue.
  • Сценарий. После миграции существующая форма NEEDS_CHECK с жёстким нарушением §7 не имеет записей document_issue. Если чтение идёт из document_issue, экран не показывает нарушение, а подтверждение проходит без акцепта.
  • Предложение. Добавить требование: при form_variant = null пакет использует полный набор. При приёме 1.x и при миграции ядро один раз считает §7 копией пакета и пишет document_issue (source = core). Тогда GET и confirm читают только document_issue. Сценарии: «мигрированная форма с нарушением — CHECKS_NOT_PASSED» и «1.x-документ после PATCH».

Important

K-3. Сырые тексты и уверенности шапки негде хранить.

  • Где. Контракт statementDocument.header.<поле>_text и <поле>_confidence для 9 полей. Спека arm-core: StatementOut отдаёт «сырые тексты полей». DBML v5.19 financial_statement_meta.
  • Проблема. В DBML есть только inn_original_value, inn_confidence, report_date_original_value, units_original_value. Нет organization_name_text / _confidence, report_date_confidence, units_confidence и okud / okpo / okopf / okfs / address _text / _confidence. Не сказано, что *_original_value теперь равно <поле>_text. Поля контракта молча теряются в ядре, экран не может показать подсказку у поля шапки.
  • Предложение. Либо колонки в DBML (или jsonb header_source), либо явное правило «*_original_value := <поле>_text, уверенность хранится только у ИНН, остальное в БД не попадает». Плюс сценарий маппинга шапки.

K-4. На реальных OCR-фикстурах ни один документ не становится done, и спека этого не признаёт. Прогон приближённой реализации правил §11 на fixtures/ocr/*.json:

  • ББ 2025 (aep_6, «БО 1 кв 2025», «Бухгалтерская отчетность 1 кв.25»): §7 сходится, неподсвеченных значений нет. Но коды 1220 и 1520 законно повторены: «НДС, начисленный по отгрузке», «Обязательство по аренде» — тот самый случай AURORA-1431. У одной из копий уверенность 50. Правило Р-16 «у неуверенной копии повтора — needs_review» даёт needs_check на 3 из 3.
  • NAME_MISMATCH на тех же строках. Ядро заблокирует RECOGNIZED ещё и NAME_MISMATCH. Добавить синонимы в словарь имён нельзя: загрузчик запрещает повтор кода (arm-core/api/src/domain/statement_lines.py:47-49), а S-4 сверяет только «по редакциям».
  • ОФР (3 из 3): 2400, 2500, 2411 с уверенностью 66–71 участвуют только в мягких соотношениях, поэтому не покрыты → needs_review → needs_check. В §11.9 признаны только «25xx / 29xx», про 2400 / 2410 / 2411 / 2412 / 2460 ничего нет.
  • Регресса нет: golden сегодня тоже needs_check. Но гайд фронта обещает «Форма из OCR теперь бывает RECOGNIZED».
  • Предложение.
    • (а) Разрешить повтор кода в упорядоченном перечне (fsbu4: 1220, 1220; 1520, 1520), чтобы структура подтверждала обе копии. Р-16 применять только к повтору, которого нет в перечне.
    • (б) Словарь имён — список имён на код (синонимы).
    • (в) Явно записать в §11.9: «2400 и строки 241x/2460 на уверенности».
    • (г) Добавить сценарии на эти случаи.

K-5. Данные упорядоченного перечня кодов не заданы.

  • Где. specs/statement-normalization «Шаг 8 — подтверждение неуверенного кода», §11.9.
  • Проблема.
    • Источник описан как «коды шаблонов банка + коды печатных форм 1С (1105, 1530, 2420, 2460, 2930 и др.)»: перечня «и др.» нет ни в одном документе.
    • Состав fsbu4 открыт (OQ-150).
    • Перечни упрощённого варианта для fsbu4 не даны.
    • Порядок для 1С-кодов (где стоят 1105, 1215, 2411/2412) не указан.
    • Не сказано, допустимы ли повторы (см. K-4).
  • От перечня зависят needs_review строки, статус и CODE_NOT_IN_EDITION. Исполнитель arm-statements не сможет написать golden-тест.
  • Предложение. Положить перечни в fixtures / docs как данные (yaml), сделать ссылку из спеки, оформить OQ на состав.

K-6. Смысл column_index не определён.

  • Проблема.
    • OCR отдаёт разреженные индексы (0, 1, 2, 4, 5), в примерах контракта колонки 0, 1.
    • Не сказано, columns[].column_index — это индекс колонки исходной сетки или порядковый номер колонки значений.
    • COLUMN_SKIPPED и COLUMN_WITHOUT_DATE несут column_index, а у пропущенной колонки нет financial_statement_column. document_issue хранит только column_id, поэтому привязку нельзя ни записать, ни показать.
    • line_refs могут указывать на строки ролей title / numbering, которых нет среди lines.
  • Предложение. Определить column_index. Для предупреждений о колонках вне columns[] добавить в document_issue поле column_index или правило «без привязки, у документа».

K-7. TABLE_PARSE_ERROR и SHEET_OTHER объявлены как предупреждения документа, но документа нет.

  • Где. Таблица §11.9 и шаг 8 спеки: уровень «error» / «info» без пометки «(файл)». В scan-gateway/spec.md — «предупреждение … документа-кандидата».
  • Проблема. При исключении в разборе таблицы документа нет: сейчас шлюз пишет строку в предупреждения файла (scan-gateway/api/src/normalizer/ocr/__init__.py:92-93). Для листа «прочее» документа тоже нет. Исполнителю придётся выдумывать пустой statementDocument с неопределённым document_type.
  • Предложение. Сделать оба предупреждения уровня файла с locator (file:…;table:i, sheet:…). TABLE_PARSE_ERROR уровня error тогда блокирует RECOGNIZED всего файла, как JSON_FAILED.

K-8. Окно выкладки противоречит требованию «Копия контракта в сервисе идентична источнику» — оно не изменено.

  • После слияния docs (шаг 2) make check tabular-parser и scan-gateway падает на тесте идентичности до их PR шага 5. Любой посторонний PR в эти сервисы в окне не пройдёт обязательную проверку.
  • Ядру для маппера 1.x нужны схемы и примеры 1.x. В docs они заменены на месте ($comment: «Схема 1.2 — в истории git … и в моделях маппера 1.x»), а ядро проверяет примеры моделями.
  • Предложение. MODIFIED-требование для окна перехода: в docs держать recognition-result.v1.schema.json, recognition-result-message.v1.schema.json и примеры 1.x до шага 6. Тест идентичности сервиса сверяет копию с той MAJOR, которую сервис пишет. Добавить сценарий.

K-9. sync-statements описан недостаточно.

  • Не определено:
    • где лежит копия в хосте (api/src/arm_statements?), как её видят mypy src, ruff и pytest;
    • где хранится маркер коммита и как он попадает в service.statements_version во время работы;
    • что возвращает --check, когда копия просто отстала (у sync-web.sh это код 10);
    • что делать без локального checkout arm-statements.
  • Требование quality-checks («make check-full») не MODIFIED, хотя check-full теперь вызывает sync-statements. dev/checks.md тоже не дополнен.
  • Предложение. Отдельное требование с кодами 0 / 10 / 20 и путями, MODIFIED quality-checks.

K-10. DISCLOSURE по type_hint выбрасывает таблицу без проверки кодов.

  • Где. Шаг 6: «DISCLOSURE по type_hint — шаги 2–7 не выполняются».
  • Сценарий. ИС подписала таблицу баланса как «Пояснения к бухгалтерскому балансу», или лист Excel называется «Расшифровка», а внутри коды 1110–1700. Документ становится DISCLOSURE done, автоподтверждается, данных нет. TYPE_CONFLICT не срабатывает, принцип «ничего не выбрасывать молча» нарушен.
  • Предложение. Шаг 2 (столбец кодов) выполнять всегда. Если большинство кодов в отрезке ББ или ОФР, тип определяется по кодам и выдаётся TYPE_CONFLICT.

K-11. Упрощённый ОФР 2400 = … жёсткий (S-5), а в полном наборе то же 2400 мягкое из-за знака 2410 (OQ-12). Это противоречие в правилах, и образца нет. На упрощённой форме с доходом по налогу (2410 отрицательный) модуль abs_terms даёт ложное нарушение и needs_check. Предложение — в S-5 ниже.

K-12. NO_ORGANIZATION_NO_INN снимается «правкой organization_name», но PATCH её не принимает.

  • §11.9 и спека arm-core («Предупреждения… — document_issue») говорят, что предупреждение снимается правкой organization_name.
  • §9.5 п. 9 разрешает в PATCH только inn, report_date, units; схема HeaderPatch — extra="forbid" (arm-core/api/src/schemas/statements.py:318-329).
  • Предложение. Либо добавить organization_name в PATCH (с аудитом), либо убрать его из правила снятия.

Minor

K-13. У document_issue нет codes, а API обещает checks с codes (их пришлось бы разбирать из текста relation). Не сказано, какие code и severity пишутся для записей видов relation и soft. Предложение: колонка codes jsonb и правило «relation → error, soft → info, code = RELATION_FAILED / RELATION_SOFT».

K-14. Не определён интерфейс функции статуса. Это одна функция для сервиса (по FormCandidate) и для ядра (по строкам БД): какие входы, откуда ядро берёт «раскладка найдена». Также неоднозначно «содержимое» строки данных: считается ли неразобранная ячейка кода с цифрами без значений содержимым. Пример — aep_6, строка "44 30\nI |" при пустых значениях.

K-15. Строка нумерации (S-13) строгая. Сбой OCR в нумерации («1 2 З 4») превращает её в строку unreadable с «суммами» 3, 4, 5 (код «2» не код, но с цифрой): документ needs_check и LINES_UNRESOLVED до ручной отметки. В Excel mezheninovskaya в столбце кодов строк нумерации стоит «2» (строки 15, 37, 49) — правило должно их ловить. Нужны сценарии с двойником и без первой ячейки.

K-16. LINES_UNRESOLVED считает только amount не null. Строка unreadable / missing, у которой есть только string_value (мусор 211(1 | в aep_3), подтверждение не блокирует, хотя сумма потеряна.

K-17. У бланка 66н только с относительными подписями неуверенная дата отчёта никогда не подтверждается (нет абсолютных дат колонок) → всегда REPORT_DATE_UNCERTAIN error. Это следует признать в §11.9.

K-18. Разнобой формулировок:

  • в шаге 2 спеки period_end, а в контракте и DBML period_date;
  • «DISCLOSURE без columns / lines», а схема требует пустые массивы;
  • статус ОСВ: «ведомость разобрана» (контракт, спека tabular-parser) против «контрольные суммы §1.4 сошлись» (§1.6, S-22);
  • в §9.5 п. 9 осталось «Код и название строки … не правятся» рядом с блоком «С v2.0»;
  • ядро сортирует «по возрастанию кода» (как сейчас, schemas/statements.py:221), а спека называет это «как на бумаге»;
  • 409 LINE_EXISTS совпадает по имени с текущей константой-сообщением LINE_EXISTS у 422 UNKNOWN_CELL (services/statements.py:49, :239-240).

K-19. Откат миграции на БД с данными v2.0 (повторы кода, пустые коды) невозможен из-за возврата уникального (meta_id, line_code). Надо оговорить: откат гарантирован только на пустой или совместимой БД.

K-20. «Того же периода» для DUPLICATE_FORM и RECOGNIZED: по period_year / period_quarter или по точной дате — не сказано.

K-21. Для 1.x не сказано, где хранится LEGACY_WARNING (в document_issue или в metrics). У 1.x-заданий statements_version = null, поэтому любой PATCH будет порождать STATEMENTS_VERSION_MISMATCH.

K-22. Словаря единиц нет в перечне содержимого пакета. В §11.2 адаптер собирает ОКЕИ, а в header контракта поля okei нет (в DBML okei есть).

K-23. Пробелы PATCH:

  • нет снятия подтверждения;
  • не определён смысл line_status: "ok" (кроме 422);
  • не сказано, сбрасывает ли акцепт подтверждение без правки («любой правкой»);
  • не сказано, допустим ли columns[].period_date = null;
  • нет пути «detail → обратно в missing».

K-24. Ядро не проверяет согласованность document.status = done из результата. Если сервис ошибся и прислал done со строками unreadable, автоподтверждение обойдёт LINES_UNRESOLVED. Дешёвая страховка: done ⇒ нет unreadable / missing с суммой и нет needs_review.

K-25 (на данных Excel, информационно). Прогнал CLI tabular-parser на всех 25 книгах fixtures/statements-xlsx и пересчитал §7 по правилу «посчитанного» соотношения.

  • done-документы манифеста (mezheninovskaya ББ/ОФР, НПЗ ББ/ОФР, unagrande ОФР «Лист 1» ×2) новых жёстких нарушений не получают. Их строки без кода уходят в detail («Авансы по инвестиционной деятельности», «Реализация иного имущества…»), а строки шапки выше строки заголовка — в title.
  • Меняются только уже needs_check документы: страницы 2 ББ («Б.Б. Лист 2», «ББ. Лист 2» ×2, st05) получают 1600 = 1700 (пустая цель), страницы 1 «только актив» — то же нарушение как «пустые члены».
  • Значит, failed_relations в tabular-parser/api/tests/statements_manifest.yaml надо обновить. Спека этого не упоминает.

3. Решения писателя S-1…S-25

S-n Суть Оценка Обоснование
S-1 Дельта к существующей recognition-contract согласен Способность уже есть, MODIFIED корректны, validate проходит.
S-2 Конверт тоже v2.0 согласен Строка → объект в metrics.warnings — смена типа, это MAJOR. Ядро и так принимает 1.x и 2.0, включая ack. Но см. K-8 про копии схем.
S-3 Диапазоны 3100–3699, 4100–4499, 5000–5999, 6100–6499 согласен На фикстурах годы и суммы кодами не становятся; правится словарём.
S-4 Правило совпадения наименований согласен с оговоркой На 3 из 3 реальных ББ 2025 законные повторы 1220 и 1520 дают NAME_MISMATCH, а добавить синонимы в словарь имён нельзя (K-4). Вхождение подстроки от 5 символов слишком широко для итогов: «итого по разделу i» входит в «…ii» и «…iii», так что подмену 1100 ↔ 1200 ↔ 1300 по имени не поймать, держится только на структуре. Предлагаю синонимы на код и исключать из правила вхождения строки «итого по разделу».
S-5 Формулы упрощённого набора, все жёсткие не согласен (частично) Формулы ББ и ОФР по прил. 5 верны. Но 2400 в полном наборе мягкое из-за знака 2410 (OQ-12). Сценарий: упрощённый ОФР с доходом по налогу, 2410 отрицательный → модуль abs_terms → ложное жёсткое нарушение и needs_check. Предлагаю ББ оставить жёсткими, ОФР 2400 сделать мягким до образца (OQ-166).
S-6 Словари подписей «код» и «наименование» согласен Совпадает с фикстурами («Код», «Наименование показателя»); пополняется данными.
S-7 Прочерк — пусто, is_recognized = true согласен с оговоркой Совпадает с Excel (литерал [=0]"-" → нет значения). Но OCR-тире на месте потерянной цифры с уверенностью 100 ловит только §7, а на строках только с мягкими проверками (2400, 25xx) не ловится ничем. Отменяет допущение OQ-148 — нужно явное утверждение.
S-8 Каталог кодов; code — строка по шаблону; VALUE_OUT_OF_RANGE — info согласен Такое значение и так needs_review (amount = null, не покрывается), так что info статус не ослабляет.
S-9 locator у предупреждения файла согласен Нужен ещё для TABLE_PARSE_ERROR и SHEET_OTHER (K-7).
S-10 needs_review_fields — только report_date согласен с оговоркой Логика верна (НПЗ ОФР без ИНН остаётся done), но сырые тексты и уверенности ИНН и организации негде хранить (K-3). Меняет смысл «needs_review у полей шапки».
S-11 Значения period_kind, 29.02 → 28.02 согласен Покрывает бланк 66н. В DBML это varchar, в контракте enum: новое значение — MINOR, «сначала потребители».
S-12 Ожидаемые даты: + 31.12 прошлого и позапрошлого года согласен Без этого любой квартальный ББ с неуверенной подписью давал бы error. Но это расширение набора дизайна — смысл меняется, нужно утверждение.
S-13 Строка нумерации — ≥ 3 целых подряд с 1 согласен с оговоркой Сбой OCR («1 2 З 4», пропущенная «1») превращает её в unreadable с «суммами» и блокирует подтверждение (K-15). Предлагаю двойники внутри детектора и допуск одного пропуска.
S-14 Повторная шапка: подпись кода или ≥ 2 совпавших ячейки согласен На фикстурах повторная шапка всегда с «Код».
S-15 row_index с 0, ref, разлепленные #k с 1 согласен с оговоркой Не сказано, как экранируются имена листов с ! и пробелами в Excel-ref ('Лист 1'!C14?). В примере Бухгалтерский баланс. Лист 1!C14 — без кавычек.
S-16 Уверенность кода с заменой двойника ≤ 99 согласен Позволяет ядру после правки признать код неуверенным по одному сохранённому полю.
S-17 Формат lines в PATCH и коды 422 согласен с оговоркой Не определены line_status: "ok", снятие подтверждения, «detail → обратно» (K-23). 409 LINE_EXISTS совпадает по имени с текущей константой-сообщением (K-18).
S-18 Детали 409 согласен Достаточно для экрана.
S-19 Имена DBML, line_ids в jsonb, без confirmed_by у строки согласен с оговоркой jsonb без FK допустим, но шаблонная строка, удаляемая при назначении кода, оставит висячий line_id в предупреждениях. Не хватает codes и column_index (K-6, K-13).
S-20 related_group.crm_id уникален, 409 CRM_ID_EXISTS согласен Как у ГК и ЮЛ; существующие данные — null, конфликтов нет. Но это новое ограничение — нужно утверждение.
S-21 Значения по умолчанию при приёме 1.x согласен с оговоркой Не сказано, где хранится LEGACY_WARNING, каким набором §7 считать 1.x и есть ли для них document_issue (K-2, K-21).
S-22 Статус ОСВ по контрольным суммам §1.4 листа согласен с оговоркой По сути верно (паритет с конвертом), но контракт и спека tabular-parser говорят «разобрана». Нужна одна формулировка (K-18).
S-23 statements_version обязателен при statementDocument согласен Схема это проверяет; книга только с ОСВ без него валидна.
S-24 Редакция перечня: fsbu4 после 2024-12-31 согласен То же правило, что edition_for ядра; без даты — обе редакции.
S-25 Не конец квартала → NEEDS_CHECK, ручное подтверждение — 409 PERIOD_REQUIRED согласен Снимает противоречие «RECOGNIZED, но не подтверждаемый» и автоподтверждения. Это пересмотр оговорки дизайна — нужно явное утверждение.

Решения, которые меняют смысл, а не уточняют значение (утвердить явно):

  • S-4 — состав условия RECOGNIZED и правило совпадения имён;
  • S-5 — жёсткость упрощённых формул;
  • S-7 — закрывает OQ-148 противоположно прежнему допущению;
  • S-10 — ИНН и организация не входят в needs_review;
  • S-12 — расширенный набор ожидаемых дат;
  • S-20 — новое ограничение уникальности;
  • S-22 — определение статуса ОСВ;
  • S-25 — пересмотр оговорки дизайна про квартал;
  • S-13 и S-14 — эвристики, от которых прямо зависят статусы строк.

4. Что проверил и признал согласованным

Согласовано:

  • Формат OpenSpec. openspec validate … --strict проходит. Имена MODIFIED совпадают с openspec/specs/recognition-contract/spec.md. В MODIFIED «Файл результата» сокращено описание extra у ОСВ (в живой спеке после архивации потеряется формулировка «заголовок колонки → строка или null») — мелочь.
  • Контракт v2.0 ↔ спека шагов.
    • line_code равен null тогда и только тогда, когда статус не ok; parent_ref обязателен у detail.
    • checks есть у ББ/ОФР со строками и отсутствует без строк; duplicate_line_codes удалён.
    • statements_version условно обязателен.
    • Перечни period_kind, form_variant, severity совпадают со спекой и §11.
  • Примеры. Все 10 валидны с проверкой форматов. Арифметику обоих примеров форм пересчитал вручную:
    • покрытие "42831 1" соотношением 1200 и разлепленные 1370/1300 сходятся;
    • в result-statement-excel.json мягкое 1110 = 11101 и жёсткое 2200 с пустой целью соответствуют правилам.
  • §7, §11, §9.3, §9.5 ↔ спека.
    • Порядок 409 одинаков в спеке arm-core, §9.5 п. 4 и гайде фронта.
    • Коды 422 и 409 PATCH одинаковы.
    • Уровни предупреждений в таблице шага 8 и §11.9 совпадают.
    • Условие RECOGNIZED одинаково в спеке и §9.3.
  • DBML v5.19 ↔ спека по строкам, значениям, колонкам, document_issue, file_ordinal, crm_id, частичным индексам и заполнению при миграции — кроме K-1, K-3, K-6, K-13.
  • Правила на фикстурах.
    • Двойники в кодах: «I1О0» → 1100, «| 140» → 1140.
    • «44 30\nI |» aep_6 не разлепляется.
    • «1500\n1700» разлепляется.
    • Excel done остаются done (K-25).

Файлы и символы, которые смотрел:

  • Change: openspec/changes/unified-statement-normalization/proposal.md, specs/{statement-normalization,recognition-contract,tabular-recognition-service,scan-gateway,arm-core,arm-web}/spec.md.
  • Живые спеки: openspec/specs/{recognition-contract,quality-checks}/spec.md.
  • Контракт: contracts/recognition-result.schema.json, contracts/recognition-result-message.schema.json, contracts/examples/*, scripts/validate_contracts.py.
  • Правила и вопросы: context/05-domain-rules.md §1.6, §7, §9.2–§9.5, §11–§12 и диф от 43b4b80; context/07-open-questions.md (OQ-12, 36, 139, 141, 142, 145–150, 152, 156, 157, 165–167).
  • Модель: model/data-model-v5.19.dbml (диф с v5.18), model/entities-v5.19.md.
  • Прочее docs: dev/frontend-api-guide.md §5.6.1, dev/checks.md, Makefile, scripts/sync-web.sh.
  • Решения писателя: /private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/12de9a9a-5665-41d0-beea-0651270d88ec/scratchpad/spec-review/writer-decisions.md.
  • Фикстуры:
    • fixtures/ocr/{aep_3,aep_6,БО 1 кв 2025,Бухгалтерская отчетность 1 кв.25}.json и expected_normalize/*;
    • fixtures/statements-xlsx/** (25 книг через python -m src.parser.cli tabular-parser);
    • tabular-parser/api/tests/statements_manifest.yaml.
  • arm-core: api/src/services/statements.py (_apply_cells, _resolve_cells, build_out, patch, LINE_EXISTS), api/src/schemas/statements.py (line_origin, _lines_out, HeaderPatch), api/src/repositories/statements.py (add_line, value_flags), api/src/domain/statement_lines.py (_form_lines), api/src/domain/statement_mapping.py (values_ok, statement_status), api/src/models/statements.py, api/rules/statement_lines.yaml, api/contracts/.
  • tabular-parser: api/src/parser/statements/checks.py (_holds, run_statement_checks), api/rules/statement_checks.yaml, number_format._literal_result.
  • scan-gateway: api/src/normalizer/ocr/__init__.py (обработка исключения таблицы), tests/unit/normalizer/test_dates.py::test_parses_real_forms (существует).

Скрипты прогона (только для воспроизведения, в репозитории их нет):

  • /private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/12de9a9a-5665-41d0-beea-0651270d88ec/scratchpad/rev/scripts/sim_excel.py
  • /private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/12de9a9a-5665-41d0-beea-0651270d88ec/scratchpad/rev/scripts/ocr_sim.py
  • /private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/12de9a9a-5665-41d0-beea-0651270d88ec/scratchpad/rev/scripts/ocr_full.py
  • /private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/12de9a9a-5665-41d0-beea-0651270d88ec/scratchpad/rev/scripts/scan_xls.py

OCR-прогон — приближённая модель правил §11, а не реализация: цифры K-4 показывают направление, но не точный результат будущего кода.