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

38 KiB
Raw Blame History

Проверка закрытия V-1…V-13 и G-1…G-30 (17.10.2026, третий раунд, Sonnet)

Ревьюер: отдельный агент (модель Sonnet), без права правки; получил design.md после коммита e6400bf, отчёты review-2026-10-17-closure.md (V-n) и review-2026-10-17-blind.md (G-n). Маркеры новых проблем — W-n, наблюдения — O-n.

Отчёт агента ниже без сокращений.


Проверка закрытия V-1…V-13 и G-1…G-30 в обновлённом design.md (коммит e6400bf)

Файлы не правил и ничего не коммитил. Читал design.md целиком (850 строк), оба отчёта ревью и фикстуры. Сценарии из находок прогонял по новым правилам. Данные проверял скриптами python3 -I, для xls — venv парсера с -I.

Итог: 43 находки.

  • closed — 31.
  • closed, new issue — 9: V-3, V-13, G-2, G-12, G-16, G-17, G-19, G-25 и ещё G-1 в связке с W-6.
  • partial — 3: V-4, V-12, G-8.
  • not closed — нет.

Новых противоречий найдено 16 (W-1…W-16). Серьёзные из них:

  • W-1 — регрессия детектора «только актив», три файла из манифеста.
  • W-2 — confirmed нигде не хранится.
  • W-4 — наследование столбца кодов ломает лист «_2» НПЗ.
  • W-3, W-5, W-6, W-9 — средней серьёзности.

1. Таблица

Находка Где закрыто Статус Что осталось / новая проблема
V-1 Шаг 2 п. 1 (а): подпись принимается при доле кодов ≥ 0,6 или кодах ≥ 3; при нескольких кандидатах — больше кодов; «документ из двух строк» = 2 из 2; решение № 3 closed Проверил на данных. Унагранде ББ Лист 1: «Коды» (5,33) даёт 1 код из 9, доля 0,11; «Код» (27,21) — 43 из 43. ОФР Лист 1 3кв.25: «Коды» 1 из 25, «Код» 17 из 17. Титульный столбец отсекается, форма берётся верно
V-2 «Код и наименование строки»: пятизначный код подтверждён через четырёхзначного родителя; применяется только к кодам с уверенностью < 100 (Р-12); Excel и чистые OCR-коды дают info ядра closed Формулировки уточнить в спеке: W-12 (правило родителя), W-13 (границы алгоритма)
V-3 Шаг 4 (values[].text); DBML v5.19 source_text; конвенция знака «от сохранённого text» closed, new issue W-8: у правленой ячейки text устаревает и участвует в голосовании за конвенцию колонки
V-4 Шаг 2 п. 2: сегмент без шапки и с кодами < 3 «наследует столбец кодов» предыдущего сегмента partial W-4: у НПЗ столбец кодов на каждом листе свой, наследование по индексу не работает
V-5 DBML v5.19: financial_statement_line.needs_review; PATCH: line_code_modified ⇒ needs_review строки = false closed —
V-6 Шаг 8 п. 2: «ИНН не найден при найденной организации» — info, «ни организации, ни ИНН» — error; «формат не совпал» — info closed Согласовано с критерием done п. 1 и решением № 1. НПЗ ОФР без ИНН остаётся done
V-7 Р-15; «Что уходит из ядра»: вторая форма не автоподтверждается, подтверждение второй — 409; вопрос о склейке — в 07-open-questions.md closed Решено владельцем. Формулировка 409 без оговорки «из одного файла», но между файлами работает автозамена (§12 п. 3), поэтому на практике это только внутри файла
V-8 Шаг 2: роль title для всего выше строки заголовка; шаг 3: missing только между заголовком и первой строкой с кодом closed Межениновская: «Дата 30 | 9 | 2025» теперь title. Повторная шапка «Коды» (стр. 48 Формы 1) открывает новый сегмент по п. 2, а не unreadable
V-9 Шаг 3 п. 2: разлепление «до разбора кода»; шаг 2 п. 1: многострочная ячейка — код, если каждая часть код closed —
V-10 DBML v5.19: recognition_job.statements_version closed —
V-11 Принцип 4, шаг 5 п. 2, «Что уходит из ядра»: словарь ядра хранит только имена, редакции берутся из копии общего кода, sync-statements --check closed Побочно: коды перечня без имён (1105, 1530, …) дают info «не в словаре имён» — безвредно
V-12 PATCH lines: переходы, 422 для пары «ok без кода», пересчёт родителя detail, аудит partial W-11: не сказано про line_code_confidence после назначения; у строк нет аналога confirmed
V-13 «Подтверждение проверкой»: чистые значения в покрытии не нуждаются closed, new issue W-7: фраза «ни одним правилом ниже не подсвечиваются» противоречит следующему пункту про непройденное жёсткое соотношение
G-1 Р-12 + «Код и наименование строки»: источник перечня — коды шаблонов банка плюс коды печатных форм 1С; пятизначные через родителя; для Excel — info closed Перечень всегда неполон: в ББ. Лист 1 (unagrande-group) есть 1231, которой нет ни в словаре ядра, ни в названных 1С-кодах. Для Excel и чистого OCR это info, для кодов с уверенностью < 100 — needs_review, что принято. Связанная дыра — W-6
G-2 Шаг 7: «цель отсутствует/null, а хотя бы один член непуст — жёсткое нарушение» closed, new issue W-1: симметричный случай (цель есть, все члены пусты) не считается — три фикстуры «только актив» станут done. O-2: остаточный риск для строк вне §7
G-3 То же, что V-3: values[].text в контракте v2.0, source_text в DBML, string_value в прежнем смысле closed Семантика string_value теперь одна (шаг 4, вводная)
G-4 Шаг 2 п. 3: словарь периодов объединяет statement_periods.yaml и шаблоны dates.py; test_parses_real_forms — обязательные тесты closed —
G-5 То же, что V-1 closed —
G-6 То же, что V-6 closed —
G-7 Шаг 8 п. 3 и «Что уходит из ядра»: RECOGNIZED ⇔ конверт done ∧ document.status = done closed O-1: document.status = done избыточен, так как конверт done уже означает «все документы done». Прежнее поведение сохранено
G-8 Шаг 1 п. 6: слабое продолжение присоединяется, только если коды в диапазоне type_hint; иначе отдельный кандидат + error; шаг 2 п. 2 partial W-4 (наследование); W-10 (отдельный кандидат: «не документ info» против error)
G-9 Решение № 16: ref OCR = row:<row_index>[#<k>], Excel = <лист>!<A1>, уникален в документе closed Мелочь, W-15: для Cell.ref у OCR позиция без столбца не уникальна на уровне ячейки
G-10 Р-13: 409 LINES_UNRESOLVED при unreadable / missing с непустым amount; detail не попадает в ЕХ closed —
G-11 DBML v5.19: line.needs_review, recognition_job.statements_version closed Маркера G-11 в тексте нет, но содержание внесено
G-12 PATCH values[].confirmed: needs_review = false, is_modified не ставится, аудит closed, new issue W-2: флаг подтверждения нигде не хранится, следующий пересчёт его теряет
G-13 PATCH columns: [{column_id, period_date}]: снимает error «колонка без даты», пересчитывает is_current; report_date: null обнуляет относительные closed W-16: исключение из правила F-8 «error сохраняются» нужно оформить явным списком
G-14 Р-1: пакет владеет своими типами, хост конвертирует; golden пакета на синтетике; sync-statements --check в make check-full closed —
G-15 Р-14: раскладка по коду остаётся; строки со статусами — под ближайшей предыдущей размещённой; detail скрыты closed W-15: не определено место missing-строки, у которой нет предыдущей размещённой (aep_3 ОФР, «Выручка» — первая строка таблицы)
G-16 Шаг 4 п. 1: parse_amount(text, source), «−» как знак, тонкие пробелы, запятая по источнику closed, new issue W-14: квантование тихо округляет OCR-мусор вроде «12,3456»; правило точки-тысяч (sg#37) не названо
G-17 Шаг 6 п. 1: form_variant = simplified (ОКУД 0710096 или структура), свой набор соотношений и перечень closed, new issue W-5: в statement_headers.yaml ОКУД 0710096 уже мапится на DISCLOSURE
G-18 Принцип 5: ИНН — только цифры; шаг 2 п. 1: порядок «пробелы и переносы → двойники → проверка», | → 1 только если результат попадает в диапазон closed —
G-19 Шаг 6 п. 1: DISCLOSURE по type_hint — без шагов 2–7; по кодам — шаги 2–5 и отбрасывание строк closed, new issue W-9: диапазоны «1xxx / 2xxx / 3xxx–6xxx» шире словарных
G-20 Шаг 8 п. 2: уровни; «организация не найдена при найденном ИНН» — info, «ни того ни другого» — error closed —
G-21 Шаг 3 п. 2: разлепление, только если хотя бы одна часть — код closed При известном type_hint «44 30» (=4430) вне диапазона ББ, не множится. При неизвестном 4430 станет кодом из-за расширения диапазонов (W-9)
G-22 Шаг 2 п. 1: «краевая пунктуация» («1220 ,» → 1220) closed —
G-23 PATCH: report_date: null обнуляет period_end относительных колонок closed —
G-24 Р-15 closed —
G-25 line_refs — список; document_issue: список line_id, очистка при перезагрузке, core-записи заменяются closed, new issue W-3: устаревшие нарушения источника service после PATCH
G-26 Принцип 3: ядро добавляет только info по словарю имён и перечню closed —
G-27 Утверждение «в трёх из четырёх» в тексте дизайна отсутствует closed Маркера нет, исправлять нечего
G-28 Шаг 2 п. 1: словарь подписей расширен («Код строки») closed —
G-29 Шаг 2 п. 7: одно правило столбца наименования closed В запасной ветке («ближайшая текстовая левее кода») пропало «кроме Пояснения»; для столбцов левее кода из 66н не критично
G-30 PATCH: аудит RECOGNIZED_DATA_EDIT с before / after по коду, статусу, родителю; назначенный код снимает needs_review closed —

2. Разбор незакрытых проблем и новых противоречий

W-1 (important). «Посчитано, если хотя бы один член непуст» (F-2) + G-2 теряют детектор односторонней формы

Где: шаг 7, пункты «цель отсутствует» и «соотношение считается посчитанным».

В G-2 «член» — это правая часть соотношения, отдельно от цели. Значит, цель есть, а все члены пусты — «не проверяется».

Сценарий: «ББ. Лист 1» с одним активом. 1600 есть, 1700 нет.

  • 1600 = 1700: цель 1600 непуста, член 1700 отсутствует, соотношение не считается.
  • 1700 = 1300 + 1400 + 1500: цели нет, членов нет, тоже пропуск.
  • Остальные соотношения сходятся, org/ИНН и период есть, строки ok, значения чистые — получается done.

Сегодня эти файлы needs_check, потому что отсутствующий член считается нулём (tabular-parser/api/src/parser/statements/checks.py ~122; arm-core/api/src/domain/statement_checks.py::run_checks). Манифест tabular-parser/api/tests/statements_manifest.yaml: строки 11, 24, 32 — три файла (unagrande-co 2024-FY, unagrande-group 2024-FY и 3кв.25), failed_relations: ["1600 = 1700"]. Половина баланса автоподтвердилась бы.

Предложение: соотношение посчитано, если непуста цель или хотя бы один член; пропуск только когда непусто ничего. Тогда правило «все null — ничего не подтверждает» (F-2) сохраняется, а G-2 становится его зеркалом. Обновить golden и манифест.

W-2 (important). confirmed (G-12) не хранится, пересчёт его сбросит

Где: PATCH values[].confirmed, «Подтверждение проверкой», список DBML v5.19.

Для правки есть is_modified, для акцепта есть checks_accepted_*. Для confirmed в v5.19 колонки нет: ни флага, ни confirmed_by/at, только audit_event. Пересчёт общим кодом «после PATCH» применяется ко всем значениям.

Сценарий: ячейка с уверенностью 71, верная, в строке вне §7. Аналитик подтвердил её, needs_review = false. Любой следующий PATCH другой ячейки пересчитывает её обратно: «нуждается в покрытии и не покрыто» даёт true. Правило G-12 не работает дольше одного PATCH.

Предложение: добавить в financial_statement_value признак (review_confirmed_by/at или is_confirmed) и вход «подтверждено» в общую функцию needs_review. Альтернатива: пересчёт монотонен для подтверждённых, но тогда сломанное соотношение должно возвращать true.

W-3 (medium). document_issue: устаревшие нарушения источника service

Где: «Что уходит из ядра», DBML v5.19.

Сказано только, что записи источника core заменяются при каждом пересчёте. Нарушения §7, пришедшие от сервиса (kind relation/soft, source = service), после правки значения остаются и показываются как действующие. Рядом появляются новые core-записи — дубли и устаревшее.

Предложение: при пересчёте после PATCH заменять все записи relation / soft независимо от источника. Предупреждения warning источника service сохранять, кроме явно снимаемых (W-16).

W-4 (important). V-4 / G-8: «наследование столбца кодов» не работает на геометрии НПЗ

Где: шаг 2 п. 2, решение № 3.

У НПЗ столбец кодов на каждом листе свой. Проверено openpyxl, номера столбцов с 1:

  • ББ: листы 1–5 — 13, 7, 7, 7, 12.
  • ОФР: лист 1 — 10, лист _2 — 6, лист _3 — 12.
  • Лист _2 (единственная строка 2400) шириной 9 столбцов.

Если «наследуется» значит «тот же индекс, что в предыдущем сегменте» (столбец 10), то в _2 его просто нет. Получается ошибка сегмента и документ needs_check вместо done. То же с st07_single_code_continuation (там индексы совпадают, но принцип тот же). Прежний relaxed (statement.py _continuation, min_codes_continuation: 1 в statement_headers.yaml) ищет столбец по содержимому, а не наследует индекс.

Предложение: заменить формулировку на ослабленный поиск для сегмента не первого, без шапки, с кодами < 3: столбец с наибольшим числом кодов из диапазона формы, минимум 1. Оставить предупреждение error, если и так не найден. Сам индекс предыдущего сегмента не использовать. Сохранить проверку диапазона type_hint (_weak_blocker).

W-5 (medium). ОКУД 0710096 как признак упрощённой формы конфликтует со словарём типов

Где: шаг 6 п. 1 (G-17), evidence.

В tabular-parser/api/rules/statement_headers.yaml (okud: DISCLOSURE: '\b07100(0(3|…|9)|(1|…|9)\d)\b') значение 0710096 попадает в DISCLOSURE. По шагу 1 п. 4 type_hint = DISCLOSURE, коды строк 1xxx → по шагу 6 это «противоречие» → тип по кодам + error → needs_check. Пока словарь не правится, упрощённая форма с этим ОКУД никогда не будет done, как бы ни работал вариант G-17.

В фикстурах 0710096 нет вообще (только в admin/source/СУЖЦ…xls). Утверждение, что упрощённые ББ/ОФР несут именно этот ОКУД, ничем не подтверждено. По памяти упрощённые бланки 66н идут с обычными 0710001/0710002, а 0710096 относится к комплекту отчётности. Это надо подтвердить на образце или у владельца; сам я не верифицировал.

Предложение: либо убрать ОКУД из признаков упрощённой формы (оставить структурный: есть 1600, нет 1100 и 1200), либо явно исключить 0710096 из DISCLOSURE в словаре. Образец упрощённой формы — в фикстуры.

W-6 (medium). Р-11 vs Р-12: код, исправленный из двойников с уверенностью 100, не проходит ни одной проверки

Где: принцип 5 / Р-11 и «Код и наименование строки» / Р-12.

Р-11 велит заменять двойники и в кодах, а для суммы вводит is_recognized = false, чтобы её проверил §7. У кода такого флага нет. Структурное подтверждение применяется только при line_code_confidence < 100. Код «I140» или «1l50», прочитанный ИС с уверенностью 100, получает ok без needs_review. §7 перепутанные соседи не ловит (сумма раздела та же), поэтому запись сохраняется без проверки.

Предложение: считать код после замены двойника «неуверенным» (эффективная уверенность < 100, или признак code_corrected) и пропускать через структурную проверку. Это одна строка в «Код и наименование строки».

W-7 (minor). Внутреннее противоречие в «Подтверждении проверкой»

Первый пункт: чистые значения «ни одним правилом ниже не подсвечиваются (V-13)». Пятый пункт: true «у всех ячеек жёсткого соотношения … независимо от уверенности». Второе включает чистые Excel-значения, поэтому первое надо сузить: «ни правилом покрытия, ни правилом знака».

W-8 (minor). Устаревший text правленой ячейки в голосовании за конвенцию знака

Конвенция — форма записи у большинства членов abs_terms с is_recognized = true, считается от сохранённого source_text. Если аналитик исправил знак у члена, source_text прежний, а amount новый, и ячейка голосует старой формой. Что делает PATCH с is_recognized, не сказано.

Предложение: исключить из голосования ячейки с is_modified (или считать форму от знака amount).

W-9 (medium). G-19: диапазоны кодов шире словарных

Где: шаг 2 п. 1, шаг 6 п. 1.

Словарь парсера: 1100–1799, 2100–2999, 5000–5999 (tabular-parser/api/rules/statement_headers.yaml, line_codes.ranges). В дизайне «1xxx — ББ, 2xxx — ОФР, 3xxx–6xxx — прочие», при неизвестном type_hint объединение.

Два риска:

  • Годы в шапках (2024, 2025) попадают в 2xxx. В НПЗ ОФР в столбце 16 стоит «2025».
  • Значения в тысячах от 3000 до 6999 считаются кодами, а столбец сумм начинает конкурировать со столбцом кодов за «наибольшее число» (кодов ≥ 3, доля ≥ 0,6 — слабая защита). Для разлепления G-21 «44 30» (=4430) тоже становится кодом, если type_hint неизвестен.

Предложение: в дизайне явно записать числовые диапазоны (оставить 1100–1799, 2100–2999, расширить DISCLOSURE до 3000–6999 только при неизвестном type_hint и тестами) вместо «1xxx / 2xxx».

W-10 (minor/medium). Одинокий лист-продолжение: «не документ, info» против error

Где: шаг 6 п. 1 и шаг 8 п. 2.

Лист «Справка» с кодом 1370 после ОФР не присоединяется (шаг 1 п. 6) и становится отдельным кандидатом. У него кодов < 3 и type_hint нет. Шаг 6 п. 1 говорит «не документ, info», шаг 8 п. 2 — «сегмент с содержимым без колонки кодов (в том числе потерянный лист-продолжение) — error». Данные листа теряются, done у соседних документов возможен: нарушается принцип 2.

Предложение: в шаге 6 п. 1 добавить исключение — лист с числами и не присоединившийся продолжением даёт файловое error (и строки сохраняются как unreadable / detail, если возможно).

W-11 (minor). Остатки V-12 / G-30

  • После назначения кода (unreadable / missing / detail → ok) не сказано, чему равна line_code_confidence (100?). Сейчас остаётся прежняя, возможно 25.
  • У строки есть needs_review (структура кода), но нет lines[].confirmed. Код с уверенностью 50, стоящий не по порядку, но верный, снимается только повторным назначением того же кода, которое в тексте как «другой код» не описано (аналог G-12 для строк).

W-12 (minor). Правило родителя пятизначного кода

В «Код и наименование строки»: «ближайшая строка выше с теми же четырьмя первыми цифрами, иначе код из первых четырёх цифр — правило subline_parents». В реальном коде (arm-core/api/src/domain/statement_checks.py::subline_parents) ближайшая четырёхзначная строка выше с теми же тремя цифрами, иначе код из первых четырёх. Первая половина фразы при буквальном чтении матчится на пятизначного соседа (11101 выше 11102). Привести к тексту кода.

W-13 (minor). Границы алгоритма подтверждения структурой

  • Наибольшая подпоследовательность не единственна. Для двух поменявшихся соседей (1230, 1220) подтверждается один из двух, и не определено, какой остаётся без подтверждения. Если выпавший код имеет уверенность 100, он освобождён от проверки, а второй числится «подтверждённым».
  • «Хотя бы в одной из проверяемых редакций»: берётся объединение подпоследовательностей двух редакций или одна общая? Правка: выбирать редакцию с наибольшей подпоследовательностью.
  • Родитель с уверенностью 100 (освобождён) для пятизначного дочернего кода считается «подтверждённым» или нет? Записать.

W-14 (minor). OCR: тихое округление и правило точки

Решение № 13 («2 знака, half-up») в общем parse_amount для OCR-текста. Сегодня шлюз отвергает такие значения: scan-gateway/api/src/normalizer/ocr/document.py:~143-150, решение № 30: «0,125», «12.3456» — артефакт, amount = null, текст в string_value. По дизайну «12,3456» станет 12,35, причём is_recognized не оговорён. Оставить квантование только для числового value (Excel), а для OCR-текста сохранить null + string_value.

Правило точки-тысяч (837.994, 1234.567 — sg#37, amounts.py _DOT_THOUSANDS_RE) в шаге 4 п. 1 не названо. Указать, что оно сохраняется для ocr и как ведёт себя для excel.

W-15 (minor). Экран и ref

  • Р-14: у missing-строки без предыдущей размещённой (aep_3 ОФР, «Выручка» — первая строка таблицы) место не задано. Предложение: в начало первого раздела.
  • Cell.ref у OCR — row:<row_index> без столбца: уникально для строки, но не для ячейки. В шаге 1 сказано «уникальная в документе», уточнить, что это RawLine.ref.

W-16 (minor). Исключения из «error сохраняются после правки» (F-8)

PATCH columns снимает error «колонка без даты» (G-13). Остальные error сервиса (несовпадение колонок сегмента, «строка заголовка не найдена», тип по кодам и др.) сохраняются. Записать явный перечень «какая операция PATCH какое error снимает», иначе реализация разойдётся.

Наблюдения (не противоречия)

  • O-1. Вторая часть RECOGNIZED («document.status = done») избыточна при требовании «конверт done»: конверт done означает «все документы done». Один плохой документ блокирует автоподтверждение всех остальных в файле. Так же было и раньше (job_done в statement_status). Если нужна независимость документов, это отдельное решение.
  • O-2. Строки вне §7 с утерянным кодом (справочные 25xx / 29xx и подобные) уходят в detail без нарушения. Раньше это был счётчик, но любое предупреждение шлюза давало needs_check. Теперь такая строка скрыта, а документ может стать done и автоподтвердиться. Принято Р-8, но шапка Р-13 ограничена unreadable / missing, а не detail.
  • O-3. В критерии done нет уверенности полей шапки. Шлюз требовал 100 для ИНН и организации. Остаётся сверка ядра (MISMATCH). Намеренно ли это ослабление, в дизайне не сказано.

3. Согласованность правок между собой

Проверил и признал согласованным:

  • Р-12, G-1, V-2 и перечень V-11. Перечень (superset словаря ядра) — в общем коде, имена — только в ядре, --check в одну сторону (словарь ⊂ перечень) достаточен. Пятизначные коды, 1105, 1530, 2420, 2460, 2930 покрыты; Excel и чистые OCR-коды дают info. Оговорки: W-6 (двойники), W-12 и W-13.
  • G-2 + Р-8 + G-17. Для «БАЛАНС» без кода: detail → цель 1600 отсутствует → жёсткое нарушение, подсвечивается родитель. Для сегмента «Лист 2» цель 1600 отсутствует, член 1700 непуст → нарушение, документ needs_check (как в golden). Упрощённая форма использует свой набор. Оговорки: W-1 (симметричный случай), W-5 (ОКУД), O-2.
  • G-3 и V-3. text и string_value с разными ролями; source_text в DBML; контракт и DBML соответствуют. Оговорка: W-8.
  • V-1 и G-5 «из двух строк». Для двухстрочного документа 2/2 = 1,0, титульный столбец отсекается: проверено на Унагранде (доли 0,11, 0,12, 0,04).
  • Уровни V-6 / G-6 / G-20 и критерий done. Список error и info согласован между п. 1 и п. 2 шага 8 и решением № 1. НПЗ ОФР без ИНН — done.
  • Статус конверта в RECOGNIZED (G-7). Согласовано с шагом 8 п. 3. Замечание — O-1.
  • PATCH lines / columns / confirmed и аудит. Один PATCH — один пересчёт, аудит RECOGNIZED_DATA_EDIT с before / after. Оговорки: W-2, W-11, W-16.
  • DBML v5.19 и контракт v2.0. Проверил соответствие:
    • line_status, line_code_text, row_index, ref → source_ref, parent_ref → parent_row_index, needs_review строки и значения;
    • values[].text → source_text, columns[].period_kind, form_variant в meta;
    • warnings[] и checks → document_issue, statements_version → recognition_job;
    • снятие уникальности document и (meta_id, line_code) плюс порядковый номер формы. Не хватает только флага подтверждения (W-2).
  • Раскладка экрана (Р-14). Совместима с Р-4 (дубли кодов в порядке файла), шаблонными строками и detail под родителем. Оговорка — W-15.
  • Р-15 и автозамена. Автозамена между файлами (§12 п. 3) убирает дубли ББ/ОФР между файлами, поэтому Р-15 действует только внутри файла.

4. Что смотрел

Дизайн и ревью (.wt/normalization/docs/openspec/changes/unified-statement-normalization/):

  • design.md целиком, включая «Целевую картину», «Решения владельца Р-1…Р-15» и «Decisions made without discussion» 1–19;
  • review-2026-10-17-closure.md (V-1…V-13) и review-2026-10-17-blind.md (G-1…G-30) целиком.

Правила и модель:

  • docs/context/05-domain-rules.md §12 п. 3 (автозамена), строки 680–695;
  • docs/model/data-model-v5.18.dbml: document (757–780), recognition_job, financial_statement_meta/column/line/value.

tabular-parser:

  • api/rules/statement_checks.yaml, statement_headers.yaml (okud, line_codes);
  • api/src/parser/statements/checks.py (run_statement_checks);
  • api/tests/statements_manifest.yaml;
  • CLAUDE.md.

scan-gateway: api/src/normalizer/ocr/amounts.py (parse_amount), document.py (_value).

arm-core:

  • api/src/domain/statement_checks.py (subline_parents, _subline_rules, run_checks);
  • api/src/services/statements.py (PATCH-цикл, пропуск несуществующей ячейки);
  • api/rules/statement_lines.yaml.

Фикстуры:

  • fixtures/golden/expected_statement_parse/**: Б.Б. Лист 2, ОФР. Лист 2, st05 — статусы и предупреждения;
  • fixtures/ocr/expected_normalize/*.json и golden BALANCE/INCOME — коды вне словаря (найдены 1105, 1231, 2930);
  • fixtures/statements-xlsx/npz/2025-Q3/*.xlsx — столбцы кодов по листам;
  • fixtures/statements-xlsx/mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx — повторная шапка и title;
  • fixtures/statements-xlsx/synthetic/*.xlsx;
  • unagrande-co/2024-FY/Б.Б Лист 1.xls, unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls, ББ. Лист 1 3кв.25.xls — подписи «Код» / «Коды» и доли кодов.

Скрипты — в scratchpad (npz.py, g2.py, ux.py, xl.py, codes.py). Не смотрел: arm-web; services/documents.py и auto_confirm.py — для Р-13 и Р-15 опирался на текст дизайна, а не на код.