# Повторное ревью 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, а не по коду.