Спеки применены: 5 новых capability (arm-core, arm-web, scan-gateway, statement-normalization, tabular-recognition-service) с Purpose, изменения quality-checks и recognition-contract. Ссылки в 05-domain-rules, DBML v5.19, entities, dev/checks.md и dev/qa-statements-v2.md ведут в архив. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
48 KiB
Повторное ревью design.md — проверка закрытия F-1…F-25 (17.10.2026, второй раунд)
Ревьюер: отдельный агент (модель Fable), без права правки; получил обновлённый design.md (после коммита 63f134b) и
review-2026-10-17.md. Задание: по каждой находке F-1…F-25 найти место закрытия, прогнать сценарий через новые
правила, оценить статус; отдельно — согласованность правок между собой. Маркеры новых проблем — V-n.
Отчёт агента ниже без сокращений.
Повторное ревью design.md (unified-statement-normalization) — закрытие F-1…F-25
Ревьюер: независимый агент, без права правки. Проверено: design.md целиком (763 строки), review-2026-10-17.md, §7/§11/§11.1/§12 05-domain-rules.md, контракт v1.2, DBML v5.18, код трёх сервисов (через codegraph + Read), фикстуры OCR (4 успешных json, golden-эталоны) и Excel (НПЗ, Унагранде ×2, Межениновская, синтетика) скриптами python3 -I / venv парсера. Решения Р-8…Р-11 не пересматривались.
1. Таблица по находкам
| F-n | Где закрыто | Статус | Что осталось / новая проблема |
|---|---|---|---|
| F-1 | Шаг 7 (abs_terms «подтверждает только величину»), «Подтверждение проверкой» (знак по конвенции колонки), шаг 4 п. 1 (}/] — закрывающая скобка), Р-7, решение № 10 |
closed, new issue | Правило верно на данных (aep_3 ОФР: все члены abs в скобках, (7744536} подтверждается, {38378) — подсвечивается). Но «форма записи знака» не доезжает ни до контракта, ни до БД — ядро не может пересчитать конвенцию после PATCH (V-3); область применения правила не ограничена сомнительными ячейками (V-13) |
| F-2 | Шаг 7 («посчитано, если хотя бы один член непуст»), «Подтверждение проверкой» (покрыто ⇒ amount не null), Р-7 |
closed | Сценарий «колонка залита полосой» даёт needs_review = true у всех мусорных ячеек; сценарий «1600 мусор, 1100/1200 читаемы» — нарушение, все три подсвечены |
| F-3 | Шаг 3 (статус detail, родитель — ближайшая строка с кодом выше в сегменте), Р-8, решение № 9, «Что уходит из ядра» (экран скрывает detail) |
closed, new issue | По фикстурам все 90 строк без кода с суммами в 8 OCR-таблицах — detail, единственная missing — «Выручка» aep_3 (код 2110 в колонке значений, верно). У Excel НПЗ/Унагранде missing нет. Но роль строк титульного блока над заголовком не определена — у Межениновской «Дата 30 | 9 | 2025» станет missing (V-8) |
| F-4 | Принцип 2, шаг 3 (row_index, ref), шаг 8 п. 5, DBML v5.19 (row_index, source_ref, parent_row_index), «Экран показывает строки как в файле» |
closed | — |
| F-5 | Шаг 2 п. 1 (а) «подпись принимается только с кодом под ней, последняя над первой строкой с кодом», п. 4 (обе ветки парсера сохранены), решение № 3 | partial | Защита «хотя бы один код под подписью» пробивается ОКОПФ 12267 (АО) в титульном блоке 1С: Унагранде ББ Лист 1 и Унагранде Групп ОФР 3кв.25 — под «Коды» стоит 12267 (1226 ∈ [1100, 1799]); правила выбора между двумя столбцами-кандидатами нет (V-1, critical). Ветка «заголовок по периодам» перенесена — верно |
| F-6 | Шаг 2 п. 3 (эффективная дата), шаг 8 п. 1 («дата отчёта или самая поздняя абсолютная дата колонок»), п. 2 (info «дата отчёта не разобрана при наличии дат колонок»), решение № 12 |
closed | Совпадает с statement_period ядра (statement_mapping.py:73-80) |
| F-7 | Шаг 9 (v2.0, порядок «сначала ядро», два маппера), Р-9 | closed | Паттерн ^1\.\d+$ в contract/common.py:33 поднимается до ^(1|2)\.\d+$; запрос recognition-request остаётся 1.x (шаг 0) — не затронут. В спеке нужно задать, чем заполняются новые поля при приёме 1.x (line_status = ok, row_index по порядку, needs_review — старое правило) — не противоречие, а пробел спеки |
| F-8 | Шаг 8 п. 1 (одна функция done), «Подтверждение проверкой» (is_modified ⇒ false, акцепт, error сохраняются), DBML v5.19 (line_status, line_code_text, row_index, value.needs_review, document_issue), решение № 11 |
partial | Пп. 2–4 закрыты. П. 1 закрыт для значений, но needs_review строки (F-9) в DBML v5.19 не добавлен, и правило «назначение кода снимает needs_review строки» не записано (V-5). Пересчёт конвенции знака после PATCH невозможен без формы записи (V-3) |
| F-9 | «Код и наименование строки» (упорядоченный перечень по редакциям, порядок бланка, редакция по эффективной дате / обе, равные соседние допустимы), шаг 5 п. 2, Р-5 | closed, new issue | Итоги после слагаемых — верно. Но пятизначные подстрочные коды (11101, 12101, 23201 — все выгрузки 1С: НПЗ, Унагранде) ни в какой перечень бланка не входят → needs_review у каждой такой строки → НПЗ ББ/ОФР и Унагранде ОФР из done уходят в needs_check (V-2, critical). Алгоритм «положение относительно соседних подтверждённых» циклически определён. Порядок/состав редакций теперь в двух местах (V-11) |
| F-10 | Шаг 2 п. 3 (period_kind = relative:<вид>, period_end = null), «Что уходит из ядра» (PATCH даты → ядро досчитывает общим кодом), Р-6, DBML column.period_kind |
closed | Согласовано с F-6: эффективная дата из колонок даёт относительным датам опору и без правки |
| F-11 | «Что уходит из ядра» (снять уникальность document, порядковый номер формы), Р-10 |
closed, new issue | Потеря устранена, но не сказано, что делать с двумя документами одного типа и периода у одного ЮЛ (чек-лист, консолидация, _pair_document), и как быть с половинками формы (OCR: «одна таблица = одна форма, склейки нет») — склейки внутри файла нет (V-7) |
| F-12 | Шаг 8 п. 2 (таблица уровней), решение № 1 | closed, new issue | Все блокираторы из ревью перечислены (json набора, BACKEND_PARSING_ERROR, лист без кодов, сегменты, колонка без даты, заголовок, тип), xlsb/дробные решены. Но «ИНН не распознан» → error противоречит критерию done «(организация или ИНН)» и ломает НПЗ ОФР без ИНН (inn_optional_for: [INCOME], сегодня done); «формат не совпал с file_type» → error противоречит собственному принципу решения № 1 (V-6) |
| F-13 | Шаг 7 («не сошлось» = вне допуска ±1 и вне округления 0,001 %; округление и мягкие → soft, info), «Подтверждение проверкой» (покрыто в пределах допуска или округления; мягкие подсветки не дают), шаг 8 п. 1 |
closed | НПЗ (1600 = 1700 на 1,94 при ~17 млрд) и Унагранде (мягкие 2400/2410) остаются done по этому пункту |
| F-14 | «Назначение кода строке» (PATCH …/statement lines: [{line_id, line_code | null, line_status}], аудит, пересчёт, missing → detail, detail → ok), экран: поле кода у unreadable/missing/detail |
closed | Мелкие пробелы спеки: переход ok → detail, недопустимая пара line_code: null + line_status: ok, parent_row_index при смене статуса (V-12, minor) |
| F-15 | «Что уходит из ядра» (одна функция «агрегат формы по коду», перечислены _line_amount, statement_amounts, subline_parents/parent_code, formLayout, _resolve_cells, metrics.fields_*), Р-4 |
closed | Все точки из ревью названы. Уточнить формулировку: сумма — по строкам ok (шаг 7 говорит это, «Что уходит из ядра» — «сумма значений по колонке» без оговорки) |
| F-16 | Шаг 1 (segment_breaks, «граница листа — всегда граница сегмента»), шаг 2 п. 2 (своя раскладка, по числам без шапки, сопоставление по порядку, число не совпало → error), п. 6 шага 1 |
closed, new issue | Сегмент-продолжение без шапки и с 1–2 кодами (НПЗ ОФР лист _2 — единственная строка 2400, синтетика st07) по правилу п. 1 (б) «кодов ≥ 3» столбца кодов не получит; решение № 3 снимает relaxed, ссылаясь на склейку в шаге 1, но склейка не помогает — раскладка у сегмента своя (V-4) |
| F-17 | Р-1 (пакет arm_statements с тестами и конфигом линтеров, импорт по имени пакета, service.statements_version, sync-statements --check в docs, словари со своим загрузчиком), шаг 9, «Что уходит из ядра» (версия в recognition_job, расхождение — error) |
closed | Все 5 пунктов закрыты. recognition_job не имеет колонки под statements_version (есть только service_version), в список DBML v5.19 не внесена (V-10, minor) |
| F-18 | «Что уходит из ядра» (RECOGNIZED ⇔ done ∧ ИНН ≠ MISMATCH ∧ период — конец квартала), шаг 8 п. 1 («квартал — знание ядра») |
closed | — |
| F-19 | Принцип 5, Р-11 (двойники в суммах и кодах ⇒ is_recognized = false, далее §7 с оговорками F-1/F-2), шаг 4 п. 2 («замена двойника — исправление») |
closed | Решение владельца, зафиксировано; §11.1 п. 1 помечен к отмене. Данные: в 4 json двойников в суммах/кодах нет ((Ю4) в aep_6 — «Ю» не в списке двойников, остаётся грязной) |
| F-20 | Шаг 4 п. 1 (квантование half-up, 16 целых цифр, дата/логическое → null + string_value, догадка > 16 цифр → null), решения № 6, № 13 |
closed | — |
| F-21 | Шаг 2 п. 1 («код — текст без пробелов и переносов»), шаг 3 п. 1 («1 100» → 1100) | closed | Порядок с разлеплением (F-25) не оговорён: «переносы убираются» в п. 1 против «N частей через перевод строки» в п. 2 (V-9, minor) |
| F-22 | Шаг 6 п. 1 (5xxx и прочие → DISCLOSURE; без кодов — type_hint; DISCLOSURE без шагов 2–7), решение № 2 |
closed | Форма 3/4 (3xxx/4xxx не в диапазонах, label не в словаре) → «не документ, info» — как сегодня (golden aep_8_form4 = error по ответу ИС); согласовано |
| F-23 | Шаг 1 (locator file:…), шаг 6 п. 3, шаг 8 п. 3 (RECOGNITION_FAILED / INVALID_DOCUMENT — различие записано), решение № 5 |
closed | — |
| F-24 | Шаг 2 п. 1 («новое поведение для обоих: шлюз сегодня пропускает со счётчиком, кодом считает любое целое») | closed | — |
| F-25 | Шаг 3 п. 2 (разлепление и при нечитаемой части → unreadable; число частей не совпало → одна строка, многострочное значение → null + string_value) |
closed | См. V-9 о порядке операций |
Итого: closed — 15, partial — 2 (F-5, F-8), closed с новой проблемой — 8 (F-1, F-3, F-9, F-11, F-12, F-16 + мелкие у F-14, F-17, F-21).
2. Разбор незакрытых и новых проблем
V-1 — critical — F-5: защита «под подписью есть код» пробивается ОКОПФ в титульном блоке 1С; тай-брейка между столбцами-кандидатами нет
Где: шаг 2 п. 1 (а), решение № 3.
Что проверил. Скриптом по xls/xlsx прошёл все ячейки ^код(ы| строки)?$ и содержимое столбца под ними:
unagrande-co/2024-FY/Б.Б Лист 1.xls: «Коды» r6c34 → под ней0710001, 31, 47888105, 3249004697, 10.51.3, **12267**, 384, 7716021332, 1037739271701; «Код» r28c22 →1110, 11101, 11102, ….unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls: «Коды» r3c18 →0710002, 30, 69583357, 7728757141, **12267**, 384, а дальше в том же столбце значения таблицы (За Январь - Сентябрь 2024 г.,287017990.57,-302669265.62…) — титульный столбец совпал с колонкой сумм.12267(ОКОПФ «АО») по правилу дизайна — код: 4–5 цифр, первые четыре1226 ∈ [1100, 1799](statement_headers.yaml:86;columns.py:63-71line_code). Для ООО (ОКОПФ 12300) то же. У НПЗ и Межениновской ОКОПФ в этом столбце нет — там защита работает. Сценарий. Два столбца удовлетворяют (а). Правило «последняя такая подпись над первой строкой с кодом»: если «первая строка с кодом» — глобально по сетке, это r13 (ОКОПФ) → единственная подпись над ней «Коды» r6 → столбец c34 → каждая строка формы становитсяunreadable/missing, у Унагранде Групп ОФР столбец сумм объявляется кодами. Если «первая строка с кодом» — по столбцу, кандидатов два, а правила выбора в дизайне нет; реализация «первый найденный сверху/слева» выберет титульный блок. Сегодня парсер защищён порядком: сначала столбец по подсчёту (find_code_column,columns.py:74-95: кодов ≥ 3, доля ≥ 0,6), потом подпись только в этом столбце (_find_header_row,:127-146). Как закрыть. Требовать под подписью не «хотя бы один код», а долю кодов среди непустых ≥min_share(под «Коды» r6c34 — 1 из 9 = 0,11, под «Код» r28c22 — ~1,0; «документ из двух строк» сохраняется: 2 из 2); при нескольких кандидатах — столбец с наибольшим числом кодов. Либо вернуть порядок парсера (подсчёт → подпись в найденном столбце → заголовок по периодам), а исключение для малых таблиц делать явной веткой. Любой вариант зафиксировать в решении № 3.
V-2 — critical — F-9 / Р-5: пятизначные подстроки не входят ни в один перечень бланка → каждая выгрузка 1С с детализацией теряет done
Где: «Код и наименование строки», шаг 5 п. 2, Р-5.
Что проверил. Пятизначные коды в фикстурах: НПЗ ББ — 11101…11103, 11501…11509, 11901, 12101…12106, 12201…, 12501…, 13401…, 15201…, НПЗ ОФР — 23201…23203, 23301, 23401…, 25101, 25102; Унагранде ББ — 11101…11507. Это подстроки «в том числе» 1С (§12 п. 5: «пятизначные подстроки отдаются как обычные строки»; §7 «родитель = Σ подстрок», statement_checks.py:141-184 subline_parents/_subline_rules). В словаре ядра statement_lines.yaml (132 строки, редакции 66n/fsbu4) пятизначных кодов нет — и в «упорядоченном перечне кодов по редакциям» их быть не может: они свои у каждой организации.
Сценарий. Правило дизайна: «Код подтверждён, если он есть в перечне и его положение … совпадает с порядком бланка; … иначе needs_review у строки, документ — needs_check». Уверенность не спасает: «Подтверждён — уверенность не важна; иначе needs_review» — оговорки для confidence = 100 нет. НПЗ ББ (43 кода на листе 2, большинство пятизначные), НПЗ ОФР, Унагранде ОФР 3кв.25 — сегодня done (манифест строки 18, 39, 47–48) — по дизайну needs_check; Excel-путь теряет автоподтверждение целиком, у шлюза 1С-подобных сканов тоже.
Второе: «положение относительно соседних подтверждённых кодов» — определение циклическое, алгоритм не задан.
Как закрыть. (а) Пятизначный код подтверждён, если его четырёхзначный родитель по правилу subline_parents (ближайшая строка выше с теми же тремя цифрами, иначе код из первых четырёх цифр) подтверждён и подстрока стоит после него; (б) алгоритм — наибольшая подпоследовательность кодов, согласованная с порядком бланка хотя бы в одной из проверяемых редакций; коды вне её — needs_review; (в) явно спросить владельца: подтверждать ли структурой коды с уверенностью 100 (Excel) — по тексту шага 8 п. 1 «Excel выполняет условия уверенности автоматически» это, видимо, не задумывалось.
V-3 — important — F-1 ↔ F-8: «форма записи знака» не хранится — ядро не может пересчитать конвенцию после PATCH; «сырой текст в подсказке» для распознанных ячеек пуст
Где: «Подтверждение проверкой» (знак), «Что уходит из ядра» (пересчёт needs_review общим кодом после PATCH), шаг 4 п. 1–3, шаг 9.
Что проверил. Контракт v1.2: values[].string_value — ["string","null"] (схема l.199), DBML financial_statement_value.string_value — «как распознано, если не число»; шлюз пишет его только при amount = null (document.py:149-151). Дизайн (шаг 4 п. 3) расширяет это до «догадки» (is_recognized = false), но у ячейки, разобравшейся чисто, string_value остаётся null. Форма записи (скобки / минус / без знака) из amount = −7744536 не восстанавливается.
Сценарий. aep_3 ОФР, колонка c2: 2120 (7744536} 88,9 → покрыто по величине, знак по конвенции (2210 (693052) 100, 2220 (278598) 100 — скобки) → needs_review = false. Аналитик правит любую ячейку документа → PATCH → ядро «пересчитывает checks и needs_review общим кодом»; входа «форма записи» у ядра нет → либо ветка знака пропускается (подсветка знака исчезает молча — то, против чего F-1), либо «сравнивать не с чем» → needs_review = true у всех членов abs_terms → done недостижим правкой. Второе следствие: 2210 (546441) с уверенностью 62,5 в c4 (чистый разбор, is_recognized = true) — «сырой текст и уверенность остаются в подсказке ячейки», но сырого текста в результате нет.
Как закрыть. В контракт v2.0 — values[].text (сырой текст источника всегда, как Cell.text шага 1), в DBML v5.19 — financial_statement_value.source_text; конвенция и подсказка считаются от него. Альтернатива — sign_form: brackets | minus | none + text только у is_recognized = false, но это два поля вместо одного.
V-4 — important — F-16 / решение № 3: сегмент-продолжение без шапки с 1–2 кодами не получает столбца кодов — строка 2400 НПЗ теряется
Где: шаг 2 п. 1 (б) («кодов ≥ 3»), п. 2 («без шапки — столбец кодов по правилу п. 1»), решение № 3 («relaxed … снят, потому что продолжения склеиваются в шаге 1»).
Что проверил. НПЗ, лист «Отчет о финансовых результата_2»: подписи нет, кодов в столбце — один (2400); манифест строка 48: «лист с единственной строкой 2400 присоединён (O-9)», документ done по 3 листам. Сегодня: _continuation → find_layout(relaxed=True) (statement.py:94-105, min_codes_continuation: 1, statement_headers.yaml:92), затем _weak_blocker (:145-160). Синтетика st07_single_code_continuation.xlsx — тот же случай.
Сценарий. По дизайну лист дописывается в сетку в шаге 1, граница листа открывает сегмент, «у сегмента своя раскладка: … без шапки — столбец кодов по правилу п. 1» → (а) подписи нет, (б) кодов 1 < 3 → столбца нет. Что происходит с сегментом без столбца кодов, не сказано; при любом чтении строка 2400 либо теряется (принцип 2 нарушен), либо весь документ получает error «лист без колонки кодов» → needs_check. Склейка в шаге 1 этому не помогает — раскладка всё равно своя.
Как закрыть. Для сегмента не первого: подписи нет → столбец кодов наследуется от предыдущего сегмента, если в нём хотя бы один код (это и есть сегодняшний relaxed + _weak_blocker «коды из диапазона формы»); иначе — правило (б); не найден → error сегмента. Поправить решение № 3.
V-5 — important — F-8 / F-9: needs_review строки негде хранить; снятие его назначением кода не описано
Где: «Код и наименование строки» (StatementLine.needs_review), шаг 8 п. 1 («нет значений и строк needs_review»), DBML v5.19 (только financial_statement_value.needs_review), «Назначение кода строке».
Сценарий. Результат с needs_review = true у строки 1230 (код с уверенностью 50 не на месте) → ядро «при загрузке ничего не пересчитывает, хранит как пришло» — колонки нет → признак теряется; после PATCH функция done получает вход «статусы строк, needs_review» неполным. Аналитик назначает код unreadable-строке (F-14) — line_code_modified = true, но правила «line_code_modified ⇒ needs_review строки = false» нет (для значений аналогичное is_modified ⇒ false записано) → строка подсвечена навсегда.
Как закрыть. DBML v5.19: financial_statement_line.needs_review (как пришло, пересчёт после PATCH); правило line_code_modified = true ⇒ needs_review = false; либо явно: признак строки хранится записью document_issue(kind = warning, line_id) и критерий done читает его оттуда — но тогда так и написать.
V-6 — important — F-12: уровень error для «ИНН не распознан» и «формат не совпал с file_type» противоречит критерию done и принципу решения № 1; регрессия НПЗ ОФР
Где: шаг 8 п. 1 («(организация или ИНН)»), п. 2 (error: «ИНН не распознан», «формат файла не совпал с file_type»), решение № 1 («error — то, что меняет данные или их полноту»).
Сценарий. НПЗ ОФР: в шапке нет ИНН (манифест строка 45 inn_optional_for: [INCOME]), организация есть; сегодня done. По дизайну критерий 1 проходит (организация есть), но предупреждение «ИНН не распознан» уровня error даёт needs_check — два правила одного документа спорят, побеждает error. Формулировка «не распознан» — из шлюза; что сервис выдаёт у Excel при отсутствующем ИНН, не сказано (сегодня парсер об ИНН не предупреждает). «Формат не совпал с file_type»: файл прочитан верно, данные и полнота не изменились — по собственному принципу решения № 1 это info; сегодня — предупреждение без влияния на статус.
Как закрыть. «ИНН не найден/не распознан» → error только если и организации нет (= критерий «организация или ИНН»); при организации — info, сверку с ЮЛ делает ядро (MISMATCH). «Формат не совпал» → info. Таблицу уровней выровнять с принципом решения № 1.
V-7 — important — Р-10 / F-11: два документа одного типа и периода у одного ЮЛ — правила выбора нет; половинки формы внутри файла не склеиваются
Где: «Что уходит из ядра» (DBML document, «привязка к версии консолидации — по периоду, как у ОСВ»), Р-10, шаг 1 п. 6 (OCR: «одна таблица = одна форма, склейки нет — без изменений»).
Сценарий 1. Лист-продолжение с маркером другого типа → отдельный кандидат того же периода; или ИС вернула ББ двумя таблицами (актив/пассив) — у ОСВ второй документ различим по счёту, у ББ — нет. Чек-лист required_document_rule (индекс (legal_entity_id, document_type, osv_account_code, period_year, period_quarter, status)), привязка к консолидации «по периоду», межформенная проверка _pair_document (repositories/statements.py:330-346: подтверждённый, затем последний по created_at — в одном файле created_at одинаков) — все берут «один документ на тип и период». Два подтверждённых ББ одного периода → неопределённость в КФО.
Сценарий 2. Половинки (актив needs_check по 1600 = 1700, пассив needs_check) теперь обе хранятся, но операции «склеить» нет (OQ-140 запрещает только между файлами, внутри файла не описано) — аналитик не может довести файл до done иначе как перезагрузкой.
Как закрыть. Записать: в чек-лист и консолидацию идёт подтверждённый документ; подтверждение второго документа того же типа и периода у ЮЛ — 409 с указанием первого (или автозамена, как между файлами, §12 «Автозамена», 05-domain-rules.md:689); склейка половинок не предоставляется — зафиксировать как известное ограничение или вопрос в 07-open-questions.md.
V-8 — minor — F-3 / шаг 2–3: роль строк над строкой заголовка не определена — титульный блок с числами становится missing
Где: выход шага 2 (TableLayout: роли header | numbering | data), шаг 3 («по строке на каждую строку с ролью data, у которой есть код или хотя бы одно число»).
Что проверил. Межениновская Форма1: над заголовком (r23) строки r6 «Дата (число, месяц, год) | 30 | 9 | 2025» и r15 «подлежит обязательному аудиту | 0 | ДА | 1» — кода нет, числа правее столбца кодов; Форма2 — r6. Сегодня они не данные: find_layout ставит first_data_row = header_last_row + 1 (columns.py:294-310), collect_lines начинается с неё.
Сценарий. Если в TableLayout эти строки — data (третьей роли нет), по шагу 3 они без кода и с числами над первой строкой с кодом → missing → done недостижим у Межениновской (сегодня done, манифест строки 50–56).
Как закрыть. Роль title (или «не строка») для всего, что выше строки заголовка сегмента; при отсутствии заголовка — выше первой строки с кодом; missing — только между заголовком и первой строкой с кодом.
V-9 — minor — F-21 ↔ F-25: порядок «разлепить по переводу строки» и «убрать переносы в коде» не задан
Где: шаг 2 п. 1 и шаг 3 п. 1 («пробелы и переносы убираются») против шага 3 п. 2 («ячейка кода содержит N частей через перевод строки → N строк»).
Сценарий. «1500\n1700» по п. 1 → «15001700» → не код → unreadable; по п. 2 → две строки ok. Записать явно: сначала п. 2 (разлепление), затем п. 1; для поиска столбца (шаг 2) многострочная ячейка считается кодом, если каждая часть — код.
V-10 — minor — F-17: recognition_job.statements_version не в списке DBML v5.19
recognition_job имеет service_name, service_version, result_schema_version, metrics jsonb — отдельной колонки под версию общего кода нет, а «Что уходит из ядра» требует писать её и сравнивать. Добавить statements_version varchar в ту же миграцию (или оговорить хранение в metrics).
V-11 — minor — F-9 ↔ Р-3: состав и порядок кодов по редакциям живут в двух местах
Общий пакет получает «упорядоченный перечень кодов по редакциям», ядро оставляет statement_lines.yaml с теми же редакциями и порядком бланка (editions: 66n l.9, fsbu4 l.72) ради наименований и проверки «код не из редакции». Это та же пара копий, которую дизайн устраняет для §7. Предложение: словарь ядра — только код → наименование (по редакциям), порядок и состав редакций ядро берёт из своей копии пакета arm_statements; sync-statements --check сверяет, что все коды словаря ядра есть в перечне пакета.
V-12 — minor — F-14: пробелы в операции назначения кода
Не описаны: переход ok → detail (ложный код, который аналитик хочет вывести из агрегата, не удаляя строку), недопустимость пары line_code: null + line_status: ok, пересчёт parent_row_index при переходе missing → detail (родитель — ближайшая строка ok выше), что происходит с line_code_confidence после назначения (100?).
V-13 — minor — F-1: область правила знака не ограничена сомнительными ячейками; null-члены не исключены из большинства
Текст: «не совпадает или сравнивать не с чем — needs_review = true независимо от соотношения». При буквальном чтении правило применяется и к чистым ячейкам (confidence = 100, is_recognized = true): Excel-ОФР, где 2120 в формате (#,##0), а 2410 — -#,##0, получит подсветку расходов; OCR-ОФР малой компании, где из членов abs_terms заполнена одна 2120 с уверенностью 100 — «сравнивать не с чем» → needs_review. Решение № 10 про «вперемешку скобками и минусом — это и нужно» относится к сомнительным ячейкам. Записать: знак проверяется только у ячеек, которым нужно покрытие (confidence < 100 или is_recognized = false); в большинство входят члены с непустым amount.
3. Согласованность правок между собой
- F-2 ↔ F-1 ↔ F-13 ↔ F-3/Р-8 ↔ Р-4/F-15. Цепочка «агрегат по коду = Σ строк
ok→ соотношение посчитано, если хотя бы один член непуст → сошлось в пределах допуска или округления → покрывает только ячейки сamount ≠ null→ дляabs_termsтолько величину» внутренне непротиворечива;detail/unreadable/missingисключены из агрегата, подстроки 5xxxx —okи проверяются по AURORA-1527 (мягко, подсветки не дают). Сбоит только хранение формы знака (V-3) и область правила (V-13). - F-6 ↔ F-10 ↔ F-9. Одна «эффективная дата» (решение № 12) питает относительные подписи, редакцию перечня кодов и критерий
done; пересчёт после PATCH даты описан. Согласовано. Не заданperiod_kindу колонки без подписи (шаг 2 п. 4) — предположительноnone, стоит записать. - Статус
done↔ пересчёт после PATCH (F-8). Входы функции названы;is_modified, акцепт, сохранениеerror— есть. Пробелы:needs_reviewстроки (V-5), форма знака (V-3). - F-9 ↔ Р-3. Перечень без наименований Р-3 не нарушает; но дублирует редакции/порядок словаря ядра (V-11) и не покрывает подстроки (V-2).
- Контракт v2.0 (F-7/Р-9) ↔ DBML v5.19 (F-4/F-8/F-11/Р-10). Соответствие полей:
line_status,line_code_text,row_index→row_index,ref→source_ref,parent_ref→parent_row_index,values[].needs_review→value.needs_review,columns[].period_kind→column.period_kind,document.status→document.status,document.warnings[]/checks→document_issue,documentбез уникальности + порядковый номер,crm_id.line_codeуже nullable в DBML v5.18,period_datenullable — миграция по ним не нужна. Не хватает:financial_statement_line.needs_review(V-5),recognition_job.statements_version(V-10), сырого текста значения (V-3).service.statements_versionложится в существующий объектservice {name, version}контракта — аддитивно. - Р-11 ↔ F-1/F-2. «Двойник ⇒
is_recognized = false⇒ нужно покрытие ⇒ покрытие требует посчитанного соотношения иamount ≠ null» — согласовано;}/]как закрывающая скобка: не сказано, считается ли это «исправлением символа» (влияет только на то, нужно ли покрытие ячейке с уверенностью 100 — у(7744536}уверенность 88,9, вопрос не возникает). - Решение № 1 ↔ шаг 8 п. 2. Списки
errorсовпадают, кромеBACKEND_PARSING_ERRORи «формат не совпал» (есть только в п. 2); принцип решения № 1 нарушен двумя пунктами (V-6). - Р-8 ↔ §12 п. 5 / §11.1 п. 4 / строка 688. Дизайн отменяет OQ-36/OQ-141; OQ-149 («восстановление строк по наименованию») не упомянут и остаётся открытым — противоречия нет, но при правке §12 его надо сохранить.
4. Что проверил и признал согласованным
- Р-2: ИС отдаёт ячейку на каждую позицию (по 8 таблицам повторно подтверждено, позиций не хватает 0).
- Статусы строк без кода по OCR: aep_3 ББ
detail2 /unreadable6 /missing0; aep_3 ОФР 1 / 1 / 1 («Выручка»); aep_6 ББ 19 / 2 / 0, ОФР 2 / 3 / 0; «БО 1 кв 2025» ББ 20 / 3 / 0, ОФР 2 / 2 / 0; «Русский бисквит 1 кв.25» ББ 40 / 3 / 0, ОФР 12 / 2 / 0 — цель Р-7 («чистый скан может статьdone») по строкам достижима. - Конвенция знака на данных: aep_3 ОФР c2 — 2120
(7744536}88,9; 2210(693052)100; 2220(278598)100; 2330{38378)71,4; 2350(241654)75; 2410(108450)87,5 → конвенция «скобки», 2120 подтверждается, 2330 подсвечивается — как и описано в дизайне. aep_6 c4: 2220(124)100, 2350(Ю4)75 — «Ю» не двойник, остаётся грязной и подсвечивается. - Диапазоны кодов словаря (BALANCE 1100–1799, INCOME 2100–2999, DISCLOSURE 5000–5999;
min_share 0.6,min_codes 3,min_codes_continuation 1) — шаг 2 п. 1 (б) воспроизводит парсер. - Шаг 6 п. 1 ↔
statement_labels.yamlшлюза (4 подписи: ББ, ОФР ×3) ↔statement_headers.yamlпарсера (ОКУД, «Пояснения» → DISCLOSURE) — слияние словарей не противоречит; форма 4 (aep_8_form4) сегодняerrorот ИС, регрессии нет. - Шаг 9 / Р-9 ↔
contract/common.py(SCHEMA_VERSION_PATTERN,ContractModel extra="ignore", strict=True) — два маппера реализуемы; запрос остаётся 1.x. - F-13 ↔
statement_checks.yaml(жёсткие 1100…1700, 2100/2200/2300; мягкие 2400 ×2, 2410, 2500;abs_terms2120/2210/2220/2330/2350/2410) — формулировки дизайна совпадают со словарём. - F-18 ↔
form_confirmable(services/statements.py:71-81: квартал обязателен) —period_knownв ядре сохранён. - F-6 ↔
statement_period(statement_mapping.py:73-80), F-9 ↔edition_for(:83-87, граница 2024-12-31) — одна формула эффективной даты и редакции. - F-15:
_pair_documentуже имеет порядок выбора (подтверждённый → последний), агрегат по коду его не ломает;line_origin(schemas/statements.py:157-159) поline_code_original_valueпри появленииline_statusможно заменить, но это не противоречие. - Уровень «xlsb без форматов», «дробные при тыс.», «подпись из словаря исключений» =
info— совпадает с сегодняшним поведением парсера (документdone).
Файлы и символы, которые смотрел
docs (worktree .wt/normalization/docs): openspec/changes/unified-statement-normalization/design.md (целиком), review-2026-10-17.md (целиком), context/05-domain-rules.md (строки 505–530 §7, 686–690, 990–999 §11.1 п. 4, 1105–1115 §12 п. 2, 1137–1155 §12 п. 5/7), contracts/recognition-result.schema.json ($comment, service, lines[], values[], checks, string_value), model/data-model-v5.18.dbml (document, recognition_job, financial_statement_meta/column/line/value).
tabular-parser: api/src/parser/statements/columns.py (_CODE, line_code 63–71, find_code_column 74–95, _first_code_row, _find_header_row 127–146, _value_columns 229–238, find_layout 294–310), statement.py (_block_draft хвост, _continuation 94–135, _sheet_codes, _weak_blocker 145–160, _fill), detect.py (is_weak_continuation 131–134, classify_statement_sheet, _Candidate), api/rules/statement_headers.yaml (19–34, 78–96), statement_checks.yaml (6–28), api/tests/statements_manifest.yaml (1–75).
scan-gateway: api/src/normalizer/ocr/amounts.py (parse_amount 44–84, is_blank), rows.py (classify_row 58–71, split_merged_row 92–118, _data_rows), document.py (_value 134–159), api/rules/statement_labels.yaml, api/src/gateway/merge.py (контекст склейки).
arm-core: api/src/domain/statement_checks.py (subline_parents 141–162, _subline_rules 165–184, run_checks 187–197), statement_mapping.py (statement_period 73–80, edition_for 83–87), contract/common.py (30–70), services/statements.py (form_confirmable 71–81), repositories/statements.py (_pair_document 330–346), schemas/statements.py (line_origin 157–159), api/rules/statement_lines.yaml (структура редакций, отсутствие пятизначных кодов), services/result_mapping.py (точки MappingFatal).
fixtures (скриптами python3 -I и venv парсера с -I): ocr/aep_3.json, aep_3_ofr.json, aep_5.json, aep_6.json, aep_8_form4.json, БО 1 кв 2025.json, Бухгалтерская отчетность 1 кв.25.json, ocr/expected_normalize/*.json + README; statements-xlsx/unagrande-co/2024-FY/Б.Б Лист 1.xls, Б.Б. Лист 2.xls, ОФР. Лист 1.xls, unagrande-group/2025-Q3/ОФР. Лист 1 3кв.25.xls, mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx, 1_Форма2_бух_09.xlsx, npz/2025-Q3/Бухгалтерская отчетность за Январь - Сентябрь 2025 г. (…).xlsx (все 9 листов), synthetic/st07_single_code_continuation.xlsx.
Не смотрел: arm-web (не индексирован, по заданию), services/documents.py, auto_confirm.py, чек-лист (repositories/consolidations.py) — следствия Р-10 для них описаны по индексу DBML, а не по коду.