# Design: unified-normalization-tp ## Контекст Change переносит в tabular-parser требования общей нормализации ББ/ОФР: шаг 1 (кандидат в форму из книги Excel) пишет адаптер сервиса, шаги 2–8 выполняет копия пакета `arm_statements`, результат и конверт — контракт v2.0. Дизайн и решения Р-1…Р-24 — change `unified-statement-normalization` в arm-ka-docs (issue RSHB/arm-ka-docs#116); реализация — план 4 (`plans/2026-10-18-plan-4-tabular-parser.md` в docs), issue RSHB/arm-tabular-parser#54, ветка `feat/54-statements-v2`. Номера P4-n — решения автора плана, R-n — рулинги контроллера при исполнении (нумерация сквозная, R-3 не присваивался). Дата всех решений ниже — 18.10.2026. ## Decisions made without discussion ### Решения автора плана (P4-n) - **P4-1.** Переход на общий код и на контракт v2.0 — одна задача (7): модели, копия схем, сборка книги, путь ОСВ и воркер меняются вместе; «контракт v2.0 в копии» отдельной последней задачей не вынесен — модели 2.0 без копии 2.0 (или наоборот) валят `result_contract` и тест идентичности. Golden и манифест — сразу за переключением (задача 8), до удаления старого кода; между задачами 7 и 8 проверка идёт с `ARM_KA_FIXTURES=/nonexistent`. Цена: одна крупная задача и окно в одну задачу без проверки на реальных файлах. - **P4-2.** Адаптер пишется новыми модулями рядом со старыми (`cells`, `forms`, `header_cells`, `adapter`, `contract`, `pipeline`); старые `statements/*` не правятся и удаляются целиком задачей 9. Цена: временное дублирование логики поиска блоков и шапки. - **P4-3.** `Cell.text` числа Excel — как в русской локали: группы по три через пробел, если в формате есть разделитель групп; десятичная запятая; литералы секции (скобки) сохраняются; «-» добавляется только у односекционного формата; `General`/неразобранный формат/xlsb — число без групп и экспоненты, целое без дробной части; дата — `dd.mm.yyyy`; логическое — «ИСТИНА»/«ЛОЖЬ». Цена: у экзотических форматов текст может отличаться от экрана Excel; `value` и статус от этого не зависят (у Excel значение берётся из `value`). Совпадает с примером docs `result-statement-excel.json` (`"586 260,26"`). - **P4-4.** Показываемое число вне `numeric(18,2)` больше не делает формат «неразобранным»: адаптер отдаёт `Decimal`, а `VALUE_OUT_OF_RANGE` и `amount = null` выдаёт пакет (спека «Шаг 4»). Цена: правка поведения #42 для чисел больше 16 целых цифр (на реальных файлах их нет). - **P4-5.** Сетка кандидата: строки блока целиком, включая пустые; объединённая ячейка — только в позиции левой верхней, прочие позиции `None`; ширина — наибольшая среди листов кандидата. Цена: подпись, объединённая на два столбца, видна пакету только в левом. - **P4-6.** Шапка ищется в строках блока выше первой строки с кодом его столбца кодов (столбец — `code_column` пакета: то же правило (а)/(б) шага 2 с защитой от блока «Коды», своего правила у адаптера нет), без кодов — во всём блоке: так титульный блок «Коды» (ОКОПФ 12267) не мешает, а подписи под формой (ИНН и ФИО в блоке подписей) не попадают в шапку. Цена: метка шапки ниже начала таблицы не найдётся. - **P4-7.** Шаблоны словаря пакета адаптер применяет к тексту, нормализованному функцией пакета по таблице «Словари» раздела API плана 1: метки шапки, заголовки, ОКУД, `auditor`, `legal_form` — `normalize_label` (= прежний `normalize_text` tabular-parser, P1-22); подпись «Код» и относительные периоды — `normalize_caption`; маркер продолжения — `is_continuation`. Цена: нет — смена нормализации в пакете меняет и `type_hint`, и адаптер. - **P4-8.** Склейка: открытая форма — последний кандидат с `type_hint` `BALANCE`/`INCOME`; лист с маркером сверяется только по типу; лист без маркера (сильное продолжение — столбец кодов по правилу шага 2 или своя строка заголовка; слабое — столбец кодов ослабленным поиском `code_column(…, relaxed=<тип формы>)`, минимум `min_codes_continuation` кодов, сразу после листа формы) — `sheet_joins` с числом колонок сумм `len(amount_columns(...))` пакета («столбцы правее столбца кодов, где в строках с кодами есть число»); открытой формы нет — отдельный кандидат без предупреждения (пакет даст `NO_ORGANIZATION_NO_INN`). Цена: «Изменение, %» с числами считается колонкой сумм с обеих сторон сравнения; лист с иной шапкой, но тем же числом колонок, присоединится и будет сопоставлен пакетом по датам. - **P4-9.** `SHEET_OTHER` для листов «прочее» выдаёт книга сама (документа и кандидата нет), а не пакет по кандидату без `type_hint`. Цена: два места порождения одного кода (у пакета — для кандидатов, не нашедших ни типа, ни кодов). - **P4-10.** `XLSB_NO_FORMATS` — у каждого кандидата книги xlsb (P1-5); `NUMBER_FORMAT_UNPARSED` — одно на кандидата с перечнем форматов всех его листов. Цена: у пояснений xlsb тоже есть `XLSB_NO_FORMATS` (`info`). - **P4-11.** Книга вызывает `normalize_candidate` по одному кандидату, а не `normalize`: так проверяется дедлайн между кандидатами (`header.form_name` пакет ставит сам из `evidence.title`, пара «кандидат → документ» не нужна). Исключение пакета на кандидате — `RECOGNITION_FAILED` всей книги, как сейчас (`TABLE_PARSE_ERROR` — код таблицы ИС). Цена: одна сломанная форма роняет книгу с ОСВ. - **P4-12.** Коды ОСВ — по источнику предупреждения, не по тексту: `OSV_HIERARCHY_BY_<СТРАТЕГИЯ>`, `OSV_HEADER`, `OSV_ROWS`, `OSV_INN_ROWS`, `OSV_NO_DATA_ROWS`, `OSV_BALANCE_IDENTITY`, `OSV_PARENT_SUM`, `OSV_TOTAL`, `OSV_XLSB_NO_OUTLINE`; тексты сообщений прежние, кроме иерархии. Цена: внутри `OSV_HEADER` и `OSV_ROWS` разные случаи различаются только текстом. - **P4-13.** Заметки читателя: несовпадение формата — `FILE_TYPE_MISMATCH` файла; «xls без кодовой страницы» — `XLS_NO_CODEPAGE` файла (`info`, код пакета, P1-23) в любой книге xls — касается и форм, и ОСВ; «xlsb без outline» — в каждый документ ОСВ книги (`OSV_XLSB_NO_OUTLINE`), в книге без ОСВ только логируется (иерархия строк — только у ОСВ). Цена: предупреждение о кодировке видно в карточке файла, а не у листа ОСВ. - **P4-14.** Предупреждение «форма не разобрана: книга завершилась ошибкой листа ОСВ» снято: в v2.0 для него нет кода, а текст ошибки и так называет лист ОСВ. Цена: аналитик не видит списка листов форм, оставшихся неразобранными. - **P4-15.** Статус документа ОСВ — прежние условия листа: контрольные суммы §1.4 сошлись и строки ИНН без нарушений (`glue.ok`, #35). Цена: формулировка спеки «когда контрольные суммы сошлись» читается шире. - **P4-16.** Нет `api/arm_statements/VERSION` — сервис не стартует (`RuntimeError`), как при ошибке словаря; в golden `service.statements_version` — volatile-поле. Цена: копия без `VERSION` не запустится даже локально. - **P4-17.** Новые синтетические случаи (форма xlsb, книга из пояснений, конфликты продолжений) — в памяти в юнит-тестах (`sheet_from_rows`, `write_xlsb`), новых файлов в `fixtures/statements-xlsx/synthetic` нет; PR fixtures — только golden. - **P4-18.** Манифест получает необязательное поле `error_warnings` (точное множество кодов `error` документа); проверка уникальности кодов строк снята (повторы законны, Р-4). Цена: при правке пакета, меняющей набор предупреждений, манифест правится вместе с golden. - **P4-19.** Тест покрытия словарей tabular-parser больше не охватывает словари форм — они в пакете. Цена: «мёртвый» синоним заголовка или метки шапки на реальных файлах не ловится (O-8). - **P4-20.** В tabular-parser — свой change `unified-normalization-tp` с дельтами `statement-parsing`, `tabular-recognition-service`, `osv-parsing` (по образцу P0-5); требование «Копия контракта идентична источнику» правит план 0, здесь не трогается. - **P4-21.** Golden — отдельные issue, ветка и PR в RSHB/arm-ka-fixtures (один PR на репозиторий); сливается сразу за PR tabular-parser. Цена: короткое окно, когда `make check` `main` одного из них красный на golden. - **P4-22.** Две формы на одном листе дают два документа с одинаковым `locator` (спека уникальности `locator` не требует; `ref` строк различаются). ### Рулинги контроллера при исполнении (R-n) - **R-1.** docs-worktree отсоединён на origin/main (docs #117 слит), а не на feat/116 (П-1/П-5 плана). Цена ошибки: нет. - **R-2.** Предупреждение tp #47 «xls без кодовой страницы…» — `XLS_NO_CODEPAGE` пакета (info, файл, без locator); нового `OSV_*`-кода нет; текст сообщения reader'а tp #47 сохранён; передаётся флагом `BookRead.no_codepage`. Цена: правка `book.py` и golden. - **R-4.** Задача 1 (markdown-дельты) без отдельного task-ревью — проверена `openspec validate --strict` и финальным ревью ветки. Цена: неточность формулировки дельты. - **R-5.** Проверка диапазона numeric(18,2) в `display_amount` не возвращается (P4-4); диапазон и `VALUE_OUT_OF_RANGE` выдаёт пакет; старый конвейер удалён задачей 9. Цена: нет после удаления. - **R-6.** Для |raw| ≥ 1e15 текст/значение числа без округления до 15 значащих цифр (иначе число из 17 цифр теряет цифры); пробел групп — обычный U+0020 как в примере docs `result-statement-excel.json`. Цена: расхождение с NBSP русского Excel, на value/статус не влияет. - **R-7.** Ячейки из одних цифр не считаются подзаголовком даты `report_date` (иначе год из тройки «число|месяц|год» принимается за подзаголовок), как в прежнем `header.py`. Цена: подзаголовок из голого числа «2025» не распознаётся. - **R-8.** Ячейка в строке-шапке таблицы (в строке есть подпись «Код» по словарю пакета `code_caption`) не заголовок формы: подпись колонки «Пояснения» (`'^пояснени'` в `form_titles` пакета) делала лист-продолжение НПЗ ещё одной формой DISCLOSURE. Дефект адаптера (спека называет «Пояснения» подписью колонки), пакет не правится. Решение владельца: оставить, посмотреть позже. Цена: компактная шапка, где заголовок формы и «Код» в одной строке, потеряет заголовок. - **R-9.** Предупреждения адаптера при кандидате без документа — предупреждения файла со своим `locator`; порядок `metrics.warnings` — сначала reader, затем по номеру первого листа; `XLS_NO_CODEPAGE` и `OSV_XLSB_NO_OUTLINE` — флаги `BookRead` (`no_codepage`, `no_outline`). Цена: правка `book.py` и golden. - **R-10.** НПЗ, ОФР, строки «Реализация иного имущества, прав требования, прочих работ и услуг» (F34, F44): значения 0 с форматом `[=0]"-"` — только прочерки, суммы нет; по спеке «строка без кода и без содержимого — не строка данных», пакет её не выдаёт; значение прочерка не превращаем в 0 (сломало бы различие null/0 и соотношения §7). `test_npz_documents` не требует detail/parent_ref 2340. Вопрос владельцу O-8.2: сценарий спеки «Реализация иного имущества… с суммами» не соответствует файлу. Цена: строки из прочерков не видны аналитику. - **R-11.** Задача 9 (удаление по списку) без отдельного task-ревью — покрыта финальным ревью ветки. - **R-12.** Остановка Р-31 по Межениновской (посторонние формулы аналитика в столбцах U–W) снята владельцем 18.10.2026: правило в пакете (docs #123, Р-44; arm-statements #6, 3390f92) — столбец без подписи правее последней подписанной колонки с числами не колонка сумм, предупреждение `NUMBERS_OUTSIDE_TABLE` (info). Копия пакета обновлена коммитом f5d071f. Цена: нет. - **R-13.** Ложная склейка/дедлайн: `plan_sheets` получает `deadline` (финальное ревью I-1, регрессия таймаута против main). Цена: нет.