Andrey Epifancev 028512603a fix(statements): правки ревью B1+B2 — T2.1, T1.1, T1.2, T2.4, T2.6, T2.8
T2.1: аудит lines[] включает итоговую смену parent_row_index после пересчёта.
T1.1: тест паритета редакции приёма и пересчёта для строки с confidence < 100 и code_fixed.
T1.2: edition_codes пропускает строку ok без кода.
T2.4: общий issue_column_index для ответа формы и detail отказа подтверждения.
T2.6: удалено дублирование needs_review в statement_line_edits.
T2.8: неизвестный relative:* не даёт 500 (адаптер stored_form и _resolve_relative_columns).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:15:21 +04:00

14 KiB
Raw Blame History

Context

Сквозной дизайн — change docs unified-statement-normalization (design.md, specs/arm-core/spec.md, план plans/2026-10-18-plan-3-arm-core.md). Здесь — решения уровня ядра для части Б; решения части А — в openspec/changes/unified-normalization-core-a/design.md (D-1…, A1-1…A5-6, P3-1…P3-24). Схема данных — ../docs/model/data-model-v5.19.dbml; код части А уже в main.

Goals / Non-Goals

Goals и Non-goals — см. proposal.md.

Decisions

  • D-1. Единственная точка входа в пакет. import arm_statements — только api/src/services/statements_engine.py (протокол StatementsEngine и его реализация) и api/src/domain/stored_form.py; остальной код ядра работает с протоколом.
  • D-2. §7 хранится только как document_issue. GET …/statement и POST …/confirm читают нарушения из записей; временный расчёт на лету (P3-14 части А) удаляется в B6.
  • D-3. Архивация вместе с частью А (R-7 части А). unified-normalization-core-b архивируется только вместе с unified-normalization-core-a и только после выкладки обеих частей; сначала архивируется А, затем Б (дельты Б MODIFIED опираются на требования, которые А добавляет).
  • D-4. Дельта «копия словаря §7 ядра» оформлена как MODIFIED. Требования с таким именем в openspec/specs/ нет: упоминание копии живёт в «Соотношения §7 по колонкам»; поэтому удаление выражено правкой этого требования (и пунктом «Словари ядра» в documents-api), а не REMOVED.

Decisions made without discussion

Решения, принятые без обсуждения с человеком на этапе плана и исполнения части Б (маркеры P3-n — из плана 3, R-n — рулинги контроллера, Р-n — решения владельца). Таблица дополняется задачами B1–B6.

Дата Решение Обоснование Цена ошибки
2026-10-18 R-1 (Р-40). CODE_NOT_IN_DICTIONARY / CODE_NOT_IN_EDITION пишутся только для кодов из ровно четырёх цифр; пятизначные подстроки Excel не проверяются. P3-7 плана в этой части заменён (запись на код документа сохраняется, пятизначные коды — нет) решение владельца 18.10.2026 (Р-40) перед исполнением Б; закрывает O-3 (поток info у пятизначных) пропущенный info у нестандартного пятизначного кода; восстановление — снять условие длины кода в маппере 2.0 и пересчёте
2026-10-18 R-2 (Р-34). Подтверждение кода строки — флаг is_confirmed; line_code_confidence и line_code_modified не меняются (P3-16 части А) решение владельца 18.10.2026; в пересчёте Б правило is_confirmed ⇒ needs_review строки = false берётся из P3-16 нет — поведение уже реализовано в части А
2026-10-18 R-3. Удаление копии словаря §7 в дельте оформлено как MODIFIED «Соотношения §7 по колонкам», а не REMOVED (см. D-4); «Ответ GET …/statement» и порядок 409 оформлены новыми ADDED-требованиями, так как в openspec/specs/ и дельтах части А одноимённых нет openspec archive требует, чтобы имя REMOVED/MODIFIED существовало; иначе архив упадёт при архивации — правка имён заголовков дельт
2026-10-18 P3-25 (B1). Тип формы во входе пересчёта — document.document_type (BALANCE / INCOME / DISCLOSURE), а не meta.statement_type (только balance / income); план писал «тип формы» без источника DISCLOSURE не представим в statement_type; тип документа — единственный источник для всех трёх типов форма DISCLOSURE собралась бы с ошибкой KeyError / потерей типа; правка — одна строка в build_stored_form
2026-10-18 P3-26 (B1, O-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
2026-10-18 P3-27 (B1). edition_codes берёт строки ok результата 2.0, сортирует по row_index и помечает код уверенным при line_code_confidence == 100 (ровно), None — неуверенный как в разделе «Что ядро вызывает» и в recompute пакета (код правленый/подтверждённый там — code_fixed) редакция приёма разойдётся с редакцией пересчёта; сверку делает test_edition_codes_and_engine_edition
2026-10-18 P3-28 (B2). StatementService получает StatementsEngine через зависимость get_statements_engine (тесты подменяют её FakeEngine); FailedRelation, StatementChecks, PeriodKind реэкспортируются из services/statements_engine.py, чтобы import arm_statements оставался только в двух файлах единственная точка импорта пакета (common.md); подмена без патчей нет
2026-10-18 P3-29 (B2). Статус после правки — 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 — как при приёме
2026-10-18 P3-30 (B2). Пустая цель при непустых членах — нарушение с expected = NULL (P1-21); тесты части А «очищенная 1700 не цель» и «цель потерянной строки не проверяется» переписаны под это; потерявшая строку 1700 форма блокирует подтверждение до правки или акцепта следствие P1-21 пакета; спека («посчитанное соотношение») форма, потерявшая итоговую строку, теперь требует действия аналитика — если владелец против, правится правило пакета (план 1), не ядро
2026-10-18 P3-31 (B2). NOTHING_TO_ACCEPT и CHECKS_NOT_PASSED читают записи relation; акцептованная форма не считается нарушенной (statement_hard_failures пуст при checks_accepted_at), поэтому повторный акцепт — 409 NOTHING_TO_ACCEPT, как в части А сохранение поведения А; буква брифа («нет записей relation») допускает оба прочтения повторный акцепт с новым комментарием станет возможен, если снять условие
2026-10-18 P3-32 (B2). «Эффективная дата» для relative:* колонок при правке header.report_date — новое значение meta.report_date (в том числе NULL ⇒ period_date = NULL), не document.reporting_date; изменение дат колонок идёт в аудит ключом column:<id> без признака правки данных период документа сам выводится из даты отчёта и дат колонок (§9.5 п. 2), поэтому циклическая зависимость недопустима колонка при пустой дате отчёта теряет дату; аналитик ставит её вручную
2026-10-18 P3-33 (B2). Запись relation / soft несёт и column_id (колонка найдена по column_key: str(column_id), затем str(column_index)), и column_index нарушения; ответ берёт индекс из колонки, иначе из записи, иначе -1 ключ колонки B4 для 2.0 — индекс нет
2026-10-18 P3-34 (B2). NAME_MISMATCH пересчитывается для всех кодов (R-1 ограничивает только CODE_NOT_IN_*); CODE_NOT_IN_* не пишутся, если пакет не определил редакцию (а CODE_NOT_IN_EDITION — и вариант); словарь имён берётся по редакции пакета, не по дате R-1 и спека (редакция документа — edition пакета) пятизначная подстрока с уверенностью < 100 даёт NAME_MISMATCH (кода нет в словаре) — если поток окажется шумным, ограничить name_mismatches четырёхзначными
2026-10-18 P3-35 (B2). Временный код 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 больше не затираются навсегда) нет
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:* не следует за датой отчёта