From ad1cd307a1065d2d810a698fc0ae5b0ee7ae7972 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 18 Oct 2026 02:38:24 +0400 Subject: [PATCH] =?UTF-8?q?docs(#116):=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=BD=D0=BE=D0=B5=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20?= =?UTF-8?q?=D1=81=D0=BF=D0=B5=D0=BA=D0=B8=20=D0=B2=D1=81=D0=BB=D0=B5=D0=BF?= =?UTF-8?q?=D1=83=D1=8E=20=D0=BD=D0=B0=20Opus=20(L-1=E2=80=A6L-30,=20?= =?UTF-8?q?=D0=BE=D1=86=D0=B5=D0=BD=D0=BA=D0=B0=20S-26=E2=80=A6S-52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W --- .../review-2026-10-18-spec-2.md | 241 ++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 openspec/changes/unified-statement-normalization/review-2026-10-18-spec-2.md diff --git a/openspec/changes/unified-statement-normalization/review-2026-10-18-spec-2.md b/openspec/changes/unified-statement-normalization/review-2026-10-18-spec-2.md new file mode 100644 index 0000000..ab5ab86 --- /dev/null +++ b/openspec/changes/unified-statement-normalization/review-2026-10-18-spec-2.md @@ -0,0 +1,241 @@ +# Повторное ревью спеки вслепую (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). \ No newline at end of file