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

241 lines
38 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, коммит 248f8b0)
Ревьюер: отдельный агент (модель Opus), без права правки, режим «чёрный ящик» — не открывал design.md и прошлые
отчёты; решения писателя S-1…S-52 получил отдельным файлом. Маркеры находок — L-n.
Сверка перед передачей владельцу (сессия): подтверждены L-3 (DBML v5.19 — уникальность `(stored_file_id, file_ordinal)`
на всех документах), L-4 (`arm-core/api/src/schemas/files.py` — `JobBrief.warnings: list[str]`), L-5 (сценарий «3450»
в спеке statement-normalization при отрезке 3100–3699).
Отчёт агента ниже без сокращений.
---
# Ревью спеки `unified-statement-normalization` (вслепую, коммит 248f8b0)
## 1. Вердикт
Спека зрелая. Цепочка шагов 1–8, контракт v2.0, DBML v5.19, §7/§9/§11 и перечень кодов в основном согласованы. `openspec validate --strict` проходит, `validate_contracts.py` зелёный (18 примеров + перечень). Схемы `*.v1.schema.json` побайтно равны схемам из 43b4b80.
К написанию плана спека **условно готова**. Сначала нужно закрыть одну критическую находку и примерно десять важных.
**Что блокирует план:**
- **L-1.** Не определено, входят ли значения строк `detail` в `needs_review` и в статус. От этого зависит, станут ли 4 из 6 реальных OCR-документов `done`. Нормативный текст спеки противоречит §11.4.
- **Переход и выкладка:**
- правило выбора схемы по `$id` ломает тест идентичности для `recognition-request` и для примеров;
- ядро не входит в шаг 0;
- `file_ordinal` задан только у форм, а уникальный индекс стоит на всех документах;
- предупреждения ОСВ и `JobBrief.warnings` в 2.0 никуда не отображены.
- **Раскладка:** не задано, как колонки сегмента со своей шапкой сопоставляются с колонками документа. Это напрямую касается сегодняшнего `done` НПЗ (листы 4 ББ и 3 ОФР со своей шапкой).
- **Разбор суммы:** нет правила «какие символы выбрасываются и когда `amount = null`». Сценарий `211(1 |` → `string_value` из этих правил не выводится.
**Что проверено на данных:**
- Excel из манифеста не даёт ни одной строки `unreadable`/`missing`.
- Нумерация Межениновской (строки 24 и 49) распознаётся новым детектором.
- Сегодняшние `done` сохраняются, если L-6 решить «по порядку» или «по дате».
## 2. Находки
### critical
**L-1. `needs_review` значений строк `detail` и статус документа.**
- Где: `statement-normalization` — «Шаг 8 — `needs_review` значения» («SHALL вычислять `needs_review` у каждого значения») и «Шаг 8 — статус» («нет значений… с `needs_review`»). Против: §11.4 «`detail`… в проверки §7, агрегаты и `done` она не входит». В `arm-web` строки `detail` скрыты переключателем. Проверка согласованности ядра (§9.3, S-48) тоже смотрит «значение с `needs_review`».
- Проблема: у значения строки `detail` нет кода, поэтому покрыть его соотношением нельзя. Любая такая ячейка с уверенностью ниже 100 даёт `needs_review = true`, и по нормативному тексту документ уходит в `needs_check`. При этом причина спрятана под переключателем.
- Проверено скриптом на фикстурах (`scratchpad/rev3/cov.py`). В ББ и ОФР РБ («Бухгалтерская отчетность 1 кв.25») и Счастливочной («БО 1 кв 2025») **все** неуверенные значения строк с кодом покрыты соотношениями. Жёсткие соотношения сходятся. Неуверенные коды подтверждаются перечнем `fsbu4` целиком, разлепленные 1500/1700 тоже покрыты. Держат документ только значения строк `detail` (`'86 157'@67`, `'134'@33`, `'(115 464)'@78` и др.).
- Итог: если `detail` исключить, 4 документа из 6 станут `done`; если включить — 0. Ожидание «доля done небольшая» (proposal, §11.9) верно только во втором случае, но явно этого нигде не сказано.
- Предложение: явно записать в шаге 8 и в функции статуса, что значения `detail` в `done` не входят (`needs_review = false` или исключаются из входа функции; подсказку оставить по `text`/`confidence`). Либо наоборот — оставить и потребовать у экрана счётчик «скрытых сомнительных ячеек» у переключателя. В любом случае нужен сценарий на РБ.
### important
**L-2. Тест идентичности на окне перехода: правило `$id` неполно.**
- Где: `recognition-contract`, требование «Копия контракта…», шаг 0 proposal, `dev/checks.md`.
- Проблемы:
- `recognition-request.schema.json` в docs имеет `$id` `arm-ka/recognition-request/v1`, а файла `recognition-request.v1.schema.json` нет. По написанному правилу тест ищет несуществующий файл и падает либо молча пропускается («недоступно — пропуск»).
- На шаге 0 (до слияния docs) файлов `*.v1.schema.json` ещё нет вообще.
- Существующие тесты (`arm-core/api/tests/test_contract.py:109-125`, `tabular-parser/api/tests/unit/test_contract.py:113-126`, `scan-gateway/api/tests/unit/test_contract.py:120-131`) сравнивают **весь набор `*.json` через `rglob`, включая `examples/`**. После шага 2 в docs появятся `examples/v1/*`, `*.v1.schema.json`, а `examples/*.json` станут v2.0. Набор и байты не совпадут у всех трёх репозиториев.
- У ядра такой же тест, но в шаге 0 ядра нет: между шагами 2 и 3 `make check` в main arm-core красный.
- Предложение:
- файл `<имя>.v1.schema.json` брать, только если он есть, иначе основной;
- явно описать сравнение примеров (копия v1 ↔ `examples/v1/`, файлы другого MAJOR игнорировать);
- включить arm-core в шаг 0 (или сливать шаги 2 и 3 одновременно);
- добавить сценарий «запрос v1.0 после слияния v2.0».
**L-3. `file_ordinal` и уникальный `(stored_file_id, file_ordinal)`.**
- Где: `arm-core` «Хранение результата формы v2.0» — `file_ordinal` назначается только `BALANCE`/`INCOME`. Маппер 1.x о нём молчит. В DBML `default 0` и `unique (stored_file_id, file_ordinal)`.
- Сценарий: книга с двумя листами ОСВ, или PDF ББ+ОФР по 1.x → два документа с `file_ordinal = 0` → нарушение уникальности → файл `ERROR`.
- Предложение: «каждый `document` (ОСВ, формы, `DISCLOSURE`/`OTHER`) при любом MAJOR получает `file_ordinal` = индекс в `documents[]`». Добавить сценарий «книга из двух ОСВ».
**L-4. Предупреждения файла и ОСВ в 2.0 никуда не отображены в ядре и API.**
- `metrics.warnings` становится массивом структур, а 1.x тоже конвертируется в структуры `LEGACY_WARNING`. При этом `JobBrief.warnings: list[str]` (`arm-core/api/src/schemas/files.py:59`) читается через `_strings(metrics.get("warnings"))` (`services/documents.py:67`). Структуры отфильтруются, и карточка файла потеряет `JSON_FAILED`/`TABLE_LOST`/`FILE_TYPE_MISMATCH`.
- `osvDocument.warnings` (`OSV_*`) и `osvDocument.status` в спеке `arm-core` не отображены никуда. Сегодня предупреждения ОСВ видны в `job.warnings` (строки `sheet=…`), после перехода tabular-parser на 2.0 они исчезнут из UI.
- В recognition_job будут лежать и строки (старые задания), и структуры.
- Гайд фронта утверждает «поля добавляются, старые не удаляются» — для `JobBrief.warnings` это неверно.
- Предложение: требование в `arm-core` — форма `JobBrief.warnings` (структуры или `message`), где хранятся и показываются предупреждения ОСВ, используется ли `osvDocument.status`.
**L-5. Ошибка в сценарии «Год и сумма — не код».**
- Где: `statement-normalization` «Диапазоны кодов».
- Проблема: «3450» лежит внутри отрезка 3100–3699 (отчёт об изменениях капитала), то есть по правилу это код. Тест, написанный по сценарию, упадёт.
- Предложение: заменить на число вне отрезков (1050, 7450) или ограничить «кодовые диапазоны» для поиска столбца.
**L-6. Сопоставление колонок сегмента со своей шапкой не задано.**
- Где: «Шаг 2 — сегменты», «Шаг 2 — номер колонки».
- Проблема: правило «по порядку» задано только для сегментов **без** шапки. Для повторной шапки (OCR) и листа-продолжения со своей шапкой (НПЗ «Бухгалтерский баланс. Лист 4» — колонки 8–10 при 16/21/24 у первого листа; «Отчет о финансовых результата_3») не сказано, становятся ли колонки новыми `columns[]` или как они сопоставляются (по порядку? по `period_date`?).
- Сценарий: исполнитель создаёт новые колонки → суммы разных листов в разных колонках → §7 НПЗ ББ ломается → сегодняшний `done` становится `needs_check`.
- Предложение: «колонки сегмента с шапкой сопоставляются с колонками первого сегмента по `period_date`, иначе по порядку; число не совпало — `SEGMENT_COLUMNS_MISMATCH`». Добавить сценарий на НПЗ.
**L-7. `parse_amount`: не задано, какие символы выбрасываются и когда `amount = null`.**
- Где: шаг 4.
- Проблема: правило перечисляет двойники, `|`, пробелы, скобки, прочерк. Но сценарии требуют выбрасывать `:`, `=`, `■`, `•` («| : 122477» → 122477), а сценарии `arm-core` и `scan-gateway` ждут, что `211(1 |` даст `string_value` без `amount`.
- Текущий `amounts.py` (`scan-gateway/api/src/normalizer/ocr/amounts.py:56-90`) выбрасывает всё, кроме цифр, `+-.`, и дал бы `211(1 |` → 2111, `(Ю4)` → −4.
- Предложение: явное правило — какой мусор выбрасывается (с `is_recognized = false`), когда результат `null` + `string_value` (непарная скобка, буква внутри цифр, доля мусора). Сценарии на `211(1 |`, `(Ю4)`, `: : 2856983 = ■`.
**L-8. `sync-statements --check`: код 20 при дрейфе перечня кодов в docs.**
- Проблема: правка `statement-code-orders.yaml` в docs до обновления `arm-statements` и пересинхронизации даёт `check-full` = 20 (красный) во **всех** хостах. Это противоречит обоснованию S-30 («отставшая копия не должна блокировать посторонние PR»).
- Предложение: сверять копию перечня с версией docs, соответствующей копии (или выдавать 10, если копия равна дереву своего `VERSION`). Либо зафиксировать порядок «docs и arm-statements сливаются в один день».
**L-9. Полярность `needs_review` при приёме 1.x и в миграции.**
- Где: `recognition-contract` («`needs_review` значения — по прежнему `values_ok` (`is_recognized` не `false` и `confidence` 100 или пуста)»), `arm-core` «Миграция», §9.5, шапка DBML.
- Проблема: в тексте описано условие **OK**, будто это значение `needs_review`. Буквальная реализация ставит `needs_review = true` чистым значениям. Сценария, ловящего это, нет.
- Предложение: «`needs_review = NOT values_ok`» и сценарий.
**L-10. Публичный API пакета для пересчёта в ядре не описан.**
- Спека задаёт вход функции статуса, но не вход пересчёта §7 и `needs_review` из строк БД.
- Не описано, как передать:
- `is_modified`/`is_confirmed`/`line_code_modified`;
- исключение правленых ячеек из голосования знака;
- `form_variant`, эффективную дату для редакции;
- строки шаблонные и `detail`;
- что делать с `needs_review_fields` и `form_variant` после правки кода или даты колонки.
- Риск: пакет пишется под `FormCandidate` сервисов, и ядро не может им воспользоваться без реконструкции сетки.
- Предложение: требование «Точка входа пакета для пересчёта по сохранённым данным» с типами и сценарием.
**L-11. Следствие одобренного S-49: правило «итого по разделу — только равенство» ломается на реальной фикстуре.**
- Счастливочная: разлепленная строка 1500 (уверенность 56) с наименованием «Итого по разделу V1» → `NAME_MISMATCH` → `NEEDS_CHECK`, хотя документ полностью сходится.
- РБ: «Итого по разделу V /» совпадает, только если после нормализации обрезаются крайние пробелы. Нормализация этого не говорит.
- Предложение: сравнивать для «итого по разделу» римский номер раздела (первый токен после фразы), явно делать `strip`.
- Кроме того: словарь имён ядра fsbu4 не содержит 1105/1130/1140/1530/2460. Синонимы Р-20 (1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410 УСН) нужно **явно** потребовать добавить в `statement_lines.yaml`. Сейчас это есть только в сценарии. Без этого законные повторы РБ/Счастливочной/aep_6 дают `NAME_MISMATCH`, а неуверенный 1105 всегда даёт `NAME_MISMATCH`.
**L-12. Страховка ядра (S-48) уже функции статуса.**
- Проверка «согласован» не включает строку `unreadable` без значений, `layout_found`, `has_organization_or_inn`, `period_known`.
- Сценарий: сервис по ошибке прислал `done` при строке `unreadable` без значений → ядро `RECOGNIZED` и автоподтверждение, хотя функция статуса даёт `needs_check`.
- Предложение: «согласован ⇔ общая функция статуса по входу, собранному из результата, даёт `done`». Это не пересчёт `needs_review` и §7, а вызов той же функции.
**L-13. Миграция задним числом меняет подтверждаемость существующих форм.**
- `document_issue` при миграции считается по новому правилу «посчитанного» соотношения. Пустая цель теперь нарушение: страницы 2 ББ без 1600, «только актив».
- Сценарий: `NEEDS_CHECK`-форма, которую до миграции можно было подтвердить, после миграции получает 409 `CHECKS_NOT_PASSED`.
- Импорт живого кода пакета из ревизии Alembic невоспроизводим (S-36 это признаёт).
- Предложение: признать следствие в спеке (сценарий) или считать при миграции по прежнему правилу. Вынести вопрос владельцу.
### minor
- **L-14.** §11.4 приводит «изо :» как пример `unreadable`, но цифр там нет. По правилу это пустой код, и в aep_3 строка (1130, значение `'3'@0`) уйдёт в `detail` и будет скрыта. Исправить пример или правило (считать цифрой и двойники).
- **L-15.** «Ближайшая строка с кодом выше» для `detail`: не сказано, `ok` ли это (если выше стоит `unreadable`). При отметке `ok` → `detail` не сказано про перепривязку детей этой строки (`parent_row_index`).
- **L-16.** Ручная дата колонки (PATCH `columns`) не меняет `period_kind`. Если колонка была `relative:*`, следующая правка `report_date` перезапишет ручную дату. Нужно: ручная дата → `absolute`.
- **L-17.** `COLUMN_DATE_UNCERTAIN` нельзя снять подтверждением той же даты: «запрос без изменений — 200 без эффектов». Нужен аналог S-42 для колонок.
- **L-18.** Подпись OCR с уверенностью ниже 100 подтверждается «ожидаемыми датами» от эффективной даты. Если даты отчёта нет, эффективная дата может определяться этой же подписью, и она подтверждает сама себя. Исключить её при вычислении.
- **L-19.** LIS:
- не определено, что значит «уверенные коды — якоря» (обязательное включение или вес);
- не сказано, что делать при конфликте якорей между собой или с перечнем;
- `form_variant` определяется «по структуре кодов» — каких строк (`ok`?);
- пересчитывается ли `form_variant` после назначения кода — не сказано.
- **L-20.** «Форма записи знака по `text`» для `{38378)` не определена: `{` не принимается за открывающую скобку. Нужно «форма знака — по результату разбора: скобки / минус / без знака». Не сказано и то, входит ли сама ячейка в «большинство».
- **L-21.** `CODE_NOT_IN_EDITION` и `CODE_NOT_IN_DICTIONARY`: какая редакция берётся — по дате (`edition`) или выбранная LIS. Межениновская на бланке 66н в 2025 году получит `info` на 1120.
- **L-22.** В схеме `form_variant` не входит в `required`, хотя спека говорит «SHALL иметь». `OTHER` в 2.0 никогда не порождается (лист «прочее» → `SHEET_OTHER`, документа нет), но остался в enum и в правилах ядра.
- **L-23.** Сценарий arm-core «Два баланса в одном файле… ни один не автоподтверждается» безусловный. Правило Р-18/S-45 требует **того же периода**.
- **L-24.** Откат:
- DBML (шапка v5.19) считает `crm_id` групп несовместимостью для `downgrade`, спека — нет;
- `downgrade` неизбежно удаляет `is_confirmed`, `document_issue`, `header_source`, что противоречит «MUST NOT удалять данные молча». Сформулировать точно.
- **L-25.** Механика копии `arm_statements`:
- `VERSION` лежит в копии, но его нет в дереве источника — исключить при сравнении;
- зависимости пакета (PyYAML и др.) должны быть в `requirements.txt` хостов (зеркало банка);
- `mypy` не поддерживает конфиг каталога копии: строгость хоста применяется к коду пакета, а править копию нельзя. Нужно требование «пакет проходит самый строгий конфиг хостов»;
- `conftest` хоста может влиять на тесты пакета.
- **L-26.** «Шаг 1 — многостраничная форма» сформулирован для «каждого адаптера», но у OCR кандидат строится на каждую таблицу ИС, склейки нет. Явно ограничить требование Excel.
- **L-27.** `TABLE_LOST` — «таблица ИС с содержимым» без определения содержимого таблицы.
- **L-28.** `validate_contracts.py` пишет «не строка из 4 цифр», а регулярка допускает 5. Не проверяет, что число копий повтора совпадает с `repeats`.
- **L-29.** Сценарий 1.x в `recognition-contract` («обе строки кода — по прежнему правилу «остаётся первая»») читается двусмысленно.
- **L-30.** Значение с текстом без цифр и уверенностью 100 (`'I'@100` в строке без кода, aep_6 строка 38) не делает строку строкой данных и молча теряется. Это противоречит принципу «ничего не выбрасывать молча». Признать или включить в «содержимое».
## 3. Решения писателя
| S-n | Суть | Оценка | Обоснование |
|---|---|---|---|
| S-4 (утв., Р-22 / S-49) | Совпадение наименований | согласен с оговоркой (следствие) | Утверждено. Следствие на фикстурах: «Итого по разделу V1» у Счастливочной → `NAME_MISMATCH`; не задан `strip` (L-11). |
| S-5 (утв. с изм.) | Упрощённый набор, ОФР 2400 мягкое | согласен | Утверждено. В упрощённом ОФР нет ни одного жёсткого соотношения, покрытие только мягким точно — так и задумано. |
| S-7 (утв.) | Прочерк = пусто, `true` | согласен | Тире на месте цифры ловит §7. |
| S-10 (утв.) | `needs_review_fields` только для даты | согласен | Сформулировано одинаково во всех документах. |
| S-12 (утв.) | Ожидаемые даты + 31.12 | согласен с оговоркой | Замкнутость при отсутствии даты отчёта (L-18). |
| S-13 → S-40 | Детектор нумерации | согласен | Проверено на Межениновской (строки 24, 49) — нумерация распознаётся. |
| S-14 (утв.) | Признак повторной шапки | согласен | Не задано сопоставление колонок такого сегмента (L-6). |
| S-15 (утв. с ог.) | `ref`, `row_index`, экранирование листа | согласен | Примеры приведены к правилу. |
| S-17 → S-43/S-47 | Формат PATCH `lines` | согласен | — |
| S-19 (ог.) | Имена DBML, `codes`, `column_index` | согласен с оговоркой | `file_ordinal` уникален для всех документов, а спека задаёт его только формам (L-3). |
| S-20 (утв.) | `crm_id` уникален | согласен | Добавить в условия отката (L-24). |
| S-21 → S-36 | Значения по умолчанию 1.x | согласен с оговоркой | Полярность `needs_review` (L-9). |
| S-22 (утв.) | Статус ОСВ | согласен с оговоркой | Ядро `osvDocument.status` и `warnings` нигде не потребляет (L-4). |
| S-24 → S-26 | Редакция в сервисах | — | см. S-26. |
| S-25 (утв.) | Квартал → `NEEDS_CHECK`, `PERIOD_REQUIRED` | согласен | — |
| S-26 | Всегда обе редакции, дата решает ничью | согласен | Межениновская 66н-2025 иначе не подтвердилась бы. **Меняет смысл дизайна (выбор по дате), владельцем не утверждено.** |
| S-27 | Перечень в docs, байтовая копия в пакете | согласен с оговоркой | Дрейф перечня делает все хосты красными (L-8). |
| S-28 | Только коды с образцом | согласен с оговоркой | Старые бланки 66н (2421/2430/2450) с неуверенным кодом дадут `needs_review`; приемлемо при OQ-168. |
| S-29 | Выбор схемы по `$id` | не согласен | `recognition-request` (`$id` /v1, без `.v1`-файла), примеры и ядро не покрыты; на шаге 0 `.v1`-файлов ещё нет. Предложение — L-2: брать `.v1`, только если файл существует; правило для `examples/`; ядро в шаг 0. |
| S-30 | Механика `sync-statements` | согласен с оговоркой | `VERSION` в сравнении, зависимости, `mypy`-конфиг, код 20 при дрейфе перечня (L-8, L-25). |
| S-31 | `column_index` = столбец сетки | согласен с оговоркой | Нужно правило для сегментов со своей шапкой (L-6). Меняет смысл поля контракта — допустимо в MAJOR. |
| S-32 | `TABLE_PARSE_ERROR`, `SHEET_OTHER` — уровень файла | согласен | — |
| S-33 | `header_source` jsonb | согласен | — |
| S-34 | PATCH `organization_name` | согласен с оговоркой | Нужен, иначе `NO_ORGANIZATION_NO_INN` не снять. **Расширение функционала — не утверждено владельцем.** |
| S-35 | `RELATION_FAILED`/`RELATION_SOFT`, `codes` | согласен | — |
| S-36 | §7 один раз для 1.x и в миграции | согласен с оговоркой | Задним числом меняет подтверждаемость существующих форм; импорт живого пакета из Alembic (L-13). **Меняет смысл — не утверждено.** |
| S-37 | Строка правки ⇔ `row_index IS NULL` | согласен | Миграция корректна (маппер 3b всегда пишет `line_code_original_value`, `repositories/statements.py:80`). **Отступление от дизайна — не утверждено.** |
| S-38 | Вход функции статуса; `unreadable` без значений | согласен с оговоркой | Не решён вопрос `detail` в `needs_review` (L-1, critical). |
| S-39 | Столбец кодов ищется и у `DISCLOSURE` | согласен | — |
| S-40 | Детектор нумерации с двойниками и пропуском | согласен | Ложных срабатываний на фикстурах нет. |
| S-41 | `LINES_UNRESOLVED` — сумма или текст | согласен | Мусор на месте суммы требует разбора. **Расширяет Р-13 — не утверждено.** |
| S-42 | Подтверждение даты тем же значением | согласен с оговоркой | Нужен аналог для даты колонки (L-17). |
| S-43 | Имена (`LINE_CODE_TAKEN`, `period_date`) | согласен | — |
| S-44 | Откат | согласен с оговоркой | `crm_id` и неизбежная потеря новых колонок (L-24). |
| S-45 | «Тот же период» = (год, квартал) | согласен | Поправить безусловный сценарий (L-23). |
| S-46 | Единицы и ОКЕИ | согласен | — |
| S-47 | Пробелы PATCH; подтверждение не сбрасывает акцепт | согласен по существу | **Сужает правило Среза 3b «акцепт сбрасывается любой правкой» — ждёт владельца.** |
| S-48 | Страховка `done` → `RESULT_STATUS_INCONSISTENT` | согласен с оговоркой | Проверка уже функции статуса; предлагается вызывать саму функцию (L-12). **Ядро переопределяет статус сервиса — не утверждено.** |
| S-49 | Синонимы; «итого по разделу» — только равенство | согласен с оговоркой | Ломается на «Итого по разделу V1»; синонимы Р-20 и 1105 явно добавить в словарь (L-11). |
| S-50 | Толкование Р-21 | согласен | Проверено на РБ/Счастливочной: 2400/2410/2411/25xx покрываются мягкими точно. |
| S-51 | Ожидания на реальных сканах | согласен с оговоркой | Верно, только если значения `detail` держат статус; иначе 4 из 6 документов `done` (L-1). |
| S-52 | Манифест tabular-parser | согласен | Подтверждено разбором: строк `unreadable`/`missing` в Excel нет; страницы 2 ББ дают `1600 = 1700` с пустой целью. |
**Меняют смысл и ещё не утверждены владельцем:** S-26, S-34, S-36, S-37, S-41, S-47 (явно вынесено владельцу), S-48. Пограничные: S-31 (смысл поля контракта) и S-38 (`unreadable` без значений → `needs_check`, но подтверждение не блокирует).
## 4. Что проверил и признал согласованным
**Согласовано:**
- Поля контракта v2.0 ↔ DBML v5.19 ↔ маппинг `arm-core`: строки, значения, колонки, шапка (`header_source`, `*_original_value`), `document_issue` (`kind`/`code`/`severity`/`codes`/`column_id`/`column_index`/`field`), `statements_version`, `form_variant`, `needs_review_fields`.
- Условные требования схемы: `line_code` null ⇔ статус не `ok`; `parent_ref` у `detail`; `checks` у `BALANCE`/`INCOME` со строками; `statements_version` при наличии формы.
- Порядок 409 одинаков в спеке, §9.5 п. 4 и гайде фронта. Коды 422 одинаковы.
- Таблица предупреждений спеки и §11.9 совпадают по кодам и уровням.
- Миграция `row_index` согласуется с текущим кодом ядра.
- `sync-statements --check` на текущих данных пройдёт: все коды словаря ядра есть в перечне своей редакции. В обратную сторону в словаре нет 1231; 2530, 2930; 1105, 1130, 1140, 1530; 2460.
- Сценарии разбора кода и сумм («1220 ,», «| 140», «I1О0», «1 2О5», «(7744536}», «4,200», «42831 1») выводятся из правил. Исключения — «3450» (L-5) и «| : 122477» (L-7).
- Формат OpenSpec: MODIFIED в `recognition-contract` и `quality-checks` содержат полный текст с прежними сценариями, заголовки совпадают с `openspec/specs`. REMOVED нет, и он не нужен.
- Ручной прогон aep_3 и aep_6 подтверждает обещанное: `missing` «Выручка», `detail` «БАЛАНС» с нарушением `1600 = 1100 + 1200`, `unreadable` «44 30\nI |» без разлепления, законные повторы 1220/1520/2410 и раздел III НКО укладываются в перечни `fsbu4`/`66n`.
**Файлы:**
- `openspec/changes/unified-statement-normalization/proposal.md` и все `specs/*/spec.md`;
- `openspec/specs/recognition-contract/spec.md`, `openspec/specs/quality-checks/spec.md`;
- `docs/contracts/*.schema.json` (v2 и v1), `statement-code-orders.yaml`, `examples/*`, `examples/v1/*`, `scripts/validate_contracts.py` (запущен), `Makefile`;
- `context/05-domain-rules.md` §1.6, §7, §9.3–§9.5, §11–§12;
- `context/07-open-questions.md` (диф), `context/03-architecture.md` (диф);
- `model/data-model-v5.19.dbml` (диф к v5.18), `model/entities-v5.19.md` (диф);
- `dev/frontend-api-guide.md` §5.6.1, `dev/checks.md` (диф);
- файл решений `writer-decisions-2.md`.
**Код и данные:**
- `fixtures/ocr/*.json` (все таблицы aep_3, aep_6, РБ и Счастливочной за 1 кв. 2025) и `fixtures/ocr/expected_normalize/*`;
- манифест `tabular-parser/api/tests/statements_manifest.yaml` и все его Excel-файлы — прогоном через `read_book_detected`, `find_forms`, `find_layout`, `line_code` tabular-parser;
- tabular-parser: `api/rules/statement_checks.yaml`, `src/parser/statements/statement.py`, `book.py`, `grid.py`;
- scan-gateway: `src/normalizer/ocr/amounts.py`, `layout.py`;
- arm-core: `api/rules/statement_lines.yaml`, `src/schemas/files.py` (`JobBrief`), `src/services/documents.py:39-68`, `src/repositories/statements.py:80`, `src/schemas/statements.py:158`, `tests/test_contract.py:100-125`;
- тесты идентичности контракта сервисов (`tests/unit/test_contract.py`) и Dockerfile трёх репозиториев (`COPY api/` — копия пакета в образ попадёт).
Codegraph не использовал: нужные места знал точно, читал файлы напрямую.
Скрипты прогона лежат в `/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/12de9a9a-5665-41d0-beea-0651270d88ec/scratchpad/rev3/`: `dump.py` (таблицы ИС), `cov.py` (покрытие §7 на OCR), `xl.py` и `xl2.py` (статусы строк и колонки Excel).