openspec(core-b): change единой нормализации, часть Б (arm-ka-docs#116)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
38c440ccd6
commit
42f0dc5ca7
35
openspec/changes/unified-normalization-core-b/design.md
Normal file
35
openspec/changes/unified-normalization-core-b/design.md
Normal file
@ -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 существовало; иначе архив упадёт | при архивации — правка имён заголовков дельт |
|
||||||
48
openspec/changes/unified-normalization-core-b/proposal.md
Normal file
48
openspec/changes/unified-normalization-core-b/proposal.md
Normal file
@ -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.
|
||||||
@ -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 при запросе не выполняется
|
||||||
@ -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`
|
||||||
@ -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`
|
||||||
@ -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` берётся из правил пакета
|
||||||
9
openspec/changes/unified-normalization-core-b/tasks.md
Normal file
9
openspec/changes/unified-normalization-core-b/tasks.md
Normal file
@ -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 ядра, редакции из перечня пакета
|
||||||
Loading…
x
Reference in New Issue
Block a user