# Единый алгоритм нормализации ББ/ОФР (scan-gateway и tabular-parser) — дизайн Issue: arm-ka-docs#116. Статус: **финальная версия** — целевая картина по шагам 1–9 согласована с владельцем (решения Р-1…Р-19, 17.10.2026) и прошла три раунда независимого ревью (`review-2026-10-17.md`, `review-2026-10-17-closure.md`, `review-2026-10-17-blind.md`, `review-2026-10-17-closure-2.md`, `review-2026-10-17-blind-2.md`). Спека (дельты OpenSpec, контракт v2.0, §7/§9/§11 в `05-domain-rules.md`, DBML v5.19) написана 18.10.2026 (решения S-1…S-25), прошла ревью вслепую (`review-2026-10-18-spec.md`, находки K-1…K-25) и доработана по нему (решения владельца Р-20…Р-22, решения S-26…S-52 ниже), прошла повторное ревью вслепую (`review-2026-10-18-spec-2.md`, находки L-1…L-30) и доработана по нему (решения владельца Р-23…Р-26, решения S-53…S-74 ниже). Где текст дизайна выше расходится с решениями Р-20…Р-26 и S-n, действуют они и спека. ## Цель Нормализация результата распознавания форм ББ/ОФР в scan-gateway (PDF → ИС Сканирование) и в tabular-parser (Excel) должна идти по одной цепочке шагов. У каждого шага — определённый результат, который передаётся в следующий шаг; результаты между шагами одинаковы для обоих сервисов (где это возможно). Все нестыковки собраны в таблицы различий по каждому шагу. Зачем: нормализовать каждый шаг; убрать бойлерплейт в arm-core (повторная дедупликация, пересчёт проверок, статус, который ядро не может взять из результата); улучшить поддерживаемость (один словарь — одно место); улучшить пользовательский опыт заказчика (старые ошибки распознавания AURORA-1406, 1462, 1511, 1517, 1519, 1522, 1523, 1524, 1634 — свойство конвейера, а не отдельные баги, см. issue #116). ## Как работает сейчас: цепочка шагов Каждый шаг описан так: **вход → что делает scan-gateway → что делает tabular-parser → выход → различия**. Код: scan-gateway `api/src/core/worker.py`, `gateway/`, `normalizer/ocr/`; tabular-parser `api/src/core/worker.py`, `parser/book.py`, `parser/readers/`, `parser/statements/`. Правила: `05-domain-rules.md` §7, §11, §11.1, §12. ### Шаг 0. Загрузка и доставка до сервиса **Вход:** файл пользователя. Общая часть (arm-core): файл принимается в карточку ЮЛ или в консолидацию (`primary_files.upload_one`): тип — по расширению и сигнатуре, дедуп по `(ЮЛ, sha256)`, файл — в S3, запись `stored_file` и `recognition_job` (`queued`). Конвейер: xls / xlsx / xlsb → `tabular`, PDF / PNG / JPG → `image`. Публикатор outbox собирает `recognition-request` (`job_id`, `file_id`, `pipeline`, `file_type`, `original_name`, ссылка S3, `reply_deadline`, `attempt`) и публикует в subject конвейера (`files.tabular_uploaded` / `files.img_uploaded`). Срок ответа по умолчанию: OCR — 20 мин, парсер — 5 мин; нет ack 30 с — повтор, после трёх повторов — `dlq`, файл `ERROR`. В сервисе (одинаково): запрос проверяется (JSON-объект, MAJOR версии 1, свой `pipeline`, схема; иначе `SCHEMA_MISMATCH` без ack); повтор `job_id` в работе — только повторный ack; ack `processing` до чтения S3; файл читается из S3 (не прочитан — `FILE_READ_ERROR`). scan-gateway дополнительно: бакет должен быть его единственным; `reply_deadline` истёк до отправки — `TIMEOUT`. **Выход:** байты исходного файла + запрос (`file_type`, `original_name`, `reply_deadline`). **Различия:** нет, кроме сроков ответа и проверки бакета. ### Шаг 1. Подготовка данных **Вход:** байты файла. #### 1.1 Разбор и чтение сырых данных **scan-gateway** — разбор делает ИС Сканирование: 1. Файл пишется во «Входящие» горячей папки как `.` (нет расширения / папка недоступна — `RECOGNITION_FAILED`). 2. ИС кладёт в «Исходящие» `/` один или несколько json — по одному на распознанный комплект отчётности. 3. Опрос каждые `SCAN_POLL_SECONDS` (5 с); набор готов, когда есть `*.json` и он не менялся `SCAN_SETTLE_SECONDS` (2 с); срок — `min(reply_deadline, запись + SCAN_TIMEOUT_SECONDS)` (1200 с), иначе `TIMEOUT`. 4. Склейка: каждый json разбирается независимо, по возрастанию имени → `{job_id, files: [{name, response, parse_error}]}`; сохраняется в S3 `raw/{job_id}.json` **до** нормализации; затем (только при `SCAN_CLEANUP`) папка очищается. Содержимое одного json (по `fixtures/ocr/*.json`, §11): корень `{file_uid, error {code, message}, data_ocr}`, успех — `error.code == ""`; `data_ocr`: поля шапки `label`, `organization`, `inn`, `units`, `date` — каждое `{form_name, confidence}` (сырой OCR-текст), **одна шапка на json**; `table_structure[]` — таблицы форм `{label {form_name, confidence}, rows[{row_index, cells[{column_index, value, confidence}]}]}`, `column_index` не обязательно подряд. Наблюдается: json с таблицами содержит ББ и ОФР одного периода под общей шапкой. **Допущение владельца (17.10.2026):** несколько периодов в одном файле ИС отдаёт разными json; образца нет — проверить на первом таком файле. **tabular-parser** — чтение делает парсер: 1. Файл больше `PARSE_MAX_BYTES` (50 МБ) — `UNSUPPORTED_FORMAT`; `PARSE_TIMEOUT_SECONDS` (120 с) — от начала разбора. 2. Формат — по сигнатуре байтов (OLE2 → XLS, zip с `xl/workbook.bin` → XLSB, иной zip → XLSX, прочее — `UNSUPPORTED_FORMAT`); несовпадение с `file_type` — предупреждение; лимиты распакованного zip и площади объединённых ячеек. 3. Книга → листы `Sheet`: непустые ячейки (текст, значение, числовой формат), объединённые диапазоны, уровни группировки, габариты. Библиотека упала — `UNREADABLE`; xlsb без стилей и группировки — предупреждение. #### 1.2 Базовая разметка **scan-gateway** — разметку сделала ИС, шлюз её принимает: ошибочный json (`parse_error`, непустой `error.code`, корень не объект) — предупреждение `<имя>: <код>: <сообщение>` и пропуск; все json ошибочны — `error` с кодом первого. Шапка json: ИНН — только цифры, организация, дата (двойники + разбор), единицы; уверенность ИС у каждого поля; предупреждения уровня файла «ИНН не распознан» / «ИНН распознан неуверенно» / «дата отчёта не разобрана». Каждая таблица `table_structure[i]` — кандидат в документ; тип — по `label` (`rules/statement_labels.yaml`). **tabular-parser** — разметка по содержимому (§12 п. 1, 6): вид листа (признак ведомости в первых 30 строках — ОСВ, отдельный путь; заголовок формы или ОКУД — форма; маркер «Форма … с.2», шапка «Код» с периодами, колонка кодов или 1–2 кода сразу после листа формы — продолжение; иначе прочее с предупреждением). Формы на листе: строки с заголовком («Бухгалтерский баланс», «Отчет о финансовых результатах», «…о прибылях и убытках») или ОКУД — кандидаты; без кодов между ними сливаются; заголовок после кодов — новая форма. Тип по ОКУД (0710001 → `BALANCE`, 0710002 → `INCOME`, прочие 0710xxx и «Пояснения» → `DISCLOSURE`), заголовок спорит с ОКУД — ОКУД и предупреждение. Шапка **своя у каждой формы** в границах блока: организация, ИНН (без ИНН аудитора), ОКПО, ОКОПФ, ОКФС, единицы, дата, адрес; найдено — 100, нет — `null`; предупреждения «в шапке не найдена организация» / «…единица измерения». Листы-продолжения присоединяются к последней открытой форме — но решение требует раскладки колонок (шаг 2). **Выход:** список кандидатов в документы форм — признак типа, шапка, сырые строки таблицы (строки × ячейки). **Различия (шаг 1):** | № | | scan-gateway | tabular-parser | |---|---|---|---| | 1 | Кто размечает | ИС Сканирование | парсер, по содержимому | | 2 | Единица разметки | json (комплект одного периода) → таблицы | книга → листы → блоки форм | | 3 | Шапка | одна на json, общая для его таблиц | своя у каждой формы | | 4 | Тип формы | `label` таблицы (уточнение по кодам — шаг 6) | ОКУД, иначе заголовок | | 5 | Уверенность | у каждого поля шапки и каждой ячейки, 0–100 | нет (найдено — 100) | | 6 | Многостраничная форма | одна таблица = одна форма, склейки нет | листы-продолжения присоединяются (шаг 6) | | 7 | Ячейка | сырой OCR-текст + `confidence` | текст, значение и числовой формат | ### Шаг 2. Раскладка таблицы **Вход:** сырые строки таблицы одной формы. **scan-gateway** (`layout.py`, `rows.parse_table_rows`, §11.1 п. 3): шапка — строка с ячейкой, начинающейся на «код»; наименование — ячейка «наименование», иначе ближайшая заполненная слева от кода; колонки значений — правее кода, где в тексте четыре цифры года или текст начинается на «на » / «за ». **Повторная шапка внутри таблицы перезаписывает раскладку** (число колонок изменилось — предупреждение; значения строки берутся по позиции, лишние отбрасываются, недостающие — «ячейки нет»). Без шапки раскладка выводится по данным: код — столбец с наибольшим числом значений из справочника кодов, наименование — левее с наибольшим числом текстов, значения — правее, где суммы не реже мусора; шапка синтезируется, колонки без дат, предупреждение. Ни шапки, ни столбца кода — таблица пропускается («колонка кодов не найдена»). Колонка: `label` — текст шапки, `period_date` — разбор даты после замены букв-двойников, только если текст «размером с дату»; нет даты — «колонка N без даты». **tabular-parser** (`columns.find_layout`, §12 п. 2–4): колонка кодов — столбец с наибольшим числом кодов (4–5 цифр, первые четыре в диапазоне формы), кодов ≥ 3 (у продолжения ≥ 1) и доля среди непустых ≥ порога; нет — документ без строк, «колонка кодов не найдена». Строка заголовка — последняя над первым кодом с подписью «Код», иначе последняя с двумя и более периодами правее кодов (или одним не голым годом); двухстрочный заголовок склеивается; строка нумерации «1 2 3 4 5» — часть заголовка. Колонки-периоды — правее кодов с распознанным периодом по словарю `statement_periods.yaml`; подпись без периода («Изменение», «%») пропускается, одно предупреждение со списком. Наименование — левее кодов с наибольшим числом текстов, кроме «Пояснения». Без заголовка (продолжение): колонки значений — где в строках с кодами есть числа, подпись пустая, дата `null`. **Выход:** раскладка формы — столбец кода, столбец наименования, колонки значений `[{label, period_date}]`, первая строка данных, строки, которые не данные (шапка, нумерация). **Различия (шаг 2):** | № | | scan-gateway | tabular-parser | |---|---|---|---| | 1 | Что считается кодом при поиске столбца | столбец — по справочнику кодов (`infer_layout`), строка — любое целое (`int(text)`) | 4–5 цифр в диапазоне формы (словарь) | | 2 | Повторная шапка внутри таблицы | перезаписывает раскладку | не ищется (одна раскладка на блок) | | 3 | Дата колонки | «размером с дату» + двойники + `parse_date` | словарь периодов `period_end` | | 4 | Без шапки | синтез шапки, колонки без дат, предупреждение | колонки по числам, подпись пустая | | 5 | Строка нумерации «1 2 3 4 5» | не распознаётся (становится строкой данных, AURORA-1517) | часть заголовка, в данных пропускается | | 6 | Подпись без периода | колонка значений, «без даты» | колонка пропускается, предупреждение | ### Шаг 3. Отбор строк данных **Вход:** раскладка + сырые строки. **scan-gateway** (`rows.py`, §11.1 п. 4): склеенная строка («1500\n1700» с N частями в суммах) разлепляется на N (OQ-156, предупреждение-счётчик). Классификация по ячейке кода: пустой код — пропуск (есть суммы — счётчик «строк без кода с суммами пропущено»); **нечисловой код («4 400\nI #4У», «ИЧ,250») — пропуск** (счётчик «строк с нечисловым кодом пропущено»); целое число — строка данных (вид `ROW` / `TOTAL` / `BALANCE` по наименованию — дальше не используется). Повтор кода — остаётся первая, предупреждение «код N повторяется: оставлена первая строка». Пропущенные строки **не восстанавливаются**. **tabular-parser** (`lines.collect_lines`, §12 п. 5): пустые строки пропускаются; код — 4–5 цифр в диапазоне, иначе строка «без кода»: нумерация «1 2 3 4 5» — пропуск; без кода с ненулевыми суммами — счётчик «строк без кода с суммами пропущено»; повтор кода — остаётся первая, код в `duplicates`. **Выход:** строки данных формы — сырой код, сырое наименование, ячейки значений по колонкам; счётчики отброшенных. **Различия (шаг 3):** | № | | scan-gateway | tabular-parser | |---|---|---|---| | 1 | Код-мусор | отдельный счётчик «нечисловой код» | не отличается от «без кода» | | 2 | Склейка двух строк в одну | разлепляется (OQ-156) | нет такого случая | | 3 | Повтор кода | предупреждение текстом | `checks.duplicate_line_codes` | | 4 | Отброшенная строка | только счётчик; строка исчезает (AURORA-1634, 1462) | только счётчик; строка исчезает | ### Шаг 4. Значения ячеек **Вход:** ячейки значений строки. **scan-gateway** (`amounts.parse_amount`, `document._value`, §11.1 п. 7): пробелы удаляются первыми; запятая / точка как разделитель тысяч по шаблонам (`4,200`, `837.994`); запятая → точка; скобки → минус; из строки выбрасывается всё, кроме цифр, `+`, `-`, `.`; больше одной точки — все удаляются; больше двух знаков после точки — `amount = null`, текст в `string_value`. Прочерк / пусто — `null`. `is_recognized` = `confidence == 100` **и** при разборе не выбрасывались символы. Ячейки под колонкой нет — `amount = null`, `confidence = 0`, `is_recognized = false`. **tabular-parser** (`lines._value`, `statement_money`, `number_format.display_amount`, §12 п. 5): число с числовым форматом — **как показывает формат ячейки** (масштабирующие запятые → тысячи, число знаков, секции и условия, округление half-up; формат не разобран — сырое значение и одно предупреждение на документ); иначе `to_money`: `Decimal`, скобки → минус, прочерк / пусто → `null`, нечисловой текст → `string_value`, `is_recognized = false`. `confidence` всегда 100. Счётчики дробных значений: при «тыс.» и ≥ 10 % дробных — предупреждение. **Выход:** `StatementValue {column_index, amount, string_value, confidence, is_recognized}`. **Различия (шаг 4):** | № | | scan-gateway | tabular-parser | |---|---|---|---| | 1 | Предобработка текста | OCR-мусор: пробелы, двойники, тысячи, скобки `}` / `]` | числовой формат ячейки Excel | | 2 | Смысл `is_recognized` | уверенность 100 ∧ ничего не выброшено | текст разобрался как число | | 3 | «42831 1» | пробел удалён → 428311, «чистое» (AURORA-1511) | не бывает | | 4 | Единицы | не проверяются | предупреждение о дробных при «тыс.» | | 5 | Пустая ячейка там, где была колонка | `null`, `confidence` ИС | `null`, 100 | ### Шаг 5. Код, наименование, редакция строки **Вход:** строка данных с сырым кодом и наименованием. **scan-gateway** (`document._line`, `_choose_edition`, §11.1 п. 6): редакция формы — по дате отчёта, иначе по самой поздней дате колонок, иначе по кодам (предупреждение); код очищается до букв, цифр, пробела и точки; **наименование — из справочника редакции по коду** (`api/rules/statement_lines.yaml` шлюза), уверенность кода и имени = уверенность ячейки кода, если текст чистый и код в справочнике, иначе `min(conf, 99)`; кода нет в справочнике — имя из OCR со своей уверенностью, код `min(conf, 99)`, предупреждение «код N из редакции …», если он есть в другой. **tabular-parser** (`lines.collect_lines`, `_line_name`): код как найден (4–5 цифр); **наименование — из файла**: ячейка колонки наименований, если пуста — ближайшая текстовая левее кодов, кроме «Пояснения»; уверенность кода 100, имени 100 (пусто — `null`). Редакции и справочника нет. **Выход:** `StatementLine {line_code, line_code_confidence, line_name, line_name_confidence, values}`. **Различия (шаг 5):** | № | | scan-gateway | tabular-parser | |---|---|---|---| | 1 | Источник наименования | справочник по коду (AURORA-1406, 1522) | файл | | 2 | Редакция (66н / ФСБУ 4) | определяется, влияет на имена | не определяется | | 3 | Справочник строк | свой `statement_lines.yaml` (не совпадает с ядром) | нет | | 4 | Уверенность кода | от ИС, срезается до 99, если не в справочнике | всегда 100 | ### Шаг 6. Сборка документа **Вход:** раскладка, строки, шапка кандидата. **scan-gateway** (`document.build_document`, `_resolve_type`): тип — по `label`; все коды одной группы и противоречат `label` — тип по кодам с предупреждением; `label` не сопоставлен — по большинству кодов. Колонки — из первой шапки. `locator = file:;table:`. Шапка — поля json (общая). **tabular-parser** (`statement._document`, `merge`): лист-продолжение присоединяется к последней открытой форме, если число колонок сумм совпало и тип в маркере не противоречит (слабое продолжение с 1–2 кодами — ещё и коды из диапазона формы); не присоединился — одинокое продолжение с пустой шапкой и предупреждение. `locator = sheet:,`. `DISCLOSURE` и форма без колонки кодов — без `columns` / `lines`. **Выход:** `StatementDocument {document_type, locator, header, columns, lines}` (без `checks`). **Различия (шаг 6):** | № | | scan-gateway | tabular-parser | |---|---|---|---| | 1 | Тип формы | `label`, сверка с кодами | ОКУД / заголовок, с кодами не сверяется | | 2 | Склейка страниц | нет | листы-продолжения | | 3 | `locator` | `file:…;table:i` | `sheet:A,B` | ### Шаг 7. Самопроверки **Вход:** документ. **scan-gateway:** не делает; `checks` отсутствует («не проверялось»). **tabular-parser** (`statements/checks.run_statement_checks`, §12 п. 7): соотношения §7 по каждой колонке из `statement_checks.yaml`: жёсткие → `relations_failed`, мягкие → `soft_relations_failed`; допуск ±1, округление 0,001 % уводит жёсткое в мягкие; `abs_terms`; «родитель = Σ подстрок» только при непустой детализации (AURORA-1527); повторы кодов → `duplicate_line_codes`; предупреждения текстом на каждое нарушение. **Выход:** `StatementChecks` или ничего. **Различия (шаг 7):** есть только у парсера; словарь §7 у парсера и ядра — две копии; ядро результат парсера **не читает**, считает заново. ### Шаг 8. Статус и предупреждения **Вход:** документы и накопленные предупреждения. **scan-gateway** (`_Build.fully_recognized`, `_normalize_file`, `normalize`): документ «полностью распознан», если ни одного предупреждения, все ячейки `is_recognized`, уверенность кода и имени 100, ИНН и организация с уверенностью 100, дата распознана. Файл — все документы распознаны ∧ нет предупреждений файла. Конверт `done` ⇔ все файлы распознаны ∧ ни один json не ошибочен; иначе `needs_check`; ни одной таблицы — `error RECOGNITION_FAILED`. Предупреждения — плоские строки `: table:: <текст>` в `metrics.warnings`. **tabular-parser** (`statement._is_ok`, `book.parse`): документ `ok` ⇔ колонка кодов найдена ∧ не одинокое продолжение ∧ есть колонки ∧ в шапке организация или ИНН ∧ `checks` есть ∧ нет жёстких нарушений ∧ нет повторов кодов; `DISCLOSURE` всегда `ok`. Книга `done` ⇔ все документы `ok` ∧ ни один лист не потерян; книга без листов ОСВ и форм — `INVALID_DOCUMENT`. Предупреждения — строки `sheet=<имя> <текст>` в `metrics.warnings`; на статус не влияют. **Выход:** `ResultStatus` конверта + `metrics.warnings[]`. **Различия (шаг 8):** | № | | scan-gateway | tabular-parser | |---|---|---|---| | 1 | Критерий `done` | всё с уверенностью 100 и без предупреждений | структура найдена, проверки сошлись | | 2 | Влияние предупреждений | любое → `needs_check` | не влияют | | 3 | Статус документа | нет (только конверт) | внутренний `ok`, в контракт не попадает | | 4 | Формат предупреждений | `: table:: …` | `sheet=<имя> …` | | 5 | Привязка предупреждения | к файлу | к документу (по префиксу) | Общее для обоих: статус один на конверт; предупреждения — свободный текст без привязки к строке или ячейке; ядро и экран не могут показать, что именно и где не так. ### Шаг 9. Результат Одинаково: `RecognitionResult` (`schema_version`, `job_id`, `file_id`, `status`, `documents`, `metrics`) → S3 `results/{file_id}/{job_id}/result.json` → конверт в `files.tabular_parsed` с `Nats-Msg-Id = result::`. У шлюза в конверте ещё `raw_s3_key`. ### Что с результатом делает ядро (для полноты картины) - `result_mapping.map_file` → `statement_mapping.build_statement`: **повторно** отбрасывает повторы колонок, кодов и значений (защита от обоих производителей), пишет `financial_statement_*` как пришло. - `statement_status` (D-6): `RECOGNIZED` ⇔ конверт `done` ∧ проверки §7 **пересчитаны ядром** и сошлись ∧ ИНН не `MISMATCH` ∧ период известен ∧ все ячейки с уверенностью 100 и `is_recognized` ∧ есть строки ∧ нет предупреждений маппинга; иначе `NEEDS_CHECK`. `checks` парсера не читаются; `metrics.warnings` сервиса на статус не влияют. `RECOGNIZED` сразу автоподтверждается. - `GET …/statement`: §7 считается заново по словарю ядра (копия словаря парсера); недостающие строки достраиваются из `statement_lines.yaml` ядра (не совпадает со справочником шлюза); предупреждения в ответе отсутствуют. - Экран: ячейка подсвечивается только по `confidence < 100` или `is_recognized = false`; нарушения §7 — списком без привязки к ячейкам; предупреждения сервиса и маппинга нигде не показываются. Бойлерплейт и потери, которые порождены различиями шагов 2–8: три копии словарей (§7 в парсере и ядре, строки в шлюзе и ядре); двойная дедупликация; пересчёт проверок; статус, который ядро собирает заново из косвенных признаков; три несвязанных канала качества (уверенность ячейки, проверки §7, предупреждения), из которых пользователь видит полтора. ## Целевая картина Каждый шаг ниже: **вход = выход предыдущего шага**, единый выход для обоих сервисов, список «различие → к чему приводим → что меняется в scan-gateway → что меняется в tabular-parser». Решения владельца собраны в конце (Р-1…Р-19, все приняты 17.10.2026). Текст прошёл три раунда независимого ревью (`review-2026-10-17*.md` рядом с этим файлом); находки внесены. Имена полей — рабочие, уточняются при написании спеки. Принципы, из которых выведены решения по строкам: 1. **Один выход на шаг.** Код, зависящий от источника, живёт только в шаге 1 (адаптеры «json ИС → кандидат», «лист Excel → кандидат»). Шаги 2–8 — общий код с общими словарями (форма общего кода — Р-1). Единственное место, где общий код смотрит на источник, — разбор запятой и точки в сумме (шаг 4). 2. **Ничего не выбрасывать молча.** Строка, колонка или ячейка, которую не удалось истолковать, идёт дальше со статусом, а не превращается в счётчик; пользователь её видит и исправляет. Положение в источнике (`ref`), порядок строк (`row_index`) и сырой текст ячейки (`text`) доезжают до базы и экрана. Подтвердить документ, не разобрав строки с суммами без кода, нельзя (Р-13). 3. **Качество — структурно и с привязкой к месту:** уверенность ячейки (от источника), `is_recognized` (разбор числа), статус строки, нарушения §7 по ячейкам, предупреждения с уровнем и привязкой. Уверенность, `is_recognized` и §7 сервис сводит в один признак `needs_review` у значения, строки и поля шапки (шаг 8) и из них выводит статус документа. Ядро при загрузке не пересчитывает ни проверки, ни статус, ни `needs_review`: хранит их как пришли и добавляет только предупреждения `info` по своему словарю имён. После правок аналитика ядро считает тем же общим кодом, добавляя своё знание: правка или подтверждение снимает `needs_review`, акцепт снимает жёсткие нарушения, квартал периода, сверка ИНН и наименований с справочником — условия `RECOGNIZED`. 4. **Один словарь — одно место:** проверки §7 (полный и упрощённый наборы), периоды, заголовки форм, упорядоченные перечни кодов форм по редакциям и вариантам (без наименований), диапазоны кодов — в общем коде сервисов; соответствие «код → наименование» — только в ядре, сервисы его не знают и отдают распознанный текст (Р-3). Порядок и состав редакций ядро берёт из своей копии общего кода, словарь ядра хранит только имена; `sync-statements --check` сверяет, что все коды словаря ядра есть в перечне. 5. **Очистка OCR (§11.1) — не отдельный шаг, а первая стадия типовых разборов текста** в общем коде, там, где уже известен смысл ячейки: дата шапки (шаг 1), подписи колонок (шаг 2), код строки и разлепление склеенных строк (шаг 3), сумма (шаг 4). Буквы-двойники («О» → 0, «З» → 3, «б» → 6, «S» → 5, «I», «l», «|» → 1) заменяются в датах, подписях, кодах и суммах (Р-11, отменяет §11.1 п. 1 и решение № 22 среза 3a «только даты»): сумма с заменой получает `is_recognized = false`, код с заменой считается неуверенным (эффективная уверенность ниже 100), а дальше их судьбу решают контрольные соотношения §7 и структурная проверка кода. В суммах крайние «|» (след границы таблицы: «| : 122477») убираются, а не заменяются. ИНН двойники не трогают: только цифры, как сейчас — иначе «3|1 21 2|» из aep_3 даёт лишние единицы и `MISMATCH` с ЮЛ. Разбор вызывается для любой ячейки без `value`; у Excel он срабатывает только на текстовых ячейках и почти всегда ничего не меняет. Из `normalizer/ocr/` шлюза `text`, `dates`, `amounts` становятся общими разборами, `layout`, `rows`, `header`, `document` — шагами 2–6 общего кода. ### Шаг 1 — Подготовка данных **Решения владельца:** 1) кто размечает — различие остаётся; 2) на выходе одинаковый контракт; 3) шапка своя у каждой таблицы; 4) тип формы — единый подход; 5) уверенность есть и у парсера, всегда 100; 6) парсер склеивает продолжения в шаге 1; 7) ячейка — один формат. **Вход:** байты файла (шаг 0). **Единый выход:** список кандидатов в форму на файл, `FormCandidate`: - `source` — `ocr` | `excel`; - `locator` — откуда взят: `file:<имя json>;table:` (как в §11 и golden-эталонах) / `sheet:[,]`; - `type_hint` — `BALANCE` | `INCOME` | `DISCLOSURE` | `null`, по единому правилу (п. 4); - `evidence` — ОКУД и заголовок как ячейки (для сверки с кодами в шаге 6); - `header` — поля `header` контракта (`organization_name`, `inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`, `okfs`, `address`), каждое — ячейка или `null`; - `rows` — плотная сетка строк × ячеек; все страницы и листы формы уже в одной сетке; пустая позиция — `null`; - `segment_breaks` — номера строк сетки, с которых начинается новый лист Excel (у OCR пусто): граница листа — всегда граница сегмента раскладки (шаг 2); - `warnings` — предупреждения шага (формат — шаг 8). Ячейка `Cell` одна для шапки и таблицы: `text` (как показывает источник), `value` (число или дата, если источник их знает; у OCR — `null`), `confidence` 0–100 (у Excel — 100), `ref` — позиция в источнике: OCR — `row:`, Excel — `<лист>!`. Уникальность в документе требуется от `ref` строки (шаг 3), не ячейки. - **1. Кто размечает** — остаётся, фиксируется полем `source` - scan-gateway: без изменений - tabular-parser: без изменений - **2. Единица разметки** — файл → список кандидатов; у OCR каждая таблица ИС — кандидат, у Excel — каждый блок формы на листе; шаги 2–8 работают только с кандидатом (Р-1 — форма общего кода) - scan-gateway: адаптер «таблица json → кандидат» - tabular-parser: адаптер «блок формы на листе → кандидат» - **3. Шапка** — у каждого кандидата своя; поля = поля `header` контракта, каждое — `Cell`; разбор текста поля — общий код по `text`: ИНН → только цифры, без двойников; дата → словарь периодов (шаг 2 п. 3), который включает все шаблоны сегодняшнего `dates.py` шлюза; единицы → словарь. Уверенность и сырой текст каждого поля доезжают до контракта (`*_confidence`, `*_text`) - scan-gateway: шапку json копирует в каждую свою таблицу - tabular-parser: добавляет сырой текст и уверенность 100 - **4. Тип формы** — одно правило: ОКУД → заголовок по одному словарю → `null`; окончательно — по кодам строк в шаге 6, у обоих - scan-gateway: `label` таблицы становится заголовком; `statement_labels.yaml` уходит в общий словарь - tabular-parser: добавляет сверку по кодам (шаг 6) - **5. Уверенность** — поле `Cell.confidence`; Excel всегда 100 - scan-gateway: без изменений - tabular-parser: добавляет поле - **6. Многостраничная форма** — одна форма = одна сетка на выходе шага 1; строки продолжения дописываются в конец сетки по маркеру «Форма … с.2» или по позиции листа, граница листа запоминается в `segment_breaks`; тип из маркера сверяется с `type_hint`, расхождение — отдельный кандидат + предупреждение `error`. Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы (как `_weak_blocker` сейчас) и, при трёх и более кодах, число колонок сумм совпадает с формой; иначе — отдельный кандидат с предупреждением `error`. Раскладка у каждого сегмента своя (шаг 2 п. 2), поэтому разная ширина колонок или объединённые ячейки на листе-продолжении без шапки не сдвигают значения - scan-gateway: без изменений - tabular-parser: склейка переезжает до раскладки; `join_blocker` сохраняет проверки по типу, диапазону кодов и числу колонок - **7. Ячейка** — один формат `Cell`; Excel: `text` — как показывает числовой формат, `value` — число по формату (tp#42; формат не разобран — сырое значение + предупреждение); OCR: `text` сырой, `value` `null`, `confidence` от ИС - scan-gateway: обёртка над `value` + `confidence` ИС - tabular-parser: отображаемый текст и значение считаются здесь, а не в шаге 4 ### Шаг 2 — Раскладка таблицы **Вход:** `FormCandidate` (сетка, `segment_breaks`, `type_hint`). **Единый выход:** `TableLayout` — сегменты сетки (участки между повторными шапками и границами листов), у каждого: столбец кода, столбец наименования, колонки значений `[{col, label, label_confidence, period_end, period_kind}]`, сопоставленные с первым сегментом по порядку; роль каждой строки сетки (`title` | `header` | `numbering` | `data`). **Диапазоны кодов** (словарь, границы уточняются в спеке): ББ 1100–1799, ОФР 2100–2999, отчёт об изменениях капитала 3100–3699, отчёт о движении денежных средств 4100–4499, отчёт о целевом использовании 6100–6499, пояснения 5000–5999. «Кодовые диапазоны» для поиска столбца — объединение этих отрезков (годы 2000–2099 и четырёхзначные суммы на 1–6 вне отрезков кодами не считаются). «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца; им проверяются разлепление строк (шаг 3), слабые продолжения и наследование столбца в сегментах. - **1. Столбец кодов** — одно правило: код — текст без пробелов, переносов и краевой пунктуации («1220 ,» → 1220), после замены двойников (порядок: убрать пробелы и переносы → двойники → проверка; «|» → 1 только если результат попадает в кодовый диапазон: «| 140» → 1140), 4–5 цифр (допускается `.0`), первые четыре в кодовых диапазонах. Многострочная ячейка — код, если каждая её часть код. (а) Столбец, над данными которого стоит подпись «Код» / «Коды» / «Код строки» (словарь подписей) **и под которой доля кодов среди непустых ячеек ≥ 0,6 или кодов ≥ 3**; несколько кандидатов — столбец с наибольшим числом кодов; берётся последняя такая подпись над первой строкой с кодом. Защита обязательна: в выгрузках 1С под подписью «Коды» титульного блока стоят ОКУД, дата, ОКПО, ИНН, ОКОПФ (12267 у АО, 12300 у ООО — по первым четырём цифрам это «коды баланса»), ОКЕИ: один код из девяти, доля 0,11. Документ из двух строк остаётся документом: два из двух, доля 1,0. (б) Подписи нет → столбец с наибольшим числом ячеек-кодов, кодов ≥ 3, доля ≥ 0,6 (так парсер сейчас); нет — «колонка кодов не найдена». Статусы строк с нечитаемым или пустым кодом — новое поведение для обоих: шлюз сегодня такие строки пропускает со счётчиком - scan-gateway: столбец по подписи получает защиту по доле; запасной подсчёт — по кодовым диапазонам, как сейчас; классификация строк `int(text)` → статусы - tabular-parser: добавляет проход по подписи до подсчёта; диапазон формы → кодовые диапазоны - **2. Повторная шапка и границы листов** — одно правило: новый сегмент открывает строка с признаком шапки («Код» / та же сигнатура) **или** граница листа из `segment_breaks`; у сегмента своя раскладка: по его шапке, а без шапки — столбец кодов по правилу п. 1, и если сегмент не первый и кодов в нём меньше трёх, ослабленный поиск: столбец с наибольшим числом кодов из диапазона столбца первого сегмента, минимум один (так `relaxed` + `_weak_blocker` парсера сейчас: лист «_2» НПЗ с единственной строкой 2400, у которого столбец кодов в другой колонке, чем на листе 1); колонки значений без шапки — по числам в строках с кодами, сопоставление с первым сегментом по порядку; число не совпало — предупреждение `error`, значения по позиции; столбец кодов не найден ни так, ни так — предупреждение `error` сегмента, строки сегмента не теряются (роль `data`, статусы по шагу 3) - scan-gateway: как сейчас, через общее правило - tabular-parser: бывший «лист-продолжение со своей раскладкой» становится сегментом; бывшая проверка числа колонок в `join_blocker` — предупреждение сегмента - **3. Дата колонки** — один словарь периодов (`statement_periods.yaml`), **объединяющий** сегодняшний yaml парсера и шаблоны `dates.py` шлюза: «На 31 марта 2025 г.», «За январь – март 2025 г.», «31.12.2024», «30/09/25», «30092025», «За 9 месяцев 2025 г.», «за I квартал 2025 г.», «1 полугодие 2026», «с 01.01.2025 по 31.03.2025», «2024», двузначный год, хвост «0:00:00», клетки «2|0|2|4|»; строки `test_dates.py::test_parses_real_forms` шлюза — обязательные тесты словаря. Результат — `period_end` и `period_kind`: абсолютные подписи → `absolute`; относительные («На отчётную дату», «На 31 декабря предыдущего года», «На 31 декабря года, предшествующего предыдущему», «За отчётный период», «За аналогичный период прошлого года») → `relative:<вид>`, дата считается от **эффективной даты документа**: дата отчёта из шапки, иначе самая поздняя абсолютная дата колонок; нет ни той ни другой → `period_end = null`, `period_kind` остаётся относительным, ядро досчитает после правки даты отчёта; подписи нет → `period_kind = none`. Подпись OCR с уверенностью ниже 100, разобранная в дату, подтверждается, если дата равна одной из ожидаемых (эффективная дата, минус год, минус два года) — иначе `error` «дата колонки неуверенна». Перед разбором — очистка двойников OCR. Относительные подписи — Р-6: это бланки по приказу 66н без впечатанных дат, у шлюза сейчас «колонка 0 без даты: На отчетную дату» - scan-gateway: `parse_date` → словарь, его шаблоны переезжают в словарь - tabular-parser: добавляет очистку двойников (no-op), относительные подписи и шаблоны шлюза - **4. Строка заголовка и её отсутствие** — одно правило (обе ветки парсера сохраняются): строка заголовка — последняя над первой строкой с кодом, где в столбце кодов стоит подпись «Код»; иначе последняя над первой строкой с кодом с двумя и более подписями-периодами правее кодов (или одной, не голым годом); двухстрочный заголовок склеивается. Роль `title` — только строки выше найденной строки заголовка (титульный блок «Дата 30 | 9 | 2025» Межениновской); в сегменте без заголовка роли `title` нет, строки выше первого кода с содержимым идут в данные (шаг 3). Заголовка нет ни у одного сегмента документа → колонки значений первого сегмента = столбцы правее кода, где в строках с кодами стоят числа; `label` пустая, `period_end` `null`, `period_kind = none`; одно предупреждение `error` «строка заголовка не найдена». Заголовок есть хотя бы у одного сегмента — остальные сегменты берут его колонки (п. 2) без предупреждения: листы 2–5 ББ НПЗ без шапки остаются `done` - scan-gateway: синтез шапки уходит (он эквивалентен); добавляет ветку «заголовок по периодам» - tabular-parser: добавляет предупреждение для документа без единого заголовка - **5. Строка нумерации «1 2 3 4 5»** — одно правило `is_numbering_row` → роль `numbering`, в данные не попадает - scan-gateway: добавляет (AURORA-1517) - tabular-parser: как сейчас - **6. Подпись без периода** — Р-6: подпись — период по словарю (абсолютный или относительный, п. 3) → колонка с датой; подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») → пропуск с предупреждением `info`; иначе, если в столбце есть числа в строках с кодами (OCR-мусор вроде «| | г» с уверенностью 17 в aep_3, обрезанная или самодельная подпись) — колонка остаётся с подписью как есть, `period_end = null`, `period_kind = none`, предупреждение `error` с привязкой к колонке, документ `needs_check`; аналитик ставит дату колонке через PATCH или подтверждает после сверки с источником; иначе пропуск. Ядру колонка без даты не мешает для периода формы (эффективная дата, п. 3), но межформенная проверка и гудвилл «на отчётную дату» по такой колонке не считаются, пока дата не проставлена. Пропуск числовой колонки с нераспознанной подписью отвергнут: для OCR это тихая потеря периода с возможным автоподтверждением - scan-gateway: добавляет словарь исключений и относительные периоды - tabular-parser: перестаёт молча пропускать неизвестные числовые колонки; добавляет относительные периоды - **7. Столбец наименования** — одно правило: подпись «Наименование (показателя)» в строке заголовка → этот столбец; иначе столбец левее кодов с наибольшим числом текстов, кроме столбца «Пояснения»; наименование строки — ячейка этого столбца, пусто → ближайшая текстовая ячейка левее кода, кроме «Пояснения» - scan-gateway: «наименование», иначе слева от кода → общее правило - tabular-parser: `_name_column` / `_line_name` → общее правило ### Шаг 3 — Отбор строк данных **Вход:** `FormCandidate` + `TableLayout`. **Единый выход:** `RawLine[]` — по строке на каждую строку сетки с ролью `data`, у которой есть содержимое: код, число или непустой текст в колонке значений с уверенностью ниже 100 (признак потерянной суммы: aep_3 row 8 «[190 :» с «Ё», «" :»). У строки: `row_index` (порядковый номер в документе), `ref` (уникален в документе; у разлепленных строк `row:#`), ячейка кода, `code_status`, ячейка наименования, ячейки значений по колонкам раскладки сегмента, `parent_ref` (для `detail`). Статусы (`ok` | `unreadable` | `missing` | `detail`): - `ok` — код разобран; - `unreadable` — ячейка кода содержит хотя бы одну цифру, но кодом не разобралась («ИЧ,250», «изо :», «[190 :»); ячейка кода без единой цифры («}», «:») считается пустой; - `detail` — Р-8: строка без кода с содержимым, стоящая под строкой с кодом (в том же сегменте или в предыдущих сегментах документа — родитель продолжается через разрыв страницы), — расшифровка («в том числе: …», «из них …», «Оборудование к установке», «Товар (41.01)»); родитель — ближайшая строка с кодом выше. Сервисы её отдают, ядро хранит, в проверки §7, агрегаты и критерий `done` она не входит, экран по умолчанию не показывает. Отменяет OQ-36 / OQ-141 («строки без кода не отдаются»); OQ-149 («восстановление строк по наименованию») остаётся открытым; - `missing` — строка без кода с содержимым, над которой во всём документе нет строки с кодом (кода не было или он потерян в самом начале таблицы: «Выручка» aep_3, где 2110 уехал в ячейку значения); строки роли `title` сюда не попадают. Строка без кода и без содержимого (заголовок раздела «АКТИВ», «в том числе:» без сумм) — не строка. Счётчиков отброшенных нет — есть статусы. Повтор кода — не статус: по правилам отчётности один код может стоять у нескольких строк с разными наименованиями, все они `ok`. Строка формы с пустой ячейкой кода под другой строкой с кодом попадёт в `detail` и будет скрыта; её значения не войдут в сумму раздела, нарушение §7 подсветит родителя (цель соотношения отсутствует при непустых членах — нарушение, шаг 7), аналитик раскроет расшифровки и назначит код (Р-13) — осознанная цена Р-8. - **1. Код-мусор** — статус `unreadable`, строка сохраняется с сырым текстом кода (AURORA-1462: «ИЧ,250» видна, аналитик назначает 1250). Разбор кода — по правилу шага 2 п. 1 - scan-gateway: счётчик → статус - tabular-parser: отличает «нет кода» от «не разобран» - **2. Склейка двух строк в одну** — одно правило, выполняется **до** разбора кода: ячейка кода содержит N частей через перевод строки и ячейки значений — N частей → N строк (OQ-156) с `ref` `row:#`; разлепление делается, если хотя бы одна часть кода разобралась как код диапазона столбца («44 30\nI |» aep_6 не множится: 4430 не в диапазоне ББ), нечитаемая часть становится строкой `unreadable`; число частей не совпало или ни одна часть не код — одна строка, многострочное значение → `amount = null`, `string_value` = текст, `is_recognized = false`. Значения разлепленных строк получают `is_recognized = false` и нуждаются в покрытии проверками §7 (Р-17: сошлись — подсветки нет, иначе `needs_review`); у Excel не срабатывает - scan-gateway: как сейчас, через общее правило; разлепление при нечитаемой части — новое; статус документа после разлепления — по общему правилу, а не безусловный `needs_check` - tabular-parser: правило общее, no-op - **3. Повтор кода** — Р-4: повтор кода — законный случай (один код, разные наименования), не ошибка и не предупреждение. Все строки сохраняются как есть со статусом `ok`; текстовое предупреждение и `checks.duplicate_line_codes` уходят. Для проверок §7, подтверждения и всего, что идёт после, значения одного кода идут суммой (см. «Что уходит из ядра»; договорённость ДБД в AURORA-1431; закрывает AURORA-1406). Неверно прочитанный код, случайно совпавший с существующим, ловит структурная проверка кода: повтор, у которого хотя бы одна копия с уверенностью ниже 100, — `needs_review` у этой строки (Р-16) - scan-gateway: «оставлена первая» → все строки - tabular-parser: `duplicates` уходит - **4. Отброшенная строка** — строки не отбрасываются: `unreadable` / `missing` / `detail` идут дальше со статусом, `row_index` и `ref` (AURORA-1634, 1462); ядро не достраивает вместо них пустую шаблонную строку молча; подтвердить документ, пока у `unreadable` / `missing` есть суммы, нельзя (Р-13). Отмена OQ-36 / OQ-141 — Р-8 - scan-gateway: счётчики → статусы - tabular-parser: счётчики → статусы; строки без кода с суммами начинают отдаваться ### Шаг 4 — Значения ячеек **Вход:** ячейки значений `RawLine` по колонкам раскладки (`Cell` или `null`). **Единый выход:** `StatementValue {column_index, amount, string_value, text, confidence, is_recognized}`: `text` — сырой текст источника всегда, как `Cell.text`; `string_value` — как сейчас, только когда сумма не разобрана (`amount = null`, непустой текст); `needs_review` досчитывается в шаге 8. - **1. Предобработка текста** — у ячейки после шага 1 либо есть `value`, либо только `text`. `value` — число → `amount = value`, квантование до 2 знаков half-up, не больше 16 целых цифр (`numeric(18,2)`), иначе `amount = null`, `string_value` = текст, `is_recognized = false` и предупреждение; `value` — дата или логическое → `amount = null`, `string_value` = текст, `is_recognized = false`; `value` нет → один `parse_amount(text, source)` для обоих: двойники (Р-11), пробелы и тонкие пробелы, минус «−» (U+2212) как знак, скобки → минус, причём `}` и `]` принимаются как закрывающая скобка (OQ-147), прочерк → `null`; разделители: у `ocr` запятая — тысячи при группе из трёх цифр (AURORA-1405, sg#35) и точка — тысячи при группах из трёх цифр («837.994», sg#37), текст с тремя и более знаками после разделителя — `amount = null`, `string_value` = текст (решение № 30 среза 3a сохраняется); у `excel` запятая и точка — десятичный разделитель («4,200» = 4,20, «1.500» = 1,5, как `to_money` сейчас); «лучшая догадка» из цифр ограничена 16 цифрами, иначе `null`; многострочное значение в строке с однострочным кодом → `amount = null`, `string_value` = текст, `is_recognized = false`; Excel-ячейки «число текстом» идут тем же путём. Разбор ОСВ (`to_money` в `classify.py`) общим `parse_amount` не заменяется — он остаётся за пределами этой работы - scan-gateway: `parse_amount` переезжает в общий код и получает квантование, предел длины, `}` / `]`, «−», тонкие пробелы - tabular-parser: `to_money` для форм заменяется общим `parse_amount` с `source = excel` - **2. Смысл `is_recognized`** — один: текст разобран как число без выброшенных и исправленных символов (замена двойника — исправление; `}` / `]` вместо скобки — тоже); `confidence` — отдельно, от источника; оба признака сервис сводит в `needs_review` (шаг 8, «Подтверждение проверкой»), экран подсвечивает по нему - scan-gateway: отвязывает от `confidence` - tabular-parser: как сейчас - **3. «42831 1»** — в `parse_amount` пробел допустим только как разделитель тысяч (группы ровно по три цифры); иначе `amount` — лучшая догадка (цифры подряд), `is_recognized = false`, сырой текст — в `text` (AURORA-1511, 1519) - scan-gateway: меняется через общий код - tabular-parser: то же правило, у Excel почти не срабатывает - **4. Единицы** — одно правило: единицы «тыс. руб.» и дробных ≥ 10 % → предупреждение документа `info` (как у парсера сейчас, документ остаётся `done`); то же сочетание у книги xlsb без числовых форматов — `error`: вероятен масштаб ×1000 (рубли с копейками, которые формат переводил в тысячи), а §7 сходится при любом масштабе - scan-gateway: добавляет - tabular-parser: как сейчас; xlsb-сочетание — новое - **5. Пустая ячейка** — Р-2: позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence` ячейки (нет ячейки — 100), собственной подсветки нет. По `fixtures/ocr` ИС отдаёт ячейку на каждую позицию каждой строки: пустые — пустой текст с уверенностью 100; непрочитанные — пустой текст с уверенностью 0, и такие нуждаются в покрытии по общему правилу шага 8 (снимается покрытием или подтверждением ячейки аналитиком); позиция отсутствует только при несовпадении колонок сегмента, что покрыто предупреждением `error` шага 2 п. 2 - scan-gateway: «ячейки нет → 0, не распознано» уходит - tabular-parser: как сейчас ### Шаг 5 — Код, наименование, редакция **Вход:** `RawLine` (статус, `row_index`, `ref`, ячейки кода и наименования) + `StatementValue[]`. **Единый выход:** `StatementLine {row_index, ref, line_status, line_code, line_code_text, line_code_confidence, line_name, line_name_confidence, parent_ref, values}`: `line_code` — разобранный код (4–5 цифр) или `null` при `unreadable` / `missing` / `detail`; `line_code_text` — сырой текст ячейки кода; `line_code_confidence` — от источника, а у кода с заменой двойника — ниже 100 (неуверенный); `line_status` = `code_status` шага 3; `parent_ref` — у `detail`. Несколько строк с одним кодом — обычные строки `ok`, порядок как в файле. - **1. Источник наименования** — Р-3: сервисы отдают распознанный текст наименования как есть, с уверенностью источника; соответствие «код → наименование» — только в ядре и только когда нужно (показ справочного имени, достройка шаблонных строк, сверка для неуверенных кодов — Р-16). Экран показывает справочное имя, файловое — рядом (AURORA-1406, 1522) - scan-gateway: имя из `statement_lines.yaml` → имя из OCR - tabular-parser: как сейчас - **2. Редакция (66н / ФСБУ 4)** — в сервисах редакция нужна только для упорядоченного перечня кодов (подтверждение кода структурой, шаг 8) и выбирается по эффективной дате документа (шаг 2 п. 3), без даты — проверяются обе редакции. Наименования и достройка шаблонных строк — ядро по его словарю имён; проверка «код не из редакции формы» — ядро по своей копии перечня общего кода - scan-gateway: `_choose_edition` → выбор редакции перечня кодов в общем коде - tabular-parser: добавляет выбор редакции перечня кодов - **3. Справочник строк** — только в ядре; в общий код сервисов не входит; копия в шлюзе удаляется - scan-gateway: удаляет `api/rules/statement_lines.yaml` - tabular-parser: без изменений - **4. Уверенность кода** — от источника (ИС / 100), без срезания до 99 по справочнику; «код не в справочнике» — проверка ядра, предупреждение `info` к строке, не сервисов - scan-gateway: убирает `min(conf, 99)` - tabular-parser: как сейчас ### Шаг 6 — Сборка документа **Вход:** `FormCandidate` (`type_hint`, `evidence`, `header`) + `TableLayout` + `StatementLine[]`. **Единый выход:** `StatementDocument {document_type, form_variant, locator, header, columns, lines}` без `checks`. - **1. Тип формы** — одно правило: большинство кодов в диапазоне ББ → `BALANCE`, ОФР → `INCOME`, прочих форм и пояснений → `DISCLOSURE`; совпало с `type_hint` или `type_hint` `null` — тип по кодам; противоречит — тип по кодам + предупреждение `error`; кодов нет — `type_hint`. Без колонки кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с предупреждением `error`); `type_hint` нет: у `ocr` каждая таблица ИС с содержимым — потерянный документ, предупреждение `error` файла (сегодня шлюз: «нет шапки и столбца кода» → `needs_check`), у `excel` лист «прочее» — `info`. `DISCLOSURE` по `type_hint` — шаги 2–7 не выполняются; `DISCLOSURE` по кодам — шаги 2–5 выполнены, строки и колонки отбрасываются (ядро строки пояснений не хранит), проверки не считаются, документ `done`. **Вариант формы** `form_variant` — `full` | `simplified`, только по структуре кодов (ОКУД для этого не годится: 0710096 — код КНД комплекта упрощённой отчётности, на бланках стоят 0710001 / 0710002): ББ — есть 1600 и нет ни 1100, ни 1200; ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300; шаг 7 и перечень кодов берут упрощённый набор. Образец упрощённой формы — в fixtures при первой возможности - scan-gateway: как сейчас, через общее правило; добавляет упрощённый вариант и `error` за потерянную таблицу - tabular-parser: добавляет сверку с кодами и упрощённый вариант - **2. Склейка страниц** — решена в шаге 1, различия нет - scan-gateway: без изменений - tabular-parser: без изменений - **3. `locator`** — одна грамматика «пары `ключ:значение` через `;`», набор ключей по источнику: `file:<имя json>;table:` / `sheet:[,]` — ключи как сейчас, golden-эталоны и §11 не меняются - scan-gateway: без изменений - tabular-parser: без изменений Шапка документа — поля `header` кандидата как есть (`text` → `*_text`, `value` → поле, `confidence` → `*_confidence` у всех полей). ### Шаг 7 — Самопроверки **Вход:** `StatementDocument`. **Единый выход:** `StatementChecks` у обоих: `relations_failed[]`, `soft_relations_failed[]` (каждое нарушение — с кодами слагаемых и `column_index`, чтобы экран подсветил ячейки). Правила счёта: - значение кода в колонке — сумма по всем строкам `ok` с этим кодом (повтор кода — законный случай, шаг 3 п. 3); строки `detail`, `unreadable`, `missing` в соотношения не входят; `duplicate_line_codes` уходит; нарушение подсвечивает все строки кода; - соотношение считается **посчитанным**, если непуста цель или хотя бы один член; пропускается, только когда пусто всё (ни строки `ok` с кодом цели, ни одного непустого члена). Пустая цель при непустых членах — жёсткое нарушение («БАЛАНС» без кода, ушедший в `detail`, делает 1600 невидимым, и это должно подсветить родителя); непустая цель при пустых членах — тоже нарушение (лист только с активом: «1600 = 1700» при отсутствующем 1700 — `needs_check`, как сегодня у трёх файлов манифеста парсера). Сегодня оба сервиса и ядро соотношение с отсутствующей целью пропускают — поведение меняется; посчитанное соотношение из всех `null` ничего не подтверждает (то же правило, что для «родитель = Σ подстрок» по AURORA-1527); - «сошлось» / «не сошлось»: жёсткое соотношение не сошлось, если расхождение больше допуска ±1 **и** больше округления 0,001 % — только тогда оно в `relations_failed`; расхождение в пределах округления и мягкие соотношения — в `soft_relations_failed`, это `info`, на статус и подсветку не влияют (OQ-138 сохраняется); - соотношение с членами по модулю (`abs_terms`: 2120, 2210, 2220, 2330, 2350, 2410) сходится при любом знаке такого члена и потому **подтверждает только величину, не знак** — см. «Подтверждение проверкой»; - набор соотношений выбирается по `form_variant`: полный или упрощённый (нет подытогов 1100 / 1200 / 1300 у ББ, 2100 / 2200 / 2300 у ОФР, свои равенства), оба — в одном словаре `statement_checks.yaml`; - родитель пятизначной подстроки — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код из первых четырёх цифр (`subline_parents`, §12 п. 7: 12310 под 1230). Проверки по словарю имён (код не в словаре) и по перечню кодов (код не из редакции формы по эффективной дате) — в ядре, как предупреждения `info` к строке; сервисы словаря имён не знают (Р-3, Р-12). - **1. Проверки только у парсера; словарь §7 в двух копиях; ядро результат не читает** — проверки — общий код с одним словарём `statement_checks.yaml` у обоих сервисов; для OCR нарушение §7 — главный признак неверно прочитанной цифры; ядро читает `checks` из результата, хранит их (`document_issue`, см. «Что уходит из ядра») и пересчитывает тем же кодом только после правок аналитика (PATCH) - scan-gateway: добавляет шаг - tabular-parser: `run_statement_checks` → общий код ### Шаг 8 — Статус и предупреждения **Вход:** `StatementDocument` + `StatementChecks` + предупреждения шагов 1–7. **Единый выход:** у каждого документа `status` (`done` | `needs_check`), `warnings[]`, `needs_review` у каждого значения, каждой строки и каждого поля шапки; у файла `metrics.warnings[]`; статус конверта — агрегат. Предупреждение — структура `{code, severity: error | info, message, line_refs?: [], column_index?, field?}`, а не строка; `line_refs` — список, потому что нарушение подсвечивает все строки кода. - **1. Критерий `done`** — одна функция общего кода с входами «раскладка, шапка, статусы строк, `needs_review`, нарушения, предупреждения, подтверждения и акцепт аналитика»: раскладка найдена ∧ (организация или ИНН) ∧ период определён (дата отчёта **или** самая поздняя абсолютная дата колонок — `statement_period` ядра; у ИС поле `date` часто мусор: у aep_6 и «БО 1 кв 2025» там название организации) ∧ все строки `ok` или `detail` ∧ нет значений, строк и полей шапки с `needs_review` (см. «Подтверждение проверкой» ниже) ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет предупреждений уровня `error`. `DISCLOSURE` — `done` без условий о раскладке, строках и проверках (как сейчас). Excel выполняет условия уверенности автоматически; структурная проверка кодов к Excel не применяется (Р-12). Квартал периода, статус конверта, сверка ИНН и наименований — знание ядра, в `done` не входят - scan-gateway: «ничего не выброшено и уверенность 100» → общий критерий - tabular-parser: `_is_ok` → общий критерий; добавляются строки и значения - **2. Влияние предупреждений** — по уровню: `error` → `needs_check`, `info` — не влияет; у обоих одинаково. Таблица уровней, полная версия — в спеке. `error`: ошибочный json набора при успешном соседе (уровень файла; решение № 28 среза 3a), `BACKEND_PARSING_ERROR` таблицы, таблица ИС с содержимым без колонки кодов и без типа (уровень файла), сегмент с содержимым без колонки кодов (в том числе потерянный лист-продолжение), лист с тремя и более кодами и другим числом колонок, не присоединившийся к форме, колонки сегмента не совпали, колонка без даты, дата колонки неуверенна, заголовка нет ни у одного сегмента, тип по кодам противоречит заголовку, тип маркера продолжения противоречит форме, ни организации, ни ИНН в шапке, период не определён (нет ни даты отчёта, ни дат колонок), дата отчёта неуверенна и не совпала с датами колонок, xlsb без форматов при «тыс. руб.» и дробных. `info`: ИНН не найден или неуверен при найденной организации (сверку с ЮЛ делает ядро — `MISMATCH`; НПЗ ОФР без ИНН остаётся `done`), организация не найдена или неуверенна при найденном ИНН, дата отчёта не разобрана при наличии дат колонок, единицы не найдены (аналитик правит в шапке), формат файла не совпал с `file_type` (файл прочитан верно), подпись из словаря исключений пропущена, дробные при «тыс. руб.» (кроме xlsb без форматов), xlsb без числовых форматов само по себе (tp#42, документ остаётся `done`), лист Excel «прочее» без `type_hint` и кодов, разлеплена склеенная строка, код не в словаре имён или не из редакции (ядро). Принцип: `error` — только то, что меняет данные или их полноту - scan-gateway: «любое → `needs_check`» уходит - tabular-parser: предупреждения начинают влиять - **3. Статус документа** — в контракте: `document.status` у каждого документа, включая ОСВ (у ОСВ `done`, когда ведомость разобрана, как сейчас; её предупреждения — `info`); конверт `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error` с кодом по источнику: `RECOGNITION_FAILED` у шлюза, `INVALID_DOCUMENT` у парсера — это различие остаётся, коды описывают разные причины. Статус конверта в условие `RECOGNIZED` ядра не входит, туда входят предупреждения `error` уровня файла (Р-19) - scan-gateway: добавляет - tabular-parser: внутренний `ok` → в контракт - **4. Формат предупреждений** — структура (см. выше) вместо строк `: table:: …` / `sheet=<имя> …` у всех документов результата, включая ОСВ; место — в `locator` документа, а не в тексте - scan-gateway: меняет - tabular-parser: меняет, включая путь ОСВ - **5. Привязка** — к документу, строкам (`line_refs`), ячейкам (`line_refs` + `column_index`) или полю шапки (`field`); `ref` строки уникален в документе и хранится в ядре вместе с `row_index`, поэтому экран показывает предупреждение у места и после сохранения - scan-gateway: меняет - tabular-parser: меняет **Подтверждение проверкой.** Проверка §7 — арбитр, уверенность — подсказка там, где проверки нет. По каждому значению сервис считает `needs_review`: - значение **нуждается в покрытии**, если `confidence` ниже порога (Р-5: 100) или `is_recognized = false` (число собрано из грязного текста, с заменой двойников или из разлепленной строки — Р-7, Р-11, Р-17); чистые значения (уверенность 100 и `is_recognized = true`) в покрытии не нуждаются: правило покрытия и правило знака к ним не применяются; - значение **покрыто**, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано и сошлось (в пределах допуска или округления), и `amount` самого значения не `null` (мусор, давший `null`, покрытым не бывает); - **знак**: для члена соотношения с `abs_terms` покрытие подтверждает только величину. Знак подтверждён, если форма записи знака у ячейки (скобки, минус, без знака — по `text`) совпадает с конвенцией колонки — формой записи у большинства членов `abs_terms` этой колонки с непустым `amount`, `is_recognized = true` и без правки аналитика (правленые ячейки в голосование не входят); не совпадает или сравнивать не с чем — `needs_review = true` независимо от соотношения. Так «(7744536}» с `}` как закрывающей скобкой даёт −7 744 536 и подтверждается, если 2210 и 2220 в той же колонке тоже в скобках; «{38378)» при остальных в скобках — подсвечивается; - `needs_review = true`, если значение нуждается в покрытии и не покрыто (или не подтверждён знак); - `true` у всех ячеек жёсткого соотношения в колонке, где оно не сошлось вне допуска и округления, независимо от уверенности и чистоты; мягкие соотношения и расхождения в пределах округления подсветки не дают; - иначе `false`: значение подтверждено проверкой, не подсвечивается и статус не трогает; сырой текст (`text`) и уверенность остаются в подсказке ячейки. Экран подсвечивает ячейки только по `needs_review`. Ядро хранит `needs_review` и `text` как пришли и пересчитывает `needs_review` вместе с `checks` общим кодом после PATCH (конвенция знака считается от сохранённого `text`), добавляя своё: правленое значение (`is_modified`) или подтверждённое аналитиком без изменения (`is_confirmed`, хранится) — `needs_review = false`; акцепт нарушений — жёсткие нарушения не считаются; предупреждения `error` документа сохраняются и после правки, кроме снимаемых соответствующей правкой (таблица — в «Что уходит из ядра»). Не покрыто проверками и остаётся на уверенности: справочные строки ОФР 25xx / 29xx, детализация без проверки родителя, `DISCLOSURE`, колонки, где есть только мягкие соотношения. Риск — две взаимно компенсирующие ошибки в одном соотношении; принимается (перекрёстные соотношения баланса его снижают). Закрывает AURORA-1524 без ущерба для 1519. Сошедшееся посчитанное равенство подтверждает и догадку из грязного текста (Р-7): случайное совпадение требует второй, компенсирующей ошибки в том же равенстве. **Поля шапки.** У каждого поля шапки свой `needs_review`: дата отчёта с уверенностью ниже 100 подтверждена, если совпала с самой поздней абсолютной датой колонок, иначе `needs_review = true` и предупреждение `error` «дата отчёта неуверенна» — иначе неверно прочитанный год давал бы неверный период и автоподтверждение; ИНН и организация с уверенностью ниже 100 — `info`, их сверяет ядро (ИНН — `MISMATCH` с ЮЛ, организация — по алиасам); единицы и прочие поля на статус не влияют. Сегодня шлюз требует уверенность 100 у ИНН и организации и распознанную дату — правило становится точнее, не слабее. **Код строки (Р-5, Р-12, Р-16).** Уверенность кода подтверждается не §7 (перепутанные соседи 1220 и 1230 дают ту же сумму раздела), а структурой и наименованием; правило применяется **только к неуверенным кодам** — уверенность ниже 100 или замена двойника (Р-12): у Excel и чистых OCR-кодов расхождение с перечнем даёт предупреждение `info` ядра «код не из редакции», а не `needs_check` — иначе пятизначные подстроки 1С (11101, 23201) и нестандартные строки клиента (1231) блокировали бы автоподтверждение. В общем коде лежит **упорядоченный перечень кодов** каждой формы по редакциям (66н, ФСБУ 4) и вариантам (полная, упрощённая) без наименований — порядок бланка, где итоги стоят после слагаемых (1110 … 1190, 1100; 2110, 2120, 2100), а не по возрастанию. Источник перечня — объединение кодов шаблонов банка (словарь ядра) и кодов печатных форм 1С по редакции: 1105, 1530, 2420, 2460, 2930 и другие, которых в словаре ядра нет. Алгоритм: четырёхзначные коды строк документа образуют последовательность; для каждой проверяемой редакции берётся наибольшая подпоследовательность, согласованная с порядком бланка; редакция — та, где она длиннее (при равенстве — по эффективной дате); если таких подпоследовательностей несколько, подтверждены только коды, общие для всех; коды с уверенностью 100 участвуют в последовательности как подтверждённые якоря. Пятизначный код подтверждён, если подтверждён его родитель (правило шага 7) и подстрока стоит после него. Равные соседние коды допустимы, но повтор кода, у которого хотя бы одна копия неуверенна, — `needs_review` у неуверенной строки (Р-16). Неподтверждённый неуверенный код — `needs_review` у строки (`StatementLine.needs_review`), документ — `needs_check`. Структура не ловит подмену неуверенного кода на незапечатанного соседа того же раздела (1110 → 1120, когда 1120 на бланке пропущен): это закрывает ядро — для строк с неуверенным кодом условие `RECOGNIZED` включает совпадение распознанного наименования со справочным по коду (Р-16; порог сходства — в спеке), расхождение подсвечивает строку через `document_issue`. По `fixtures/ocr` коды часто приходят с уверенностью 50 и 25, поэтому требовать 100, как шлюз сейчас, нельзя. Перечень кодов — не словарь строк: наименований в нём нет, Р-3 не нарушается; ядро пользуется своей копией перечня для проверки «код не из редакции», словарь ядра хранит только имена. Уверенность наименования на статус и подсветку в сервисах не влияет: сервисы отдают текст как есть. ### Шаг 9 — Результат Одинаково, как сейчас; контракт `recognition-result.schema.json` → **v2.0** (Р-9: по правилам контракта смена типа поля — MAJOR). Что меняется: `line_code` nullable, `line_status`, `line_code_text`, `row_index`, `ref`, `parent_ref`, `needs_review` у значения, строки и полей шапки, `values[].text`, `header.*_confidence` и `*_text` у всех полей; `document.status` у всех документов, включая ОСВ, `document.form_variant`; `document.warnings[]` и `metrics.warnings[]` — структуры с уровнем и привязкой у всех документов, включая ОСВ; `columns[].period_kind` и `label_confidence`; `checks` обязателен для `BALANCE` / `INCOME` со строками у обоих конвейеров (у документа без строк отсутствует), нарушения с кодами слагаемых, без `duplicate_line_codes`; `service.statements_version` — коммит копии общего кода, которым посчитан результат. Выкладка: ядро поднимает `SUPPORTED_MAJOR` и на время перехода принимает 1.x и 2.0 (два маппера, ack тоже 1.x и 2.0; при приёме 1.x статус документа считается по прежнему правилу D-6 — `job_done ∧ проверки ∧ values_ok ∧ …`, новые поля заполняются по умолчанию: `line_status = ok`, `row_index` по порядку, `text = string_value`), сервисы переходят на 2.0 после ядра — порядок «сначала ядро» обязателен, иначе первый же результат с `null` в `line_code` даёт файлу `ERROR`. Запрос `recognition-request` остаётся 1.x. Путь ОСВ в парсере меняется только в части структуры предупреждений и `document.status`. ### Что уходит из ядра и что в нём меняется - `build_statement`: повторная дедупликация колонок, кодов и значений (контракт гарантирует уникальность колонок; повторы кодов — законные строки, хранятся все как пришли). - **DBML v5.19 и одна миграция:** `financial_statement_line` — снять уникальность `(meta_id, line_code)`, добавить `row_index` (у строк из источника; у шаблонных и созданных правкой — `null`), `source_ref` (частичный уникальный индекс по `(meta_id, source_ref)` там, где не `null`), `line_status`, `line_code_text`, `parent_row_index` (родитель `detail`), `needs_review`, `is_confirmed` (подтверждение кода аналитиком); `financial_statement_value` — `source_text` (из `values[].text`), `needs_review` (как пришло от сервиса, пересчитывается ядром после PATCH; пересматривает решение v5.13 «считается, не хранится»), `is_confirmed` (подтверждено без изменения, хранится вместе с `confirmed_by / at`); `financial_statement_column.period_kind`, `label_confidence`; `financial_statement_meta.form_variant`, `needs_review` полей шапки; таблица `document_issue` (вид `warning` | `relation` | `soft`, код, уровень, сообщение, список `line_id`, `column_id?`, `field?`, источник `service` | `core`) — хранит предупреждения и нарушения как пришли; при перезагрузке файла очищается; при пересчёте после PATCH записи `relation` / `soft` заменяются целиком независимо от источника, записи `warning` источника `service` сохраняются, кроме снимаемых правкой; `recognition_job.statements_version`; `document` — снять уникальность `(stored_file_id, document_type, osv_account_code)` только для `BALANCE` / `INCOME` (частичный индекс: ОСВ по счёту и `DISCLOSURE` / `OTHER` остаются «один на тип на файл», §9.5 п. 2) и добавить порядковый номер формы в файле (Р-10); в ту же миграцию — `crm_id` ГСК / ГСЗ (AURORA-1619, arm-core#112). Заполнение при миграции: `row_index` существующих строк — по порядку кода, `line_status = ok`, `needs_review` значений — по прежнему `values_ok`, `source_text = string_value`. - Повторы кодов (Р-4): для проверок §7, подтверждения и всего, что идёт после (КФО, выгрузки, сверки), документ читается через **одну функцию «агрегат формы по коду»** — сумма значений строк `ok` по колонке, список `line_id`, справочное имя — чистой функцией, без хранения (DECISION-5). На неё переводятся все точки, где сегодня «одна строка на код»: `_line_amount` (межформенная проверка 1370 / 2400), `statement_amounts`, `subline_parents` / `parent_code`, `formLayout` (дети под родителем), `_resolve_cells`, `metrics.fields_*`. - `statement_status` (D-6) при приёме 2.0: `RECOGNIZED` ⇔ `document.status = done` ∧ нет предупреждений `error` уровня файла (потерянный json или таблица ИС, решение № 28 среза 3a; `needs_check` соседнего документа само по себе не блокирует — Р-19) ∧ ИНН ≠ `MISMATCH` ∧ период — конец квартала (`period_known`: иначе документ `RECOGNIZED`, но не подтверждаемый, 409 `PERIOD_REQUIRED`) ∧ у строк с неуверенным кодом распознанное наименование совпало со справочным (Р-16) ∧ в файле нет второй формы того же типа и периода (Р-18: при двух и более — ни одна не автоподтверждается). Остальные признаки D-6 не пересчитываются. - §7 при загрузке: `checks`, `needs_review` и предупреждения берутся из результата и хранятся (`document_issue`); ядро добавляет только `info` по словарю имён и перечню кодов и свою сверку наименований для неуверенных кодов. Пересчёт общим кодом — только после PATCH, с правилами `is_modified` или `is_confirmed` ⇒ `needs_review = false` у значения; `line_code_modified` или `is_confirmed` ⇒ `needs_review` строки = false; акцепт ⇒ жёсткие нарушения сняты. Версия общего кода из результата (`statements_version`) пишется в `recognition_job`; при пересчёте ядро сравнивает её со своей копией, расхождение — предупреждение `info` к документу (на окне выкладки «сначала ядро» расхождение штатно). - **Какая правка какое `error` снимает:** PATCH `header.report_date` — «период не определён», «дата отчёта не разобрана», «дата отчёта неуверенна»; PATCH `header.inn` или `organization_name` — «ни организации, ни ИНН»; PATCH `columns[].period_date` — «колонка без даты», «дата колонки неуверенна»; назначение кода или отметка `detail` — нарушения, пересчитываемые §7. Остальные `error` сервиса (колонки сегмента не совпали, заголовка нет, тип противоречит, потерянный json, лист или таблица, xlsb с масштабом) снимаются только перезагрузкой файла. - **PATCH `…/statement`** (Р-13): помимо `header` и `values` принимает `values[].confirmed` (подтвердить ячейку без изменения значения: `is_confirmed`, `needs_review = false`, `is_modified` не ставится, аудит), `columns: [{column_id, period_date}]` (дата колонке без подписи или с неуверенной подписью; пересчитывает `is_current`; `report_date: null` обнуляет `period_end` относительных колонок) и `lines`: «назначить код» (`line_id`, `line_code`), «отметить расшифровкой» (`line_id`, `line_status = detail`), «подтвердить код» (`line_id`, `confirmed`). Переходы: `unreadable` / `missing` / `detail` → `ok` назначением кода; `unreadable` / `missing` / `ok` → `detail` отметкой; `ok` → `ok` с другим кодом (смена кода, прежний в `line_code_original_value`); `ok` неуверенный → подтверждён. Правила: код 4–5 цифр, может совпадать с существующим (Р-4); незнакомый словарю — `info`; назначенный или подтверждённый код снимает `needs_review` строки, `line_code_confidence` становится 100, `line_code_modified`, `line_code_original_value`; у `detail` родитель пересчитывается как ближайшая строка `ok` выше по `row_index`; шаблонная строка с таким кодом, если она уже создана правкой аналитика с суммами, — 409 `LINE_EXISTS` с подсказкой (аналитик сначала очищает её суммы или выбирает другой код), без сумм — удаляется, её место занимает настоящая; пара «`line_status = ok` без кода» — 422; удаления строк нет (первичка не мутируется, `detail` — способ вывести строку из сумм). Строки, созданные правкой: `row_index = null`, `source_ref = null`, `line_status = ok`. Аудит `RECOGNIZED_DATA_EDIT` с `before / after` по коду, статусу, родителю и подтверждению. Один PATCH — один пересчёт и одна запись. - **Подтверждение документа** (Р-13, Р-18): порядок проверок 409 по §9.5 п. 4: `PERIOD_REQUIRED` → `LINES_UNRESOLVED` (есть строки `unreadable` / `missing` с непустым `amount`; строки без сумм не мешают) → `DUPLICATE_FORM` (в файле уже подтверждена другая форма того же типа и периода; 409 со ссылкой на неё; между файлами работает автозамена §9.5 п. 3) → жёсткие нарушения без акцепта. Автоподтверждения `LINES_UNRESOLVED` не касается (`done` таких строк не допускает). `detail` в агрегат, КФО и снимок ЕХ не попадают — цена Р-8. - Правка даты отчёта (PATCH шапки): для колонок с `period_kind = relative:<вид>` ядро досчитывает `period_end` общим кодом от новой даты. - Словарь §7: единственный экземпляр в общем коде (Р-1), копия ядра удаляется. Словарь имён строк (код → наименование по редакциям) остаётся только в ядре, копия шлюза удаляется; порядок и состав редакций ядро берёт из своей копии перечня общего кода. - Достройка шаблонных строк: по словарю ядра; код, у которого есть хотя бы одна строка `ok` из файла, шаблоном не достраивается. - **Экран** (Р-14): раскладка остаётся «как на бумаге» — разделы, внутри строки `ok` по возрастанию кода, несколько строк одного кода в порядке файла, шаблонные строки на своих местах, пятизначные подстроки под родителем. Строка `unreadable` / `missing` ставится под ближайшей предыдущей по `row_index` строкой, которая разместилась по коду, а без такой — в начало первого раздела; подсвечена целиком, в ячейке кода серым сырой текст с подсказкой. Строки `detail` скрыты под переключателем «Показать расшифровки (N)», как пустые строки детализации, показываются под родителем с отступом. Подсветка ячеек только по `needs_review` (уверенность, `is_recognized` и нарушения §7 уже сведены сервисом), в подсказке — `text` и уверенность; поля шапки с `needs_review` подсвечены; предупреждения и нарушения — из `document_issue`, с привязкой к месту. В режиме «Редактировать»: ячейка кода у `unreadable` / `missing` — поле ввода с подсказкой из справочника (код → справочное имя), меню строки «Это расшифровка»; у `detail` — «Назначить код»; у обычной строки — «Сделать расшифровкой», смена кода и «Подтвердить код» у неуверенного; подтверждение ячейки без правки; дата у колонки без подписи. Кнопка «Подтвердить данные» при неразобранных строках с суммами отключена с подсказкой «Разберите строки без кода: N» и переходом к первой; сервер отвечает тем же 409. - Открытый вопрос: склейка двух половинок формы из одного файла не предоставляется, файл перезагружают — в `07-open-questions.md` (Р-15). Вопрос о сопоставлении файлового наименования со справочным закрыт Р-16: для неуверенных кодов это условие `RECOGNIZED`, для остальных — только показ справочного имени. ### Решения владельца (Р-1…Р-19 — 17.10.2026, Р-20…Р-44 — 18.10.2026) - **Р-1** (шаг 1, п. 2) — **копия по скрипту.** Платформа не собирает Python-пакеты и не кладёт их в свой Nexus, поэтому общий код — dataclass-ы шага 1 (сериализуемые в json), шаги 2–8, словари §7, периодов, заголовков форм, перечни и диапазоны кодов (без словаря имён) со своим загрузчиком — живёт в отдельном репозитории `arm-statements` со своими тестами и оформлен как пакет `arm_statements/` с `pyproject`. Границы пакета: пакет владеет своими типами входа и выхода, хост конвертирует выход в свою модель контракта (у ядра на переходе их две); golden-тесты пакета — внутри пакета на синтетических и обезличенных данных, тесты на реальных фикстурах остаются в scan-gateway и tabular-parser и соседний репозиторий не читают. В tabular-parser, scan-gateway и arm-core пакет лежит копией вместе со своими тестами и конфигом линтеров (конфиг пакета подключается к `pytest` и `ruff` хоста как include, чтобы правила не конфликтовали), импортируется по имени пакета `arm_statements` (путь добавляется в `pyproject`), поэтому замена копии на зависимость не трогает импорты; маркер коммита источника попадает в результат как `service.statements_version`; раскладывает и проверяет свежесть скрипт `sync-statements` в docs по образцу `sync-web.sh` (`--check`, код 20 при прямых правках), вызывается из `make check-full` перед PR локально — **не** из `make check` и не из пайплайна: тест, читающий соседний репозиторий, уже ломал main (arm-core #33, пайплайн платформы вне репозиториев). Прямые правки копии запрещены. Срочное исправление общего кода: PR в `arm-statements` → `sync-statements` → PR в каждом потребителе, порядок выкладки «сначала ядро». - **Р-2** (шаг 4, п. 5) — отсутствующая позиция под колонкой — чистый `null` без собственной подсветки. Основание: по fixtures ИС отдаёт ячейку на каждую позицию (пустая — уверенность 100, непрочитанная — 0, и её ловит общее правило `needs_review`); отсутствие позиции бывает только при несовпадении колонок сегмента и покрыто предупреждением шага 2 п. 2. - **Р-3** (шаг 5, п. 1) — сервисы отдают распознанный текст наименования; соответствие «код → наименование» — только в ядре, при необходимости (= решение № 2 по спринту 2, AURORA-1406 / 1522; договорённость ДБД «из справочника» выполняется на экране). - **Р-4** (шаг 3, п. 3) — повторы кодов законны; все строки сохраняются и показываются, для проверок и всех операций после подтверждения или автоподтверждения — суммой по коду. Следствие: снять уникальность `(meta_id, line_code)` в DBML; все чтения «одна строка на код» в ядре переводятся на агрегат. - **Р-5** (шаг 8, «Подтверждение проверкой») — порог для значений — 100 (у ИС уверенность дискретная: 100, ~80, 71, 67, 60, 50, 25, 0, промежутка 90–99 нет; в группе 70–89 есть настоящие ошибки, например «11 30» с уверенностью 80); ложных подсветок становится меньше за счёт подтверждения проверкой §7, а не порога. Код строки подтверждается структурой — упорядоченным перечнем кодов формы по редакции, не уверенностью; область применения — Р-12, дополнение — Р-16 (= решение № 7 по спринту 2, AURORA-1519 / 1524). - **Р-6** (шаг 2, п. 6) — числовая колонка с нераспознанной подписью остаётся в документе без даты с предупреждением `error` (`needs_check`, подтверждение руками); пропуск отвергнут как тихая потеря периода. Относительные подписи бланка 66н («На отчётную дату», «На 31 декабря предыдущего года», …) добавляются в словарь периодов и считаются от эффективной даты документа; после правки даты отчёта ядро досчитывает их; дату колонке можно поставить руками. - **Р-7** (шаг 8, «Подтверждение проверкой») — **да:** сошедшееся жёсткое соотношение §7 снимает подсветку и с `is_recognized = false`; такой документ может получить `done` и автоподтвердиться, сырой текст остаётся в подсказке ячейки. Оговорки по ревью: соотношение должно быть посчитанным, а не «0 = 0»; для членов по модулю знак подтверждается конвенцией колонки, а не соотношением; правило знака — только для сомнительных ячеек. Варианты «всегда подсвечивать» и «снимать подсветку, но не автоподтверждать» отвергнуты: первый возвращает ложные подсветки AURORA-1524, второй даёт статус «требует проверки» без подсветок. - **Р-8** (шаг 3) — расшифровки без кода («в том числе …») отдаются сервисами со статусом `detail` и хранятся ядром; в проверки, агрегаты и `done` не входят; экран по умолчанию их не показывает (переключатель, как у пустых строк детализации). Отменяет OQ-36 / OQ-141. - **Р-9** (шаг 9) — контракт **v2.0**, а не аддитивный 1.3; ядро на время перехода принимает 1.x и 2.0; порядок выкладки — сначала ядро. - **Р-10** (ядро) — снять уникальность `document` по `(stored_file_id, document_type, osv_account_code)` для форм ББ / ОФР, вторая форма того же типа в файле хранится; в ту же миграцию DBML v5.19 — `crm_id` ГСК / ГСЗ из AURORA-1619 (arm-core#112). - **Р-11** (принцип 5) — буквы-двойники заменяются и в суммах, и в кодах; результат — `is_recognized = false` у суммы и неуверенный код, дальше их проверяют контрольные соотношения §7 и структура, и если всё сошлось, подсвечивать не нужно (Р-7 с оговорками). Отменяет §11.1 п. 1 «только даты» и решение № 22 среза 3a. ИНН двойники не трогают. Данные: в четырёх json ИС двойников в суммах и кодах нет вовсе — правило на вырост, не под текущие сканы. - **Р-12** (шаг 8, «Код строки») — структурное подтверждение кода применяется только к неуверенным кодам (уверенность ниже 100 или замена двойника); у Excel и чистых OCR-кодов расхождение с перечнем даёт `info` от ядра, не `needs_check`. - **Р-13** (подтверждение документа) — подтверждение документа блокируется (409 `LINES_UNRESOLVED`), пока есть строки `unreadable` / `missing` с суммами; аналитик назначает код или отмечает строку расшифровкой в режиме правки, через `PATCH …/statement lines` (детали — в «Что уходит из ядра»). - **Р-14** (экран) — раскладка по коду «как на бумаге» остаётся (просьба владельца 09.10.2026); строки со статусами ставятся под ближайшей предыдущей размещённой строкой по `row_index`, расшифровки под родителем и скрыты по умолчанию; переключатель «как в файле» — не в этой работе. - **Р-15** (ядро) — вторая форма того же типа и периода у одного ЮЛ из того же файла хранится; склейка половинок формы из одного файла не предоставляется (файл перезагружают), вопрос — в `07-open-questions.md`. От ИС Сканирование такой случай не ожидается (владелец), основной источник — Excel с листом-продолжением другого типа или двойной печатью. Уточнено Р-18. - **Р-16** (шаг 8, «Код строки»; ядро) — защита от подмены неуверенного кода на соседний: повтор кода, у которого хотя бы одна копия неуверенна, — `needs_review` у этой строки (сервис); для строк с неуверенным кодом условие `RECOGNIZED` в ядре включает совпадение распознанного наименования со справочным по коду, расхождение подсвечивает строку. Закрывает открытый вопрос о сопоставлении наименований: для неуверенных кодов — условие автоподтверждения, для остальных — только показ справочного имени. - **Р-17** (шаг 3, п. 2) — разлепленные строки OQ-156 не держат документ в `needs_check` безусловно (решение 08.10.2026 заменяется): их значения нуждаются в покрытии, как грязные, и при сошедшихся соотношениях §7 не подсвечиваются; само разлепление — предупреждение `info`. - **Р-18** (ядро) — при двух и более формах одного типа и периода в одном файле ни одна не автоподтверждается; подтверждение второй при подтверждённой первой — 409 `DUPLICATE_FORM` только внутри файла (между файлами работает автозамена §9.5 п. 3). - **Р-19** (ядро) — `RECOGNIZED` не зависит от статуса конверта: достаточно `document.status = done` и отсутствия предупреждений `error` уровня файла (потерянный json или таблица ИС блокируют, `needs_check` соседнего документа из-за одной ячейки — нет). - **Р-20** (18.10.2026; ревью спеки K-4, повторы кодов) — законные повторы кодов (в реальных ББ 2025 — 1220 «НДС, начисленный по отгрузке» и 1520 «Обязательство по аренде») допускаются в упорядоченном перечне кодов: структура подтверждает обе копии. Правило Р-16 «повтор с неуверенной копией — `needs_review`» применяется только к повтору, которого нет в перечне. Словарь имён ядра — список имён (синонимов) на код; загрузчик перестаёт запрещать повтор кода в этом смысле (сверка наименования — с любым из имён). В спеке: перечень `contracts/statement-code-orders.yaml` (`repeats`: у `fsbu4` — 1220, 1520, 2410 печатной формы 1С; у `66n` — раздел III НКО, см. S-28), §11.9, §7 «Полный состав формы», спеки `statement-normalization` и `arm-core`. - **Р-21** (18.10.2026; ревью спеки K-4, мягкие соотношения ОФР) — неуверенное значение подтверждается и мягким соотношением, если оно сошлось точно (расхождение 0 или в пределах допуска ±1; не «в пределах округления»). Применяется к 2400, 2410, 2411, 2412, 2460 и прочим строкам, у которых есть только мягкие соотношения. Знак для `abs_terms` — по прежнему правилу конвенции колонки. В спеке: §7, §11.9, требование «Шаг 8 — `needs_review` значения». - **Р-22** (18.10.2026; решения писателя S-1…S-25) — приняты все, с оговорками ревьюера из таблицы S-n в `review-2026-10-18-spec.md`: S-4 — синонимы на код и исключение «итого по разделу» из правила вхождения; S-5 — в упрощённом ОФР `2400` мягкое, как в полном, ББ — жёсткие; S-7 — OQ-148 закрывается этим решением; S-10 / S-22 — одна формулировка; S-13 — двойники внутри детектора нумерации и допуск одного пропуска; S-15 — экранирование имени листа в Excel-`ref`; S-17 — доопределить по K-23; S-19 — `codes` и `column_index` у `document_issue`; S-21 — по K-2 / K-21. Явно утверждены владельцем S-4, S-5, S-7, S-10, S-12, S-13, S-14, S-20, S-22, S-25 (отмечены ниже). - **Р-23** (18.10.2026; повторное ревью спеки L-1) — значения строк `detail` в `needs_review` и в статус документа не входят: у них `needs_review = false`, функция статуса их не учитывает; подсказка на экране по `text` и `confidence` остаётся. Следствие: на реальных сканах РБ и Счастливочной за 1 кв. 2025 формы могут стать `done` (по прогону ревьюера держали их только ячейки расшифровок). В спеке: `statement-normalization` («Шаг 8 — `needs_review` значения», функция статуса, сценарий на РБ), `arm-core` (пересчёт после правки, согласованность), `arm-web`, §11.9, proposal; S-51 пересмотрено. - **Р-24** (18.10.2026; L-13 и упрощение S-36) — перед выкладкой ядра база очищается от форм ББ/ОФР (документы `BALANCE` / `INCOME`, их `financial_statement_*`, комментарии, привязки к версиям «в работе»; файлы, где кроме форм ничего нет, — целиком с заданиями; ОСВ, консолидации, справочники не трогаются; детали и отказы — S-73). Поэтому миграция v5.19 только добавляет колонки, индексы и значения по умолчанию; §7 для существующих форм не пересчитывается, импорта копии `arm_statements` в ревизии Alembic нет. Миграция обязана проходить и на непустой базе (колонки по умолчанию, `file_ordinal` по порядку, `document_issue` пуст), но корректность нарушений у старых форм не гарантируется — сказано явно. Для приёма 1.x после выкладки (окно перехода) §7 по-прежнему считается ядром копией пакета при приёме (остаётся из S-36). В спеке: proposal (порядок выкладки), `arm-core` («Миграция DBML v5.19», «Очистка форм перед выкладкой», «Формы результата 1.x — §7 при приёме»), §7 «Ядро», §9.5 п. 2, DBML v5.19 (шапка). - **Р-25** (18.10.2026) — утверждены владельцем решения писателя S-26, S-34, S-36 (в редакции Р-24), S-37, S-41, S-47 (подтверждение без правки не сбрасывает акцепт), S-48 (в редакции L-12: ядро вызывает общую функцию статуса). Отмечены ниже. - **Р-26** (18.10.2026; OQ-168) — коды без образца в fixtures (2421 / 2430 / 2450 в 66н, 1120 и другие в ФСБУ 4) в перечень кодов не входят до появления образцов; неуверенный такой код на скане — `needs_review` строки. Временное правило, принятое владельцем; OQ-168 остаётся открытым до образцов. - **Р-27** (18.10.2026; S-73, открытый вопрос доработки) — смешанные файлы «ОСВ + формы» при очистке перед выкладкой: удалять целиком (`--include-mixed`) или только формы — владелец решает по отчёту `purge_statements.py --dry-run` в момент очистки; до этого скрипт по умолчанию удаляет только формы. - **Р-28** (18.10.2026; S-55) — принято: при приёме 2.0 ОСВ получает `RECOGNIZED` по своему `osvDocument.status` и отсутствию предупреждений `error` уровня файла, независимо от статуса соседней формы в той же книге (как Р-19 для форм). - **Р-29** (18.10.2026; S-67) — расхождение «шаблонные строки экрана по редакции по дате, проверки ядра — по редакции по структуре» (бланк 66н, сданный в 2025 году) в этой работе не сводится; отдельная задача после реализации. - **Р-30** (18.10.2026; S-59) — принято: вариант формы документа, принятого по контракту 1.x, определяется по структуре кодов (как у 2.0), а не остаётся пустым. Решения владельца по открытым вопросам планов реализации (18.10.2026, `plans/`): - **Р-31** (план 4 В-2, план 5 Q-3) — если на прогоне реальных файлов (РБ, Счастливочная, НПЗ, Межениновская) форма получает `needs_check` по правилу, которое спека считает законным, исполнитель останавливается и поднимает вопрос владельцу по конкретному случаю; адаптер под файл не подгоняется. - **Р-44** (план 4, ответ на Р-31; 18.10.2026) — числа правее таблицы формы не колонки сумм: при наличии колонки с подписью периода столбец без текста подписи правее последней такой колонки в значения не попадает, вместо `COLUMN_WITHOUT_DATE` — `NUMBERS_OUTSIDE_TABLE` (`info`, адреса ячеек), статус не меняется (пример — Межениновская 2025-Q3, формулы аналитика в U–W). Уточняет P1-17 (№ 120). Цена ошибки: настоящая колонка сумм с потерянной подписью будет отброшена с `info`; поэтому подписанный столбец без чисел границей не служит. Единые для Excel и OCR. - **Р-32** (план 5 Q-1) — успешный json ИС без таблиц даёт `JSON_FAILED`, а не молчаливый пропуск. - **Р-33** (план 5 Q-2) — если все json набора ошибочны, конверт `error` перечисляет `JSON_FAILED` каждого json; код ошибки файла — от первого. - **Р-34** (план 3 O-2) — подтверждение кода строки уверенность кода не меняет: ставится флаг `is_confirmed`, `line_code_confidence` остаётся как пришло, `line_code_modified` не ставится; снятие подтверждения возвращает `needs_review` по прежнему правилу. Спека arm-core приводится к этому. - **Р-35** (план 3 O-5, P3-12) — очистка форм перед выкладкой отказывает только при привязке формы к версии консолидации в статусе «Верифицирована» или дальше; версии в работе, на согласовании, отклонённые и отменённые не мешают. На стенде 18.10.2026 версий дальше «в работе» нет. - **Р-36** (план 0 O-1) — в окне перехода (до перехода сервисов на 2.0) новые примеры контракта верхнего уровня в docs не добавляются. - **Р-37** (план 6 В-1) — экран принимается по подстановке ответа 2.0 из фикстуры и регрессии на 1.x; прогон на aep_3 — follow-up после перехода scan-gateway на 2.0. - **Р-38** (план 6 В-3) — подтверждённая ячейка помечается серой галочкой. - **Р-39** (план 4 В-1) — коды предупреждений ОСВ — по источнику (`OSV_*`), без дробления. - **Р-40** (план 3 O-3) — предупреждения `CODE_NOT_IN_DICTIONARY` / `CODE_NOT_IN_EDITION` пишутся только для четырёхзначных кодов. - **Р-41** (план 3 O-6) — смена `crm_id` ГСК / ГСЗ пишется в аудит как изменение группы. - **Р-42** (план 2 O-3) — `sync-statements --check` добавляется в сборку снимка для контура (`prepare-contour-snapshot.sh`), по аналогии с `sync-web`. - **Р-43** (план 2 O-4) — репозиторий `arm-statements` вносится в таблицу workspace (`CLAUDE.md` workspace и `context/00-workspace-layout.md`) в рамках плана 1. ## Decisions made without discussion Решения агента 17.10.2026, принятые при оформлении целевой картины без отдельного обсуждения с владельцем; все обратимы правкой спеки или словаря. 1. **Уровни предупреждений** (шаг 8, п. 2): `error` — только то, что меняет данные или их полноту; полный список — в шаге 8 п. 2, таблица «какая правка какое `error` снимает» — в «Что уходит из ядра». Цена ошибки: лишний или пропущенный `needs_check`, правится таблицей уровней в спеке. 2. **`DISCLOSURE` всегда `done`** без условий о раскладке, строках и проверках; по `type_hint` шаги 2–7 не выполняются, по кодам прочих форм строки отбрасываются — сохранено текущее поведение парсера. Цена: пояснения и формы 3, 4, 6 не проверяются; так и сейчас. 3. **Столбец кодов** — подпись «Код» принимается при доле кодов под ней ≥ 0,6 или кодах ≥ 3, при нескольких кандидатах — больше кодов; запасной подсчёт «кодов ≥ 3 и доля ≥ 0,6» взят у парсера; сегмент не первый с кодами < 3 — ослабленный поиск по диапазону столбца первого сегмента (как `relaxed` парсера). Цена: таблица без подписи и с одним-двумя кодами в первом сегменте → документ без строк с `needs_check`. 4. **Диапазоны кодов** — явные отрезки по формам (шаг 2), объединение для поиска столбца, «диапазон столбца» для разлепления и продолжений; границы форм 3, 4, 6 уточняются в спеке. Цена: код вне отрезка не распознаётся как код; правится словарём. 5. **Начальный словарь исключений подписей** («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») — по тестам парсера; пополняется словарём, не кодом. 6. **Грамматика `locator`** — пары `ключ:значение` через `;`, ключи по источнику, как сейчас. Цена: нулевая. 7. **Грамматика `ref`** — OCR `row:[#]`, Excel `<лист>!`; `ref` строки уникален в документе. 8. **`value`, который не число** (дата, логическое) в колонке сумм — `amount = null`, `string_value` = текст, `is_recognized = false`. Цена: минимальная. 9. **Предел и квантование суммы** — 16 целых цифр, 2 знака, half-up — только для числового `value` Excel; текст OCR с тремя и более знаками после разделителя остаётся `null` + `string_value` (решение № 30 среза 3a); «догадка из цифр» длиннее 16 — `null`. 10. **`parse_amount` знает источник**: запятая и точка у Excel — десятичные, у OCR — тысячи при группах из трёх цифр. Единственное место, где шаги 2–8 смотрят на `source`. Разбор ОСВ не затрагивается. 11. **`checks` отсутствует у документа без строк**, обязателен у `BALANCE` / `INCOME` со строками. 12. **Правило `detail`**: строка без кода с содержимым под строкой с кодом (родитель продолжается через границу сегмента); ячейка кода без единой цифры — пустая. Цена: строка формы с потерянным кодом под другой строкой скрывается до нарушения §7 у родителя (цель отсутствует — нарушение); лечится назначением кода (Р-13). 13. **Посчитанное соотношение** — непуста цель или хотя бы один член; пустая цель при непустых членах и непустая цель при пустых членах — нарушение; сегодня соотношение с отсутствующей целью пропускается у обоих сервисов и ядра. Цена: формы без подытогов — только с упрощённым набором соотношений, иначе `needs_check`, как и сегодня. 14. **Упрощённая форма** определяется только по структуре кодов (ББ: есть 1600, нет 1100 и 1200; ОФР: есть 2400 или 2110, нет 2100, 2200, 2300) и получает свой набор соотношений и перечень кодов; ОКУД 0710096 признаком не служит. Цена: ошибка определения → не те соотношения → `needs_check`; образца упрощённой формы в fixtures пока нет. 15. **Конвенция знака** для членов `abs_terms`: форма записи у большинства членов `abs_terms` колонки с непустым `amount`, `is_recognized = true` и без правки; проверяется только у ячеек, нуждающихся в покрытии. Цена: ОФР, где сомнительные расходы записаны не так, как остальные, подсвечивает их; это и нужно. 16. **Подтверждение даты отчёта структурой**: неуверенная дата отчёта подтверждается совпадением с самой поздней датой колонок, неуверенная подпись колонки — попаданием в ожидаемые даты; иначе `error`. Цена: скан с верной, но неуверенной датой без колонок с датами — `needs_check`, снимается правкой даты. 17. **Хранить `needs_review`, `text`, подтверждения, нарушения и предупреждения** в ядре (`financial_statement_value`, `financial_statement_line`, `financial_statement_meta`, `document_issue`) вместо пересчёта на каждом GET — пересматривает решение v5.13 «считается, не хранится». Цена: ещё одна таблица и миграция; взамен ядро при чтении ничего не считает, а подтверждения аналитика переживают пересчёт. 18. **Эффективная дата документа** = дата отчёта, иначе самая поздняя абсолютная дата колонок — та же формула, что `statement_period` ядра; от неё считаются относительные подписи и выбирается редакция перечня кодов. 19. **`document_issue`**: очищается при перезагрузке файла, записи `relation` / `soft` заменяются целиком при пересчёте, `line_id` — список; `error` сервиса снимаются только правкой из таблицы соответствия. 20. **Границы пакета `arm_statements`**: свои типы входа и выхода, конвертация в модель контракта на стороне хоста, golden внутри пакета на синтетике, тесты на реальных фикстурах — только в сервисах; расхождение версий копий — `info`. 21. **Алгоритм структурного подтверждения кода**: наибольшая подпоследовательность по порядку бланка, редакция с длиннейшей, пересечение при неоднозначности, уверенные коды — якоря, пятизначные — через родителя. Цена: при двух переставленных неуверенных соседях оба `needs_review`; это и нужно. 22. **Слияние шаблонной строки с назначенным кодом**: шаблонная строка с суммами аналитика — 409 `LINE_EXISTS`, без сумм — заменяется настоящей. Цена: лишний шаг у аналитика в редком случае; двойного счёта нет. 23. **Рабочие имена** `FormCandidate`, `TableLayout`, `RawLine`, `needs_review`, `line_status`, `line_code_text`, `row_index`, `source_ref`, `parent_ref`, `period_kind`, `form_variant`, `document_issue`, `statements_version`, `is_confirmed`, `LINES_UNRESOLVED`, `DUPLICATE_FORM`, `arm-statements`, `sync-statements` — по образцу существующих (`sync-web.sh`); меняются при написании спеки без последствий. ### Решения при написании спеки (18.10.2026) Решения агента при оформлении спеки, контракта v2.0, правил §11 и DBML v5.19 там, где дизайн оставлял «уточняется в спеке» или не давал значения. Маркер `S-n` — в отчёте спеки; все обратимы правкой спеки, словаря или DBML до реализации. 24. **S-1. Имя способности контракта** — дельта к существующей `recognition-contract`, а не новая `recognition-result-contract`. Основание: спека с этим смыслом уже есть в `openspec/specs/`. Цена: нулевая. 25. **S-2. Конверт `recognition-result-message` тоже v2.0**: `metrics.warnings` в конверте — те же структуры, что в файле результата; производитель пишет в конверт тот же MAJOR. Основание: «ack тоже 1.x и 2.0» (шаг 9) и одна форма предупреждений. Цена: ещё одна схема поднимает MAJOR; ядро и так принимает оба. 26. **S-3. Диапазоны кодов** — как в дизайне, без изменений (3100–3699, 4100–4499, 5000–5999, 6100–6499). Цена: код вне отрезка не код; правится словарём. 27. **S-4. Совпадение наименований (Р-16):** нижний регистр, `ё` → `е`, всё кроме букв и цифр — пробел; совпало при равенстве, вхождении более короткой строки (≥ 5 символов) или `difflib.SequenceMatcher` ≥ 0,8 с любым справочным именем кода в любой редакции; нет кода в словаре имён — не совпало. Цена: ложное несовпадение — `NEEDS_CHECK` и ручное подтверждение; ложное совпадение маловероятно у разных статей одного раздела. **Утверждено владельцем 18.10.2026 (Р-22)** с оговоркой: у кода несколько имён (синонимы, Р-20); если одно из имён начинается с «итого по разделу», совпадение — только равенство (S-49). 28. **S-5. Формулы упрощённого набора** по бланку приказа 66н, прил. 5 (ББ: `1600 = 1150 + 1170 + 1210 + 1230 + 1250`, `1700 = 1300 + 1350 + 1360 + 1410 + 1450 + 1510 + 1520 + 1550`, `1600 = 1700`; ОФР: `2400 = 2110 − 2120 − 2330 + 2340 − 2350 − 2410`), все жёсткие; `ASSUMPTION` до образца (OQ-166). Цена: неверная формула — `needs_check` упрощённых форм. **Утверждено владельцем 18.10.2026 (Р-22) с изменением:** ОФР `2400 = …` упрощённого набора — мягкое, как `2400` полного (знак 2410, OQ-12; ревью K-11); соотношения ББ — жёсткие. 29. **S-6. Словари подписей:** кода — «код», «коды», «код строки», «код показателя»; наименования — подпись, начинающаяся с «наименование». Цена: пополняется словарём. 30. **S-7. Прочерк** (`-`, `–`, `—`, `−` без цифр) — пустое значение, `is_recognized = true` (как пустая ячейка, Р-2); OQ-148 — допущение. Цена: тире-мусор на месте цифры не подсвечивается сам по себе, его ловит §7 (пустой член). **Утверждено владельцем 18.10.2026 (Р-22):** OQ-148 закрыт этим решением. 31. **S-8. Каталог кодов предупреждений** (таблица §11.9 `05-domain-rules.md`); `code` в контракте — строка `^[A-Z][A-Z0-9_]*$`, не перечисление (новый код — не MAJOR); `VALUE_OUT_OF_RANGE` — `info` (значение и так `needs_review`). Цена: опечатка кода не ловится схемой — ловится тестами пакета. 32. **S-9. Предупреждение уровня файла** имеет `locator` (json набора ИС, лист). Цена: нулевая. 33. **S-10. `needs_review` полей шапки** — массив `header.needs_review_fields` в контракте и `jsonb` в `financial_statement_meta`; сейчас туда попадает только `report_date`: сомнения в ИНН и организации — `info` по дизайну, `needs_review` у них дал бы `needs_check` вопреки «НПЗ ОФР без ИНН остаётся `done`». Цена: экран подсвечивает ИНН и организацию по `issues` с `field`, а не по `needs_review`. **Утверждено владельцем 18.10.2026 (Р-22)**; формулировка одна во всех документах: `needs_review_fields` — поля, которые держат документ в `needs_check`; сырой текст и уверенность всех полей шапки хранятся (`header_source`, S-33). 34. **S-11. Значения `period_kind`:** `absolute`, `relative:current`, `relative:prev_year_end`, `relative:prev_prev_year_end`, `relative:prev_period`, `none`; «За аналогичный период» — эффективная дата − 1 год (29.02 → 28.02). Цена: новая относительная подпись — новое значение (MINOR). 35. **S-12. Ожидаемые даты неуверенной подписи колонки** — эффективная дата, она же − 1 и − 2 года **и 31 декабря предыдущего и предшествующего ему года**. Основание: у квартального ББ колонки «на 31.12 прошлого года», и набор дизайна (дата, − 1 год, − 2 года) давал бы `error` на каждой такой форме. Цена: чуть шире окно подтверждения. **Утверждено владельцем 18.10.2026 (Р-22).** 36. **S-13. Строка нумерации** — не меньше трёх непустых ячеек, все целые, подряд с 1 слева направо. Цена: нумерация с пропуском не распознаётся (строка уйдёт в данные со статусом). **Утверждено владельцем 18.10.2026 (Р-22) с оговоркой:** двойники внутри детектора и допуск одного пропуска — уточнено S-40. 37. **S-14. Признак повторной шапки** — подпись кода в строке или ≥ 2 непустых ячеек, совпавших с шапкой первого сегмента. Цена: шапка без «Код» и с другими подписями не начнёт сегмент. **Утверждено владельцем 18.10.2026 (Р-22).** 38. **S-15. `ref` и `row_index`:** `row_index` с 0; Excel `ref` строки — адрес ячейки столбца кодов (без столбца — первой непустой ячейки); разлепленные `row:#` с k от 1; у разлепленных строк `line_code_text` и `text` — k-е части. Цена: нулевая. Оговорка Р-22: имя листа в Excel-`ref` экранируется по правилу Excel (кавычки, удвоенный апостроф), примеры контракта исправлены. 39. **S-16. Уверенность кода с заменой двойника** — не выше 99 (так «неуверенный» виден из одного поля). Цена: нулевая. 40. **S-17. Формат PATCH `lines`** — один элемент = одно действие (`line_code` | `line_status: "detail"` | `confirmed`); коды 422 `INVALID_LINE_CODE`, `LINE_CODE_REQUIRED`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `UNKNOWN_COLUMN`; правка суммы по `line_code` при наличии строки `ok` — 422 `UNKNOWN_CELL`. Цена: имена кодов меняются до реализации без последствий. Оговорка Р-22: доопределено по K-23 (S-47); 409 `LINE_EXISTS` переименован в `LINE_CODE_TAKEN` (S-43). 41. **S-18. Детали 409:** `LINES_UNRESOLVED` — `detail.lines[] {line_id, row_index, line_code_text}`; `DUPLICATE_FORM` — `detail.document_id`. Цена: нулевая. 42. **S-19. Имена DBML v5.19:** `document.file_ordinal` (с 0, уникален в файле), `financial_statement_meta.needs_review_fields` (`jsonb`), `document_issue.line_ids` (`jsonb` — сверка ORM↔DBML не знает массивов), enum'ы `statement_line_status`, `statement_form_variant`, `document_issue_kind`, `issue_severity`, `issue_source`; индекс `(meta_id, line_code)` остаётся неуникальным; у строки `is_confirmed` без `confirmed_by/at` (строго по дизайну, кто — в аудите). Новые колонки — последними в таблицах (как их добавит миграция). Цена: нулевая до миграции. Оговорка Р-22: у `document_issue` добавлены `codes` и `column_index` (S-31, S-35); висячий `line_id` удалённой шаблонной строки убирается из `line_ids`. 43. **S-20. `related_group.crm_id` уникален** (как у ГК и ЮЛ), повтор — 409 `CRM_ID_EXISTS`. Цена: две группы с одним ID CRM завести нельзя; так и должно быть. **Утверждено владельцем 18.10.2026 (Р-22).** 44. **S-21. Значения по умолчанию при приёме 1.x:** `ref = null`, `needs_review` строки `false`, `period_kind` — `absolute` при дате иначе `none`, `form_variant = null`, строки-предупреждения — `LEGACY_WARNING` `info`. Цена: на окне выкладки документы 1.x без новых сигналов качества. Оговорка Р-22: по K-2 / K-21 — S-36. 45. **S-22. Статус документа ОСВ** — `done`, когда контрольные суммы §1.4 листа сошлись, иначе `needs_check` (прежнее правило конверта, применённое к листу). Цена: нулевая — конверт ведёт себя как сейчас. **Утверждено владельцем 18.10.2026 (Р-22)**; формулировка «контрольные суммы §1.4 листа сошлись» теперь одна в контракте, §11, спеках `recognition-contract` и `tabular-recognition-service`. 46. **S-23. `service.statements_version` обязателен**, если в результате есть `statementDocument` (схема проверяет). Цена: результат только с ОСВ может его не нести. 47. **S-24. Редакция перечня кодов в сервисах** — `fsbu4` при эффективной дате после 2024-12-31 (то же правило, что `edition` ядра, OQ-145). Цена: досрочный ФСБУ 4/2023 — проверяются обе через «длиннейшую подпоследовательность». Уточнено S-26: проверяются всегда обе, дата решает только ничью. 48. **S-25. Квартал периода (условие `RECOGNIZED`):** дата не конец квартала → `NEEDS_CHECK` (как D-6), ручное подтверждение — 409 `PERIOD_REQUIRED`. Основание: оговорка дизайна «иначе документ `RECOGNIZED`, но не подтверждаемый» противоречит автоподтверждению `RECOGNIZED`. Цена: нулевая — поведение как сейчас. **Утверждено владельцем 18.10.2026 (Р-22).** ### Решения при доработке спеки по ревью K-1…K-25 (18.10.2026) Решения агента при закрытии находок `review-2026-10-18-spec.md` там, где ни ревьюер, ни владелец не задали значение, или где агент отступил от предложения ревьюера. Маркер `S-n` продолжает нумерацию; все обратимы правкой спеки, словаря или DBML до реализации. 49. **S-26. Редакции перечня кодов — всегда обе** (K-5): для неуверенного кода считаются наибольшие подпоследовательности по `66n` и `fsbu4`, берётся длиннейшая, эффективная дата решает только ничью. Основание: Межениновская за 3 кв. 2025 сдана на бланке 66н — выбор только по дате (S-24) не подтвердил бы её 1120; формула дизайна «редакция — где длиннее, при равенстве — по дате» иначе не имеет смысла. Цена: на ББ с кодами, общими для обеих редакций, выбор безразличен; неверный выбор возможен только при равной длине — тогда решает дата, как в S-24. Уточнено S-65 (лучшая подпоследовательность — сначала по числу уверенных кодов). **Утверждено владельцем 18.10.2026 (Р-25).** 50. **S-27. Перечень кодов — данные в docs** (K-5): источник — `contracts/statement-code-orders.yaml` (обращение «как контракт»: общие данные сервисов и ядра), в пакете — байтовая копия `rules/statement_code_orders.yaml`, идентичность проверяет `sync-statements --check`; форма файла — `scripts/validate_contracts.py`, для этого в `make venv` docs добавлен PyYAML. Цена: ещё одна зависимость docs-проверок (`make venv` нужно перезапустить); перенос источника в `arm-statements` — правкой одного требования. 51. **S-28. Состав перечня — только подтверждённые образцом коды** (K-5): шаблоны банка + коды печатных форм 1С и сканов из `fixtures/` (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; раздел III НКО (1310 «Паевой фонд» … 1370, 1300 — aep_3) — законный повтор `66n`; клиентский 1231 — в `66n` (Унагранде 2024). Коды без образца (1120 у `fsbu4`, 2421 / 2430 / 2450 у `66n`, упрощённые перечни) — OQ-168, OQ-166 (временное правило утверждено владельцем — Р-26). Все 12 последовательностей кодов из `fixtures` (Excel и OCR) укладываются в свою редакцию целиком. Цена: на скане с неподтверждённым кодом и неуверенностью ниже 100 — `needs_review` строки; у Excel и уверенного кода — только `info`. 52. **S-29. Окно перехода контракта** (K-8): схемы 1.2 / 1.0 лежат в docs как `*.v1.schema.json` (байтовые копии, сервисы сверяются с ними без правки копий), примеры — `examples/v1/`; тест идентичности сервиса выбирает файл по `$id` копии (`…/v1` → `*.v1.schema.json`) — это шаг 0 выкладки в tabular-parser и scan-gateway до слияния docs. Отступление от предложения ревьюера «тест сверяет с MAJOR, который сервис пишет»: признак — `$id` копии, а не настройка сервиса, чтобы правило работало без знания кода сервиса. Цена: маленький PR в двух сервисах до слияния docs; без него их `make check` красный на окне. **Заменено S-53** (повторное ревью L-2: файла `.v1` у запроса нет, примеры и ядро не были покрыты). 53. **S-30. Механика `sync-statements`** (K-9): копия — `api/arm_statements/` (импорт как `src`), тесты пакета в `testpaths` хоста, `mypy src arm_statements`; маркер — файл `api/arm_statements/VERSION` с полным хэшем; `--check`: `0` синхронно, `10` отстала (зелёный с предупреждением — иначе любой посторонний PR в сервис требовал бы подтягивать чужие изменения пакета), `20` прямая правка / чужая версия / расхождение перечня кодов / код словаря ядра вне перечня, `2` нет checkout или аргумент. Без checkout `arm-statements` `check-full` красный (код 2): это проверка перед PR, а не `make check`. Цена: отставшая копия проходит `check-full`; видна в описании PR. 54. **S-31. `column_index` — номер столбца сетки кандидата** (K-6): у OCR — `column_index` ИС как есть (разреженный), у Excel — номер столбца листа первого сегмента с 0; у `document_issue` — `column_id` для колонки формы и `column_index` для столбца вне колонок; `line_refs` — только строки `lines[]`. Отступление от варианта «порядковый номер колонки значений»: иначе пропущенная колонка (`COLUMN_SKIPPED`) не имела бы номера. Примеры контракта приведены к правилу (Excel — 3, 4; OCR — 2). Цена: номера не с 0 и не подряд — экран и ядро не должны считать их позициями. Дополнено S-56 (колонки сегментов со своей шапкой). 55. **S-32. `TABLE_PARSE_ERROR` и `SHEET_OTHER` — предупреждения файла** (K-7): с `locator`; документа нет; `TABLE_PARSE_ERROR` (`error`) блокирует `RECOGNIZED` всех документов файла, как `JSON_FAILED`. Цена: один упавший разбор таблицы держит весь файл на ручном подтверждении. 56. **S-33. Шапка: `header_source jsonb` + прежние `*_original_value`** (K-3): сырой текст и уверенность всех десяти полей — одной колонкой `{поле: {text, confidence}}`; `inn_original_value`, `report_date_original_value`, `units_original_value` заполняются как прежде, `inn_confidence` остаётся. Отступление от варианта «колонки на каждое поле»: 20 колонок ради подсказки на экране. Цена: поиск по сырому тексту поля шапки в SQL — через `jsonb`. 57. **S-34. `organization_name` в PATCH шапки** (K-12): непустая строка до 1000 символов, `organization_name_modified`, `organization_name_original_value` (новые колонки DBML), пересчёт `counterparty_check`, аудит; снимает `NO_ORGANIZATION_NO_INN`. Выбран вариант «добавить», а не «убрать из правила снятия»: иначе форма без шапки снималась бы только перезагрузкой. Цена: правка наименования влияет на сверку контрагента по алиасам. **Утверждено владельцем 18.10.2026 (Р-25).** 58. **S-35. Коды нарушений в `document_issue`** (K-13): `relation` → `RELATION_FAILED` / `error`, `soft` → `RELATION_SOFT` / `info`, колонка `codes jsonb`, `line_ids` — все строки `ok` кодов. Цена: нулевая. 59. **S-36. Формы без `checks`** (K-2, K-21): у результата 1.x и у форм до миграции ядро один раз считает §7 копией пакета (`form_variant = null` — полный набор) и пишет `document_issue` (`source = core`): при приёме 1.x и **в ревизии миграции** (ревизия импортирует копию `arm_statements`). D-6 «проверки сошлись» для 1.x — из этого расчёта; `GET` и `confirm` читают нарушения только из `document_issue`. Предупреждения маппера 1.x по документу — `MAPPING_WARNING` `info`; строки `metrics.warnings` 1.x — `LEGACY_WARNING` в `recognition_job.metrics` (уровень файла, не `document_issue`); `STATEMENTS_VERSION_MISMATCH` при `statements_version = null` не пишется. Цена: ревизия миграции зависит от кода пакета на момент миграции (старая ревизия при смене пакета считает по новым правилам — для разовой миграции приемлемо); отказ от миграционного расчёта дал бы потерю нарушений у существующих форм (K-2). **Утверждено владельцем 18.10.2026 (Р-25) в редакции Р-24:** миграционного расчёта нет — база перед выкладкой очищается от форм; расчёт при приёме 1.x остаётся (вариант формы — по структуре кодов, S-59). 60. **S-37. Признак строки правки — `row_index IS NULL`** (K-1): маппер 1.x и 2.0 всегда пишет `row_index`; миграция ставит `NULL` строкам с `line_code_original_value IS NULL`; `line_code_original_value` — код из источника, правками не меняется. Отступление от дизайна («смена кода — прежний в `line_code_original_value`»): при первой смене это то же значение, а при повторной правке исходный код источника не теряется. Цена: «предыдущий код» после двух правок — только в аудите. **Утверждено владельцем 18.10.2026 (Р-25).** 61. **S-38. Функция статуса и «содержимое» строки** (K-14): вход — данные без типов источника (`layout_found`, у ядра — есть строка из источника; `has_organization_or_inn`; `period_known`; статусы и `needs_review`; число жёстких нарушений; `accepted`; уровни предупреждений). Содержимое строки — цифра в ячейке кода, сумма в значениях или непустая ячейка значений с уверенностью ниже 100; строка `unreadable` без значений («44 30\nI |» aep_6) — строка данных: документ `needs_check`, подтверждение не блокирует. Цена: такая строка требует от аналитика «Это расшифровка» или кода, хотя сумм в ней нет. 62. **S-39. Столбец кодов ищется всегда, и у `DISCLOSURE`** (K-10): `type_hint = DISCLOSURE` при кодах ББ/ОФР → тип по кодам + `TYPE_CONFLICT`; у `DISCLOSURE` шаги 3–7 не выполняются, `columns` / `lines` — пустые массивы. Цена: лишний проход поиска столбца по пояснениям. 63. **S-40. Детектор нумерации** (S-13 с оговоркой Р-22, K-15): ≥ 3 непустых ячеек, каждая после замены двойников — целое; возрастают на 1, начинаются с 1 или 2, допускается один пропуск. Цена: настоящая строка данных из трёх маленьких целых подряд («1 | 2 | 3» в суммах) станет нумерацией — на формах ББ/ОФР не встречалась. 64. **S-41. `LINES_UNRESOLVED` — сумма или текст** (K-16): блокирует строка `unreadable` / `missing`, у которой хотя бы в одной ячейке `amount` или `string_value` не `null`. Цена: строка с мусором вместо суммы требует разбора. **Утверждено владельцем 18.10.2026 (Р-25).** 65. **S-42. Подтверждение неуверенной даты отчёта** (K-17): PATCH `header.report_date` тем же значением у поля из `needs_review_fields` снимает его и `REPORT_DATE_UNCERTAIN`; у бланка 66н только с относительными подписями это единственный путь — признано в §11.9. Цена: аналитик должен явно подтвердить дату на каждом таком бланке. 66. **S-43. Имена** (K-18): 409 `LINE_EXISTS` → `LINE_CODE_TAKEN` (имя занято сообщением 422 `UNKNOWN_CELL` в коде ядра); `period_end` колонки → `period_date` во всех документах (как в контракте и DBML); порядок `lines` в API — «по возрастанию кода, как сейчас», «как на бумаге» — раскладка экрана; `DISCLOSURE` — пустые массивы. Цена: нулевая. 67. **S-44. Откат миграции** (K-19): гарантирован на пустой и совместимой с v5.18 БД; на несовместимых данных `downgrade` останавливается с ошибкой и ничего не удаляет. Цена: откат после появления данных v2.0 — только восстановлением из резервной копии. 68. **S-45. «Того же периода»** (K-20) — та же пара `period_year`, `period_quarter` (как автозамена §9.5 п. 3); формы без квартала не считаются (их и так держит `PERIOD_REQUIRED`). Цена: нулевая. 69. **S-46. Единицы и ОКЕИ** (K-22): словарь `statement_units.yaml` в пакете (написания и коды ОКЕИ 383 / 384 / 385); единицы по тексту, иначе по ОКЕИ; `okei` (+ `_text`, `_confidence`) — в `header` контракта v2.0 и `headerField`. Цена: ОКЕИ, противоречащий тексту единиц, не ловится (текст главнее). 70. **S-47. Пробелы PATCH** (K-23, S-17): `confirmed: false` снимает подтверждение ячейки и кода; `line_status` принимает только `"detail"` (`"ok"` — 422 `LINE_CODE_REQUIRED`); обратного `detail` → `unreadable` / `missing` нет (исправляется назначением кода, исходное — перезагрузкой); `columns[].period_date = null` — 422 валидации; **акцепт сбрасывает правка данных** (сумма, код, статус строки, дата колонки, поле шапки), подтверждения (`confirmed`) — нет. Последнее сужает правило Среза 3b «акцепт сбрасывается любой правкой формы» — вынесено владельцу (отчёт O-n). Цена: если владелец считает подтверждение правкой — одна строка спеки. **Утверждено владельцем 18.10.2026 (Р-25):** подтверждение без правки акцепт не сбрасывает. 71. **S-48. Страховка `done`** (K-24): `done` из результата при строке `unreadable` / `missing` со значением, `needs_review`, `relations_failed` или `error`-предупреждении документа → `NEEDS_CHECK` и `RESULT_STATUS_INCONSISTENT` `info`. Цена: проверка на каждом маппинге — линейная по строкам. **Утверждено владельцем 18.10.2026 (Р-25) в редакции L-12:** согласованность — вызов общей функции статуса по входу из результата (не список признаков). 72. **S-49. Словарь имён ядра** (Р-20, S-4): повтор кода в редакции разрешён — все имена кода синонимы; шаблонная строка и `reference_name` берут первое; «итого по разделу» — только равенство. Цена: справочное имя законного повтора на экране — первое имя кода, файловое — рядом. 73. **S-50. Толкование Р-21:** мягкое соотношение, сошедшееся в пределах ±1, покрывает значения любого своего кода; у кода в несошедшемся жёстком соотношении подсветка остаётся (правило «все ячейки несошедшегося жёсткого»), поэтому на практике Р-21 действует на строки без жёстких соотношений, как сформулировал владелец. Цена: нулевая. 74. **S-51. Ожидания на реальных сканах признаны в §11.9 и proposal** (K-4): большинство из шести OCR-документов останутся `needs_check`; формулировка гайда фронта смягчена («может стать `RECOGNIZED`»). Цена: нулевая. **Пересмотрено Р-23:** значения `detail` статус не держат, ББ и ОФР РБ и Счастливочной за 1 кв. 2025 могут стать `done`; держат `needs_check` aep_3 и aep_6. 75. **S-52. Манифест tabular-parser** (K-25): в спеке `tabular-recognition-service` перечислены записи, получающие `1600 = 1700` (страницы 2 баланса ×3), и записи, чьи статусы `done` меняться не должны. Цена: нулевая. ### Решения при доработке спеки по повторному ревью L-1…L-30 (18.10.2026) Решения агента при закрытии находок `review-2026-10-18-spec-2.md` там, где ни ревьюер, ни владелец (Р-23…Р-26) не задали значение, или где агент отступил от предложения ревьюера. Маркер `S-n` продолжает нумерацию; все обратимы правкой спеки, словаря или DBML до реализации. 76. **S-53. Правило теста идентичности копии контракта** (L-2; заменяет S-29): схема копии с `$id` `…/v` сверяется с `<имя>.v.schema.json` docs, **если такой файл есть**, иначе с основным; пример — с `examples/v/<имя>.json`, если он есть, иначе с `examples/<имя>.json` (`M` — `MAJOR` копии схемы вида примера, вид — по префиксу имени); прочие файлы копии (`.v`, `examples/v/` у ядра) — с одноимёнными; файлы docs другого `MAJOR` сервис не сверяет. Правило вводится в шаг 0 у **всех трёх** потребителей, включая ядро. Основание: существующие тесты (`arm-core/api/tests/ test_contract.py`, `tabular-parser` и `scan-gateway` `api/tests/unit/test_contract.py`) сравнивают весь набор `*.json` через `rglob`, включая `examples/`; правило «файл, если есть» проходит до слияния docs (файлов `.v1` нет), на окне и после, у запроса без `.v1`. Цена: три маленьких PR до слияния docs; удаление `MAJOR` 1 — в порядке «сначала ядро убирает копии `.v1`, затем docs». 77. **S-54. `file_ordinal` у всех документов** (L-3): индекс документа в `documents[]` результата при любом `MAJOR`, у ОСВ, форм, `DISCLOSURE` и `OTHER`; несозданный документ (второй лист ОСВ с тем же счётом) номер не занимает и других не сдвигает — номера с пропусками. Цена: номер не «порядковый без дыр»; экран его не показывает. 78. **S-55. Предупреждения файла и ОСВ в ядре** (L-4): `recognition_job.metrics.warnings` и `JobBrief.warnings` — структуры `fileWarning`; строки старых заданий конвертируются при чтении в `LEGACY_WARNING` (данные не переписываются); `JobBrief.mapping_warnings` — строки, как были. Предупреждения листа ОСВ 2.0 — `document_issue` документа ОСВ, отдаются в новом `DocumentOut.issues[]`. `osvDocument.status` используется: ОСВ 2.0 `RECOGNIZED` ⇔ `status = done` ∧ `checks` чисты ∧ нет `error` файла ∧ контрагент не `MISMATCH` ∧ нет предупреждений маппинга листа; статус конверта не входит (как Р-19 для форм). Отступление от гайда «поля только добавляются»: тип `JobBrief.warnings` меняется — записано в гайде фронта. Цена: фронт обязан перейти на объекты в той же выкладке, что и ядро; ОСВ в книге с сомнительной ОФР теперь автоподтверждается (раньше держалась статусом задания). 79. **S-56. Колонки сегмента со своей шапкой** (L-6): `columns[]` задаёт первый сегмент; колонки следующего сегмента со своей шапкой сопоставляются с ними по `period_date` (если у каждой колонки сегмента дата есть и совпадает ровно с одной), иначе по порядку; при сопоставлении по порядку другое число колонок — `SEGMENT_COLUMNS_MISMATCH`, лишние столбцы сегмента в `columns[]` не попадают, их номера — в `message`. Цена: сегмент с переставленными колонками без дат сопоставится по порядку неверно — §7 это поймает; НПЗ (лист 4 ББ со своей шапкой) остаётся `done`. 80. **S-57. Явное правило мусора в `parse_amount`** (L-7): «ядро числа» от первого «цифра, `(`, знак» до последнего «цифра, `)`, `}`, `]`»; всё вне ядра — краевой мусор, выбрасывается с `is_recognized = false`; в ядре — только цифры, пробелы, `,`, `.`, знак в начале и пара скобок вокруг числа, иначе `amount = null` + `string_value` (буква в ядре, непарная скобка, символ между цифрами); крайние `|` удаляются до замены двойников; `{` и `[` открывающей скобкой не считаются. Так `211(1 |` и `(Ю4)` дают `null`, `: : 2856983 = ■` — 2856983, `{38378)` — `null` (сценарий «знак вне конвенции» переписан на значение без знака). Отступление от сегодняшнего `amounts.py` (выбрасывал всё, кроме цифр): осознанно. Цена: «12:34» даёт `null` и требует внимания аналитика; «1 234 руб.» даёт 1234 с `is_recognized = false` (буквы после последней цифры — краевой мусор, значение нуждается в покрытии §7). 81. **S-58. Дрейф перечня кодов — код 10, не 20** (L-8): `sync-statements --check` в хосте сверяет копию перечня с docs, расхождение — `10` (предупреждение), а не `20`; `20` — только прямая правка копии, чужая версия, недостающая зависимость пакета и (в ядре) код словаря имён вне перечня **копии**. Порядок правки перечня: docs → `arm-statements` → хосты. Цена: копия с устаревшим перечнем проходит `check-full` с предупреждением; видна в описании PR. 82. **S-59. Точка входа пакета для пересчёта** (L-10): `StoredForm` (без сетки) → `{form_variant, edition, checks, needs_review строк и значений, родители detail, status}`; признаки `code_fixed` / `value_fixed`, строки `row_index = null` участвуют в §7, `needs_review_fields` — вход (пересчёт шапку не переоценивает). Ядро вызывает её после PATCH и при приёме 1.x; вариант формы при приёме 1.x — **по структуре кодов**, а не `null` (уточняет S-36: иначе упрощённая форма 1.x получила бы нарушения полного набора до первой правки, а после правки — другой набор). Цена: у формы 1.x `form_variant` может отличаться от «полного по умолчанию»; образца упрощённой формы нет (OQ-166). 83. **S-60. «Итого по разделу» — сравнение номеров раздела** (L-11): номер — начальные буквы `i`/`v`/`x` первого слова после фразы (хвост OCR «V1», «V /» отбрасывается), `strip` явно; номера равны ⇔ совпало; номер не выделен — равенство строк. Отступление от S-49 («только равенство»): ломалось на «Итого по разделу V1» Счастливочной. Цена: «II», прочитанное как «I1», даст I ≠ II — ложное `NAME_MISMATCH` (безопасная сторона). 84. **S-61. «изо :» — пустой код, правило не меняется** (L-14): двойники цифрами в ячейке кода без цифр не считаются (иначе слова, затёкшие в столбец кодов, стали бы `unreadable` и блокировали подтверждение); пример в §11.4 исправлен. Цена: потерянная строка 1130 aep_3 («изо :», `'3'` с уверенностью 0) уходит в `detail` и скрыта; её сумма не входит в 1100, и `1100 = …` подсвечивает раздел, если сумма больше допуска ±1. 85. **S-62. Родитель `detail`** (L-15): ближайшая строка `ok` выше (`unreadable` / `missing` пропускаются; нет `ok` выше — строка `missing`); после каждого PATCH ядро пересчитывает родителей **всех** строк `detail`; нет `ok` выше — родитель `null`, статус остаётся `detail`. Цена: строка `detail` без родителя на экране — в начале раздела. 86. **S-63. Дата колонки руками** (L-16, L-17): ручная дата ставит `period_kind = absolute`; дата, равная текущей, у колонки с `COLUMN_DATE_UNCERTAIN` / `COLUMN_WITHOUT_DATE` — подтверждение (аналог S-42), снимает предупреждение и акцепт не сбрасывает. Цена: после ручной даты относительная колонка не следует за правкой даты отчёта — так и нужно. 87. **S-64. Подтверждение дат только уверенными источниками** (L-18): для неуверенной подписи колонки эффективная дата — дата отчёта с уверенностью 100, иначе самая поздняя абсолютная дата колонок с подписью уверенности 100; неуверенная дата отчёта подтверждается только датами уверенных подписей. Для периода и относительных подписей эффективная дата — по всем источникам. Цена: скан, где и дата отчёта, и все подписи неуверенны, — всегда `needs_check` с подтверждением даты руками. 88. **S-65. Структурная проверка кода — уточнения** (L-19): «лучшая» подпоследовательность — сначала по числу уверенных кодов (якоря), затем по длине; коды вне перечня редакции в её последовательность не входят; противоречащие якоря — входит наибольшее согласованное их множество; неуверенный код подтверждён, если входит во все лучшие; вариант формы — по всем строкам `ok` и пересчитывается ядром после правки кода. Цена: при перепутанных уверенных соседях неуверенный код между ними может остаться неподтверждённым — `needs_review`, безопасная сторона. 89. **S-66. Форма знака** (L-20): по результату разбора (скобки, включая `}` / `]`; минус; без знака); сама ячейка в голосование не входит; нужно строгое большинство, равенство — не подтверждён. Цена: колонка с двумя голосующими разной формы не подтверждает знак ни одной сомнительной ячейки. 90. **S-67. Редакция для проверок ядра** (L-21): `CODE_NOT_IN_EDITION` и `CODE_NOT_IN_DICTIONARY` — по редакции точки входа (лучшая подпоследовательность по всем строкам `ok`, ничья — по дате); `edition` ответа API и шаблонные строки — по дате, как сейчас. Цена: на бланке 66н, сданном в 2025 году, шаблонные строки на экране — по `fsbu4`, а проверки — по `66n`; сведение — отдельной задачей при необходимости (O-1 отчёта). 91. **S-68. `OTHER` удалён из v2.0, `form_variant` обязателен** (L-22): в схеме v2.0 `statementDocument.document_type` — `BALANCE` | `INCOME` | `DISCLOSURE`, `form_variant` — в `required` (`null` у `DISCLOSURE`); `OTHER` остаётся в схеме 1.x и в `document_type` ядра. Цена: нулевая — производителей `OTHER` в 2.0 нет. 92. **S-69. Откат миграции — точная формулировка** (L-24; уточняет S-44): `downgrade` останавливается только на данных, которые v5.18 не может представить без потери первичных данных и ручного ввода (повтор кода, строка без кода, две формы одного типа в файле, непустой `crm_id` групп, правленое наименование организации); производные и сигнальные колонки v5.19 и `document_issue` удаляет с записью числа удалённых значений в журнал миграции (подтверждения — в аудите). Цена: откат после работы аналитиков теряет подтверждения ячеек и кодов как признаки (история — в аудите). 93. **S-70. Механика копии пакета — уточнения** (L-25): `VERSION` из сравнения исключён (в источнике его нет); зависимости пакета из его `pyproject` обязаны быть в зависимостях хоста (иначе `--check` = 20); пакет проходит самый строгий конфиг `ruff` / `mypy` среди хостов; тесты пакета со своим `conftest`, `autouse`-фикстуры хоста ограничены его каталогом `tests/`. Цена: пакет живёт по самым строгим правилам всех хостов. 94. **S-71. Содержимое таблицы ИС и `TABLE_SKIPPED`** (L-27): содержимое — хотя бы одна ячейка с цифрой или «цифрой под видом буквы»; таблица только из текста без столбца кодов и типа — новое предупреждение файла `TABLE_SKIPPED` (`info`), а не `TABLE_LOST` (`error`). Цена: текстовая таблица ИС не блокирует автоподтверждение; если в ней была форма без единой цифры — такой формы не бывает. 95. **S-72. «Цифра под видом буквы» — содержимое строки** (L-30): ячейка значений, текст которой после замены двойников без условия соседства содержит цифру и не содержит других букв (`'I'`, `'| О'`), делает строку строкой данных; текст из настоящих букв при уверенности 100 (подписи под формой Excel: «Руководитель … Косарев», «(расшифровка подписи)» у Межениновской) — нет: иначе каждая книга получала бы мусорные строки `detail`. Признано осознанным исключением из «ничего не выбрасывать молча». Цена: текстовая строка без цифр с уверенностью 100 по-прежнему теряется молча. 96. **S-73. Очистка форм перед выкладкой — детали** (Р-24): скрипт ядра `scripts/purge_statements.py`, `--dry-run` по умолчанию, `--apply` одной транзакцией; отказ без изменений, если форма упомянута в `ex_snapshot_item` или привязана к версии не «в работе»; файлы только с формами удаляются целиком (как `DELETE /files/{id}`, без запрета `FILE_HAS_CONFIRMED_DOCUMENTS`, задания — тоже); в смешанных файлах «ОСВ + формы» удаляются только формы, файл остаётся — повторно его не загрузить (дедуп `sha256`), поэтому `--dry-run` перечисляет их и владелец решает про `--include-mixed`; системный аудит `FILE_DELETE`. Цена: подтверждённые формы и их привязки к версиям «в работе» исчезают до повторной загрузки; КФО этих версий пересчитывается. 97. **S-74. `needs_review` значений после выхода строки из `detail`** (Р-23): строка, которой назначили код, получает `needs_review` значений по общему правилу (покрытие §7); строка, отмеченная `detail`, — `false`; значения `unreadable` / `missing` считаются по общему правилу (документ и так `needs_check` по статусу строки). Цена: нулевая. ### Решения при планировании и исполнении плана 0 (18.10.2026) 98. **P0-1. Правило теста идентичности — копией в трёх репозиториях**: правило живёт файлом `api/tests/contract_identity.py` + сценарный тест, побайтно одинаковыми в arm-core, tabular-parser и scan-gateway, а не общим пакетом и не скриптом docs; эталон — копия arm-core, расхождение ловит `shasum` (план 0, задача 5) и ревью; соседние репозитории в `make check` не читаются. Цена: правка правила — три PR; разошедшиеся копии до сверки не видны. 99. **P0-2. «Копия держит MAJOR N»**: в копии есть хотя бы один файл `*.v.schema.json` или `examples/v/*.json`; тогда все такие файлы docs обязательны в копии. Цена: ядро, положившее один файл `.v1`, обязано положить все. 100. **P0-3. Неопределимая пара — ошибка**: пример с неизвестным префиксом, схема без `$id` вида `…/v`, пример вида, схемы которого нет в копии — ошибка теста с именем файла, а не пропуск. Цена: новый вид примера в docs требует правки правила. 101. **P0-4. `ARM_KA_DOCS` в scan-gateway**: тест идентичности scan-gateway читает `ARM_KA_DOCS` (спека называет переменную); поиск `fixtures` не меняется. Цена: расхождение в `dev/checks.md` сужается до fixtures. 102. **P0-5. OpenSpec change `contract-identity-window` в tabular-parser и scan-gateway**: MODIFIED требования «Копия контракта идентична источнику» (прежний текст «байт в байт равны `../docs/contracts/`» на окне неверен); arm-core change не получает — правило описано в спеке docs `recognition-contract`. Цена: у arm-core при закрытии нет `openspec archive`. 103. **R-1 (план 0). Дорожку плана 0 исполняет фоновый агент на sonnet**: а не сама сессия: сессия координирует четыре параллельные дорожки (планы 0, 1, 3А, 6). Метод — executing-plans (Native, ≤5 задач), ревью всей ветки — opus. Цена: слабее контекст сессии при финальных рулингах; страхует ревью и таблица находок. 104. **P1-1.** Конфиг линтеров пакета — только в корневом `pyproject.toml` репозитория `arm-statements` (в дерево `arm_statements/` не входит и в хост не копируется): `ruff.toml` внутри `arm_statements/` по иерархии ruff перекрыл бы конфиг хоста, а спека требует «конфиг хоста применяется к копии». Код пакета написан под самый строгий конфиг, так что он проходит любой хост. Цена: «конфиг линтеров вместе с копией» (формулировка спеки) читается как «код, проходящий конфиг», а не как файл; см. O-1. 105. **P1-2.** Версии инструментов пакета — пины scan-gateway (`ruff==0.16.10`, `mypy==2.4.0`, `pytest==9.1.1`): это единственный хост с явными пинами; у tabular-parser и arm-core `>=`, в их окружениях встречаются ruff 0.16.10–0.17.1. Цена: новая версия ruff в хосте может найти новое замечание в копии; см. O-2. 106. **P1-3.** `PLR2004` в тестах пакета не отключается: `per-file-ignores` хостов (`tests/**` у scan-gateway) на копию `api/arm_statements/tests/` не распространяются. Цена: числа в тестах — кортежами и `D("…")`. 107. **P1-4.** Ничья редакций при `effective = None` — `edition = None`, неуверенный код подтверждён только при подтверждении в обеих редакциях. Цена: лишняя подсветка кода у формы без даты (она и так `needs_check` — `PERIOD_UNKNOWN`). 108. **P1-5.** «Книга xlsb без числовых форматов» шаг 4 узнаёт по предупреждению адаптера `XLSB_NO_FORMATS` в `FormCandidate.warnings` — отдельного поля у кандидата спека не задаёт. Цена: адаптер Excel обязан класть это предупреждение в кандидат каждой формы книги xlsb. 109. **P1-6.** У `DISCLOSURE` предупреждения шапки не выдаются, статус всегда `done` (спека: «`DISCLOSURE` — `done` без условий»). Цена: пустая шапка пояснений не видна предупреждением. 110. **P1-7.** Код Excel из текстовой ячейки с заменой двойника — неуверенный (≤ 99) и проходит структурную проверку, как OCR; «у Excel `needs_review` по структуре не ставится» прочитано как «у чистых кодов Excel» (уверенность 100). Цена: редкий текстовый код «I1О0» в Excel может подсветиться. 111. **P1-8.** Строка правки (`row_index = None`) у ядра участвует в §7 и в выборе варианта, только если у неё есть сумма; в структурной последовательности и как родитель `detail` — нет (позиции в файле нет). Цена: код правки не служит якорем для подтверждения соседних неуверенных кодов. 112. **P1-9.** Двухстрочный заголовок склеивается, когда подпись верхней строки отдельно — не период, а «верхняя + нижняя» — период («На 30 сентября» + «2025 г.»; нижняя «2025 г.» сама по себе дала бы ошибочное 31.12.2025). Цена: заголовок в два ряда без даты не склеивается (колонка всё равно остаётся по числам); если над строкой заголовка стоит посторонний текст, который вместе с нижней подписью случайно читается как период, подпись склеится. 113. **P1-10.** Текстовая сумма без пробелов длиннее 16 целых цифр — `amount = null`, `string_value` = текст, `is_recognized = false`, без `VALUE_OUT_OF_RANGE` (предупреждение — только для числового `value` Excel, как в спеке). 114. **P1-11.** `COLUMN_DATE_UNCERTAIN` проверяется только у абсолютных подписей: относительные даты вычисляются от эффективной даты и в ожидаемые входят по построению. 115. **P1-12.** Поля шапки, правила которых спека не задаёт: `okpo`, `okopf`, `okfs` — только цифры; `okud` — первое совпадение `0710\d{3}`; `organization_name`, `address` — текст без пробелов по краям. 116. **P1-13.** В словарь пакета `statement_headers.yaml` переезжают и метки шапки Excel (`labels`, `auditor`, `legal_form`) — proposal: «`statement_headers.yaml` переезжает в пакет»; у tabular-parser не остаётся своего словаря форм. Цена: в общем словаре есть данные, которыми пользуется один адаптер. 117. **P1-14.** «Под подписью» в правиле (а) столбца кодов — ячейки столбца ниже подписи до конца сегмента. 118. **P1-15.** `FailedRelation` несёт и `column_index`, и `column_key` — чтобы ядро получало нарушения в своих ключах колонок без обратного сопоставления. 119. **P1-16.** Перечень кодов копируется из ветки docs `feat/116-unified-normalization` до слияния docs PR #117. 120. **P1-17.** Столбец правее кода без подписи, но с числами, в сегменте с заголовком — колонка с `label = ""`, `period_kind = none` и `COLUMN_WITHOUT_DATE` (`error`): «числовая колонка без разобранной подписи» по таблице уровней. 121. **P1-18.** Заголовок есть у одного из сегментов, но не у первого — колонки документа задаёт первый сегмент с заголовком (спека: «остальные сегменты берут его колонки»), `column_index` — номера столбцов этого сегмента. 122. **P1-19.** Документ `BALANCE` / `INCOME` без строк (`CODE_COLUMN_NOT_FOUND`) получает `form_variant = full`: в схеме v2.0 `form_variant` обязателен и `null` только у `DISCLOSURE`. 123. **P1-20.** При `accepted` (у ядра) правило «`needs_review` у всех ячеек несошедшегося жёсткого соотношения» не применяется: иначе акцепт снимал бы нарушение со статуса, а подсвеченные им ячейки держали бы `needs_check` через условие «нет значений с `needs_review`» (спека: «`accepted` ⇒ жёсткие нарушения статус не держат»). У сервиса `accepted` всегда ложно. Цена: после акцепта ячейки несошедшегося соотношения не подсвечиваются (нарушение видно в `checks`). 124. **P1-21.** `FailedRelation.expected` / `actual` (и `RelationOutcome`) — `Decimal | None`: `None` — цель или все члены пусты; такое посчитанное соотношение — всегда нарушение (допуск не применяется). Контракт v2.0 так и задаёт (`expected` — `money`, «`null` — цели нет»; `actual` — `money`), агрегат ядра тоже не превращает пустое в 0 (план 3, Review Focus 2). Цена: хосты и ядро обрабатывают `None` в тексте нарушения («пусто»); при ошибке — `0` вместо пустоты прятал бы «пустую цель» в допуск ±1 (`1600 = 0` при пустом `1700` считалось бы сошедшимся). 125. **P1-22.** Две публичные нормализации текста: `normalize_label` (дословно `normalize_text` tabular-parser — пунктуация сохраняется; под неё записаны перенесённые из tabular-parser метки шапки, заголовки, ОКУД, маркер продолжения) и `normalize_caption` (подписи колонок, единицы, относительные периоды). Таблица «ключ словаря → нормализация» — в разделе API; `type_hint` и `is_continuation` пакета пользуются `normalize_label`. Цена: новый синоним пишется под нормализацию своего ключа; ошибка — синоним молча не срабатывает (ловят тесты пакета на синтетике и покрытие словарей в tabular-parser, O-8 плана 4). 126. **P1-23.** Новый код `XLS_NO_CODEPAGE` (`info`, файл): книга xls без кодовой страницы — заметка читателя, относящаяся ко всей книге (и к формам, и к ОСВ). Строки добавлены в таблицу спеки «Шаг 8 — предупреждения» и §11.9 `05-domain-rules.md` (правило спеки: «новый код вводится словарём пакета и таблицей §11»). Цена: tabular-parser выдаёт его в `metrics.warnings` вместо `OSV_XLS_NO_CODEPAGE` в каждом документе ОСВ (план 4, P4-13); экран показывает его в карточке файла, а не у листа ОСВ. 127. **P1-24.** `resolve_relative(ABSOLUTE | NONE, …)` → `None`, а не исключение: у таких колонок дата не вычисляется, а ядро перебирает колонки формы одним циклом. Цена: вызов с неверным видом не заметен (ядро зовёт функцию только для `relative:*`, план 3 B2). 128. **P1-25.** `choose_edition(document_type, codes: Sequence[tuple[str, bool]], effective, rules, *, variant=None)` вместо запрошенной `choose_edition(effective, codes, rules)`: перечень кодов зависит от формы и варианта, а «лучшая подпоследовательность» по спеке сравнивает сначала число **уверенных** кодов — без типа и признака уверенности результат расходился бы с `recompute`. `confirm_codes` и `recompute` берут редакцию этой же функцией. Цена: ядро передаёт пары `(код, уверенный)`, собранные из строк результата (`line_code_confidence == 100`). 129. **P1-26.** Перевод выхода в форму контракта v2.0 (`to_contract`, `warning_to_contract`) — в пакете, а не в каждом сервисе: одно уплощение шапки, одно правило «необязательные ключи без `null`», один отброс `column_key`. Пакет теперь знает форму `statementDocument` v2.0. Цена: новый MAJOR контракта меняет и пакет (синхронизация копий — план 2), а модель контракта хоста валидирует `dict` пакета — расхождение видно тестом схемы хоста, а не пакета. 130. **P1-27.** Статус документа — функция документа: `status_input(StatementDocument)` (конвейер считает им `status`) и `status_input_from_contract(dict)` для ядра (страховка `document.status` результата 2.0); свойство `status_input_from_contract(to_contract(d)) == status_input(d)` закреплено тестами задач 19 и 20. `layout_found` выводится из «строки есть» (строк нет ⇔ столбец кодов не найден, задача 17 п. 2). Цена: если в будущем появится документ со столбцом кодов, но без строк, функции нужен явный признак раскладки. 131. **P1-28.** `code_column(rows, rules, *, relaxed)` и `amount_columns(rows, code_col, rules, *, source)` — публичные обёртки шага 2 над сеткой `Cell` (адаптер Excel строит её из листа); минимум ослабленного поиска и слабого продолжения — данные словаря (`min_codes_continuation: 1`, был литерал «минимум один» в коде шага 2 и `min_codes_continuation` tabular-parser). `relaxed` задаётся типом формы, а не внутренним `CodeSection`. Цена: адаптеру нужна сетка `Cell` листа, чтобы спросить столбец кодов (у tabular-parser она уже есть — `grid_rows` плана 4). 132. **P1-29.** `form_name` — поле `StatementHeader` (в контракте он в `header`), а не `StatementDocument`; ставит конвейер из `evidence.title.text` как есть, `parse_header` оставляет `None`. Цена: хост, собирающий шапку `parse_header` сам (без конвейера), заголовка не получит — таких вызовов в планах нет. 133. **P1-30.** Коды предупреждений — `WarningCode` (`StrEnum`) в `issues.py`; загрузчик требует равенства множества кодов словаря и перечисления; `make_warning` с неизвестным кодом — `KeyError`. Цена: новый код — две правки в пакете (словарь + член перечисления) и строка §11.9 docs; опечатку в коде хоста ловит mypy (член перечисления), а строковый литерал — только `KeyError` при вызове. 134. **P1-31.** `Rules.code_orders.sequence(…, DISCLOSURE, …)` → `()`, а не `KeyError`: у пояснений перечня нет, и структурная проверка, и проверки ядра по редакции к ним не применяются. Цена: вызов для пояснений молча даёт пустой перечень. 135. **R-1 (план 1). Дорожка плана 1 — фоновый SDD-контроллер на sonnet**: дорожки волны 1 (планы 0, 1, 3А, 6) идут параллельно четырьмя фоновыми агентами; исполнители задач — sonnet, финальное ревью ветки — opus. Цена: слабее контекст сессии; страхуют пошаговые ревью и таблица находок. 136. **R-2 (план 1). Р-34 и Р-43 учтены в брифах**: подтверждение кода — флаг `code_fixed` (`is_confirmed`), уверенность не меняется; `arm-statements` внесён в таблицу workspace (`CLAUDE.md` workspace, `context/00-workspace-layout.md`, коммит e6eef27 ветки feat/116). Цена: нет. 137. **R-3 (план 1). `parse_code`: краевая `|` — как в спеке**: `|` → 1, если результат в кодовом диапазоне (строка 178 spec.md), допустимы 4–5 цифр: «1220|» → 12201, «|1220» → 11220 с `fixed=True` (подсвечивается). Не менялось: буквальное чтение спеки. Цена: код с краевой чертой OCR становится пятизначным с подсветкой; вопрос владельцу спеки (кандидат в follow-up). Ledger I-2 (T4). 138. **R-4 (план 1). Тест плана `test_sublines` исправлен по спеке**: тест плана (1230 = 10 против 4 + 5 = 9) противоречил допуску ±1 мягких соотношений (расхождение 1 — в допуске); оставлено поведение по спеке, тест заменён парой (4+4 → нарушение, 4+5 → нет). Цена: если для подстрок хотели строже — допуск подстрок 0 вместо ±1. 139. **R-5 (план 1). Неуверенный код вне перечня редакции не подтверждается структурой**: спека явного правила не даёт; по общему «подтверждён ⇔ входит во все лучшие подпоследовательности», а код вне перечня в последовательность не входит. Цена: неуверенный нестандартный код из OCR получает `needs_review` (безопасная сторона). 140. **R-6 (план 1). История ветки feat/1-arm-statements-package переписана `git filter-branch --msg-filter`**: один коммит имел трейлер «Claude Sonnet 5.5» вместо требуемого «Claude Opus 5.5»; ветка не публиковалась. Цена: SHA коммитов в ledger/отчётах до переписи устарели. 141. **R-7 (план 1). Решения исполнителей задач при пробелах спеки**: T6.2 (смешанные/повторные разделители суммы → цифры подряд ≤ 16 и `is_recognized=false`; excel допускает ≥ 3 знака после разделителя); T10.1–T10.4 (`SEGMENT_WITHOUT_CODES` только если столбец кодов найден хотя бы у одного сегмента; ослабленный поиск включается, когда строгий не нашёл и отрезок известен; сегмент двухстрочной повторной шапки открывается строкой с подписью кода; сопоставление по датам требует разных целевых колонок); T11.2 (сегмент без столбца кодов — наименование слева, значения по `value_cols` или правее наименования); T14.3–T14.4 (ничья без даты — `edition=None`, подтверждено в обеих; родитель пятизначного — ближайшая выше строка с нужным кодом, уверенная или подтверждённая); T15.2 («строгое большинство» знака = более 50 %). Ревью задач: со спекой не расходятся. Цена: перечисленные трактовки при пробелах спеки; пересмотр — правкой кода пакета и словарей. 142. **R-8 (план 1). Расширение публичного API сверх раздела плана**: в `__all__` добавлены `Rules` (нужен хостам в сигнатурах) и `RulesError` (обработка `load_rules`); абзац про них — в README пакета. Цена: хосты могут зависеть от двух лишних имён. **Решения плана 2 (sync-statements), 18.10.2026** (дорожка плана 2, ветка docs `feat/120-sync-statements`, arm-ka-docs#120; решения P2-n — автор плана, R-n — контроллер): 143. **P2-1 (O-1 плана 1).** Файл конфига линтеров в копию не кладётся; «конфиг пакета подключён к хосту» — это: ruff хоста уже совпадает с пакетом (`line-length = 120`, `py311`, `E F I UP B PL`), в isort `known-first-party` добавлен `arm_statements`; в `mypy` хоста модули `arm_statements.*` входят в строгий блок (те же флаги, что `strict = true` пакета). Цена: блок флагов повторён в трёх хостах; если хост когда-нибудь станет строже пакета, копия покраснеет в хосте — чинить в `arm-statements`. 144. **P2-2 (O-2).** Пины `ruff==0.16.10`, `mypy==2.4.0`, `types-PyYAML==6.0.12.20260906` во всех хостах (у tabular-parser и arm-core были `>=`; в их окружениях уже стоят эти версии — пины ничего не переформатируют). `pytest` не закрепляется: тесты пакета не зависят от его версии. Цена: обновление ruff/mypy — согласованно, сначала пакет. 145. **P2-3 (O-3).** `testpaths = tests arm_statements/tests` в `pytest.ini` хостов; `conftest` хостов не меняется: у них нет `api/conftest.py`, а `tests/conftest.py` действует только на `tests/`; `env`, `asyncio_mode`, `--strict-markers` тестам пакета не мешают. Проверка — равное число собранных тестов пакета в хосте и в `arm-statements`. 146. **P2-4 (O-4).** Dockerfile хостов не меняется: `COPY api/ ./` уже кладёт `api/arm_statements/rules/*.yaml`, `.dockerignore` их не исключает; платформа уже возит `api/rules/*.yaml` и `api/contracts/` (сервисы читают их с диска в контуре). Проверка — запуск `default_rules()` в собранном образе (задачи 7–9). 147. **P2-5.** Подключение хостов — три bounded PR этого плана (задачи 7–9), а не части планов 3/4/5: правки одинаковые и механические, план 3 уже ждёт «PR плана 2 в arm-core», первая раскладка проверяет скрипт на настоящих хостах, а планы 4/5 стартуют от `main` с копией. Цена: ещё три PR и копия в `main` хостов до того, как её вызывает код сервиса. 148. **P2-6.** Режим `--in-place` (раскладка в существующий worktree без issue, коммита и PR) — для подключения хоста и для PR фичи, которой нужна новая версия пакета; спека дополнена (задача 6). Цена: второй путь раскладки, мимо отдельного PR синхронизации. 149. **P2-7.** Скрипт — bash по образцу `sync-web.sh` (spec называет `sync-statements.sh`), разбор TOML/YAML — помощник `statements_check.py` (Python 3.11 + PyYAML); интерпретатор — `STATEMENTS_PY` → `docs/.venv` → `api/.venv` хоста → `python3`; подходящего нет — код `2`. Цена: `make venv` docs ставит PyYAML (как и в PR #117). 150. **P2-8.** В `--check` отсутствие копии — `20` («копии нет»): хост, в котором вызывают проверку, обязан держать копию. В обычном режиме хост без копии — `2` с указанием на `--in-place`: подключение хоста — ручная задача с правкой конфигов. 151. **P2-9.** Нет `contracts/statement-code-orders.yaml` в docs — `10` (docs старше пакета; так до слияния PR #117). 152. **P2-10.** Зависимости сверяются по имени (PEP 503) с `api/requirements.txt` хоста (его ставит образ); разные версии — примечание, код не меняют. Цена: несовпадение версии PyYAML ловят только тесты пакета в хосте. 153. **P2-11.** «Копия отстала» — буквально по спеке: `VERSION` ≠ `--ref`, даже если новые коммиты источника не трогают `arm_statements/` (README, Makefile). Цена: лишние предупреждения `10` после таких коммитов (см. O-1). 154. **P2-12.** Ошибка `fetch` в `--check` не фатальна (предупреждение, сравнение с локальными ссылками): проверка перед PR не должна краснеть без сети, если копия на месте. 155. **P2-13.** Сравнение — по файлам рабочего дерева (`diff -rq`), без `VERSION` и кэшей (`__pycache__`, `*.pyc`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache`, `.DS_Store`); биты режима не сравниваются. Незакоммиченная правка — тоже `20`. 156. **P2-14.** Обычный режим перед коммитом гоняет `make venv` и `make check` хоста в новом worktree (как `sync-web.sh` — проверки фронта). Цена: минута на окружение. 157. **P2-15.** «Сначала ядро» при синхронизации сервиса — предупреждение (в выводе и в описании PR), не отказ: порядок важен для смены формата результата, а не для каждой правки пакета. 158. **P2-16.** `make -C docs statements-check` — обзор по трём хостам рядом (`0`/`10` — зелёные); в `make check` docs не входит: читает соседние репозитории. 159. **P2-17.** Имя хоста — `--host` или последний компонент `origin` (`arm-core`, `arm-tabular-parser`, `arm-scan-gateway`), иначе имя каталога; сверка словаря имён — только у `arm-core`. 160. **P2-18.** Раскладка отказывает (код 1), если файлы копии игнорируются `.gitignore` хоста или содержат то, на чём остановится снимок для контура (`/Users/`, `/home/…`, `git.aepif.ru`, `uv venv`, `uv pip`). 161. **P2-19.** Строки `make venv` docs — дословно как в ветке `feat/116-unified-normalization`, строки таблицы `dev/checks.md`, которые меняет PR #117, не трогаются: два PR сливаются в любом порядке без конфликта. 162. **P2-20.** В `CLAUDE.md` каждого хоста — правило «копию не править, обновлять скриптом»: агенты читают его первым. Шаблоны `docs/repo-templates/` не меняются (разошлись с репозиториями давно). 163. **P2-21.** Ветка docs плана — от `origin/main` (шаг 1 выкладки раньше PR #117); спека, `design.md` и раздел «Копия» `dev/checks.md` правятся в `feat/116-unified-normalization` (задача 6). 164. **R-1 (план 2). `make check` в docs красный по `validate-openspec`**: в Makefile `CHANGE ?= slice-0-skeleton`, change в архиве (O-1 плана 0); план 2 это не чинит. Цели проверялись по отдельности (`validate-contracts`, `test-snapshot`, `test-sync-web`, `test-sync-statements` — зелёные), в PR красный `validate-openspec` назван. Цена: `make check` остаётся красным до отдельной правки Makefile. 165. **R-2 (план 2). Р-42 исполнен в этом плане (задача 5b)**: `prepare-contour-snapshot.sh` для каждого из трёх сервисов запускает `sync-statements.sh --check` по дереву `api` коммита `$REF` (`git archive` во временный каталог, `--host <имя>`): `0` — тихо; `10` — предупреждение и абзац в `SNAPSHOT.md`; `20` — стоп, код 1; `2` (нет источника или интерпретатора) — предупреждение, снимок делается; если в `$REF` нет `api/arm_statements` — проверка пропускается (пакет в хост ещё не подключён: иначе `20` «копии нет» блокировал бы снимок до задач 7–9). Цена: пока хост без копии, снимок о ней не напомнит; тест снимка требует Python ≥ 3.11 с PyYAML (`STATEMENTS_PY=$(PY)` в `test-snapshot`). 166. **R-3 (план 2). Спека: «и его конфигом линтеров» убрано**: предложение спеки о копии «вместе с … конфигом линтеров» противоречило P2-1 (конфиг пакета в копию не кладётся, применяется конфиг хоста); исправлена спека и раздел «Копия общего кода» `dev/checks.md`. Цена: нет. 167. **R-4 (план 2). Защиты раскладки липкие, копия при отказе защиты остаётся в дереве**: игнорируемые `.gitignore` файлы и пути контура (P2-18) проверяются и после раскладки, и при повторе `--in-place` с той же версией («раскладывать нечего» код 1, если копию нельзя коммитить); при коде 1 копия остаётся в дереве хоста, об этом сказано в сообщении. После `make check` хоста в обычном режиме `evaluate` повторяется (форматтеры могли переписать копию) — `20` блокирует коммит. Цена: после отказа защиты оператор сам убирает копию (`git checkout`/`clean`). 168. **R-5 (план 2). `--in-place` требует `--host-dir` и не работает на основном checkout и ветке main/master** (финальное ревью ветки docs, I-1): `--in-place --host <имя>` разворачивался в основной checkout на main и стирал там `api/arm_statements`; теперь код 2. Цена: `--in-place` только с `--host-dir` внутри worktree фичи. ### Решения при исполнении (18.10.2026) Перенос из `design.md` change'ов исполнения (разделы «Decisions made without discussion»): текст решений, обоснований и цен ошибки — как в источнике, без сокращений; маркеры источника сохранены дословно, нумерация docs продолжает общую (по подразделам ниже — по дорожкам). Номер в начале пункта — номер в этом файле. Решения владельца (Р-n) — в разделе «Решения владельца» выше; решения плана 0–2 и Р-44 перенесены раньше. #### План 3, часть А (arm-core, change `unified-normalization-core-a`) Источник: `openspec/changes/unified-normalization-core-a/design.md` в репозитории `arm-core`, коммит `c2ee31d` (ветка исполнения; полный текст — там). Перенесено пунктов: 107. 169. **P3-1** (план 3А): Миграция пишет `header_source` с ключами `inn`, `report_date`, `units`, `organization_name`: `text` — из `inn_original_value`, `report_date_original_value`, `units_original_value`, `organization_name`; `confidence` — `inn_confidence` у `inn`, у остальных `null`. Обоснование: спека: «из `*_original_value` и `inn_confidence`»; прочих полей в v5.18 сырыми нет. Цена ошибки: подсказка у поля старой формы беднее; формы старых времён на контуре удалены очисткой. 170. **P3-2** (план 3А): `organization_name_modified` — `false` у всех форм (миграция и оба маппера), не `NULL`. Обоснование: как `inn_modified` в маппере 1.x; API отдаёт `bool`. Цена ошибки: одна строка `UPDATE`. 171. **P3-3** (план 3А): `needs_review_fields`: маппер `1.x` пишет `[]`, миграция оставляет `NULL`, чтение `NULL` = `[]`. Обоснование: спека миграции поле не заполняет. Цена ошибки: нет. 172. **P3-4** (план 3А): Маппер `1.x` тоже хранит вторую форму того же типа (Р-10); формы одного типа и одной пары `period_year`, `period_quarter` получают `MAPPING_WARNING` «в файле есть другая форма <тип> за тот же период» и по D-6 — `NEEDS_CHECK` (аналог Р-18). Обоснование: частичный индекс v5.19 больше не заставляет пропускать; одно правило хранения для обоих MAJOR. Цена ошибки: O-1: если владелец хочет прежний пропуск — вернуть ветку `continue`. 173. **P3-5** (план 3А): Предупреждения маппинга листа ОСВ (повтор счёта, строки без родителя, нарастающая по данным Q1) пишутся ещё и в `document_issue` документа листа (`warning`, `MAPPING_WARNING`, `info`, `core`) при любом MAJOR; строки в `metrics.mapping_warnings` остаются. Обоснование: спека: `DocumentOut.issues` ОСВ — «вместе с предупреждениями маппинга ядра этого листа». Цена ошибки: нет. 174. **P3-6** (план 3А): `file_ordinal` объединённого документа `DISCLOSURE` / `OTHER` — индекс первого такого документа в `documents[]`. Обоснование: несозданный документ «номер не занимает». Цена ошибки: нет. 175. **P3-7** (план 3А): **Переопределено Р-40 (владелец, 18.10.2026): `CODE_NOT_IN_DICTIONARY` / `CODE_NOT_IN_EDITION` пишутся только для четырёхзначных кодов** (пятизначные подстроки Excel не дают `info`; закрывает O-3). Остальное — как ниже. `CODE_NOT_IN_DICTIONARY` и `CODE_NOT_IN_EDITION` — одна запись на код документа (`line_ids` — все строки `ok` с кодом), пятизначные коды тоже проверяются; при приёме `1.x` проверки ядра уровня `info` не пишутся. Обоснование: спека для `1.x` перечисляет только §7 и `MAPPING_WARNING`. Цена ошибки: O-3 (поток `info` у пятизначных). 176. **P3-8** (план 3А): После правки записи `core` `CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH` и `STATEMENTS_VERSION_MISMATCH` пересчитываются (заменяются); `NAME_MISMATCH` не ставится строке с `code_fixed`; `MAPPING_WARNING`, `RESULT_STATUS_INCONSISTENT`, `LEGACY_WARNING` сохраняются. Обоснование: код строки меняется правкой; страховка статуса — факт приёма. Цена ошибки: O-4. 177. **P3-9** (план 3А): Результат `2.0`: значение с `column_index` вне колонок формы и повтор `column_index` колонки — первое остаётся, `MAPPING_WARNING`. Обоснование: FK значения на колонку; уникальность `(meta_id, column_index)`. Цена ошибки: нет. 178. **P3-10** (план 3А): Части `PATCH` применяются в порядке `lines` → `values` → `columns` → `header`; вся адресация (422) проверяется до первой записи; `UNKNOWN_CELL` по `line_code` — по кодам строк `ok` с учётом назначений `lines` этого запроса; один `line_id` дважды в `lines` — 422 `INVALID_LINE_PATCH`. Обоснование: атомарность и предсказуемость. Цена ошибки: нет. 179. **P3-11** (план 3А): `reference_name` — первое имя кода в редакции ответа (`edition` по дате); нет редакции или кода — `null`. Обоснование: спека: «первое имя кода в словаре»; `edition` ответа — по дате. Цена ошибки: нет (Р-29). 180. **P3-12** (план 3А): (= Р-35, решение владельца.) Очистка отказывает, только если документ формы упомянут в `ex_snapshot_item` или привязан к версии в статусе «Верифицирована» или дальше: `VERIFIED` или `ARCHIVED` с `ex_snapshot.source_version_id` = версия; привязки к версиям в работе, на согласовании, `REJECTED`, `CANCELLED` (и архивированным отменённым) снимаются, статус версий не меняется. Обоснование: решение владельца 18.10.2026 (Р-35): верифицированная версия read-only и заморожена в ЕХ, прочие — нет; на стенде версий дальше «в работе» нет. Цена ошибки: архивированная версия без снимка ЕХ (из `CANCELLED`) очистке не мешает — если бы мешала, владелец увидел бы лишний отказ. 181. **P3-13** (план 3А): Коды выхода `purge_statements.py`: `0` — отчёт или выполнено, `1` — отказ (п. 2 спеки), `2` — ошибка аргументов. Обоснование: как у `check_dbml_vs_orm.py`. Цена ошибки: нет. 182. **P3-14** (план 3А): До части Б §7 считает ядро на лету (`statement_checks.yaml`) по агрегату; `checks.failed[].codes` берутся из правила, `line_ids` — по агрегату. Обоснование: часть А должна быть самосогласованной. Цена ошибки: временный код удаляется в B6. 183. **P3-15** (план 3А): Для условия `RECOGNIZED` `2.0` предупреждения файла берутся из `metrics.warnings` файла результата; в `recognition_job.metrics.warnings` — из конверта (inbox). Обоснование: маппер читает файл результата; по контракту наборы одинаковы. Цена ошибки: нет. 184. **P3-16** (план 3А): (= Р-34, решение владельца.) Подтверждение кода (`lines[] {confirmed: true}`) ставит `is_confirmed = true` и `needs_review = false`, `line_code_confidence` и `line_code_modified` **не меняет**; назначение кода — `confidence = 100`, `line_code_modified = true`. Обоснование: иначе исходная уверенность теряется, а снятие подтверждения не может вернуть `needs_review` (гайд §5.6.1: «при `false` — пересчитывается»); аналогия с ячейкой (`is_modified` не ставится); подтверждено владельцем 18.10.2026 (Р-34), спека `arm-core`, §9.5 п. 9 и гайд §5.6.1 приведены к этому. Цена ошибки: нет. 185. **P3-17** (план 3А): Порядок строк ответа: строки `ok` (с шаблонными) — по коду, затем `row_index` (`NULL` последними), затем `id`; за каждой строкой `ok` источника — её «вложения» по `row_index`: строки `detail` с этим родителем и строки `unreadable` / `missing` / `detail` без родителя, у которых она — ближайшая предыдущая по `row_index` строка `ok` источника; вложения без такой строки — в начале. Обоснование: спека «Ответ `GET …/statement`». Цена ошибки: нет. 186. **P3-18** (план 3А): **Уточнено Р-41 (владелец): смена `crm_id` пишется в аудит как изменение группы (`GROUP_CHANGE`) — закрывает O-6.** Изменение `crm_id` ГСК/ГСЗ пишет одно событие `GROUP_CHANGE` с `before/after` всех изменённых полей вместо `MEMBERSHIP_CHANGE`. Обоснование: спека: «изменение — аудит `GROUP_CHANGE`». Цена ошибки: O-6. 187. **P3-19** (план 3А): Ядро зовёт у пакета `choose_edition` (`engine.edition`) и `status_input_from_contract` (`engine.contract_document_status`) вместо сборки `StoredForm` ради редакции и своего `status_input_from_v2`; `to_contract` ядру не нужен. Обоснование: одно правило с сервисом и с `recompute` (план 1, P1-25, P1-27); пары `(код, уверенный)` — из строк `ok` результата по `row_index`. Цена ошибки: при ошибке сборки пар редакция приёма разойдётся с редакцией после правки — тест B1 сравнивает их. 188. **P3-20** (план 3А): `header.source.<поле>.confidence` — строка Decimal с двумя знаками (`"61.54"`), как прочие уверенности API; в JSONB `header_source` остаётся число как пришло. Обоснование: фронт читает все уверенности одним способом (план 6, A8). Цена ошибки: округление до сотых в подсказке; исходное число — в БД. 189. **P3-21** (план 3А): `StatementHeaderOut.okei` — из `financial_statement_meta.okei` (колонка есть с v5.18), только чтение; в `PATCH header` не входит. Обоснование: спека `header` контракта несёт `okei`, экран показывает `issues` с `field = "okei"` у поля. Цена ошибки: нет. 190. **P3-22** (план 3А): Справочник строк для подсказки кода — поле `StatementOut.edition_lines [{code, name}]`, а не отдельный эндпоинт: данные уже загружены (`LineCatalog`, A6) при сборке ответа, нет нового маршрута, права, кэша и запроса на фронте; около 80 пар на форму (≈ 4 КБ) против строк формы с значениями; у формы без даты — коды всех редакций, по одному. Обоснование: наименьший объём реализации в ядре и во фронте; закрывает пробел «у формы без даты подсказки нет» (план 6, O-3). Цена ошибки: ответ `GET` тяжелее на ≈ 4 КБ; если понадобится справочник вне формы — отдельный эндпоинт поверх того же `LineCatalog.edition_lines`. 191. **P3-23** (план 3А): `DocumentBrief.issue_counts {error, info}` — все записи `document_issue` документа (и `relation` / `soft`, в том числе акцептованные) по `severity`; один групповой запрос на файл. Обоснование: панели файлов нужен признак «есть что смотреть» без запроса на каждый документ (план 6, O-4). Цена ошибки: акцептованное нарушение продолжает помечать документ в панели — подробности на экране документа. 192. **P3-24** (план 3А): Ошибки элемента `lines[]` (409 `LINE_CODE_TAKEN`, 422 `LINE_CODE_REQUIRED`, `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`) несут в `detail` `line_id` и `line_code` первого неверного элемента; `LINE_CODE_REQUIRED` добавлен к четырём кодам запроса фронта. Обоснование: экран ставит ошибку у поля нужной строки, не угадывая (план 6, O-2); одно правило для всех ошибок элемента. Цена ошибки: при нескольких неверных элементах фронт видит только первый — как и прежде. 193. **R-1** (план 3А): Порядок исполнения части А: A0–A4, затем A6–A13; A5 (контракт 2.0, `recognition_exchange.py`) — последней, после слияния плана 0 (в `origin/main` нет `api/tests/contract_identity.py`). Обоснование: A5 зависит от теста идентичности плана 0, остальные задачи его результат не потребляют. Цена ошибки: возможные конфликты слияния в `documents.py` / `recognition_exchange.py` при A5 в конце. 194. **R-2** (план 3А): Исполнители задач и их ревьюеры — sonnet; финальное ревью ветки — opus. Обоснование: план даёт прозу, а не готовый код; ревью ветки ловит сквозные расхождения. Цена ошибки: более слабая модель на задаче — лишние итерации ревью. 195. **R-3** (план 3А): Тесты с БД идут на scratch-БД `armcore_pytest_*` compose-Postgres :5432 (устройство conftest); контейнеры не останавливаются и не пересоздаются. Обоснование: общий стенд разработки не должен падать из-за прогонов. Цена ошибки: недоступный Postgres — задача BLOCKED, а не пропуск тестов. 196. **R-4** (план 3А): A0 (только артефакты OpenSpec) без отдельного ревью сабагентом: достаточно `openspec validate --strict`. Обоснование: кода в задаче нет. Цена ошибки: неточность формулировок дельт; поймало финальное ревью. 197. **R-5** (план 3А): Ревью A1 и A2 объединено в один пакет после A2. Обоснование: downgrade-тесты A1 проверяемы только с A2. Цена ошибки: находки по A1 пришли на один раунд позже. 198. **R-6** (план 3А): Ревью малых задач объединялось парами (A3+A4, A8+A9, A10+A11, A12+A13). Обоснование: экономия раундов ревью. Цена ошибки: находки по первой задаче пары приходят на шаг позже. 199. **R-7** (план 3А): Change `unified-normalization-core-a` архивируется только вместе с `unified-normalization-core-b`; тексты поведения части Б в дельтах А остаются. Обоснование: разбор дельт на два change дороже и не нужен до выкладки обеих частей. Цена ошибки: если А заархивировать отдельно, `openspec/specs` опишет нереализованное. 200. **R-8** (план 3А): Одна волна правок по итогам финального ревью (sonnet), затем один scoped re-review; M-6, M-8, M-13 не правятся (deferred / rejected). Обоснование: замечания малы; M-13 (advisory-блокировки purge пофайлово) — окно обслуживания. Цена ошибки: M-6 (отдельный SUM для `code_amount`) и M-8 остаются техдолгом. 201. **A1-1** (план 3А): `result_mapping.map_file` до A5/A9 присваивает `file_ordinal` порядковым номером создания документов. Обоснование: иначе `uq_document_stored_file_id_file_ordinal` ломает файл с 2+ документами. Цена ошибки: номера не равны индексу в `documents[]`; заменено в A9/A5. 202. **A1-2** (план 3А): `organization_name_modified` — nullable boolean (как в DBML); миграция пишет `false`. Обоснование: соответствие DBML v5.19. Цена ошибки: нет. 203. **A1-3** (план 3А): Тест-фабрика `tests/factories.py` и `tests/test_models_part_04_06._document` нумеруют `file_ordinal`. Обоснование: уникальность `(stored_file_id, file_ordinal)`. Цена ошибки: нет. 204. **A1-4** (план 3А): Старый тест «две BALANCE с NULL -> IntegrityError» переписан под DISCLOSURE. Обоснование: по v5.19 для форм это больше не нарушение. Цена ошибки: нет. 205. **A1-5** (план 3А): Имена FK/PK новых объектов (`fk_financial_statement_value_confirmed_by_app_user`, `fk_document_issue_*`, `pk_document_issue`) — по NAMING_CONVENTION. Обоснование: совпадают с `create_all`, тест схемы зелёный. Цена ошибки: нет. 206. **A2-1** (план 3А): `line_status` считается потерянным при `<> 'ok'` (колонка NOT NULL default 'ok'). Обоснование: `IS NOT NULL` всегда истинно. Цена ошибки: шум в журнале отката. 207. **A2-2** (план 3А): Булевы счётчики и `organization_name_modified` считаются как `IS TRUE`; причина отказа «наименование правилось» — `organization_name_modified IS TRUE`. Обоснование: NULL не считается правкой. Цена ошибки: нет. 208. **A2-3** (план 3А): Причина «строки формы без кода» — по `line_code IS NULL` (любые), «повтор» — только по ненулевым кодам. Обоснование: точность причин отказа отката. Цена ошибки: нет. 209. **A2-4** (план 3А): Сообщение журнала склеивает счётчики через `; `. Обоснование: читаемость. Цена ошибки: нет. 210. **A3-1** (план 3А): Только PATCH, меняющий `crm_id`, пишет `GROUP_CHANGE` (`before/after` всех изменённых полей запроса); PATCH только имени/даты остаётся `MEMBERSHIP_CHANGE`; создание — `MEMBERSHIP_CHANGE` с `crm_id` в payload. Обоснование: P3-18 / Р-41 про смену `crm_id`; минимум изменения поведения. Цена ошибки: фильтр аудита по действию для правок групп расходится; правка в одну строку. 211. **A3-2** (план 3А): Предпроверка уникальности `crm_id` не учитывает видимость (глобальная уникальность), как для ГК. Обоснование: единый подход. Цена ошибки: `crm_id` невидимой группы раскрывает существование через 409 (как ГК/ЮЛ). 212. **A3-3** (план 3А): Before-payload удаления группы включает `crm_id`. Обоснование: полнота аудита. Цена ошибки: нет. 213. **A4-1** (план 3А): `run` принимает подменяемую фабрику транзакции (по умолчанию `session_transaction`). Обоснование: тесты идут на `db_session`. Цена ошибки: нет (боевой путь — по умолчанию). 214. **A4-2** (план 3А): `PurgePlan` несёт `include_mixed`; `BlockingVersion` по `ex_snapshot_item` имеет статус `EX_SNAPSHOT_ITEM` и `version_id = ex_snapshot.source_version_id`. Обоснование: сигнатура `apply_purge` без флага. Цена ошибки: нет. 215. **A4-3** (план 3А): Проверка блокировки покрывает ВСЕ документы удаляемых целиком файлов (в том числе OSV/DISCLOSURE; OSV смешанных файлов с `--include-mixed`). Обоснование: иначе удаление сломало бы FK/заморозку. Цена ошибки: лишний отказ, если OSV удаляемого файла в верифицированной версии (корректно: она заморожена). 216. **A4-4** (план 3А): `apply_purge` блокирует все файлы плана (jobs -> file -> documents, по id файла), перепланирует под блокировками и отказывает до первой мутации (`PurgeRefusedError`); версии блокируются позже (file -> doc -> version). Обоснование: защита от гонки с загрузкой. Цена ошибки: окно между проверкой и блокировкой версий (T4.3, rejected). 217. **A4-5** (план 3А): Dry-run с блокировкой печатает блок отказа, но выходит с 0; `--apply` с блокировкой: отчёт в stdout, сообщение в stderr, код 1. Обоснование: P3-13: 0 = отчёт. Цена ошибки: нет. 218. **A4-6** (план 3А): `FILE_DELETE.after_value`: для целого файла `{unbound_version_ids}`, для смешанного `{deleted_documents, unbound_version_ids}`. Обоснование: трассируемость очистки. Цена ошибки: нет. 219. **A4-7** (план 3А): `superseded_with_osv` — файлы, у которых `superseded_by_file_id` указывает на удаляемый файл и которые содержат OSV. Обоснование: сохранение замещения ОСВ. Цена ошибки: нет. 220. **A6-1** (план 3А): `section_number` возвращает строчные буквы (работает по нормализованному тексту). Обоснование: внутренний формат. Цена ошибки: нет. 221. **A6-2** (план 3А): `names()` — имена без повторов в порядке редакций и появления; `edition_codes` — по всем записям. Обоснование: стабильность подсказки. Цена ошибки: нет. 222. **A6-3** (план 3А): Если «итого по разделу» только у одного из имён, номер у другого `None` — сравнение только по равенству строк. Обоснование: спека. Цена ошибки: нет. 223. **A6-4** (план 3А): Пустые справочные имена пропускаются. Обоснование: шум. Цена ошибки: нет. 224. **A5-1** (план 3А): `FileWarning` наследует `DocumentWarning` (есть также `line_refs`/`column_index`/`field`): схема fileWarning = warning + locator. Обоснование: надмножество брифа, без дублирования полей. Цена ошибки: нет. 225. **A5-2** (план 3А): `MetricsV2` наследует `Metrics` (переопределяет `warnings`); ветвление в `_metrics_json` идёт по классу конверта, не метрик. Обоснование: изоляция v1/v2 при общей базе метрик. Цена ошибки: v2-конверт обернул бы уже готовые структуры; покрыто `test_inbox_v2_warnings_kept`. 226. **A5-3** (план 3А): `parse_message`/`parse_result`: не-2 MAJOR отдаётся модели v1, чей шаблон `^1\.\d+$` даёт `ValidationError` (3.0, «banana», нет поля); нечитаемый JSON в `parse_result` — `json.JSONDecodeError` (ValueError), не `ValidationError`. Обоснование: единая точка отказа для неподдерживаемых версий. Цена ошибки: вызывающий код, ждущий только `ValidationError`, пропустит ошибку чтения JSON (см. открытый вопрос). 227. **A5-4** (план 3А): У v2-предупреждения без `locator` в метрики пишется как пришло (ключа нет), у LEGACY — `locator: None`. Обоснование: не домысливать данные, не менять пришедшее. Цена ошибки: потребитель обязан читать `locator` как необязательный (`file_warnings` так и делает). 228. **A5-5** (план 3А): `RecognitionResultMessageV2` не наследует v1; общие поля вынесены в `_MessageBase`. Обоснование: изоляция `isinstance` между версиями. Цена ошибки: нет. 229. **A5-6** (план 3А): `file_warnings`: `locator` не-строка/не-null -> объект пропускается. Обоснование: защита от мусора во входящем конверте. Цена ошибки: потеря предупреждения с некорректным locator. 230. **A6-5** (план 3А): Старые тесты API обновлены под код 1105 в `fsbu4` (yaml дополнен 1105/1130/1140/1530/2460 и вторыми именами). Обоснование: данные docs. Цена ошибки: для ГСК-кода 1105 на 66н с датой `fsbu4` появляется пустая строка «Гудвил» (O-2 A6). 231. **A7-1** (план 3А): `FailedCheck.codes` — поле с default `()`, участвует в сравнении. Обоснование: P3-14. Цена ошибки: низкая. 232. **A7-2** (план 3А): `insert_statement` до маппера 2.0 пишет `row_index` = порядковый номер строки черновика. Обоснование: иначе все строки источника стали бы `origin = template`. Цена ошибки: заменено в A9/части Б. 233. **A7-3** (план 3А): `checks_input` принимает объекты-колонки (id, column_index) — протокол, без импорта моделей в домен. Обоснование: чистый домен. Цена ошибки: нет. 234. **A7-4** (план 3А): `code_amount`: колонка — первая по `column_index`, затем SUM по ней; нет колонки или непустых — `None`. Обоснование: определённость суммы. Цена ошибки: M-6: отдельный SUM, deferred в часть Б. 235. **A7-5** (план 3А): В API `checks.failed[].codes` и `line_ids` не выведены в A7 (только домен). Обоснование: не в брифе A7. Цена ошибки: закрыто в A8. 236. **A7-6** (план 3А): Порядок строк ответа в A7 не менялся. Обоснование: P3-17 — задача A8. Цена ошибки: нет. 237. **A8-1** (план 3А): `insert_statement` временно пишет `period_kind` (`absolute` при дате, иначе `none`). Обоснование: иначе тест брифа на 1.x не проходит до A9. Цена ошибки: заменено в A9; совпадает с backfill миграции. 238. **A8-2** (план 3А): `FailedCheckOut.line_ids` — объединение `line_ids` агрегата по `codes` правила; у межформенной проверки — строки только читаемой формы. Обоснование: кода второй формы в агрегате нет. Цена ошибки: фронт не подсветит строку другой формы. 239. **A8-3** (план 3А): `header.source`: запись с `text: null` отдаётся как есть, нечисловая уверенность -> `null`, не-dict записи пропускаются. Обоснование: T1.1: «нет подсказки». Цена ошибки: нет. 240. **A8-4** (план 3А): `order_lines`: `detail` с неизвестным родителем ведёт себя как `detail` без родителя; вложения по `row_index`; равные ключи — порядок входа (по `id`). Обоснование: P3-17. Цена ошибки: нет. 241. **A8-5** (план 3А): `IssueCounts` — dataclass репозитория, сервис переводит в `IssueCountsOut`. Обоснование: слои. Цена ошибки: нет. 242. **A9-1** (план 3А): `ColumnDraft` вместо кортежа в `StatementDraft.columns` (несёт `period_kind` / `label_confidence`). Обоснование: нужны поля колонки. Цена ошибки: правка 4 утверждений в тестах. 243. **A9-2** (план 3А): `header_source.*.confidence` в БД — число как пришло из 1.x (float), не Decimal. Обоснование: JSONB; P3-20. Цена ошибки: нет. 244. **A9-3** (план 3А): Повтор формы определяется только при известных годе и квартале у обеих; текст issue без префикса locator, в `metrics.mapping_warnings` — с префиксом. Обоснование: P3-4. Цена ошибки: нет. 245. **A9-4** (план 3А): Пустая ячейка (нет значения) -> `needs_review = false` (`value_clean(None, None)` истинно). Обоснование: формула приёма 1.x. Цена ошибки: нет. 246. **A9-5** (план 3А): У листа ОСВ issue пишутся и для предупреждения о повторе счёта (относится к первому листу). Обоснование: P3-5. Цена ошибки: нет. 247. **A10-1** (план 3А): Контрагент `UNKNOWN` трактуется как `None`, блокирует только `MISMATCH`. Обоснование: спека: `counterparty_check != MISMATCH`. Цена ошибки: низкая. 248. **A10-2** (план 3А): `name_mismatches`: строка `ok` с `line_code_confidence = None` считается уверенной (не проверяется); `line_status` сравнивается со `StatementLineStatus.ok` (StrEnum). Обоснование: спека: «confidence < 100» (T10.1: покрыто комментарием и тестом). Цена ошибки: строка без уверенности не даёт `NAME_MISMATCH`. 249. **A10-3** (план 3А): `has_error_warning`: строго `severity == "error"` (регистр важен), не список -> `False`. Обоснование: строгий контракт. Цена ошибки: нет. 250. **A10-4** (план 3А): `noqa PLR0913` на `form_status_v2` и тестовом хелпере. Обоснование: сигнатура задана планом. Цена ошибки: нет. 251. **A11-1** (план 3А): Тест `test_disclosure_is_confirmable` удалён: `form_blocker` принимает meta, у расшифровки её нет; не-форма проверяется вызывающими (FORM_TYPES) — **тест возвращён в волне правок (T11.1)** на уровне сервиса. Обоснование: покрытие подтверждения DISCLOSURE/OTHER. Цена ошибки: нет. 252. **A11-2** (план 3А): `confirmed_twin` при пустом периоде возвращает `None` (`PERIOD_REQUIRED` сработает раньше); первым выбирается близнец с меньшим `file_ordinal`. Обоснование: детерминизм. Цена ошибки: нет. 253. **A11-3** (план 3А): Дубль ищется только среди `CONFIRMED`; второй `NEEDS_CHECK`-близнец не блокирует. Обоснование: бриф. Цена ошибки: нет. 254. **A11-4** (план 3А): `line_id` в `extra` — строка (`str(uuid)`), как `document_id`. Обоснование: единообразие. Цена ошибки: нет. 255. **A12-1** (план 3А): Новый код 422 `DUPLICATE_COLUMN` (колонка дважды в `columns[]`). Обоснование: в брифе не было. Цена ошибки: низкая. 256. **A12-2** (план 3А): Подтверждение даты отчёта требует непустого `report_date` (null == null не подтверждение). Обоснование: смысл подтверждения. Цена ошибки: низкая. 257. **A12-3** (план 3А): Равное текущему `organization_name` — no-op. Обоснование: подтверждать нечего. Цена ошибки: нет. 258. **A12-4** (план 3А): Снятие подтверждения ячейки и возврат суммы к исходной: `needs_review` = не(правлена | подтверждена | строка detail | value_clean) — формула приёма 1.x. Обоснование: для 2.0 точное значение даст часть Б. Цена ошибки: ячейка 2.0 с иным сомнением (знак) может показаться чистой до Б. 259. **A12-5** (план 3А): `recompute` дополнительно сбрасывает `needs_review` у правленых/подтверждённых значений и значений строк `detail` (`settle_edited_values`). Обоснование: согласованность флагов. Цена ошибки: T13.3: флаг навсегда `false` у значений detail (часть Б). 260. **A12-6** (план 3А): Подтверждение ячейки без записи значения в БД — 422 `UNKNOWN_CELL` до записей; колонка не этой формы в `values[]` — `UNKNOWN_CELL`, `UNKNOWN_COLUMN` — только для `columns[]`. Обоснование: существующий тест. Цена ошибки: нет. 261. **A12-7** (план 3А): `is_current` пересчитывается только при смене дат колонок (самая поздняя дата, меньший `column_index`). Обоснование: как при приёме. Цена ошибки: нет. 262. **A12-8** (план 3А): Ключи аудита подтверждений: `value::confirmed`, `column::date_confirmed`, `needs_review_fields`; смена даты колонки — `column:`. Обоснование: единый формат. Цена ошибки: нет. 263. **A12-9** (план 3А): Смена `report_date` убирает `report_date` из `needs_review_fields` без отдельного ключа аудита. Обоснование: минимум шума. Цена ошибки: нет. 264. **A12-10** (план 3А): `lines[]` в `StatementPatch` нет в A12 (добавлено в A13). Обоснование: разбиение задач. Цена ошибки: нет. 265. **A13-1** (план 3А): Тесты используют код 1215 вместо 1250 из брифа. Обоснование: 1250 есть в источнике ББ Межениновской, а нужен «только шаблонный» код. Цена ошибки: нет. 266. **A13-2** (план 3А): `tests/test_api_statements.py::insert_line` кладёт `StatementLineStatus`, а не строку. Обоснование: identity map сервиса. Цена ошибки: нет. 267. **A13-3** (план 3А): Пометка шаблонной строки (`row_index IS NULL`) как `detail` — 422 `INVALID_LINE_PATCH`; подтверждение кода у строки не `ok` — тоже. Обоснование: спека такого перехода не перечисляет; у `detail` нужен `row_index` для родителя. Цена ошибки: низкая, фронт такого не шлёт. 268. **A13-4** (план 3А): Назначение того же кода строке `ok` с этим кодом — no-op; `detail -> detail` — no-op. Обоснование: нечего менять, аудита нет. Цена ошибки: T13.1: no-op элемент может дать ложный 409 — недостижимо. 269. **A13-5** (план 3А): Порядок проверок по элементам: действие, повтор, `UNKNOWN_LINE`, `LINE_CODE_REQUIRED`, `INVALID_LINE_CODE`, допустимость; затем отдельным проходом `LINE_CODE_TAKEN` (409 после всех 422). Обоснование: предсказуемость. Цена ошибки: нет. 270. **A13-6** (план 3А): `needs_review` при снятии подтверждения: `ok` и не `line_code_modified` и уверенность < 100. Обоснование: правило приёма 1.x; для 2.0 уточнит часть Б. Цена ошибки: нет. 271. **A13-7** (план 3А): Назначение кода: `confidence = Decimal("100.00")` (ответ отдаёт объект сессии). Обоснование: без перечитывания. Цена ошибки: нет. 272. **A13-8** (план 3А): Удаление пустой шаблонной строки пропускается, если она сама — цель элемента запроса. Обоснование: избежать удаления цели. Цена ошибки: нет. 273. **A13-9** (план 3А): **Отступление от P3-10:** 422 в `values` / `columns` после применённых `lines` откатывает всю транзакцию запроса; отдельной предпроверки адресации `values` для `lines` нет. `UNKNOWN_CELL` по `line_code` видит назначения `lines` этого запроса (они применены и flush-нуты раньше); 409 приоритетнее такого 422. Обоснование: эквивалентное наблюдаемое поведение при меньшем коде (T13.5). Цена ошибки: при ошибке в откате частичная запись — тест M-10 фиксирует отсутствие записей и аудита. 274. **A13-10** (план 3А): `user_id` в `apply_line_edits` принят по интерфейсу, не используется; аудит строк — ключ `line:` с `{line_code, line_status, parent_row_index, is_confirmed}`, в него попадают и `detail` со сменой только родителя. Обоснование: у строки нет автора правки. Цена ошибки: нет. 275. **A13-11** (план 3А): Пометка шаблонной строки как `detail` = 422 `INVALID_LINE_PATCH`. Обоснование: см. A13-3. Цена ошибки: низкая. #### План 3, часть Б (arm-core, change `unified-normalization-core-b`) Источник: `openspec/changes/unified-normalization-core-b/design.md` в репозитории `arm-core`, коммит `0f84088` (ветка исполнения; полный текст — там). Перенесено пунктов: 40. 276. **R-1 (Р-40)** (план 3Б): `CODE_NOT_IN_DICTIONARY` / `CODE_NOT_IN_EDITION` пишутся только для кодов из ровно четырёх цифр; пятизначные подстроки Excel не проверяются. **P3-7 плана в этой части заменён** (запись на код документа сохраняется, пятизначные коды — нет). Обоснование: решение владельца 18.10.2026 (Р-40) перед исполнением Б; закрывает O-3 (поток `info` у пятизначных). Цена ошибки: пропущенный `info` у нестандартного пятизначного кода; восстановление — снять условие длины кода в маппере 2.0 и пересчёте. 277. **R-2 (Р-34)** (план 3Б): Подтверждение кода строки — флаг `is_confirmed`; `line_code_confidence` и `line_code_modified` не меняются (P3-16 части А). Обоснование: решение владельца 18.10.2026; в пересчёте Б правило `is_confirmed ⇒ needs_review строки = false` берётся из P3-16. Цена ошибки: нет — поведение уже реализовано в части А. 278. **R-3** (план 3Б): Удаление копии словаря §7 в дельте оформлено как MODIFIED «Соотношения §7 по колонкам», а не REMOVED (см. D-4); «Ответ `GET …/statement`» и порядок 409 оформлены новыми ADDED-требованиями, так как в `openspec/specs/` и дельтах части А одноимённых нет. Обоснование: `openspec archive` требует, чтобы имя REMOVED/MODIFIED существовало; иначе архив упадёт. Цена ошибки: при архивации — правка имён заголовков дельт. 279. **P3-25 (B1)** (план 3Б): Тип формы во входе пересчёта — `document.document_type` (`BALANCE` / `INCOME` / `DISCLOSURE`), а не `meta.statement_type` (только `balance` / `income`); план писал «тип формы» без источника. Обоснование: `DISCLOSURE` не представим в `statement_type`; тип документа — единственный источник для всех трёх типов. Цена ошибки: форма `DISCLOSURE` собралась бы с ошибкой `KeyError` / потерей типа; правка — одна строка в `build_stored_form`. 280. **P3-26 (B1, O-3 часть А)** (план 3Б): Копия `statement-code-orders.yaml` в `api/contracts/` не заводится: перечень кодов читает пакет из своей байтовой копии `api/arm_statements/rules/statement_code_orders.yaml` (`loader._ORDERS`), ядро файла не открывает; идентичность копии пакета с `docs/contracts/statement-code-orders.yaml` проверяет `sync-statements --check`; тест идентичности контракта сверяет только `*.json`. Сейчас копия пакета побайтно равна файлу docs. Обоснование: второй экземпляр файла в ядре никто бы не читал и он мог бы разойтись с пакетом. Цена ошибки: нет; если sync-statements когда-либо перестанет сверять YAML — добавить сверку в `contract_identity.py`. 281. **P3-27 (B1)** (план 3Б): `edition_codes` берёт строки `ok` результата `2.0`, сортирует по `row_index` и помечает код уверенным при `line_code_confidence == 100` (ровно), `None` — неуверенный. Обоснование: как в разделе «Что ядро вызывает» и в `recompute` пакета (код правленый/подтверждённый там — `code_fixed`). Цена ошибки: редакция приёма разойдётся с редакцией пересчёта; сверку делает `test_edition_codes_and_engine_edition`. 282. **P3-28 (B2)** (план 3Б): `StatementService` получает `StatementsEngine` через зависимость `get_statements_engine` (тесты подменяют её `FakeEngine`); `FailedRelation`, `StatementChecks`, `PeriodKind` реэкспортируются из `services/statements_engine.py`, чтобы `import arm_statements` оставался только в двух файлах. Обоснование: единственная точка импорта пакета (common.md); подмена без патчей. Цена ошибки: нет. 283. **P3-29 (B2)** (план 3Б): Статус после правки — `form_status_v2`: `service_done = (result.status == DONE)`, `file_error_warning` — `metrics.warnings` задания, `names_ok` — нет `NAME_MISMATCH` пересчёта, `duplicate_in_file` — `duplicate_period_forms` по формам файла (`file_form_periods`); `statement_status` D-6 с пути правки убран. Обоснование: один источник статуса с приёмом `2.0` (Р-16, Р-18, Р-19). Цена ошибки: форма с повтором периода в файле после правки остаётся `NEEDS_CHECK` — как при приёме. 284. **P3-30 (B2)** (план 3Б): Пустая цель при непустых членах — нарушение с `expected = NULL` (P1-21); тесты части А «очищенная 1700 не цель» и «цель потерянной строки не проверяется» переписаны под это; потерявшая строку 1700 форма блокирует подтверждение до правки или акцепта. Обоснование: следствие P1-21 пакета; спека («посчитанное соотношение»). Цена ошибки: форма, потерявшая итоговую строку, теперь требует действия аналитика — если владелец против, правится правило пакета (план 1), не ядро. 285. **P3-31 (B2)** (план 3Б): `NOTHING_TO_ACCEPT` и `CHECKS_NOT_PASSED` читают записи `relation`; акцептованная форма не считается нарушенной (`statement_hard_failures` пуст при `checks_accepted_at`), поэтому повторный акцепт — 409 `NOTHING_TO_ACCEPT`, как в части А. Обоснование: сохранение поведения А; буква брифа («нет записей `relation`») допускает оба прочтения. Цена ошибки: повторный акцепт с новым комментарием станет возможен, если снять условие. 286. **P3-32 (B2)** (план 3Б): «Эффективная дата» для `relative:*` колонок при правке `header.report_date` — новое значение `meta.report_date` (в том числе `NULL` ⇒ `period_date = NULL`), не `document.reporting_date`; изменение дат колонок идёт в аудит ключом `column:` без признака правки данных. Обоснование: период документа сам выводится из даты отчёта и дат колонок (§9.5 п. 2), поэтому циклическая зависимость недопустима. Цена ошибки: колонка при пустой дате отчёта теряет дату; аналитик ставит её вручную. 287. **P3-33 (B2)** (план 3Б): Запись `relation` / `soft` несёт и `column_id` (колонка найдена по `column_key`: `str(column_id)`, затем `str(column_index)`), и `column_index` нарушения; ответ берёт индекс из колонки, иначе из записи, иначе `-1`. Обоснование: ключ колонки B4 для `2.0` — индекс. Цена ошибки: нет. 288. **P3-34 (B2)** (план 3Б): `NAME_MISMATCH` пересчитывается для всех кодов (R-1 ограничивает только `CODE_NOT_IN_*`); `CODE_NOT_IN_*` не пишутся, если пакет не определил редакцию (а `CODE_NOT_IN_EDITION` — и вариант); словарь имён берётся по редакции пакета, не по дате. Обоснование: R-1 и спека (редакция документа — `edition` пакета). Цена ошибки: пятизначная подстрока с уверенностью < 100 даёт `NAME_MISMATCH` (кода нет в словаре) — если поток окажется шумным, ограничить `name_mismatches` четырёхзначными. 289. **P3-35 (B2)** (план 3Б): Временный код A12/A13 удалён: тело `StatementService.recompute`, `settle_edited_values`, `has_review_values`, `has_source_lines`, `_value_needs_review`, `nearest_ok_parents` и `_recompute_parents`; `parent_row_index` `detail` пишет только пересчёт пакета. Обоснование: задание B2; T13.3 (флаги detail больше не затираются навсегда). Цена ошибки: нет. 290. **P3-36 (B3)** (план 3Б): `checks_on_intake_1x` живёт в `statement_recompute.py`; движок передаётся в `_insert_statements` необязательным параметром (по умолчанию `default_engine()`), `map_file` сигнатуру не меняет. Приём `1.x` пишет `meta.form_variant` и записи `relation` / `soft` (`core`), но не `CODE_NOT_IN_*` / `NAME_MISMATCH` (P3-7) и не трогает `needs_review` (A9); условие D-6 «проверки сошлись» — `not result.checks.relations_failed` (нарушение с `expected = NULL` — жёсткое). Обоснование: задание B3, спека «Пересчёт при приёме»; расхождение с прежним `run_checks` (его `failed`) возможно только на формах с пустой целью, где пакет строже. Цена ошибки: форма `1.x` с пустой целью получит `NEEDS_CHECK` вместо `RECOGNIZED`. 291. **P3-37 (fix1, T2.1)** (план 3Б): Аудит `lines[]` довносит смену `parent_row_index` после пересчёта: у правленных строк значение заменяется итоговым, у остальных появляется запись `line:` с одним ключом `parent_row_index` (`{"parent_row_index": старый}` → `{…: новый}`). Обоснование: родителей пишет `recompute_form` уже после снимка строк правки; полный снимок для не правленных строк не нужен. Цена ошибки: в событии `RECOGNIZED_DATA_EDIT` у не правленных строк неполный набор ключей. 292. **P3-38 (fix1, T1.1/T1.2/T2.4/T2.6/T2.8)** (план 3Б): Паритет редакции приёма и пересчёта для строки с `line_code_confidence < 100` и `code_fixed` подтверждён тестом (расхождения нет, код не менялся); `edition_codes` пропускает строку `ok` без кода; `column_index` нарушения — общим помощником `issue_column_index` (запись, иначе колонка по `column_id`) в ответе и в `detail` отказа; `_unconfirmed_needs_review` и присвоения `needs_review` в `_apply_item` удалены (флаг пишет только пересчёт, он всегда идёт после правки строк); неизвестный `relative:*` в `_resolve_relative_columns` пропускается без ошибки. Обоснование: замечания ревью B1+B2. Цена ошибки: колонка с неизвестным `relative:*` не следует за датой отчёта. 293. **P3-39 (B4)** (план 3Б): Проверки ядра `info` (`CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`, `STATEMENTS_VERSION_MISMATCH`, `RESULT_STATUS_INCONSISTENT`, `code_issues`, `name_issues`, `version_issue`, `name_check_lines`) вынесены из `services/statement_recompute.py` в `domain/statement_issues.py`; единая функция `core_info_issues(lines, *, form, edition: str | None, order: Collection[str] | None, catalog) -> CoreInfo(drafts, mismatched)` используется приёмом `2.0` и `recompute_form` (поведение пересчёта не изменилось, P3-34). Домен пакет `arm_statements` не импортирует (исключение — `domain/stored_form.py` по D-1): редакцию (`engine.edition`) и перечень кодов (`engine.code_order`) считает сервис; `statement_recompute` реэкспортирует прежние имена. Вместо сигнатуры плана `core_info_issues(*, lines, edition, form_variant, catalog, engine)`. Обоснование: домен не должен знать `StatementsEngine`/`arm_statements` (D-1); одно правило для приёма и пересчёта. Цена ошибки: нет; импортёры прежних имён из `statement_recompute` работают. 294. **P3-40 (B4)** (план 3Б): Приём форм `2.0` — отдельный сервис `services/statement_intake_v2.py` (`map_statement_docs_v2`, `insert_forms_v2`); `map_file` выбирает маппер по типу результата через `_plan_forms` / `_insert_forms` (иначе функция превышала лимит ruff по операторам) и получил необязательный параметр `engine`. `_load_result` и тип в `process_once` остаются под `1.x` до B5. Обоснование: размер `result_mapping.py` и лимиты линтера; граница B4/B5 из брифов. Цена ошибки: нет. 295. **P3-41 (B4)** (план 3Б): `DISCLOSURE` в `2.0` — по-прежнему один документ на файл со статусом `RECOGNIZED` (D-3), `file_ordinal` первого; предупреждения всех объединённых документов результата пишутся в его `document_issue` источником `service`. Статус сервиса и предупреждения файла уровня `error` на него не влияют. Обоснование: спека условие `RECOGNIZED` относится к формам ББ/ОФР; у `DISCLOSURE` нет данных. Цена ошибки: `DISCLOSURE` с `needs_check` сервиса автоподтверждается (данных нет — подтверждать нечего). 296. **P3-42 (B4)** (план 3Б): `STATEMENTS_VERSION_MISMATCH` пишется и при приёме `2.0` (`version_issue(job.statements_version, engine.version)`), а не только при пересчёте после правки. Обоснование: иначе запись появлялась бы только после первой правки; один набор записей `core` у приёма и пересчёта. Цена ошибки: лишний `info` на формах, принятых копией иной версии. 297. **P3-43 (B4)** (план 3Б): Результат с повтором `row_index` или `ref` у строк: вторая и далее строка пропускается с `MAPPING_WARNING`; `detail` с `parent_ref`, которого нет, хранится с `parent_row_index = null` и тем же предупреждением. Обоснование: уникальность `(meta_id, source_ref)` и ключа строки при вставке иначе роняет приём (файл `ERROR` после попыток); схема уникальности `ref`/`row_index` не проверяет. Цена ошибки: потерянная строка дубля видна только в предупреждении. 298. **P3-44 (B4)** (план 3Б): Запись `relation` / `soft` из `checks` результата несёт `column_id` (колонка с таким `column_index`, если есть) и всегда `column_index` (как P3-33); запись `warning` с `column_index` — либо `column_id` (колонка есть), либо `column_index` (колонки нет). Обоснование: спека «Пропущенная колонка» и ответ API, читающий индекс из записи. Цена ошибки: нет. 299. **P3-45 (B4)** (план 3Б): `is_current` колонок `2.0` — как у `1.x` (колонка с самой поздней датой, при равенстве меньший `column_index`; нет дат — `NULL`), в том числе у колонок `relative:*` с датой; значение, которого нет в результате, пишется с `is_recognized = NULL`, остальными полями пустыми и `needs_review = false`. Обоснование: §9.5 п. 2; спека «Хранение результата формы v2.0». Цена ошибки: колонка `relative:*` без даты не станет текущей. 300. **P3-46 (B5)** (план 3Б): `_load_result` выбирает модель по MAJOR через `contract.versions.parse_result`; неизвестный MAJOR, неверный формат версии и любое нарушение схемы — `MappingFatal` с итогом проверки схемы (`результат не соответствует схеме recognition-result: ошибок N, первая: …`), файл `ERROR`, без повтора. Обоснование: спека «Приём файла результата»; D-2. Цена ошибки: файл с MAJOR 3 не мапится, а не обрабатывается молча как `1.x`. 301. **P3-47 (B5, T5.2 части А)** (план 3Б): Нечитаемый JSON (`JSONDecodeError`, не UTF-8) в `_load_result` — тоже `MappingFatal` с текстом «результат не соответствует схеме recognition-result: файл не является JSON» (текст исключения парсера пользователю не показывается; M-1); не-объект (`[]`, `null`) отсекается моделью как нарушение схемы. Обоснование: иначе `ValueError` из `parse_result` уходил бы в повторяемые ошибки и файл `ERROR` получал бы только после `MAPPING_MAX_ATTEMPTS` попыток. Цена ошибки: ошибка JSON не отличается в тексте от прочих ошибок схемы. 302. **P3-48 (B5)** (план 3Б): Статус ОСВ `2.0` — `osv_status_v2(osv_done = sheet.status == "done", checks_ok, file_error_warning, counterparty, sheet_mapping_warnings)`; «предупреждения маппинга листа» = повтор счёта + сироты + «нарастающая по данным Q1» (вход `document_status` `1.x` тот же); у `1.x` статус как прежде (§9.3, статус задания). Предупреждение уровня `error` ищется в `metrics.warnings` результата (`file_warnings`), а не в `recognition_job.metrics`. Обоснование: Р-28; единый источник с формами. Цена ошибки: ОСВ с `error` в метриках файла — `NEEDS_CHECK`. 303. **P3-49 (B5)** (план 3Б): `osvDocument.warnings` пишутся в `document_issue` как `warning` / `service` с `severity` из предупреждения (по контракту `info`, `error` не отбрасывается); `line_refs` у ОСВ не сохраняются (строк формы нет), `column_index` и `field` — как пришли. Записи сервиса идут раньше записей маппинга ядра. Дубль листа (повтор счёта) своих предупреждений/статуса не вносит — документом остаётся первый лист. Обоснование: у ОСВ нет `line_ids`/колонок формы; повтор — D-7. Цена ошибки: `error`-предупреждение листа ОСВ в `issues` не влияет на статус само по себе (статус — по `status` листа). 304. **P3-50 (B6)** (план 3Б): `FailedCheck.codes` в ядре остаётся (вопреки плану, который удалял поле): оно нужно межформенной проверке 1370↔2400 (`codes = ("1370", "2400")`) для `codes` / `line_ids` в `checks.cross_form` ответа; нарушения §7 по колонкам читаются из `document_issue`, где коды хранятся сами. Обоснование: межформенную проверку пакет не считает (она по двум документам), без `codes` подсветка строк в ответе пропала бы. Цена ошибки: у `cross_form` не было бы `codes`/`line_ids`, фронт не подсвечивал бы строки 1370 и 2400. 305. **P3-51 (B6)** (план 3Б): `checks_report` уже брал допуск у `engine.tolerance()` (с B2), поэтому правки сервиса в B6 нет; константа допуска ядра удалена вместе с `load_check_rules`. Состав и порядок редакций справочника имён `statement_lines.yaml` не переписывается на `engine.editions()` в коде: согласованность проверяет тест `test_name_dictionary_codes_in_code_orders` (редакции справочника = редакции пакета в том же порядке; каждый код справочника есть в перечне редакции пакета, любой вариант формы). Обоснование: справочник остаётся данными имён и состава экрана; перечни кодов — единственный источник в пакете, расхождение ловит тест, а не молчаливая подмена в рантайме. Цена ошибки: при добавлении редакции в пакет без справочника имён тест красный; в рантайме новая редакция дала бы форму без шаблонных строк. 306. **P3-52 (B6)** (план 3Б): `edition_for` (граница 66n/fsbu4 по дате для шаблона экрана) в `domain/statement_mapping.py` не тронут: переход на `engine.edition(...)` требует кодов формы и варианта и относится к ответу формы (B7/доработка), а не к удалению копии словаря. Обоснование: не расширять B6 за рамки брифа. Цена ошибки: ответ формы и приём могут расходиться в редакции на формах без даты; существующее поведение сохранено. 307. **P3-53 (fix-волна, I-3, R-11)** (план 3Б): Источник «ошибки файла» единый: при приёме результата `2.0` `map_file` сохраняет `result.metrics.warnings` структурами в `recognition_job.metrics.file_warnings` (рядом с `mapping_warnings`, прочие ключи не затираются, `_mapped_metrics`); `recompute_form` читает его через `file_error_in_metrics(metrics)`, при отсутствии ключа (результат `1.x`, задания до части Б) — прежний `metrics.warnings` конверта. Приём (`insert_forms_v2`, ОСВ) по-прежнему читает результат напрямую. Обоснование: раньше приём смотрел предупреждения файла результата, а пересчёт — конверта: статус формы мог измениться от несвязанной правки. Цена ошибки: при расхождении конверта и результата форма менялась бы `NEEDS_CHECK` ↔ `RECOGNIZED` без причины; задание без ключа читается по-старому. 308. **P3-54 (fix-волна, I-2)** (план 3Б): `build_stored_form` передаёт пакету `is_recognized = value.is_recognized is not False`: `NULL` (заглушки маппера `1.x`/`2.0`, ячейки, созданные правкой) — не сомнение, как в `value_clean` ядра и в спеке пакета («позиции нет → `is_recognized = true`»). Прежнее `bool(...)` превращало `NULL` в `False`, и после любой правки шапки значения без сомнений получали `needs_review`, а форма — `NEEDS_CHECK`. Обоснование: `is_recognized = NULL` означает «признака нет», а не «не распознано». Цена ошибки: форма из заглушек или с созданной ячейкой уходила бы в `NEEDS_CHECK` от правки шапки. 309. **P3-55 (fix-волна, T4.2)** (план 3Б): `checks_on_intake_1x(..., now)` принимает момент приёма параметром (из `map_file`), а не `utc_now()`: записи `relation` / `soft` формы `1.x` получают тот же `created_at`, что и остальные записи приёма. Обоснование: единое «сейчас» приёма, воспроизводимость тестов. Цена ошибки: различие `created_at` на миллисекунды между записями одного приёма; порядок записей в ответе мог бы нарушиться. 310. **P3-56 (fix-волна, M-4)** (план 3Б): `statement_mapping_v2._lines_and_values` строит `ref → row_index` только по принятым строкам (первый вход побеждает; ссылка вперёд на ещё не встреченную принятую строку допустима): `parent_ref` строки, чей `ref` повторяет прежний, указывает на первую принятую строку, а не на пропущенный дубль (раньше побеждал последний). Обоснование: пропущенная строка не должна становиться родителем; детализация висела бы на несохранённом `row_index`. Цена ошибки: расшифровка привязана не к той строке; пересчёт `detail_parents` всё равно исправляет её после первой правки. 311. **P3-57 (fix-волна, T4.4, T6.1)** (план 3Б): Закрыты тестами ветки приёма `2.0`: пропуск дубля `row_index` / `ref`, потерянный `parent_ref`, `STATEMENTS_VERSION_MISMATCH` (и его отсутствие при совпадении версии), `DISCLOSURE` с предупреждением `error` (документ `RECOGNIZED`, запись сохранена; вопрос владельцу P3-41 остаётся открытым), `edition is None` (нет `CODE_NOT_IN_*`, `NAME_MISMATCH` остаётся), предупреждение листа ОСВ `error` не блокирует подтверждение (P3-49). Обоснование: покрытие веток без изменения поведения. Цена ошибки: нет. 312. **P3-58 (fix-волна, I-4)** (план 3Б): Тесты правки и акцепта формы, принятой маппером `2.0` (`tests/test_statements_v2_edit.py`): записи `relation` / `soft` источника `service` после первой правки заменяются записями `core`; `warning` сервиса уровня `error` и `RESULT_STATUS_INCONSISTENT` сохраняются и держат `NEEDS_CHECK`; `RECOGNIZED` не меняется от правки шапки; акцепт нарушения, найденного ядром. Наблюдение: «чистая» форма с фабрики `balance_lines()` по реальному пакету не сходится (1300 без членов) — для тестов пересчёта берётся согласованная форма. Обоснование: тест должен ловить регрессию, а не проходить на форме, у которой пересчёт и так находит нарушение (мутационная проверка I-2 подтверждает). Цена ошибки: тесты на несогласованной форме проходили бы вхолостую. 313. **P3-59 (fix-волна, M-5, M-6, M-7, T6.3)** (план 3Б): Документация: CLAUDE.md репозитория исправлен под P3-30 (пустая цель при заполненных членах — нарушение с `expected = NULL`), «сбрасывает акцепт» — только правка данных (Р-25), добавлено описание приёма результата `2.0`; README — формы, принятые кодом части А, теряют проверку `CHECKS_NOT_PASSED` до первой правки (§7 в `document_issue` у них нет; обход — очистка форм `purge_statements.py`). P3-39 уточнён: домен не импортирует `arm_statements`, исключение — `domain/stored_form.py` (D-1). Обоснование: документация должна совпадать с кодом. Цена ошибки: читатель CLAUDE.md/README получал бы неверные гарантии о проверках и акцепте. 314. **P3-60 (fix-волна, I-5)** (план 3Б): В ответах нарушение §7 может иметь `expected` / `actual` = `null` (пустая цель при непустых членах, P1-21). Индекс колонки при её отсутствии различается: `checks.failed[]` ответа `GET …/statement` отдаёт `-1` (`FailedCheckOut.column_index: int`, межформенная `cross_form` всегда `-1`), а `detail.failures[]` отказа 409 `CHECKS_NOT_PASSED` — `null` (`issue_column_index`). Спека описывает поведение как есть; код не правился. **Вопрос на выравнивание (`-1` или `null`) — план 6 / фронт (arm-web)**. Обоснование: фиксация фактического контракта до решения владельца; тип поля у фронта (arm-web) нужно сверить. Цена ошибки: фронт, ожидающий число, получит `null` в `detail.failures[].column_index` (только в 409, когда у записи нет ни `column_id`, ни `column_index`). 315. **P3-61 (fix-волна, C-1, M-1)** (план 3Б): Дельты B переписаны под `openspec archive`: MODIFIED «Пересчёт после правки», «Словари ядра и достройка шаблонных строк», «Формы результата 1.x — §7 при приёме» теперь несут полный текст требования части А со всеми сценариями плюс уточнения Б (иначе архивация падает: «scenario(s) not present in the modified block»); добавлен MODIFIED «Хранение результата формы v2.0 (DBML v5.19)» и «Форма ББ/ОФР для экрана проверки» (сценарий «Строка, потерянная источником» приведён к P3-30). ADDED «Маппер результата 2.0 для форм» заменён MODIFIED «Хранение…» (устранено противоречие «первое остаётся» vs «без повторной дедупликации») и ADDED «Проверки ядра при приёме результата 2.0»; ADDED «`CHECKS_NOT_PASSED` из `document_issue`» и «Блок `checks`…» оставлены только с тем, чего нет в А (форма элемента нарушения, `null`), дубль порядка 409 убран. Проверено архивацией А, затем Б на копии `openspec/`. **Дополняет R-3 и D-4**. Обоснование: архивация обязана проходить; дубли и противоречия между А и Б вводят в заблуждение. Цена ошибки: если архивация А и Б падает — править заголовки и сценарии дельт Б; при будущей правке требований А в самой А тексты MODIFIED Б нужно обновить синхронно. #### План 4 (tabular-parser, change `unified-normalization-tp`) Источник: `openspec/changes/unified-normalization-tp/design.md` в репозитории `tabular-parser`, коммит `b583b04` (ветка исполнения; полный текст — там). Перенесено пунктов: 30. 316. **P4-1** (план 4): Переход на общий код и на контракт v2.0 — одна задача (7): модели, копия схем, сборка книги, путь ОСВ и воркер меняются вместе; «контракт v2.0 в копии» отдельной последней задачей не вынесен — модели 2.0 без копии 2.0 (или наоборот) валят `result_contract` и тест идентичности. Golden и манифест — сразу за переключением (задача 8), до удаления старого кода; между задачами 7 и 8 проверка идёт с `ARM_KA_FIXTURES=/nonexistent`. Цена: одна крупная задача и окно в одну задачу без проверки на реальных файлах. 317. **P4-2** (план 4): Адаптер пишется новыми модулями рядом со старыми (`cells`, `forms`, `header_cells`, `adapter`, `contract`, `pipeline`); старые `statements/*` не правятся и удаляются целиком задачей 9. Цена: временное дублирование логики поиска блоков и шапки. 318. **P4-3** (план 4): `Cell.text` числа Excel — как в русской локали: группы по три через пробел, если в формате есть разделитель групп; десятичная запятая; литералы секции (скобки) сохраняются; «-» добавляется только у односекционного формата; `General`/неразобранный формат/xlsb — число без групп и экспоненты, целое без дробной части; дата — `dd.mm.yyyy`; логическое — «ИСТИНА»/«ЛОЖЬ». Цена: у экзотических форматов текст может отличаться от экрана Excel; `value` и статус от этого не зависят (у Excel значение берётся из `value`). Совпадает с примером docs `result-statement-excel.json` (`"586 260,26"`). 319. **P4-4** (план 4): Показываемое число вне `numeric(18,2)` больше не делает формат «неразобранным»: адаптер отдаёт `Decimal`, а `VALUE_OUT_OF_RANGE` и `amount = null` выдаёт пакет (спека «Шаг 4»). Цена: правка поведения #42 для чисел больше 16 целых цифр (на реальных файлах их нет). 320. **P4-5** (план 4): Сетка кандидата: строки блока целиком, включая пустые; объединённая ячейка — только в позиции левой верхней, прочие позиции `None`; ширина — наибольшая среди листов кандидата. Цена: подпись, объединённая на два столбца, видна пакету только в левом. 321. **P4-6** (план 4): Шапка ищется в строках блока выше первой строки с кодом его столбца кодов (столбец — `code_column` пакета: то же правило (а)/(б) шага 2 с защитой от блока «Коды», своего правила у адаптера нет), без кодов — во всём блоке: так титульный блок «Коды» (ОКОПФ 12267) не мешает, а подписи под формой (ИНН и ФИО в блоке подписей) не попадают в шапку. Цена: метка шапки ниже начала таблицы не найдётся. 322. **P4-7** (план 4): Шаблоны словаря пакета адаптер применяет к тексту, нормализованному функцией пакета по таблице «Словари» раздела API плана 1: метки шапки, заголовки, ОКУД, `auditor`, `legal_form` — `normalize_label` (= прежний `normalize_text` tabular-parser, P1-22); подпись «Код» и относительные периоды — `normalize_caption`; маркер продолжения — `is_continuation`. Цена: нет — смена нормализации в пакете меняет и `type_hint`, и адаптер. 323. **P4-8** (план 4): Склейка: открытая форма — последний кандидат с `type_hint` `BALANCE`/`INCOME`; лист с маркером сверяется только по типу; лист без маркера (сильное продолжение — столбец кодов по правилу шага 2 или своя строка заголовка; слабое — столбец кодов ослабленным поиском `code_column(…, relaxed=<тип формы>)`, минимум `min_codes_continuation` кодов, сразу после листа формы) — `sheet_joins` с числом колонок сумм `len(amount_columns(...))` пакета («столбцы правее столбца кодов, где в строках с кодами есть число»); открытой формы нет — отдельный кандидат без предупреждения (пакет даст `NO_ORGANIZATION_NO_INN`). Цена: «Изменение, %» с числами считается колонкой сумм с обеих сторон сравнения; лист с иной шапкой, но тем же числом колонок, присоединится и будет сопоставлен пакетом по датам. 324. **P4-9** (план 4): `SHEET_OTHER` для листов «прочее» выдаёт книга сама (документа и кандидата нет), а не пакет по кандидату без `type_hint`. Цена: два места порождения одного кода (у пакета — для кандидатов, не нашедших ни типа, ни кодов). 325. **P4-10** (план 4): `XLSB_NO_FORMATS` — у каждого кандидата книги xlsb (P1-5); `NUMBER_FORMAT_UNPARSED` — одно на кандидата с перечнем форматов всех его листов. Цена: у пояснений xlsb тоже есть `XLSB_NO_FORMATS` (`info`). 326. **P4-11** (план 4): Книга вызывает `normalize_candidate` по одному кандидату, а не `normalize`: так проверяется дедлайн между кандидатами (`header.form_name` пакет ставит сам из `evidence.title`, пара «кандидат → документ» не нужна). Исключение пакета на кандидате — `RECOGNITION_FAILED` всей книги, как сейчас (`TABLE_PARSE_ERROR` — код таблицы ИС). Цена: одна сломанная форма роняет книгу с ОСВ. 327. **P4-12** (план 4): Коды ОСВ — по источнику предупреждения, не по тексту: `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` разные случаи различаются только текстом. 328. **P4-13** (план 4): Заметки читателя: несовпадение формата — `FILE_TYPE_MISMATCH` файла; «xls без кодовой страницы» — `XLS_NO_CODEPAGE` файла (`info`, код пакета, P1-23) в любой книге xls — касается и форм, и ОСВ; «xlsb без outline» — в каждый документ ОСВ книги (`OSV_XLSB_NO_OUTLINE`), в книге без ОСВ только логируется (иерархия строк — только у ОСВ). Цена: предупреждение о кодировке видно в карточке файла, а не у листа ОСВ. 329. **P4-14** (план 4): Предупреждение «форма не разобрана: книга завершилась ошибкой листа ОСВ» снято: в v2.0 для него нет кода, а текст ошибки и так называет лист ОСВ. Цена: аналитик не видит списка листов форм, оставшихся неразобранными. 330. **P4-15** (план 4): Статус документа ОСВ — прежние условия листа: контрольные суммы §1.4 сошлись и строки ИНН без нарушений (`glue.ok`, #35). Цена: формулировка спеки «когда контрольные суммы сошлись» читается шире. 331. **P4-16** (план 4): Нет `api/arm_statements/VERSION` — сервис не стартует (`RuntimeError`), как при ошибке словаря; в golden `service.statements_version` — volatile-поле. Цена: копия без `VERSION` не запустится даже локально. 332. **P4-17** (план 4): Новые синтетические случаи (форма xlsb, книга из пояснений, конфликты продолжений) — в памяти в юнит-тестах (`sheet_from_rows`, `write_xlsb`), новых файлов в `fixtures/statements-xlsx/synthetic` нет; PR fixtures — только golden. 333. **P4-18** (план 4): Манифест получает необязательное поле `error_warnings` (точное множество кодов `error` документа); проверка уникальности кодов строк снята (повторы законны, Р-4). Цена: при правке пакета, меняющей набор предупреждений, манифест правится вместе с golden. 334. **P4-19** (план 4): Тест покрытия словарей tabular-parser больше не охватывает словари форм — они в пакете. Цена: «мёртвый» синоним заголовка или метки шапки на реальных файлах не ловится (O-8). 335. **P4-20** (план 4): В tabular-parser — свой change `unified-normalization-tp` с дельтами `statement-parsing`, `tabular-recognition-service`, `osv-parsing` (по образцу P0-5); требование «Копия контракта идентична источнику» правит план 0, здесь не трогается. 336. **P4-21** (план 4): Golden — отдельные issue, ветка и PR в RSHB/arm-ka-fixtures (один PR на репозиторий); сливается сразу за PR tabular-parser. Цена: короткое окно, когда `make check` `main` одного из них красный на golden. 337. **P4-22** (план 4): Две формы на одном листе дают два документа с одинаковым `locator` (спека уникальности `locator` не требует; `ref` строк различаются). 338. **R-2** (план 4): Предупреждение tp #47 «xls без кодовой страницы…» — `XLS_NO_CODEPAGE` пакета (info, файл, без locator); нового `OSV_*`-кода нет; текст сообщения reader'а tp #47 сохранён; передаётся флагом `BookRead.no_codepage`. Цена: правка `book.py` и golden. 339. **R-5** (план 4): Проверка диапазона numeric(18,2) в `display_amount` не возвращается (P4-4); диапазон и `VALUE_OUT_OF_RANGE` выдаёт пакет; старый конвейер удалён задачей 9. Цена: нет после удаления. 340. **R-6** (план 4): Для |raw| ≥ 1e15 текст/значение числа без округления до 15 значащих цифр (иначе число из 17 цифр теряет цифры); пробел групп — обычный U+0020 как в примере docs `result-statement-excel.json`. Цена: расхождение с NBSP русского Excel, на value/статус не влияет. 341. **R-7** (план 4): Ячейки из одних цифр не считаются подзаголовком даты `report_date` (иначе год из тройки «число|месяц|год» принимается за подзаголовок), как в прежнем `header.py`. Цена: подзаголовок из голого числа «2025» не распознаётся. 342. **R-8** (план 4): Ячейка в строке-шапке таблицы (в строке есть подпись «Код» по словарю пакета `code_caption`) не заголовок формы: подпись колонки «Пояснения» (`'^пояснени'` в `form_titles` пакета) делала лист-продолжение НПЗ ещё одной формой DISCLOSURE. Дефект адаптера (спека называет «Пояснения» подписью колонки), пакет не правится. Решение владельца: оставить, посмотреть позже. Цена: компактная шапка, где заголовок формы и «Код» в одной строке, потеряет заголовок. 343. **R-9** (план 4): Предупреждения адаптера при кандидате без документа — предупреждения файла со своим `locator`; порядок `metrics.warnings` — сначала reader, затем по номеру первого листа; `XLS_NO_CODEPAGE` и `OSV_XLSB_NO_OUTLINE` — флаги `BookRead` (`no_codepage`, `no_outline`). Цена: правка `book.py` и golden. 344. **R-10** (план 4): НПЗ, ОФР, строки «Реализация иного имущества, прав требования, прочих работ и услуг» (F34, F44): значения 0 с форматом `[=0]"-"` — только прочерки, суммы нет; по спеке «строка без кода и без содержимого — не строка данных», пакет её не выдаёт; значение прочерка не превращаем в 0 (сломало бы различие null/0 и соотношения §7). `test_npz_documents` не требует detail/parent_ref 2340. Вопрос владельцу O-8.2: сценарий спеки «Реализация иного имущества… с суммами» не соответствует файлу. Цена: строки из прочерков не видны аналитику. 345. **R-13** (план 4): Ложная склейка/дедлайн: `plan_sheets` получает `deadline` (финальное ревью I-1, регрессия таймаута против main). Цена: нет. #### План 5 (scan-gateway, change `statements-v2`) Источник: `openspec/changes/statements-v2/design.md` в репозитории `scan-gateway`, коммит `c5e4d0f` (ветка исполнения; полный текст — там). Перенесено пунктов: 27. 346. **P5-1** (план 5): Сетка OCR — одна строка сетки на строку ИС по возрастанию `row_index`, пропуски `row_index` пустыми строками не заполняются; ширина строк таблицы — `max(column_index) + 1`. «Плотная по `row_index`» прочитано как «по порядку `row_index`», `ref` всё равно несёт номер ИС. Цена: шаг 2 не видит «дыры» между строками (в реальных json их нет). 347. **P5-2** (план 5): Нарушенная структура таблицы ИС (повтор `row_index`, повтор `column_index` в строке, нецелые индексы, `rows` не список) — исключение адаптера → `TABLE_PARSE_ERROR` всей таблицы. Сегодня такие ячейки молча пропускались или перезаписывались; повтор `row_index` сломал бы уникальность `ref`. Цена: одна битая ячейка теряет таблицу (видно — `error`). 348. **P5-3** (план 5): `value` ячейки: `None` → `""`, не строка → `str(value)`, без `strip`; `confidence`: число (не `bool`) → `[0, 100]`, иначе `0` (как `clamp_confidence`, §11.1 «NaN → 0»). Цена: числовой `value` ИС (не встречался) даст текст `"1234.0"`. 349. **P5-4** (план 5): Ячейка ИС с пустым текстом — `Cell("", …)`, а не `None`: ИС отдаёт ячейку на каждую позицию строки, `None` — только там, где ячейки нет. Цена: нет. 350. **P5-5** (план 5): Шапка: поле ИС отсутствует, не объект или `form_name` пустой → `None` (а не `Cell("", 0)`); `ref` полей шапки — `data_ocr.<поле ИС>` (пакет `ref` шапки в выход не переносит — у `HeaderField` его нет, план 1, раздел API); `data_ocr.label` (заголовок комплекта) не используется. Цена: `<поле>_text` пустого поля — `null`, а не `""`. 351. **P5-6** (план 5): `evidence.title = Cell(label.form_name, …, ref="table:.label")`; `header.form_name` контракта — `document.header.form_name` пакета (= `evidence.title.text` как в источнике, пустой — `null`, план 1 P1-29); шлюз `form_name` сам не ставит и пару «кандидат → документ» не хранит. 352. **P5-7** (план 5): Успешный json без `data_ocr`, без `table_structure` или с пустым `table_structure` — `JSON_FAILED` (`error`): «ничего не выбрасывать молча» (§11, принцип 2). Цена: если ИС вернёт успешный пустой json рядом с нормальными, конверт будет `needs_check`. Q-1 закрыт решением владельца Р-32 (подтверждает выбор плана). 353. **P5-8** (план 5): Все json набора ошибочны — конверт `error` с кодом первого, а `metrics.warnings` всё равно перечисляют `JSON_FAILED` каждого json (как строки Среза 0): иначе коды остальных json теряются. Цена: спека называет `JSON_FAILED` «при успешном соседе»; на `error`-конверт ядро `RECOGNIZED` не считает, вреда нет. Q-2 закрыт решением владельца Р-33 (подтверждает выбор плана). 354. **P5-9** (план 5): `TABLE_PARSE_ERROR.message` = `BACKEND_PARSING_ERROR: <тип>: <текст>` (текст до 300 символов) — код старого §11.1 п. 9 сохранён в тексте для поиска по логам и golden. Цена: нет. 355. **P5-10** (план 5): Предел `metrics.warnings` (100 структур, `message` до 1000 символов) сохранён; при переполнении остаются все `error`, затем `info`; строки-сводки нет — новый код предупреждения вводится только словарём пакета. Список в конверте и в файле результата одинаков (спека). Цена: при > 100 предупреждениях часть `info` не видна ни там, ни там (сырой ответ — в `raw/`). 356. **P5-11** (план 5): `statements_version` читается при создании `OcrNormalizer`; нет `VERSION` — сервис не стартует. Версия едет в `NormalizeOutcome.statements_version` и в `service.statements_version` файла результата; в конверт не пишется (в схеме конверта поля нет). Цена: dev-checkout без `VERSION` не стартует (копию всегда раскладывает скрипт). 357. **P5-12** (план 5): `api/rules/` удалён целиком, `SCAN_RULES_DIR` убран из настроек; словари — `default_rules()` пакета. Цена: переопределить словарь без релиза нельзя (и не нужно — словари меняются через `arm-statements`). 358. **P5-13** (план 5): `CONTRACT_VERSION = "2.0"` — для файла результата и всех конвертов, включая ack; MAJOR запроса — отдельная константа (`SUPPORTED_MAJOR = 1`). 359. **P5-14** (план 5): Golden v2 — `{schema_version, status, error, metrics, documents}` файла результата без `job_id`, `file_id`, `service`, `raw_s3_key` (`statements_version` меняется при каждой раскладке копии); успешный golden дополнительно проходит схему v2.0. Цена: golden не ловит регресс поля `service`. 360. **P5-15** (план 5): Перевод в контракт — `to_contract` / `warning_to_contract` пакета (план 1, P1-26): у предупреждений документа нет `locator` и пустых необязательных ключей (схема не допускает `null` у `column_index` и `field`), у нарушений §7 нет `column_key`, у предупреждений файла `locator` есть; шлюз только валидирует `dict` своими моделями. Цена: правка правил перевода — в `arm-statements`, а не здесь. 361. **P5-16** (план 5): Модули: `OcrNormalizer` остаётся в `src/normalizer/ocr/__init__.py` (меньше правок `app.py` и тестов), адаптер — `ocr/adapter.py`, обход набора — `ocr/answer_set.py`, ошибки json — `normalizer/files.py` (снимает цикл импортов `src.normalizer` ↔ `src.normalizer.ocr`); своего конвертера в контракт (`contract_map.py`) нет. 362. **P5-17** (план 5): Review Focus 3 утверждает `done` для обоих документов «БО 1 кв 2025» и «Бухгалтерская отчетность 1 кв.25» (proposal, Р-23). Падение при верном кандидате — находка для пакета, а не правка шлюза; ослабить утверждение может только контроллер рулингом. 363. **P5-18** (план 5): `metrics.rows` — число строк `lines` всех документов (как в v1.2), теперь включая строки без кода. 364. **P5-19** (план 5): `NullNormalizer` сохраняется для тестов воркера и выдаёт `JSON_FAILED` через `make_warning` пакета. 365. **P5-20** (план 5): Обход набора зовёт `normalize_candidate` на каждую таблицу, а не `normalize`: исключение одной таблицы — `TABLE_PARSE_ERROR` с `locator` таблицы, остальные таблицы разбираются (план 1: `normalize` исключения кандидата не перехватывает). `AnswerSetResult.documents` — документы пакета как есть (заголовок — в `header.form_name`). 366. **R-1** (план 5): Коммиты ветки несут трейлеры `Co-Authored-By: Claude Opus 5.5 ` (бриф владельца) и `Claude-Session: …` (план) — совпадает с обоими источниками. Цена ошибки: нет. 367. **R-2 (Р-36)** (план 5): Задача 4 копирует из `docs/contracts/examples` только существующие файлы верхнего уровня как есть, новых примеров контракта верхнего уровня не добавляет (решение владельца Р-36). Цена ошибки: нет. 368. **R-3** (план 5): Усечение до 300 символов (P5-9) применяется к тексту исключения, а не ко всей строке `Тип: текст`. Цена ошибки: сообщение чуть длиннее 300 символов при длинном имени типа. 369. **R-4 (T4)** (план 5): `NormalizeOutcome` вынесен в `src/normalizer/outcome.py` с реэкспортом из `src.normalizer` — иначе цикл импортов. Цена ошибки: лишний модуль; импорт из `src.normalizer` работает как раньше. 370. **R-5 (T4)** (план 5): `app.py` вызывает `OcrNormalizer()` (правила `default_rules()` внутри), а не `OcrNormalizer(default_rules())`: импорт `arm_statements` разрешён только в `src/normalizer/**` и `tests/**`. Цена ошибки: нет, эквивалентно. 371. **R-6 (T4)** (план 5): Сравнение с эталонами в `test_golden` временно стояло под `pytest.skip` до задачи 6 (эталоны fixtures были старого формата); в задаче 6 skip снят. Цена ошибки: окно без golden-проверки на ветке между задачами 4 и 6. 372. **R-7** (план 5): Набор «ошибочный json + успешный json без таблиц / без документов» даёт `RECOGNITION_FAILED` / «ни одной таблицы не разобрано», а код первого ошибочного — только если успешных json нет (чтение Р-32 + Р-33; исправление T3.I-1, commit 3b9e1f6). Цена ошибки: при другом чтении код ошибки конверта в смешанном случае будет другим; ядро на `error`-конверт `RECOGNIZED` не считает. #### План 6 (arm-web, change `statements-v2`) Источник: `openspec/changes/statements-v2/design.md` в репозитории `arm-web`, коммит `b4b8c04` (ветка исполнения; полный текст — там). Перенесено пунктов: 73. 373. **P6-1** (план 6): Подсказка кода — `StatementOut.edition_lines` (справочник строк редакции формы, у формы без даты — всех редакций; план 3 P3-22), затем коды строк `ok` файла, которых в справочнике нет (имя — `reference_name` ?? `line_name`); отдельного запроса нет. Обоснование: справочник приходит с ответом формы. Цена ошибки: нет. 374. **P6-2** (план 6): Якорь строки `unreadable` / `missing` — строка `ok` из источника (`row_index` не `null`) с наибольшим `row_index` меньше своего; якорь `detail` — строка `ok` из источника с `row_index = parent_row_index`; нет якоря (в том числе `detail` с `parent_row_index = null` или указывающим не на `ok`) — в начало первого раздела (после ведущих заголовков разделов; у ОФР — в начало таблицы). Вложение встаёт сразу после строки-якоря; несколько вложений одного якоря — по `row_index`. Обоснование: спека `arm-web` «Раскладка», S-62 («строка `detail` без родителя на экране — в начале раздела»), P3-17 (тот же порядок вложений в ответе ядра). Цена ошибки: нет. 375. **P6-3** (план 6): Скрытие (пустые подстроки, расшифровки) применяется **после** раскладки, фильтром. Обоснование: иначе скрытый якорь сдвинул бы вложения к соседу. Цена ошибки: нет. 376. **P6-4** (план 6): Справочное имя (`reference_name`) — основное; файловое (`line_name`, после срезания приклеенных заголовков разделов) показывается вторым серым текстом, только если отличается после нормализации: нижний регистр, `ё` → `е`, оставлены только буквы и цифры. Обоснование: переносы OCR («задолжен-ность») не должны давать ложного «отличается». Цена ошибки: лишнее или пропущенное второе имя — только показ. 377. **P6-5** (план 6): Маркеры «у места» строятся из **всех** записей `issues` (в том числе `relation` / `soft` — у них `line_ids` + `column_id`); список `IssuesPanel` показывает только `kind = warning`; нарушения §7 остаются списком в `ChecksPanel` (из `checks`, с акцептом) и получают ссылку «Показать» по `line_ids[0]`. Запись с `column_id` без `line_ids` (например, `COLUMN_WITHOUT_DATE`) показывается у заголовка колонки; поле шапки, которого нет в блоке шапки, и строки, которых нет в ответе, — у документа. Обоснование: одна запись не дублируется в двух списках; заголовок колонки — естественное место для предупреждения о дате колонки. Цена ошибки: нет. 378. **P6-6** (план 6): Подтверждение ячейки — в черновике; доступно, только если у строки есть `id` и у значения есть `id` (у достроенных ячеек — нет, ядро ответит `UNKNOWN_CELL`); сумма в черновике той же ячейки отменяет её подтверждение. Обоснование: `PATCH`: в элементе `values` ровно одно из `amount` / `confirmed`; правка суммы и так снимает `needs_review`. Цена ошибки: нет. 379. **P6-7** (план 6): Одно действие на строку в черновике (`code` / `detail` / `confirm`), новое заменяет прежнее; пустое поле кода и код, равный текущему, — не действие; у достроенных шаблонных строк (`id = null`) действий строки нет. Обоснование: 422 `INVALID_LINE_PATCH` при двух действиях; `lines[]` адресуется только `line_id`. Цена ошибки: нет. 380. **P6-8** (план 6): Сумма в шаблонную строку по `line_code` X и назначение кода X другой строке в одном сохранении — ошибка черновика «Код X назначается строке, а в шаблонную строку X введены суммы — сохраните по очереди», запрос не уходит. Обоснование: ядро ответит 422 `UNKNOWN_CELL` (P3-10), а сообщение ядра не говорит, что делать. Цена ошибки: нет. 381. **P6-9** (план 6): Ошибка элемента `lines[]` (409 `LINE_CODE_TAKEN`, 422 `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `LINE_CODE_REQUIRED`) показывается у поля кода строки `detail.line_id` (текст сервера в подсказке поля и `message.error`); строки с этим `id` на экране нет — текстом над таблицей. Обоснование: ядро отдаёт `detail.line_id` и `detail.line_code` (план 3 P3-24). Цена ошибки: нет. 382. **P6-10** (план 6): Строка «неразобранная» для кнопки «Подтвердить данные» — `line_status ∈ {unreadable, missing}` и хотя бы у одного значения `amount !== null` или `string_value !== null` (ровно условие ядра для 409 `LINES_UNRESOLVED`). Обоснование: кнопка и сервер считают одинаково. Цена ошибки: нет. 383. **P6-11** (план 6): Подтверждение даты отчёта — кнопка «Подтвердить дату» у поля из `needs_review_fields` (уходит `header.report_date` тем же значением); дата колонки — поле `ГГГГ-ММ-ДД` у заголовка колонки (как поле даты отчёта) с кнопкой «Подтвердить дату», если дата есть и у колонки `COLUMN_DATE_UNCERTAIN` / `COLUMN_WITHOUT_DATE`. Обоснование: перепечатывать ту же дату ради подтверждения неочевидно; формат ввода — как у существующего поля даты. Цена ошибки: нет. 384. **P6-12** (план 6): Панель файлов: у строки документа — значок с числом «предупреждения файла (`job.warnings`) + `issue_counts.error` + `issue_counts.info`» документа, красный при хотя бы одном `error` (в файле или `issue_counts.error > 0`); у строки файла без документов — только `job.warnings`; нуль — значка нет. Клик открывает список: предупреждения файла (`message` и `locator`), `job.mapping_warnings` (как `info`) и `DocumentOut.issues` документа, загружаемые при открытии (`GET /documents/{id}`). Обоснование: `DocumentBrief.issue_counts` (план 3 P3-23) помечает документ без запроса на каждый; подробности — по клику. Цена ошибки: акцептованное нарушение тоже даёт значок — подробности в списке. 385. **P6-13** (план 6): `job.warnings` на клиенте нормализуется так же, как ядро: строка → `{code: "LEGACY_WARNING", severity: "info", message, locator: null}`, объект с `code`, `severity ∈ {error, info}`, `message` — как есть, прочее пропускается, не список → `[]`. Обоснование: экран не падает при ошибке порядка выкладки. Цена ошибки: нет. 386. **P6-14** (план 6): Сценарий T11 — на локальном стенде; строки `unreadable` / `missing` / `detail` и записи `issues` всех видов подставляются перехватом `GET` и `PATCH …/statement` (`page.route`) из фикстуры плана, пока сервисы шлют 1.x; на реальных формах 1.x — регрессия. Обоснование: до шага 5 выкладки 2.0-данных на стенде нет. Цена ошибки: повторный прогон на `aep_3` после шага 5 — follow-up. 387. **P6-15** (план 6): Цвета: ячейка `needs_review` — `#fff7e6`, исправлено — `#e6f4ff`, строка `unreadable` / `missing` целиком — `#fff1f0`, строка `ok` с `needs_review` — фон ячеек наименования и кода `#fff7e6`; подтверждённая ячейка — маленькая серая галочка перед суммой; уровень `error` — красный значок `ExclamationCircleOutlined` `#cf1322`, `info` — синий `InfoCircleOutlined` `#1677ff`. Обоснование: палитра уже используется на экране и в панели файлов. Цена ошибки: только вид. 388. **P6-16** (план 6): Ячейка кода: `ok` — код; `unreadable` — серым `line_code_text` (пусто — «—»), подсказка «Код не распознан: «<текст>»»; `missing` — серым «—», подсказка «Кода в строке нет»; `detail` — пусто, подсказка «Расшифровка — в суммы формы не входит»; у `ok` в подсказке «Уверенность кода: N%» (если меньше 100 и код не правлен), «Код подтверждён», «Код назначен аналитиком». Обоснование: спека: «в ячейке кода серым — сырой текст с подсказкой». Цена ошибки: только вид. 389. **P6-17** (план 6): `header.source.<поле>.confidence` — `string | null` (строка Decimal, как прочие уверенности API, план 3 P3-20), в подсказке — `Number()` и округление до целого. Обоснование: одно правило для всех уверенностей. Цена ошибки: нет. 390. **P6-18** (план 6): `node_modules` worktree — симлинк на установленный каталог с eslint, prettier и vitest 3.1.4; поэтому вместо `make check` (он начинается с `npm install` и поставил бы пакеты в чужой каталог) гоняется `make lint typecheck test build` — те же проверки без установки. Обоснование: память владельца: симлинк, как в прежних срезах. Цена ошибки: нет; в описании PR сказано, что install пропущен. 391. **P6-19** (план 6): Предупреждения ОСВ-документа (`DocumentOut.issues`, `OSV_*`) — блок `IssuesPanel` над таблицей экрана ОСВ, все записи — у документа (строки ОСВ не адресуются `line_ids`). Обоснование: спека: «предупреждения листа ОСВ … из `DocumentOut.issues[]`»; экран документа уже загружает `DocumentOut`. Цена ошибки: нет. 392. **R-1** (план 6): Шаги «сверка с макетом / прогон в браузере» (T3 s5, T4 s4, T7–T10) перенесены в T11 (дата 2026-10-18). Обоснование: нет стенда ядра с v5.19, общий Playwright-браузер делят параллельные дорожки. Цена ошибки: визуальные дефекты найдутся позже, в T11. 393. **R-2** (план 6): Ссылка «Показать» также у проверок со статусом `soft_failed` (дата 2026-10-18). Обоснование: согласованное расширение: нарушение так же адресуется строкой. Цена ошибки: только показ. 394. **R-3** (план 6): Компонент меню строки назван `LineMenuButton.tsx`, а не `LineMenu.tsx` (дата 2026-10-18). Обоснование: на регистронезависимой ФС (macOS) `LineMenu.tsx` и `lineMenu.ts` дают TS1149. Цена ошибки: нет. 395. **O-1** (план 6): Два MODIFIED-требования («Форма ББ/ОФР — просмотр и правка», «Подтверждение документа») составлены исполнителем из текущей спеки: в docs-спеке `arm-web` все требования ADDED. «Предупреждения ОСВ-документа» (P6-19) в docs-спеке нет; написано по плану. Сверить в arm-ka-docs#116 (дата 2026-10-18). Обоснование: иначе change нельзя было бы описать дельтой. Цена ошибки: расхождение формулировок со спекой docs при архивации. 396. **T1.D-1** (план 6): `cellFlags` возвращает ещё и `confirmed`; двум старым ожиданиям добавлен `confirmed: false`; тест «внимание по уверенности» заменён на «внимание только по needs_review» (дата 2026-10-18). Обоснование: `toEqual` падает на новом ключе; так велит бриф. Цена ошибки: только тесты. 397. **T1.D-2** (план 6): В тестовый хелпер `doc()` добавлен `issue_counts: {error: 0, info: 0}` (дата 2026-10-18). Обоснование: поле обязательно в `DocumentBrief`, иначе падает tsc. Цена ошибки: нет. 398. **T1.D-3** (план 6): Значения фикстуры `statement-v2.json`, которых нет в брифе, придуманы исполнителем (форматы сумм, уверенности, `text`, UUID, `codes`, сообщения, `is_current`, пустая колонка c2, порядок `edition_lines`) (дата 2026-10-18). Обоснование: фикстура должна быть полной. Цена ошибки: тесты завязаны на выдуманные значения; реальные данные — в T11. 399. **T1.D-4** (план 6): Фикстура приведена к `StatementOut` через `as` в тесте (дата 2026-10-18). Обоснование: импорт JSON расширяет литералы до `string`; полноту проверяет рантайм-тест пар «строка × колонка». Цена ошибки: слабая проверка типов фикстуры. 400. **T1.D-5** (план 6): Тест «переносит поля v2» проверяет ключи по `l-u7`, `code-1250`, `l-1230`, `l-d2` (дата 2026-10-18). Обоснование: ровно как в брифе. Цена ошибки: нет. 401. **T3.D-1** (план 6): Несколько строк подсказки кода склеиваются переводом строки; у `codeModified` уверенность не показывается (дата 2026-10-18). Обоснование: читаемость подсказки. Цена ошибки: только вид. 402. **T3.D-2** (план 6): Подсказка сумм принимает необязательный `issues` (добавляет message) (дата 2026-10-18). Обоснование: задел для T4. Цена ошибки: нет. 403. **T3.D-3** (план 6): Уверенность округляется `Math.round(Number())` (P6-17) (дата 2026-10-18). Обоснование: одно правило для всех уверенностей. Цена ошибки: нет. 404. **T3.D-4** (план 6): В режиме правки у подтверждённой ячейки галочка — `prefix` поля ввода (дата 2026-10-18). Обоснование: ячейка в режиме правки — Input. Цена ошибки: наложение значков, см. T8.D-3. 405. **T3.D-5** (план 6): `id` строки `row-` выставляется только на строках-линиях, не на заголовках разделов (дата 2026-10-18). Обоснование: якорь прокрутки нужен только строкам. Цена ошибки: нет. 406. **T4.D-1** (план 6): Подписи мест полей шапки: organization_name — «Организация», inn — «ИНН», report_date — «Дата отчёта», units — «Единица измерения»; неизвестное поле — своим кодом, запись уходит в `document` (дата 2026-10-18). Обоснование: P6-5. Цена ошибки: только вид. 407. **T4.D-2** (план 6): Запись с `field` из блока шапки всегда в `byField`, независимо от `line_ids` (дата 2026-10-18). Обоснование: поле шапки — самостоятельное место. Цена ошибки: нет. 408. **T4.D-3** (план 6): `warning` с `line_ids` без `column_id` — место «строка …», с `column_id` — «<строка>, колонка «…»»; `relation` / `soft` в список не входят (дата 2026-10-18). Обоснование: P6-5. Цена ошибки: только вид. 409. **T4.D-5** (план 6): `onShow` панелей передаёт ключ строки; для достроенных строк (`id = null`) перехода нет — `line_ids` их не содержат (дата 2026-10-18). Обоснование: нет id — нет адреса. Цена ошибки: нет ссылки «Показать» у достроенной строки. 410. **T4.D-6** (план 6): Значок у наименования строки — только по `byRow`; записи `byCell` дают значок только в ячейке (дата 2026-10-18). Обоснование: не дублировать. Цена ошибки: только вид. 411. **T5.D-1** (план 6): Кнопка «Подтвердить дату» рисуется только при переданном `onConfirmDate` (дата 2026-10-18). Обоснование: иначе мёртвая кнопка. Цена ошибки: нет. 412. **T5.D-2** (план 6): У организации в подсказке при modified — «Распознано: «…»»; нет `source` — подсказки нет (дата 2026-10-18). Обоснование: нечего показывать. Цена ошибки: нет. 413. **T5.D-3** (план 6): Уверенность в подсказке шапки через `Number()` ('67.00' → 67%) (дата 2026-10-18). Обоснование: P6-17. Цена ошибки: нет. 414. **T5.D-4** (план 6): Placeholder организации — «Наименование организации» (текст не из плана) (дата 2026-10-18). Обоснование: поле в режиме правки должно быть подписано. Цена ошибки: только вид. 415. **T6.D-1** (план 6): `buildStatementPatch` для суммы в неизвестную строку возвращает ошибку `amount` (раньше бросалось исключение) (дата 2026-10-18). Обоснование: экран не должен падать. Цена ошибки: нет. 416. **T6.D-2** (план 6): Подтверждение ячейки и действие строки у строк без `id` молча пропускаются (дата 2026-10-18). Обоснование: UI их не создаёт (P6-6, P6-7). Цена ошибки: молчаливая потеря действия при ошибке UI. 417. **T6.D-3** (план 6): Код сравнивается после `trim`; пустой или равный текущему — не действие и не ошибка, проверка до формата (дата 2026-10-18). Обоснование: P6-7. Цена ошибки: нет. 418. **T6.D-4** (план 6): Конфликт P6-8 ищется по сумме в шаблонную строку (`id = null`) с тем же кодом; `key` ошибки — `lineId` строки с действием (дата 2026-10-18). Обоснование: P6-8. Цена ошибки: нет. 419. **T6.D-5** (план 6): Тексты ошибок: «Наименование организации — не длиннее 1000 символов» (длина организации); «Исправьте суммы с неверным форматом» (суммы, как раньше) (дата 2026-10-18). Обоснование: тексты не из плана. Цена ошибки: только вид. 420. **T6.D-6** (план 6): `confirm_report_date` при отсутствии текущей даты ничего не отправляет (дата 2026-10-18). Обоснование: нечего подтверждать. Цена ошибки: пустое тело PATCH, «Сохранить» молчит. 421. **T6.D-7** (план 6): Ошибка `amount` подсвечивает ячейку красной рамкой и сама по себе (дата 2026-10-18). Обоснование: раньше подсветка шла только по разбору формата. Цена ошибки: только вид. 422. **T7.D-2** (план 6): Подпись пункта меню: «Это расшифровка» для `unreadable` / `missing`, «Сделать расшифровкой» для `ok` (дата 2026-10-18). Обоснование: разные смыслы действия. Цена ошибки: только вид. 423. **T7.D-3** (план 6): Меню строки у `ok` — только при `origin = 'source'`; шаблонные строки с id меню не получают (дата 2026-10-18). Обоснование: бриф. Цена ошибки: нет. 424. **T7.D-4** (план 6): `lineErrorTarget` читает `ApiError.body.detail` (отдельного поля `detail` у `ApiError` нет) (дата 2026-10-18). Обоснование: иначе деталей ошибки не достать. Цена ошибки: нет. 425. **T7.D-5** (план 6): Ошибка сервера сбрасывается любой правкой черновика (в `setDraft`) (дата 2026-10-18). Обоснование: иначе устаревшая ошибка висит после правки. Цена ошибки: нет. 426. **T7.D-6** (план 6): У `unreadable` / `missing` с действием detail / confirm поле кода остаётся, рядом тег (дата 2026-10-18). Обоснование: бриф: поле всегда. Цена ошибки: см. T7.2: очистка поля теряет действие. 427. **T8.D-1** (план 6): Подтверждение ячейки без `id` значения молча пропускается в `buildStatementPatch`; кнопка не рисуется (`cellConfirmState → null`) (дата 2026-10-18). Обоснование: P6-6. Цена ошибки: нет. 428. **T8.D-2** (план 6): Ожидающая кнопка даты — disabled с текстом «Дата подтверждена (до сохранения)»; без ожидания «Подтвердить дату»; отменить подтверждение даты можно только отменой всей правки. Для колонки ожидание = черновая дата равна `period_date`. Затем текст подсказки при нажатой кнопке подтверждения ячейки: «Подтверждение будет сохранено — нажмите, чтобы убрать» (дата 2026-10-18). Обоснование: пользователь должен видеть, что подтверждение в черновике. Цена ошибки: текст не из плана; ширина колонки (T8.2) проверяется в T11. 429. **T8.D-3** (план 6): При значке записи у Input `paddingLeft: 14`, чтобы значок не перекрывал галочку (дата 2026-10-18). Обоснование: без браузера не проверено. Цена ошибки: визуальное наложение; проверка в T11. 430. **T8.D-4** (план 6): `cellConfirmState` возвращает `pending-unconfirm` при `confirms[key] === false` (дата 2026-10-18). Обоснование: нужно третье состояние. Цена ошибки: нет. 431. **T8.D-5** (план 6): Подтверждение ячейки только строкам с `lineId` (дата 2026-10-18). Обоснование: P6-6, P6-7. Цена ошибки: нет. 432. **T9.D-1** (план 6): «Значения есть» = `amount !== null || string_value !== null` по любой ячейке; `detail` не блокирует независимо от значений (дата 2026-10-18). Обоснование: P6-10. Цена ошибки: расхождение с условием ядра. 433. **T9.D-2** (план 6): Ключ ссылки в 409 `LINES_UNRESOLVED` — `line_id` (дата 2026-10-18). Обоснование: ключ GridRow для строк из БД. Цена ошибки: ссылка молчит, если строки нет в таблице (T9.6). 434. **T9.D-3** (план 6): Описание Alert подтверждения собирается императивно (`confirmDescription`); приоритет: failures > lines > duplicate (дата 2026-10-18). Обоснование: линтер запрещает вложенные тернарники. Цена ошибки: только вид. 435. **T10.D-1** (план 6): `toFileWarnings` отбрасывает объекты без `code` / `message` или с `severity` не `error` / `info` (дата 2026-10-18). Обоснование: P6-13. Цена ошибки: нет. 436. **T10.D-2** (план 6): В режиме `document` у `IssuesPanel` колонка «место» не показывается; `OsvDocument` кладёт place '' / targetKey null (дата 2026-10-18). Обоснование: у документа нет места. Цена ошибки: нет. 437. **T10.D-3** (план 6): Подробности документа в поповере грузятся при открытии (`trigger=click`), состояние сбрасывается при закрытии (дата 2026-10-18). Обоснование: P6-12. Цена ошибки: лишний запрос при повторном открытии. 438. **T10.D-4** (план 6): Значок — `