docs(openspec): решения P3-53…P3-59 fix-волны, уточнение P3-39

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-18 22:38:28 +04:00
parent e6292a24df
commit 0f84088b73

View File

@ -47,7 +47,7 @@ Goals и Non-goals — см. `proposal.md`.
| 2026-10-18 | **P3-36 (B3).** `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` |
| 2026-10-18 | **P3-37 (fix1, T2.1).** Аудит `lines[]` довносит смену `parent_row_index` после пересчёта: у правленных строк значение заменяется итоговым, у остальных появляется запись `line:<id>` с одним ключом `parent_row_index` (`{"parent_row_index": старый}` → `{…: новый}`) | родителей пишет `recompute_form` уже после снимка строк правки; полный снимок для не правленных строк не нужен | в событии `RECOGNIZED_DATA_EDIT` у не правленных строк неполный набор ключей |
| 2026-10-18 | **P3-38 (fix1, T1.1/T1.2/T2.4/T2.6/T2.8).** Паритет редакции приёма и пересчёта для строки с `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:*` не следует за датой отчёта |
| 2026-10-18 | **P3-39 (B4).** Проверки ядра `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). Домен пакет не импортирует: редакцию (`engine.edition`) и перечень кодов (`engine.code_order`) считает сервис; `statement_recompute` реэкспортирует прежние имена. Вместо сигнатуры плана `core_info_issues(*, lines, edition, form_variant, catalog, engine)` | домен не должен знать `StatementsEngine`/`arm_statements` (D-1); одно правило для приёма и пересчёта | нет; импортёры прежних имён из `statement_recompute` работают |
| 2026-10-18 | **P3-39 (B4).** Проверки ядра `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` работают |
| 2026-10-18 | **P3-40 (B4).** Приём форм `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 из брифов | нет |
| 2026-10-18 | **P3-41 (B4).** `DISCLOSURE` в `2.0` — по-прежнему один документ на файл со статусом `RECOGNIZED` (D-3), `file_ordinal` первого; предупреждения всех объединённых документов результата пишутся в его `document_issue` источником `service`. Статус сервиса и предупреждения файла уровня `error` на него не влияют | спека условие `RECOGNIZED` относится к формам ББ/ОФР; у `DISCLOSURE` нет данных | `DISCLOSURE` с `needs_check` сервиса автоподтверждается (данных нет — подтверждать нечего) |
| 2026-10-18 | **P3-42 (B4).** `STATEMENTS_VERSION_MISMATCH` пишется и при приёме `2.0` (`version_issue(job.statements_version, engine.version)`), а не только при пересчёте после правки | иначе запись появлялась бы только после первой правки; один набор записей `core` у приёма и пересчёта | лишний `info` на формах, принятых копией иной версии |
@ -61,5 +61,12 @@ Goals и Non-goals — см. `proposal.md`.
| 2026-10-18 | **P3-50 (B6).** `FailedCheck.codes` в ядре остаётся (вопреки плану, который удалял поле): оно нужно межформенной проверке 1370↔2400 (`codes = ("1370", "2400")`) для `codes` / `line_ids` в `checks.cross_form` ответа; нарушения §7 по колонкам читаются из `document_issue`, где коды хранятся сами | межформенную проверку пакет не считает (она по двум документам), без `codes` подсветка строк в ответе пропала бы | у `cross_form` не было бы `codes`/`line_ids`, фронт не подсвечивал бы строки 1370 и 2400 |
| 2026-10-18 | **P3-51 (B6).** `checks_report` уже брал допуск у `engine.tolerance()` (с B2), поэтому правки сервиса в B6 нет; константа допуска ядра удалена вместе с `load_check_rules`. Состав и порядок редакций справочника имён `statement_lines.yaml` не переписывается на `engine.editions()` в коде: согласованность проверяет тест `test_name_dictionary_codes_in_code_orders` (редакции справочника = редакции пакета в том же порядке; каждый код справочника есть в перечне редакции пакета, любой вариант формы) | справочник остаётся данными имён и состава экрана; перечни кодов — единственный источник в пакете, расхождение ловит тест, а не молчаливая подмена в рантайме | при добавлении редакции в пакет без справочника имён тест красный; в рантайме новая редакция дала бы форму без шаблонных строк |
| 2026-10-18 | **P3-52 (B6).** `edition_for` (граница 66n/fsbu4 по дате для шаблона экрана) в `domain/statement_mapping.py` не тронут: переход на `engine.edition(...)` требует кодов формы и варианта и относится к ответу формы (B7/доработка), а не к удалению копии словаря | не расширять B6 за рамки брифа | ответ формы и приём могут расходиться в редакции на формах без даты; существующее поведение сохранено |
| 2026-10-18 | **P3-53 (fix-волна, I-3, R-11).** Источник «ошибки файла» единый: при приёме результата `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` без причины; задание без ключа читается по-старому |
| 2026-10-18 | **P3-54 (fix-волна, I-2).** `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` от правки шапки |
| 2026-10-18 | **P3-55 (fix-волна, T4.2).** `checks_on_intake_1x(..., now)` принимает момент приёма параметром (из `map_file`), а не `utc_now()`: записи `relation` / `soft` формы `1.x` получают тот же `created_at`, что и остальные записи приёма | единое «сейчас» приёма, воспроизводимость тестов | различие `created_at` на миллисекунды между записями одного приёма; порядок записей в ответе мог бы нарушиться |
| 2026-10-18 | **P3-56 (fix-волна, M-4).** `statement_mapping_v2._lines_and_values` строит `ref → row_index` только по принятым строкам (первый вход побеждает; ссылка вперёд на ещё не встреченную принятую строку допустима): `parent_ref` строки, чей `ref` повторяет прежний, указывает на первую принятую строку, а не на пропущенный дубль (раньше побеждал последний) | пропущенная строка не должна становиться родителем; детализация висела бы на несохранённом `row_index` | расшифровка привязана не к той строке; пересчёт `detail_parents` всё равно исправляет её после первой правки |
| 2026-10-18 | **P3-57 (fix-волна, T4.4, T6.1).** Закрыты тестами ветки приёма `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) | покрытие веток без изменения поведения | нет |
| 2026-10-18 | **P3-58 (fix-волна, I-4).** Тесты правки и акцепта формы, принятой маппером `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 подтверждает) | тесты на несогласованной форме проходили бы вхолостую |
| 2026-10-18 | **P3-59 (fix-волна, M-5, M-6, M-7, T6.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 получал бы неверные гарантии о проверках и акцепте |
| 2026-10-18 | **P3-60 (fix-волна, I-5).** В ответах нарушение §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`) |
| 2026-10-18 | **P3-61 (fix-волна, C-1, M-1).** Дельты 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 Б нужно обновить синхронно |