From 42f0dc5ca777be2a54f53c979b92d5d6aeb01d66 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 18 Oct 2026 19:54:40 +0400 Subject: [PATCH] =?UTF-8?q?openspec(core-b):=20change=20=D0=B5=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=BD=D0=BE=D1=80=D0=BC=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D0=B8,=20=D1=87=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D1=8C=20=D0=91=20(arm-ka-docs#116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../unified-normalization-core-b/design.md | 35 ++++++++++ .../unified-normalization-core-b/proposal.md | 48 +++++++++++++ .../specs/documents-api/spec.md | 67 +++++++++++++++++++ .../specs/osv-result-mapping/spec.md | 57 ++++++++++++++++ .../specs/recognition-job-exchange/spec.md | 32 +++++++++ .../specs/statement-checks/spec.md | 52 ++++++++++++++ .../unified-normalization-core-b/tasks.md | 9 +++ 7 files changed, 300 insertions(+) create mode 100644 openspec/changes/unified-normalization-core-b/design.md create mode 100644 openspec/changes/unified-normalization-core-b/proposal.md create mode 100644 openspec/changes/unified-normalization-core-b/specs/documents-api/spec.md create mode 100644 openspec/changes/unified-normalization-core-b/specs/osv-result-mapping/spec.md create mode 100644 openspec/changes/unified-normalization-core-b/specs/recognition-job-exchange/spec.md create mode 100644 openspec/changes/unified-normalization-core-b/specs/statement-checks/spec.md create mode 100644 openspec/changes/unified-normalization-core-b/tasks.md diff --git a/openspec/changes/unified-normalization-core-b/design.md b/openspec/changes/unified-normalization-core-b/design.md new file mode 100644 index 0000000..270ad02 --- /dev/null +++ b/openspec/changes/unified-normalization-core-b/design.md @@ -0,0 +1,35 @@ +## 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 существовало; иначе архив упадёт | при архивации — правка имён заголовков дельт | diff --git a/openspec/changes/unified-normalization-core-b/proposal.md b/openspec/changes/unified-normalization-core-b/proposal.md new file mode 100644 index 0000000..c2e4992 --- /dev/null +++ b/openspec/changes/unified-normalization-core-b/proposal.md @@ -0,0 +1,48 @@ +## Why + +Единая нормализация ББ/ОФР (RSHB/arm-ka-docs#116, change docs `unified-statement-normalization`, DBML v5.19): спека +`arm-core` разбита на две части поставки. Часть А (`unified-normalization-core-a`, RSHB/arm-core#126) уже в `main` и не +зависит от пакета `arm_statements`. Этот change — **часть Б** (RSHB/arm-core#131): переход ядра на пакет +`arm_statements` (копия `api/arm_statements/`, раскладывается `sync-statements`), мапперы результата `2.0`, удаление +временного кода и копии словаря §7 ядра. + +## What Changes + +Колонка «Б» таблицы покрытия спеки `arm-core` (план 3): + +- **B1** — адаптер пакета `StatementsEngine`, вход пересчёта из БД (`StoredForm`). +- **B2** — пересчёт после правки точкой входа пакета; §7, `checks` ответа и `CHECKS_NOT_PASSED` — только из + `document_issue`; относительные даты колонок; сценарий «после PATCH … `RELATION_FAILED`»: `documents-api`. +- **B3** — §7 и вариант формы при приёме `1.x` копией пакета (Р-30): `osv-result-mapping`. +- **B4** — маппер `2.0` для форм: хранение как пришло, `document_issue` (`service` и `core`), условие `RECOGNIZED` + (страховка — `engine.contract_document_status`): `osv-result-mapping`. +- **B5** — маппер `2.0` для ОСВ (Р-28), выбор маппера по MAJOR, приём файла результата: `osv-result-mapping`, + `recognition-job-exchange`. +- **B6** — удаление копии словаря §7 ядра (`api/rules/statement_checks.yaml`, `run_checks`, `load_check_rules`), + редакции — из перечня кодов пакета: `statement-checks`, `documents-api`. + +**Архивация (R-7 части А).** Change `unified-normalization-core-b` архивируется только вместе с +`unified-normalization-core-a` и только после выкладки обеих частей: дельты части А содержат поведение части Б, дельты +этой части уточняют его (порядок архивации — сначала А, затем Б). + +## Capabilities + +### New Capabilities + +### Modified Capabilities +- `osv-result-mapping`: маппер `2.0` для форм и ОСВ, §7 при приёме `1.x` копией пакета. +- `documents-api`: пересчёт, `CHECKS_NOT_PASSED` и `checks` из `document_issue`, словари без копии §7. +- `statement-checks`: §7 только копией пакета; копии словаря в ядре нет. +- `recognition-job-exchange`: выбор маппера результата по MAJOR. + +## Impact + +`api/src/domain/{stored_form,statement_mapping_v2,statement_checks,statement_lines}.py`, +`api/src/services/{statements_engine,statement_recompute,statements,documents,result_mapping}.py`, +`api/src/repositories/statements.py`, `api/src/schemas/statements.py`, `api/rules/statement_checks.yaml` (удаляется), +`CLAUDE.md`, `README.md`. `import arm_statements` — только в `statements_engine.py` и `stored_form.py`. + +## Non-goals + +Схема v5.19 и миграция (часть А), фронт (arm-web), копия `api/arm_statements/` (раскладывается `sync-statements`), +docs-worktree. diff --git a/openspec/changes/unified-normalization-core-b/specs/documents-api/spec.md b/openspec/changes/unified-normalization-core-b/specs/documents-api/spec.md new file mode 100644 index 0000000..f50334f --- /dev/null +++ b/openspec/changes/unified-normalization-core-b/specs/documents-api/spec.md @@ -0,0 +1,67 @@ +## MODIFIED Requirements + +### Requirement: Пересчёт после правки +После каждого PATCH формы ядро SHALL пересчитывать `checks`, `needs_review`, `form_variant`, родителей строк `detail` +и статус **точкой входа пакета `arm_statements` для пересчёта** по сохранённым данным (конвенция знака — по +`source_text`); временного пересчёта ядра MUST NOT быть. Правила: `is_modified` или `is_confirmed` значения ⇒ +`needs_review = false`; `line_code_modified` или `is_confirmed` строки ⇒ `needs_review` строки = `false`; значения строк +`detail` — `needs_review = false`; акцепт ⇒ жёсткие нарушения не учитываются; правленые ячейки в голосование знака не +входят. Результат пишется: `needs_review` строк и значений, `form_variant`, `parent_row_index` строк `detail`, записи +`document_issue` видов `relation` / `soft` (заменяются целиком), статус. Относительные даты колонок +(`period_kind`, `statement-normalization`) пересчитываются от даты отчёта после правки `header.report_date` и +`columns[].period_date`. Отличие `statements_version` задания от версии копии ядра — `document_issue` +`STATEMENTS_VERSION_MISMATCH` (`info`); у задания без версии (`1.x`) запись не пишется. Записи `core` +`CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION` (только четырёхзначные коды), `NAME_MISMATCH` и +`STATEMENTS_VERSION_MISMATCH` пересчитываются заменой; `NAME_MISMATCH` не ставится строке с назначенным аналитиком кодом; +`MAPPING_WARNING`, `RESULT_STATUS_INCONSISTENT`, `LEGACY_WARNING` сохраняются. Один PATCH — один пересчёт и одна запись +аудита `RECOGNIZED_DATA_EDIT`. + +#### Scenario: Правка исправляет нарушение +- **WHEN** форма принята с `RELATION_FAILED` по `1200 = 1210 + … + 1260`, аналитик правит сумму 1230 так, что + соотношение сходится +- **THEN** запись `relation` по 1200 заменена (её больше нет), статус пересчитан точкой входа пакета + +#### Scenario: После PATCH остаётся RELATION_FAILED +- **WHEN** аналитик правит сумму, но соотношение всё ещё не сходится +- **THEN** запись `RELATION_FAILED` пересоздана с новыми `expected` / `actual`; `POST …/confirm` без акцепта — 409 + `CHECKS_NOT_PASSED` + +### Requirement: Словари ядра и достройка шаблонных строк +Копия словаря §7 ядра (`api/rules/statement_checks.yaml`) SHALL быть удалена: проверки ядро считает копией пакета +`arm_statements`. `api/rules/statement_lines.yaml` SHALL оставаться только словарём имён (код → наименования по +редакциям; у кода может быть несколько имён, Р-20); порядок и состав редакций — из копии перечня кодов пакета +(`code_orders.editions`); код словаря имён, которого нет в перечне своей редакции, — ошибка проверки. В редакции +`fsbu4` словарь дополнен вторыми именами законных повторов 1220, 1520, 2410 и кодами печатной формы 1С 1105, 1130, 1140, +1530, 2460. Шаблонные строки достраиваются по словарю имён (одна на код, первое имя); код, у которого есть хотя бы одна +строка `ok` из файла, шаблоном MUST NOT достраиваться; строки `unreadable` / `missing` не заменяются шаблонной молча. + +#### Scenario: Код словаря вне перечня +- **WHEN** в `statement_lines.yaml` у редакции есть код, которого нет в перечне этой редакции копии пакета +- **THEN** проверка согласованности словаря с перечнем падает + +#### Scenario: Код есть только у строки unreadable +- **WHEN** у формы нет строки `ok` с кодом 1250, но есть строка `unreadable` «ИЧ,250» +- **THEN** шаблонная строка 1250 достраивается пустой, строка `unreadable` показывается отдельно со статусом + +## ADDED Requirements + +### Requirement: `CHECKS_NOT_PASSED` из `document_issue` +`POST /documents/{id}/confirm` для формы ББ/ОФР SHALL проверять 409 в порядке (требование «Подтверждение документа»): `FILE_SUPERSEDED` → `ALREADY_CONFIRMED` → +`DOCUMENT_NOT_CONFIRMABLE` → `PERIOD_REQUIRED` → `LINES_UNRESOLVED` (есть строки `unreadable` / `missing`, у которых хотя +бы в одной ячейке `amount` или `string_value` не `null`; `detail.lines[] {line_id, row_index, line_code_text}`) → +`DUPLICATE_FORM` (в файле уже подтверждена другая форма того же типа, `period_year` и `period_quarter`; +`detail.document_id`) → `CHECKS_NOT_PASSED`. Нарушения для `CHECKS_NOT_PASSED` SHALL читаться **только из записей +`document_issue` вида `relation`** без акцепта; пересчёта §7 в запросе подтверждения нет. + +#### Scenario: Жёсткое нарушение из document_issue +- **WHEN** у формы есть запись `relation` без акцепта и нет строк `unreadable` / `missing` со значениями +- **THEN** 409 `CHECKS_NOT_PASSED` + +### Requirement: Блок `checks` ответа формы из `document_issue` +Блок `checks` ответа `GET …/statement` SHALL строиться из записей `document_issue` видов `relation` (`failed`) и `soft` +(`soft_failed`) с `codes`, `expected`, `actual`, `line_ids`; собственного расчёта §7 при чтении нет. Межформенная +проверка `cross_form` считается по агрегату формы по коду. + +#### Scenario: checks из document_issue +- **WHEN** у формы две записи `relation` и одна `soft` +- **THEN** `checks.failed` содержит две записи, `checks.soft_failed` — одну; расчёт §7 при запросе не выполняется diff --git a/openspec/changes/unified-normalization-core-b/specs/osv-result-mapping/spec.md b/openspec/changes/unified-normalization-core-b/specs/osv-result-mapping/spec.md new file mode 100644 index 0000000..611a7f4 --- /dev/null +++ b/openspec/changes/unified-normalization-core-b/specs/osv-result-mapping/spec.md @@ -0,0 +1,57 @@ +## ADDED Requirements + +### Requirement: Маппер результата 2.0 для форм ББ/ОФР +Для результата `2.0` ядро SHALL создавать документ формы, храня строки, значения, колонки и шапку **как пришли** +(DBML v5.19): строка — `row_index`, `source_ref = ref`, `line_status`, `line_code`, `line_code_original_value`, +`line_code_text`, `line_code_confidence`, `line_name`, `parent_row_index` (по `parent_ref`), `needs_review`, +`is_confirmed = false`; значение — на каждую пару «строка × колонка» (нет в результате — пустая запись с +`needs_review = false`); колонка — с `period_kind`, `label_confidence`; шапка — разобранные поля, `header_source` +(`{поле: {text, confidence}}`), `*_original_value`, `needs_review_fields`, `form_variant`. Значение с `column_index` вне +колонок формы и повтор `column_index` колонки — первое остаётся, остальное — `document_issue` `MAPPING_WARNING`. +Предупреждения и `checks` результата SHALL писаться в `document_issue` (источник `service`); проверки ядра — источник +`core`: `CODE_NOT_IN_DICTIONARY` и `CODE_NOT_IN_EDITION` (редакция — `edition` точки входа пакета, а не по дате; одна +запись на код документа, `line_ids` — все строки `ok` кода; **только для кодов из ровно четырёх цифр** — пятизначные +подстроки Excel не проверяются), `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`, `STATEMENTS_VERSION_MISMATCH`. Статус +документа — по требованию «Условие `RECOGNIZED` при приёме 2.0»; согласованность `done` — вызовом +`engine.contract_document_status` пакета. Проверки §7, `needs_review` и предупреждения сервиса MUST NOT пересчитываться +при загрузке. + +#### Scenario: Форма 2.0 хранится как пришла +- **WHEN** результат `2.0` содержит две строки с кодом 1230 и строку без кода (`ref` с `parent_ref`) +- **THEN** создано три строки с их `row_index`, `source_ref` и `parent_row_index`; повтор кода не сливается + +#### Scenario: Пятизначный код не даёт info +- **WHEN** в ББ строка с кодом `12301` (подстрока Excel), которого нет в словаре +- **THEN** `CODE_NOT_IN_DICTIONARY` и `CODE_NOT_IN_EDITION` для него не пишутся + +#### Scenario: Четырёхзначный код вне перечня +- **WHEN** в ББ строка 1120 на бланке `fsbu4`, где такого кода нет +- **THEN** запись `CODE_NOT_IN_EDITION` (`info`, `core`) с `line_ids` строк `ok` этого кода + +### Requirement: Ведомость ОСВ по контракту 2.0 +Для листа ОСВ результата `2.0` ядро SHALL создавать документ ОСВ по `osvDocument`, предупреждения листа +(`osvDocument.warnings`, `OSV_*`) писать в `document_issue` (вид `warning`, источник `service`), а статус документа +определять по требованию «Предупреждения файла и ОСВ в API, статус ОСВ при приёме 2.0». Статус ОСВ MUST NOT зависеть от +статуса соседнего документа книги и от статуса конверта. + +#### Scenario: ОСВ в книге с ОФР, требующим проверки +- **WHEN** книга из ОСВ (`status = done`, контрольные суммы сошлись) и ОФР (`needs_check`) +- **THEN** документ ОСВ `RECOGNIZED`, документ ОФР `NEEDS_CHECK` + +## MODIFIED Requirements + +### Requirement: Формы результата 1.x — §7 при приёме +Для формы, принятой по результату `1.x` (окно перехода после выкладки ядра; `checks` контракта 2.0 у неё нет), ядро +SHALL **один раз при приёме** посчитать §7 точкой входа пакета для пересчёта (`statement-normalization`; вариант формы — +по структуре кодов, как у 2.0) по суммам результата и записать нарушения в `document_issue` (источник `core`, виды +`relation` / `soft`) и `form_variant` в `financial_statement_meta`; собственного пересчёта §7 ядро MUST NOT выполнять. +Условие D-6 «проверки сошлись» для `1.x` SHALL браться из этого расчёта; `needs_review` значения при приёме `1.x` = +**НЕ** `values_ok` (`values_ok` — `is_recognized` не `false` и `confidence` равна 100 или пуста), `needs_review` строки — +`false`. Предупреждения маппера `1.x` по документу — `document_issue` `MAPPING_WARNING` (`info`, источник `core`); +строки `metrics.warnings` результата `1.x` — в `recognition_job.metrics.warnings` как `LEGACY_WARNING`. Формы, +существовавшие в БД до миграции v5.19, §7 не пересчитываются. + +#### Scenario: Форма 1.x с нарушением +- **WHEN** результат `1.2` содержит ББ с несошедшимся `1600 = 1700` +- **THEN** у формы запись `document_issue` `relation` `RELATION_FAILED` с `codes = ["1600", "1700"]` (источник `core`), + документ `NEEDS_CHECK`; `POST …/confirm` без акцепта — 409 `CHECKS_NOT_PASSED` diff --git a/openspec/changes/unified-normalization-core-b/specs/recognition-job-exchange/spec.md b/openspec/changes/unified-normalization-core-b/specs/recognition-job-exchange/spec.md new file mode 100644 index 0000000..f38f03e --- /dev/null +++ b/openspec/changes/unified-normalization-core-b/specs/recognition-job-exchange/spec.md @@ -0,0 +1,32 @@ +## MODIFIED Requirements + +### Requirement: Приём конвертов и результатов обоих MAJOR контракта +Inbox SHALL принимать конверты (включая ack `processing`) и файлы результата и `MAJOR` 1, и `MAJOR` 2 («сначала ядро», +`recognition-contract`): версия определяется по `schema_version`, конверт валидируется моделью своего `MAJOR`, неизвестный +`MAJOR` (например, `3.0`) отбрасывается с предупреждением в лог (задание закроется по сроку). Файл результата +обрабатывается маппером своего `MAJOR` (`2.0` — маппер результата `2.0`, `1.x` — маппер `1.x`); файл с неизвестным +`MAJOR` получает `ERROR` с `mapping_error`. Ядро SHALL держать копии схем и примеров обоих `MAJOR` (`api/contracts/`), +сверяемые с `docs/contracts/` общим правилом. Предупреждения уровня файла SHALL записываться в +`recognition_job.metrics.warnings` структурами `{code, severity, message, locator?}`: у `2.0` — как пришли, у `1.x` — +каждая строка как `{code: "LEGACY_WARNING", severity: "info", message: <строка>, locator: null}`. +`recognition_job.statements_version` SHALL сохранять `service.statements_version` результата `2.0` (у `1.x` — `NULL`). + +#### Scenario: Ack нового сервиса +- **WHEN** сервис публикует ack `processing` со `schema_version = "2.0"` +- **THEN** задание переходит в `processing`, как для ack `1.0` + +#### Scenario: Предупреждения 1.x оборачиваются +- **WHEN** конверт `1.0` несёт `metrics.warnings = ["sheet=ОСВ 60 строки без родителя"]` +- **THEN** в `recognition_job.metrics.warnings` — `{code: "LEGACY_WARNING", severity: "info", message: "sheet=ОСВ 60 строки без родителя", locator: null}` + +#### Scenario: Предупреждения 2.0 сохраняются как пришли +- **WHEN** конверт `2.0` несёт предупреждение `JSON_FAILED` уровня `error` с `locator` +- **THEN** структура сохранена без изменений + +#### Scenario: Неизвестный MAJOR +- **WHEN** приходит конверт со `schema_version = "3.0"` +- **THEN** конверт отбрасывается с предупреждением в лог, задание не меняется + +#### Scenario: Результат 2.0 разбирает маппер 2.0 +- **WHEN** inbox принял файл результата со `schema_version = "2.0"` +- **THEN** документы создаёт маппер результата `2.0`; результат `1.x` по-прежнему разбирает маппер `1.x` diff --git a/openspec/changes/unified-normalization-core-b/specs/statement-checks/spec.md b/openspec/changes/unified-normalization-core-b/specs/statement-checks/spec.md new file mode 100644 index 0000000..e3e47bd --- /dev/null +++ b/openspec/changes/unified-normalization-core-b/specs/statement-checks/spec.md @@ -0,0 +1,52 @@ +## MODIFIED Requirements + +### Requirement: Соотношения §7 по колонкам +Проверки SHALL считаться чистой функцией по текущим `financial_statement_value.amount` документа для каждой колонки +отдельно по словарю §7 копии пакета `arm_statements` (`statement-normalization`; своей копии словаря ядро не держит): соотношение проверяется, только +если присутствует его левая часть; отсутствующий код и пустая сумма — 0; выполнено, если `|левая − правая| ≤ tolerance` +(1, в единицах формы) хотя бы в одном варианте правой части — как записано или с кодами `abs_terms` по модулю. Нарушение +жёсткого (`strict`) соотношения, не превышающее `rounding_tolerance_percent` (0,001 %) от наибольшего по модулю числа +соотношения, SHALL считаться мягким («в пределах округления»). Мягкие: соотношения `strict: false` и «сумма пятизначных +подстрок = четырёхзначный родитель». Результат — `{failed: [...], soft_failed: [...]}`, элемент — `{column_index, +relation, expected, actual}`; не хранится как есть: нарушения при приёме и пересчёте записываются в `document_issue`. + +Присутствующими SHALL считаться суммы **агрегата формы по коду** (требование «Агрегат формы по коду»): строки `ok` формы из БД, кроме строк, созданных правкой аналитика (`row_index IS NULL`), у которых все суммы пустые; строки `detail`, `unreadable`, `missing` и строки, достроенные в ответе по справочнику (`documents-api`, `id = null`), в проверках MUST NOT участвовать. Созданная правкой строка с хотя бы одной непустой суммой проверяется по всем колонкам, пустые суммы — 0. Повтор кода в форме суммируется. Результат нарушения несёт `codes` (коды цели и членов соотношения) и `line_ids` (все строки `ok` этих кодов). Соотношения считает точка входа пакета для пересчёта по агрегату формы; запись в БД — только в виде `document_issue` при приёме и пересчёте, `api/rules/statement_checks.yaml` и код его загрузки в ядре MUST NOT существовать. + +#### Scenario: Сходящийся баланс +- **WHEN** проверяется форма `mezheninovskaya/2025-Q3/1_Форма1_бух_09` +- **THEN** `failed` пуст + +#### Scenario: Нарушение жёсткого соотношения +- **WHEN** у баланса 1600 = 1000, 1100 = 300, 1200 = 600 в колонке 0 +- **THEN** `failed` содержит `{column_index: 0, relation: "1600 = 1100 + 1200", expected: 1000, actual: 900}` + +#### Scenario: Расходы со знаком минус +- **WHEN** ОФР 2110 = 100, 2120 = −60, 2100 = 40 +- **THEN** `2100 = 2110 − 2120` выполнено через вариант с `abs_terms` + +#### Scenario: Правка исправляет нарушение +- **WHEN** аналитик правит 1200 с 600 на 700 в форме из предыдущего сценария +- **THEN** проверка по текущим суммам: `failed` пуст + +#### Scenario: Пустая шаблонная строка не проверяется +- **WHEN** в форме нет строки 1700 (в ответе она достроена пустой), 1300 + 1400 + 1500 = 2 371 333 в колонке 0 +- **THEN** соотношения с целью 1700 (`1700 = 1300 + 1400 + 1500`) не проверяются; `1600 = 1700` (цель 1600 есть в + форме, 1700 = 0) нарушено — как и без достройки: потерянная строка «БАЛАНС» сама по себе даёт `NEEDS_CHECK` + +#### Scenario: Заполненная шаблонная строка проверяется +- **WHEN** аналитик заполнил 1700 = 2 371 333 только в колонке 0, а в колонке 1 1300 + 1400 + 1500 = 2 000 000 +- **THEN** в колонке 0 соотношение выполнено; в колонке 1 `failed` содержит `1700 = 1300 + 1400 + 1500` с `expected 0`, + `actual 2000000` + +#### Scenario: Очищенная шаблонная строка +- **WHEN** затем аналитик очищает 1700 в колонке 0 (`amount = null`) +- **THEN** строка 1700 снова не участвует в проверках + +#### Scenario: Повтор кода суммируется +- **WHEN** в ББ две строки 1230 со значениями 100 и 50 в колонке 0 +- **THEN** соотношения берут 1230 = 150 + +#### Scenario: Копии словаря §7 в ядре нет +- **WHEN** проверяется репозиторий ядра после перехода на пакет +- **THEN** файла `api/rules/statement_checks.yaml` нет, `load_check_rules` / `run_checks` в `api/src` не встречаются; + допуск `tolerance` берётся из правил пакета diff --git a/openspec/changes/unified-normalization-core-b/tasks.md b/openspec/changes/unified-normalization-core-b/tasks.md new file mode 100644 index 0000000..7f1b2f0 --- /dev/null +++ b/openspec/changes/unified-normalization-core-b/tasks.md @@ -0,0 +1,9 @@ +## Задачи (SDD-план `docs/openspec/changes/unified-statement-normalization/plans/2026-10-18-plan-3-arm-core.md`, часть Б) + +- [ ] B0. Change OpenSpec части Б в arm-core +- [ ] B1. Адаптер пакета `StatementsEngine` и вход пересчёта из БД +- [ ] B2. Пересчёт после правки общим кодом и §7 из `document_issue` +- [ ] B3. Маппер `1.x` — §7 при приёме и вариант формы (Р-30) +- [ ] B4. Маппер `2.0` — формы +- [ ] B5. Маппер `2.0` — ОСВ, выбор по MAJOR, приём файла результата +- [ ] B6. Удаление копии словаря §7 ядра, редакции из перечня пакета