149 lines
48 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.

# Повторное ревью design.md — проверка закрытия F-1…F-25 (17.10.2026, второй раунд)
Ревьюер: отдельный агент (модель Fable), без права правки; получил обновлённый design.md (после коммита 63f134b) и
`review-2026-10-17.md`. Задание: по каждой находке F-1…F-25 найти место закрытия, прогнать сценарий через новые
правила, оценить статус; отдельно — согласованность правок между собой. Маркеры новых проблем — V-n.
Отчёт агента ниже без сокращений.
---
# Повторное ревью design.md (unified-statement-normalization) — закрытие F-1…F-25
Ревьюер: независимый агент, без права правки. Проверено: `design.md` целиком (763 строки), `review-2026-10-17.md`, §7/§11/§11.1/§12 `05-domain-rules.md`, контракт v1.2, DBML v5.18, код трёх сервисов (через codegraph + Read), фикстуры OCR (4 успешных json, golden-эталоны) и Excel (НПЗ, Унагранде ×2, Межениновская, синтетика) скриптами `python3 -I` / venv парсера. Решения Р-8…Р-11 не пересматривались.
## 1. Таблица по находкам
| F-n | Где закрыто | Статус | Что осталось / новая проблема |
|---|---|---|---|
| F-1 | Шаг 7 (abs_terms «подтверждает только величину»), «Подтверждение проверкой» (знак по конвенции колонки), шаг 4 п. 1 (`}`/`]` — закрывающая скобка), Р-7, решение № 10 | closed, new issue | Правило верно на данных (aep_3 ОФР: все члены abs в скобках, `(7744536}` подтверждается, `{38378)` — подсвечивается). Но «форма записи знака» не доезжает ни до контракта, ни до БД — ядро не может пересчитать конвенцию после PATCH (**V-3**); область применения правила не ограничена сомнительными ячейками (**V-13**) |
| F-2 | Шаг 7 («посчитано, если хотя бы один член непуст»), «Подтверждение проверкой» (покрыто ⇒ `amount` не `null`), Р-7 | closed | Сценарий «колонка залита полосой» даёт `needs_review = true` у всех мусорных ячеек; сценарий «1600 мусор, 1100/1200 читаемы» — нарушение, все три подсвечены |
| F-3 | Шаг 3 (статус `detail`, родитель — ближайшая строка с кодом выше в сегменте), Р-8, решение № 9, «Что уходит из ядра» (экран скрывает `detail`) | closed, new issue | По фикстурам все 90 строк без кода с суммами в 8 OCR-таблицах — `detail`, единственная `missing` — «Выручка» aep_3 (код 2110 в колонке значений, верно). У Excel НПЗ/Унагранде `missing` нет. Но роль строк титульного блока над заголовком не определена — у Межениновской «Дата 30 \| 9 \| 2025» станет `missing` (**V-8**) |
| F-4 | Принцип 2, шаг 3 (`row_index`, `ref`), шаг 8 п. 5, DBML v5.19 (`row_index`, `source_ref`, `parent_row_index`), «Экран показывает строки как в файле» | closed | — |
| F-5 | Шаг 2 п. 1 (а) «подпись принимается только с кодом под ней, последняя над первой строкой с кодом», п. 4 (обе ветки парсера сохранены), решение № 3 | partial | Защита «хотя бы один код под подписью» пробивается ОКОПФ **12267** (АО) в титульном блоке 1С: Унагранде ББ Лист 1 и Унагранде Групп ОФР 3кв.25 — под «Коды» стоит 12267 (1226 ∈ [1100, 1799]); правила выбора между двумя столбцами-кандидатами нет (**V-1**, critical). Ветка «заголовок по периодам» перенесена — верно |
| F-6 | Шаг 2 п. 3 (эффективная дата), шаг 8 п. 1 («дата отчёта **или** самая поздняя абсолютная дата колонок»), п. 2 (`info` «дата отчёта не разобрана при наличии дат колонок»), решение № 12 | closed | Совпадает с `statement_period` ядра (`statement_mapping.py:73-80`) |
| F-7 | Шаг 9 (v2.0, порядок «сначала ядро», два маппера), Р-9 | closed | Паттерн `^1\.\d+$` в `contract/common.py:33` поднимается до `^(1\|2)\.\d+$`; запрос `recognition-request` остаётся 1.x (шаг 0) — не затронут. В спеке нужно задать, чем заполняются новые поля при приёме 1.x (`line_status = ok`, `row_index` по порядку, `needs_review` — старое правило) — не противоречие, а пробел спеки |
| F-8 | Шаг 8 п. 1 (одна функция `done`), «Подтверждение проверкой» (is_modified ⇒ false, акцепт, `error` сохраняются), DBML v5.19 (`line_status`, `line_code_text`, `row_index`, `value.needs_review`, `document_issue`), решение № 11 | partial | Пп. 2–4 закрыты. П. 1 закрыт для значений, но **`needs_review` строки** (F-9) в DBML v5.19 не добавлен, и правило «назначение кода снимает `needs_review` строки» не записано (**V-5**). Пересчёт конвенции знака после PATCH невозможен без формы записи (**V-3**) |
| F-9 | «Код и наименование строки» (упорядоченный перечень по редакциям, порядок бланка, редакция по эффективной дате / обе, равные соседние допустимы), шаг 5 п. 2, Р-5 | closed, new issue | Итоги после слагаемых — верно. Но пятизначные подстрочные коды (11101, 12101, 23201 — все выгрузки 1С: НПЗ, Унагранде) ни в какой перечень бланка не входят → `needs_review` у каждой такой строки → НПЗ ББ/ОФР и Унагранде ОФР из `done` уходят в `needs_check` (**V-2**, critical). Алгоритм «положение относительно соседних подтверждённых» циклически определён. Порядок/состав редакций теперь в двух местах (**V-11**) |
| F-10 | Шаг 2 п. 3 (`period_kind = relative:<вид>`, `period_end = null`), «Что уходит из ядра» (PATCH даты → ядро досчитывает общим кодом), Р-6, DBML `column.period_kind` | closed | Согласовано с F-6: эффективная дата из колонок даёт относительным датам опору и без правки |
| F-11 | «Что уходит из ядра» (снять уникальность `document`, порядковый номер формы), Р-10 | closed, new issue | Потеря устранена, но не сказано, что делать с двумя документами одного типа и периода у одного ЮЛ (чек-лист, консолидация, `_pair_document`), и как быть с половинками формы (OCR: «одна таблица = одна форма, склейки нет») — склейки внутри файла нет (**V-7**) |
| F-12 | Шаг 8 п. 2 (таблица уровней), решение № 1 | closed, new issue | Все блокираторы из ревью перечислены (json набора, `BACKEND_PARSING_ERROR`, лист без кодов, сегменты, колонка без даты, заголовок, тип), xlsb/дробные решены. Но «ИНН не распознан» → `error` противоречит критерию `done` «(организация **или** ИНН)» и ломает НПЗ ОФР без ИНН (`inn_optional_for: [INCOME]`, сегодня `done`); «формат не совпал с `file_type`» → `error` противоречит собственному принципу решения № 1 (**V-6**) |
| F-13 | Шаг 7 («не сошлось» = вне допуска ±1 **и** вне округления 0,001 %; округление и мягкие → `soft`, `info`), «Подтверждение проверкой» (покрыто в пределах допуска или округления; мягкие подсветки не дают), шаг 8 п. 1 | closed | НПЗ (1600 = 1700 на 1,94 при ~17 млрд) и Унагранде (мягкие 2400/2410) остаются `done` по этому пункту |
| F-14 | «Назначение кода строке» (`PATCH …/statement lines: [{line_id, line_code \| null, line_status}]`, аудит, пересчёт, `missing → detail`, `detail → ok`), экран: поле кода у `unreadable`/`missing`/`detail` | closed | Мелкие пробелы спеки: переход `ok → detail`, недопустимая пара `line_code: null` + `line_status: ok`, `parent_row_index` при смене статуса (**V-12**, minor) |
| F-15 | «Что уходит из ядра» (одна функция «агрегат формы по коду», перечислены `_line_amount`, `statement_amounts`, `subline_parents`/`parent_code`, `formLayout`, `_resolve_cells`, `metrics.fields_*`), Р-4 | closed | Все точки из ревью названы. Уточнить формулировку: сумма — по строкам `ok` (шаг 7 говорит это, «Что уходит из ядра» — «сумма значений по колонке» без оговорки) |
| F-16 | Шаг 1 (`segment_breaks`, «граница листа — всегда граница сегмента»), шаг 2 п. 2 (своя раскладка, по числам без шапки, сопоставление по порядку, число не совпало → `error`), п. 6 шага 1 | closed, new issue | Сегмент-продолжение **без шапки и с 1–2 кодами** (НПЗ ОФР лист `_2` — единственная строка 2400, синтетика st07) по правилу п. 1 (б) «кодов ≥ 3» столбца кодов не получит; решение № 3 снимает `relaxed`, ссылаясь на склейку в шаге 1, но склейка не помогает — раскладка у сегмента своя (**V-4**) |
| F-17 | Р-1 (пакет `arm_statements` с тестами и конфигом линтеров, импорт по имени пакета, `service.statements_version`, `sync-statements --check` в docs, словари со своим загрузчиком), шаг 9, «Что уходит из ядра» (версия в `recognition_job`, расхождение — `error`) | closed | Все 5 пунктов закрыты. `recognition_job` не имеет колонки под `statements_version` (есть только `service_version`), в список DBML v5.19 не внесена (**V-10**, minor) |
| F-18 | «Что уходит из ядра» (`RECOGNIZED ⇔ done ∧ ИНН ≠ MISMATCH ∧ период — конец квартала`), шаг 8 п. 1 («квартал — знание ядра») | closed | — |
| F-19 | Принцип 5, Р-11 (двойники в суммах и кодах ⇒ `is_recognized = false`, далее §7 с оговорками F-1/F-2), шаг 4 п. 2 («замена двойника — исправление») | closed | Решение владельца, зафиксировано; §11.1 п. 1 помечен к отмене. Данные: в 4 json двойников в суммах/кодах нет (`(Ю4)` в aep_6 — «Ю» не в списке двойников, остаётся грязной) |
| F-20 | Шаг 4 п. 1 (квантование half-up, 16 целых цифр, дата/логическое → `null` + `string_value`, догадка > 16 цифр → `null`), решения № 6, № 13 | closed | — |
| F-21 | Шаг 2 п. 1 («код — текст без пробелов и переносов»), шаг 3 п. 1 («1 100» → 1100) | closed | Порядок с разлеплением (F-25) не оговорён: «переносы убираются» в п. 1 против «N частей через перевод строки» в п. 2 (**V-9**, minor) |
| F-22 | Шаг 6 п. 1 (5xxx и прочие → `DISCLOSURE`; без кодов — `type_hint`; `DISCLOSURE` без шагов 2–7), решение № 2 | closed | Форма 3/4 (3xxx/4xxx не в диапазонах, label не в словаре) → «не документ, `info`» — как сегодня (golden aep_8_form4 = `error` по ответу ИС); согласовано |
| F-23 | Шаг 1 (`locator file:…`), шаг 6 п. 3, шаг 8 п. 3 (`RECOGNITION_FAILED` / `INVALID_DOCUMENT` — различие записано), решение № 5 | closed | — |
| F-24 | Шаг 2 п. 1 («новое поведение для обоих: шлюз сегодня пропускает со счётчиком, кодом считает любое целое») | closed | — |
| F-25 | Шаг 3 п. 2 (разлепление и при нечитаемой части → `unreadable`; число частей не совпало → одна строка, многострочное значение → `null` + `string_value`) | closed | См. V-9 о порядке операций |
Итого: closed — 15, partial — 2 (F-5, F-8), closed с новой проблемой — 8 (F-1, F-3, F-9, F-11, F-12, F-16 + мелкие у F-14, F-17, F-21).
## 2. Разбор незакрытых и новых проблем
### V-1 — critical — F-5: защита «под подписью есть код» пробивается ОКОПФ в титульном блоке 1С; тай-брейка между столбцами-кандидатами нет
**Где:** шаг 2 п. 1 (а), решение № 3.
**Что проверил.** Скриптом по xls/xlsx прошёл все ячейки `^код(ы| строки)?$` и содержимое столбца под ними:
- `unagrande-co/2024-FY/Б.Б Лист 1.xls`: «Коды» r6c34 → под ней `0710001, 31, 47888105, 3249004697, 10.51.3, **12267**, 384, 7716021332, 1037739271701`; «Код» r28c22 → `1110, 11101, 11102, …`.
- `unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls`: «Коды» r3c18 → `0710002, 30, 69583357, 7728757141, **12267**, 384`, а дальше в том же столбце **значения таблицы** (`За Январь - Сентябрь 2024 г.`, `287017990.57`, `-302669265.62`…) — титульный столбец совпал с колонкой сумм.
`12267` (ОКОПФ «АО») по правилу дизайна — код: 4–5 цифр, первые четыре `1226 ∈ [1100, 1799]` (`statement_headers.yaml:86`; `columns.py:63-71` `line_code`). Для ООО (ОКОПФ 12300) то же. У НПЗ и Межениновской ОКОПФ в этом столбце нет — там защита работает.
**Сценарий.** Два столбца удовлетворяют (а). Правило «последняя такая подпись над первой строкой с кодом»: если «первая строка с кодом» — глобально по сетке, это r13 (ОКОПФ) → единственная подпись над ней «Коды» r6 → столбец c34 → каждая строка формы становится `unreadable`/`missing`, у Унагранде Групп ОФР столбец сумм объявляется кодами. Если «первая строка с кодом» — по столбцу, кандидатов два, а правила выбора в дизайне нет; реализация «первый найденный сверху/слева» выберет титульный блок. Сегодня парсер защищён порядком: сначала столбец по подсчёту (`find_code_column`, `columns.py:74-95`: кодов ≥ 3, доля ≥ 0,6), потом подпись только в этом столбце (`_find_header_row`, `:127-146`).
**Как закрыть.** Требовать под подписью не «хотя бы один код», а долю кодов среди непустых ≥ `min_share` (под «Коды» r6c34 — 1 из 9 = 0,11, под «Код» r28c22 — ~1,0; «документ из двух строк» сохраняется: 2 из 2); при нескольких кандидатах — столбец с наибольшим числом кодов. Либо вернуть порядок парсера (подсчёт → подпись в найденном столбце → заголовок по периодам), а исключение для малых таблиц делать явной веткой. Любой вариант зафиксировать в решении № 3.
### V-2 — critical — F-9 / Р-5: пятизначные подстроки не входят ни в один перечень бланка → каждая выгрузка 1С с детализацией теряет `done`
**Где:** «Код и наименование строки», шаг 5 п. 2, Р-5.
**Что проверил.** Пятизначные коды в фикстурах: НПЗ ББ — `11101…11103, 11501…11509, 11901, 12101…12106, 12201…, 12501…, 13401…, 15201…`, НПЗ ОФР — `23201…23203, 23301, 23401…, 25101, 25102`; Унагранде ББ — `11101…11507`. Это подстроки «в том числе» 1С (§12 п. 5: «пятизначные подстроки отдаются как обычные строки»; §7 «родитель = Σ подстрок», `statement_checks.py:141-184` `subline_parents`/`_subline_rules`). В словаре ядра `statement_lines.yaml` (132 строки, редакции 66n/fsbu4) пятизначных кодов нет — и в «упорядоченном перечне кодов по редакциям» их быть не может: они свои у каждой организации.
**Сценарий.** Правило дизайна: «Код подтверждён, если он **есть в перечне** и его положение … совпадает с порядком бланка; … иначе `needs_review` у строки, документ — `needs_check`». Уверенность не спасает: «Подтверждён — уверенность не важна; иначе `needs_review`» — оговорки для `confidence = 100` нет. НПЗ ББ (43 кода на листе 2, большинство пятизначные), НПЗ ОФР, Унагранде ОФР 3кв.25 — сегодня `done` (манифест строки 18, 39, 47–48) — по дизайну `needs_check`; Excel-путь теряет автоподтверждение целиком, у шлюза 1С-подобных сканов тоже.
Второе: «положение относительно соседних **подтверждённых** кодов» — определение циклическое, алгоритм не задан.
**Как закрыть.** (а) Пятизначный код подтверждён, если его четырёхзначный родитель по правилу `subline_parents` (ближайшая строка выше с теми же тремя цифрами, иначе код из первых четырёх цифр) подтверждён и подстрока стоит после него; (б) алгоритм — наибольшая подпоследовательность кодов, согласованная с порядком бланка хотя бы в одной из проверяемых редакций; коды вне её — `needs_review`; (в) явно спросить владельца: подтверждать ли структурой коды с уверенностью 100 (Excel) — по тексту шага 8 п. 1 «Excel выполняет условия уверенности автоматически» это, видимо, не задумывалось.
### V-3 — important — F-1 ↔ F-8: «форма записи знака» не хранится — ядро не может пересчитать конвенцию после PATCH; «сырой текст в подсказке» для распознанных ячеек пуст
**Где:** «Подтверждение проверкой» (знак), «Что уходит из ядра» (пересчёт `needs_review` общим кодом после PATCH), шаг 4 п. 1–3, шаг 9.
**Что проверил.** Контракт v1.2: `values[].string_value` — `["string","null"]` (схема l.199), DBML `financial_statement_value.string_value` — «как распознано, если не число»; шлюз пишет его только при `amount = null` (`document.py:149-151`). Дизайн (шаг 4 п. 3) расширяет это до «догадки» (`is_recognized = false`), но у ячейки, разобравшейся чисто, `string_value` остаётся `null`. Форма записи (скобки / минус / без знака) из `amount = −7744536` не восстанавливается.
**Сценарий.** aep_3 ОФР, колонка c2: 2120 `(7744536}` 88,9 → покрыто по величине, знак по конвенции (2210 `(693052)` 100, 2220 `(278598)` 100 — скобки) → `needs_review = false`. Аналитик правит любую ячейку документа → PATCH → ядро «пересчитывает `checks` и `needs_review` общим кодом»; входа «форма записи» у ядра нет → либо ветка знака пропускается (подсветка знака исчезает молча — то, против чего F-1), либо «сравнивать не с чем» → `needs_review = true` у всех членов `abs_terms` → `done` недостижим правкой. Второе следствие: 2210 `(546441)` с уверенностью 62,5 в c4 (чистый разбор, `is_recognized = true`) — «сырой текст и уверенность остаются в подсказке ячейки», но сырого текста в результате нет.
**Как закрыть.** В контракт v2.0 — `values[].text` (сырой текст источника всегда, как `Cell.text` шага 1), в DBML v5.19 — `financial_statement_value.source_text`; конвенция и подсказка считаются от него. Альтернатива — `sign_form: brackets | minus | none` + `text` только у `is_recognized = false`, но это два поля вместо одного.
### V-4 — important — F-16 / решение № 3: сегмент-продолжение без шапки с 1–2 кодами не получает столбца кодов — строка 2400 НПЗ теряется
**Где:** шаг 2 п. 1 (б) («кодов ≥ 3»), п. 2 («без шапки — столбец кодов по правилу п. 1»), решение № 3 («`relaxed` … снят, потому что продолжения склеиваются в шаге 1»).
**Что проверил.** НПЗ, лист «Отчет о финансовых результата_2»: подписи нет, кодов в столбце — **один** (2400); манифест строка 48: «лист с единственной строкой 2400 присоединён (O-9)», документ `done` по 3 листам. Сегодня: `_continuation` → `find_layout(relaxed=True)` (`statement.py:94-105`, `min_codes_continuation: 1`, `statement_headers.yaml:92`), затем `_weak_blocker` (`:145-160`). Синтетика `st07_single_code_continuation.xlsx` — тот же случай.
**Сценарий.** По дизайну лист дописывается в сетку в шаге 1, граница листа открывает сегмент, «у сегмента своя раскладка: … без шапки — столбец кодов по правилу п. 1» → (а) подписи нет, (б) кодов 1 < 3 → столбца нет. Что происходит с сегментом без столбца кодов, не сказано; при любом чтении строка 2400 либо теряется (принцип 2 нарушен), либо весь документ получает `error` «лист без колонки кодов» → `needs_check`. Склейка в шаге 1 этому не помогает — раскладка всё равно своя.
**Как закрыть.** Для сегмента не первого: подписи нет → столбец кодов наследуется от предыдущего сегмента, если в нём хотя бы один код (это и есть сегодняшний `relaxed` + `_weak_blocker` «коды из диапазона формы»); иначе — правило (б); не найден → `error` сегмента. Поправить решение № 3.
### V-5 — important — F-8 / F-9: `needs_review` строки негде хранить; снятие его назначением кода не описано
**Где:** «Код и наименование строки» (`StatementLine.needs_review`), шаг 8 п. 1 («нет значений **и строк** `needs_review`»), DBML v5.19 (только `financial_statement_value.needs_review`), «Назначение кода строке».
**Сценарий.** Результат с `needs_review = true` у строки 1230 (код с уверенностью 50 не на месте) → ядро «при загрузке ничего не пересчитывает, хранит как пришло» — колонки нет → признак теряется; после PATCH функция `done` получает вход «статусы строк, `needs_review`» неполным. Аналитик назначает код `unreadable`-строке (F-14) — `line_code_modified = true`, но правила «`line_code_modified` ⇒ `needs_review` строки = false» нет (для значений аналогичное `is_modified` ⇒ false записано) → строка подсвечена навсегда.
**Как закрыть.** DBML v5.19: `financial_statement_line.needs_review` (как пришло, пересчёт после PATCH); правило `line_code_modified = true` ⇒ `needs_review = false`; либо явно: признак строки хранится записью `document_issue(kind = warning, line_id)` и критерий `done` читает его оттуда — но тогда так и написать.
### V-6 — important — F-12: уровень `error` для «ИНН не распознан» и «формат не совпал с `file_type`» противоречит критерию `done` и принципу решения № 1; регрессия НПЗ ОФР
**Где:** шаг 8 п. 1 («(организация или ИНН)»), п. 2 (`error`: «ИНН не распознан», «формат файла не совпал с `file_type`»), решение № 1 («`error` — то, что меняет данные или их полноту»).
**Сценарий.** НПЗ ОФР: в шапке нет ИНН (манифест строка 45 `inn_optional_for: [INCOME]`), организация есть; сегодня `done`. По дизайну критерий 1 проходит (организация есть), но предупреждение «ИНН не распознан» уровня `error` даёт `needs_check` — два правила одного документа спорят, побеждает `error`. Формулировка «не распознан» — из шлюза; что сервис выдаёт у Excel при отсутствующем ИНН, не сказано (сегодня парсер об ИНН не предупреждает). «Формат не совпал с `file_type`»: файл прочитан верно, данные и полнота не изменились — по собственному принципу решения № 1 это `info`; сегодня — предупреждение без влияния на статус.
**Как закрыть.** «ИНН не найден/не распознан» → `error` только если и организации нет (= критерий «организация или ИНН»); при организации — `info`, сверку с ЮЛ делает ядро (`MISMATCH`). «Формат не совпал» → `info`. Таблицу уровней выровнять с принципом решения № 1.
### V-7 — important — Р-10 / F-11: два документа одного типа и периода у одного ЮЛ — правила выбора нет; половинки формы внутри файла не склеиваются
**Где:** «Что уходит из ядра» (DBML `document`, «привязка к версии консолидации — по периоду, как у ОСВ»), Р-10, шаг 1 п. 6 (OCR: «одна таблица = одна форма, склейки нет — без изменений»).
**Сценарий 1.** Лист-продолжение с маркером другого типа → отдельный кандидат того же периода; или ИС вернула ББ двумя таблицами (актив/пассив) — у ОСВ второй документ различим по счёту, у ББ — нет. Чек-лист `required_document_rule` (индекс `(legal_entity_id, document_type, osv_account_code, period_year, period_quarter, status)`), привязка к консолидации «по периоду», межформенная проверка `_pair_document` (`repositories/statements.py:330-346`: подтверждённый, затем последний по `created_at` — в одном файле `created_at` одинаков) — все берут «один документ на тип и период». Два подтверждённых ББ одного периода → неопределённость в КФО.
**Сценарий 2.** Половинки (актив `needs_check` по `1600 = 1700`, пассив `needs_check`) теперь обе хранятся, но операции «склеить» нет (OQ-140 запрещает только между файлами, внутри файла не описано) — аналитик не может довести файл до `done` иначе как перезагрузкой.
**Как закрыть.** Записать: в чек-лист и консолидацию идёт подтверждённый документ; подтверждение второго документа того же типа и периода у ЮЛ — 409 с указанием первого (или автозамена, как между файлами, §12 «Автозамена», `05-domain-rules.md:689`); склейка половинок не предоставляется — зафиксировать как известное ограничение или вопрос в `07-open-questions.md`.
### V-8 — minor — F-3 / шаг 2–3: роль строк над строкой заголовка не определена — титульный блок с числами становится `missing`
**Где:** выход шага 2 (`TableLayout`: роли `header | numbering | data`), шаг 3 («по строке на каждую строку с ролью `data`, у которой есть код или хотя бы одно число»).
**Что проверил.** Межениновская Форма1: над заголовком (r23) строки r6 «Дата (число, месяц, год) | 30 | 9 | 2025» и r15 «подлежит обязательному аудиту | 0 | ДА | 1» — кода нет, числа правее столбца кодов; Форма2 — r6. Сегодня они не данные: `find_layout` ставит `first_data_row = header_last_row + 1` (`columns.py:294-310`), `collect_lines` начинается с неё.
**Сценарий.** Если в `TableLayout` эти строки — `data` (третьей роли нет), по шагу 3 они без кода и с числами над первой строкой с кодом → `missing` → `done` недостижим у Межениновской (сегодня `done`, манифест строки 50–56).
**Как закрыть.** Роль `title` (или «не строка») для всего, что выше строки заголовка сегмента; при отсутствии заголовка — выше первой строки с кодом; `missing` — только между заголовком и первой строкой с кодом.
### V-9 — minor — F-21 ↔ F-25: порядок «разлепить по переводу строки» и «убрать переносы в коде» не задан
**Где:** шаг 2 п. 1 и шаг 3 п. 1 («пробелы и переносы убираются») против шага 3 п. 2 («ячейка кода содержит N частей через перевод строки → N строк»).
**Сценарий.** «1500\n1700» по п. 1 → «15001700» → не код → `unreadable`; по п. 2 → две строки `ok`. Записать явно: сначала п. 2 (разлепление), затем п. 1; для поиска столбца (шаг 2) многострочная ячейка считается кодом, если **каждая** часть — код.
### V-10 — minor — F-17: `recognition_job.statements_version` не в списке DBML v5.19
`recognition_job` имеет `service_name`, `service_version`, `result_schema_version`, `metrics jsonb` — отдельной колонки под версию общего кода нет, а «Что уходит из ядра» требует писать её и сравнивать. Добавить `statements_version varchar` в ту же миграцию (или оговорить хранение в `metrics`).
### V-11 — minor — F-9 ↔ Р-3: состав и порядок кодов по редакциям живут в двух местах
Общий пакет получает «упорядоченный перечень кодов по редакциям», ядро оставляет `statement_lines.yaml` с теми же редакциями и порядком бланка (`editions: 66n` l.9, `fsbu4` l.72) ради наименований и проверки «код не из редакции». Это та же пара копий, которую дизайн устраняет для §7. Предложение: словарь ядра — только `код → наименование` (по редакциям), порядок и состав редакций ядро берёт из своей копии пакета `arm_statements`; `sync-statements --check` сверяет, что все коды словаря ядра есть в перечне пакета.
### V-12 — minor — F-14: пробелы в операции назначения кода
Не описаны: переход `ok → detail` (ложный код, который аналитик хочет вывести из агрегата, не удаляя строку), недопустимость пары `line_code: null` + `line_status: ok`, пересчёт `parent_row_index` при переходе `missing → detail` (родитель — ближайшая строка `ok` выше), что происходит с `line_code_confidence` после назначения (100?).
### V-13 — minor — F-1: область правила знака не ограничена сомнительными ячейками; `null`-члены не исключены из большинства
Текст: «не совпадает или сравнивать не с чем — `needs_review = true` независимо от соотношения». При буквальном чтении правило применяется и к чистым ячейкам (`confidence = 100`, `is_recognized = true`): Excel-ОФР, где 2120 в формате `(#,##0)`, а 2410 — `-#,##0`, получит подсветку расходов; OCR-ОФР малой компании, где из членов `abs_terms` заполнена одна 2120 с уверенностью 100 — «сравнивать не с чем» → `needs_review`. Решение № 10 про «вперемешку скобками и минусом — это и нужно» относится к сомнительным ячейкам. Записать: знак проверяется только у ячеек, которым нужно покрытие (`confidence < 100` или `is_recognized = false`); в большинство входят члены с непустым `amount`.
## 3. Согласованность правок между собой
- **F-2 ↔ F-1 ↔ F-13 ↔ F-3/Р-8 ↔ Р-4/F-15.** Цепочка «агрегат по коду = Σ строк `ok` → соотношение посчитано, если хотя бы один член непуст → сошлось в пределах допуска или округления → покрывает только ячейки с `amount ≠ null` → для `abs_terms` только величину» внутренне непротиворечива; `detail`/`unreadable`/`missing` исключены из агрегата, подстроки 5xxxx — `ok` и проверяются по AURORA-1527 (мягко, подсветки не дают). Сбоит только хранение формы знака (V-3) и область правила (V-13).
- **F-6 ↔ F-10 ↔ F-9.** Одна «эффективная дата» (решение № 12) питает относительные подписи, редакцию перечня кодов и критерий `done`; пересчёт после PATCH даты описан. Согласовано. Не задан `period_kind` у колонки без подписи (шаг 2 п. 4) — предположительно `none`, стоит записать.
- **Статус `done` ↔ пересчёт после PATCH (F-8).** Входы функции названы; `is_modified`, акцепт, сохранение `error` — есть. Пробелы: `needs_review` строки (V-5), форма знака (V-3).
- **F-9 ↔ Р-3.** Перечень без наименований Р-3 не нарушает; но дублирует редакции/порядок словаря ядра (V-11) и не покрывает подстроки (V-2).
- **Контракт v2.0 (F-7/Р-9) ↔ DBML v5.19 (F-4/F-8/F-11/Р-10).** Соответствие полей: `line_status`, `line_code_text`, `row_index` → `row_index`, `ref` → `source_ref`, `parent_ref` → `parent_row_index`, `values[].needs_review` → `value.needs_review`, `columns[].period_kind` → `column.period_kind`, `document.status` → `document.status`, `document.warnings[]`/`checks` → `document_issue`, `document` без уникальности + порядковый номер, `crm_id`. `line_code` уже nullable в DBML v5.18, `period_date` nullable — миграция по ним не нужна. Не хватает: `financial_statement_line.needs_review` (V-5), `recognition_job.statements_version` (V-10), сырого текста значения (V-3). `service.statements_version` ложится в существующий объект `service {name, version}` контракта — аддитивно.
- **Р-11 ↔ F-1/F-2.** «Двойник ⇒ `is_recognized = false` ⇒ нужно покрытие ⇒ покрытие требует посчитанного соотношения и `amount ≠ null`» — согласовано; `}`/`]` как закрывающая скобка: не сказано, считается ли это «исправлением символа» (влияет только на то, нужно ли покрытие ячейке с уверенностью 100 — у `(7744536}` уверенность 88,9, вопрос не возникает).
- **Решение № 1 ↔ шаг 8 п. 2.** Списки `error` совпадают, кроме `BACKEND_PARSING_ERROR` и «формат не совпал» (есть только в п. 2); принцип решения № 1 нарушен двумя пунктами (V-6).
- **Р-8 ↔ §12 п. 5 / §11.1 п. 4 / строка 688.** Дизайн отменяет OQ-36/OQ-141; OQ-149 («восстановление строк по наименованию») не упомянут и остаётся открытым — противоречия нет, но при правке §12 его надо сохранить.
## 4. Что проверил и признал согласованным
- Р-2: ИС отдаёт ячейку на каждую позицию (по 8 таблицам повторно подтверждено, позиций не хватает 0).
- Статусы строк без кода по OCR: aep_3 ББ `detail` 2 / `unreadable` 6 / `missing` 0; aep_3 ОФР 1 / 1 / 1 («Выручка»); aep_6 ББ 19 / 2 / 0, ОФР 2 / 3 / 0; «БО 1 кв 2025» ББ 20 / 3 / 0, ОФР 2 / 2 / 0; «Русский бисквит 1 кв.25» ББ 40 / 3 / 0, ОФР 12 / 2 / 0 — цель Р-7 («чистый скан может стать `done`») по строкам достижима.
- Конвенция знака на данных: aep_3 ОФР c2 — 2120 `(7744536}` 88,9; 2210 `(693052)` 100; 2220 `(278598)` 100; 2330 `{38378)` 71,4; 2350 `(241654)` 75; 2410 `(108450)` 87,5 → конвенция «скобки», 2120 подтверждается, 2330 подсвечивается — как и описано в дизайне. aep_6 c4: 2220 `(124)` 100, 2350 `(Ю4)` 75 — «Ю» не двойник, остаётся грязной и подсвечивается.
- Диапазоны кодов словаря (BALANCE 1100–1799, INCOME 2100–2999, DISCLOSURE 5000–5999; `min_share 0.6`, `min_codes 3`, `min_codes_continuation 1`) — шаг 2 п. 1 (б) воспроизводит парсер.
- Шаг 6 п. 1 ↔ `statement_labels.yaml` шлюза (4 подписи: ББ, ОФР ×3) ↔ `statement_headers.yaml` парсера (ОКУД, «Пояснения» → DISCLOSURE) — слияние словарей не противоречит; форма 4 (aep_8_form4) сегодня `error` от ИС, регрессии нет.
- Шаг 9 / Р-9 ↔ `contract/common.py` (`SCHEMA_VERSION_PATTERN`, `ContractModel extra="ignore", strict=True`) — два маппера реализуемы; запрос остаётся 1.x.
- F-13 ↔ `statement_checks.yaml` (жёсткие 1100…1700, 2100/2200/2300; мягкие 2400 ×2, 2410, 2500; `abs_terms` 2120/2210/2220/2330/2350/2410) — формулировки дизайна совпадают со словарём.
- F-18 ↔ `form_confirmable` (`services/statements.py:71-81`: квартал обязателен) — `period_known` в ядре сохранён.
- F-6 ↔ `statement_period` (`statement_mapping.py:73-80`), F-9 ↔ `edition_for` (`:83-87`, граница 2024-12-31) — одна формула эффективной даты и редакции.
- F-15: `_pair_document` уже имеет порядок выбора (подтверждённый → последний), агрегат по коду его не ломает; `line_origin` (`schemas/statements.py:157-159`) по `line_code_original_value` при появлении `line_status` можно заменить, но это не противоречие.
- Уровень «xlsb без форматов», «дробные при тыс.», «подпись из словаря исключений» = `info` — совпадает с сегодняшним поведением парсера (документ `done`).
## Файлы и символы, которые смотрел
**docs** (worktree `.wt/normalization/docs`): `openspec/changes/unified-statement-normalization/design.md` (целиком), `review-2026-10-17.md` (целиком), `context/05-domain-rules.md` (строки 505–530 §7, 686–690, 990–999 §11.1 п. 4, 1105–1115 §12 п. 2, 1137–1155 §12 п. 5/7), `contracts/recognition-result.schema.json` (`$comment`, `service`, `lines[]`, `values[]`, `checks`, `string_value`), `model/data-model-v5.18.dbml` (`document`, `recognition_job`, `financial_statement_meta/column/line/value`).
**tabular-parser**: `api/src/parser/statements/columns.py` (`_CODE`, `line_code` 63–71, `find_code_column` 74–95, `_first_code_row`, `_find_header_row` 127–146, `_value_columns` 229–238, `find_layout` 294–310), `statement.py` (`_block_draft` хвост, `_continuation` 94–135, `_sheet_codes`, `_weak_blocker` 145–160, `_fill`), `detect.py` (`is_weak_continuation` 131–134, `classify_statement_sheet`, `_Candidate`), `api/rules/statement_headers.yaml` (19–34, 78–96), `statement_checks.yaml` (6–28), `api/tests/statements_manifest.yaml` (1–75).
**scan-gateway**: `api/src/normalizer/ocr/amounts.py` (`parse_amount` 44–84, `is_blank`), `rows.py` (`classify_row` 58–71, `split_merged_row` 92–118, `_data_rows`), `document.py` (`_value` 134–159), `api/rules/statement_labels.yaml`, `api/src/gateway/merge.py` (контекст склейки).
**arm-core**: `api/src/domain/statement_checks.py` (`subline_parents` 141–162, `_subline_rules` 165–184, `run_checks` 187–197), `statement_mapping.py` (`statement_period` 73–80, `edition_for` 83–87), `contract/common.py` (30–70), `services/statements.py` (`form_confirmable` 71–81), `repositories/statements.py` (`_pair_document` 330–346), `schemas/statements.py` (`line_origin` 157–159), `api/rules/statement_lines.yaml` (структура редакций, отсутствие пятизначных кодов), `services/result_mapping.py` (точки `MappingFatal`).
**fixtures** (скриптами `python3 -I` и venv парсера с `-I`): `ocr/aep_3.json`, `aep_3_ofr.json`, `aep_5.json`, `aep_6.json`, `aep_8_form4.json`, `БО 1 кв 2025.json`, `Бухгалтерская отчетность 1 кв.25.json`, `ocr/expected_normalize/*.json` + README; `statements-xlsx/unagrande-co/2024-FY/Б.Б Лист 1.xls`, `Б.Б. Лист 2.xls`, `ОФР. Лист 1.xls`, `unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls`, `mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx`, `1_Форма2_бух_09.xlsx`, `npz/2025-Q3/Бухгалтерская отчетность за Январь - Сентябрь 2025 г. (…).xlsx` (все 9 листов), `synthetic/st07_single_code_continuation.xlsx`.
Не смотрел: arm-web (не индексирован, по заданию), `services/documents.py`, `auto_confirm.py`, чек-лист (`repositories/consolidations.py`) — следствия Р-10 для них описаны по индексу DBML, а не по коду.