Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
41 KiB
Ревью спеки вслепую (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.19financial_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.19financial_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 checktabular-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. Документ становится
DISCLOSUREdone, автоподтверждается, данных нет.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, а в контракте и DBMLperiod_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у 422UNKNOWN_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.clitabular-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 показывают направление, но не точный результат будущего кода.