144 lines
19 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.

# Spec Delta
## 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` (версия установленной библиотеки, полученная через `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`, `severity`, `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`
#### Scenario: Версия установленной библиотеки в результате
- **WHEN** установлен пакет `pylib_ai_arm_statements==0.1.1` и успешно нормализуется `aep_6.json`
- **THEN** файл результата содержит `service.statements_version = "0.1.1"`, конверт содержит только `service.name` и `service.version`; чтение локальной копии `api/arm_statements/VERSION` не требуется
### Requirement: Нормализация ответа ИС в `statementDocument`
Шлюз SHALL оставить у себя обмен с ИС Сканирование (горячие папки, опрос, срок, склейку набора json в `raw/{job_id}.json` до нормализации, очистку папки) и адаптер «таблица `table_structure[i]` json → `FormCandidate`»; шаги 2–8 SHALL выполняться установленной библиотекой `arm_statements` (дистрибутив `pylib_ai_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` таблицы; остальные таблицы файла разобраны
## ADDED Requirements
### Requirement: Локальная установка библиотеки нормализации
Окружение сервиса SHALL устанавливать `pylib_ai_arm_statements` из локального источника без обращения за ним в PyPI. `requirements.txt` SHALL фиксировать версию `0.1.1`; `make venv` SHALL исключать `pylib_ai*` из установки по этому файлу и устанавливать библиотеку из `STATEMENTS_DIR` (по умолчанию `../pylib_ai_arm_statements`). `make install` SHALL переустанавливать её в окружение backend. Сервис MUST NOT содержать копию `api/arm_statements/`.
#### Scenario: Создание окружения
- **WHEN** выполнен `make venv` при доступной локальной библиотеке версии `0.1.1`
- **THEN** в `api/.venv` установлена библиотека, импорт `arm_statements` работает без копии в проекте, пакет `pylib_ai_arm_statements` не запрашивается в PyPI
#### Scenario: Переустановка библиотеки из указанного пути
- **WHEN** выполнен `make install STATEMENTS_DIR=<локальный путь>` с доступным исходным пакетом
- **THEN** пакет установлен или переустановлен в `api/.venv`, окружение создано при его отсутствии
#### Scenario: Локальный источник недоступен
- **WHEN** выполняется `make venv` или `make install`, а `STATEMENTS_DIR` не содержит доступного устанавливаемого пакета
- **THEN** команда завершается ошибкой и не заменяет локальный источник загрузкой библиотеки из PyPI
### Requirement: Образ содержит установленную библиотеку
`make docker-build` SHALL собирать образ с библиотекой из `STATEMENTS_DIR`, переданной дополнительным контекстом сборки. Образ SHALL поддерживать импорт `arm_statements` и запуск нормализатора без доступа к каталогу исходников библиотеки на хосте во время работы.
#### Scenario: Автономный образ
- **WHEN** образ собран через `make docker-build` с локальной библиотекой версии `0.1.1`
- **THEN** в образе доступны импорт `arm_statements`, словари и версия `0.1.1`; монтирование исходников библиотеки при запуске не требуется
### Requirement: Недоступная версия библиотеки препятствует старту
Сервис MUST NOT стартовать, если версия библиотеки нормализации не определена. Ошибка SHALL указывать на установленную библиотеку и способ её установки, без инструкции синхронизации копии исходников.
#### Scenario: Версия не определена
- **WHEN** при создании нормализатора API библиотеки не возвращает версию и явная версия не задана
- **THEN** создание приложения завершается ошибкой с указанием выполнить `make install`