From 41c328f2ab5791ad3f1d26b15a9744e56aeb2927 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 17 Oct 2026 21:16:12 +0400 Subject: [PATCH] =?UTF-8?q?docs(#116):=20=D1=82=D1=80=D0=B5=D1=82=D0=B8?= =?UTF-8?q?=D0=B9=20=D1=80=D0=B0=D1=83=D0=BD=D0=B4=20=E2=80=94=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0=20=D0=B7=D0=B0=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D1=82=D0=B8=D1=8F=20V/G=20=D0=BD=D0=B0=20Sonnet=20?= =?UTF-8?q?(W-1=E2=80=A6W-16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W --- .../review-2026-10-17-closure-2.md | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 openspec/changes/unified-statement-normalization/review-2026-10-17-closure-2.md diff --git a/openspec/changes/unified-statement-normalization/review-2026-10-17-closure-2.md b/openspec/changes/unified-statement-normalization/review-2026-10-17-closure-2.md new file mode 100644 index 0000000..c685a5d --- /dev/null +++ b/openspec/changes/unified-statement-normalization/review-2026-10-17-closure-2.md @@ -0,0 +1,256 @@ +# Проверка закрытия V-1…V-13 и G-1…G-30 (17.10.2026, третий раунд, Sonnet) + +Ревьюер: отдельный агент (модель Sonnet), без права правки; получил design.md после коммита e6400bf, отчёты +`review-2026-10-17-closure.md` (V-n) и `review-2026-10-17-blind.md` (G-n). Маркеры новых проблем — W-n, наблюдения — O-n. + +Отчёт агента ниже без сокращений. + +--- + +# Проверка закрытия V-1…V-13 и G-1…G-30 в обновлённом design.md (коммит e6400bf) + +Файлы не правил и ничего не коммитил. Читал design.md целиком (850 строк), оба отчёта ревью и фикстуры. Сценарии из находок прогонял по новым правилам. Данные проверял скриптами `python3 -I`, для xls — venv парсера с `-I`. + +Итог: 43 находки. +- `closed` — 31. +- `closed, new issue` — 9: V-3, V-13, G-2, G-12, G-16, G-17, G-19, G-25 и ещё G-1 в связке с W-6. +- `partial` — 3: V-4, V-12, G-8. +- `not closed` — нет. + +Новых противоречий найдено 16 (W-1…W-16). Серьёзные из них: +- W-1 — регрессия детектора «только актив», три файла из манифеста. +- W-2 — `confirmed` нигде не хранится. +- W-4 — наследование столбца кодов ломает лист «_2» НПЗ. +- W-3, W-5, W-6, W-9 — средней серьёзности. + +## 1. Таблица + +| Находка | Где закрыто | Статус | Что осталось / новая проблема | +|---|---|---|---| +| V-1 | Шаг 2 п. 1 (а): подпись принимается при доле кодов ≥ 0,6 или кодах ≥ 3; при нескольких кандидатах — больше кодов; «документ из двух строк» = 2 из 2; решение № 3 | closed | Проверил на данных. Унагранде ББ Лист 1: «Коды» (5,33) даёт 1 код из 9, доля 0,11; «Код» (27,21) — 43 из 43. ОФР Лист 1 3кв.25: «Коды» 1 из 25, «Код» 17 из 17. Титульный столбец отсекается, форма берётся верно | +| V-2 | «Код и наименование строки»: пятизначный код подтверждён через четырёхзначного родителя; применяется только к кодам с уверенностью < 100 (Р-12); Excel и чистые OCR-коды дают `info` ядра | closed | Формулировки уточнить в спеке: W-12 (правило родителя), W-13 (границы алгоритма) | +| V-3 | Шаг 4 (`values[].text`); DBML v5.19 `source_text`; конвенция знака «от сохранённого `text`» | closed, new issue | W-8: у правленой ячейки `text` устаревает и участвует в голосовании за конвенцию колонки | +| V-4 | Шаг 2 п. 2: сегмент без шапки и с кодами < 3 «наследует столбец кодов» предыдущего сегмента | partial | W-4: у НПЗ столбец кодов на каждом листе свой, наследование по индексу не работает | +| V-5 | DBML v5.19: `financial_statement_line.needs_review`; PATCH: `line_code_modified` ⇒ `needs_review` строки = false | closed | — | +| V-6 | Шаг 8 п. 2: «ИНН не найден при найденной организации» — `info`, «ни организации, ни ИНН» — `error`; «формат не совпал» — `info` | closed | Согласовано с критерием `done` п. 1 и решением № 1. НПЗ ОФР без ИНН остаётся `done` | +| V-7 | Р-15; «Что уходит из ядра»: вторая форма не автоподтверждается, подтверждение второй — 409; вопрос о склейке — в `07-open-questions.md` | closed | Решено владельцем. Формулировка 409 без оговорки «из одного файла», но между файлами работает автозамена (§12 п. 3), поэтому на практике это только внутри файла | +| V-8 | Шаг 2: роль `title` для всего выше строки заголовка; шаг 3: `missing` только между заголовком и первой строкой с кодом | closed | Межениновская: «Дата 30 \| 9 \| 2025» теперь `title`. Повторная шапка «Коды» (стр. 48 Формы 1) открывает новый сегмент по п. 2, а не `unreadable` | +| V-9 | Шаг 3 п. 2: разлепление «до разбора кода»; шаг 2 п. 1: многострочная ячейка — код, если каждая часть код | closed | — | +| V-10 | DBML v5.19: `recognition_job.statements_version` | closed | — | +| V-11 | Принцип 4, шаг 5 п. 2, «Что уходит из ядра»: словарь ядра хранит только имена, редакции берутся из копии общего кода, `sync-statements --check` | closed | Побочно: коды перечня без имён (1105, 1530, …) дают `info` «не в словаре имён» — безвредно | +| V-12 | PATCH `lines`: переходы, 422 для пары «`ok` без кода», пересчёт родителя `detail`, аудит | partial | W-11: не сказано про `line_code_confidence` после назначения; у строк нет аналога `confirmed` | +| V-13 | «Подтверждение проверкой»: чистые значения в покрытии не нуждаются | closed, new issue | W-7: фраза «ни одним правилом ниже не подсвечиваются» противоречит следующему пункту про непройденное жёсткое соотношение | +| G-1 | Р-12 + «Код и наименование строки»: источник перечня — коды шаблонов банка плюс коды печатных форм 1С; пятизначные через родителя; для Excel — `info` | closed | Перечень всегда неполон: в `ББ. Лист 1` (unagrande-group) есть 1231, которой нет ни в словаре ядра, ни в названных 1С-кодах. Для Excel и чистого OCR это `info`, для кодов с уверенностью < 100 — `needs_review`, что принято. Связанная дыра — W-6 | +| G-2 | Шаг 7: «цель отсутствует/`null`, а хотя бы один член непуст — жёсткое нарушение» | closed, new issue | W-1: симметричный случай (цель есть, все члены пусты) не считается — три фикстуры «только актив» станут `done`. O-2: остаточный риск для строк вне §7 | +| G-3 | То же, что V-3: `values[].text` в контракте v2.0, `source_text` в DBML, `string_value` в прежнем смысле | closed | Семантика `string_value` теперь одна (шаг 4, вводная) | +| G-4 | Шаг 2 п. 3: словарь периодов объединяет `statement_periods.yaml` и шаблоны `dates.py`; `test_parses_real_forms` — обязательные тесты | closed | — | +| G-5 | То же, что V-1 | closed | — | +| G-6 | То же, что V-6 | closed | — | +| G-7 | Шаг 8 п. 3 и «Что уходит из ядра»: `RECOGNIZED` ⇔ конверт `done` ∧ `document.status = done` | closed | O-1: `document.status = done` избыточен, так как конверт `done` уже означает «все документы `done`». Прежнее поведение сохранено | +| G-8 | Шаг 1 п. 6: слабое продолжение присоединяется, только если коды в диапазоне `type_hint`; иначе отдельный кандидат + `error`; шаг 2 п. 2 | partial | W-4 (наследование); W-10 (отдельный кандидат: «не документ `info`» против `error`) | +| G-9 | Решение № 16: `ref` OCR = `row:[#]`, Excel = `<лист>!`, уникален в документе | closed | Мелочь, W-15: для `Cell.ref` у OCR позиция без столбца не уникальна на уровне ячейки | +| G-10 | Р-13: 409 `LINES_UNRESOLVED` при `unreadable` / `missing` с непустым `amount`; `detail` не попадает в ЕХ | closed | — | +| G-11 | DBML v5.19: `line.needs_review`, `recognition_job.statements_version` | closed | Маркера G-11 в тексте нет, но содержание внесено | +| G-12 | PATCH `values[].confirmed`: `needs_review` = false, `is_modified` не ставится, аудит | closed, new issue | W-2: флаг подтверждения нигде не хранится, следующий пересчёт его теряет | +| G-13 | PATCH `columns: [{column_id, period_date}]`: снимает `error` «колонка без даты», пересчитывает `is_current`; `report_date: null` обнуляет относительные | closed | W-16: исключение из правила F-8 «`error` сохраняются» нужно оформить явным списком | +| G-14 | Р-1: пакет владеет своими типами, хост конвертирует; golden пакета на синтетике; `sync-statements --check` в `make check-full` | closed | — | +| G-15 | Р-14: раскладка по коду остаётся; строки со статусами — под ближайшей предыдущей размещённой; `detail` скрыты | closed | W-15: не определено место `missing`-строки, у которой нет предыдущей размещённой (aep_3 ОФР, «Выручка» — первая строка таблицы) | +| G-16 | Шаг 4 п. 1: `parse_amount(text, source)`, «−» как знак, тонкие пробелы, запятая по источнику | closed, new issue | W-14: квантование тихо округляет OCR-мусор вроде «12,3456»; правило точки-тысяч (sg#37) не названо | +| G-17 | Шаг 6 п. 1: `form_variant = simplified` (ОКУД 0710096 или структура), свой набор соотношений и перечень | closed, new issue | W-5: в `statement_headers.yaml` ОКУД 0710096 уже мапится на `DISCLOSURE` | +| G-18 | Принцип 5: ИНН — только цифры; шаг 2 п. 1: порядок «пробелы и переносы → двойники → проверка», `\|` → 1 только если результат попадает в диапазон | closed | — | +| G-19 | Шаг 6 п. 1: `DISCLOSURE` по `type_hint` — без шагов 2–7; по кодам — шаги 2–5 и отбрасывание строк | closed, new issue | W-9: диапазоны «1xxx / 2xxx / 3xxx–6xxx» шире словарных | +| G-20 | Шаг 8 п. 2: уровни; «организация не найдена при найденном ИНН» — `info`, «ни того ни другого» — `error` | closed | — | +| G-21 | Шаг 3 п. 2: разлепление, только если хотя бы одна часть — код | closed | При известном `type_hint` «44 30» (=4430) вне диапазона ББ, не множится. При неизвестном 4430 станет кодом из-за расширения диапазонов (W-9) | +| G-22 | Шаг 2 п. 1: «краевая пунктуация» («1220 ,» → 1220) | closed | — | +| G-23 | PATCH: `report_date: null` обнуляет `period_end` относительных колонок | closed | — | +| G-24 | Р-15 | closed | — | +| G-25 | `line_refs` — список; `document_issue`: список `line_id`, очистка при перезагрузке, `core`-записи заменяются | closed, new issue | W-3: устаревшие нарушения источника `service` после PATCH | +| G-26 | Принцип 3: ядро добавляет только `info` по словарю имён и перечню | closed | — | +| G-27 | Утверждение «в трёх из четырёх» в тексте дизайна отсутствует | closed | Маркера нет, исправлять нечего | +| G-28 | Шаг 2 п. 1: словарь подписей расширен («Код строки») | closed | — | +| G-29 | Шаг 2 п. 7: одно правило столбца наименования | closed | В запасной ветке («ближайшая текстовая левее кода») пропало «кроме Пояснения»; для столбцов левее кода из 66н не критично | +| G-30 | PATCH: аудит `RECOGNIZED_DATA_EDIT` с `before / after` по коду, статусу, родителю; назначенный код снимает `needs_review` | closed | — | + +## 2. Разбор незакрытых проблем и новых противоречий + +### W-1 (important). «Посчитано, если хотя бы один член непуст» (F-2) + G-2 теряют детектор односторонней формы + +Где: шаг 7, пункты «цель отсутствует» и «соотношение считается посчитанным». + +В G-2 «член» — это правая часть соотношения, отдельно от цели. Значит, цель есть, а все члены пусты — «не проверяется». + +Сценарий: «ББ. Лист 1» с одним активом. 1600 есть, 1700 нет. +- `1600 = 1700`: цель 1600 непуста, член 1700 отсутствует, соотношение не считается. +- `1700 = 1300 + 1400 + 1500`: цели нет, членов нет, тоже пропуск. +- Остальные соотношения сходятся, org/ИНН и период есть, строки `ok`, значения чистые — получается `done`. + +Сегодня эти файлы `needs_check`, потому что отсутствующий член считается нулём (`tabular-parser/api/src/parser/statements/checks.py` ~122; `arm-core/api/src/domain/statement_checks.py::run_checks`). Манифест `tabular-parser/api/tests/statements_manifest.yaml`: строки 11, 24, 32 — три файла (unagrande-co 2024-FY, unagrande-group 2024-FY и 3кв.25), `failed_relations: ["1600 = 1700"]`. Половина баланса автоподтвердилась бы. + +Предложение: соотношение посчитано, если непуста цель или хотя бы один член; пропуск только когда непусто ничего. Тогда правило «все null — ничего не подтверждает» (F-2) сохраняется, а G-2 становится его зеркалом. Обновить golden и манифест. + +### W-2 (important). `confirmed` (G-12) не хранится, пересчёт его сбросит + +Где: PATCH `values[].confirmed`, «Подтверждение проверкой», список DBML v5.19. + +Для правки есть `is_modified`, для акцепта есть `checks_accepted_*`. Для `confirmed` в v5.19 колонки нет: ни флага, ни `confirmed_by/at`, только `audit_event`. Пересчёт общим кодом «после PATCH» применяется ко всем значениям. + +Сценарий: ячейка с уверенностью 71, верная, в строке вне §7. Аналитик подтвердил её, `needs_review` = false. Любой следующий PATCH другой ячейки пересчитывает её обратно: «нуждается в покрытии и не покрыто» даёт true. Правило G-12 не работает дольше одного PATCH. + +Предложение: добавить в `financial_statement_value` признак (`review_confirmed_by/at` или `is_confirmed`) и вход «подтверждено» в общую функцию `needs_review`. Альтернатива: пересчёт монотонен для подтверждённых, но тогда сломанное соотношение должно возвращать true. + +### W-3 (medium). `document_issue`: устаревшие нарушения источника `service` + +Где: «Что уходит из ядра», DBML v5.19. + +Сказано только, что записи источника `core` заменяются при каждом пересчёте. Нарушения §7, пришедшие от сервиса (`kind` relation/soft, `source = service`), после правки значения остаются и показываются как действующие. Рядом появляются новые `core`-записи — дубли и устаревшее. + +Предложение: при пересчёте после PATCH заменять все записи `relation` / `soft` независимо от источника. Предупреждения `warning` источника `service` сохранять, кроме явно снимаемых (W-16). + +### W-4 (important). V-4 / G-8: «наследование столбца кодов» не работает на геометрии НПЗ + +Где: шаг 2 п. 2, решение № 3. + +У НПЗ столбец кодов на каждом листе свой. Проверено openpyxl, номера столбцов с 1: +- ББ: листы 1–5 — 13, 7, 7, 7, 12. +- ОФР: лист 1 — 10, лист `_2` — 6, лист `_3` — 12. +- Лист `_2` (единственная строка 2400) шириной 9 столбцов. + +Если «наследуется» значит «тот же индекс, что в предыдущем сегменте» (столбец 10), то в `_2` его просто нет. Получается ошибка сегмента и документ `needs_check` вместо `done`. То же с `st07_single_code_continuation` (там индексы совпадают, но принцип тот же). Прежний `relaxed` (`statement.py` `_continuation`, `min_codes_continuation: 1` в `statement_headers.yaml`) ищет столбец по содержимому, а не наследует индекс. + +Предложение: заменить формулировку на ослабленный поиск для сегмента не первого, без шапки, с кодами < 3: столбец с наибольшим числом кодов из диапазона формы, минимум 1. Оставить предупреждение `error`, если и так не найден. Сам индекс предыдущего сегмента не использовать. Сохранить проверку диапазона `type_hint` (`_weak_blocker`). + +### W-5 (medium). ОКУД 0710096 как признак упрощённой формы конфликтует со словарём типов + +Где: шаг 6 п. 1 (G-17), `evidence`. + +В `tabular-parser/api/rules/statement_headers.yaml` (`okud: DISCLOSURE: '\b07100(0(3|…|9)|(1|…|9)\d)\b'`) значение 0710096 попадает в `DISCLOSURE`. По шагу 1 п. 4 `type_hint` = `DISCLOSURE`, коды строк 1xxx → по шагу 6 это «противоречие» → тип по кодам + `error` → `needs_check`. Пока словарь не правится, упрощённая форма с этим ОКУД никогда не будет `done`, как бы ни работал вариант G-17. + +В фикстурах 0710096 нет вообще (только в `admin/source/СУЖЦ…xls`). Утверждение, что упрощённые ББ/ОФР несут именно этот ОКУД, ничем не подтверждено. По памяти упрощённые бланки 66н идут с обычными 0710001/0710002, а 0710096 относится к комплекту отчётности. Это надо подтвердить на образце или у владельца; сам я не верифицировал. + +Предложение: либо убрать ОКУД из признаков упрощённой формы (оставить структурный: есть 1600, нет 1100 и 1200), либо явно исключить 0710096 из `DISCLOSURE` в словаре. Образец упрощённой формы — в фикстуры. + +### W-6 (medium). Р-11 vs Р-12: код, исправленный из двойников с уверенностью 100, не проходит ни одной проверки + +Где: принцип 5 / Р-11 и «Код и наименование строки» / Р-12. + +Р-11 велит заменять двойники и в кодах, а для суммы вводит `is_recognized = false`, чтобы её проверил §7. У кода такого флага нет. Структурное подтверждение применяется только при `line_code_confidence < 100`. Код «I140» или «1l50», прочитанный ИС с уверенностью 100, получает `ok` без `needs_review`. §7 перепутанные соседи не ловит (сумма раздела та же), поэтому запись сохраняется без проверки. + +Предложение: считать код после замены двойника «неуверенным» (эффективная уверенность < 100, или признак `code_corrected`) и пропускать через структурную проверку. Это одна строка в «Код и наименование строки». + +### W-7 (minor). Внутреннее противоречие в «Подтверждении проверкой» + +Первый пункт: чистые значения «ни одним правилом ниже не подсвечиваются (V-13)». Пятый пункт: `true` «у всех ячеек жёсткого соотношения … независимо от уверенности». Второе включает чистые Excel-значения, поэтому первое надо сузить: «ни правилом покрытия, ни правилом знака». + +### W-8 (minor). Устаревший `text` правленой ячейки в голосовании за конвенцию знака + +Конвенция — форма записи у большинства членов `abs_terms` с `is_recognized = true`, считается от сохранённого `source_text`. Если аналитик исправил знак у члена, `source_text` прежний, а `amount` новый, и ячейка голосует старой формой. Что делает PATCH с `is_recognized`, не сказано. + +Предложение: исключить из голосования ячейки с `is_modified` (или считать форму от знака `amount`). + +### W-9 (medium). G-19: диапазоны кодов шире словарных + +Где: шаг 2 п. 1, шаг 6 п. 1. + +Словарь парсера: 1100–1799, 2100–2999, 5000–5999 (`tabular-parser/api/rules/statement_headers.yaml`, `line_codes.ranges`). В дизайне «1xxx — ББ, 2xxx — ОФР, 3xxx–6xxx — прочие», при неизвестном `type_hint` объединение. + +Два риска: +- Годы в шапках (2024, 2025) попадают в 2xxx. В НПЗ ОФР в столбце 16 стоит «2025». +- Значения в тысячах от 3000 до 6999 считаются кодами, а столбец сумм начинает конкурировать со столбцом кодов за «наибольшее число» (кодов ≥ 3, доля ≥ 0,6 — слабая защита). Для разлепления G-21 «44 30» (=4430) тоже становится кодом, если `type_hint` неизвестен. + +Предложение: в дизайне явно записать числовые диапазоны (оставить 1100–1799, 2100–2999, расширить DISCLOSURE до 3000–6999 только при неизвестном `type_hint` и тестами) вместо «1xxx / 2xxx». + +### W-10 (minor/medium). Одинокий лист-продолжение: «не документ, `info`» против `error` + +Где: шаг 6 п. 1 и шаг 8 п. 2. + +Лист «Справка» с кодом 1370 после ОФР не присоединяется (шаг 1 п. 6) и становится отдельным кандидатом. У него кодов < 3 и `type_hint` нет. Шаг 6 п. 1 говорит «не документ, `info`», шаг 8 п. 2 — «сегмент с содержимым без колонки кодов (в том числе потерянный лист-продолжение) — `error`». Данные листа теряются, `done` у соседних документов возможен: нарушается принцип 2. + +Предложение: в шаге 6 п. 1 добавить исключение — лист с числами и не присоединившийся продолжением даёт файловое `error` (и строки сохраняются как `unreadable` / `detail`, если возможно). + +### W-11 (minor). Остатки V-12 / G-30 + +- После назначения кода (`unreadable` / `missing` / `detail` → `ok`) не сказано, чему равна `line_code_confidence` (100?). Сейчас остаётся прежняя, возможно 25. +- У строки есть `needs_review` (структура кода), но нет `lines[].confirmed`. Код с уверенностью 50, стоящий не по порядку, но верный, снимается только повторным назначением того же кода, которое в тексте как «другой код» не описано (аналог G-12 для строк). + +### W-12 (minor). Правило родителя пятизначного кода + +В «Код и наименование строки»: «ближайшая строка выше с теми же четырьмя первыми цифрами, иначе код из первых четырёх цифр — правило `subline_parents`». В реальном коде (`arm-core/api/src/domain/statement_checks.py::subline_parents`) ближайшая четырёхзначная строка выше с теми же тремя цифрами, иначе код из первых четырёх. Первая половина фразы при буквальном чтении матчится на пятизначного соседа (11101 выше 11102). Привести к тексту кода. + +### W-13 (minor). Границы алгоритма подтверждения структурой + +- Наибольшая подпоследовательность не единственна. Для двух поменявшихся соседей (1230, 1220) подтверждается один из двух, и не определено, какой остаётся без подтверждения. Если выпавший код имеет уверенность 100, он освобождён от проверки, а второй числится «подтверждённым». +- «Хотя бы в одной из проверяемых редакций»: берётся объединение подпоследовательностей двух редакций или одна общая? Правка: выбирать редакцию с наибольшей подпоследовательностью. +- Родитель с уверенностью 100 (освобождён) для пятизначного дочернего кода считается «подтверждённым» или нет? Записать. + +### W-14 (minor). OCR: тихое округление и правило точки + +Решение № 13 («2 знака, half-up») в общем `parse_amount` для OCR-текста. Сегодня шлюз отвергает такие значения: `scan-gateway/api/src/normalizer/ocr/document.py:~143-150`, решение № 30: «0,125», «12.3456» — артефакт, `amount = null`, текст в `string_value`. По дизайну «12,3456» станет 12,35, причём `is_recognized` не оговорён. Оставить квантование только для числового `value` (Excel), а для OCR-текста сохранить `null` + `string_value`. + +Правило точки-тысяч (`837.994`, `1234.567` — sg#37, `amounts.py` `_DOT_THOUSANDS_RE`) в шаге 4 п. 1 не названо. Указать, что оно сохраняется для `ocr` и как ведёт себя для `excel`. + +### W-15 (minor). Экран и `ref` + +- Р-14: у `missing`-строки без предыдущей размещённой (aep_3 ОФР, «Выручка» — первая строка таблицы) место не задано. Предложение: в начало первого раздела. +- `Cell.ref` у OCR — `row:` без столбца: уникально для строки, но не для ячейки. В шаге 1 сказано «уникальная в документе», уточнить, что это `RawLine.ref`. + +### W-16 (minor). Исключения из «`error` сохраняются после правки» (F-8) + +PATCH `columns` снимает `error` «колонка без даты» (G-13). Остальные `error` сервиса (несовпадение колонок сегмента, «строка заголовка не найдена», тип по кодам и др.) сохраняются. Записать явный перечень «какая операция PATCH какое `error` снимает», иначе реализация разойдётся. + +### Наблюдения (не противоречия) + +- O-1. Вторая часть `RECOGNIZED` («`document.status = done`») избыточна при требовании «конверт `done`»: конверт `done` означает «все документы `done`». Один плохой документ блокирует автоподтверждение всех остальных в файле. Так же было и раньше (`job_done` в `statement_status`). Если нужна независимость документов, это отдельное решение. +- O-2. Строки вне §7 с утерянным кодом (справочные 25xx / 29xx и подобные) уходят в `detail` без нарушения. Раньше это был счётчик, но любое предупреждение шлюза давало `needs_check`. Теперь такая строка скрыта, а документ может стать `done` и автоподтвердиться. Принято Р-8, но шапка Р-13 ограничена `unreadable` / `missing`, а не `detail`. +- O-3. В критерии `done` нет уверенности полей шапки. Шлюз требовал 100 для ИНН и организации. Остаётся сверка ядра (`MISMATCH`). Намеренно ли это ослабление, в дизайне не сказано. + +## 3. Согласованность правок между собой + +Проверил и признал согласованным: +- **Р-12, G-1, V-2 и перечень V-11.** Перечень (superset словаря ядра) — в общем коде, имена — только в ядре, `--check` в одну сторону (словарь ⊂ перечень) достаточен. Пятизначные коды, 1105, 1530, 2420, 2460, 2930 покрыты; Excel и чистые OCR-коды дают `info`. Оговорки: W-6 (двойники), W-12 и W-13. +- **G-2 + Р-8 + G-17.** Для «БАЛАНС» без кода: `detail` → цель 1600 отсутствует → жёсткое нарушение, подсвечивается родитель. Для сегмента «Лист 2» цель 1600 отсутствует, член 1700 непуст → нарушение, документ `needs_check` (как в golden). Упрощённая форма использует свой набор. Оговорки: W-1 (симметричный случай), W-5 (ОКУД), O-2. +- **G-3 и V-3.** `text` и `string_value` с разными ролями; `source_text` в DBML; контракт и DBML соответствуют. Оговорка: W-8. +- **V-1 и G-5 «из двух строк».** Для двухстрочного документа 2/2 = 1,0, титульный столбец отсекается: проверено на Унагранде (доли 0,11, 0,12, 0,04). +- **Уровни V-6 / G-6 / G-20 и критерий `done`.** Список `error` и `info` согласован между п. 1 и п. 2 шага 8 и решением № 1. НПЗ ОФР без ИНН — `done`. +- **Статус конверта в `RECOGNIZED` (G-7).** Согласовано с шагом 8 п. 3. Замечание — O-1. +- **PATCH `lines` / `columns` / `confirmed` и аудит.** Один PATCH — один пересчёт, аудит `RECOGNIZED_DATA_EDIT` с `before / after`. Оговорки: W-2, W-11, W-16. +- **DBML v5.19 и контракт v2.0.** Проверил соответствие: + - `line_status`, `line_code_text`, `row_index`, `ref` → `source_ref`, `parent_ref` → `parent_row_index`, `needs_review` строки и значения; + - `values[].text` → `source_text`, `columns[].period_kind`, `form_variant` в meta; + - `warnings[]` и `checks` → `document_issue`, `statements_version` → `recognition_job`; + - снятие уникальности `document` и `(meta_id, line_code)` плюс порядковый номер формы. + Не хватает только флага подтверждения (W-2). +- **Раскладка экрана (Р-14).** Совместима с Р-4 (дубли кодов в порядке файла), шаблонными строками и `detail` под родителем. Оговорка — W-15. +- **Р-15 и автозамена.** Автозамена между файлами (§12 п. 3) убирает дубли ББ/ОФР между файлами, поэтому Р-15 действует только внутри файла. + +## 4. Что смотрел + +**Дизайн и ревью** (`.wt/normalization/docs/openspec/changes/unified-statement-normalization/`): +- `design.md` целиком, включая «Целевую картину», «Решения владельца Р-1…Р-15» и «Decisions made without discussion» 1–19; +- `review-2026-10-17-closure.md` (V-1…V-13) и `review-2026-10-17-blind.md` (G-1…G-30) целиком. + +**Правила и модель:** +- `docs/context/05-domain-rules.md` §12 п. 3 (автозамена), строки 680–695; +- `docs/model/data-model-v5.18.dbml`: `document` (757–780), `recognition_job`, `financial_statement_meta/column/line/value`. + +**tabular-parser:** +- `api/rules/statement_checks.yaml`, `statement_headers.yaml` (`okud`, `line_codes`); +- `api/src/parser/statements/checks.py` (`run_statement_checks`); +- `api/tests/statements_manifest.yaml`; +- `CLAUDE.md`. + +**scan-gateway:** `api/src/normalizer/ocr/amounts.py` (`parse_amount`), `document.py` (`_value`). + +**arm-core:** +- `api/src/domain/statement_checks.py` (`subline_parents`, `_subline_rules`, `run_checks`); +- `api/src/services/statements.py` (PATCH-цикл, пропуск несуществующей ячейки); +- `api/rules/statement_lines.yaml`. + +**Фикстуры:** +- `fixtures/golden/expected_statement_parse/**`: `Б.Б. Лист 2`, `ОФР. Лист 2`, `st05` — статусы и предупреждения; +- `fixtures/ocr/expected_normalize/*.json` и golden BALANCE/INCOME — коды вне словаря (найдены 1105, 1231, 2930); +- `fixtures/statements-xlsx/npz/2025-Q3/*.xlsx` — столбцы кодов по листам; +- `fixtures/statements-xlsx/mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx` — повторная шапка и title; +- `fixtures/statements-xlsx/synthetic/*.xlsx`; +- `unagrande-co/2024-FY/Б.Б Лист 1.xls`, `unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls`, `ББ. Лист 1 3кв.25.xls` — подписи «Код» / «Коды» и доли кодов. + +Скрипты — в scratchpad (`npz.py`, `g2.py`, `ux.py`, `xl.py`, `codes.py`). Не смотрел: arm-web; `services/documents.py` и `auto_confirm.py` — для Р-13 и Р-15 опирался на текст дизайна, а не на код.