## 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, а не пропуск тестов | | 2026-10-18 | **R-4.** A0 (только артефакты OpenSpec) без отдельного ревью сабагентом: достаточно `openspec validate --strict` | кода в задаче нет | неточность формулировок дельт; поймало финальное ревью | | 2026-10-18 | **R-5.** Ревью A1 и A2 объединено в один пакет после A2 | downgrade-тесты A1 проверяемы только с A2 | находки по A1 пришли на один раунд позже | | 2026-10-18 | **R-6.** Ревью малых задач объединялось парами (A3+A4, A8+A9, A10+A11, A12+A13) | экономия раундов ревью | находки по первой задаче пары приходят на шаг позже | | 2026-10-18 | **R-7.** Change `unified-normalization-core-a` архивируется только вместе с `unified-normalization-core-b`; тексты поведения части Б в дельтах А остаются | разбор дельт на два change дороже и не нужен до выкладки обеих частей | если А заархивировать отдельно, `openspec/specs` опишет нереализованное | | 2026-10-18 | **R-8.** Одна волна правок по итогам финального ревью (sonnet), затем один scoped re-review; M-6, M-8, M-13 не правятся (deferred / rejected) | замечания малы; M-13 (advisory-блокировки purge пофайлово) — окно обслуживания | M-6 (отдельный SUM для `code_amount`) и M-8 остаются техдолгом | | 2026-10-18 | **A1-1.** `result_mapping.map_file` до A5/A9 присваивает `file_ordinal` порядковым номером создания документов | иначе `uq_document_stored_file_id_file_ordinal` ломает файл с 2+ документами | номера не равны индексу в `documents[]`; заменено в A9/A5 | | 2026-10-18 | **A1-2.** `organization_name_modified` — nullable boolean (как в DBML); миграция пишет `false` | соответствие DBML v5.19 | нет | | 2026-10-18 | **A1-3.** Тест-фабрика `tests/factories.py` и `tests/test_models_part_04_06._document` нумеруют `file_ordinal` | уникальность `(stored_file_id, file_ordinal)` | нет | | 2026-10-18 | **A1-4.** Старый тест «две BALANCE с NULL -> IntegrityError» переписан под DISCLOSURE | по v5.19 для форм это больше не нарушение | нет | | 2026-10-18 | **A1-5.** Имена FK/PK новых объектов (`fk_financial_statement_value_confirmed_by_app_user`, `fk_document_issue_*`, `pk_document_issue`) — по NAMING_CONVENTION | совпадают с `create_all`, тест схемы зелёный | нет | | 2026-10-18 | **A2-1.** `line_status` считается потерянным при `<> 'ok'` (колонка NOT NULL default 'ok') | `IS NOT NULL` всегда истинно | шум в журнале отката | | 2026-10-18 | **A2-2.** Булевы счётчики и `organization_name_modified` считаются как `IS TRUE`; причина отказа «наименование правилось» — `organization_name_modified IS TRUE` | NULL не считается правкой | нет | | 2026-10-18 | **A2-3.** Причина «строки формы без кода» — по `line_code IS NULL` (любые), «повтор» — только по ненулевым кодам | точность причин отказа отката | нет | | 2026-10-18 | **A2-4.** Сообщение журнала склеивает счётчики через `; ` | читаемость | нет | | 2026-10-18 | **A3-1.** Только PATCH, меняющий `crm_id`, пишет `GROUP_CHANGE` (`before/after` всех изменённых полей запроса); PATCH только имени/даты остаётся `MEMBERSHIP_CHANGE`; создание — `MEMBERSHIP_CHANGE` с `crm_id` в payload | P3-18 / Р-41 про смену `crm_id`; минимум изменения поведения | фильтр аудита по действию для правок групп расходится; правка в одну строку | | 2026-10-18 | **A3-2.** Предпроверка уникальности `crm_id` не учитывает видимость (глобальная уникальность), как для ГК | единый подход | `crm_id` невидимой группы раскрывает существование через 409 (как ГК/ЮЛ) | | 2026-10-18 | **A3-3.** Before-payload удаления группы включает `crm_id` | полнота аудита | нет | | 2026-10-18 | **A4-1.** `run` принимает подменяемую фабрику транзакции (по умолчанию `session_transaction`) | тесты идут на `db_session` | нет (боевой путь — по умолчанию) | | 2026-10-18 | **A4-2.** `PurgePlan` несёт `include_mixed`; `BlockingVersion` по `ex_snapshot_item` имеет статус `EX_SNAPSHOT_ITEM` и `version_id = ex_snapshot.source_version_id` | сигнатура `apply_purge` без флага | нет | | 2026-10-18 | **A4-3.** Проверка блокировки покрывает ВСЕ документы удаляемых целиком файлов (в том числе OSV/DISCLOSURE; OSV смешанных файлов с `--include-mixed`) | иначе удаление сломало бы FK/заморозку | лишний отказ, если OSV удаляемого файла в верифицированной версии (корректно: она заморожена) | | 2026-10-18 | **A4-4.** `apply_purge` блокирует все файлы плана (jobs -> file -> documents, по id файла), перепланирует под блокировками и отказывает до первой мутации (`PurgeRefusedError`); версии блокируются позже (file -> doc -> version) | защита от гонки с загрузкой | окно между проверкой и блокировкой версий (T4.3, rejected) | | 2026-10-18 | **A4-5.** Dry-run с блокировкой печатает блок отказа, но выходит с 0; `--apply` с блокировкой: отчёт в stdout, сообщение в stderr, код 1 | P3-13: 0 = отчёт | нет | | 2026-10-18 | **A4-6.** `FILE_DELETE.after_value`: для целого файла `{unbound_version_ids}`, для смешанного `{deleted_documents, unbound_version_ids}` | трассируемость очистки | нет | | 2026-10-18 | **A4-7.** `superseded_with_osv` — файлы, у которых `superseded_by_file_id` указывает на удаляемый файл и которые содержат OSV | сохранение замещения ОСВ | нет | | 2026-10-18 | **A6-1.** `section_number` возвращает строчные буквы (работает по нормализованному тексту) | внутренний формат | нет | | 2026-10-18 | **A6-2.** `names()` — имена без повторов в порядке редакций и появления; `edition_codes` — по всем записям | стабильность подсказки | нет | | 2026-10-18 | **A6-3.** Если «итого по разделу» только у одного из имён, номер у другого `None` — сравнение только по равенству строк | спека | нет | | 2026-10-18 | **A6-4.** Пустые справочные имена пропускаются | шум | нет | | 2026-10-18 | **A5-1.** `FileWarning` наследует `DocumentWarning` (есть также `line_refs`/`column_index`/`field`): схема fileWarning = warning + locator | надмножество брифа, без дублирования полей | нет | | 2026-10-18 | **A5-2.** `MetricsV2` наследует `Metrics` (переопределяет `warnings`); ветвление в `_metrics_json` идёт по классу конверта, не метрик | изоляция v1/v2 при общей базе метрик | v2-конверт обернул бы уже готовые структуры; покрыто `test_inbox_v2_warnings_kept` | | 2026-10-18 | **A5-3.** `parse_message`/`parse_result`: не-2 MAJOR отдаётся модели v1, чей шаблон `^1\.\d+$` даёт `ValidationError` (3.0, «banana», нет поля); нечитаемый JSON в `parse_result` — `json.JSONDecodeError` (ValueError), не `ValidationError` | единая точка отказа для неподдерживаемых версий | вызывающий код, ждущий только `ValidationError`, пропустит ошибку чтения JSON (см. открытый вопрос) | | 2026-10-18 | **A5-4.** У v2-предупреждения без `locator` в метрики пишется как пришло (ключа нет), у LEGACY — `locator: None` | не домысливать данные, не менять пришедшее | потребитель обязан читать `locator` как необязательный (`file_warnings` так и делает) | | 2026-10-18 | **A5-5.** `RecognitionResultMessageV2` не наследует v1; общие поля вынесены в `_MessageBase` | изоляция `isinstance` между версиями | нет | | 2026-10-18 | **A5-6.** `file_warnings`: `locator` не-строка/не-null -> объект пропускается | защита от мусора во входящем конверте | потеря предупреждения с некорректным locator | | 2026-10-18 | **A6-5.** Старые тесты API обновлены под код 1105 в `fsbu4` (yaml дополнен 1105/1130/1140/1530/2460 и вторыми именами) | данные docs | для ГСК-кода 1105 на 66н с датой `fsbu4` появляется пустая строка «Гудвил» (O-2 A6) | | 2026-10-18 | **A7-1.** `FailedCheck.codes` — поле с default `()`, участвует в сравнении | P3-14 | низкая | | 2026-10-18 | **A7-2.** `insert_statement` до маппера 2.0 пишет `row_index` = порядковый номер строки черновика | иначе все строки источника стали бы `origin = template` | заменено в A9/части Б | | 2026-10-18 | **A7-3.** `checks_input` принимает объекты-колонки (id, column_index) — протокол, без импорта моделей в домен | чистый домен | нет | | 2026-10-18 | **A7-4.** `code_amount`: колонка — первая по `column_index`, затем SUM по ней; нет колонки или непустых — `None` | определённость суммы | M-6: отдельный SUM, deferred в часть Б | | 2026-10-18 | **A7-5.** В API `checks.failed[].codes` и `line_ids` не выведены в A7 (только домен) | не в брифе A7 | закрыто в A8 | | 2026-10-18 | **A7-6.** Порядок строк ответа в A7 не менялся | P3-17 — задача A8 | нет | | 2026-10-18 | **A8-1.** `insert_statement` временно пишет `period_kind` (`absolute` при дате, иначе `none`) | иначе тест брифа на 1.x не проходит до A9 | заменено в A9; совпадает с backfill миграции | | 2026-10-18 | **A8-2.** `FailedCheckOut.line_ids` — объединение `line_ids` агрегата по `codes` правила; у межформенной проверки — строки только читаемой формы | кода второй формы в агрегате нет | фронт не подсветит строку другой формы | | 2026-10-18 | **A8-3.** `header.source`: запись с `text: null` отдаётся как есть, нечисловая уверенность -> `null`, не-dict записи пропускаются | T1.1: «нет подсказки» | нет | | 2026-10-18 | **A8-4.** `order_lines`: `detail` с неизвестным родителем ведёт себя как `detail` без родителя; вложения по `row_index`; равные ключи — порядок входа (по `id`) | P3-17 | нет | | 2026-10-18 | **A8-5.** `IssueCounts` — dataclass репозитория, сервис переводит в `IssueCountsOut` | слои | нет | | 2026-10-18 | **A9-1.** `ColumnDraft` вместо кортежа в `StatementDraft.columns` (несёт `period_kind` / `label_confidence`) | нужны поля колонки | правка 4 утверждений в тестах | | 2026-10-18 | **A9-2.** `header_source.*.confidence` в БД — число как пришло из 1.x (float), не Decimal | JSONB; P3-20 | нет | | 2026-10-18 | **A9-3.** Повтор формы определяется только при известных годе и квартале у обеих; текст issue без префикса locator, в `metrics.mapping_warnings` — с префиксом | P3-4 | нет | | 2026-10-18 | **A9-4.** Пустая ячейка (нет значения) -> `needs_review = false` (`value_clean(None, None)` истинно) | формула приёма 1.x | нет | | 2026-10-18 | **A9-5.** У листа ОСВ issue пишутся и для предупреждения о повторе счёта (относится к первому листу) | P3-5 | нет | | 2026-10-18 | **A10-1.** Контрагент `UNKNOWN` трактуется как `None`, блокирует только `MISMATCH` | спека: `counterparty_check != MISMATCH` | низкая | | 2026-10-18 | **A10-2.** `name_mismatches`: строка `ok` с `line_code_confidence = None` считается уверенной (не проверяется); `line_status` сравнивается со `StatementLineStatus.ok` (StrEnum) | спека: «confidence < 100» (T10.1: покрыто комментарием и тестом) | строка без уверенности не даёт `NAME_MISMATCH` | | 2026-10-18 | **A10-3.** `has_error_warning`: строго `severity == "error"` (регистр важен), не список -> `False` | строгий контракт | нет | | 2026-10-18 | **A10-4.** `noqa PLR0913` на `form_status_v2` и тестовом хелпере | сигнатура задана планом | нет | | 2026-10-18 | **A11-1.** Тест `test_disclosure_is_confirmable` удалён: `form_blocker` принимает meta, у расшифровки её нет; не-форма проверяется вызывающими (FORM_TYPES) — **тест возвращён в волне правок (T11.1)** на уровне сервиса | покрытие подтверждения DISCLOSURE/OTHER | нет | | 2026-10-18 | **A11-2.** `confirmed_twin` при пустом периоде возвращает `None` (`PERIOD_REQUIRED` сработает раньше); первым выбирается близнец с меньшим `file_ordinal` | детерминизм | нет | | 2026-10-18 | **A11-3.** Дубль ищется только среди `CONFIRMED`; второй `NEEDS_CHECK`-близнец не блокирует | бриф | нет | | 2026-10-18 | **A11-4.** `line_id` в `extra` — строка (`str(uuid)`), как `document_id` | единообразие | нет | | 2026-10-18 | **A12-1.** Новый код 422 `DUPLICATE_COLUMN` (колонка дважды в `columns[]`) | в брифе не было | низкая | | 2026-10-18 | **A12-2.** Подтверждение даты отчёта требует непустого `report_date` (null == null не подтверждение) | смысл подтверждения | низкая | | 2026-10-18 | **A12-3.** Равное текущему `organization_name` — no-op | подтверждать нечего | нет | | 2026-10-18 | **A12-4.** Снятие подтверждения ячейки и возврат суммы к исходной: `needs_review` = не(правлена | подтверждена | строка detail | value_clean) — формула приёма 1.x | для 2.0 точное значение даст часть Б | ячейка 2.0 с иным сомнением (знак) может показаться чистой до Б | | 2026-10-18 | **A12-5.** `recompute` дополнительно сбрасывает `needs_review` у правленых/подтверждённых значений и значений строк `detail` (`settle_edited_values`) | согласованность флагов | T13.3: флаг навсегда `false` у значений detail (часть Б) | | 2026-10-18 | **A12-6.** Подтверждение ячейки без записи значения в БД — 422 `UNKNOWN_CELL` до записей; колонка не этой формы в `values[]` — `UNKNOWN_CELL`, `UNKNOWN_COLUMN` — только для `columns[]` | существующий тест | нет | | 2026-10-18 | **A12-7.** `is_current` пересчитывается только при смене дат колонок (самая поздняя дата, меньший `column_index`) | как при приёме | нет | | 2026-10-18 | **A12-8.** Ключи аудита подтверждений: `value::confirmed`, `column::date_confirmed`, `needs_review_fields`; смена даты колонки — `column:` | единый формат | нет | | 2026-10-18 | **A12-9.** Смена `report_date` убирает `report_date` из `needs_review_fields` без отдельного ключа аудита | минимум шума | нет | | 2026-10-18 | **A12-10.** `lines[]` в `StatementPatch` нет в A12 (добавлено в A13) | разбиение задач | нет | | 2026-10-18 | **A13-1.** Тесты используют код 1215 вместо 1250 из брифа | 1250 есть в источнике ББ Межениновской, а нужен «только шаблонный» код | нет | | 2026-10-18 | **A13-2.** `tests/test_api_statements.py::insert_line` кладёт `StatementLineStatus`, а не строку | identity map сервиса | нет | | 2026-10-18 | **A13-3.** Пометка шаблонной строки (`row_index IS NULL`) как `detail` — 422 `INVALID_LINE_PATCH`; подтверждение кода у строки не `ok` — тоже | спека такого перехода не перечисляет; у `detail` нужен `row_index` для родителя | низкая, фронт такого не шлёт | | 2026-10-18 | **A13-4.** Назначение того же кода строке `ok` с этим кодом — no-op; `detail -> detail` — no-op | нечего менять, аудита нет | T13.1: no-op элемент может дать ложный 409 — недостижимо | | 2026-10-18 | **A13-5.** Порядок проверок по элементам: действие, повтор, `UNKNOWN_LINE`, `LINE_CODE_REQUIRED`, `INVALID_LINE_CODE`, допустимость; затем отдельным проходом `LINE_CODE_TAKEN` (409 после всех 422) | предсказуемость | нет | | 2026-10-18 | **A13-6.** `needs_review` при снятии подтверждения: `ok` и не `line_code_modified` и уверенность < 100 | правило приёма 1.x; для 2.0 уточнит часть Б | нет | | 2026-10-18 | **A13-7.** Назначение кода: `confidence = Decimal("100.00")` (ответ отдаёт объект сессии) | без перечитывания | нет | | 2026-10-18 | **A13-8.** Удаление пустой шаблонной строки пропускается, если она сама — цель элемента запроса | избежать удаления цели | нет | | 2026-10-18 | **A13-9.** **Отступление от P3-10:** 422 в `values` / `columns` после применённых `lines` откатывает всю транзакцию запроса; отдельной предпроверки адресации `values` для `lines` нет. `UNKNOWN_CELL` по `line_code` видит назначения `lines` этого запроса (они применены и flush-нуты раньше); 409 приоритетнее такого 422 | эквивалентное наблюдаемое поведение при меньшем коде (T13.5) | при ошибке в откате частичная запись — тест M-10 фиксирует отсутствие записей и аудита | | 2026-10-18 | **A13-10.** `user_id` в `apply_line_edits` принят по интерфейсу, не используется; аудит строк — ключ `line:` с `{line_code, line_status, parent_row_index, is_confirmed}`, в него попадают и `detail` со сменой только родителя | у строки нет автора правки | нет | | 2026-10-18 | **A13-11.** Пометка шаблонной строки как `detail` = 422 `INVALID_LINE_PATCH` | см. A13-3 | низкая | ## Open questions Вопросы владельцу (O-n из плана 3, не закрытые решениями Р-34 / Р-35 / Р-40 / Р-41; нумерация плана). - **O-1.** Р-10 для `1.x` (P3-4): вторая форма того же типа хранится и при `1.x` (план) или пропускается, как раньше? - **O-3 (закрыт Р-40 для четырёхзначных).** `CODE_NOT_IN_DICTIONARY` / `CODE_NOT_IN_EDITION` — только для четырёхзначных кодов; пятизначные подстроки Excel `info` не дают. Подтвердить, что сценарий спеки «Нестандартный код Excel» приведён к этому. - **O-4.** Пересчёт проверок ядра уровня `info` после правки (P3-8) спека не описывает; после смены кода они не пересчитываются в части А (T13.4), пересчёт — в части Б. - **O-6 (закрыт Р-41).** Смена `crm_id` ГСК/ГСЗ — `GROUP_CHANGE`. - **O-7.** Конверт с неизвестным MAJOR (`3.0`) inbox отбрасывает с предупреждением в лог (задание закроется по сроку); сценарий «файл получает `ERROR` с `mapping_error`» выполняется для файла результата. Оставить? - **O-9.** Выкладка: PR части А в `main` без снимка контура до части Б; встроенный фронт `arm-core/web` получает новые типы только через `sync-web` после плана 6. - **O-10.** Метрики `fields_*` ядро не считает по строкам формы (копирует из конверта, `recognition_exchange.FIELD_COUNTERS`). Подтвердить, что спека («метрики `fields_*`») не требует нового расчёта. - **Наблюдение.** Хранимая вторая ББ теперь входит в ключи автозамещения: файл с ББ на 30.09.2025 и ББ на 31.12.2024 может заместить более старый файл; его ОФР при этом выпадает с предупреждением OQ-154. - **A5-O1.** `parse_result` на битом JSON бросает `JSONDecodeError`/`UnicodeDecodeError`, а не `ValidationError`: в части Б при переключении `_load_result` ловить `ValueError`. - **A5-O2.** `statement-code-orders.yaml` не скопирован в `api/contracts` (тест идентичности его не покрывает); при необходимости ядру — скопировать в части Б. - **A5-O3.** `frontend-api-guide` в docs должен описывать `JobBrief.warnings` как объекты (docs, план 6). - **Нумерация.** Пропуски в нумерации O-n (O-2, O-5, O-8) закрыты планом/рулингами Р-n.