Andrey Epifancev fc9d6d4902 openspec: архив nats-files-subjects (#28)
Change слит PR #29; спеки обновлены (subject'ы files.tabular_uploaded,
files.img_uploaded, files.tabular_parsed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-08 23:50:24 +04:00

134 lines
14 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.

# tabular-recognition-service Specification
## Purpose
Stateless-сервис разбора табличных файлов (XLS/XLSX/XLSB): принимает задания из NATS без JetStream, подтверждает их,
читает исходник из S3, отдаёт результат в S3 и конверт в NATS по контракту v1.1, не имея доступа к БД и фронта.
С Среза 2a реализация по умолчанию разбирает ОСВ (спека `osv-parsing`); заглушка `NullParser` — только для тестов воркера.
## Requirements
### Requirement: Сервис не зависит от БД и фронта
Репозиторий MUST NOT содержать каталоги `web/`, `alembic/`, `src/db/`, `src/models/`, `src/repositories/`; зависимости
`SQLAlchemy`, `asyncpg`, `alembic`, `smbprotocol`, `raisa_fastapi_protected_api` MUST NOT присутствовать в
`requirements.txt`; настройки `OPENBAO.DATABASE` и `OPENBAO.SMB` MUST NOT требоваться для старта.
#### Scenario: Старт с минимальным окружением
- **WHEN** сервис запускается с `.env`, содержащим только `OPENBAO__MINIO__*`, `OPENBAO__NATS__NATS_URL`, `DEBUG=true`
- **THEN** `GET /healthcheck` отвечает 200 с полями `uptime`, `version`, `nats`
### Requirement: Подтверждение запроса до обработки
Сервис SHALL быть подписан на `files.tabular_uploaded` в queue group `tabular-parser`. Получив запрос, проходящий
`recognition-request.schema.json`, сервис SHALL до чтения файла из S3 опубликовать в `files.tabular_parsed` конверт
`status = processing` с `job_id`, `file_id`, `service {name: "tabular-parser", version}` и заголовком
`Nats-Msg-Id = ack:<job_id>:<attempt>`. Повторный запрос с `job_id`, который уже обрабатывается или обработан этим
процессом, SHALL получить повторный ack и MUST NOT обрабатываться второй раз.
#### Scenario: Ack раньше результата
- **WHEN** опубликован запрос из `docs/contracts/examples/request-tabular.json`
- **THEN** первым сообщением сервиса в `files.tabular_parsed` является конверт `processing`, и только затем — результат
#### Scenario: Повторный запрос во время обработки
- **WHEN** тот же запрос доставлен второй раз, пока первый обрабатывается
- **THEN** публикуется второй ack `processing`, файл из S3 повторно не читается, результат публикуется один раз
### Requirement: Результат по контракту
После подтверждения сервис SHALL прочитать объект `s3.key` из бакета `s3.bucket`, вызвать парсер, записать файл результата
(по `recognition-result.schema.json`) в `results/{file_id}/{job_id}/result.json` и опубликовать конверт результата (по
`recognition-result-message.schema.json`) с заголовком `Nats-Msg-Id = result:<job_id>:<attempt>`. При неудаче публикации
сервис 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: Повтор публикации
- **WHEN** первая публикация конверта результата завершается ошибкой соединения, вторая — успешно
- **THEN** конверт опубликован один раз с тем же `Nats-Msg-Id`, обработка не повторяется
### Requirement: Ошибки транспорта превращаются в результат
Сервис SHALL отвечать конвертом `status = error` в случаях: тело не проходит схему или `MAJOR` версии ≠ 1 →
`SCHEMA_MISMATCH`; объект S3 не читается → `FILE_READ_ERROR`; исключение парсера → `UNKNOWN_ERROR` (сообщение без
трассировки). Сервис MUST NOT падать по этим причинам. `SCHEMA_MISMATCH` SHALL публиковаться (без ack), когда из тела
читаемы `job_id` и `file_id` (uuid), а `attempt` — из тела или из заголовка запроса `Nats-Msg-Id = <job_id>:<attempt>`,
и этот `job_id` сервису ещё не известен (не в работе и не среди обработанных). Иначе валидный конверт собрать нельзя
либо он ушёл бы под тем же `Nats-Msg-Id`, что и настоящий результат: сервис SHALL записать в лог ошибку
(`error_category=schema_mismatch`, причина) — уровнем error, если идентификаторы не читаемы, уровнем warning, если
`job_id` уже известен, — и MUST NOT публиковать ни ack, ни результат; ядро обнаружит такое задание по `dlq`/`timeout`
(явная запись в контракт — открытый вопрос «невалидный запрос без читаемых `job_id`/`file_id`/`attempt`» в
`open-questions-proposal.md`). FILE_READ_ERROR и UNKNOWN_ERROR публикуются всегда: сервис MUST NOT молчать по ним.
#### Scenario: Нет объекта в S3
- **WHEN** `s3.key` запроса указывает на несуществующий объект
- **THEN** публикуется конверт `error.code = FILE_READ_ERROR`, процесс продолжает работу
#### Scenario: Несовместимая версия
- **WHEN** запрос содержит `schema_version = "2.0"`
- **THEN** публикуется конверт `error.code = SCHEMA_MISMATCH`, ack не публикуется
#### Scenario: Невалидный запрос без читаемых идентификаторов
- **WHEN** тело запроса не JSON либо в нём не читаются `job_id` или `file_id`, либо `attempt` нет ни в теле, ни в
заголовке `Nats-Msg-Id`
- **THEN** в лог пишется ошибка `error_category=schema_mismatch`, ни ack, ни результат не публикуются, процесс
продолжает работу
#### Scenario: Невалидный запрос с уже известным `job_id`
- **WHEN** запрос не проходит схему, а его `job_id` уже в работе или среди обработанных
- **THEN** в лог пишется предупреждение `error_category=schema_mismatch`, ни ack, ни результат не публикуются;
обработка или результат задания с этим `job_id` не меняются
### Requirement: Парсер — заменяемый интерфейс
Разбор файла SHALL выполняться через единый интерфейс «байты + тип файла → документы/метрики/ошибка», не знающий о
NATS и S3; воркер MUST NOT зависеть от конкретной реализации. Реализация по умолчанию (`get_parser()`) SHALL быть
оркестратором книги `BookParser`: он читает книгу один раз, классифицирует листы по содержимому и отдаёт листы ОСВ
конвейеру спеки `osv-parsing`, листы форм и их продолжения — конвейеру спеки `statement-parsing`; прочие листы
пропускаются с предупреждением. Файл результата SHALL писаться с `schema_version = "1.2"`; документы книги — ОСВ в
порядке листов, затем формы в порядке листов. Заглушка `NullParser` (`error NOT_IMPLEMENTED`) SHALL использоваться только
в тестах воркера и MUST NOT быть реализацией по умолчанию. Реализация по умолчанию SHALL создаваться один раз при старте
приложения (словари читаются и проверяются там же). Каталог `api/rules/` SHALL содержать словари ОСВ (`osv_headers`,
`osv_groups`, `periods`, `data_types`), словари форм (`statement_headers`, `statement_periods`, `statement_checks`) и README.
#### 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 = "1.2"`, файл результата проходит схему v1.2 и
содержит один `osvDocument` с `account_code_raw = "66"`
#### Scenario: Реальные формы через воркер
- **WHEN** в интеграционном тесте в S3 лежит книга `statements-xlsx/npz/2025-Q3/…xlsx` и опубликован запрос `file_type = XLSX`
- **THEN** после ack публикуется конверт `done` с `result_schema_version = "1.2"`, файл результата содержит два
`statementDocument` (`BALANCE`, `INCOME`) с пустыми `checks.relations_failed`; расхождения итогов в пределах округления
(`1600 = 1700` на 1.94 / 9.02 / 2.48, `2100`/`2200`/`2300`) — в `soft_relations_failed` (OQ-138)
#### Scenario: Книга без ОСВ и форм
- **WHEN** в S3 лежит книга Excel, ни один лист которой не имеет признаков ОСВ или формы
- **THEN** публикуется конверт `error`, `error.code = INVALID_DOCUMENT`, сообщение называет число просмотренных листов
### Requirement: Копия контракта идентична источнику
`api/contracts/*.schema.json` SHALL быть байт в байт равны файлам `../docs/contracts/`; pydantic-модели SHALL валидировать
все `../docs/contracts/examples/*.json` и порождать JSON, проходящий схему. Проверка входит в `make check`.
#### Scenario: `make check`
- **WHEN** выполняются тесты контракта при доступном `../docs` (или `ARM_KA_DOCS`)
- **THEN** идентичность подтверждена, примеры валидны; при недоступном docs тест идентичности пропущен с предупреждением
### Requirement: Соединение с NATS по правилам шаблона, без JetStream
При пустых `NATS_SEED`/`NATS_JWT` сервис SHALL подключаться без учётных данных, иначе — с nkeys/JWT. Сервис MUST NOT
требовать stream'ов или consumer'ов JetStream; при разрыве соединения SHALL переподключаться автоматически и восстанавливать
подписку; `/healthcheck` SHALL отражать состояние (`nats = ok | down`; `off` — NATS выключен флагом
`NATS_ENABLED=false`).
#### Scenario: NATS перезапущен
- **WHEN** контейнер NATS перезапущен во время работы сервиса
- **THEN** в течение 20 с после старта NATS подписка восстановлена, `nats = ok`, новый запрос обрабатывается