106 lines
12 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 прочитать объект `s3.key` из бакета `s3.bucket`, вызвать парсер, записать файл результата
(по `recognition-result.schema.json` v2.0, `schema_version = "2.0"`) в `results/{file_id}/{job_id}/result.json` и
опубликовать конверт результата (по `recognition-result-message.schema.json` v2.0, `result_schema_version = "2.0"`) с
заголовком `Nats-Msg-Id = result:<job_id>:<attempt>`. Ack `processing` SHALL нести `schema_version = "2.0"`; запрос
`recognition-request` остаётся v1.0. `service.statements_version` файла результата SHALL быть версией библиотеки
`arm_statements` (VERSION или метаданные установленного дистрибутива; без версии сервис не стартует) и записываться при наличии `statementDocument`.
`metrics.warnings` конверта SHALL содержать предупреждения уровня файла структурами `{code, severity, message, locator?}`,
не строками. Статус конверта — по общему правилу: `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня
`error`. При неудаче публикации сервис SHALL повторить её до трёх раз с паузой, затем залогировать ошибку.
#### Scenario: Запрос с заглушкой парсера (Срез 0)
- **WHEN** воркеру явно передана заглушка `NullParser` (только тесты воркера; реализация по умолчанию — реальный парсер),
запрос подтверждён и объект существует в `uploads/`
- **THEN** в `files.tabular_parsed` появляется конверт `status = error`, `error.code = NOT_IMPLEMENTED`, `job_id`/`file_id`
из запроса, `metrics.duration_ms ≥ 0`; файл результата может отсутствовать
#### Scenario: Запрос с реальным парсером
- **WHEN** запрос подтверждён и объект в `uploads/` существует, но не является книгой Excel (сигнатура не XLS/XLSX/XLSB)
- **THEN** в `files.tabular_parsed` появляется конверт `status = error`, `error.code = UNSUPPORTED_FORMAT` (повреждённая
книга — `UNREADABLE`), `job_id`/`file_id` из запроса, `metrics.duration_ms ≥ 0`; файл результата может отсутствовать;
корректная ОСВ в том же потоке даёт `done` (см. «Парсер — заменяемый интерфейс»)
#### Scenario: Результат и ack версии 2.0
- **WHEN** обработан запрос `schema_version = "1.0"` с книгой, содержащей баланс
- **THEN** ack `processing` и конверт результата несут `schema_version = "2.0"`, `result_schema_version = "2.0"`, файл
результата проходит схему v2.0, `service.statements_version` равен версии библиотеки `arm_statements`
#### Scenario: Предупреждение файла структурой
- **WHEN** в книге лист «Примечания» без признаков ОСВ и формы
- **THEN** `metrics.warnings` конверта содержит `{code: "SHEET_OTHER", severity: "info", message, locator: "sheet:Примечания"}`
#### Scenario: Повтор публикации
- **WHEN** первая публикация конверта результата завершается ошибкой соединения, вторая — успешно
- **THEN** конверт опубликован один раз с тем же `Nats-Msg-Id`, обработка не повторяется
### Requirement: Парсер — заменяемый интерфейс
Разбор файла SHALL выполняться через единый интерфейс «байты + тип файла → документы/метрики/ошибка», не знающий о
NATS и S3; воркер MUST NOT зависеть от конкретной реализации. Реализация по умолчанию (`get_parser()`) SHALL быть
оркестратором книги `BookParser`: он читает книгу один раз, классифицирует листы по содержимому и отдаёт листы ОСВ
конвейеру спеки `osv-parsing`, листы форм и их продолжения — конвейеру спеки `statement-parsing` (адаптер Excel и установленная библиотека
`arm_statements`); прочие листы дают предупреждение файла `SHEET_OTHER`. Файл результата SHALL писаться с
`schema_version = "2.0"`; документы книги — ОСВ в порядке листов, затем формы в порядке листов. Заглушка `NullParser`
(`error NOT_IMPLEMENTED`) SHALL использоваться только в тестах воркера и MUST NOT быть реализацией по умолчанию.
Реализация по умолчанию SHALL создаваться один раз при старте приложения (словари ОСВ читаются и проверяются там же,
словари форм — `default_rules()` пакета). Каталог `api/rules/` SHALL содержать словари ОСВ (`osv_headers`, `osv_groups`,
`periods`, `data_types`, `osv_checks`) и README; словарей форм ББ/ОФР в сервисе нет — они часть библиотеки `arm_statements`.
#### Scenario: Подмена реализации в тесте
- **WHEN** в тесте воркеру передан парсер, возвращающий один `osvDocument`
- **THEN** файл результата с этим документом записан в S3, конверт имеет `status = done` и `result_s3` на этот файл
#### Scenario: Реальная ОСВ через воркер
- **WHEN** в интеграционном тесте в S3 лежит `rb/2024-FY/ОСВ_66 за 2024.xls` и опубликован запрос `file_type = XLS`
- **THEN** после ack публикуется конверт `done` с `result_schema_version = "2.0"`, файл результата проходит схему v2.0 и
содержит один `osvDocument` со `status = done` и `account_code_raw = "66"`
#### Scenario: Реальные формы через воркер
- **WHEN** в интеграционном тесте в S3 лежит книга `statements-xlsx/npz/2025-Q3/…xlsx` и опубликован запрос `file_type = XLSX`
- **THEN** после ack публикуется конверт `done` с `result_schema_version = "2.0"`, файл результата проходит схему v2.0 и
содержит два `statementDocument` (`BALANCE`, `INCOME`) со строками `line_status`, `ref`, `checks` и пустыми
`checks.relations_failed`; расхождения итогов в пределах округления (`1600 = 1700` на 1.94 / 9.02 / 2.48, `2100`/`2200`/
`2300`) — в `soft_relations_failed` (OQ-138); `service.statements_version` равен версии библиотеки
#### Scenario: Книга без ОСВ и форм
- **WHEN** в S3 лежит книга Excel, ни один лист которой не имеет признаков ОСВ или формы
- **THEN** публикуется конверт `error`, `error.code = INVALID_DOCUMENT`, сообщение называет число просмотренных листов
#### Scenario: Сервис без версии общего кода не стартует
- **WHEN** у библиотеки `arm_statements` нет ни VERSION, ни версии установленного дистрибутива
- **THEN** `require_statements_version()` бросает `RuntimeError` с текстом «библиотека arm_statements без VERSION и версии пакета: выполните make install», сервис не
стартует
#### Scenario: Установленный дистрибутив без VERSION
- **WHEN** библиотека установлена как `pylib_ai_arm_statements==0.1.1`, файл VERSION отсутствует, а метаданные дистрибутива доступны
- **THEN** сервис стартует и указывает `service.statements_version = "0.1.1"` при обработке реальной книги `statements-xlsx/npz/2025-Q3/…xlsx`
## ADDED Requirements
### Requirement: Локальная поставка библиотеки форм
Окружение сервиса SHALL использовать установленную библиотеку `pylib_ai_arm_statements==0.1.1` с импортом `arm_statements`. `make venv` SHALL пропускать строки `pylib_ai*` и `raisa_*` при установке из requirements через индекс и устанавливать библиотеку из `STATEMENTS_DIR` (по умолчанию `../pylib_ai_arm_statements`). `make install` SHALL создавать окружение при необходимости и переустанавливать библиотеку из этого пути.
#### Scenario: Подготовка окружения
- **WHEN** разработчик выполняет `make venv` при доступной локальной библиотеке
- **THEN** runtime- и dev-зависимости установлены в `api/.venv`, библиотека импортируется из установленного пакета; запрос на скачивание `pylib_ai*` из PyPI не выполняется
#### Scenario: Переустановка после изменения библиотеки
- **WHEN** разработчик выполняет `make install STATEMENTS_DIR=<путь-к-библиотеке>` после изменения её исходников без смены версии
- **THEN** библиотека переустановлена из указанного пути в `api/.venv` и изменения доступны сервису
### Requirement: Библиотека форм в контейнерном образе
`make docker-build` SHALL передавать `STATEMENTS_DIR` именованным контекстом BuildKit `statements`. Сборка SHALL устанавливать библиотеку из этого контекста вместе с публичными зависимостями, без загрузки `pylib_ai*` из PyPI. Образ SHALL содержать установленный пакет и его словари, доступные при запуске без исходной папки хоста.
#### Scenario: Сборка и запуск с локальной библиотекой
- **WHEN** образ собран через `make docker-build` при доступном BuildKit и локальной библиотеке
- **THEN** внутри образа импортируется `arm_statements`, доступны словари и версия `0.1.1`; исходная папка хоста при запуске не требуется
#### Scenario: Прямая сборка
- **WHEN** сборка запускается напрямую командой `docker build`
- **THEN** вызывающая сторона SHALL передать `--build-context statements=<путь-к-библиотеке>`; отсутствие контекста не заменяется загрузкой библиотеки из PyPI