From 10317ae4d36b9bdd4f01efb3a0f79adfcbbf8dbb Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 18 Oct 2026 22:57:03 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B8=D1=82=D0=BE=D0=B3=D0=B8=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B5?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D0=BE=D0=B9=20=D0=BD=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D0=91=D0=91/?= =?UTF-8?q?=D0=9E=D0=A4=D0=A0=20(#124,=20#116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Решения дорожек (планы 3А, 3Б, 4, 5, 6) перенесены в design.md change'а, добавлен раздел «Открытые вопросы исполнения»; гайд фронта по API приведён к arm-core main 35a24e7a; уточнена строка про зависимости в dev/checks.md. Co-Authored-By: Claude Opus 5.5 --- dev/checks.md | 2 +- dev/frontend-api-guide.md | 43 +-- .../unified-statement-normalization/design.md | 329 ++++++++++++++++++ 3 files changed, 353 insertions(+), 21 deletions(-) diff --git a/dev/checks.md b/dev/checks.md index e053180..3fc41dd 100644 --- a/dev/checks.md +++ b/dev/checks.md @@ -185,7 +185,7 @@ PR убедитесь, что `docs` и `fixtures` доступны (пропу | `make sync-statements` | копия пакета `arm_statements` в хост одной командой (worktree, issue, PR); `SYNC_ARGS="--host arm-core [--check\|--dry-run]"` | | `make statements-check` | обзор свежести копий по трём хостам рядом (`arm-core`, `tabular-parser`, `scan-gateway`): строка итога на хост; в `check` не входит | -Без `make`: `python scripts/validate_contracts.py` (нужен `jsonschema`) и `openspec validate --strict`. +Без `make`: `python scripts/validate_contracts.py` (нужны `jsonschema` и `PyYAML`) и `openspec validate --strict`. Менять `contracts/**` нужно осознанно: байтовые копии схем лежат в `api/contracts/` каждого сервиса, после правки их нужно пересинхронизировать (`cp`), иначе тест идентичности в сервисах упадёт. Правило теста идентичности (у ядра и сервисов одно, верно до, во время и после окна перехода между MAJOR — сейчас 1 → 2): схема копии с `$id` `…/v` diff --git a/dev/frontend-api-guide.md b/dev/frontend-api-guide.md index b4aa97d..78bc106 100644 --- a/dev/frontend-api-guide.md +++ b/dev/frontend-api-guide.md @@ -3,6 +3,7 @@ Состояние — `arm-core` на `main` от 08.10.2026 (коммит `d8bf5b2`: после PR #46 «API до сводной ОСВ», #51 «Основной РФ объектом», #52 «КПП ЮЛ», #53 «Схема БД настройкой», #59 «Алиас приоритетнее имени» и #60 «Предупреждение автозамены»). Схема сверена с `GET /openapi.json` (62 операции на 49 путях), коды ошибок — с `api/src/services/*` и `arm-core/README.md` «Ошибки API». +**Раздел 5.6.1 (формы ББ/ОФР после единой нормализации #116) приведён к фактическому API `arm-core` `main` `35a24e7a`** (часть А — PR #129, часть Б — PR #132; схемы сверены с pydantic-моделями `api/src/schemas/{statements,files,issues}.py`, ошибки — с README ядра «Ошибки API»). Это **точка входа**: здесь всё, что нужно, чтобы писать клиент, — подключение, общие правила, каждый маршрут, ошибки, сценарии экранов, известные ограничения и то, что изменится в ближайших PR. Подробные контракты по областям остаются источником деталей @@ -29,6 +30,7 @@ | Когда, PR | Что изменилось в API | Что сделать в клиенте | |---|---|---| +| 18.10, arm-core #129 и #132 (arm-ka-docs#116, DBML v5.19, контракт распознавания 2.0) | **Единая нормализация ББ/ОФР** (подробно — §5.6.1). `GET/PATCH …/statement`: все строки источника со статусами (`line_status`) и повторами кода, `needs_review`/`is_confirmed`, `issues[]`, `edition_lines[]`, `header.source`/`needs_review_fields`, `columns[].period_kind`; `PATCH` — части `lines`, `columns`, `confirmed` в `values`, `header.organization_name`; подтверждение — новые 409 `LINES_UNRESOLVED`, `DUPLICATE_FORM`; новые 409/422 правки: `LINE_CODE_TAKEN`, `LINE_CODE_REQUIRED`, `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `UNKNOWN_COLUMN`, `DUPLICATE_COLUMN`. **Несовместимо:** `FileOut.job.warnings` — список объектов `{code, severity, message, locator}`, а не строк; `FileOut.documents[].issue_counts` и `DocumentOut.issues[]`; `StatementLineOut.line_code` — `null` у строк не `ok`; `FailedCheckOut.expected`/`actual` — могут быть `null`; `crm_id` ГСК/ГСЗ сохраняется (#112) | обновить типы `JobBrief.warnings`, `LineOut`, `FailedCheckOut` (в `types/api.ts`) вместе с выкладкой ядра; не держать словарь «код → строка», подсветку строить по `needs_review`; остальное — §5.6.1 | | 16.10, arm-core #118 (AURORA-1527) | `GET/PATCH …/statement`, `POST …/statement/checks/accept`: у каждой строки `lines[]` новое поле `parent_code` — у пятизначной подстроки (11101…) код её четырёхзначного родителя, иначе `null`; вычисляется ядром по правилу проверки «сумма подстрок = родитель», в БД не хранится | выводить подстроки под родителем по `parent_code`, не вычисляя родителя на клиенте; пустые подстроки (нет значения ни в одной колонке) можно сворачивать | | 14.10, arm-core #111 | **`crm_id` у ГСК/ГСЗ — принимается, но не сохраняется.** `POST …/company-groups/{id}/related-groups` и `PATCH /directories/related-groups/{id}` принимают необязательный `crm_id` (раньше — 422 `extra_forbidden`); значение отбрасывается — колонки в модели пока нет (arm-core #112). В `RelatedGroupOut`/`RelatedGroupDetail` появилось поле `crm_id`, сейчас всегда `null` | формы [51-03]/[51-04]/[77-02]: поле «ID ГСК/ГСЗ в CRM» можно отправлять; после сохранения не ждать его в ответе — показывать то, что пришло (`null`), пока не закрыта #112 | | 13.10, arm-core #110 (#109) | **Членства ГСК/ГСЗ в списке ЮЛ.** `memberships[] {related_group_id, code, kind, name, valid_from}` перенесено из `LegalEntityDetail` в `LegalEntityOut`: приходит в `GET /directories/legal-entities`, в ответах `move`/`withdraw` (после них — `[]`) и, как раньше, в карточке. Только текущие членства; нет — `[]`. Серверного фильтра по ГСК/ГСЗ нет | карточка ГК [36] и модалка [42]: колонка «ГСК»/«ГСЗ» и фильтр по ним — из `memberships` строк списка, без `related-groups/{id}` и без карточки каждого ЮЛ | @@ -411,14 +413,15 @@ UI прячет элементы по `GET /me → permissions` (объедин - `cross_form` зависит от второго документа ЮЛ за тот же период: появилась ОФР отдельным файлом — перечитайте ББ. - Документ формы появляется после того, как файл стал `PROCESSED`; до этого `…/statement` — 404. -### 5.6.1 Формы ББ/ОФР после единой нормализации — **после реализации** +### 5.6.1 Формы ББ/ОФР после единой нормализации (arm-core `main` `35a24e7a`) -> **Пока не работает на `main`.** Раздел описывает API после change `unified-statement-normalization` -> (RSHB/arm-ka-docs#116; контракт распознавания v2.0, DBML v5.19; дочерние PR arm-core и arm-web ещё не созданы). -> Правила — `context/05-domain-rules.md` §7, §9.3, §9.5 пп. 2, 4, 9, §11; экран — спека `arm-web` того же change. -> В `…/statement` поля добавляются, старые не удаляются: клиент, написанный по §5.6, продолжит читать ответ. -> **Исключение — карточка файла:** `FileOut.job.warnings` меняет тип со строк на объекты (ниже, «Карточка файла»), это -> несовместимо — экран файлов обновляется в той же выкладке, что и ядро. После слияния PR раздел переедет в §5.6 и §1. +> Работает на `main` ядра: change `unified-statement-normalization` (RSHB/arm-ka-docs#116; контракт распознавания v2.0, +> DBML v5.19), части А (arm-core #129) и Б (#132). Встроенный фронт `arm-core/web` получает новые типы через `sync-web` +> после плана 6 (arm-web). Правила — `context/05-domain-rules.md` §7, §9.3, §9.5 пп. 2, 4, 9, §11; экран — спека `arm-web` того же change. +> В `…/statement` поля добавлены, старые не удалены: клиент, написанный по §5.6, продолжит читать ответ. +> **Исключение — карточка файла:** `FileOut.job.warnings` сменил тип со строк на объекты (ниже, «Карточка файла»), это +> несовместимо — экран файлов обновляется в той же выкладке, что и ядро. Раздел 5.6 ниже описывает базовые маршруты; где он +> расходится с этим разделом, прав этот раздел. **`GET /documents/{id}/statement` — новые поля `StatementOut`:** @@ -433,7 +436,7 @@ UI прячет элементы по `GET /me → permissions` (объедин `relative:prev_period` | `none`; `columns[].label_confidence`. Колонка с `period_kind = none` и `period_date = null` — нужна дата от аналитика (ввод даты у колонки). - `lines[]` — **все строки источника**, а не только строки с кодом: `row_index` (порядок в файле; `null` — и только - тогда — у строк, созданных правкой, и у достроенных шаблонных; `origin = template` ⇔ `row_index = null`), `source_ref`, `line_status` (`ok` | `unreadable` | `missing` | `detail`), `line_code` (`null` у + тогда — у строк, созданных правкой, и у достроенных шаблонных; `origin = template` ⇔ `row_index = null`), `source_ref`, `line_status` (`ok` | `unreadable` | `missing` | `detail`), `line_code` (`string | null`: `null` у статусов кроме `ok`), `line_code_text` (сырой текст ячейки кода — показывать серым у `unreadable` / `missing`), `line_code_confidence` (строка Decimal или `null`; подтверждение кода её не меняет), `line_name` (наименование **из файла**), `reference_name` (справочное имя по коду или `null` — его показывать основным, @@ -451,7 +454,7 @@ UI прячет элементы по `GET /me → permissions` (объедин `column_id` — столбец, которого нет среди колонок формы (пропущенная колонка «Изменение, %»): показывать у документа. Нарушения §7 приходят и здесь (`kind = relation` / `soft`, `code = RELATION_FAILED` / `RELATION_SOFT`) — у любой формы, в том числе загруженной до перехода. -- `checks.failed[]` / `soft_failed[]` — у нарушения ещё `codes[]` и `line_ids[]` (все строки кодов соотношения). +- `checks.failed[]` / `soft_failed[]` — у нарушения ещё `codes[]` и `line_ids[]` (все строки кодов соотношения). **`expected` и `actual` — строки-суммы или `null`**: `expected = null`, если у соотношения пустая цель при непустых членах (форма потеряла итоговую строку; такое нарушение блокирует подтверждение, пока не исправлено или не акцептовано). `column_index` — число; **`-1`** у межформенной проверки `cross_form` и у нарушения без колонки (P3-60). В `detail.failures[]` отказа 409 `CHECKS_NOT_PASSED` у записи без колонки `column_index` — **`null`**, не `-1`; обрабатывайте оба значения. - `edition_lines[] {code, name}` — справочник строк формы для подсказки кода при назначении: коды редакции формы по одному (имя — первое из словаря), у формы без даты (редакции нет) — коды всех редакций по одному. - Порядок `lines` — по возрастанию кода, как сейчас (одинаковые коды — по `row_index`); `unreadable` / `missing` — после @@ -464,14 +467,14 @@ UI прячет элементы по `GET /me → permissions` (объедин | Часть | Тело | Эффект | Ошибки | |---|---|---|---| | подтвердить ячейку / снять подтверждение | `values: [{line_id, column_id, confirmed: true \| false}]` | `is_confirmed`, `needs_review = false` (при `false` — пересчитывается), сумма не меняется, акцепт не сбрасывается | 422 `UNKNOWN_CELL` | -| дата колонки | `columns: [{column_id, period_date}]` (дата обязательна) | дата колонки, `is_current`, период документа, `period_kind = absolute` (правка даты отчёта её больше не пересчитывает); снимает `COLUMN_WITHOUT_DATE` / `COLUMN_DATE_UNCERTAIN`; та же дата у колонки с таким предупреждением — подтверждение (акцепт не сбрасывает) | 422 `UNKNOWN_COLUMN`; `period_date: null` — 422 массивом | +| дата колонки | `columns: [{column_id, period_date}]` (дата обязательна) | дата колонки, `is_current`, период документа, `period_kind = absolute` (правка даты отчёта её больше не пересчитывает); снимает `COLUMN_WITHOUT_DATE` / `COLUMN_DATE_UNCERTAIN`; та же дата у колонки с таким предупреждением — подтверждение (акцепт не сбрасывает) | 422 `UNKNOWN_COLUMN`; колонка дважды в `columns[]` — 422 `DUPLICATE_COLUMN`; `period_date: null` — 422 массивом | | назначить / сменить код | `lines: [{line_id, line_code}]` | `unreadable` / `missing` / `detail` / `ok` → `ok` с этим кодом, `line_code_confidence = 100` | 422 `INVALID_LINE_CODE` (не 4–5 цифр), 409 `LINE_CODE_TAKEN` (код занят строкой, созданной правкой, с суммами — сначала очистить её) | | отметить расшифровкой | `lines: [{line_id, line_status: "detail"}]` | строка выходит из сумм и не блокирует подтверждение; обратно — только назначением кода | 422 `LINE_CODE_REQUIRED` (`line_status: "ok"` — в `ok` переводит только `line_code`) | | подтвердить код / снять подтверждение | `lines: [{line_id, confirmed: true \| false}]` | `is_confirmed`, `needs_review` строки = `false` (при `false` — пересчитывается); `line_code_confidence` и `line_code_modified` не меняются — в подсказке остаётся исходная уверенность | — | -| наименование организации | `header: {organization_name}` (непустая строка) | `organization_name_modified`, `counterparty_check`; снимает `NO_ORGANIZATION_NO_INN` | 422 массивом | +| наименование организации | `header: {organization_name}` (непустая строка после обрезки пробелов, до 1000 символов) | `organization_name_modified`, `counterparty_check`; снимает `NO_ORGANIZATION_NO_INN`; равное текущему — без эффекта | 422 массивом; **`organization_name: null` — 422** (как `inn: null`; «не менять» — отсутствие поля) | | подтвердить дату отчёта | `header: {report_date}` тем же значением, если `report_date` в `needs_review_fields` | поле уходит из `needs_review_fields`, снимается `REPORT_DATE_UNCERTAIN` | — | -Общие ошибки `lines`: 422 `UNKNOWN_LINE` (строка не этого документа), 422 `INVALID_LINE_PATCH` (в элементе нет действия или +Части применяются в порядке `lines` → `values` → `columns` → `header`; ошибка адресации или 409 откатывает весь запрос (ничего не применено). Общие ошибки `lines`: 422 `UNKNOWN_LINE` (строка не этого документа), 422 `INVALID_LINE_PATCH` (в элементе нет действия или их несколько). У ошибок элемента `lines[]` (`LINE_CODE_TAKEN`, `LINE_CODE_REQUIRED`, `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`) в `detail` есть `line_id` и `line_code` первого неверного элемента — показывать ошибку у поля этой строки. Правка суммы по `line_code` (достроенная строка) для кода, у которого уже есть строка `ok`, — 422 @@ -483,7 +486,7 @@ UI прячет элементы по `GET /me → permissions` (объедин `DOCUMENT_NOT_CONFIRMABLE` → `PERIOD_REQUIRED` → **`LINES_UNRESOLVED`** (`detail.lines[] {line_id, row_index, line_code_text}` — строки `unreadable` / `missing`, у которых в ячейках есть сумма или нераспознанный текст; кнопку «Подтвердить данные» отключать заранее с подсказкой «Разберите строки без кода: N») → **`DUPLICATE_FORM`** -(`detail.document_id` — в этом файле уже подтверждена форма того же типа, года и квартала) → `CHECKS_NOT_PASSED`. +(`detail.document_id` — в этом файле уже подтверждена форма того же типа, года и квартала) → `CHECKS_NOT_PASSED` (`detail.failures[] {column_index | null, relation, expected | null, actual | null}`). **Статус документа.** Форма из OCR может стать `RECOGNIZED` (и сразу автоподтвердиться), если сервис всё подтвердил проверками: сомнительные ячейки расшифровок (`detail`) статус не держат, поэтому чистые сканы 1С (например, ББ и ОФР за @@ -658,7 +661,7 @@ async function download(path: string, fallbackName: string): Promise { - Склеенные квартальные строки свода не содержат `null` (пустое = `"0.00"`); строки нарастающего документа — как в файле, могут быть `null`. Показывайте `null` как пусто, а не «0,00». -- `FailedCheckOut.expected`/`actual` — строки-суммы; `relation` — готовая читаемая запись соотношения («1600 = 1100 + 1200»). +- `FailedCheckOut.expected`/`actual` — строки-суммы или `null` (пустая цель при непустых членах); `relation` — готовая читаемая запись соотношения («1600 = 1100 + 1200»). - `edition` формы (`"66n"`, `"fsbu4"`, `null`) — только подсказка для шаблона строк, на проверки не влияет. - Строки формы без кода (расшифровочные «в том числе») в ответ не попадают. - ОСВ со скользящим периодом: `period_year` есть, `period_quarter` и `is_cumulative` — `null`; документ можно подтвердить, но в @@ -697,11 +700,11 @@ async function download(path: string, fallbackName: string): Promise { | `LAST_ADMIN` | 409 | администрирование | — | «Нельзя оставить систему без администратора» | | `ALREADY_CONFIRMED`, `FILE_SUPERSEDED`, `DOCUMENT_NOT_CONFIRMABLE` | 409 | подтверждение, правка формы | — | перечитать документ | | `PERIOD_REQUIRED` | 409 | подтверждение формы | — | «Укажите дату отчёта» → режим правки шапки | -| `CHECKS_NOT_PASSED` | 409 | подтверждение формы | `failures[] {column_index, relation, expected, actual}` | показать нарушения, предложить акцепт | -| `LINES_UNRESOLVED` | 409 | подтверждение формы (после #116, §5.6.1) | `lines[] {line_id, row_index, line_code_text}` | перейти к первой строке без кода, предложить назначить код или «Это расшифровка» | -| `DUPLICATE_FORM` | 409 | подтверждение формы (после #116) | `document_id` | ссылка на уже подтверждённую форму того же типа и периода в файле | -| `LINE_CODE_TAKEN` | 409 | правка формы, назначение кода (после #116) | — | ошибка поля кода: код занят строкой, созданной правкой, с суммами | -| `INVALID_LINE_CODE`, `LINE_CODE_REQUIRED`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `UNKNOWN_COLUMN` | 422 | правка формы `lines` / `columns` (после #116) | — | ошибка поля / ошибка клиента (перечитать форму) | +| `CHECKS_NOT_PASSED` | 409 | подтверждение формы | `failures[] {column_index (число или null), relation, expected (или null), actual (или null)}` | показать нарушения, предложить акцепт | +| `LINES_UNRESOLVED` | 409 | подтверждение формы | `lines[] {line_id, row_index, line_code_text}` | перейти к первой строке без кода, предложить назначить код или «Это расшифровка» | +| `DUPLICATE_FORM` | 409 | подтверждение формы | `document_id` | ссылка на уже подтверждённую форму того же типа и периода в файле | +| `LINE_CODE_TAKEN` | 409 | правка формы, назначение кода | — | ошибка поля кода: код занят строкой, созданной правкой, с суммами | +| `INVALID_LINE_CODE`, `LINE_CODE_REQUIRED`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `UNKNOWN_COLUMN`, `DUPLICATE_COLUMN` | 422 | правка формы `lines` / `columns` | — | ошибка поля / ошибка клиента (перечитать форму) | | `DOCUMENT_NOT_STATEMENT` | 409 | `…/statement` не по форме ББ/ОФР | — | не открывать экран [41] для такого документа | | `DOCUMENT_READONLY` | 409 | правка/акцепт подтверждённой формы | — | перевести экран в просмотр | | `UNKNOWN_CELL`, `DUPLICATE_CELL` | 422 | правка формы | — | ошибка клиента (перечитать форму); `UNKNOWN_CELL` также: `line_code` уже есть в форме (слать `line_id`), код вне справочника редакции, нет редакции | @@ -753,7 +756,7 @@ async function download(path: string, fallbackName: string): Promise { | Задача | Что поменяется для фронта | Как подготовиться | |---|---|---| | OQ-54 — SSO РАИСА | Меняется только получение токена (вместо dev-формы) | держать слой получения токена изолированным (`frontend-auth-guide.md` §9) | -| arm-ka-docs#116 — единая нормализация ББ/ОФР (контракт v2.0, DBML v5.19) | `GET/PATCH …/statement`: строки со статусами и повторами кода, `needs_review`, `issues[]`, `lines`/`columns`/`confirmed` в PATCH; подтверждение — 409 `LINES_UNRESOLVED`, `DUPLICATE_FORM`; `crm_id` ГСК/ГСЗ сохраняется; **`FileOut.job.warnings` — объекты вместо строк**, `DocumentOut.issues[]` | §5.6.1; не держать словарь «код → строка», подсветку строить по `needs_review`; тип `JobBrief.warnings` в `types/api.ts` поменять вместе с выкладкой ядра | +| arm-ka-docs#116 — единая нормализация ББ/ОФР | Выполнено в `arm-core` `main` (#129, #132) — см. §1 и §5.6.1; ждёт `arm-web` (план 6) и выкладки сервисов tabular-parser / scan-gateway на контракт 2.0 | типы из §5.6.1 | Состояние этих задач — в Gitea (`RSHB/arm-core`, `RSHB/arm-ka-docs`); после слияния каждой этот раздел переезжает в §1. diff --git a/openspec/changes/unified-statement-normalization/design.md b/openspec/changes/unified-statement-normalization/design.md index 34ae640..d6652df 100644 --- a/openspec/changes/unified-statement-normalization/design.md +++ b/openspec/changes/unified-statement-normalization/design.md @@ -1397,3 +1397,332 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо 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): Значок — `