2026-10-18 02:38:24 +04:00

38 KiB
Raw Blame History

Повторное ревью спеки вслепую (18.10.2026, Opus, коммит 248f8b0)

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

Сверка перед передачей владельцу (сессия): подтверждены L-3 (DBML v5.19 — уникальность (stored_file_id, file_ordinal) на всех документах), L-4 (arm-core/api/src/schemas/files.py — JobBrief.warnings: list[str]), L-5 (сценарий «3450» в спеке statement-normalization при отрезке 3100–3699).

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


Ревью спеки unified-statement-normalization (вслепую, коммит 248f8b0)

1. Вердикт

Спека зрелая. Цепочка шагов 1–8, контракт v2.0, DBML v5.19, §7/§9/§11 и перечень кодов в основном согласованы. openspec validate --strict проходит, validate_contracts.py зелёный (18 примеров + перечень). Схемы *.v1.schema.json побайтно равны схемам из 43b4b80.

К написанию плана спека условно готова. Сначала нужно закрыть одну критическую находку и примерно десять важных.

Что блокирует план:

  • L-1. Не определено, входят ли значения строк detail в needs_review и в статус. От этого зависит, станут ли 4 из 6 реальных OCR-документов done. Нормативный текст спеки противоречит §11.4.
  • Переход и выкладка:
    • правило выбора схемы по $id ломает тест идентичности для recognition-request и для примеров;
    • ядро не входит в шаг 0;
    • file_ordinal задан только у форм, а уникальный индекс стоит на всех документах;
    • предупреждения ОСВ и JobBrief.warnings в 2.0 никуда не отображены.
  • Раскладка: не задано, как колонки сегмента со своей шапкой сопоставляются с колонками документа. Это напрямую касается сегодняшнего done НПЗ (листы 4 ББ и 3 ОФР со своей шапкой).
  • Разбор суммы: нет правила «какие символы выбрасываются и когда amount = null». Сценарий 211(1 | → string_value из этих правил не выводится.

Что проверено на данных:

  • Excel из манифеста не даёт ни одной строки unreadable/missing.
  • Нумерация Межениновской (строки 24 и 49) распознаётся новым детектором.
  • Сегодняшние done сохраняются, если L-6 решить «по порядку» или «по дате».

2. Находки

critical

L-1. needs_review значений строк detail и статус документа.

  • Где: statement-normalization — «Шаг 8 — needs_review значения» («SHALL вычислять needs_review у каждого значения») и «Шаг 8 — статус» («нет значений… с needs_review»). Против: §11.4 «detail… в проверки §7, агрегаты и done она не входит». В arm-web строки detail скрыты переключателем. Проверка согласованности ядра (§9.3, S-48) тоже смотрит «значение с needs_review».
  • Проблема: у значения строки detail нет кода, поэтому покрыть его соотношением нельзя. Любая такая ячейка с уверенностью ниже 100 даёт needs_review = true, и по нормативному тексту документ уходит в needs_check. При этом причина спрятана под переключателем.
  • Проверено скриптом на фикстурах (scratchpad/rev3/cov.py). В ББ и ОФР РБ («Бухгалтерская отчетность 1 кв.25») и Счастливочной («БО 1 кв 2025») все неуверенные значения строк с кодом покрыты соотношениями. Жёсткие соотношения сходятся. Неуверенные коды подтверждаются перечнем fsbu4 целиком, разлепленные 1500/1700 тоже покрыты. Держат документ только значения строк detail ('86 157'@67, '134'@33, '(115 464)'@78 и др.).
  • Итог: если detail исключить, 4 документа из 6 станут done; если включить — 0. Ожидание «доля done небольшая» (proposal, §11.9) верно только во втором случае, но явно этого нигде не сказано.
  • Предложение: явно записать в шаге 8 и в функции статуса, что значения detail в done не входят (needs_review = false или исключаются из входа функции; подсказку оставить по text/confidence). Либо наоборот — оставить и потребовать у экрана счётчик «скрытых сомнительных ячеек» у переключателя. В любом случае нужен сценарий на РБ.

important

L-2. Тест идентичности на окне перехода: правило $id неполно.

  • Где: recognition-contract, требование «Копия контракта…», шаг 0 proposal, dev/checks.md.
  • Проблемы:
    • recognition-request.schema.json в docs имеет $id arm-ka/recognition-request/v1, а файла recognition-request.v1.schema.json нет. По написанному правилу тест ищет несуществующий файл и падает либо молча пропускается («недоступно — пропуск»).
    • На шаге 0 (до слияния docs) файлов *.v1.schema.json ещё нет вообще.
    • Существующие тесты (arm-core/api/tests/test_contract.py:109-125, tabular-parser/api/tests/unit/test_contract.py:113-126, scan-gateway/api/tests/unit/test_contract.py:120-131) сравнивают весь набор *.json через rglob, включая examples/. После шага 2 в docs появятся examples/v1/*, *.v1.schema.json, а examples/*.json станут v2.0. Набор и байты не совпадут у всех трёх репозиториев.
    • У ядра такой же тест, но в шаге 0 ядра нет: между шагами 2 и 3 make check в main arm-core красный.
  • Предложение:
    • файл <имя>.v1.schema.json брать, только если он есть, иначе основной;
    • явно описать сравнение примеров (копия v1 ↔ examples/v1/, файлы другого MAJOR игнорировать);
    • включить arm-core в шаг 0 (или сливать шаги 2 и 3 одновременно);
    • добавить сценарий «запрос v1.0 после слияния v2.0».

L-3. file_ordinal и уникальный (stored_file_id, file_ordinal).

  • Где: arm-core «Хранение результата формы v2.0» — file_ordinal назначается только BALANCE/INCOME. Маппер 1.x о нём молчит. В DBML default 0 и unique (stored_file_id, file_ordinal).
  • Сценарий: книга с двумя листами ОСВ, или PDF ББ+ОФР по 1.x → два документа с file_ordinal = 0 → нарушение уникальности → файл ERROR.
  • Предложение: «каждый document (ОСВ, формы, DISCLOSURE/OTHER) при любом MAJOR получает file_ordinal = индекс в documents[]». Добавить сценарий «книга из двух ОСВ».

L-4. Предупреждения файла и ОСВ в 2.0 никуда не отображены в ядре и API.

  • metrics.warnings становится массивом структур, а 1.x тоже конвертируется в структуры LEGACY_WARNING. При этом JobBrief.warnings: list[str] (arm-core/api/src/schemas/files.py:59) читается через _strings(metrics.get("warnings")) (services/documents.py:67). Структуры отфильтруются, и карточка файла потеряет JSON_FAILED/TABLE_LOST/FILE_TYPE_MISMATCH.
  • osvDocument.warnings (OSV_*) и osvDocument.status в спеке arm-core не отображены никуда. Сегодня предупреждения ОСВ видны в job.warnings (строки sheet=…), после перехода tabular-parser на 2.0 они исчезнут из UI.
  • В recognition_job будут лежать и строки (старые задания), и структуры.
  • Гайд фронта утверждает «поля добавляются, старые не удаляются» — для JobBrief.warnings это неверно.
  • Предложение: требование в arm-core — форма JobBrief.warnings (структуры или message), где хранятся и показываются предупреждения ОСВ, используется ли osvDocument.status.

L-5. Ошибка в сценарии «Год и сумма — не код».

  • Где: statement-normalization «Диапазоны кодов».
  • Проблема: «3450» лежит внутри отрезка 3100–3699 (отчёт об изменениях капитала), то есть по правилу это код. Тест, написанный по сценарию, упадёт.
  • Предложение: заменить на число вне отрезков (1050, 7450) или ограничить «кодовые диапазоны» для поиска столбца.

L-6. Сопоставление колонок сегмента со своей шапкой не задано.

  • Где: «Шаг 2 — сегменты», «Шаг 2 — номер колонки».
  • Проблема: правило «по порядку» задано только для сегментов без шапки. Для повторной шапки (OCR) и листа-продолжения со своей шапкой (НПЗ «Бухгалтерский баланс. Лист 4» — колонки 8–10 при 16/21/24 у первого листа; «Отчет о финансовых результата_3») не сказано, становятся ли колонки новыми columns[] или как они сопоставляются (по порядку? по period_date?).
  • Сценарий: исполнитель создаёт новые колонки → суммы разных листов в разных колонках → §7 НПЗ ББ ломается → сегодняшний done становится needs_check.
  • Предложение: «колонки сегмента с шапкой сопоставляются с колонками первого сегмента по period_date, иначе по порядку; число не совпало — SEGMENT_COLUMNS_MISMATCH». Добавить сценарий на НПЗ.

L-7. parse_amount: не задано, какие символы выбрасываются и когда amount = null.

  • Где: шаг 4.
  • Проблема: правило перечисляет двойники, |, пробелы, скобки, прочерк. Но сценарии требуют выбрасывать :, =, ■, • («| : 122477» → 122477), а сценарии arm-core и scan-gateway ждут, что 211(1 | даст string_value без amount.
  • Текущий amounts.py (scan-gateway/api/src/normalizer/ocr/amounts.py:56-90) выбрасывает всё, кроме цифр, +-., и дал бы 211(1 | → 2111, (Ю4) → −4.
  • Предложение: явное правило — какой мусор выбрасывается (с is_recognized = false), когда результат null + string_value (непарная скобка, буква внутри цифр, доля мусора). Сценарии на 211(1 |, (Ю4), : : 2856983 = ■.

L-8. sync-statements --check: код 20 при дрейфе перечня кодов в docs.

  • Проблема: правка statement-code-orders.yaml в docs до обновления arm-statements и пересинхронизации даёт check-full = 20 (красный) во всех хостах. Это противоречит обоснованию S-30 («отставшая копия не должна блокировать посторонние PR»).
  • Предложение: сверять копию перечня с версией docs, соответствующей копии (или выдавать 10, если копия равна дереву своего VERSION). Либо зафиксировать порядок «docs и arm-statements сливаются в один день».

L-9. Полярность needs_review при приёме 1.x и в миграции.

  • Где: recognition-contract («needs_review значения — по прежнему values_ok (is_recognized не false и confidence 100 или пуста)»), arm-core «Миграция», §9.5, шапка DBML.
  • Проблема: в тексте описано условие OK, будто это значение needs_review. Буквальная реализация ставит needs_review = true чистым значениям. Сценария, ловящего это, нет.
  • Предложение: «needs_review = NOT values_ok» и сценарий.

L-10. Публичный API пакета для пересчёта в ядре не описан.

  • Спека задаёт вход функции статуса, но не вход пересчёта §7 и needs_review из строк БД.
  • Не описано, как передать:
    • is_modified/is_confirmed/line_code_modified;
    • исключение правленых ячеек из голосования знака;
    • form_variant, эффективную дату для редакции;
    • строки шаблонные и detail;
    • что делать с needs_review_fields и form_variant после правки кода или даты колонки.
  • Риск: пакет пишется под FormCandidate сервисов, и ядро не может им воспользоваться без реконструкции сетки.
  • Предложение: требование «Точка входа пакета для пересчёта по сохранённым данным» с типами и сценарием.

L-11. Следствие одобренного S-49: правило «итого по разделу — только равенство» ломается на реальной фикстуре.

  • Счастливочная: разлепленная строка 1500 (уверенность 56) с наименованием «Итого по разделу V1» → NAME_MISMATCH → NEEDS_CHECK, хотя документ полностью сходится.
  • РБ: «Итого по разделу V /» совпадает, только если после нормализации обрезаются крайние пробелы. Нормализация этого не говорит.
  • Предложение: сравнивать для «итого по разделу» римский номер раздела (первый токен после фразы), явно делать strip.
  • Кроме того: словарь имён ядра fsbu4 не содержит 1105/1130/1140/1530/2460. Синонимы Р-20 (1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410 УСН) нужно явно потребовать добавить в statement_lines.yaml. Сейчас это есть только в сценарии. Без этого законные повторы РБ/Счастливочной/aep_6 дают NAME_MISMATCH, а неуверенный 1105 всегда даёт NAME_MISMATCH.

L-12. Страховка ядра (S-48) уже функции статуса.

  • Проверка «согласован» не включает строку unreadable без значений, layout_found, has_organization_or_inn, period_known.
  • Сценарий: сервис по ошибке прислал done при строке unreadable без значений → ядро RECOGNIZED и автоподтверждение, хотя функция статуса даёт needs_check.
  • Предложение: «согласован ⇔ общая функция статуса по входу, собранному из результата, даёт done». Это не пересчёт needs_review и §7, а вызов той же функции.

L-13. Миграция задним числом меняет подтверждаемость существующих форм.

  • document_issue при миграции считается по новому правилу «посчитанного» соотношения. Пустая цель теперь нарушение: страницы 2 ББ без 1600, «только актив».
  • Сценарий: NEEDS_CHECK-форма, которую до миграции можно было подтвердить, после миграции получает 409 CHECKS_NOT_PASSED.
  • Импорт живого кода пакета из ревизии Alembic невоспроизводим (S-36 это признаёт).
  • Предложение: признать следствие в спеке (сценарий) или считать при миграции по прежнему правилу. Вынести вопрос владельцу.

minor

  • L-14. §11.4 приводит «изо :» как пример unreadable, но цифр там нет. По правилу это пустой код, и в aep_3 строка (1130, значение '3'@0) уйдёт в detail и будет скрыта. Исправить пример или правило (считать цифрой и двойники).
  • L-15. «Ближайшая строка с кодом выше» для detail: не сказано, ok ли это (если выше стоит unreadable). При отметке ok → detail не сказано про перепривязку детей этой строки (parent_row_index).
  • L-16. Ручная дата колонки (PATCH columns) не меняет period_kind. Если колонка была relative:*, следующая правка report_date перезапишет ручную дату. Нужно: ручная дата → absolute.
  • L-17. COLUMN_DATE_UNCERTAIN нельзя снять подтверждением той же даты: «запрос без изменений — 200 без эффектов». Нужен аналог S-42 для колонок.
  • L-18. Подпись OCR с уверенностью ниже 100 подтверждается «ожидаемыми датами» от эффективной даты. Если даты отчёта нет, эффективная дата может определяться этой же подписью, и она подтверждает сама себя. Исключить её при вычислении.
  • L-19. LIS:
    • не определено, что значит «уверенные коды — якоря» (обязательное включение или вес);
    • не сказано, что делать при конфликте якорей между собой или с перечнем;
    • form_variant определяется «по структуре кодов» — каких строк (ok?);
    • пересчитывается ли form_variant после назначения кода — не сказано.
  • L-20. «Форма записи знака по text» для {38378) не определена: { не принимается за открывающую скобку. Нужно «форма знака — по результату разбора: скобки / минус / без знака». Не сказано и то, входит ли сама ячейка в «большинство».
  • L-21. CODE_NOT_IN_EDITION и CODE_NOT_IN_DICTIONARY: какая редакция берётся — по дате (edition) или выбранная LIS. Межениновская на бланке 66н в 2025 году получит info на 1120.
  • L-22. В схеме form_variant не входит в required, хотя спека говорит «SHALL иметь». OTHER в 2.0 никогда не порождается (лист «прочее» → SHEET_OTHER, документа нет), но остался в enum и в правилах ядра.
  • L-23. Сценарий arm-core «Два баланса в одном файле… ни один не автоподтверждается» безусловный. Правило Р-18/S-45 требует того же периода.
  • L-24. Откат:
    • DBML (шапка v5.19) считает crm_id групп несовместимостью для downgrade, спека — нет;
    • downgrade неизбежно удаляет is_confirmed, document_issue, header_source, что противоречит «MUST NOT удалять данные молча». Сформулировать точно.
  • L-25. Механика копии arm_statements:
    • VERSION лежит в копии, но его нет в дереве источника — исключить при сравнении;
    • зависимости пакета (PyYAML и др.) должны быть в requirements.txt хостов (зеркало банка);
    • mypy не поддерживает конфиг каталога копии: строгость хоста применяется к коду пакета, а править копию нельзя. Нужно требование «пакет проходит самый строгий конфиг хостов»;
    • conftest хоста может влиять на тесты пакета.
  • L-26. «Шаг 1 — многостраничная форма» сформулирован для «каждого адаптера», но у OCR кандидат строится на каждую таблицу ИС, склейки нет. Явно ограничить требование Excel.
  • L-27. TABLE_LOST — «таблица ИС с содержимым» без определения содержимого таблицы.
  • L-28. validate_contracts.py пишет «не строка из 4 цифр», а регулярка допускает 5. Не проверяет, что число копий повтора совпадает с repeats.
  • L-29. Сценарий 1.x в recognition-contract («обе строки кода — по прежнему правилу «остаётся первая»») читается двусмысленно.
  • L-30. Значение с текстом без цифр и уверенностью 100 ('I'@100 в строке без кода, aep_6 строка 38) не делает строку строкой данных и молча теряется. Это противоречит принципу «ничего не выбрасывать молча». Признать или включить в «содержимое».

3. Решения писателя

S-n Суть Оценка Обоснование
S-4 (утв., Р-22 / S-49) Совпадение наименований согласен с оговоркой (следствие) Утверждено. Следствие на фикстурах: «Итого по разделу V1» у Счастливочной → NAME_MISMATCH; не задан strip (L-11).
S-5 (утв. с изм.) Упрощённый набор, ОФР 2400 мягкое согласен Утверждено. В упрощённом ОФР нет ни одного жёсткого соотношения, покрытие только мягким точно — так и задумано.
S-7 (утв.) Прочерк = пусто, true согласен Тире на месте цифры ловит §7.
S-10 (утв.) needs_review_fields только для даты согласен Сформулировано одинаково во всех документах.
S-12 (утв.) Ожидаемые даты + 31.12 согласен с оговоркой Замкнутость при отсутствии даты отчёта (L-18).
S-13 → S-40 Детектор нумерации согласен Проверено на Межениновской (строки 24, 49) — нумерация распознаётся.
S-14 (утв.) Признак повторной шапки согласен Не задано сопоставление колонок такого сегмента (L-6).
S-15 (утв. с ог.) ref, row_index, экранирование листа согласен Примеры приведены к правилу.
S-17 → S-43/S-47 Формат PATCH lines согласен —
S-19 (ог.) Имена DBML, codes, column_index согласен с оговоркой file_ordinal уникален для всех документов, а спека задаёт его только формам (L-3).
S-20 (утв.) crm_id уникален согласен Добавить в условия отката (L-24).
S-21 → S-36 Значения по умолчанию 1.x согласен с оговоркой Полярность needs_review (L-9).
S-22 (утв.) Статус ОСВ согласен с оговоркой Ядро osvDocument.status и warnings нигде не потребляет (L-4).
S-24 → S-26 Редакция в сервисах — см. S-26.
S-25 (утв.) Квартал → NEEDS_CHECK, PERIOD_REQUIRED согласен —
S-26 Всегда обе редакции, дата решает ничью согласен Межениновская 66н-2025 иначе не подтвердилась бы. Меняет смысл дизайна (выбор по дате), владельцем не утверждено.
S-27 Перечень в docs, байтовая копия в пакете согласен с оговоркой Дрейф перечня делает все хосты красными (L-8).
S-28 Только коды с образцом согласен с оговоркой Старые бланки 66н (2421/2430/2450) с неуверенным кодом дадут needs_review; приемлемо при OQ-168.
S-29 Выбор схемы по $id не согласен recognition-request ($id /v1, без .v1-файла), примеры и ядро не покрыты; на шаге 0 .v1-файлов ещё нет. Предложение — L-2: брать .v1, только если файл существует; правило для examples/; ядро в шаг 0.
S-30 Механика sync-statements согласен с оговоркой VERSION в сравнении, зависимости, mypy-конфиг, код 20 при дрейфе перечня (L-8, L-25).
S-31 column_index = столбец сетки согласен с оговоркой Нужно правило для сегментов со своей шапкой (L-6). Меняет смысл поля контракта — допустимо в MAJOR.
S-32 TABLE_PARSE_ERROR, SHEET_OTHER — уровень файла согласен —
S-33 header_source jsonb согласен —
S-34 PATCH organization_name согласен с оговоркой Нужен, иначе NO_ORGANIZATION_NO_INN не снять. Расширение функционала — не утверждено владельцем.
S-35 RELATION_FAILED/RELATION_SOFT, codes согласен —
S-36 §7 один раз для 1.x и в миграции согласен с оговоркой Задним числом меняет подтверждаемость существующих форм; импорт живого пакета из Alembic (L-13). Меняет смысл — не утверждено.
S-37 Строка правки ⇔ row_index IS NULL согласен Миграция корректна (маппер 3b всегда пишет line_code_original_value, repositories/statements.py:80). Отступление от дизайна — не утверждено.
S-38 Вход функции статуса; unreadable без значений согласен с оговоркой Не решён вопрос detail в needs_review (L-1, critical).
S-39 Столбец кодов ищется и у DISCLOSURE согласен —
S-40 Детектор нумерации с двойниками и пропуском согласен Ложных срабатываний на фикстурах нет.
S-41 LINES_UNRESOLVED — сумма или текст согласен Мусор на месте суммы требует разбора. Расширяет Р-13 — не утверждено.
S-42 Подтверждение даты тем же значением согласен с оговоркой Нужен аналог для даты колонки (L-17).
S-43 Имена (LINE_CODE_TAKEN, period_date) согласен —
S-44 Откат согласен с оговоркой crm_id и неизбежная потеря новых колонок (L-24).
S-45 «Тот же период» = (год, квартал) согласен Поправить безусловный сценарий (L-23).
S-46 Единицы и ОКЕИ согласен —
S-47 Пробелы PATCH; подтверждение не сбрасывает акцепт согласен по существу Сужает правило Среза 3b «акцепт сбрасывается любой правкой» — ждёт владельца.
S-48 Страховка done → RESULT_STATUS_INCONSISTENT согласен с оговоркой Проверка уже функции статуса; предлагается вызывать саму функцию (L-12). Ядро переопределяет статус сервиса — не утверждено.
S-49 Синонимы; «итого по разделу» — только равенство согласен с оговоркой Ломается на «Итого по разделу V1»; синонимы Р-20 и 1105 явно добавить в словарь (L-11).
S-50 Толкование Р-21 согласен Проверено на РБ/Счастливочной: 2400/2410/2411/25xx покрываются мягкими точно.
S-51 Ожидания на реальных сканах согласен с оговоркой Верно, только если значения detail держат статус; иначе 4 из 6 документов done (L-1).
S-52 Манифест tabular-parser согласен Подтверждено разбором: строк unreadable/missing в Excel нет; страницы 2 ББ дают 1600 = 1700 с пустой целью.

Меняют смысл и ещё не утверждены владельцем: S-26, S-34, S-36, S-37, S-41, S-47 (явно вынесено владельцу), S-48. Пограничные: S-31 (смысл поля контракта) и S-38 (unreadable без значений → needs_check, но подтверждение не блокирует).

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

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

  • Поля контракта v2.0 ↔ DBML v5.19 ↔ маппинг arm-core: строки, значения, колонки, шапка (header_source, *_original_value), document_issue (kind/code/severity/codes/column_id/column_index/field), statements_version, form_variant, needs_review_fields.
  • Условные требования схемы: line_code null ⇔ статус не ok; parent_ref у detail; checks у BALANCE/INCOME со строками; statements_version при наличии формы.
  • Порядок 409 одинаков в спеке, §9.5 п. 4 и гайде фронта. Коды 422 одинаковы.
  • Таблица предупреждений спеки и §11.9 совпадают по кодам и уровням.
  • Миграция row_index согласуется с текущим кодом ядра.
  • sync-statements --check на текущих данных пройдёт: все коды словаря ядра есть в перечне своей редакции. В обратную сторону в словаре нет 1231; 2530, 2930; 1105, 1130, 1140, 1530; 2460.
  • Сценарии разбора кода и сумм («1220 ,», «| 140», «I1О0», «1 2О5», «(7744536}», «4,200», «42831 1») выводятся из правил. Исключения — «3450» (L-5) и «| : 122477» (L-7).
  • Формат OpenSpec: MODIFIED в recognition-contract и quality-checks содержат полный текст с прежними сценариями, заголовки совпадают с openspec/specs. REMOVED нет, и он не нужен.
  • Ручной прогон aep_3 и aep_6 подтверждает обещанное: missing «Выручка», detail «БАЛАНС» с нарушением 1600 = 1100 + 1200, unreadable «44 30\nI |» без разлепления, законные повторы 1220/1520/2410 и раздел III НКО укладываются в перечни fsbu4/66n.

Файлы:

  • openspec/changes/unified-statement-normalization/proposal.md и все specs/*/spec.md;
  • openspec/specs/recognition-contract/spec.md, openspec/specs/quality-checks/spec.md;
  • docs/contracts/*.schema.json (v2 и v1), statement-code-orders.yaml, examples/*, examples/v1/*, scripts/validate_contracts.py (запущен), Makefile;
  • context/05-domain-rules.md §1.6, §7, §9.3–§9.5, §11–§12;
  • context/07-open-questions.md (диф), context/03-architecture.md (диф);
  • model/data-model-v5.19.dbml (диф к v5.18), model/entities-v5.19.md (диф);
  • dev/frontend-api-guide.md §5.6.1, dev/checks.md (диф);
  • файл решений writer-decisions-2.md.

Код и данные:

  • fixtures/ocr/*.json (все таблицы aep_3, aep_6, РБ и Счастливочной за 1 кв. 2025) и fixtures/ocr/expected_normalize/*;
  • манифест tabular-parser/api/tests/statements_manifest.yaml и все его Excel-файлы — прогоном через read_book_detected, find_forms, find_layout, line_code tabular-parser;
  • tabular-parser: api/rules/statement_checks.yaml, src/parser/statements/statement.py, book.py, grid.py;
  • scan-gateway: src/normalizer/ocr/amounts.py, layout.py;
  • arm-core: api/rules/statement_lines.yaml, src/schemas/files.py (JobBrief), src/services/documents.py:39-68, src/repositories/statements.py:80, src/schemas/statements.py:158, tests/test_contract.py:100-125;
  • тесты идентичности контракта сервисов (tests/unit/test_contract.py) и Dockerfile трёх репозиториев (COPY api/ — копия пакета в образ попадёт).

Codegraph не использовал: нужные места знал точно, читал файлы напрямую.

Скрипты прогона лежат в /private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/12de9a9a-5665-41d0-beea-0651270d88ec/scratchpad/rev3/: dump.py (таблицы ИС), cov.py (покрытие §7 на OCR), xl.py и xl2.py (статусы строк и колонки Excel).