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

221 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Финальное ревью 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`.
- Файлы не правил.