211 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

## 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`.
Предупреждение уровня файла пишется там, где документа нет (потерянный json, таблица ИС, исключение разбора таблицы,
лист «прочее»).
`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?}` (`line_refs` — только `ref` строк из `lines[]`);
у `osvDocument` все предупреждения — `info`, `status = done`, когда контрольные суммы §1.4 листа сошлись, иначе
`needs_check`. Остальное в `osvDocument` — как в v1.2: у строки `rows[]` необязательное поле `extra` — объект «заголовок
колонки → строка или null» для колонок внутри таблицы, не сопоставленных с шестью суммами и ИНН (§1.2 «не терять»); у
шапки — `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`, `okei`, `address` и для каждого `<поле>_text` и
`<поле>_confidence`, плюс `needs_review_fields[]`; `columns[]` — `column_index` (номер столбца в сетке кандидата: у OCR
— `column_index` ИС, у Excel — номер столбца листа с 0), `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` (у Excel `<лист>!<A1>`,
имя листа в кавычках по правилу Excel), `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` в v2.0 не
порождается: лист «прочее» — предупреждение файла `SHEET_OTHER`, документа нет; значение остаётся только в схеме 1.x и в
`document_type` ядра для документов прежних результатов); `form_variant` обязателен (`null` у `DISCLOSURE`); у документа
без строк `columns` и `lines` — пустые массивы. `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: Документ OTHER в v2.0
- **WHEN** в результате v2.0 `statementDocument` с `document_type = OTHER` или без `form_variant`
- **THEN** файл не проходит схему 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). На окне перехода схемы прежнего `MAJOR` SHALL лежать
в `docs/contracts/` рядом с текущими как `recognition-result.v1.schema.json` и `recognition-result-message.v1.schema.json`
(байтовые копии 1.2 и 1.0), их примеры — в `docs/contracts/examples/v1/`; `scripts/validate_contracts.py` проверяет оба
набора. Они удаляются тем же изменением, что удаляет маппер `1.x` ядра.
#### 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`, повтор не
выполняется (как для любого результата не по схеме)
### Requirement: Копия контракта в сервисе идентична источнику
Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check`
их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением).
Файл копии SHALL сверяться с файлом docs по правилу, одинаковому у ядра и сервисов и верному до, во время и после окна
перехода между `MAJOR`:
- **схема** `<имя>.schema.json` копии: `M` — номер из её `$id` (`…/v<M>`); пара — `docs/contracts/<имя>.v<M>.schema.json`,
**если такой файл есть**, иначе основной `docs/contracts/<имя>.schema.json` (у `recognition-request` файла `.v1` нет
— его копия `…/v1` сверяется с основным файлом; до слияния v2.0 в docs файлов `.v1` нет вовсе — все копии сверяются с
основными);
- **пример** `examples/<имя>.json` копии: вид — по префиксу имени (`request-` — запрос, `result-message-` — конверт,
`result-` — файл результата), `M` — `MAJOR` копии схемы этого вида; пара — `docs/contracts/examples/v<M>/<имя>.json`,
если такой файл есть, иначе `docs/contracts/examples/<имя>.json`;
- **прочие файлы копии** (`<имя>.v<N>.schema.json`, `examples/v<N>/*.json` — их держит ядро на окне) — с одноимённым
файлом docs; нет его в docs — расхождение;
- **набор:** каждой схеме и каждому примеру верхнего уровня docs (`contracts/*.schema.json` без `.v<N>`,
`contracts/examples/*.json`) SHALL соответствовать файл копии; файлы docs `*.v<N>.schema.json` и `examples/v<N>/`
требуются в копии, только если копия держит этот `MAJOR` (ядро на окне); файлы другого `MAJOR` сервисом не
сверяются.
Правило вводится в тест идентичности **всех трёх** потребителей (`arm-core`, `tabular-parser`, `scan-gateway`) до
слияния контракта v2.0 в docs (шаг 0 выкладки); иначе их `make check` упадёт на окне перехода. Ядро держит копии обоих
`MAJOR` с PR шага 3; при удалении `MAJOR` 1 (шаг 6) ядро сначала удаляет свои копии `.v1`, затем docs — файлы `.v1`.
#### Scenario: Копия устарела
- **WHEN** в `docs/contracts/` изменена схема, а копия в сервисе — нет
- **THEN** тест идентичности в `make check` сервиса падает с указанием имени файла
#### Scenario: Сервис ещё пишет 1.x после слияния v2.0 в docs
- **WHEN** в docs основной `recognition-result.schema.json` уже v2.0, а копия в `tabular-parser` — v1.2 (`$id` `…/v1`)
- **THEN** тест идентичности сверяет копию с `recognition-result.v1.schema.json` и проходит; посторонний PR в сервис на
окне перехода не краснеет
#### Scenario: Сервис перешёл на 2.0
- **WHEN** `scan-gateway` в PR шага 5 обновил копию до v2.0 (`$id` `…/v2`) и примеры до v2.0
- **THEN** тест сверяет схему с основным файлом docs (файла `.v2` нет), примеры `result-*` — с `examples/*.json`;
`examples/v1/` и `*.v1.schema.json` docs не сверяются
#### Scenario: Запрос v1.0 после слияния v2.0
- **WHEN** в docs уже есть `recognition-result.v1.schema.json`, а `recognition-request.v1.schema.json` нет; копия
запроса в сервисе — v1.0 (`$id` `…/v1`), примеры `request-image.json`, `request-tabular.json`
- **THEN** копия запроса сверяется с основным `recognition-request.schema.json`, примеры запроса — с
`examples/request-*.json` (в `examples/v1/` их нет); тест проходит
#### Scenario: Примеры прежнего MAJOR
- **WHEN** копия `tabular-parser` — v1.2, в ней `examples/result-statement-excel.json` версии 1.2, а в docs
`examples/result-statement-excel.json` уже 2.0, `examples/v1/result-statement-excel.json` — 1.2
- **THEN** пример копии сверяется с `examples/v1/result-statement-excel.json` и проходит
#### Scenario: Шаг 0 до слияния docs
- **WHEN** тест с новым правилом слит в `arm-core`, `tabular-parser` и `scan-gateway`, а docs ещё на v1.2 без файлов `.v1`
- **THEN** все копии сверяются с основными файлами и примерами верхнего уровня, тест проходит, как прежде
#### Scenario: Ядро на окне перехода
- **WHEN** копия ядра после шага 3 держит основные схемы v2.0, `*.v1.schema.json` и `examples/v1/`
- **THEN** основные сверяются с основными docs, `.v1` и `examples/v1/` — с одноимёнными файлами docs; набор совпадает
## ADDED Requirements
### Requirement: Переход на контракт v2.0 — «сначала ядро»
Ядро SHALL до перехода любого сервиса принимать конверты (включая ack `processing`) и файлы результата и `1.x`, и
`2.0`, с двумя мапперами файла результата. При приёме `1.x` статус документа формы SHALL считаться по прежнему правилу
D-6, где «проверки §7 сошлись» — по расчёту ядра копией `arm_statements` (полный набор), записанному в `document_issue`,
а поля v2.0 заполняться по умолчанию: `line_status = ok`, `row_index` — по порядку строк в результате с 0, `ref =
null`, `line_code_original_value = line_code`, `text = string_value`, `needs_review` значения = **НЕ** `values_ok`
(`values_ok` — `is_recognized` не `false` и `confidence` 100 или пуста: чистое значение — `needs_review = false`),
`needs_review` строки — `false`, `period_kind` — `absolute` при `period_date`, иначе `none`, `form_variant` — по
структуре кодов (точка входа пакета для пересчёта, спека `arm-core`), `file_ordinal` — индекс в `documents[]`; строки `metrics.warnings` — в `recognition_job.metrics.warnings`
структурами `{code: "LEGACY_WARNING", severity: "info", message}`; предупреждения маппера по документу —
`document_issue` `MAPPING_WARNING` (`info`); `statements_version` задания — `null`. Сервисы SHALL переходить на `2.0`
только после выкладки ядра, принимающего `2.0`. Маппер `1.x` и схемы `*.v1.schema.json` удаляются после перехода обоих
сервисов отдельным изменением.
#### Scenario: Сервис ещё на v1.2
- **WHEN** после выкладки ядра tabular-parser присылает результат `1.2` с повтором кода и `checks.duplicate_line_codes`
- **THEN** ядро принимает его маппером `1.x`: из двух строк с одним кодом хранится первая (прежнее правило маппера
`1.x`), вторая не хранится, у документа `MAPPING_WARNING` о повторе; нарушения §7 — в `document_issue` от ядра, статус
документа — по D-6, файл не `ERROR`
#### Scenario: Чистое значение 1.x
- **WHEN** в результате `1.2` значение с `confidence = 100` и `is_recognized = true`
- **THEN** `needs_review = false`; значение с `confidence = 60` — `needs_review = true`
#### 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`) — поэтому порядок
«сначала ядро» обязателен