2026-10-17 21:16:12 +04:00

257 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.

# Проверка закрытия 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:<row_index>[#<k>]`, Excel = `<лист>!<A1>`, уникален в документе | 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:<row_index>` без столбца: уникально для строки, но не для ячейки. В шаге 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 опирался на текст дизайна, а не на код.