# Финальное ревью design.md вслепую — третий раунд (17.10.2026, Opus) Ревьюер: отдельный агент (модель Opus), без права правки и без доступа к отчётам предыдущих раундов; получил design.md после коммита e6400bf, правила, контракт, DBML, фикстуры и код; прогонял текущий парсер на Excel-фикстурах. Маркеры находок — H-n. Сверка перед передачей владельцу (сессия): подтверждены по коду и правилам H-1 (= W-1; манифест парсера, строки 11, 24, 32), H-2 (`document.py:203-206` — шлюз требует уверенность 100 у ИНН и организации и распознанную дату), H-8 (`result_mapping.py` R-1: первый документ при повторе типа — `NEEDS_CHECK`), H-10 (§11.1 п. 4, строка 1006: «документ `needs_check`: разбивку подтверждает аналитик», решение владельца 08.10.2026), H-16 (= W-12; `subline_parents`: «те же три цифры»). Пересечения с отчётом Sonnet: H-1 = W-1, H-5 = W-9, H-9 = W-10, H-11б = W-2, H-11в = W-11, H-13б = W-5, H-16 = W-12, H-18 = W-14. Отчёт агента ниже без сокращений. --- # Ревью дизайна unified-statement-normalization (свежий взгляд) Документ: `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs/openspec/changes/unified-statement-normalization/design.md`. Номера строк дизайна ниже приблизительные (±2). ## Critical **H-1 (critical). Лист только с активом снова получает `done` и автоподтверждается.** - Где: шаг 7, правило «соотношение посчитано, только если хотя бы один член непуст» (F-2, ~стр. 554); «Подтверждение проверкой». - Дыра. Членом считается только правая часть: так прямо сказано по аналогии с AURORA-1527 и так же используется в правиле G-2. Правило `1600 = 1700` при отсутствующей строке 1700 больше не считается. - Сценарий. `unagrande-co/2024-FY/Б.Б Лист 1.xls` — в манифесте парсера `needs_check` «только актив: 1600 = 1700 не сходится» (`tabular-parser/api/tests/statements_manifest.yaml:8-11`). Я прогнал парсер: - 1100, 1200, 1600 сходятся (1200 — в пределах ±1); - 1700, 1300, 1400 и 1500 в файле нет; - по новым правилам `1600 = 1700` не считается, жёстких нарушений нет; - организация, ИНН и период есть, коды Excel структурно не проверяются → `done` → в ядре `RECOGNIZED` → автоподтверждение половины баланса. - То же у `unagrande-group/*/ББ. Лист 1*.xls`. - Сегодня правило проверяется при присутствующей цели, отсутствующие члены = 0 (`tabular-parser/api/src/parser/statements/checks.py:121-127`; ядро `arm-core/api/src/domain/statement_checks.py:200-205`). - Как закрыть: считать цель членом при проверке «посчитано» (пропускать только случай «все пусты, включая цель»). Добавить в обязательные тесты общего кода строки манифеста со статусом `needs_check` — они должны остаться `needs_check`. **H-2 (critical). Уверенность шапки выпала из критерия `done`: неверно прочитанная дата отчёта даёт неверный период и автоподтверждение.** - Где: шаг 8 п. 1 (критерий `done`) и п. 2 (уровни предупреждений); «Подтверждение проверкой» — фраза «Не покрыто проверками и остаётся на уверенности: поля шапки» (~стр. 616); шаг 9 (список изменений контракта); шаг 6 («`confidence` → `*_confidence`»). - Дыра. Механизма для полей шапки нет: - `needs_review` есть только у значений и строк; - в критерии `done` уверенности шапки нет; - в уровнях есть только «ИНН не найден или неуверен при найденной организации» (`info`) и «дата не разобрана» (`info`); случая «дата разобрана, но уверенность < 100» нет вовсе; - в контракте v2.0 не добавлены `report_date_confidence`, `units_confidence`, `*_text`: шаг 6 обещает их, шаг 9 — нет. - Сегодня шлюз требует 100 у ИНН и организации и «распознана» у даты (`scan-gateway/api/src/normalizer/ocr/document.py:202-208`, `ocr/__init__.py:79-84,103`). Ядро требует, чтобы конверт был `done`. - Сценарий. В шапке ИС ИНН и организация с уверенностью 100, а «30 сентября 2025» прочитано как «30 сентября 2024» с уверенностью 71. Таблица чистая. - Эффективная дата = дата отчёта (она приоритетнее колонок), период 3 кв. 2024. - Относительные колонки тоже считаются от неё. - Итог: `done` → автоподтверждение и привязка к версиям 3 кв. 2024. - Автозамена (§9.5 п. 3) вытесняет чужой файл того же ключа, в том числе с подтверждённым документом. - В фикстурах дата шапки часто мусор: aep_3 «2|0|2|4|»@75, у aep_6 и «БО 1 кв» в поле даты — название организации с уверенностью 100. - Как закрыть: - ввести признак «требует проверки» для полей шапки (как `needs_review`); - дату отчёта с уверенностью < 100 подтверждать структурой: совпала с самой поздней абсолютной датой колонок — подтверждена, иначе `error`; - то же для подписей колонок OCR с уверенностью < 100; - добавить поля уверенности шапки в список изменений v2.0. **H-3 (critical). В сегменте без шапки роль `title` поглощает строки с суммами — данные теряются молча, `missing` недостижим.** - Где: шаг 2, определение ролей (`title` — «а без заголовка — выше первой строки с кодом», ~стр. 410) и п. 4; шаг 3, определение `missing` («между заголовком сегмента и первой строкой с кодом … потерян в самом начале таблицы», ~стр. 450). - Дыра. Для сегмента без шапки всё выше первой строки с кодом — `title`, и в данные оно не попадает. Поэтому строка с потерянным кодом в начале такой таблицы исчезает. Это противоречит принципу 2 и определению `missing`. - Сценарий 1, aep_3 (в ИС нет строки шапки): - table 0, row 0: «Нематериальные активы», код `'НИ'@0`, сумма `'30127'@60`. Первая строка с кодом — row 1 (`1120`), поэтому row 0 — `title`, НМА пропадает; - table 1, row 0: «Выручка», код пуст, `'211(1 |'`. Это выше `2120`, тоже `title`. - Сегодня обе строки хотя бы считаются в «строк без кода с суммами пропущено» (golden `fixtures/ocr/expected_normalize/aep_3.json`). Теперь даже счётчика нет, Р-13 не блокирует подтверждение, ядро достраивает пустую шаблонную 1110. - Сценарий 2, Excel. Лист-продолжение без шапки, первая строка — расшифровка без кода с суммой. В НПЗ такие строки есть: «Депозитные счета» на Листе 3, «Авансы по инвестиционной деятельности» на Листе 1. Если разрыв страницы придётся на них, строка становится `title` и пропадает. - Как закрыть: `title` — только выше строки заголовка (и титульный блок первого сегмента Excel). В сегменте без шапки строки выше первого кода, где в колонках значений есть содержимое: `missing`, а при непустой ячейке кода — `unreadable`. **H-4 (critical). Структурная проверка кода не ловит подмену на соседний или незапечатанный код — автоподтверждение с неверной разноской.** - Где: «Код и наименование строки» (LIS по порядку бланка, «Равные соседние коды допустимы (Р-4)»), Р-5, Р-12. Дизайн сам приводит мотив: «перепутанные соседи 1220 и 1230 дают ту же сумму раздела». - Дыра. LIS ловит только нарушение порядка. Есть два пути мимо: - **(а) дубль.** OCR прочитал 1230 как 1220@50. Последовательность 1210, 1220, 1220, 1240 допустима: равные соседи разрешены Р-4. Сумма раздела в `1200 = …` та же → покрыто → `done`. Итог: 1220 = НДС + ДЗ, 1230 пуст. Такой дубль неотличим от законного: в «Бухгалтерская отчетность 1 кв.25.json» row 28 — настоящая вторая строка `1220@50` «НДС, начисленный по отгрузке». - **(б) подмена на незапечатанный код того же раздела.** 66н позволяет не печатать пустые строки; в том же файле 1120 нет: 1105, 1110, 1130. Чтение 1110 как 1120@50 даёт порядок 1105, 1120, 1130 — согласован. Сумма 1100 та же → `done` с НМА в строке «Результаты исследований». - Как закрыть: решение владельца, варианты: - для кода с уверенностью < 100 сделать условием `RECOGNIZED` в ядре совпадение распознанного наименования со справочным по коду (открытый вопрос ядра перестаёт быть открытым — это знание ядра, как квартал); - либо дубль с уверенностью < 100 и подмена в «дыру» бланка → `needs_review`. - В любом случае признать риск в тексте: сейчас текст обещает защиту, которой нет. ## Important **H-5 (important). Диапазоны кодов противоречат сами себе и молча расширены.** - Где: шаг 2 п. 1 («1xxx — ББ, 2xxx — ОФР, 3xxx–6xxx …; `type_hint` неизвестен — объединение»), пункт про tabular-parser («диапазон формы → объединение»), шаг 3 п. 2 (G-21: «"44 30\nI |" aep_6 не множится»), шаг 6 п. 1 (тип по большинству кодов). - Дыра: - Если `type_hint` известен и берётся только его диапазон, то в OCR-таблице с `label` «ББ» и кодами 2xxx колонка кодов не найдётся. Сверка «тип по кодам» в шаге 6 станет недостижимой, а сегодняшнее поведение шлюза #30 (единодушные коды) потеряется. - Если всегда объединение, то «4430» — код 4xxx, и пример G-21 неверен: строка разлепится. Строка aep_6 row 90 `'4 400\nI #4У'` даст строку `ok` с кодом 4400 в балансе. - Расширение до 1000–6999 по сравнению с сегодняшними 1100–1799 / 2100–2999 / 5000–5999 (`tabular-parser/api/rules/statement_headers.yaml:86-88`) делает кодами годы 2000–2099 («2025» титульных блоков) и 4-значные суммы на 1–6. - Как закрыть: явно задать «кодовые диапазоны» (объединение узких диапазонов, как сейчас, плюс 3xxx/4xxx/6xxx по точным границам форм) и «диапазон формы» (для наследования и слабых продолжений). Исправить пример G-21. **H-6 (important). Неясно, на каком уровне выдаётся `error` «строка заголовка не найдена»: если на сегмент — НПЗ теряет `done`.** - Где: шаг 2 п. 4 и п. 2. - Сценарий. Книга НПЗ сегодня `done` (оба документа). У листов «Бухгалтерский баланс. Лист 2/3/5» и «Отчет о финансовых результата_2» шапки нет: я выгрузил ячейки. Если предупреждение п. 4 выдаётся на каждый сегмент — документ `needs_check`, регрессия. - Дополнительно. Колонки сегмента без шапки «по числам» сопоставляются с первым сегментом по порядку. Если на листе-продолжении средняя колонка пустая или в прочерках, найдутся две колонки, и значения 2023 окажутся в колонке 2024. Документ `needs_check`, но в БД данные лягут неверно. Сегодня `join_blocker` такой лист не присоединяет (`tabular-parser/api/src/parser/statements/merge.py:39-40`). - Как закрыть: `error` — только если заголовка нет ни у одного сегмента документа. Колонки сегмента без шапки сопоставлять по положению относительно столбца кода или по колонкам первого сегмента; при несовпадении значения сегмента не раскладывать. **H-7 (important). Сборка листов до раскладки снимает защиту по числу колонок.** - Где: шаг 1 п. 6 («по маркеру или по позиции листа»; «проверка числа колонок переезжает в шаг 2 п. 2»). - Сценарий. После формы идёт лист без маркера и заголовка с 3+ кодами того же диапазона, например расшифровка или форма другого периода, с другим числом колонок. - Сегодня это отдельное «одинокое продолжение». - По дизайну он вливается сегментом. Его коды дублируют коды формы и по Р-4 суммируются. §7 падает, но разделить данные аналитик может только пометкой каждой строки как `detail`. - Как закрыть: оставить несовпадение числа колонок условием отказа в склейке (продолжение становится отдельным кандидатом с `error`). **H-8 (important). Р-15 автоподтверждает первую форму, а сегодня первая — `NEEDS_CHECK`; аналитик может оказаться в тупике.** - Где: Р-15, «Что уходит из ядра» (`statement_status`, «Подтверждение документа»). - Дыра: - Сегодня при второй форме того же типа первая получает `NEEDS_CHECK` (`arm-core/api/src/services/result_mapping.py:352,364`, R-1/OQ-77). - Дизайн запрещает автоподтверждение только второй. Первая может автоподтвердиться, а подтверждение второй — 409. - Если неверна первая (двойная печать, половинка), выхода нет: `CONFIRMED` только для чтения, повторная загрузка того же файла отсекается дедупом по sha256, удаление часто запрещено (§9.5 п. 5). - Неоднозначно и «у ЮЛ» в 409: только тот же файл или любые файлы ЮЛ (после правки даты отчёта)? - Как закрыть: при двух и более формах одного типа и периода в файле не автоподтверждать ни одну. Уточнить область 409. **H-9 (important). Противоречие: OCR-таблица без колонки кодов с неопознанным `label` пропадает с `info`.** - Где: шаг 6 п. 1 («`type_hint` нет → не документ, `info`») и список `info` в шаге 8 п. 2 («не документ без `type_hint` и кодов») против списка `error` («таблица или сегмент с содержимым без колонки кодов»). - Сценарий. Плохой скан: `label` ОФР зашумлён (не сопоставился), колонка кодов не найдена. Таблица — `info`, а соседний ББ `done` → конверт `done` → ББ автоподтверждён, потеря ОФР не видна. Сегодня шлюз выдаёт «нет шапки и столбца кода» → `needs_check` (`scan-gateway/api/src/normalizer/ocr/__init__.py:96-98`). - Как закрыть: для источника `ocr` каждая таблица ИС — кандидат, её потеря — `error`. Для Excel «прочее» — `info`. **H-10 (important). Разлепление склеенных строк молча меняет решение владельца по OQ-156.** - Где: шаг 3 п. 2; таблица уровней шага 8 п. 2. - Дыра. В §11.1 п. 4 по решению владельца 08.10.2026: «документ `needs_check`: разбивку подтверждает аналитик». В таблице уровней разлепления нет. В «Бухгалтерская отчетность 1 кв.25» строка `1500\n1700` по суммам сходится, и документ может стать `done` и автоподтвердиться. Решение не вынесено ни в Р-n, ни в «Decisions made without discussion». - Как закрыть: явно указать уровень и записать в решения. **H-11 (important). Пересчёт ядра после PATCH: устаревшие `error` сервиса и подтверждения, которые нигде не хранятся.** - Где: принцип 3; «Подтверждение проверкой» («предупреждения `error` документа сохраняются и после правки»); PATCH; DBML v5.19. - Дыры: - **(а)** Из `error` сервиса правкой снимается только «колонка без даты». После PATCH `report_date` остаются «период не определён» и «дата не разобрана», после PATCH `inn` — «ни организации, ни ИНН». Статус после правки навсегда `NEEDS_CHECK`, а экран показывает уже исправленные причины. - **(б)** `values[].confirmed` негде хранить: в списке изменений v5.19 у `financial_statement_value` нет признака подтверждения. На следующем пересчёте `needs_review` вычисляется заново из уверенности и §7, и подтверждение пропадает (G-12). - **(в)** Нет способа подтвердить верный код с низкой уверенностью без смены. Переход `ok → ok` описан только «с другим кодом», поэтому `needs_review` строки снять нельзя. - Как закрыть: таблица «какое `error` сервиса снимается какой правкой»; колонка `is_confirmed` (или `confirmed_by/at`) у значения и у строки в v5.19; переход «подтвердить код». **H-12 (important). Шаблонные строки и новые поля строки.** - Где: PATCH `lines` («шаблонная строка с таким кодом заменяется настоящей»), DBML v5.19. - Сценарий. Код строки НМА потерян (`unreadable`). 1. Аналитик вводит суммы в достроенную шаблонную 1110. Ядро создаёт её в БД (`line_code_original_value = NULL`; адресация по `line_code` — `arm-core/api/src/services/statements.py:219-245`). 2. Потом аналитик назначает код 1110 строке `unreadable`. 3. Возможны два исхода: обе строки `ok` и суммируются по Р-4 (двойной счёт), либо «замена» стирает ручные суммы (аудит?). - Также не определены: - `row_index`, `source_ref`, `line_status` для строк, созданных правкой; - заполнение этих полей для существующих строк при миграции; - уникальность `source_ref` при NULL. - `repositories/statements.py:74` ключует вставку по `line_code`. - Как закрыть: правило слияния шаблонной строки (перенос правок или запрет с 409), значения полей для строк из правки, заполнение при миграции. **H-13 (important). Упрощённые формы: регрессия ОФР и недостижимое определение.** - Где: шаг 6 п. 1 (`form_variant`), шаг 7, решения 14–15. - Дыры: - **(а)** Правило G-2 «цель отсутствует при непустых членах — жёсткое нарушение» делает упрощённый ОФР (2110, 2120, 2330, 2340, 2350, 2410, 2400 без 2100/2200/2300) вечным `needs_check`. Структурный признак упрощённой формы задан только для баланса. Решение 14 утверждает «как и сегодня», но сегодня цель отсутствует — правило пропускается, и упрощённый ОФР может быть `done`. - **(б)** 0710096 — по моим сведениям КНД ФНС, а не ОКУД печатной формы; на бланке ББ/ОФР стоят 0710001/0710002. Проверить. Кроме того, 0710096 подпадает под шаблон DISCLOSURE словаря ОКУД (`statement_headers.yaml:26`) → `type_hint = DISCLOSURE`, а для DISCLOSURE по подсказке шаги 2–7 не выполняются. До определения варианта дело не дойдёт. - Это частный случай противоречия «окончательно — по кодам строк в шаге 6, у обоих» и «DISCLOSURE по `type_hint` — шаги 2–7 не выполняются». - Как закрыть: структурный признак для ОФР (есть 2400/2110, нет 2100/2200/2300); снять 0710096 или поправить его разбор; оговорить исключение для DISCLOSURE. **H-14 (important). Объём контракта v2.0 и перехода не охватывает ОСВ и конверт.** - Где: шаг 9, шаг 8 п. 3. - Дыры: - `metrics.warnings` становится структурой. В том же результате tabular-parser отдаёт `osvDocument` и строковые предупреждения ОСВ; переделка пути ОСВ не описана. - «Конверт `done` ⇔ все документы `done`» — у ОСВ нет `document.status`. Не определено, как агрегируется смешанная книга ОСВ + формы. - При приёме 1.x у документа нет `document.status`, значит нужен откат к старому D-6 (`job_done ∧ checks ∧ values_ok …`). Сказано только про `needs_review`. - ack тоже несёт `schema_version` — ядро должно принимать ack 1.x/2.0 в переходный период. - `to_money` используется и ОСВ (`tabular-parser/api/src/parser/classify.py:183`). Замена его общим `parse_amount` (шаг 4 п. 1) без ограничения областью форм меняет разбор ОСВ: двойники, «лучшая догадка». - Как закрыть: описать в спеке контракта ОСВ-часть и агрегацию; запасной статус для 1.x; ограничить замену `to_money` формами. **H-15 (important). xlsb + «тыс. руб.» + дробные значения = вероятный масштаб ×1000, а уровень — `info`.** - Где: шаг 4 п. 4; шаг 8 п. 2 (`info`: «дробные при тыс. руб.», «xlsb без числовых форматов»). - Сценарий. Выгрузка 1С в xlsb хранит рубли с копейками, а в тысячи их переводит формат, который для xlsb не читается. Значения в 1000 раз больше, §7 сходится при любом масштабе → `done` → автоподтверждение в КФО. Это унаследовано (сегодня статус тоже не меняется), но дизайн закрепляет это явно. - Как закрыть: сочетание «xlsb без форматов ∧ доля дробных ≥ порога при тыс.» → `error`. ## Minor **H-16 (minor). Правило родителя пятизначной подстроки описано неверно.** - Дизайн: «ближайшая строка выше с теми же четырьмя первыми цифрами, иначе код из первых четырёх цифр — правило `subline_parents`». - Код: первые **три** цифры (M-9): `arm-core/api/src/domain/statement_checks.py:141-162`, `tabular-parser/.../checks.py:91-96`; §12 п. 7 «12310 под 1230». - По тексту дизайна 12310–12314 НПЗ (Лист 2) теряют родителя. Для OCR-кодов с уверенностью < 100 это ещё и неподтверждённая структура → `needs_check`; ломаются мягкая проверка и показ. **H-17 (minor). Неточности базовой линии.** - Таблица шага 2, строка 1: шлюз ищет столбец кодов по справочнику кодов (`infer_layout`, `scan-gateway/api/src/normalizer/ocr/layout.py:82`), а `int()` — это классификация строк. - G-2 «сегодня пропуск»: верно только для отсутствующей цели. Цель с `null` при непустых членах уже сегодня жёсткое нарушение (null = 0 при присутствующей строке). **H-18 (minor). `parse_amount` не доопределён.** - Не указано, как правило «точка — разделитель тысяч» (sg#37) работает для `excel`. Сегодня Excel «1.500» = 1,5, при общем правиле станет 1500. - Не указана судьба текста OCR с более чем двумя знаками после запятой («0,125», решение №30: `amount = null`). - Не определена многострочная сумма в строке с однострочным кодом: склейка «7А 794\n8 У 201» в одну «догадку». Правило «многострочное → `null`» сформулировано только в контексте разлепления. **H-19 (minor). Р-2 и шаг 8 расходятся для пустого текста с уверенностью 0.** aep_3 table 1 row 15: «пустая — без подсветки» или «непрочитанная — `needs_review`»? **H-20 (minor). Строки без кода, где в суммах только мусор, выбрасываются как «не строка».** aep_3 row 8: код `[190 :`, значения `'Ё'@0`, `' : ।'@0`, `'" :'@33`. Ячейки с непустым текстом и уверенностью < 100 — признак потерянных сумм; по принципу 2 их стоит считать содержимым. **H-21 (minor). `missing` в начале нового сегмента или листа.** - Расшифровка, перенесённая через разрыв страницы, получает `missing` вместо `detail` и блокирует подтверждение (Р-13). - Предложение: родитель `detail` продолжается из предыдущего сегмента документа; `missing` — только в первом сегменте. **H-22 (minor). Мусор в ячейке кода, в которой нет ни одной цифры.** aep_6 ОФР row 4: «в том числе: Крестьянская, 175», код `'}'@0`, сумма `'по\n(0 215)'`. Строка становится `unreadable` с суммой и блокирует подтверждение, хотя это расшифровка. Стоит решить, считать ли такой код пустым (`detail`/`missing`). **H-23 (minor). Р-11 и вертикальные линии таблицы.** «|» вплотную к цифрам в суммах — частый след границы таблицы (aep_3 `'| : 122477'`). Замена на 1 даёт лишнюю цифру: §7 подсветит, но ложных подсветок станет больше. Для сумм крайние «|» убирать, а не заменять. **H-24 (minor). Мелкие противоречия и пробелы.** - DISCLOSURE по кодам при `type_hint` BALANCE — одновременно «`error`» (тип противоречит) и «DISCLOSURE всегда `done`». - Снятие уникальности `document` (Р-10) не должно снимать её у ОСВ по счёту и у DISCLOSURE/OTHER — «один на тип на файл», §9.5 п. 2. Нужен частичный уникальный индекс. - Место `LINES_UNRESOLVED` в порядке 409 (§9.5 п. 4) не задано. **H-25 (minor). Сцепка через статус конверта (G-7) шире, чем нужно.** - Сценарий: в «Бухгалтерская отчетность 1 кв.25» ОФР — `needs_check` из-за 2400@71, а 2400 есть только в мягких соотношениях. Из-за этого не автоподтверждается и чистый ББ того же файла. - При статусе на документ достаточно условия «`document.status = done` ∧ нет `error` уровня файла». **H-26 (minor). Р-1: сопровождение копии.** - Расхождение `statements_version` с копией ядра → `error` при пересчёте. Но предписанный порядок выкладки («сначала ядро») гарантирует расхождение на всё окно выкладки: правленые документы будут массово `NEEDS_CHECK`. Лучше `info` или сравнение хэша словарей. - Копия «со своими тестами и конфигом линтеров» попадает под ruff и pytest хоста: конфликты правил, `testpaths`, `conftest`. - Срочное исправление требует трёх PR в разное время — нужен описанный порядок. ## Проверено и признано согласованным - Р-4 (сумма по коду) на реальных данных верна. «Бухгалтерская отчетность 1 кв.25»: 1220 = 19 062 + 9 273 сходится с 1200 = 1 677 325; 1520 = 771 605 + 89 221 сходится с 1500 = 917 775. Сегодняшнее «оставлена первая» здесь даёт ложное нарушение. - `detail` по Р-8 на НПЗ («Авансы…», «Депозитные счета») и в «1 кв.25» («в том числе…») сохраняет `done`, если строки стоят под строкой с кодом в том же сегменте. - Защита от титульного блока «Коды» (V-1/G-5) на НПЗ (кол. 23 против кол. 13) и Межениновской (кол. 16 против кол. 13) работает: под титульной подписью 0 кодов. Роль `title` выше строки заголовка корректно убирает «Дата 30 | 9 | 2025». - Конвенция знака (F-1) на ОФР «1 кв.25» и aep_3: «(7744536}» → минус, «{38378)» → подсветка. Ведёт себя, как описано. - Р-7 при посчитанных соотношениях: подтверждение `is_recognized = false` требует компенсирующей ошибки — согласовано с Р-5 и V-13. - Межформенная проверка, `_line_amount` и `statement_amounts` перечислены в F-15. Привязка к версии (`consolidation_documents._key` + «самый поздний `confirmed_at`») согласуется с 409 по Р-15. ## Что смотрел - Дизайн целиком; `05-domain-rules.md` §7, §9.2–9.5, §11, §11.1, §12; контракт (`$comment`, `statementDocument`, `metrics`, `service`, `status`); DBML: `document`, `financial_statement_*`, `ex_snapshot_item`; `03-architecture.md` (контракт и копии словарей). - **scan-gateway:** `normalizer/ocr/__init__.py`, `document.py` (`build_document`, `_resolve_type`, `_columns`, `_choose_edition`, `_value`, `_line`, `_statement_header`), `layout.py`, `rows.py` (`split_merged_row`, `parse_table_rows`, `_count_skipped`), `amounts.py`, `text.py`, `header.py`, `normalizer/rules.py`, `rules/ocr_lookalikes.yaml`, `statement_labels.yaml`. - **tabular-parser:** `statements/columns.py`, `lines.py`, `merge.py`, `statement.py`, `checks.py`, `book.py`, `numbers.py` (`to_money`, `_parse_text`, `quantize_money`), `rules/statement_headers.yaml`, `statement_checks.yaml`, `tests/statements_manifest.yaml`. - **arm-core:** `domain/statement_mapping.py` (`build_statement`, `statement_period`, `statement_status`, `values_ok`), `domain/statement_checks.py` (`run_checks`, `subline_parents`, `cross_form_check`), `services/statements.py` (`form_confirmable`, `_apply_values`, `_resolve_cells`, `_apply_header`), `services/documents.py` (`_check_statement`), `services/result_mapping.py` (OQ-77/R-1), `services/consolidation_documents.py` (`_key`, `bind_confirmed`), `repositories/statements.py`, `web/src/widgets/DocumentReview/statementGrid.ts`. - **Фикстуры:** построчно `fixtures/ocr/aep_3.json`, `aep_6.json`, «БО 1 кв 2025.json», «Бухгалтерская отчетность 1 кв.25.json»; golden-эталоны `expected_normalize/*`. Прогон текущего парсера на `unagrande-co/2024-FY/Б.Б Лист 1.xls`, `Б.Б. Лист 2.xls`, НПЗ, Межениновская Форма1/Форма2; выгрузка ячеек листов НПЗ и Межениновской. Скрипты лежат в `/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/12de9a9a-5665-41d0-beea-0651270d88ec/scratchpad/runp.py` и `/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/12de9a9a-5665-41d0-beea-0651270d88ec/scratchpad/dump.py`. - Файлы не правил.