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

38 KiB
Raw Blame History

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