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

264 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Ревью спеки вслепую (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 показывают направление, но не точный результат будущего кода.