docs(#116): proposal и дельты спек единой нормализации ББ/ОФР
- proposal: зачем, что меняется, порядок выкладки «сначала ядро», non-goals - statement-normalization: шаги 1–8 общего кода arm_statements (кандидат, раскладка, статусы строк, значения, §7, needs_review, структурная проверка кода, уровни предупреждений, статус) - recognition-contract: файл результата и конверт v2.0, переход 1.x → 2.0 - tabular-recognition-service, scan-gateway: адаптеры шага 1 и что уходит из сервисов - arm-core: хранение v2.0, агрегат по коду, document_issue, RECOGNIZED, PATCH lines/columns/confirmed, порядок 409, миграция v5.19, crm_id ГСК/ГСЗ - arm-web: экран проверки формы по новым полям - design.md: автозамена между файлами — §9.5 п. 3, а не §12 п. 3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
43b4b808ae
commit
90ea429642
@ -746,7 +746,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
одна запись.
|
||||
- **Подтверждение документа** (Р-13, Р-18): порядок проверок 409 по §9.5 п. 4: `PERIOD_REQUIRED` → `LINES_UNRESOLVED`
|
||||
(есть строки `unreadable` / `missing` с непустым `amount`; строки без сумм не мешают) → `DUPLICATE_FORM` (в файле уже
|
||||
подтверждена другая форма того же типа и периода; 409 со ссылкой на неё; между файлами работает автозамена §12 п. 3)
|
||||
подтверждена другая форма того же типа и периода; 409 со ссылкой на неё; между файлами работает автозамена §9.5 п. 3)
|
||||
→ жёсткие нарушения без акцепта. Автоподтверждения `LINES_UNRESOLVED` не касается (`done` таких строк не
|
||||
допускает). `detail` в агрегат, КФО и снимок ЕХ не попадают — цена Р-8.
|
||||
- Правка даты отчёта (PATCH шапки): для колонок с `period_kind = relative:<вид>` ядро досчитывает `period_end` общим
|
||||
@ -850,7 +850,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
само разлепление — предупреждение `info`.
|
||||
- **Р-18** (ядро) — при двух и более формах одного типа и периода в одном файле ни одна не автоподтверждается;
|
||||
подтверждение второй при подтверждённой первой — 409 `DUPLICATE_FORM` только внутри файла (между файлами работает
|
||||
автозамена §12 п. 3).
|
||||
автозамена §9.5 п. 3).
|
||||
- **Р-19** (ядро) — `RECOGNIZED` не зависит от статуса конверта: достаточно `document.status = done` и отсутствия
|
||||
предупреждений `error` уровня файла (потерянный json или таблица ИС блокируют, `needs_check` соседнего документа
|
||||
из-за одной ячейки — нет).
|
||||
|
||||
114
openspec/changes/unified-statement-normalization/proposal.md
Normal file
114
openspec/changes/unified-statement-normalization/proposal.md
Normal file
@ -0,0 +1,114 @@
|
||||
## Why
|
||||
|
||||
Формы ББ/ОФР сегодня нормализуются двумя разными конвейерами: scan-gateway (PDF → ИС Сканирование) и tabular-parser
|
||||
(Excel). У них разные правила раскладки таблицы, отбора строк, разбора сумм, проверок §7 и статуса; ядро не доверяет
|
||||
ни одному из них и пересчитывает проверки и статус заново по косвенным признакам, дважды дедуплицирует строки и держит
|
||||
третью и четвёртую копии словарей (§7 и «код → наименование»). Пользователь видит последствия как отдельные «ошибки
|
||||
распознавания» — AURORA-1406, 1462, 1511, 1517, 1519, 1522, 1523, 1524, 1634: строки молча пропадают, повтор кода
|
||||
теряет сумму, «чистая» ячейка с потерянной цифрой не подсвечивается, предупреждения сервисов нигде не видны.
|
||||
|
||||
Дизайн `design.md` (финальная версия, решения владельца Р-1…Р-19 от 17.10.2026, три раунда ревью) приводит оба
|
||||
сервиса к одной цепочке шагов 1–9 с единым выходом каждого шага и общим кодом шагов 2–8; ядро хранит результат как
|
||||
пришёл и пересчитывает его тем же общим кодом только после правок аналитика.
|
||||
|
||||
Срез: вне плана срезов `09-delivery-plan.md` — переработка Среза 2b (Excel-формы), Среза 3a (OCR-нормализатор) и
|
||||
Среза 3b (формы в ядре). Issue RSHB/arm-ka-docs#116.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Общий код нормализации** — новый репозиторий `arm-statements`, пакет `arm_statements/` (Р-1): типы шага 1
|
||||
(`FormCandidate`, `Cell`), шаги 2–8 (раскладка, отбор строк, значения, код и наименование, сборка документа,
|
||||
самопроверки §7, статус и `needs_review`), словари §7 (полный и упрощённый наборы), периодов, заголовков форм,
|
||||
подписей колонок, упорядоченные перечни кодов форм по редакциям и вариантам, диапазоны кодов. Словаря «код →
|
||||
наименование» в пакете нет (Р-3). В tabular-parser, scan-gateway и arm-core пакет лежит копией, которую раскладывает
|
||||
и проверяет скрипт `sync-statements` репозитория docs (по образцу `sync-web.sh`).
|
||||
- **Строки не выбрасываются молча:** статусы строк `ok` | `unreadable` | `missing` | `detail`, `row_index`, `ref`,
|
||||
сырой текст кода и ячейки (`text`) доезжают до БД и экрана; повтор кода — законный случай, все строки сохраняются
|
||||
(Р-4, Р-8). **BREAKING** для потребителей: `line_code` становится nullable.
|
||||
- **Качество структурно и с привязкой к месту:** `needs_review` у значения, строки и полей шапки; «подтверждение
|
||||
проверкой» §7 вместо порога уверенности (Р-5, Р-7, Р-11, Р-12, Р-16, Р-17); `document.status` у каждого документа;
|
||||
предупреждения — структуры `{code, severity, message, line_refs, column_index, field}` вместо строк.
|
||||
- **Контракт `recognition-result.schema.json` v2.0** (MAJOR, Р-9) и конверт `recognition-result-message.schema.json`
|
||||
v2.0 (структурные `metrics.warnings`); запрос `recognition-request` остаётся v1.0. Путь ОСВ в tabular-parser
|
||||
меняется только в части структуры предупреждений и `document.status`.
|
||||
- **arm-core:** DBML v5.19 и одна миграция (статусы и порядок строк, `needs_review`, `is_confirmed`, `source_text`,
|
||||
`period_kind`, `form_variant`, таблица `document_issue`, снятие уникальности `(meta_id, line_code)` и — для ББ/ОФР —
|
||||
`document (stored_file_id, document_type, osv_account_code)`, `recognition_job.statements_version`,
|
||||
`related_group.crm_id` по arm-core#112); «агрегат формы по коду» вместо «одна строка на код»; приём 1.x и 2.0 на
|
||||
время перехода; новое условие `RECOGNIZED`; PATCH `…/statement` с `lines`, `columns`, `values[].confirmed`; 409
|
||||
`LINES_UNRESOLVED` и `DUPLICATE_FORM` при подтверждении.
|
||||
- **arm-web:** экран проверки формы — строки со статусами на своих местах, расшифровки под переключателем, подсветка
|
||||
только по `needs_review`, предупреждения и нарушения у места, назначение кода, «Это расшифровка», подтверждение кода
|
||||
и ячейки, дата колонки.
|
||||
- **Документы:** `05-domain-rules.md` — §11 становится общей главой нормализации ББ/ОФР с подразделами по
|
||||
источникам, §12 — заголовок-редирект; §7 — «посчитанное» соотношение, `abs_terms` и знак, упрощённый набор; §9.3,
|
||||
§9.5 — статус, маппинг и подтверждение формы по v2.0. `07-open-questions.md` — закрыты OQ-36, OQ-141 (Р-8) и
|
||||
вопросы, снятые решениями дизайна; новые вопросы — склейка половинок формы (Р-15) и открытое в дизайне.
|
||||
`frontend-api-guide.md` — новые поля формы и PATCH (после реализации).
|
||||
|
||||
Читают и пишут контракт: `arm-core` (читает результат и конверт, публикует запрос), `tabular-parser` и `scan-gateway`
|
||||
(читают запрос, пишут результат и конверт). Дочерние change'и (заводятся при планировании, по одному на репозиторий):
|
||||
`arm-statements` (новый), RSHB/arm-core, RSHB/arm-tabular-parser, RSHB/arm-scan-gateway, RSHB/arm-web; связанная
|
||||
задача — RSHB/arm-core#112 (`related_group.crm_id` входит в ту же миграцию, Р-10).
|
||||
|
||||
### Порядок выкладки — «сначала ядро» (обязателен)
|
||||
|
||||
1. `arm-statements` — пакет с тестами; `sync-statements` в docs.
|
||||
2. docs: контракт v2.0, DBML v5.19, правила (этот change).
|
||||
3. arm-core: миграция v5.19, маппер 2.0 рядом с маппером 1.x, приём ack и конвертов 1.x и 2.0, PATCH, экранные поля
|
||||
API. До этого шага ни один сервис не шлёт 2.0: первый же `line_code = null` дал бы файлу `ERROR`.
|
||||
4. arm-web: экран по новым полям (поля 1.x-документов заполнены по умолчанию — экран работает и на них).
|
||||
5. tabular-parser и scan-gateway переходят на 2.0 в любом порядке относительно друг друга.
|
||||
6. После перехода обоих сервисов маппер 1.x удаляется из ядра отдельным PR (не в этом change).
|
||||
|
||||
Копии схем в сервисах (`api/contracts/`, тест идентичности в `make check`) после слияния контракта v2.0 в docs
|
||||
устаревают: каждый сервис обновляет копию в своём PR шага 5, ядро — в PR шага 3.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Разбор ОСВ (`classify.py`, `to_money` ОСВ, правила §1) не меняется; путь ОСВ в tabular-parser меняется только в
|
||||
структуре предупреждений и `document.status`.
|
||||
- Переключатель «как в файле» (порядок строк по `row_index`) на экране — не в этой работе (Р-14).
|
||||
- Склейка двух половинок формы из одного файла в ядре не предоставляется — файл перезагружают; вопрос в
|
||||
`07-open-questions.md` (Р-15). Склейка страниц из разных файлов (OQ-140) — тоже нет.
|
||||
- Восстановление строк по наименованию (OQ-149) и сопоставление формы без колонки кодов по именам строк (OQ-139) — нет.
|
||||
- Запрос `recognition-request` остаётся v1.0; subject'ы NATS, `Nats-Msg-Id`, ack и повторы не меняются по механике.
|
||||
- Межформенная проверка `1370 − 1370(нач. года) = 2400` остаётся в ядре и мягкой; формулы §7 по редакциям (OQ-146) и
|
||||
знаки 2410/2420 (OQ-12) не пересматриваются.
|
||||
- Замена пакета-копии на зависимость из Nexus платформы — не сейчас (платформа Python-пакеты не собирает, Р-1).
|
||||
- Приведение единиц измерения сверх числового формата ячейки (OQ-138) — нет.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `statement-normalization`: общая цепочка нормализации ББ/ОФР (шаги 1–8) — пакет `arm_statements`, его типы,
|
||||
правила и словари, `sync-statements`.
|
||||
- `tabular-recognition-service`: адаптер Excel шага 1 в tabular-parser и что остаётся в сервисе (чтение книги, листы
|
||||
ОСВ, сборка конверта v2.0).
|
||||
- `scan-gateway`: адаптер json ИС шага 1 в scan-gateway и что остаётся в сервисе (горячие папки, склейка набора json,
|
||||
`raw/`, сборка конверта v2.0).
|
||||
- `arm-core`: хранение результата v2.0 (DBML v5.19), агрегат по коду, `document_issue`, условие `RECOGNIZED`, PATCH
|
||||
формы, порядок 409 подтверждения, приём 1.x и 2.0.
|
||||
- `arm-web`: экран проверки формы ББ/ОФР по новым полям.
|
||||
|
||||
### Modified Capabilities
|
||||
- `recognition-contract`: файл результата и конверт — v2.0; правила перехода (ядро принимает 1.x и 2.0, «сначала
|
||||
ядро»); статус и структурные предупреждения у всех документов, включая ОСВ.
|
||||
|
||||
## Impact
|
||||
|
||||
- Новый репозиторий `arm-statements`; скрипт `sync-statements` и цель Makefile в docs (вызывается из `make check-full`
|
||||
потребителей, не из `make check`).
|
||||
- tabular-parser: `parser/statements/*` (кроме разбора книги и адаптера шага 1) уходит в общий код; словари
|
||||
`statement_checks.yaml`, `statement_periods.yaml`, `statement_headers.yaml` переезжают в пакет; путь ОСВ — структура
|
||||
предупреждений и `document.status`.
|
||||
- scan-gateway: `normalizer/ocr/text|dates|amounts` становятся общими разборами, `layout|rows|header|document` — шагами
|
||||
2–6 общего кода; `api/rules/statement_lines.yaml` удаляется, `statement_labels.yaml` и `ocr_lookalikes.yaml` уходят в
|
||||
словари пакета.
|
||||
- arm-core: миграция v5.19 (`financial_statement_*`, `document`, `document_issue`, `recognition_job`,
|
||||
`related_group`), `result_mapping`, `statement_mapping`, `statements`, `documents`, `auto_confirm`, схемы API формы;
|
||||
`api/rules/statement_checks.yaml` удаляется (копия словаря §7), `statement_lines.yaml` остаётся только словарём имён.
|
||||
- arm-web: `widgets/DocumentReview` (экран [41]), типы API.
|
||||
- Решение v5.13 «`needs_review` считается, не хранится» пересматривается (design, Decisions № 17).
|
||||
- Риск окна выкладки: пока сервис шлёт 1.x, документы его конвейера получают статус по прежнему правилу D-6 и поля
|
||||
v2.0 по умолчанию — экран и подтверждение работают, новых сигналов качества нет.
|
||||
@ -0,0 +1,202 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Хранение результата формы v2.0 (DBML v5.19)
|
||||
Маппер результата `2.0` SHALL писать документ формы как пришёл, без повторной дедупликации колонок, кодов и значений:
|
||||
каждая строка результата — `financial_statement_line` (`row_index`, `source_ref` = `ref`, `line_status`, `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`; предупреждения и нарушения документа — `document_issue` (источник `service`); версия общего
|
||||
кода — `recognition_job.statements_version`. Уникальность `(meta_id, line_code)` снята; `(meta_id, source_ref)`
|
||||
уникален там, где `source_ref` не `null`. Каждый `BALANCE` / `INCOME` результата SHALL становиться отдельным
|
||||
`document` с `file_ordinal` (порядковый номер документа в результате); вторая форма того же типа в файле хранится
|
||||
(Р-10); ОСВ по счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и счёт) на файл.
|
||||
|
||||
#### Scenario: Повтор кода сохраняется
|
||||
- **WHEN** в результате две строки `ok` с кодом 2410 (пустая и с `(649)`)
|
||||
- **THEN** в БД две строки 2410 с разными `row_index` и `source_ref`, обе со своими значениями
|
||||
|
||||
#### Scenario: Строка без кода
|
||||
- **WHEN** в результате строка `unreadable` с `line_code_text = "ИЧ,250"` и суммой
|
||||
- **THEN** строка хранится с `line_code = null`, `line_status = unreadable`, `line_code_text = "ИЧ,250"`
|
||||
|
||||
#### Scenario: Два баланса в одном файле
|
||||
- **WHEN** результат содержит два `BALANCE` (лист-продолжение другого типа или двойная печать)
|
||||
- **THEN** создаются два документа с разными `file_ordinal`; ни один не автоподтверждается (Р-18)
|
||||
|
||||
### 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
|
||||
|
||||
### Requirement: Условие `RECOGNIZED` при приёме 2.0
|
||||
При приёме `2.0` статус документа формы SHALL быть `RECOGNIZED` ⇔ `document.status = done` ∧ в `metrics.warnings` файла
|
||||
нет предупреждений уровня `error` ∧ `counterparty_check ≠ MISMATCH` ∧ период — конец квартала (`period_year` и
|
||||
`period_quarter` определены) ∧ у каждой строки `ok` с неуверенным кодом (`line_code_confidence < 100`) распознанное
|
||||
наименование совпало со справочным по коду ∧ в файле нет второй формы того же типа и периода; иначе `NEEDS_CHECK`.
|
||||
Статус конверта и `needs_check` соседнего документа в условие MUST NOT входить (Р-19). Проверки §7, `needs_review` и
|
||||
предупреждения сервиса при загрузке MUST NOT пересчитываться. Совпадение наименований: обе строки приводятся к нижнему
|
||||
регистру, `ё` → `е`, всё, кроме букв и цифр, — пробел, пробелы схлопываются; совпало, если строки равны, или более
|
||||
короткая (не короче 5 символов) входит в более длинную, или отношение сходства `difflib.SequenceMatcher` ≥ 0,8; с
|
||||
любым справочным именем кода в любой редакции словаря ядра. Кода нет в словаре имён или наименование пусто — не
|
||||
совпало. Несовпадение — `document_issue` `NAME_MISMATCH` (`info`, источник `core`, строка).
|
||||
`RECOGNIZED` SHALL, как и прежде, автоподтверждаться (OQ-157).
|
||||
|
||||
#### Scenario: Ячейка соседнего документа
|
||||
- **WHEN** ББ `done`, ОФР того же файла `needs_check`, предупреждений файла уровня `error` нет, ИНН совпал, период —
|
||||
30.09.2025
|
||||
- **THEN** ББ `RECOGNIZED` и автоподтверждается, ОФР — `NEEDS_CHECK`
|
||||
|
||||
#### Scenario: Потерянный json
|
||||
- **WHEN** все документы `done`, но в `metrics.warnings` файла `JSON_FAILED` уровня `error`
|
||||
- **THEN** все документы формы файла `NEEDS_CHECK`
|
||||
|
||||
#### Scenario: Подмена неуверенного кода на соседа
|
||||
- **WHEN** строка с кодом 1120 (уверенность 50) и наименованием «Нематериальные активы»
|
||||
- **THEN** наименование не совпало со справочным «Результаты исследований и разработок», документ `NEEDS_CHECK`,
|
||||
`NAME_MISMATCH` у строки
|
||||
|
||||
#### Scenario: Период не конец квартала
|
||||
- **WHEN** документ `done` с датой отчёта 15.08.2025
|
||||
- **THEN** квартал не определён, документ `NEEDS_CHECK` (как в D-6) и не автоподтверждается; ручное подтверждение —
|
||||
409 `PERIOD_REQUIRED`, пока аналитик не поправит дату отчёта
|
||||
|
||||
### Requirement: Предупреждения и нарушения документа — `document_issue`
|
||||
Ядро SHALL хранить предупреждения и нарушения документа в `document_issue`: вид (`warning` | `relation` | `soft`), код,
|
||||
уровень (`error` | `info`), сообщение, список `line_id`, `column_id`, поле шапки, источник (`service` | `core`), для
|
||||
нарушений — текст соотношения, `expected`, `actual`. При загрузке пишутся предупреждения и `checks` результата
|
||||
(источник `service`) и проверки ядра уровня `info` (источник `core`): `CODE_NOT_IN_DICTIONARY` (кода нет в словаре
|
||||
имён), `CODE_NOT_IN_EDITION` (код не из перечня редакции по эффективной дате — по копии перечня `arm_statements`),
|
||||
`NAME_MISMATCH`. При перезагрузке файла записи документа удаляются вместе с ним. При пересчёте после правки записи
|
||||
`relation` и `soft` заменяются целиком независимо от источника; записи `warning` источника `service` сохраняются,
|
||||
кроме снимаемых правкой: правка `header.report_date` снимает `PERIOD_UNKNOWN`, `REPORT_DATE_UNPARSED`,
|
||||
`REPORT_DATE_UNCERTAIN`; правка `header.inn` или `organization_name` — `NO_ORGANIZATION_NO_INN`; правка
|
||||
`columns[].period_date` — `COLUMN_WITHOUT_DATE` и `COLUMN_DATE_UNCERTAIN` этой колонки; прочие `error` сервиса снимаются
|
||||
только перезагрузкой файла.
|
||||
|
||||
#### Scenario: Дата колонке
|
||||
- **WHEN** у колонки предупреждение `COLUMN_WITHOUT_DATE`, аналитик ставит ей `period_date` через PATCH
|
||||
- **THEN** предупреждение удаляется, статус пересчитывается
|
||||
|
||||
#### Scenario: Неснимаемое предупреждение
|
||||
- **WHEN** у документа `SEGMENT_COLUMNS_MISMATCH`, аналитик правит значения
|
||||
- **THEN** предупреждение остаётся; документ можно подтвердить вручную (оно подтверждение не блокирует)
|
||||
|
||||
### Requirement: Пересчёт после правки
|
||||
После каждого PATCH формы ядро SHALL пересчитывать `checks`, `needs_review` и статус тем же общим кодом
|
||||
`arm_statements` по сохранённым данным (конвенция знака — по `source_text`), с правилами: `is_modified` или
|
||||
`is_confirmed` значения ⇒ `needs_review = false`; `line_code_modified` или `is_confirmed` строки ⇒ `needs_review`
|
||||
строки = `false`; акцепт ⇒ жёсткие нарушения не учитываются; правленые ячейки в голосование знака не входят.
|
||||
Отличие `statements_version` задания от версии копии ядра — `document_issue` `STATEMENTS_VERSION_MISMATCH` (`info`).
|
||||
Один PATCH — один пересчёт и одна запись аудита `RECOGNIZED_DATA_EDIT`. Акцепт сбрасывается любой правкой, как и
|
||||
прежде.
|
||||
|
||||
#### Scenario: Подтверждение ячейки без правки
|
||||
- **WHEN** аналитик подтверждает ячейку с `needs_review = true` без изменения суммы
|
||||
- **THEN** `is_confirmed = true`, `confirmed_by/at`, `is_modified` не ставится, `needs_review = false`, аудит
|
||||
|
||||
### Requirement: PATCH формы — строки, колонки, подтверждение ячеек
|
||||
`PATCH /documents/{id}/statement` SHALL принимать помимо `header` и `values` (сумма):
|
||||
- `values[] {line_id, column_id, confirmed: true}` — подтвердить ячейку без изменения значения;
|
||||
- `columns[] {column_id, period_date}` — дата колонке (пересчитывает `is_current` и период документа по правилу §9.5
|
||||
п. 2); правка `header.report_date` (в том числе `null`) пересчитывает `period_end` колонок с `period_kind =
|
||||
relative:*` общим кодом (`null` даёт `null`);
|
||||
- `lines[] {line_id, line_code}` — назначить или сменить код; `lines[] {line_id, line_status: "detail"}` — отметить
|
||||
расшифровкой; `lines[] {line_id, confirmed: true}` — подтвердить код.
|
||||
|
||||
Переходы: `unreadable` / `missing` / `detail` → `ok` назначением кода; `unreadable` / `missing` / `ok` → `detail`
|
||||
отметкой; `ok` → `ok` сменой кода (прежний — в `line_code_original_value`); `ok` с неуверенным кодом — подтверждением.
|
||||
Назначенный или подтверждённый код: `line_code_confidence = 100`, `line_code_modified = true`, `needs_review`
|
||||
строки = `false`; у `detail` `parent_row_index` пересчитывается как ближайшая строка `ok` выше по `row_index`. Код —
|
||||
4–5 цифр (иначе 422 `INVALID_LINE_CODE`), может совпадать с существующим; незнакомый словарю — `info`. Шаблонная строка
|
||||
с тем же кодом, созданная правкой аналитика с суммами, — 409 `LINE_EXISTS` (сначала очистить её суммы или выбрать
|
||||
другой код); без сумм — удаляется, её место занимает назначенная. `line_status = ok` без кода — 422
|
||||
`LINE_CODE_REQUIRED`; в одном элементе больше одного действия или ни одного — 422 `INVALID_LINE_PATCH`; строка не этого
|
||||
документа — 422 `UNKNOWN_LINE`; колонка не этого документа — 422 `UNKNOWN_COLUMN`. Правка суммы по `line_code` (для
|
||||
достроенной строки) при наличии строки `ok` с этим кодом — 422 `UNKNOWN_CELL` (передайте `line_id`). Удаления строк
|
||||
нет. Строки, созданные правкой: `row_index = null`, `source_ref = null`, `line_status = ok`. Аудит
|
||||
`RECOGNIZED_DATA_EDIT` с `before/after` по коду, статусу, родителю и подтверждениям. Ограничения состояния — как
|
||||
прежде (`DOCUMENT_READONLY`, `FILE_SUPERSEDED`, `DOCUMENT_NOT_STATEMENT`).
|
||||
|
||||
#### Scenario: Назначение кода строке-мусору (AURORA-1462)
|
||||
- **WHEN** аналитик назначает строке `unreadable` «ИЧ,250» код 1250
|
||||
- **THEN** строка `ok`, `line_code = 1250`, `line_code_confidence = 100`, `line_code_modified = true`,
|
||||
`line_code_original_value = null`; §7 и статус пересчитаны
|
||||
|
||||
#### Scenario: Отметка расшифровкой
|
||||
- **WHEN** аналитик отмечает строку `missing` с суммой как расшифровку
|
||||
- **THEN** `line_status = detail`, строка выходит из сумм и не блокирует подтверждение
|
||||
|
||||
#### Scenario: Код занят шаблонной строкой с суммами
|
||||
- **WHEN** аналитик назначает строке `unreadable` код 1250, а строка 1250 создана правкой аналитика и имеет сумму
|
||||
- **THEN** 409 `LINE_EXISTS`, ничего не изменено
|
||||
|
||||
### Requirement: Подтверждение формы — порядок 409
|
||||
`POST /documents/{id}/confirm` для формы ББ/ОФР SHALL проверять в порядке: `FILE_SUPERSEDED` → `ALREADY_CONFIRMED` →
|
||||
`DOCUMENT_NOT_CONFIRMABLE` → `PERIOD_REQUIRED` → `LINES_UNRESOLVED` (есть строки `unreadable` / `missing` с непустым
|
||||
`amount`; `detail.lines[] {line_id, row_index, line_code_text}`; строки без сумм не мешают) → `DUPLICATE_FORM` (в файле
|
||||
уже подтверждена другая форма того же типа и периода; `detail.document_id`) → `CHECKS_NOT_PASSED` (жёсткие нарушения
|
||||
без акцепта). Между файлами повтор формы решает автозамена §9.5 п. 3.
|
||||
|
||||
#### Scenario: Неразобранная строка с суммой
|
||||
- **WHEN** у формы строка `missing` с суммой и одновременно жёсткое нарушение
|
||||
- **THEN** 409 `LINES_UNRESOLVED` с этой строкой в `detail.lines`
|
||||
|
||||
#### Scenario: Вторая форма того же периода
|
||||
- **WHEN** в файле два ББ на 30.09.2025, первый подтверждён, аналитик подтверждает второй
|
||||
- **THEN** 409 `DUPLICATE_FORM` со ссылкой на первый
|
||||
|
||||
### Requirement: Словари ядра и достройка шаблонных строк
|
||||
Копия словаря §7 ядра (`api/rules/statement_checks.yaml`) SHALL удаляться: проверки ядро считает копией пакета
|
||||
`arm_statements`. `api/rules/statement_lines.yaml` SHALL оставаться только словарём имён (код → наименование по
|
||||
редакциям); порядок и состав редакций — из копии перечня кодов пакета; `sync-statements --check` сверяет, что все коды
|
||||
словаря имён есть в перечне. Шаблонные строки достраиваются по словарю имён; код, у которого есть хотя бы одна строка
|
||||
`ok` из файла, шаблоном MUST NOT достраиваться; строки `unreadable` / `missing` не заменяются шаблонной строкой молча.
|
||||
|
||||
#### Scenario: Код есть только у строки unreadable
|
||||
- **WHEN** у формы нет строки `ok` с кодом 1250, но есть строка `unreadable` «ИЧ,250»
|
||||
- **THEN** шаблонная строка 1250 достраивается пустой, строка `unreadable` показывается отдельно со статусом
|
||||
|
||||
### Requirement: Ответ `GET …/statement` по v2.0
|
||||
`StatementOut` SHALL отдавать: у документа `form_variant`; у шапки `needs_review_fields` и сырые тексты полей; у
|
||||
колонок `period_kind`, `label_confidence`; у строк `id`, `origin`, `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`, `field`; `checks` — нарушения с `codes` и `line_ids`. Порядок `lines` — по коду «как на бумаге» (Р-14):
|
||||
внутри строк `ok` по возрастанию кода, одинаковые коды по `row_index`; строки `unreadable` / `missing` — после
|
||||
ближайшей предыдущей по `row_index` строки, размещённой по коду, без такой — в начале; `detail` — после родителя.
|
||||
|
||||
#### Scenario: Документ, принятый по 1.x
|
||||
- **WHEN** форма загружена до перехода сервиса (результат 1.2)
|
||||
- **THEN** у строк `line_status = ok`, `row_index` по порядку, `source_ref = null`, у значений `text = string_value`,
|
||||
`issues` содержат прежние предупреждения маппинга, экран работает
|
||||
|
||||
### Requirement: Миграция DBML v5.19
|
||||
Одна миграция Alembic SHALL приводить схему к DBML v5.19 и откатываться; заполнение существующих данных:
|
||||
`row_index` строк — по возрастанию `line_code` внутри формы с 0, `line_status = ok`, `needs_review` строк — `false`,
|
||||
`needs_review` значений — по прежнему `values_ok`, `source_text = string_value`, `is_confirmed = false`,
|
||||
`period_kind` — `absolute` при `period_date`, иначе `none`, `document.file_ordinal` — по `(created_at, id)` внутри
|
||||
файла с 0; `related_group.crm_id` — `null`. Сверка `check_dbml_vs_orm.py` — против `data-model-v5.19.dbml`.
|
||||
|
||||
#### Scenario: Цикл миграций
|
||||
- **WHEN** `alembic upgrade head` → `downgrade base` → `upgrade head` на чистой БД
|
||||
- **THEN** все три шага проходят, сверка ORM с DBML v5.19 — без расхождений
|
||||
|
||||
### Requirement: `crm_id` ГСК/ГСЗ
|
||||
`related_group.crm_id` (`varchar`, nullable, уникален) SHALL сохраняться в `POST …/related-groups` и
|
||||
`PATCH /directories/related-groups/{id}` и отдаваться в `RelatedGroupOut` / `RelatedGroupDetail`; повтор — 409
|
||||
`CRM_ID_EXISTS`; изменение — аудит `GROUP_CHANGE` с `before/after` (arm-core#112). Импорт ЕЦП `crm_id` групп не
|
||||
заполняет (OQ-91).
|
||||
|
||||
#### Scenario: Занятый crm_id
|
||||
- **WHEN** создаётся ГСЗ с `crm_id`, который уже есть у другой ГСК или ГСЗ
|
||||
- **THEN** 409 `CRM_ID_EXISTS`, ничего не создано
|
||||
@ -0,0 +1,46 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Раскладка строк формы со статусами
|
||||
Экран проверки формы ББ/ОФР ([41]) SHALL сохранять раскладку «как на бумаге» (Р-14): разделы, внутри — строки `ok` по
|
||||
возрастанию кода, несколько строк одного кода — в порядке файла (`row_index`), шаблонные строки на своих местах,
|
||||
пятизначные подстроки под родителем. Строка `unreadable` / `missing` SHALL стоять под ближайшей предыдущей по
|
||||
`row_index` строкой, размещённой по коду, а без такой — в начале первого раздела; она подсвечена целиком, в ячейке
|
||||
кода серым — сырой текст `line_code_text` с подсказкой. Строки `detail` SHALL быть скрыты под переключателем
|
||||
«Показать расшифровки (N)» и показываться под родителем (`parent_row_index`) с отступом. Экран показывает справочное
|
||||
имя строки (`reference_name`), файловое (`line_name`) — рядом, если отличается. Переключателя «как в файле» нет.
|
||||
|
||||
#### Scenario: Код-мусор на своём месте
|
||||
- **WHEN** строка `unreadable` «ИЧ,250» в файле стоит после строки 1240
|
||||
- **THEN** на экране она стоит сразу под 1240, подсвечена, в ячейке кода серым «ИЧ,250»
|
||||
|
||||
#### Scenario: Расшифровки скрыты
|
||||
- **WHEN** у формы три строки `detail`
|
||||
- **THEN** по умолчанию их не видно, переключатель «Показать расшифровки (3)»; включённый показывает их под родителями
|
||||
|
||||
### Requirement: Подсветка и предупреждения у места
|
||||
Экран SHALL подсвечивать ячейки только по `needs_review` (в подсказке — `text` и уверенность), строки — по
|
||||
`needs_review` строки, поля шапки — по `needs_review_fields`. Предупреждения и нарушения (`issues`, `checks`) SHALL
|
||||
показываться у места: у строк (`line_ids`), ячеек (`line_ids` + `column_id`), полей шапки (`field`) или у документа;
|
||||
уровень `error` отличается от `info`. Подсветки по `confidence < 100` и `is_recognized = false` напрямую нет.
|
||||
|
||||
#### Scenario: Догадка подтверждена проверкой
|
||||
- **WHEN** значение `is_recognized = false`, `needs_review = false`
|
||||
- **THEN** ячейка не подсвечена; в подсказке сырой текст и уверенность
|
||||
|
||||
### Requirement: Правка строк, колонок и подтверждение в режиме «Редактировать»
|
||||
В режиме «Редактировать» экран SHALL давать: у `unreadable` / `missing` — поле ввода кода с подсказкой из справочника
|
||||
(код → справочное имя) и пункт меню строки «Это расшифровка»; у `detail` — «Назначить код»; у строки `ok` — «Сделать
|
||||
расшифровкой», смену кода и «Подтвердить код» у строки с `needs_review`; подтверждение ячейки без правки; ввод даты у
|
||||
колонки без даты (`period_kind = none`) или с предупреждением о дате. «Сохранить» отправляет один `PATCH …/statement`
|
||||
(`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами
|
||||
SHALL быть отключена с подсказкой «Разберите строки без кода: N» и переходом к первой такой строке; ответ сервера 409
|
||||
`LINES_UNRESOLVED` показывается тем же текстом, 409 `DUPLICATE_FORM` — со ссылкой на подтверждённую форму, 409
|
||||
`LINE_EXISTS` — у поля кода.
|
||||
|
||||
#### Scenario: Назначить код
|
||||
- **WHEN** аналитик вводит 1250 в ячейку кода строки `unreadable` и сохраняет
|
||||
- **THEN** уходит `PATCH` с `lines: [{line_id, line_code: "1250"}]`; после ответа строка встаёт на место 1250
|
||||
|
||||
#### Scenario: Подтверждение заблокировано
|
||||
- **WHEN** у формы две строки `missing` с суммами
|
||||
- **THEN** «Подтвердить данные» отключена, подсказка «Разберите строки без кода: 2», клик по подсказке ведёт к первой
|
||||
@ -0,0 +1,129 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Сообщение результата распознавания
|
||||
Тело сообщения на subject ответа (настройка окружения `NATS_RESULT_SUBJECT`, по умолчанию `files.tabular_parsed`)
|
||||
SHALL соответствовать `docs/contracts/recognition-result-message.schema.json`
|
||||
v2.0: обязательные поля `schema_version` (`"2.0"`), `job_id`, `file_id`, `status` (`processing` | `done` | `needs_check` |
|
||||
`error`), `service {name, version}`; при `status ∈ {done, needs_check}` обязательны `result_s3 {bucket, key}` и
|
||||
`result_schema_version`; при `status = error` обязателен `error {code, message}`; опциональны `raw_s3_key`
|
||||
(только `scan-gateway`) и `metrics`. `metrics.warnings[]` — структуры `warning` (как в файле результата), не строки.
|
||||
Поле `group_schema` в конверт NOT включается — источник `recognition_job.group_schema` — файл результата.
|
||||
Производитель v2.0 SHALL писать в конверт тот же MAJOR, что и в файл результата.
|
||||
|
||||
#### Scenario: Успешный результат
|
||||
- **WHEN** сервис завершил обработку файла
|
||||
- **THEN** файл результата записан в S3 по ключу `results/{file_id}/{job_id}/result.json`, а затем опубликован конверт
|
||||
со `status = done | needs_check`, `result_s3` указывает на этот файл, `result_schema_version = "2.0"`, заголовок
|
||||
`Nats-Msg-Id` = `result:<job_id>:<attempt>`
|
||||
|
||||
#### Scenario: Ошибка обработки
|
||||
- **WHEN** сервис не смог обработать файл (нечитаем, таймаут внешней системы, тип не поддерживается)
|
||||
- **THEN** публикуется конверт со `status = error` и `error.code` из перечня схемы (`UNSUPPORTED_FORMAT`, `UNREADABLE`,
|
||||
`TIMEOUT`, `SCHEMA_MISMATCH`, `NOT_IMPLEMENTED`, `FILE_READ_ERROR`, `INVALID_FORMAT`, `INVALID_DOCUMENT`,
|
||||
`RECOGNITION_FAILED`, `UNKNOWN_ERROR`); `result_s3` может отсутствовать
|
||||
|
||||
#### Scenario: Предупреждения в конверте
|
||||
- **WHEN** у файла есть предупреждение уровня файла (ошибочный json набора ИС)
|
||||
- **THEN** `metrics.warnings` конверта содержит объект `{code: "JSON_FAILED", severity: "error", message, locator}`
|
||||
|
||||
### Requirement: Файл результата
|
||||
Файл результата SHALL соответствовать `docs/contracts/recognition-result.schema.json` v2.0: один файл (`stored_file`)
|
||||
→ один `job_id` → массив `documents` из `osvDocument` и/или `statementDocument`; денежные значения — строки
|
||||
`^-?\d+(\.\d{1,2})?$`; `confidence` — 0–100. Пустой массив `documents` допустим только при `status = error`.
|
||||
`service.statements_version` — коммит копии общего кода `arm_statements`, которым посчитан результат (обязателен при
|
||||
наличии `statementDocument`). `metrics.warnings[]` — предупреждения уровня файла (структура `warning` с `locator`).
|
||||
|
||||
Каждый документ (и `osvDocument`, и `statementDocument`) SHALL иметь `status` (`done` | `needs_check`) и `warnings[]` —
|
||||
структуры `{code, severity, message, line_refs?, column_index?, field?}`; у `osvDocument` все предупреждения — `info`,
|
||||
`status = done`, когда ведомость разобрана. Остальное в `osvDocument` — как в v1.2 (`extra`, `period_start`,
|
||||
`period_end`, `is_from_year_start`).
|
||||
|
||||
`statementDocument` SHALL иметь `form_variant` (`full` | `simplified` | `null`); `header` — поля `organization_name`,
|
||||
`inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`, `okfs`, `address` и для каждого `<поле>_text` и
|
||||
`<поле>_confidence`, плюс `needs_review_fields[]`; `columns[]` — `column_index`, `label`, `label_confidence`,
|
||||
`period_date`, `period_kind` (`absolute` | `relative:current` | `relative:prev_year_end` |
|
||||
`relative:prev_prev_year_end` | `relative:prev_period` | `none`); `lines[]` — `row_index`, `ref`, `line_status` (`ok` |
|
||||
`unreadable` | `missing` | `detail`), `line_code` (строка 4–5 цифр или `null`; `null` ⇔ статус не `ok`),
|
||||
`line_code_text`, `line_code_confidence`, `line_name` (строка или `null`), `line_name_confidence`, `parent_ref` (у
|
||||
`detail`), `needs_review`, `values[]` — `column_index`, `amount`, `string_value`, `text`, `confidence`,
|
||||
`is_recognized`, `needs_review`. `checks` SHALL присутствовать у `BALANCE` / `INCOME` со строками и отсутствовать у
|
||||
документа без строк; `relations_failed` и `soft_relations_failed` — `{column_index, relation, codes[], expected,
|
||||
actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `INCOME` | `DISCLOSURE` | `OTHER`. `ref`
|
||||
уникален в документе.
|
||||
|
||||
#### Scenario: Результат валидируется примерами
|
||||
- **WHEN** CI сервиса запускает тесты контракта
|
||||
- **THEN** все файлы `docs/contracts/examples/result-*.json` проходят схему файла результата v2.0, а все
|
||||
`request-*.json` / `result-message-*.json` — схемы сообщений
|
||||
|
||||
#### Scenario: Лишняя колонка сохранена
|
||||
- **WHEN** в ОСВ между ИНН и суммами есть колонка «КПП»
|
||||
- **THEN** у строк контрагентов `extra = {"КПП": "<значение>"}`, файл проходит схему v2.0
|
||||
|
||||
#### Scenario: Форма из Excel с проверками
|
||||
- **WHEN** tabular-parser разобрал книгу с балансом на двух листах, ОФР со строкой без кода под 2110 и пояснениями
|
||||
(`examples/result-statement-excel.json`)
|
||||
- **THEN** файл проходит схему v2.0: `BALANCE` с `locator` из двух листов, `status`, `checks` и мягким нарушением;
|
||||
`INCOME` со строкой `detail` (`line_code = null`, `parent_ref`); `DISCLOSURE` без `columns`/`lines` и без `checks`
|
||||
|
||||
#### Scenario: Форма из OCR с сомнительными значениями
|
||||
- **WHEN** scan-gateway разобрал ББ, где одна сумма собрана из грязного текста и покрыта соотношением, а одна строка
|
||||
`unreadable` (`examples/result-statement-minimal.json`)
|
||||
- **THEN** файл проходит схему v2.0; у покрытого значения `is_recognized = false`, `needs_review = false`, `text` —
|
||||
сырой; у строки `unreadable` `line_code = null`, `line_code_text` — сырой текст; документ `needs_check`
|
||||
|
||||
#### Scenario: Границы периода ОСВ
|
||||
- **WHEN** в шапке ОСВ период «Октябрь 2024 г. – Сентябрь 2025 г.»
|
||||
- **THEN** `period_start = "2024-10-01"`, `period_end = "2025-09-30"`, `is_from_year_start = false`; документ ОСВ с
|
||||
`status` и `warnings`, файл проходит схему v2.0
|
||||
|
||||
#### Scenario: Строка без кода с кодом в поле
|
||||
- **WHEN** у строки `line_status = unreadable` передан непустой `line_code`
|
||||
- **THEN** файл не проходит схему v2.0
|
||||
|
||||
### Requirement: Версионирование контракта
|
||||
Поле `schema_version` SHALL быть строкой `MAJOR.MINOR`. Добавление необязательных полей или значений enum —
|
||||
`MINOR` (обратно совместимо); удаление/переименование полей, смена типов или семантики — `MAJOR`.
|
||||
Сервис SHALL отвергать запрос с неизвестным `MAJOR` (`error.code = SCHEMA_MISMATCH`); ядро SHALL принимать результат
|
||||
с известным `MAJOR` и любым известным или новым `MINOR`. Запрос `recognition-request` остаётся v1.0; файл результата и
|
||||
конверт — v2.0 (`MAJOR` поднят: `line_code` стал nullable, `metrics.warnings` и `document.warnings` — структуры, удалён
|
||||
`checks.duplicate_line_codes`, изменён смысл `is_recognized` у OCR).
|
||||
|
||||
#### Scenario: Несовместимая версия запроса
|
||||
- **WHEN** сервис, поддерживающий запрос `1.x`, получает запрос `schema_version = "2.0"`
|
||||
- **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH` **без ack `processing`**: ответом на
|
||||
запрос служит сам финальный конверт, ядро закрывает им задание (`published → error`), и повторной доставки запроса не
|
||||
будет. Конверт публикуется только если из тела извлекаются корректные `job_id` и `file_id`; иначе — как для нечитаемого
|
||||
запроса (ответа нет, ядро закроет задание по `NO_ACK` / `dlq`). `attempt` для `Nats-Msg-Id` берётся из тела запроса; при его
|
||||
отсутствии реализации расходятся (`tabular-parser` берёт `attempt` из заголовка `Nats-Msg-Id` запроса или не отвечает,
|
||||
`scan-gateway` подставляет 1) — см. OQ-71
|
||||
|
||||
#### Scenario: Ядро получает MAJOR, которого не знает
|
||||
- **WHEN** ядро, знающее `1.x` и `2.x`, получает конверт или файл результата `schema_version = "3.0"`
|
||||
- **THEN** результат не проходит проверку схемы, файл получает `ERROR` с причиной в `metrics.mapping_error`, повтор не
|
||||
выполняется (как для любого результата не по схеме)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Переход на контракт v2.0 — «сначала ядро»
|
||||
Ядро SHALL до перехода любого сервиса принимать конверты (включая ack `processing`) и файлы результата и `1.x`, и
|
||||
`2.0`, с двумя мапперами файла результата. При приёме `1.x` статус документа формы SHALL считаться по прежнему правилу
|
||||
D-6, а поля v2.0 заполняться по умолчанию: `line_status = ok`, `row_index` — по порядку строк в результате, `ref =
|
||||
null`, `text = string_value`, `needs_review` значения — по прежнему `values_ok` (`is_recognized` не `false` и
|
||||
`confidence` 100 или пуста), `needs_review` строки — `false`, `period_kind` — `absolute` при `period_date`, иначе `none`,
|
||||
`form_variant = null`, предупреждения-строки — `warning` `{code: "LEGACY_WARNING", severity: "info", message}`.
|
||||
Сервисы SHALL переходить на `2.0` только после выкладки ядра, принимающего `2.0`. Маппер `1.x` удаляется после
|
||||
перехода обоих сервисов отдельным изменением.
|
||||
|
||||
#### Scenario: Сервис ещё на v1.2
|
||||
- **WHEN** после выкладки ядра tabular-parser присылает результат `1.2` с повтором кода и `checks.duplicate_line_codes`
|
||||
- **THEN** ядро принимает его маппером `1.x`, статус документа — по D-6, файл не `ERROR`
|
||||
|
||||
#### Scenario: Ack нового сервиса
|
||||
- **WHEN** scan-gateway после перехода публикует ack `processing` со `schema_version = "2.0"`
|
||||
- **THEN** ядро переводит задание в `processing`, как для ack `1.0`
|
||||
|
||||
#### Scenario: Порядок нарушен
|
||||
- **WHEN** сервис прислал результат `2.0` с `line_code = null` ядру, знающему только `1.x`
|
||||
- **THEN** результат не проходит проверку схемы, файл получает `ERROR` (`metrics.mapping_error`) — поэтому порядок
|
||||
«сначала ядро» обязателен
|
||||
@ -0,0 +1,41 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Адаптер json ИС шага 1
|
||||
scan-gateway SHALL оставить у себя обмен с ИС Сканирование (горячие папки, опрос, срок, склейку набора json в
|
||||
`raw/{job_id}.json` до нормализации, очистку папки) и адаптер «таблица `table_structure[i]` json → `FormCandidate`»;
|
||||
шаги 2–8 SHALL выполняться копией пакета `arm_statements`. Адаптер SHALL: создавать кандидата на каждую таблицу каждого
|
||||
успешного json; копировать шапку json (`organization`, `inn`, `units`, `date`) в шапку каждой своей таблицы как `Cell`
|
||||
(`text = form_name`, `confidence` ИС); передавать `label.form_name` таблицы как заголовок в `evidence` (тип — общим
|
||||
правилом шага 1 по словарю заголовков, `statement_labels.yaml` уходит в словарь пакета); строить плотную сетку по
|
||||
`row_index` и `column_index` ИС (пустые позиции — `null`), `Cell.ref = row:<row_index>`; `locator =
|
||||
file:<имя json>;table:<i>`. `value` у OCR-ячеек всегда `null`.
|
||||
|
||||
#### Scenario: Один json — ББ и ОФР
|
||||
- **WHEN** json `aep_3.json` содержит две таблицы — «Бухгалтерский баланс» и «Отчет о финансовых результатах» — под
|
||||
общей шапкой
|
||||
- **THEN** два кандидата с `locator` `file:aep_3.json;table:0` и `…;table:1`, у каждого копия шапки json
|
||||
|
||||
### Requirement: Ошибки набора json
|
||||
Ошибочный json набора (`parse_error`, непустой `error.code`, корень не объект) при хотя бы одном успешном соседе SHALL
|
||||
давать предупреждение файла `JSON_FAILED` (`error`, `locator = file:<имя json>`, в `message` — код и сообщение ИС);
|
||||
все json ошибочны — конверт `error` с кодом первого. Исключение при разборе таблицы — предупреждение `TABLE_PARSE_ERROR`
|
||||
(`error`) документа-кандидата, остальные таблицы разбираются; ни одной таблицы не разобрано — `error
|
||||
RECOGNITION_FAILED`.
|
||||
|
||||
#### Scenario: Потерянный ОФР (решение № 28 среза 3a)
|
||||
- **WHEN** `<job_id>.json` успешен (ББ), а `<job_id>_ofr.json` вернул `INVALID_DOCUMENT`
|
||||
- **THEN** документ ББ получает свой статус; в `metrics.warnings` — `JSON_FAILED` уровня `error`; конверт
|
||||
`needs_check`; ядро не автоподтверждает документы файла
|
||||
|
||||
### Requirement: Что уходит из шлюза
|
||||
scan-gateway SHALL удалить `api/rules/statement_lines.yaml` и подстановку наименований по справочнику, срезание
|
||||
уверенности кода до 99 по справочнику, выбор редакции для имён (`_choose_edition`), пропуск строк с нечисловым и пустым
|
||||
кодом со счётчиками, правило «оставлена первая строка» при повторе кода, синтез шапки, правило «ячейки под колонкой
|
||||
нет → `confidence = 0`, не распознано», привязку `is_recognized` к `confidence` и правило «любое предупреждение →
|
||||
`needs_check`». Модули `normalizer/ocr/text|dates|amounts` переезжают в общие разборы пакета, `layout|rows|header|
|
||||
document` — в шаги 2–6 пакета; `ocr_lookalikes.yaml` — в словарь пакета.
|
||||
|
||||
#### Scenario: Строки без кода в aep_3
|
||||
- **WHEN** в `aep_3.json` строки «БАЛАНС» и «Выручка» имеют пустой код, а в колонке значения мусор `211(1 |`
|
||||
- **THEN** строки попадают в результат со статусами (`detail` под строкой с кодом, `missing` в начале таблицы) и сырым
|
||||
текстом; счётчиков «строк без кода с суммами пропущено» нет
|
||||
@ -0,0 +1,530 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Пакет общего кода `arm_statements` и его копии
|
||||
Общий код нормализации ББ/ОФР SHALL жить в репозитории `arm-statements` пакетом `arm_statements/` с `pyproject`,
|
||||
своими тестами и конфигом линтеров. Пакет SHALL содержать: типы шага 1 (`FormCandidate`, `Cell`; сериализуемые в
|
||||
json dataclass-ы), шаги 2–8, общие разборы текста (двойники, даты, суммы, коды), словари — проверок §7
|
||||
`statement_checks.yaml` (полный и упрощённый наборы), периодов `statement_periods.yaml`, заголовков форм и ОКУД,
|
||||
подписей колонок («Код», «Наименование», исключения), букв-двойников, упорядоченные перечни кодов форм по редакциям
|
||||
(`66n`, `fsbu4`) и вариантам (`full`, `simplified`) без наименований, диапазоны кодов — и загрузчик словарей. Словаря
|
||||
«код → наименование» в пакете MUST NOT быть (Р-3). Пакет SHALL владеть своими типами входа и выхода; конвертацию
|
||||
выхода в модель контракта делает хост. Golden-тесты пакета — внутри пакета на синтетических и обезличенных данных;
|
||||
тесты на реальных фикстурах остаются в сервисах и соседние репозитории не читают.
|
||||
|
||||
В tabular-parser, scan-gateway и arm-core пакет SHALL лежать копией вместе с тестами и конфигом линтеров, подключённым
|
||||
к `pytest` и `ruff` хоста как include, и импортироваться по имени `arm_statements`. Копию SHALL раскладывать и
|
||||
проверять скрипт `sync-statements` репозитория docs (по образцу `sync-web.sh`): `--check` возвращает код 20, если
|
||||
копия отличается от источника или правилась напрямую; скрипт вызывается из `make check-full` потребителя и MUST NOT
|
||||
вызываться из `make check` и пайплайна платформы. Маркер коммита источника копии SHALL попадать в результат как
|
||||
`service.statements_version`.
|
||||
|
||||
#### Scenario: Прямая правка копии
|
||||
- **WHEN** в `tabular-parser` изменён файл копии пакета, а источник в `arm-statements` — нет
|
||||
- **THEN** `sync-statements --check` завершается с кодом 20 и называет файл; `make check` сервиса этого не проверяет
|
||||
|
||||
#### Scenario: Версия общего кода в результате
|
||||
- **WHEN** сервис собирает результат копией пакета, разложенной из коммита `abc1234` репозитория `arm-statements`
|
||||
- **THEN** в результате `service.statements_version = "abc1234"`
|
||||
|
||||
### Requirement: Шаг 1 — кандидат в форму
|
||||
Каждый адаптер шага 1 SHALL выдавать на файл список `FormCandidate`, и только с ним работают шаги 2–8. Поля
|
||||
кандидата: `source` (`ocr` | `excel`); `locator` (`file:<имя json>;table:<i>` у OCR, `sheet:<A>[,<B>…]` у Excel);
|
||||
`type_hint` (`BALANCE` | `INCOME` | `DISCLOSURE` | `null`); `evidence` (ОКУД и заголовок формы как `Cell`); `header`
|
||||
(поля `organization_name`, `inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`, `okfs`, `address` — каждое `Cell`
|
||||
или `null`); `rows` — плотная сетка строк × ячеек, все страницы и листы формы уже в одной сетке, пустая позиция —
|
||||
`null`; `segment_breaks` — номера строк сетки, с которых начинается новый лист Excel (у OCR пусто); `warnings`.
|
||||
|
||||
`Cell` SHALL быть одной для шапки и таблицы: `text` — как показывает источник; `value` — число или дата, если
|
||||
источник их знает (у OCR всегда `null`; у Excel — значение по числовому формату ячейки, tp#42); `confidence` 0–100
|
||||
(у Excel всегда 100); `ref` — позиция в источнике (OCR — `row:<row_index ИС>`, Excel — `<лист>!<A1>`).
|
||||
|
||||
`type_hint` SHALL вычисляться одним правилом у обоих адаптеров: ОКУД по словарю (0710001 → `BALANCE`, 0710002 →
|
||||
`INCOME`, прочие 0710xxx → `DISCLOSURE`) → иначе заголовок по словарю заголовков (у OCR заголовок — `label` таблицы
|
||||
ИС; «Пояснения», «Расшифровка» → `DISCLOSURE`) → иначе `null`.
|
||||
|
||||
#### Scenario: Excel-ячейка с числовым форматом
|
||||
- **WHEN** в Excel ячейка хранит 106311116.46 с форматом `#,##0,`
|
||||
- **THEN** у `Cell` `text = "106 311"`, `value = 106311`, `confidence = 100`, `ref = "Баланс!D27"` (лист и адрес
|
||||
ячейки)
|
||||
|
||||
#### Scenario: OCR-ячейка
|
||||
- **WHEN** в таблице ИС ячейка строки `row_index = 8` содержит `"[190 :"` с уверенностью 25
|
||||
- **THEN** у `Cell` `text = "[190 :"`, `value = null`, `confidence = 25`, `ref = "row:8"`
|
||||
|
||||
#### Scenario: Тип по ОКУД против заголовка
|
||||
- **WHEN** у блока формы ОКУД 0710002, а заголовок «Бухгалтерский баланс»
|
||||
- **THEN** `type_hint = INCOME`; окончательный тип выбирается по кодам на шаге 6
|
||||
|
||||
### Requirement: Шаг 1 — шапка кандидата
|
||||
У каждого кандидата SHALL быть своя шапка. Текст каждого поля разбирается общим кодом по `Cell.text`: ИНН — только
|
||||
цифры, без замены двойников, длина 10 или 12, иначе `null`; дата отчёта — словарём периодов (тот же, что для подписей
|
||||
колонок, шаг 2); единицы — словарём единиц. Сырой текст и уверенность каждого поля SHALL доезжать до контракта
|
||||
(`<поле>_text`, `<поле>_confidence`).
|
||||
|
||||
#### Scenario: ИНН с мусором клеток
|
||||
- **WHEN** поле ИНН шапки OCR — `"3|1 21 2|"` с цифрами ИНН между вертикальными чертами
|
||||
- **THEN** ИНН собирается только из цифр, `|` единицей не считается; `inn_text` хранит исходный текст
|
||||
|
||||
### Requirement: Очистка OCR-текста — первая стадия типовых разборов
|
||||
Очистка OCR SHALL выполняться не отдельным шагом, а первой стадией разборов даты, подписи колонки, кода строки,
|
||||
разлепления склеенной строки и суммы — для любой ячейки без `value` (у Excel — только текстовые ячейки). Буквы-
|
||||
двойники SHALL заменяться в датах, подписях, кодах и суммах (Р-11): `О/о/O/o → 0`, `З/з → 3`, `б → 6`, `S/s → 5`,
|
||||
`I/l/|/і → 1`, `Ч → 4` — только вплотную к цифре, в несколько проходов; слова не портятся. В суммах крайние `|`
|
||||
(след границы таблицы) SHALL убираться, а не заменяться. ИНН двойники MUST NOT трогать. Сумма с заменой двойника
|
||||
получает `is_recognized = false`; код с заменой считается неуверенным. Текст длиннее 200 символов разбором даты не
|
||||
обрабатывается (`null`).
|
||||
|
||||
#### Scenario: Двойник в сумме
|
||||
- **WHEN** ячейка суммы OCR — `"1 2О5"` с уверенностью 100
|
||||
- **THEN** `amount = 1205`, `is_recognized = false`, `text = "1 2О5"`
|
||||
|
||||
#### Scenario: Двойник в коде
|
||||
- **WHEN** ячейка кода — `"I1О0"` с уверенностью 100
|
||||
- **THEN** код разбирается как `1100` и считается неуверенным (подтверждается структурой, шаг 8)
|
||||
|
||||
#### Scenario: Граница таблицы в сумме
|
||||
- **WHEN** ячейка суммы — `"| : 122477"`
|
||||
- **THEN** крайняя `|` удаляется, а не становится единицей; сумма 122477, `is_recognized = false`
|
||||
|
||||
### Requirement: Шаг 1 — многостраничная форма
|
||||
Одна форма SHALL быть одной сеткой на выходе шага 1. Строки продолжения дописываются в конец сетки по маркеру
|
||||
«Форма … с.2» или по позиции листа; граница листа записывается в `segment_breaks`. Тип из маркера продолжения SHALL
|
||||
сверяться с `type_hint` формы: расхождение — отдельный кандидат и предупреждение `CONTINUATION_TYPE_CONFLICT`
|
||||
(`error`). Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы и, при трёх и
|
||||
более кодах, число колонок сумм совпадает с формой; иначе — отдельный кандидат и предупреждение
|
||||
`SHEET_NOT_JOINED` (`error`).
|
||||
|
||||
#### Scenario: Лист-продолжение с одной строкой
|
||||
- **WHEN** после листа ОФР идёт лист без шапки с единственной строкой 2400
|
||||
- **THEN** лист присоединяется к ОФР (код в диапазоне формы, кодов меньше трёх — число колонок не сверяется), граница
|
||||
листа — в `segment_breaks`
|
||||
|
||||
#### Scenario: Лист другой ширины с тремя кодами
|
||||
- **WHEN** лист без маркера и шапки содержит три кода ББ и четыре колонки сумм при трёх у открытой формы
|
||||
- **THEN** лист — отдельный кандидат, предупреждение `SHEET_NOT_JOINED` уровня `error`
|
||||
|
||||
### Requirement: Диапазоны кодов и разбор кода
|
||||
Словарь диапазонов кодов SHALL содержать отрезки: ББ 1100–1799, ОФР 2100–2999, отчёт об изменениях капитала
|
||||
3100–3699, отчёт о движении денежных средств 4100–4499, пояснения 5000–5999, отчёт о целевом использовании
|
||||
6100–6499. Кодовые диапазоны для поиска столбца — объединение отрезков. Разбор кода: убрать пробелы и переносы →
|
||||
заменить двойники (`|` → 1 только если результат попадает в кодовый диапазон) → убрать краевую пунктуацию → 4–5 цифр
|
||||
(допускается хвост `.0`), первые четыре — в кодовом диапазоне. Многострочная ячейка — код, только если каждая её часть
|
||||
код. «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца.
|
||||
|
||||
#### Scenario: Год и сумма — не код
|
||||
- **WHEN** в ячейке «2024» или четырёхзначная сумма «3450»
|
||||
- **THEN** кодом это не считается (вне отрезков)
|
||||
|
||||
#### Scenario: Краевая пунктуация и черта
|
||||
- **WHEN** ячейка кода — `"1220 ,"` или `"| 140"`
|
||||
- **THEN** коды `1220` и `1140`
|
||||
|
||||
### Requirement: Шаг 2 — столбец кодов
|
||||
Столбец кодов SHALL находиться одним правилом: (а) столбец с подписью из словаря подписей кода («код», «коды», «код
|
||||
строки», «код показателя» — после нормализации регистра и пунктуации) над первой строкой с кодом, если под подписью
|
||||
доля кодов среди непустых ячеек ≥ 0,6 или кодов ≥ 3; при нескольких кандидатах — с наибольшим числом кодов; берётся
|
||||
последняя такая подпись над первой строкой с кодом; (б) иначе — столбец с наибольшим числом ячеек-кодов при кодах ≥ 3
|
||||
и доле ≥ 0,6; иначе предупреждение `CODE_COLUMN_NOT_FOUND`.
|
||||
|
||||
#### Scenario: Титульный блок 1С с подписью «Коды»
|
||||
- **WHEN** под подписью «Коды» титульного блока выгрузки 1С стоят ОКУД, дата, ОКПО, ИНН, ОКОПФ 12267, ОКФС, ОКЕИ
|
||||
(один код из девяти, доля 0,11), а коды строк формы — в другом столбце
|
||||
- **THEN** столбец под «Коды» не выбирается; столбец кодов — по правилу (б)
|
||||
|
||||
#### Scenario: Документ из двух строк
|
||||
- **WHEN** форма содержит две строки с кодами под подписью «Код»
|
||||
- **THEN** столбец кодов найден (два из двух, доля 1,0)
|
||||
|
||||
### Requirement: Шаг 2 — сегменты, повторная шапка и границы листов
|
||||
Раскладка SHALL состоять из сегментов. Новый сегмент открывает строка-шапка (в ней есть ячейка из словаря подписей
|
||||
кода, или не менее двух её непустых ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница
|
||||
листа из `segment_breaks`. У сегмента своя раскладка: по его шапке; без шапки — столбец кодов по правилу шага 2, а если
|
||||
сегмент не первый и кодов в нём меньше трёх — ослабленный поиск: столбец с наибольшим числом кодов из диапазона
|
||||
столбца первого сегмента, минимум один. Колонки значений сегмента без шапки — столбцы правее кода, где в строках с
|
||||
кодами есть числа; они сопоставляются с колонками первого сегмента по порядку; число не совпало — предупреждение
|
||||
`SEGMENT_COLUMNS_MISMATCH` (`error`) и значения по позиции. Столбец кодов не найден ни так, ни так — предупреждение
|
||||
`SEGMENT_WITHOUT_CODES` (`error`) сегмента; строки сегмента не теряются (роль `data`, статусы по шагу 3).
|
||||
|
||||
#### Scenario: Лист «_2» НПЗ со столбцом кодов в другой колонке
|
||||
- **WHEN** второй лист формы без шапки содержит единственную строку 2400, и код стоит не в той колонке, что на первом
|
||||
листе
|
||||
- **THEN** у сегмента свой столбец кодов (ослабленный поиск), значения строки 2400 не сдвинуты
|
||||
|
||||
#### Scenario: Повторная шапка на второй странице скана
|
||||
- **WHEN** в таблице ИС посередине стоит повторная строка «Наименование показателя | Код | На 30.09.2025 | …»
|
||||
- **THEN** с неё начинается новый сегмент со своей раскладкой; строка-шапка в данные не попадает
|
||||
|
||||
### Requirement: Шаг 2 — строка заголовка, нумерация и роли строк
|
||||
Строка заголовка SHALL быть последней над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе
|
||||
последней над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом);
|
||||
двухстрочный заголовок склеивается. Строка нумерации — строка, где не меньше трёх непустых ячеек, каждая — целое число,
|
||||
и числа идут подряд, начиная с 1, слева направо — SHALL получать роль `numbering` и в данные не попадать. Роль
|
||||
`title` — только строки выше найденной строки заголовка; в сегменте без заголовка роли `title` нет. Заголовка нет ни у
|
||||
одного сегмента документа → колонки значений первого сегмента — столбцы правее кода, где в строках с кодами числа,
|
||||
`label = ""`, `period_end = null`, `period_kind = none`, одно предупреждение `HEADER_ROW_NOT_FOUND` (`error`).
|
||||
Заголовок есть хотя бы у одного сегмента — остальные сегменты берут его колонки без предупреждения.
|
||||
|
||||
#### Scenario: Нумерация в скане (AURORA-1517)
|
||||
- **WHEN** под шапкой таблицы ИС строка «1 | 2 | 3 | 4 | 5»
|
||||
- **THEN** роль `numbering`, строки данных из неё нет
|
||||
|
||||
#### Scenario: Листы 2–5 ББ без шапки
|
||||
- **WHEN** у первого листа ББ заголовок есть, у листов 2–5 — нет
|
||||
- **THEN** листы 2–5 берут колонки первого, предупреждения о заголовке нет, документ может быть `done`
|
||||
|
||||
### Requirement: Шаг 2 — подпись и дата колонки
|
||||
Подпись колонки SHALL разбираться одним словарём периодов `statement_periods.yaml`, объединяющим словарь
|
||||
tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dates.py::test_parses_real_forms` scan-gateway —
|
||||
обязательные тесты словаря. Результат — `period_end` и `period_kind`:
|
||||
- абсолютная подпись («На 31 марта 2025 г.», «31.12.2024», «30/09/25», «30092025», «За 9 месяцев 2025 г.», «с
|
||||
01.01.2025 по 31.03.2025», «2024», хвост «0:00:00», клетки «2|0|2|4|») → `absolute`;
|
||||
- относительная: «На отчётную дату», «За отчётный период» → `relative:current` (= эффективная дата); «На 31 декабря
|
||||
предыдущего года» → `relative:prev_year_end` (31.12 года эффективной даты − 1); «На 31 декабря года,
|
||||
предшествующего предыдущему» → `relative:prev_prev_year_end` (31.12 года − 2); «За аналогичный период предыдущего
|
||||
года» → `relative:prev_period` (эффективная дата − 1 год, 29 февраля → 28 февраля);
|
||||
- подписи нет → `none`.
|
||||
|
||||
Эффективная дата документа SHALL быть датой отчёта из шапки, иначе самой поздней абсолютной датой колонок; нет ни
|
||||
той, ни другой → у относительных колонок `period_end = null`, `period_kind` остаётся относительным.
|
||||
|
||||
Подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») SHALL пропускаться с
|
||||
предупреждением `COLUMN_SKIPPED` (`info`). Подпись, не разобранная ни как период, ни как исключение, при числах в
|
||||
столбце в строках с кодами SHALL оставлять колонку с подписью как есть, `period_end = null`, `period_kind = none` и
|
||||
предупреждением `COLUMN_WITHOUT_DATE` (`error`, `column_index`); без чисел — пропуск. Подпись OCR с уверенностью ниже
|
||||
100, разобранная в дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два
|
||||
года, 31 декабря предыдущего и предшествующего ему года; иначе предупреждение `COLUMN_DATE_UNCERTAIN` (`error`,
|
||||
`column_index`). Уверенность подписи — `label_confidence` (у Excel 100; подписи нет — `null`).
|
||||
|
||||
#### Scenario: Бланк 66н без впечатанных дат
|
||||
- **WHEN** подписи колонок ББ — «На отчетную дату», «На 31 декабря предыдущего года», «На 31 декабря года,
|
||||
предшествующего предыдущему», дата отчёта шапки 30.09.2025
|
||||
- **THEN** `period_end` колонок 2025-09-30, 2024-12-31, 2023-12-31, `period_kind` — `relative:current`,
|
||||
`relative:prev_year_end`, `relative:prev_prev_year_end`; предупреждений о дате колонок нет
|
||||
|
||||
#### Scenario: Мусорная подпись над числами
|
||||
- **WHEN** подпись колонки — `"| | г"` с уверенностью 17, а в колонке суммы
|
||||
- **THEN** колонка остаётся, `period_end = null`, `period_kind = none`, предупреждение `COLUMN_WITHOUT_DATE` уровня
|
||||
`error` с `column_index`, документ `needs_check`
|
||||
|
||||
#### Scenario: Колонка «Изменение»
|
||||
- **WHEN** правее периодов колонка «Изменение, %»
|
||||
- **THEN** колонка пропускается, предупреждение `COLUMN_SKIPPED` уровня `info`
|
||||
|
||||
### Requirement: Шаг 2 — столбец наименования
|
||||
Столбец наименования SHALL быть столбцом с подписью, начинающейся с «наименование» (после нормализации), в строке
|
||||
заголовка; иначе — столбцом левее кода с наибольшим числом текстов, кроме столбца «Пояснения». Наименование строки —
|
||||
ячейка этого столбца; пустая — ближайшая текстовая ячейка левее кода, кроме «Пояснения».
|
||||
|
||||
#### Scenario: Наименование без подписи
|
||||
- **WHEN** у формы нет подписи «Наименование показателя», а левее кода два столбца — «Пояснения» (короткие номера) и
|
||||
тексты статей
|
||||
- **THEN** наименование берётся из столбца текстов статей
|
||||
|
||||
### Requirement: Шаг 3 — строки данных и их статусы
|
||||
Шаг 3 SHALL выдавать `RawLine` на каждую строку сетки с ролью `data`, у которой есть содержимое: код, число или
|
||||
непустой текст в колонке значений с уверенностью ниже 100. У строки — `row_index` (порядковый номер в документе с 0,
|
||||
по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные
|
||||
`row:<i>#<k>` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!<A1>`, без столбца кодов — первой непустой
|
||||
ячейки строки), ячейка кода, `line_status`, ячейка наименования, ячейки значений по колонкам сегмента, `parent_ref`.
|
||||
Статусы:
|
||||
- `ok` — код разобран;
|
||||
- `unreadable` — в ячейке кода есть хотя бы одна цифра, но код не разобран; ячейка кода без цифр считается пустой;
|
||||
- `detail` — строка без кода с содержимым под строкой с кодом (в том же или в предыдущих сегментах документа);
|
||||
`parent_ref` — `ref` ближайшей строки с кодом выше;
|
||||
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки с кодом; строки роли `title`
|
||||
сюда не попадают.
|
||||
|
||||
Строка без кода и без содержимого — не строка данных. Счётчиков отброшенных строк MUST NOT быть. Повтор кода — не
|
||||
статус и не предупреждение: все строки с кодом — `ok`, порядок как в файле.
|
||||
|
||||
#### Scenario: Код-мусор (AURORA-1462)
|
||||
- **WHEN** ячейка кода — `"ИЧ,250"`, в колонках суммы
|
||||
- **THEN** строка `unreadable` с `line_code_text = "ИЧ,250"`, суммами и `ref`; строка не пропадает
|
||||
|
||||
#### Scenario: Расшифровка «в том числе»
|
||||
- **WHEN** под строкой 1150 стоит строка «в том числе: Оборудование к установке» без кода с суммой
|
||||
- **THEN** строка `detail`, `parent_ref` = `ref` строки 1150
|
||||
|
||||
#### Scenario: Потерянный код в начале таблицы
|
||||
- **WHEN** первая строка данных «Выручка» без кода с суммами (код уехал в ячейку значения), выше строк с кодом нет
|
||||
- **THEN** строка `missing`
|
||||
|
||||
#### Scenario: Повтор кода (AURORA-1406)
|
||||
- **WHEN** код 2410 стоит у двух строк с разными наименованиями
|
||||
- **THEN** обе строки `ok`, порядок как в файле, предупреждения нет
|
||||
|
||||
### Requirement: Шаг 3 — разлепление склеенных строк
|
||||
До разбора кода SHALL выполняться разлепление: ячейка кода содержит N ≥ 2 частей через перевод строки, и каждая
|
||||
непустая ячейка значений — ровно N частей; разлепление делается, если хотя бы одна часть кода разобралась как код
|
||||
диапазона столбца. Получается N строк с `ref` `row:<i>#<k>`; нечитаемая часть кода — строка `unreadable`; наименование —
|
||||
k-я из непустых частей, если их тоже N, иначе целиком у каждой. Значения разлепленных строк получают
|
||||
`is_recognized = false`; разлепление — предупреждение `ROW_SPLIT` (`info`). Число частей не совпало или ни одна часть
|
||||
не код — одна строка, многострочное значение → `amount = null`, `string_value` = текст, `is_recognized = false`.
|
||||
|
||||
#### Scenario: Склейка «1500\n1700» (OQ-156)
|
||||
- **WHEN** ячейка кода — `"1500\n1700"`, суммы — `"917 775\n2 371 333"`
|
||||
- **THEN** две строки `ok` (`row:i#1`, `row:i#2`) с суммами 917775 и 2371333, `is_recognized = false`, предупреждение
|
||||
`ROW_SPLIT`; при сошедшихся соотношениях §7 значения не подсвечиваются (Р-17)
|
||||
|
||||
#### Scenario: Часть не из диапазона
|
||||
- **WHEN** в ББ ячейка кода — `"44 30\nI |"`
|
||||
- **THEN** строка не разлепляется (4430 вне диапазона ББ)
|
||||
|
||||
### Requirement: Шаг 4 — значение ячейки
|
||||
Шаг 4 SHALL выдавать `StatementValue {column_index, amount, string_value, text, confidence, is_recognized}`: `text` —
|
||||
всегда сырой текст источника; `string_value` — только когда `amount = null` и текст непуст и не прочерк. Правила:
|
||||
- `value` — число: `amount` квантуется до 2 знаков half-up; больше 16 целых цифр → `amount = null`, `string_value` =
|
||||
текст, `is_recognized = false`, предупреждение `VALUE_OUT_OF_RANGE` (`info`);
|
||||
- `value` — дата или логическое → `amount = null`, `string_value` = текст, `is_recognized = false`;
|
||||
- `value` нет → `parse_amount(text, source)`: двойники (Р-11), пробелы и тонкие пробелы, минус «−» (U+2212) как знак,
|
||||
скобки → минус, `}` и `]` — закрывающая скобка; прочерк (`-`, `–`, `—`, `−` без цифр) и пусто → `amount = null`,
|
||||
`is_recognized = true`; разделители: у `ocr` запятая — тысячи при шаблоне `[+-]?[1-9]\d{0,2}(,\d{3})+`, точка —
|
||||
тысячи при `[+-]?[1-9]\d*(\.\d{3})+`, иначе десятичный разделитель, три и более знаков после него → `amount = null`,
|
||||
`string_value` = текст; у `excel` запятая и точка — десятичный разделитель; пробел допустим только как разделитель
|
||||
тысяч (группы ровно по три цифры), иначе `amount` — цифры подряд («лучшая догадка», не длиннее 16 цифр, иначе
|
||||
`null`) и `is_recognized = false`; многострочное значение в строке с однострочным кодом → `amount = null`,
|
||||
`string_value` = текст, `is_recognized = false`;
|
||||
- `is_recognized = true` — текст разобран как число без выброшенных и исправленных символов (замена двойника, `}` или
|
||||
`]` вместо скобки — исправление); `confidence` — от источника и в `is_recognized` не входит;
|
||||
- позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence`
|
||||
ячейки (позиции нет — 100).
|
||||
|
||||
`parse_amount` — единственное место шагов 2–8, где разбор зависит от `source`. Разбор сумм ОСВ этим правилом MUST NOT
|
||||
затрагиваться.
|
||||
|
||||
#### Scenario: Потерянная цифра (AURORA-1511)
|
||||
- **WHEN** ячейка OCR — `"42831 1"` с уверенностью 100
|
||||
- **THEN** `amount = 428311`, `is_recognized = false`, `text = "42831 1"`
|
||||
|
||||
#### Scenario: Скобка `}` (OQ-147)
|
||||
- **WHEN** ячейка OCR — `"(7744536}"`
|
||||
- **THEN** `amount = -7744536`, `is_recognized = false`
|
||||
|
||||
#### Scenario: Запятая по источнику
|
||||
- **WHEN** текст `"4,200"`
|
||||
- **THEN** у `ocr` `amount = 4200`; у `excel` `amount = 4.20`
|
||||
|
||||
#### Scenario: Длинное тире (OQ-148)
|
||||
- **WHEN** ячейка OCR — `"—"` с уверенностью 100
|
||||
- **THEN** `amount = null`, `string_value = null`, `is_recognized = true`
|
||||
|
||||
### Requirement: Шаг 4 — единицы и дробные значения
|
||||
При единицах «тыс. руб.» и доле значений с дробной частью ≥ 10 % SHALL выдаваться предупреждение документа
|
||||
`FRACTIONAL_IN_THOUSANDS` (`info`); то же сочетание в книге xlsb без числовых форматов — `XLSB_SCALE_SUSPECT`
|
||||
(`error`). Книга xlsb без числовых форматов сама по себе — `XLSB_NO_FORMATS` (`info`); формат ячейки не разобран —
|
||||
`NUMBER_FORMAT_UNPARSED` (`info`, одно на документ с перечнем форматов).
|
||||
|
||||
#### Scenario: xlsb с копейками при «тыс. руб.»
|
||||
- **WHEN** книга xlsb без стилей, в шапке «тыс. руб.», 40 % значений дробные
|
||||
- **THEN** предупреждение `XLSB_SCALE_SUSPECT` уровня `error`, документ `needs_check`
|
||||
|
||||
### Requirement: Шаг 5 — строка формы
|
||||
Шаг 5 SHALL выдавать `StatementLine {row_index, ref, line_status, line_code, line_code_text, line_code_confidence,
|
||||
line_name, line_name_confidence, parent_ref, needs_review, values}`: `line_code` — разобранный код (4–5 цифр) или
|
||||
`null` у `unreadable` / `missing` / `detail`; `line_code_text` — сырой текст ячейки кода; `line_code_confidence` — от
|
||||
источника, у кода с заменой двойника — не выше 99; `line_name` — распознанный текст наименования как есть, с
|
||||
уверенностью источника. Сервисы MUST NOT подставлять наименование из справочника и MUST NOT срезать уверенность кода
|
||||
по справочнику (Р-3). Редакция (`66n` / `fsbu4`) в сервисах выбирается по эффективной дате (`fsbu4` при дате после
|
||||
2024-12-31) только для перечня кодов; без даты проверяются обе редакции.
|
||||
|
||||
#### Scenario: Наименование из OCR (AURORA-1522)
|
||||
- **WHEN** у строки 1230 OCR-наименование «Дебиторская задолжен-ность»
|
||||
- **THEN** `line_name = "Дебиторская задолжен-ность"` с уверенностью ячейки; справочное имя показывает ядро
|
||||
|
||||
### Requirement: Шаг 6 — тип формы, вариант и документ
|
||||
Шаг 6 SHALL собирать `StatementDocument {document_type, form_variant, locator, header, columns, lines}`. Тип —
|
||||
большинство кодов строк `ok` в отрезке ББ → `BALANCE`, ОФР → `INCOME`, прочих форм и пояснений → `DISCLOSURE`;
|
||||
совпало с `type_hint` или `type_hint = null` — тип по кодам; противоречит — тип по кодам и предупреждение
|
||||
`TYPE_CONFLICT` (`error`); кодов нет — `type_hint`. Без столбца кодов: `type_hint` есть → документ без строк
|
||||
(`DISCLOSURE` — `done`, иначе `needs_check` с `CODE_COLUMN_NOT_FOUND` уровня `error`); `type_hint` нет → у `ocr`
|
||||
таблица ИС с содержимым — предупреждение файла `TABLE_LOST` (`error`), у `excel` — лист «прочее», `SHEET_OTHER`
|
||||
(`info`). `DISCLOSURE` по `type_hint` — шаги 2–7 не выполняются; `DISCLOSURE` по кодам — строки и колонки
|
||||
отбрасываются, проверки не считаются, документ `done`.
|
||||
|
||||
`form_variant` SHALL определяться только по структуре кодов: ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`;
|
||||
ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` — `null`. ОКУД
|
||||
признаком варианта не служит. Шапка документа — поля `header` кандидата: `text` → `<поле>_text`, разобранное → поле,
|
||||
`confidence` → `<поле>_confidence`.
|
||||
|
||||
#### Scenario: Упрощённый баланс
|
||||
- **WHEN** в ББ есть 1150, 1210, 1600, 1310, 1700 и нет 1100 и 1200
|
||||
- **THEN** `form_variant = simplified`; шаг 7 и перечень кодов берут упрощённый набор
|
||||
|
||||
#### Scenario: Заголовок спорит с кодами
|
||||
- **WHEN** `type_hint = BALANCE`, а коды строк — 2110, 2120, 2100, 2200
|
||||
- **THEN** `document_type = INCOME`, предупреждение `TYPE_CONFLICT` уровня `error`
|
||||
|
||||
### Requirement: Шаг 7 — самопроверки §7
|
||||
Шаг 7 SHALL считаться у обоих источников одним словарём `statement_checks.yaml` и выдавать `StatementChecks
|
||||
{relations_failed[], soft_relations_failed[]}`; каждое нарушение — `{column_index, relation, codes, expected,
|
||||
actual}`, где `codes` — коды цели и членов соотношения. Правила:
|
||||
- значение кода в колонке — сумма по всем строкам `ok` с этим кодом; строки `detail`, `unreadable`, `missing` в
|
||||
соотношения не входят; нарушение относится ко всем строкам его кодов;
|
||||
- соотношение **посчитано**, если непуста цель или хотя бы один член (есть строка `ok` с кодом и `amount` не `null`);
|
||||
пусто всё — соотношение пропускается; пустая цель при непустых членах и непустая цель при пустых членах — нарушение;
|
||||
- жёсткое соотношение не сошлось, если расхождение больше допуска ±1 в единицах формы **и** больше 0,001 % наибольшего
|
||||
по модулю числа соотношения — тогда оно в `relations_failed`; расхождение в пределах округления и мягкие
|
||||
соотношения — в `soft_relations_failed`;
|
||||
- члены `abs_terms` (2120, 2210, 2220, 2330, 2350, 2410) сравниваются и как записаны, и по модулю; такое соотношение
|
||||
подтверждает величину, но не знак (шаг 8);
|
||||
- набор соотношений — по `form_variant` (полный или упрощённый);
|
||||
- родитель пятизначной подстроки — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код из
|
||||
первых четырёх цифр; «сумма подстрок = родитель» (мягкое) — только если хотя бы одна подстрока в колонке непуста.
|
||||
|
||||
`checks` SHALL быть у каждого `BALANCE` / `INCOME` со строками и отсутствовать у документа без строк.
|
||||
`duplicate_line_codes` MUST NOT выдаваться.
|
||||
|
||||
#### Scenario: «БАЛАНС» без кода
|
||||
- **WHEN** строка «БАЛАНС» ушла в `detail` (код потерян), в колонке есть 1100 и 1200
|
||||
- **THEN** соотношение `1600 = 1100 + 1200` посчитано и нарушено (пустая цель при непустых членах), в
|
||||
`relations_failed`
|
||||
|
||||
#### Scenario: Лист только с активом
|
||||
- **WHEN** в ББ заполнены 1100, 1200, 1600, а 1300–1700 отсутствуют
|
||||
- **THEN** `1600 = 1700` нарушено (непустая цель при пустых членах), документ `needs_check`
|
||||
|
||||
#### Scenario: Повтор кода в соотношении
|
||||
- **WHEN** две строки 2410 с суммами 100 и 649
|
||||
- **THEN** в соотношениях код 2410 равен 749
|
||||
|
||||
#### Scenario: Округление итогов
|
||||
- **WHEN** `1600 = 1700` расходится на 9,02 при итогах ~17 млрд
|
||||
- **THEN** нарушение в `soft_relations_failed`, на статус и подсветку не влияет
|
||||
|
||||
### Requirement: Шаг 8 — `needs_review` значения
|
||||
Сервис SHALL вычислять `needs_review` у каждого значения:
|
||||
- значение **нуждается в покрытии**, если `confidence < 100` или `is_recognized = false`; чистые значения в покрытии не
|
||||
нуждаются;
|
||||
- значение **покрыто**, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано и сошлось
|
||||
(в пределах допуска или округления), и `amount` значения не `null`;
|
||||
- **знак**: у члена `abs_terms`, нуждающегося в покрытии, знак подтверждён, если форма записи знака (скобки, минус,
|
||||
без знака — по `text`) совпадает с формой у большинства членов `abs_terms` этой колонки с непустым `amount`,
|
||||
`is_recognized = true` и без правки аналитика; не совпадает или сравнивать не с чем — знак не подтверждён;
|
||||
- `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех
|
||||
ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности;
|
||||
- иначе `needs_review = false`.
|
||||
|
||||
#### Scenario: Догадка подтверждена соотношением (Р-7)
|
||||
- **WHEN** у 1230 `"42831 1"` → 428311, `is_recognized = false`, и `1200 = 1210 + 1220 + 1230 + …` в колонке посчитано
|
||||
и сошлось
|
||||
- **THEN** `needs_review = false`; `text` и уверенность остаются в значении
|
||||
|
||||
#### Scenario: Знак расхода в фигурной скобке
|
||||
- **WHEN** в колонке ОФР 2210 и 2220 записаны в скобках, а 2120 — `"(7744536}"`, и `2100 = 2110 − 2120` сошлось
|
||||
- **THEN** у 2120 знак подтверждён, `needs_review = false`
|
||||
|
||||
#### Scenario: Знак вне конвенции
|
||||
- **WHEN** 2210 и 2220 в скобках, а сомнительная 2120 — `"{38378)"` без минуса после разбора
|
||||
- **THEN** `needs_review = true` у 2120 независимо от соотношения
|
||||
|
||||
#### Scenario: Справочная строка без проверки
|
||||
- **WHEN** значение 2510 с уверенностью 80 (строк 25xx нет в жёстких соотношениях)
|
||||
- **THEN** `needs_review = true`
|
||||
|
||||
### Requirement: Шаг 8 — подтверждение неуверенного кода структурой
|
||||
Код строки SHALL считаться неуверенным, если `line_code_confidence < 100` или при разборе заменён двойник; только к
|
||||
неуверенным кодам применяется структурная проверка (Р-12). Четырёхзначные коды строк `ok` документа образуют
|
||||
последовательность; для каждой проверяемой редакции перечня (по эффективной дате; без даты — обе) берётся наибольшая
|
||||
подпоследовательность, согласованная с порядком бланка (итоги после слагаемых); редакция — с более длинной
|
||||
подпоследовательностью, при равенстве — по эффективной дате; уверенные коды участвуют как подтверждённые якоря;
|
||||
при нескольких наибольших подпоследовательностях подтверждены только коды, общие для всех. Пятизначный код подтверждён,
|
||||
если подтверждён его родитель и подстрока стоит после него. Повтор кода, у которого хотя бы одна копия неуверенна, —
|
||||
`needs_review = true` у неуверенной копии (Р-16). Неподтверждённый неуверенный код — `StatementLine.needs_review = true`.
|
||||
У Excel и чистых OCR-кодов `needs_review` строки по структуре MUST NOT ставиться.
|
||||
|
||||
#### Scenario: Неуверенный код на своём месте
|
||||
- **WHEN** код 1230 с уверенностью 50 стоит между 1220 и 1240 (уверенными)
|
||||
- **THEN** код подтверждён, `needs_review = false`
|
||||
|
||||
#### Scenario: Переставленные неуверенные соседи
|
||||
- **WHEN** порядок кодов 1210, 1230 (уверенность 50), 1220 (уверенность 50), 1240
|
||||
- **THEN** у обеих неуверенных строк `needs_review = true`, документ `needs_check`
|
||||
|
||||
#### Scenario: Нестандартный код Excel
|
||||
- **WHEN** в Excel-форме код 1231 или пятизначная подстрока 11101 вне перечня
|
||||
- **THEN** `needs_review` строки не ставится; проверка «код не из редакции» — `info` ядра
|
||||
|
||||
### Requirement: Шаг 8 — поля шапки
|
||||
Дата отчёта с уверенностью ниже 100 SHALL подтверждаться совпадением с самой поздней абсолютной датой колонок; иначе
|
||||
поле `report_date` попадает в `needs_review_fields` и выдаётся `REPORT_DATE_UNCERTAIN` (`error`, `field =
|
||||
report_date`). ИНН и организация с уверенностью ниже 100 — предупреждения `INN_UNCERTAIN` / `ORGANIZATION_UNCERTAIN`
|
||||
(`info`, с `field`), в `needs_review_fields` не попадают; единицы и прочие поля на статус не влияют.
|
||||
|
||||
#### Scenario: Неуверенная, но верная дата
|
||||
- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, самая поздняя дата колонок 30.09.2025
|
||||
- **THEN** `needs_review_fields` пуст, предупреждения о дате нет
|
||||
|
||||
#### Scenario: Неуверенный год
|
||||
- **WHEN** дата отчёта 30.09.2023 с уверенностью 60, колонки на 30.09.2025 и 31.12.2024
|
||||
- **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` уровня `error`, документ `needs_check`
|
||||
|
||||
### Requirement: Шаг 8 — предупреждения
|
||||
Предупреждение SHALL быть структурой `{code, severity, message, line_refs?, column_index?, field?}`: `severity` —
|
||||
`error` | `info`; `line_refs` — список `ref` строк; `column_index` — колонка; `field` — поле шапки. Место в файле —
|
||||
`locator` документа, а не текст. Предупреждения уровня файла (`metrics.warnings`) дополнительно имеют `locator?`
|
||||
(json набора ИС или лист). Уровни SHALL быть такими:
|
||||
|
||||
| Код | Уровень | Когда |
|
||||
|---|---|---|
|
||||
| `JSON_FAILED` | error (файл) | json набора ИС ошибочен при успешном соседе |
|
||||
| `TABLE_PARSE_ERROR` | error | исключение при разборе таблицы ИС (`BACKEND_PARSING_ERROR`) |
|
||||
| `TABLE_LOST` | error (файл) | таблица ИС с содержимым без колонки кодов и без `type_hint` |
|
||||
| `CODE_COLUMN_NOT_FOUND` | error | у формы с `type_hint` `BALANCE`/`INCOME` нет столбца кодов |
|
||||
| `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в том числе лист-продолжение) |
|
||||
| `SHEET_NOT_JOINED` | error | лист с ≥ 3 кодами и другим числом колонок не присоединился |
|
||||
| `CONTINUATION_TYPE_CONFLICT` | error | тип маркера продолжения противоречит форме |
|
||||
| `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента не совпали с первым |
|
||||
| `COLUMN_WITHOUT_DATE` | error | числовая колонка без разобранной подписи |
|
||||
| `COLUMN_DATE_UNCERTAIN` | error | неуверенная подпись не попала в ожидаемые даты |
|
||||
| `HEADER_ROW_NOT_FOUND` | error | заголовка нет ни у одного сегмента |
|
||||
| `TYPE_CONFLICT` | error | тип по кодам противоречит `type_hint` |
|
||||
| `NO_ORGANIZATION_NO_INN` | error | в шапке нет ни организации, ни ИНН |
|
||||
| `PERIOD_UNKNOWN` | error | нет ни даты отчёта, ни абсолютных дат колонок |
|
||||
| `REPORT_DATE_UNCERTAIN` | error | неуверенная дата отчёта не совпала с датами колонок |
|
||||
| `XLSB_SCALE_SUSPECT` | error | xlsb без форматов при «тыс. руб.» и ≥ 10 % дробных |
|
||||
| `INN_NOT_FOUND`, `INN_UNCERTAIN` | info | ИНН не найден или неуверен при найденной организации |
|
||||
| `ORGANIZATION_NOT_FOUND`, `ORGANIZATION_UNCERTAIN` | info | организация не найдена или неуверенна при найденном ИНН |
|
||||
| `REPORT_DATE_UNPARSED` | info | дата отчёта не разобрана при наличии дат колонок |
|
||||
| `UNITS_NOT_FOUND` | info | единицы не найдены |
|
||||
| `FILE_TYPE_MISMATCH` | info (файл) | формат файла не совпал с `file_type` запроса |
|
||||
| `COLUMN_SKIPPED` | info | подпись из словаря исключений пропущена |
|
||||
| `FRACTIONAL_IN_THOUSANDS` | info | дробные при «тыс. руб.» (кроме xlsb без форматов) |
|
||||
| `XLSB_NO_FORMATS`, `NUMBER_FORMAT_UNPARSED` | info | числовые форматы недоступны или не разобраны |
|
||||
| `VALUE_OUT_OF_RANGE` | info | число Excel длиннее 16 целых цифр |
|
||||
| `SHEET_OTHER` | info | лист Excel «прочее» без `type_hint` и кодов |
|
||||
| `ROW_SPLIT` | info | разлеплена склеенная строка |
|
||||
|
||||
Предупреждения пути ОСВ — уровня `info`, код с префиксом `OSV_`. Новый код предупреждения вводится словарём пакета и
|
||||
таблицей §11 `05-domain-rules.md`; `code` в контракте — строка `^[A-Z][A-Z0-9_]*$`, не перечисление.
|
||||
|
||||
#### Scenario: НПЗ ОФР без ИНН
|
||||
- **WHEN** в шапке ОФР есть организация и нет ИНН
|
||||
- **THEN** `INN_NOT_FOUND` уровня `info`, документ может быть `done`
|
||||
|
||||
#### Scenario: Привязка к ячейке
|
||||
- **WHEN** не сошлось `1200 = 1210 + … + 1260` в колонке 1
|
||||
- **THEN** нарушение в `checks.relations_failed` с `column_index = 1` и `codes`, `needs_review = true` у ячеек этих
|
||||
кодов в колонке 1
|
||||
|
||||
### Requirement: Шаг 8 — статус документа и конверта
|
||||
Статус документа SHALL вычисляться одной функцией общего кода: `done` ⇔ раскладка найдена ∧ (организация или ИНН) ∧
|
||||
период определён (дата отчёта или самая поздняя абсолютная дата колонок) ∧ все строки `ok` или `detail` ∧ нет
|
||||
значений, строк и полей шапки с `needs_review` ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет
|
||||
предупреждений документа уровня `error`; иначе `needs_check`. `DISCLOSURE` — `done` без условий о раскладке, строках и
|
||||
проверках. Функция SHALL принимать на вход и подтверждения и акцепт аналитика (у ядра после правок): правленое или
|
||||
подтверждённое значение и строка не имеют `needs_review`, акцепт снимает жёсткие нарушения. Статус конверта: `done` ⇔
|
||||
все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error`
|
||||
(`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser). Квартал периода, сверка ИНН и наименований
|
||||
со справочником в `done` MUST NOT входить.
|
||||
|
||||
#### Scenario: Excel-форма без замечаний
|
||||
- **WHEN** Excel-ББ с найденной раскладкой, организацией, датой, всеми строками `ok`, сошедшимися соотношениями
|
||||
- **THEN** документ `done`; уверенность 100 у Excel выполняет условия покрытия автоматически
|
||||
|
||||
#### Scenario: Строка без кода с суммой
|
||||
- **WHEN** в форме есть строка `unreadable` с суммой
|
||||
- **THEN** документ `needs_check`
|
||||
|
||||
#### Scenario: Соседний документ не тянет файл
|
||||
- **WHEN** ББ `done`, ОФР того же файла `needs_check` из-за одной ячейки, предупреждений файла нет
|
||||
- **THEN** конверт `needs_check`, у ББ `document.status = done`
|
||||
@ -0,0 +1,49 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Адаптер Excel шага 1
|
||||
tabular-parser SHALL оставить у себя чтение книги (лимиты `PARSE_MAX_BYTES`, `PARSE_TIMEOUT_SECONDS`, формат по
|
||||
сигнатуре, лимиты zip и объединённых ячеек, `UNREADABLE`), классификацию листов (ОСВ — отдельный путь; форма;
|
||||
продолжение; прочее) и адаптер «блок формы на листе → `FormCandidate`» общего кода; шаги 2–8 по формам SHALL
|
||||
выполняться копией пакета `arm_statements`. Адаптер SHALL: выделять блоки форм на листе по заголовку или ОКУД (без
|
||||
кодов между ними блоки сливаются, заголовок после кодов — новая форма); собирать шапку каждого блока в его границах
|
||||
(организация, ИНН без ИНН аудитора, ОКПО, ОКОПФ, ОКФС, единицы, дата, адрес) как `Cell` с `confidence = 100`;
|
||||
заполнять у каждой ячейки `text` по числовому формату (tp#42) и `value`, `ref = <лист>!<A1>`; дописывать
|
||||
листы-продолжения в сетку открытой формы до раскладки (маркер «Форма … с.2» или позиция листа; проверки типа,
|
||||
диапазона кодов и, при трёх и более кодах, числа колонок — как `join_blocker`), записывая границы листов в
|
||||
`segment_breaks`. Несовпадение формата с `file_type` — предупреждение файла `FILE_TYPE_MISMATCH` (`info`).
|
||||
|
||||
#### Scenario: Книга ББ + ОФР
|
||||
- **WHEN** книга содержит лист «Баланс» (ОКУД 0710001), лист «Баланс (2)» с маркером «Форма 0710001 с.2» и лист «ОФР»
|
||||
- **THEN** адаптер выдаёт два кандидата: `BALANCE` с `locator = sheet:Баланс,Баланс (2)` и одной сеткой из двух листов
|
||||
(`segment_breaks` — первая строка второго листа) и `INCOME` с `locator = sheet:ОФР`
|
||||
|
||||
#### Scenario: Ссылки ячеек
|
||||
- **WHEN** код строки стоит в ячейке C27 листа «Баланс»
|
||||
- **THEN** `ref` строки — `Баланс!C27`
|
||||
|
||||
### Requirement: Строки без кода и неизвестные колонки в Excel
|
||||
tabular-parser SHALL отдавать строки без кода с содержимым со статусами `detail` / `missing` / `unreadable` (Р-8; ранее
|
||||
не отдавались, OQ-36 / OQ-141), числовые колонки с нераспознанной подписью — колонкой без даты и `error` (ранее
|
||||
пропускались), повторы кодов — всеми строками без `duplicate_line_codes`.
|
||||
|
||||
#### Scenario: НПЗ «Реализация иного имущества»
|
||||
- **WHEN** в ОФР ФСБУ 4/2023 строка «Реализация иного имущества…» без кода с суммами стоит под строкой 2340
|
||||
- **THEN** в результате строка `detail` с `parent_ref` строки 2340; ранее она отбрасывалась со счётчиком
|
||||
|
||||
### Requirement: Путь ОСВ в контракте v2.0
|
||||
У `osvDocument` tabular-parser SHALL выдавать `status` (`done`, когда ведомость разобрана, иначе `needs_check` — по
|
||||
прежним условиям §1.6 листа) и `warnings[]` — прежние предупреждения листа структурами с `severity = info` и кодом с
|
||||
префиксом `OSV_`; строк вида `sheet=<имя> <текст>` в `metrics.warnings` больше нет. Остальной разбор ОСВ не меняется.
|
||||
|
||||
#### Scenario: Предупреждение ОСВ
|
||||
- **WHEN** на листе ОСВ есть строка без родителя
|
||||
- **THEN** у документа ОСВ предупреждение `{code: "OSV_…", severity: "info", message}`, статус документа по §1.6
|
||||
|
||||
### Requirement: Статус книги
|
||||
Статус конверта tabular-parser SHALL вычисляться по общему правилу: `done` ⇔ все документы `done` ∧ нет предупреждений
|
||||
файла уровня `error`; книга без листов ОСВ и форм — `error INVALID_DOCUMENT`. Порядок документов — сначала ОСВ в
|
||||
порядке листов, затем формы.
|
||||
|
||||
#### Scenario: Книга только из пояснений
|
||||
- **WHEN** книга содержит только листы пояснений (0710003, «Пояснения»)
|
||||
- **THEN** документы `DISCLOSURE` со статусом `done`, конверт `done`
|
||||
Loading…
x
Reference in New Issue
Block a user