135 lines
18 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: Результат по контракту
Сервис SHALL публиковать в `files.tabular_parsed` конверт по `recognition-result-message.schema.json` с
`service.name = "scan-gateway"`, `Nats-Msg-Id = result:<job_id>:<attempt>`; при `status ∈ {done, needs_check}` — файл
результата (`recognition-result.schema.json`, `schema_version = "2.0"` — `CONTRACT_VERSION` сервиса; запрос `recognition-request` остаётся `1.x`, запрос `2.0` → `SCHEMA_MISMATCH`) в `results/{file_id}/{job_id}/result.json` и `result_s3`;
при неудаче публикации SHALL повторить её до трёх раз. Нормализатор получает склеенный ответ и SHALL считать файл
успешным, если `error.code == ""` (поле `error` реального успешного ответа — `{"code": "", "message": ""}`, а не `null`;
отсутствующий или `null` `error` ошибкой не считается), и ошибочным, если `error.code` непуст, `parse_error` не `null`
либо корень ответа файла не является объектом JSON (ошибка этого файла — `INVALID_FORMAT`); успех MUST NOT определяться
проверкой «`error` не `null`». Конверт (включая ack `processing`) и файл результата несут `schema_version = "2.0"`, `result_schema_version = "2.0"`; `service.statements_version` (содержимое `api/arm_statements/VERSION`) SHALL быть в файле результата, в конверте `service` — `{name, version}`. Если ошибочны **все** файлы — `status = error`, `error.code` и `error.message` берутся у
первого ошибочного файла по имени (для `parse_error` и не-объекта — `INVALID_FORMAT`); если код ИС этого файла не
входит в перечень кодов контракта, `error.code = UNKNOWN_ERROR`, а исходный код SHALL быть указан в `error.message`.
Если есть хотя бы один успешный файл — документы собираются из успешных файлов по требованию «Нормализация ответа ИС в `statementDocument`», а статус определяется по требованию «Статус результата нормализации». `metrics.warnings` SHALL быть списком структур предупреждений (`code`, `level`, `message`, `locator`, …) по схеме v2.0 — одним и тем же в конверте и в файле результата (предел 100 структур, `message` до 1000 символов; при переполнении остаются все `error`, затем `info`). Каждый ошибочный json набора SHALL давать предупреждение `JSON_FAILED` (`error`, `locator = file:<имя json>`, `message = "<код ИС>: <сообщение ИС>"`; для `parse_error` и не-объекта — код `INVALID_FORMAT`), в том числе когда ошибочны все json; исключение при разборе таблицы — `TABLE_PARSE_ERROR` (`error`, `locator = file:<имя json>;table:<i>`, `message = "BACKEND_PARSING_ERROR: <тип>: <текст>"`). Успешный json без `data_ocr`, без `table_structure` или с пустым `table_structure` SHALL давать `JSON_FAILED`. Если предупреждений нет, поле `warnings` SHALL быть опущено.
#### Scenario: Успешный ответ ИС в Срезе 0
- **WHEN** ответ из одного файла с `error = {"code": "", "message": ""}` и непустым `data_ocr` (по образцу `aep_6.json`)
- **THEN** начиная со Среза 3a конверт больше не `NOT_IMPLEMENTED`: поведение — по сценарию «Успешный ответ ИС» ниже
#### Scenario: Успешный ответ ИС
- **WHEN** ответ из одного файла с `error = {"code": "", "message": ""}` и непустым `data_ocr` (`aep_6.json`)
- **THEN** конверт `status = needs_check`, `raw_s3_key = raw/<job_id>.json`, `result_s3` указывает на файл результата с двумя документами (`BALANCE`, `INCOME`)
#### Scenario: Ошибка ИС
- **WHEN** единственный файл ответа `<job_id>.json` содержит `error.code = INVALID_DOCUMENT`
- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT`, `error.message` из ответа, `raw_s3_key` заполнен, `metrics.warnings` содержит одну структуру `JSON_FAILED` (`error`, `locator = file:<job_id>.json`, `message = INVALID_DOCUMENT: <сообщение>`)
#### Scenario: Ошибка одного из документов
- **WHEN** ответ состоит из `<job_id>.json` (успех, `aep_3.json`) и `<job_id>_ofr.json` (`INVALID_DOCUMENT`, `aep_3_ofr.json`)
- **THEN** конверт `status = needs_check`, документы собраны из `<job_id>.json`, `metrics.warnings` содержит `JSON_FAILED` (`error`, `locator = file:<job_id>_ofr.json`, `message = INVALID_DOCUMENT: <сообщение>`) и предупреждения пакета; ядро не автоподтверждает документы файла
#### Scenario: Все документы ошибочны
- **WHEN** оба файла ответа ошибочны (первый по имени — `INVALID_DOCUMENT`, второй — не JSON)
- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT` (код первого ошибочного файла по имени), `metrics.warnings` содержит два `JSON_FAILED` (по одному на json); у файла не JSON сообщение начинается с `INVALID_FORMAT`
#### Scenario: Все файлы не JSON
- **WHEN** единственный файл ответа не разбирается как JSON
- **THEN** конверт `status = error`, `error.code = INVALID_FORMAT`, `metrics.warnings` содержит один `JSON_FAILED` с `message = INVALID_FORMAT: <сообщение разбора>`
#### Scenario: Корень ответа не объект
- **WHEN** файл ответа — корректный JSON, но корень — массив
- **THEN** этот файл ошибочен с кодом `INVALID_FORMAT`; остальные файлы оцениваются по общим правилам
#### Scenario: Код ошибки ИС вне перечня контракта
- **WHEN** единственный файл ответа содержит `error.code = SOME_NEW_CODE`
- **THEN** конверт `status = error`, `error.code = UNKNOWN_ERROR`, `error.message` содержит `SOME_NEW_CODE`, `JSON_FAILED` в `metrics.warnings` с `message = SOME_NEW_CODE: <сообщение>`
#### Scenario: Все json набора ошибочны
- **WHEN** оба json набора ошибочны (первый по имени — `INVALID_DOCUMENT`)
- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT` (код и сообщение первого ошибочного json), `metrics.warnings` содержит `JSON_FAILED` каждого json
#### Scenario: Успешный json без таблиц
- **WHEN** успешный json не содержит `data_ocr.table_structure` или `table_structure: []`
- **THEN** этот json даёт `JSON_FAILED`; если успешных json с таблицами нет — конверт `status = error`
### Requirement: Нормализация ответа ИС в `statementDocument`
Шлюз SHALL оставить у себя обмен с ИС Сканирование (горячие папки, опрос, срок, склейку набора json в `raw/{job_id}.json` до нормализации, очистку папки) и адаптер «таблица `table_structure[i]` json → `FormCandidate`»; шаги 2–8 SHALL выполняться копией пакета `arm_statements` (`api/arm_statements/`, правке не подлежит). Адаптер SHALL: создавать кандидата на каждую таблицу каждого успешного json; копировать шапку json (`organization`, `inn`, `units`, `date`) в шапку каждой своей таблицы как `Cell` (`text = form_name`, `confidence` ИС); передавать `label.form_name` таблицы как заголовок в `evidence` (тип — общим правилом шага 1 по словарю заголовков пакета, `type_hint(okud_text, title_text, rules)`, у OCR `okud_text = None`); строить плотную сетку по `row_index` и `column_index` ИС (пустые позиции — `null`), `Cell.ref = row:<row_index>`; `locator = file:<имя json>;table:<i>`. `value` у OCR-ячеек всегда `null`, `source = OCR`. Нарушенная структура таблицы ИС SHALL давать исключение адаптера и `TABLE_PARSE_ERROR` всей таблицы, остальные таблицы разбираются. Статус конверта SHALL определяться только `envelope_status` пакета; перевод документов и предупреждений в контракт — только `to_contract` / `warning_to_contract` пакета.
#### Scenario: Один json — ББ и ОФР
- **WHEN** json `aep_3.json` содержит две таблицы — «Бухгалтерский баланс» и «Отчет о финансовых результатах» — под общей шапкой
- **THEN** два кандидата с `locator` `file:aep_3.json;table:0` и `…;table:1`, у каждого копия шапки json
#### Scenario: Таблица упала при разборе
- **WHEN** разбор `table:1` json `aep_3.json` бросил исключение, `table:0` разобрана
- **THEN** документ `BALANCE` из `table:0`; в `metrics.warnings` — `TABLE_PARSE_ERROR` уровня `error` с `locator = "file:aep_3.json;table:1"`; конверт `needs_check`
#### Scenario: Таблица ИС только с текстом
- **WHEN** в json есть таблица без столбца кодов и без распознанного типа, в ячейках только текст подписей
- **THEN** документа нет, в `metrics.warnings` — `TABLE_SKIPPED` уровня `info` с `locator` таблицы; конверт из-за неё не `needs_check`
#### Scenario: Чистый скан с шапкой (`aep_6.json`)
- **WHEN** нормализуется `aep_6.json` (две таблицы: «Бухгалтерский баланс» и «Отчет о финансовых результатах»)
- **THEN** два кандидата, из них два документа `BALANCE` и `INCOME` по шагам 2–8 пакета; поля, колонки и значения — по golden `fixtures/ocr/expected_normalize/`; `header.units = "тыс. руб."`; статус определяет `envelope_status` пакета
#### Scenario: Зашумлённый скан без шапки (`aep_3.json`)
- **WHEN** нормализуется `aep_3.json` (таблица «Бухгалтерский баланс» без строки-шапки, коды с мусором)
- **THEN** разбор столбца кода, синтез шапки, предупреждения и статус документа — правилами пакета (шаги 2–8), шлюз их не дублирует; `header.inn` и `report_date` — по правилам очистки пакета; строки без кода сохраняются со статусом строки
#### Scenario: Суммы в скобках и с пробелами (`aep_3.json`, ОФР)
- **WHEN** ячейка значения содержит `(7744536}` с `confidence 88.9`, другая — `(6032409)`, третья — `13 070`
- **THEN** разбор сумм и `is_recognized` — общие правила пакета (шаг 4); шлюз передаёт текст ячейки как есть (`value = None`), без собственного разбора
#### Scenario: Нечисловая ячейка значения
- **WHEN** ячейка значения содержит `не ****` с `confidence 5.6`
- **THEN** `amount = null`, `string_value = "не ****"`, `is_recognized = false` (правило шага 4 пакета)
#### Scenario: Повтор кода строки
- **WHEN** в таблице баланса две строки с кодом `1150`
- **THEN** поведение определяет шаг пакета для повтора кода; шлюз ничего не отбрасывает до шагов 2–8, предупреждение приходит от пакета структурой `metrics.warnings`
#### Scenario: Тип по кодам при зашумлённом label
- **WHEN** `label.form_name` таблицы — `Бvxгалтерский баланс` (не сопоставлен словарём заголовков), коды строк `1110…1700`
- **THEN** тип определяется общим правилом шага 1 пакета (по кодам), предупреждение — структурой пакета
#### Scenario: Label противоречит единодушным кодам
- **WHEN** `label.form_name` таблицы — `Бухгалтерский баланс`, все коды строк данных `2110…2910`
- **THEN** тип и предупреждение о расхождении — по правилам пакета; `header.form_name` — текст `label` как распознан; статус `needs_check`
#### Scenario: Label противоречит смешанным кодам
- **WHEN** `label.form_name` таблицы — `Отчет о финансовых результатах`, коды строк данных `1150`, `1100` и `2110`
- **THEN** тип и предупреждение о расхождении — по правилам пакета; статус `needs_check`
#### Scenario: Таблица без столбца кода
- **WHEN** в таблице нет строки с ячейкой «Код» и ни в одном столбце нет кодов
- **THEN** документа нет, в `metrics.warnings` — `TABLE_SKIPPED` с `locator` таблицы; остальные таблицы файла разобраны
### Requirement: Статус результата нормализации
Статус конверта SHALL определяться функцией `envelope_status` пакета `arm_statements` по документам и предупреждениям. Если из успешных файлов не собран ни один документ — `status = error`, `error.code = RECOGNITION_FAILED`, `error.message = "ни одной таблицы не разобрано"`, причины по таблицам — в `metrics.warnings` (`TABLE_PARSE_ERROR`, `TABLE_SKIPPED`, `TABLE_LOST`); если все json ошибочны — код и сообщение первого ошибочного json. Если документы есть — `done` или `needs_check` по `envelope_status`; потерянный json набора (`JSON_FAILED`) или таблица (`TABLE_PARSE_ERROR`, `TABLE_LOST`) исключают `done`. `metrics.rows` — число строк `lines` всех документов, включая строки без кода.
#### Scenario: Ни одной таблицы
- **WHEN** единственный успешный файл содержит `table_structure: []`
- **THEN** json даёт `JSON_FAILED`, конверт `status = error`, `raw_s3_key` заполнен
#### Scenario: Все таблицы упали при разборе
- **WHEN** в успешном json разбор каждой таблицы бросил исключение
- **THEN** конверт `status = error`, `error.code = RECOGNITION_FAILED`, `error.message = "ни одной таблицы не разобрано"`, `metrics.warnings` содержит `TABLE_PARSE_ERROR` каждой таблицы
#### Scenario: Потерянный документ набора
- **WHEN** `<job_id>.json` успешен, `<job_id>_ofr.json` ошибочен
- **THEN** статус не `done`, а `needs_check`
#### Scenario: Всё распознано
- **WHEN** синтетический ответ, где все ячейки распознаны уверенно, шапка и даты разобраны, предупреждений нет
- **THEN** `status = done`, поле `metrics.warnings` отсутствует
#### Scenario: Исключение в одной таблице
- **WHEN** разбор `table_structure[1]` бросает исключение, `table_structure[0]` разбирается
- **THEN** один документ, `TABLE_PARSE_ERROR` (`error`, `locator = file:<имя json>;table:1`, `message = BACKEND_PARSING_ERROR: <класс>: <сообщение>`), статус `needs_check`
## REMOVED Requirements
### Requirement: Справочники нормализации — данные с редакциями форм
**Reason**: справочник строк (`statement_lines.yaml`) и выбор редакции удалены; двойники (`ocr_lookalikes.yaml`) и заголовки форм (`statement_labels.yaml`) — словари пакета `arm_statements` (`default_rules()`).
**Migration**: `api/rules/` и настройка `SCAN_RULES_DIR` удалены; словари меняются через репозиторий `arm-statements` и `sync-statements`.