## Context Сквозной дизайн — change docs `unified-statement-normalization` (`design.md`, `specs/arm-core/spec.md`, плана `plans/2026-10-18-plan-3-arm-core.md`). Здесь — решения уровня ядра для части А: где лежит код, как идёт миграция, какие допущения сделаны планом. Схема данных — `../docs/model/data-model-v5.19.dbml`. ## Goals / Non-Goals Goals и Non-goals — см. `proposal.md`. ## Decisions - **D-1. Часть А не зависит от пакета.** До части Б §7 считается на лету по `api/rules/statement_checks.yaml` и агрегату формы по коду (P3-14); временный код удаляется в B6. - **D-2. Миграция только добавляет.** Одна ревизия (0007): новые колонки, таблица, enum'ы, индексы и значения по умолчанию; §7 по существующим формам не пересчитывается (Р-24); откат останавливается на несовместимых данных (спека «Миграция DBML v5.19»). - **D-3. `origin = template` ⇔ `row_index IS NULL`.** `line_code_original_value IS NULL` для `origin` больше не используется. - **D-4. Порядок A5 — последним (R-1).** ## Decisions made without discussion Решения, принятые без обсуждения с человеком на этапе плана и исполнения части А (маркеры `P3-n` — из плана 3; `R-n` — рулинги контроллера; `Р-n` — решения владельца, поверх плана). | Дата | Решение | Обоснование | Цена ошибки | |---|---|---|---| | 2026-10-18 | **P3-1.** Миграция пишет `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 сырыми нет | подсказка у поля старой формы беднее; формы старых времён на контуре удалены очисткой | | 2026-10-18 | **P3-2.** `organization_name_modified` — `false` у всех форм (миграция и оба маппера), не `NULL` | как `inn_modified` в маппере 1.x; API отдаёт `bool` | одна строка `UPDATE` | | 2026-10-18 | **P3-3.** `needs_review_fields`: маппер `1.x` пишет `[]`, миграция оставляет `NULL`, чтение `NULL` = `[]` | спека миграции поле не заполняет | нет | | 2026-10-18 | **P3-4.** Маппер `1.x` тоже хранит вторую форму того же типа (Р-10); формы одного типа и одной пары `period_year`, `period_quarter` получают `MAPPING_WARNING` «в файле есть другая форма <тип> за тот же период» и по D-6 — `NEEDS_CHECK` (аналог Р-18) | частичный индекс v5.19 больше не заставляет пропускать; одно правило хранения для обоих MAJOR | O-1: если владелец хочет прежний пропуск — вернуть ветку `continue` | | 2026-10-18 | **P3-5.** Предупреждения маппинга листа ОСВ (повтор счёта, строки без родителя, нарастающая по данным Q1) пишутся ещё и в `document_issue` документа листа (`warning`, `MAPPING_WARNING`, `info`, `core`) при любом MAJOR; строки в `metrics.mapping_warnings` остаются | спека: `DocumentOut.issues` ОСВ — «вместе с предупреждениями маппинга ядра этого листа» | нет | | 2026-10-18 | **P3-6.** `file_ordinal` объединённого документа `DISCLOSURE` / `OTHER` — индекс первого такого документа в `documents[]` | несозданный документ «номер не занимает» | нет | | 2026-10-18 | **P3-7.** **Переопределено Р-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` у пятизначных) | | 2026-10-18 | **P3-8.** После правки записи `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 | | 2026-10-18 | **P3-9.** Результат `2.0`: значение с `column_index` вне колонок формы и повтор `column_index` колонки — первое остаётся, `MAPPING_WARNING` | FK значения на колонку; уникальность `(meta_id, column_index)` | нет | | 2026-10-18 | **P3-10.** Части `PATCH` применяются в порядке `lines` → `values` → `columns` → `header`; вся адресация (422) проверяется до первой записи; `UNKNOWN_CELL` по `line_code` — по кодам строк `ok` с учётом назначений `lines` этого запроса; один `line_id` дважды в `lines` — 422 `INVALID_LINE_PATCH` | атомарность и предсказуемость | нет | | 2026-10-18 | **P3-11.** `reference_name` — первое имя кода в редакции ответа (`edition` по дате); нет редакции или кода — `null` | спека: «первое имя кода в словаре»; `edition` ответа — по дате | нет (Р-29) | | 2026-10-18 | **P3-12.** (= Р-35, решение владельца.) Очистка отказывает, только если документ формы упомянут в `ex_snapshot_item` или привязан к версии в статусе «Верифицирована» или дальше: `VERIFIED` или `ARCHIVED` с `ex_snapshot.source_version_id` = версия; привязки к версиям в работе, на согласовании, `REJECTED`, `CANCELLED` (и архивированным отменённым) снимаются, статус версий не меняется | решение владельца 18.10.2026 (Р-35): верифицированная версия read-only и заморожена в ЕХ, прочие — нет; на стенде версий дальше «в работе» нет | архивированная версия без снимка ЕХ (из `CANCELLED`) очистке не мешает — если бы мешала, владелец увидел бы лишний отказ | | 2026-10-18 | **P3-13.** Коды выхода `purge_statements.py`: `0` — отчёт или выполнено, `1` — отказ (п. 2 спеки), `2` — ошибка аргументов | как у `check_dbml_vs_orm.py` | нет | | 2026-10-18 | **P3-14.** До части Б §7 считает ядро на лету (`statement_checks.yaml`) по агрегату; `checks.failed[].codes` берутся из правила, `line_ids` — по агрегату | часть А должна быть самосогласованной | временный код удаляется в B6 | | 2026-10-18 | **P3-15.** Для условия `RECOGNIZED` `2.0` предупреждения файла берутся из `metrics.warnings` файла результата; в `recognition_job.metrics.warnings` — из конверта (inbox) | маппер читает файл результата; по контракту наборы одинаковы | нет | | 2026-10-18 | **P3-16.** (= Р-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 приведены к этому | нет | | 2026-10-18 | **P3-17.** Порядок строк ответа: строки `ok` (с шаблонными) — по коду, затем `row_index` (`NULL` последними), затем `id`; за каждой строкой `ok` источника — её «вложения» по `row_index`: строки `detail` с этим родителем и строки `unreadable` / `missing` / `detail` без родителя, у которых она — ближайшая предыдущая по `row_index` строка `ok` источника; вложения без такой строки — в начале | спека «Ответ `GET …/statement`» | нет | | 2026-10-18 | **P3-18.** **Уточнено Р-41 (владелец): смена `crm_id` пишется в аудит как изменение группы (`GROUP_CHANGE`) — закрывает O-6.** Изменение `crm_id` ГСК/ГСЗ пишет одно событие `GROUP_CHANGE` с `before/after` всех изменённых полей вместо `MEMBERSHIP_CHANGE` | спека: «изменение — аудит `GROUP_CHANGE`» | O-6 | | 2026-10-18 | **P3-19.** Ядро зовёт у пакета `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 сравнивает их | | 2026-10-18 | **P3-20.** `header.source.<поле>.confidence` — строка Decimal с двумя знаками (`"61.54"`), как прочие уверенности API; в JSONB `header_source` остаётся число как пришло | фронт читает все уверенности одним способом (план 6, A8) | округление до сотых в подсказке; исходное число — в БД | | 2026-10-18 | **P3-21.** `StatementHeaderOut.okei` — из `financial_statement_meta.okei` (колонка есть с v5.18), только чтение; в `PATCH header` не входит | спека `header` контракта несёт `okei`, экран показывает `issues` с `field = "okei"` у поля | нет | | 2026-10-18 | **P3-22.** Справочник строк для подсказки кода — поле `StatementOut.edition_lines [{code, name}]`, а не отдельный эндпоинт: данные уже загружены (`LineCatalog`, A6) при сборке ответа, нет нового маршрута, права, кэша и запроса на фронте; около 80 пар на форму (≈ 4 КБ) против строк формы с значениями; у формы без даты — коды всех редакций, по одному | наименьший объём реализации в ядре и во фронте; закрывает пробел «у формы без даты подсказки нет» (план 6, O-3) | ответ `GET` тяжелее на ≈ 4 КБ; если понадобится справочник вне формы — отдельный эндпоинт поверх того же `LineCatalog.edition_lines` | | 2026-10-18 | **P3-23.** `DocumentBrief.issue_counts {error, info}` — все записи `document_issue` документа (и `relation` / `soft`, в том числе акцептованные) по `severity`; один групповой запрос на файл | панели файлов нужен признак «есть что смотреть» без запроса на каждый документ (план 6, O-4) | акцептованное нарушение продолжает помечать документ в панели — подробности на экране документа | | 2026-10-18 | **P3-24.** Ошибки элемента `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); одно правило для всех ошибок элемента | при нескольких неверных элементах фронт видит только первый — как и прежде | | 2026-10-18 | **R-1.** Порядок исполнения части А: 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 в конце | | 2026-10-18 | **R-2.** Исполнители задач и их ревьюеры — sonnet; финальное ревью ветки — opus | план даёт прозу, а не готовый код; ревью ветки ловит сквозные расхождения | более слабая модель на задаче — лишние итерации ревью | | 2026-10-18 | **R-3.** Тесты с БД идут на scratch-БД `armcore_pytest_*` compose-Postgres :5432 (устройство conftest); контейнеры не останавливаются и не пересоздаются | общий стенд разработки не должен падать из-за прогонов | недоступный Postgres — задача BLOCKED, а не пропуск тестов | Открытые вопросы плана (`O-n`) — в `.superpowers/sdd/2026-10-18-plan-3-arm-core/p3-decisions.md` (ledger, в git не попадает); закрытые владельцем: O-2 (Р-34), O-3 (Р-40), O-5 (Р-35), O-6 (Р-41). Решения исполнителей по ходу задач A1–A13 дописываются сюда контроллером.