diff --git a/openspec/changes/unified-statement-normalization/review-2026-10-18-spec.md b/openspec/changes/unified-statement-normalization/review-2026-10-18-spec.md new file mode 100644 index 0000000..51a0764 --- /dev/null +++ b/openspec/changes/unified-statement-normalization/review-2026-10-18-spec.md @@ -0,0 +1,264 @@ +# Ревью спеки вслепую (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 показывают направление, но не точный результат будущего кода. \ No newline at end of file