Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
38 KiB
Повторное ревью спеки вслепую (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 имеет$idarm-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 о нём молчит. В DBMLdefault 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иconfidence100 или пуста)»),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-форма, которую до миграции можно было подтвердить, после миграции получает 409CHECKS_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 удалять данные молча». Сформулировать точно.
- DBML (шапка v5.19) считает
- 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_codenull ⇔ статус не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_codetabular-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).