Спека части Б: дельты под архивацию, null в нарушениях §7, отметки задач
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
570e82b5d3
commit
e944565e55
@ -61,3 +61,5 @@ 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-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 Б нужно обновить синхронно |
|
||||
|
||||
@ -37,8 +37,9 @@
|
||||
|
||||
## 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/domain/{stored_form,statement_mapping_v2,statement_checks,statement_issues,statement_mapping,statement_aggregate,statement_layout,osv_mapping}.py`,
|
||||
`api/src/services/{statements_engine,statement_recompute,statement_intake_v2,statement_line_edits,statements,result_mapping}.py`,
|
||||
`api/src/api/dependencies/files.py`, `api/src/contract/result.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`.
|
||||
|
||||
|
||||
@ -1,20 +1,105 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Форма ББ/ОФР для экрана проверки
|
||||
`GET /api/v1/documents/{document_id}/statement` (`DOCUMENT_VIEW`, policy по ЮЛ) SHALL отдавать для документа
|
||||
`BALANCE`/`INCOME`: `document` (`id`, `document_type`, `status`, `period_year`, `period_quarter`, `reporting_date`,
|
||||
`counterparty_check`, `locator`, `file_id`); `edition` (`66n` при дате периода ≤ 2024-12-31, `fsbu4` — позже, `null` без
|
||||
даты); `header` — для `inn`, `report_date`, `units`: `value`, `original_value`, `confidence` (у ИНН), `verified`,
|
||||
`modified`; плюс `organization_name`, `okud`, `okpo`, `okopf`, `okfs`, `address`; `columns[]` (`id`, `column_index`,
|
||||
`label`, `period_date`, `is_current`); `lines[]` в порядке, описанном ниже (`id`, `origin`, `line_code`, `line_code_confidence`,
|
||||
`line_name`, `line_name_confidence`, `values[]` по `column_index`: `id`, `column_id`, `amount`, `string_value`,
|
||||
`confidence`, `is_recognized`, `is_modified`, `original_amount`, `modified_by`, `modified_at`); `checks` (`failed`,
|
||||
`soft_failed`, `cross_form`, `accepted`: `by`, `at`, `comment` или `null`). Элемент нарушения в `checks` — `{column_index,
|
||||
relation, expected, actual, codes, line_ids}`; `expected` и `actual` SHALL быть `null`, когда сторона соотношения пуста
|
||||
(пустая цель при непустых членах — `expected = null`, пакет `arm_statements`, P1-21), см. требование «Блок `checks`
|
||||
ответа формы из `document_issue`». Документ другого типа — 409 `DOCUMENT_NOT_STATEMENT`; чужой или несуществующий — 404.
|
||||
|
||||
**Состав ответа v2.0.** `StatementOut` SHALL отдавать: у документа `form_variant`; у шапки `needs_review_fields`, `source` (`header_source`;
|
||||
`confidence` — строкой Decimal, как прочие уверенности API), `organization_name_modified` и `okei`; у колонок `period_kind`, `label_confidence`; у строк `id`, `origin` (`template` ⇔
|
||||
`row_index = null`), `row_index`, `source_ref`, `line_status`, `line_code`, `line_code_text`, `line_code_confidence`,
|
||||
`line_name` (из файла), `reference_name` (справочное имя по коду — первое имя кода в словаре — или `null`),
|
||||
`parent_row_index`, `parent_code`, `needs_review`, `is_confirmed`, `line_code_modified`; у значений `text`
|
||||
(`source_text`), `needs_review`, `is_confirmed` и прежние поля; `issues[]` — записи `document_issue` с `line_ids`,
|
||||
`column_id`, `column_index`, `field`, `codes`; `checks` — нарушения из `document_issue` с `codes` и `line_ids`;
|
||||
`edition_lines[] {code, name}` — справочник строк для подсказки кода: коды словаря имён формы в редакции ответа по одному
|
||||
(имя — первое), у формы без редакции — коды всех редакций по одному. `DocumentBrief` (`FileOut.documents[]`) и
|
||||
`DocumentOut` SHALL отдавать `issue_counts {error, info}` — число записей `document_issue` документа по уровню. Порядок
|
||||
`lines` — по возрастанию кода, как сейчас (`schemas/statements.py`), одинаковые коды — по `row_index`; строки
|
||||
`unreadable` / `missing` — после ближайшей предыдущей по `row_index` строки, размещённой по коду, без такой — в начале;
|
||||
`detail` — после родителя. Разделы и размещение итогов на экране («как на бумаге», Р-14) строит экран.
|
||||
|
||||
Форма SHALL достраиваться в ответе по справочнику строк редакции `edition` (`api/rules/statement_lines.yaml`, свой
|
||||
список кодов и наименований для `BALANCE` и `INCOME` каждой редакции): строки справочника, кода которых нет в форме,
|
||||
добавляются с `id = null`, `origin = "template"`, наименованием из справочника и `*_confidence = null`; у каждой строки
|
||||
SHALL быть значение по каждой колонке формы — недостающее добавляется с `id = null`, `amount = null`,
|
||||
`string_value = null`, `confidence = null`, `is_recognized = null`, `is_modified = false`, `original_amount = null`,
|
||||
`modified_by = null`, `modified_at = null`. При `edition = null` строки не достраиваются, недостающие значения —
|
||||
достраиваются. Строка из БД имеет `origin = "template"` ⇔ `row_index IS NULL` (создана правкой аналитика), иначе `"source"`. Строки источника, которых нет в справочнике, отдаются как есть.
|
||||
Достроенные строки и значения MUST NOT записываться в БД при чтении и MUST NOT участвовать в проверках §7.
|
||||
|
||||
#### Scenario: Форма для экрана
|
||||
- **WHEN** запрошена форма `BALANCE` из `mezheninovskaya/2025-Q3/1_Форма1_бух_09`
|
||||
- **THEN** `edition = fsbu4`, все 37 строк файла — `origin = source` с `id`, каждый код справочника `fsbu4` / `BALANCE`
|
||||
есть среди `lines`, у каждой строки значения по всем колонкам, `checks.failed` пуст
|
||||
|
||||
#### Scenario: Строка, потерянная источником
|
||||
- **WHEN** у формы `BALANCE` за 2025 Q1 в БД нет строки 1700 (OCR склеил «1500\n1700» до OQ-156)
|
||||
- **THEN** в `lines` есть 1700: `id = null`, `origin = template`, `line_name = "БАЛАНС"`, значения по всем колонкам
|
||||
`id = null`, `amount = null`, `is_recognized = null`; достройка при чтении не меняет статус документа и проверки:
|
||||
`checks.failed` содержит соотношения с пустой целью 1700 (`1700 = 1300 + 1400 + 1500`, `expected = null`, `actual` —
|
||||
сумма членов) и `1600 = 1700` (цель 1600 заполнена, 1700 пуста) — такие же, как без достройки; подтверждение формы
|
||||
требует правки строки 1700 или акцепта проверок
|
||||
|
||||
#### Scenario: Недостающая ячейка
|
||||
- **WHEN** у строки 1150 в БД есть значение только для колонки 0, а у формы две колонки
|
||||
- **THEN** у 1150 два значения; значение колонки 1 — `id = null`, `amount = null`
|
||||
|
||||
#### Scenario: Нет даты периода
|
||||
- **WHEN** у формы OCR не определена дата периода (`edition = null`)
|
||||
- **THEN** `lines` — только строки из БД, у каждой строки значения по всем колонкам
|
||||
|
||||
#### Scenario: Не форма
|
||||
- **WHEN** запрошена форма документа `OSV`
|
||||
- **THEN** 409 `DOCUMENT_NOT_STATEMENT`
|
||||
|
||||
#### Scenario: Документ, принятый по 1.x
|
||||
- **WHEN** форма загружена до перехода сервиса (результат 1.2)
|
||||
- **THEN** у строк `line_status = ok`, `row_index` по порядку, `source_ref = null`, `origin = source`, у значений `text =
|
||||
string_value`, `issues` содержат нарушения §7, посчитанные ядром, и `MAPPING_WARNING`; экран работает
|
||||
|
||||
### 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` и
|
||||
и статус **точкой входа пакета `arm_statements` для пересчёта** по сохранённым данным (`statement-normalization`;
|
||||
конвенция знака — по `source_text`); временного пересчёта ядра MUST NOT быть. Правила: `is_modified` или `is_confirmed`
|
||||
значения ⇒ `needs_review = false`; `line_code_modified` или `is_confirmed` строки ⇒ `needs_review` строки = `false`;
|
||||
значения строк `detail` — `needs_review = false` (Р-23), у строки, выведенной из `detail` назначением кода,
|
||||
`needs_review` её значений считается по общему правилу; акцепт ⇒ жёсткие нарушения не учитываются; правленые ячейки в
|
||||
голосование знака не входят. Результат пишется: `needs_review` строк и значений, `form_variant`, `parent_row_index` всех
|
||||
строк `detail` (ближайшая строка `ok` выше по `row_index`; нет такой — `null`, строка остаётся `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`.
|
||||
аудита `RECOGNIZED_DATA_EDIT`. Акцепт SHALL сбрасываться правкой данных — суммы, кода или статуса строки, даты
|
||||
колонки, поля шапки; подтверждение или снятие подтверждения (`confirmed`) и подтверждение даты отчёта или даты колонки
|
||||
тем же значением акцепт MUST NOT сбрасывать (Р-25, S-47).
|
||||
|
||||
#### Scenario: Подтверждение ячейки без правки
|
||||
- **WHEN** аналитик подтверждает ячейку с `needs_review = true` без изменения суммы
|
||||
- **THEN** `is_confirmed = true`, `confirmed_by/at`, `is_modified` не ставится, `needs_review = false`, аудит; акцепт
|
||||
проверок, если был, сохраняется
|
||||
|
||||
#### Scenario: Снятие подтверждения
|
||||
- **WHEN** аналитик снимает подтверждение с ячейки (`confirmed: false`), которая не покрыта соотношением
|
||||
- **THEN** `is_confirmed = false`, `needs_review = true` после пересчёта, аудит
|
||||
|
||||
#### Scenario: Родитель уходит в расшифровки
|
||||
- **WHEN** у строки 1150 две расшифровки (`detail`, `parent_row_index` = 1150), аналитик отмечает саму 1150 расшифровкой
|
||||
- **THEN** после пересчёта родитель всех трёх строк `detail` — ближайшая строка `ok` выше 1150; строк над ними нет — `null`
|
||||
(на экране — в начале раздела)
|
||||
|
||||
#### Scenario: Правка исправляет нарушение
|
||||
- **WHEN** форма принята с `RELATION_FAILED` по `1200 = 1210 + … + 1260`, аналитик правит сумму 1230 так, что
|
||||
@ -29,11 +114,17 @@
|
||||
### 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` не заменяются шаблонной молча.
|
||||
редакциям): у кода может быть несколько имён (синонимы и законные повторы кода в бланке, Р-20), загрузчик повтор кода в
|
||||
редакции MUST NOT запрещать; сверка наименования — с любым именем кода; шаблонная строка берёт первое имя. Порядок и
|
||||
состав редакций — из копии перечня кодов пакета (`code_orders.editions`); код словаря имён, которого нет в перечне своей
|
||||
редакции, — ошибка проверки (тест согласованности, `sync-statements --check`). В редакции `fsbu4` словарь имён SHALL быть
|
||||
дополнен вторыми именами законных повторов 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410
|
||||
«Налог при упрощенной системе налогообложения» (Р-20) и кодами печатной формы 1С 1105 «Гудвил», 1130 «Нематериальные
|
||||
поисковые активы», 1140 «Материальные поисковые активы», 1530 «Доходы будущих периодов», 2460 «Прочее» (имена — по
|
||||
`fixtures/ocr` и шаблонам банка); без этого законные повторы РБ, Счастливочной и aep_6 и любой неуверенный 1105 дают
|
||||
`NAME_MISMATCH`. Шаблонные строки достраиваются по словарю имён (одна на код, первое имя); код, у которого есть хотя бы
|
||||
одна строка `ok` из файла, шаблоном MUST NOT достраиваться; строки `unreadable` / `missing` не заменяются шаблонной строкой
|
||||
молча.
|
||||
|
||||
#### Scenario: Код словаря вне перечня
|
||||
- **WHEN** в `statement_lines.yaml` у редакции есть код, которого нет в перечне этой редакции копии пакета
|
||||
@ -43,25 +134,46 @@
|
||||
- **WHEN** у формы нет строки `ok` с кодом 1250, но есть строка `unreadable` «ИЧ,250»
|
||||
- **THEN** шаблонная строка 1250 достраивается пустой, строка `unreadable` показывается отдельно со статусом
|
||||
|
||||
#### Scenario: Два имени у кода
|
||||
- **WHEN** в редакции `fsbu4` словаря имён код 1520 записан дважды — «Кредиторская задолженность» и «Обязательство по
|
||||
аренде»
|
||||
- **THEN** словарь загружается; шаблонная строка 1520 одна, с именем «Кредиторская задолженность»
|
||||
|
||||
#### Scenario: Гудвил с заголовком раздела в имени
|
||||
- **WHEN** строка 1105 (уверенность 67) с наименованием «АКТИВ I. Внеоборотные активы Гудвил»
|
||||
- **THEN** справочное «Гудвил» (≥ 5 символов) входит в него — совпало, `NAME_MISMATCH` нет
|
||||
|
||||
## 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 в запросе подтверждения нет.
|
||||
Нарушения для 409 `CHECKS_NOT_PASSED` при `POST /documents/{id}/confirm` формы ББ/ОФР (порядок проверок и условия —
|
||||
«Подтверждение документа») SHALL читаться **только из записей `document_issue` вида `relation`** без акцепта
|
||||
(акцептованная форма нарушенной не считается); пересчёта §7 в запросе подтверждения нет. `detail.failures[]` —
|
||||
`{column_index, relation, expected, actual}`: `expected` и `actual` — строки Decimal или `null` (пустая цель при
|
||||
непустых членах — `expected = null`); `column_index` — индекс колонки записи, а если колонку определить нельзя
|
||||
(у записи нет ни `column_id`, ни `column_index`) — `null`.
|
||||
|
||||
#### Scenario: Жёсткое нарушение из document_issue
|
||||
- **WHEN** у формы есть запись `relation` без акцепта и нет строк `unreadable` / `missing` со значениями
|
||||
- **THEN** 409 `CHECKS_NOT_PASSED`
|
||||
- **THEN** 409 `CHECKS_NOT_PASSED`, `detail.failures[]` — по записи на нарушение
|
||||
|
||||
#### Scenario: Нарушение с пустой целью
|
||||
- **WHEN** у формы нет строки 1700 (потеряна источником), 1300 + 1400 + 1500 заполнены
|
||||
- **THEN** 409 `CHECKS_NOT_PASSED`; в `detail.failures[]` нарушение `1700 = 1300 + 1400 + 1500` с `expected = null`
|
||||
|
||||
### Requirement: Блок `checks` ответа формы из `document_issue`
|
||||
Блок `checks` ответа `GET …/statement` SHALL строиться из записей `document_issue` видов `relation` (`failed`) и `soft`
|
||||
(`soft_failed`) с `codes`, `expected`, `actual`, `line_ids`; собственного расчёта §7 при чтении нет. Межформенная
|
||||
проверка `cross_form` считается по агрегату формы по коду.
|
||||
(`soft_failed`) с `codes`, `expected`, `actual`, `line_ids`; собственного расчёта §7 при чтении нет. Элемент нарушения —
|
||||
`{column_index, relation, expected, actual, codes, line_ids}`: `expected` и `actual` — `Decimal` или `null`
|
||||
(`expected = null` — цель соотношения пуста при непустых членах); `column_index` — индекс колонки формы по `column_id`
|
||||
записи, иначе `column_index` записи, иначе `-1` (колонки нет: межформенная проверка `cross_form` всегда `-1`).
|
||||
Фактическое различие с `detail.failures[]` 409 (там отсутствующий индекс — `null`, а не `-1`) сохраняется как есть
|
||||
(P3-60). Межформенная проверка `cross_form` считается по агрегату формы по коду.
|
||||
|
||||
#### Scenario: checks из document_issue
|
||||
- **WHEN** у формы две записи `relation` и одна `soft`
|
||||
- **THEN** `checks.failed` содержит две записи, `checks.soft_failed` — одну; расчёт §7 при запросе не выполняется
|
||||
|
||||
#### Scenario: Нарушение с пустой целью в ответе
|
||||
- **WHEN** у формы запись `relation` по `1700 = 1300 + 1400 + 1500` с `expected = NULL`
|
||||
- **THEN** элемент `checks.failed` имеет `expected = null`, `actual` — сумма членов
|
||||
|
||||
@ -1,25 +1,105 @@
|
||||
## ADDED Requirements
|
||||
## MODIFIED 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 пересчитываться
|
||||
при загрузке.
|
||||
### Requirement: Хранение результата формы v2.0 (DBML v5.19)
|
||||
Маппер результата `2.0` SHALL писать документ формы как пришёл: строки, коды и значения не сливаются и не
|
||||
дедуплицируются (повтор кода — отдельные строки), каждая строка результата — `financial_statement_line` (`row_index`,
|
||||
`source_ref` = `ref`, `line_status`, `line_code`, `line_code_original_value` = `line_code` результата, `line_code_text`,
|
||||
`line_code_confidence`, `line_name`, `line_name_confidence`, `parent_row_index` — `row_index` строки с `ref = parent_ref`,
|
||||
`needs_review`, `is_confirmed = false`); каждая пара «строка × колонка» — одна `financial_statement_value` (`amount`,
|
||||
`string_value`, `source_text` = `values[].text`, `confidence`, `is_recognized`, `needs_review`; значения, которого нет в
|
||||
результате, — пустая запись с `needs_review = false`); колонки — `financial_statement_column` с `period_kind`,
|
||||
`label_confidence`; шапка — `financial_statement_meta` с `form_variant`, `needs_review_fields` и `header_source`
|
||||
(раздел «Шапка формы»); предупреждения и нарушения документа — `document_issue` (источник `service`); версия общего кода —
|
||||
`recognition_job.statements_version`. Единственные случаи, когда элемент результата не записывается, — нарушение
|
||||
уникальности ключей хранения, и каждый из них MUST фиксироваться `document_issue` `MAPPING_WARNING` (`info`, источник
|
||||
`core`): значение с `column_index` вне колонок формы; повтор `column_index` колонки — остаётся первая колонка, остальные
|
||||
пропускаются; повтор `row_index` или `ref` у строк — остаётся первая строка, остальные пропускаются; `detail` с `parent_ref`,
|
||||
которого нет, хранится с `parent_row_index = null`. Уникальность `(meta_id, line_code)` снята; `(meta_id, source_ref)`
|
||||
уникален там, где `source_ref` не `null`. Каждый `BALANCE` / `INCOME` результата SHALL становиться отдельным `document`;
|
||||
вторая форма того же типа в файле хранится (Р-10); ОСВ по счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип
|
||||
(и счёт) на файл. **Каждый** создаваемый `document` (ОСВ, формы, `DISCLOSURE`, `OTHER`) при приёме результата любого
|
||||
`MAJOR` (мапперы `1.x` и `2.0`) SHALL получать `file_ordinal` = индекс документа в `documents[]` результата (с 0);
|
||||
документ, который не создаётся (второй лист ОСВ с тем же счётом, второй `DISCLOSURE`), номер не занимает, но и не
|
||||
сдвигает остальные — номера уникальны в файле и могут идти с пропусками.
|
||||
|
||||
#### Scenario: Книга из двух ОСВ
|
||||
- **WHEN** результат содержит две ОСВ по счетам 60 и 62 (`documents[0]`, `documents[1]`), принятый маппером `1.x` или `2.0`
|
||||
- **THEN** два документа с `file_ordinal` 0 и 1; уникальность `(stored_file_id, file_ordinal)` не нарушена, файл не `ERROR`
|
||||
|
||||
#### Scenario: PDF с ББ и ОФР по 1.x
|
||||
- **WHEN** результат `1.2` содержит `BALANCE` и `INCOME`
|
||||
- **THEN** документы с `file_ordinal` 0 и 1
|
||||
|
||||
#### Scenario: Повтор кода сохраняется
|
||||
- **WHEN** в результате две строки `ok` с кодом 2410 (пустая и с `(649)`)
|
||||
- **THEN** в БД две строки 2410 с разными `row_index` и `source_ref`, обе со своими значениями
|
||||
|
||||
#### Scenario: Форма 2.0 хранится как пришла
|
||||
- **WHEN** результат `2.0` содержит две строки с кодом 1230 и строку без кода (`ref` с `parent_ref`)
|
||||
- **THEN** создано три строки с их `row_index`, `source_ref` и `parent_row_index`; повтор кода не сливается
|
||||
|
||||
#### Scenario: Строка без кода
|
||||
- **WHEN** в результате строка `unreadable` с `line_code_text = "ИЧ,250"` и суммой
|
||||
- **THEN** строка хранится с `line_code = null`, `line_code_original_value = null`, `line_status = unreadable`,
|
||||
`line_code_text = "ИЧ,250"` и `row_index` из результата
|
||||
|
||||
#### Scenario: Два баланса одного периода в одном файле
|
||||
- **WHEN** результат содержит два `BALANCE` с одной парой `period_year`, `period_quarter` (лист-продолжение другого
|
||||
типа или двойная печать)
|
||||
- **THEN** создаются два документа с разными `file_ordinal`; ни один не автоподтверждается (Р-18)
|
||||
|
||||
#### Scenario: Два баланса разных периодов в одном файле
|
||||
- **WHEN** результат содержит `BALANCE` на 30.09.2025 и `BALANCE` на 31.12.2024
|
||||
- **THEN** два документа с разными `file_ordinal`; каждый получает статус по своим условиям, правило Р-18 их не касается
|
||||
|
||||
#### Scenario: Повтор column_index колонки
|
||||
- **WHEN** в `columns[]` результата два элемента с `column_index = 1`
|
||||
- **THEN** сохраняется первая колонка, вторая пропускается, у документа `MAPPING_WARNING`
|
||||
|
||||
#### Scenario: Повтор ref строки
|
||||
- **WHEN** в результате две строки с одним `ref`
|
||||
- **THEN** сохраняется первая, вторая пропускается с `MAPPING_WARNING`; приём не падает
|
||||
|
||||
### Requirement: Формы результата 1.x — §7 при приёме
|
||||
Для формы, принятой по результату `1.x` (окно перехода после выкладки ядра; `checks` контракта 2.0 у неё нет), ядро
|
||||
SHALL **один раз при приёме** посчитать §7 точкой входа пакета для пересчёта (`statement-normalization`; вариант формы —
|
||||
по структуре кодов, как у 2.0) по суммам результата и записать нарушения в `document_issue` (источник `core`, виды
|
||||
`relation` / `soft`, как в требовании о `document_issue`) и `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` структурами `{code: "LEGACY_WARNING", severity: "info", message}`. После правки такая
|
||||
форма пересчитывается общим кодом, как форма 2.0; `MAPPING_WARNING` и `LEGACY_WARNING` статус не держат. Формы,
|
||||
существовавшие в БД до миграции v5.19, §7 не пересчитываются (требования «Миграция DBML v5.19» и «Очистка форм перед
|
||||
выкладкой»).
|
||||
|
||||
#### Scenario: Полярность needs_review при приёме 1.x
|
||||
- **WHEN** в результате `1.2` у значения `confidence = 100`, `is_recognized = true`, у другого — `confidence = 80`
|
||||
- **THEN** у первого `needs_review = false`, у второго — `true`
|
||||
|
||||
#### 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`
|
||||
|
||||
#### Scenario: 1.x-документ после PATCH
|
||||
- **WHEN** форма принята по результату `1.2`, аналитик правит сумму 1230 так, что `1200 = 1210 + … + 1260` сходится
|
||||
- **THEN** ядро пересчитывает §7 и статус общим кодом, запись `relation` по 1200 заменена (её больше нет), статус
|
||||
пересчитан; `STATEMENTS_VERSION_MISMATCH` не пишется (у задания 1.x версии нет)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Проверки ядра при приёме результата 2.0
|
||||
При приёме результата `2.0` ядро SHALL писать в `document_issue` формы предупреждения и `checks` результата (источник
|
||||
`service`) и проверки ядра (источник `core`): `CODE_NOT_IN_DICTIONARY` и `CODE_NOT_IN_EDITION` (редакция — `edition` точки
|
||||
входа пакета, а не по дате; одна запись на код документа, `line_ids` — все строки `ok` кода; **только для кодов из ровно
|
||||
четырёх цифр** — пятизначные подстроки Excel не проверяются; запись не пишется, если пакет не определил редакцию),
|
||||
`NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT` (согласованность `done` — вызовом `engine.contract_document_status`
|
||||
пакета) и `STATEMENTS_VERSION_MISMATCH` (при отличии `statements_version` задания от версии копии ядра; у `1.x` не
|
||||
пишется). Проверки §7, `needs_review` и предупреждения сервиса MUST NOT пересчитываться при загрузке. Статус документа
|
||||
формы — по требованию «Условие `RECOGNIZED` при приёме 2.0».
|
||||
|
||||
#### Scenario: Пятизначный код не даёт info
|
||||
- **WHEN** в ББ строка с кодом `12301` (подстрока Excel), которого нет в словаре
|
||||
- **THEN** `CODE_NOT_IN_DICTIONARY` и `CODE_NOT_IN_EDITION` для него не пишутся
|
||||
@ -28,6 +108,10 @@
|
||||
- **WHEN** в ББ строка 1120 на бланке `fsbu4`, где такого кода нет
|
||||
- **THEN** запись `CODE_NOT_IN_EDITION` (`info`, `core`) с `line_ids` строк `ok` этого кода
|
||||
|
||||
#### Scenario: Версия копии отличается
|
||||
- **WHEN** результат `2.0` принят с `statements_version`, отличной от версии копии ядра
|
||||
- **THEN** запись `STATEMENTS_VERSION_MISMATCH` (`info`, `core`) уже при приёме
|
||||
|
||||
### Requirement: Ведомость ОСВ по контракту 2.0
|
||||
Для листа ОСВ результата `2.0` ядро SHALL создавать документ ОСВ по `osvDocument`, предупреждения листа
|
||||
(`osvDocument.warnings`, `OSV_*`) писать в `document_issue` (вид `warning`, источник `service`), а статус документа
|
||||
@ -37,21 +121,3 @@
|
||||
#### 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`
|
||||
|
||||
@ -2,15 +2,18 @@
|
||||
|
||||
### Requirement: Соотношения §7 по колонкам
|
||||
Проверки SHALL считаться чистой функцией по текущим `financial_statement_value.amount` документа для каждой колонки
|
||||
отдельно по словарю §7 копии пакета `arm_statements` (`statement-normalization`; своей копии словаря ядро не держит): соотношение проверяется, только
|
||||
если присутствует его левая часть; отсутствующий код и пустая сумма — 0; выполнено, если `|левая − правая| ≤ tolerance`
|
||||
отдельно по словарю §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}`; не хранится как есть: нарушения при приёме и пересчёте записываются в `document_issue`.
|
||||
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 существовать.
|
||||
Присутствующими 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`
|
||||
@ -29,18 +32,25 @@ relation, expected, actual}`; не хранится как есть: наруш
|
||||
- **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`
|
||||
- **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 0`,
|
||||
`actual 2000000`
|
||||
- **THEN** в колонке 0 соотношение выполнено; в колонке 1 `failed` содержит `1700 = 1300 + 1400 + 1500` с
|
||||
`expected = null` (1700 в колонке 1 пуста), `actual = 2000000`
|
||||
|
||||
#### Scenario: Очищенная шаблонная строка
|
||||
- **WHEN** затем аналитик очищает 1700 в колонке 0 (`amount = null`)
|
||||
- **THEN** строка 1700 снова не участвует в проверках
|
||||
- **THEN** строка 1700 снова не участвует в проверках как значение: пустая цель при непустых членах — нарушение
|
||||
`expected = null` в каждой колонке (прежнее «очищенная строка не цель» не действует, P3-30)
|
||||
|
||||
#### Scenario: Повтор кода суммируется
|
||||
- **WHEN** в ББ две строки 1230 со значениями 100 и 50 в колонке 0
|
||||
|
||||
@ -1,9 +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 ядра, редакции из перечня пакета
|
||||
- [x] B0. Change OpenSpec части Б в arm-core
|
||||
- [x] B1. Адаптер пакета `StatementsEngine` и вход пересчёта из БД
|
||||
- [x] B2. Пересчёт после правки общим кодом и §7 из `document_issue`
|
||||
- [x] B3. Маппер `1.x` — §7 при приёме и вариант формы (Р-30)
|
||||
- [x] B4. Маппер `2.0` — формы
|
||||
- [x] B5. Маппер `2.0` — ОСВ, выбор по MAJOR, приём файла результата
|
||||
- [x] B6. Удаление копии словаря §7 ядра, редакции из перечня пакета
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user