# Проверка закрытия 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 опирался на текст дизайна, а не на код.