Andrey Epifancev 4e42a93f54 chore(openspec): архив unified-normalization-core-a и -core-b (#126, #131)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:26:03 +04:00

108 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# statement-checks Specification
## Purpose
Логические проверки форм ББ/ОФР §7 в ядре: чистые функции над текущими суммами документа, без хранения результата;
акцепт непройденных проверок аналитиком.
## Requirements
### Requirement: Соотношения §7 по колонкам
Проверки SHALL считаться чистой функцией по текущим `financial_statement_value.amount` документа для каждой колонки
отдельно по словарю §7 копии пакета `arm_statements` (`statement-normalization`; своей копии словаря ядро не держит):
соотношение **посчитано**, если непуста цель (левая часть) или хотя бы один член (есть строка `ok` с кодом и непустой
суммой); если пусто всё — соотношение пропускается. В посчитанном соотношении отсутствующий код и пустая сумма в
арифметике — 0, но пустая цель при непустых членах и непустая цель при пустых членах — нарушение (пустая цель даёт
`expected = null`, P1-21 пакета). Выполнено, если `|левая − правая| ≤ tolerance`
(1, в единицах формы) хотя бы в одном варианте правой части — как записано или с кодами `abs_terms` по модулю. Нарушение
жёсткого (`strict`) соотношения, не превышающее `rounding_tolerance_percent` (0,001 %) от наибольшего по модулю числа
соотношения, SHALL считаться мягким («в пределах округления»). Мягкие: соотношения `strict: false` и «сумма пятизначных
подстрок = четырёхзначный родитель». Результат — `{failed: [...], soft_failed: [...]}`, элемент — `{column_index,
relation, expected, actual}`, где `expected` и `actual` могут быть `null` (`expected = null` — пустая цель); не хранится как есть: нарушения при приёме и пересчёте записываются в `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` посчитано и нарушено: `expected = null`, `actual = 2371333`;
`1600 = 1700` (цель 1600 есть в форме, 1700 пуста) тоже нарушено; потерявшая итоговую строку форма блокирует
подтверждение (`CHECKS_NOT_PASSED`) до правки строки или акцепта (P3-30)
#### Scenario: Все стороны пусты
- **WHEN** в колонке нет ни цели, ни членов соотношения
- **THEN** соотношение пропускается, нарушения нет
#### Scenario: Заполненная шаблонная строка проверяется
- **WHEN** аналитик заполнил 1700 = 2 371 333 только в колонке 0, а в колонке 1 1300 + 1400 + 1500 = 2 000 000
- **THEN** в колонке 0 соотношение выполнено; в колонке 1 `failed` содержит `1700 = 1300 + 1400 + 1500` с
`expected = null` (1700 в колонке 1 пуста), `actual = 2000000`
#### Scenario: Очищенная шаблонная строка
- **WHEN** затем аналитик очищает 1700 в колонке 0 (`amount = null`)
- **THEN** строка 1700 снова не участвует в проверках как значение: пустая цель при непустых членах — нарушение
`expected = null` в каждой колонке (прежнее «очищенная строка не цель» не действует, P3-30)
#### 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` берётся из правил пакета
### Requirement: Межформенная проверка
Для документа `BALANCE` или `INCOME` SHALL считаться мягкая проверка `1370(колонка отчётной даты ББ) − 1370(колонка
31 декабря прошлого года ББ) = 2400(колонка отчётного периода ОФР)` с допуском `tolerance`, если найдена форма-пара:
документ другого типа формы того же ЮЛ, `period_year` и `period_quarter`, файла не `SUPERSEDED` (подтверждённый, затем
самый поздний по `created_at`), и все три колонки. Значение `1370` / `2400` берётся по агрегату формы по коду (сумма строк `ok` с этим кодом). Нарушение SHALL попадать в `cross_form` и MUST NOT блокировать
подтверждение.
#### Scenario: Пара найдена
- **WHEN** у ЮЛ ББ за 2025 Q3 (1370 = 500 на 30.09.2025, 300 на 31.12.2024) и ОФР за 2025 Q3 (2400 = 150)
- **THEN** `cross_form` — нарушение `expected 200, actual 150`; подтверждение ББ разрешено
#### Scenario: Пары нет
- **WHEN** у ЮЛ есть только ББ за период
- **THEN** `cross_form` — `null`
#### Scenario: Межформенная проверка по агрегату
- **WHEN** в ББ две строки 1370 (сумма 500 и 300 на отчётную дату)
- **THEN** межформенная проверка берёт 1370 = 800
### Requirement: Акцепт непройденных проверок
Акцепт SHALL записывать `financial_statement_meta.checks_accepted_by`, `checks_accepted_at`,
`checks_accepted_comment`; документ с акцептом SHALL считаться прошедшим жёсткие проверки для статуса и подтверждения.
Любая правка значений или шапки формы SHALL сбрасывать акцепт (три поля — `NULL`).
#### Scenario: Акцепт и правка
- **WHEN** аналитик акцептовал нарушение 1600 и затем поправил любое значение
- **THEN** поля акцепта пусты, при непройденной проверке документ `NEEDS_CHECK`
### Requirement: Агрегат формы по коду
Все чтения формы «по коду» SHALL идти через одну чистую функцию «агрегат формы по коду» без хранения (DECISION-5):
для кода и колонки — сумма `amount` строк `ok` с этим кодом (`null`, если ни одной непустой), список `line_id`,
справочное имя. На неё переводятся межформенная проверка `1370 / 2400`, суммы формы для консолидации и выгрузок,
`parent_code` подстрок, раскладка строк на экране, правка ячеек по `line_code`, метрики `fields_*`. Строки `detail`,
`unreadable`, `missing` в агрегат, КФО и снимок ЕХ MUST NOT попадать.
#### Scenario: Межформенная проверка по агрегату
- **WHEN** в ББ две строки 1370 (сумма 500 и 300 на отчётную дату)
- **THEN** межформенная проверка берёт 1370 = 800