Andrey Epifancev 7a35b79d5e docs: change slice-0-skeleton под факты платформы — NATS без JetStream, Python 3.11, без CI и Nexus
Обмен с ядром на core NATS: подписка queue group, ack status=processing сразу по получении, process-local
LRU job_id против повторных запросов, публикация результата с повторами; удалены durable consumer / max_deliver /
ack_wait. Dockerfile python:3.11-slim; make check / make check-full вместо CI; raisa_* фильтруются при
dev-установке. Обновлены proposal, design (решения без обсуждения), specs, tasks, CLAUDE.md, openspec/config.yaml.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 16:21:45 +04:00

88 lines
7.9 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: tabular-recognition-service
## Purpose
Stateless-сервис разбора табличных файлов (XLS/XLSX/XLSB): принимает задания из NATS без JetStream, подтверждает их,
читает исходник из S3, отдаёт результат в S3 и конверт в NATS по контракту v1.0, не имея доступа к БД и фронта.
В Срезе 0 разбор заменён заглушкой.
## ADDED 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 быть подписан на `recognition.tabular.request` в queue group `tabular-parser`. Получив запрос, проходящий
`recognition-request.schema.json`, сервис SHALL до чтения файла из S3 опубликовать в `recognition.result` конверт
`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** первым сообщением сервиса в `recognition.result` является конверт `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** запрос подтверждён и объект существует в `uploads/`
- **THEN** в `recognition.result` появляется конверт `status = error`, `error.code = NOT_IMPLEMENTED`, `job_id`/`file_id`
из запроса, `metrics.duration_ms ≥ 0`; файл результата может отсутствовать
#### Scenario: Повтор публикации
- **WHEN** первая публикация конверта результата завершается ошибкой соединения, вторая — успешно
- **THEN** конверт опубликован один раз с тем же `Nats-Msg-Id`, обработка не повторяется
### Requirement: Ошибки транспорта превращаются в результат
Сервис SHALL отвечать конвертом `status = error` в случаях: тело не проходит схему или `MAJOR` версии ≠ 1 →
`SCHEMA_MISMATCH` (если `job_id` читаем); объект S3 не читается → `FILE_READ_ERROR`; исключение парсера →
`UNKNOWN_ERROR` (сообщение без трассировки). Сервис MUST NOT падать и 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 не публикуется
### Requirement: Парсер — заменяемый интерфейс
Разбор файла SHALL выполняться через единый интерфейс «байты + тип файла → документы/метрики/ошибка», не знающий о
NATS и S3; воркер MUST NOT зависеть от конкретной реализации. В Срезе 0 реализация SHALL возвращать `NOT_IMPLEMENTED`
для всех типов; каталог `api/rules/` SHALL существовать с README о назначении словарей.
#### Scenario: Подмена реализации в тесте
- **WHEN** в тесте воркеру передан парсер, возвращающий один `osvDocument`
- **THEN** файл результата с этим документом записан в S3, конверт имеет `status = done` и `result_s3` на этот файл
### 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`).
#### Scenario: NATS перезапущен
- **WHEN** контейнер NATS перезапущен во время работы сервиса
- **THEN** в течение 20 с после старта NATS подписка восстановлена, `nats = ok`, новый запрос обрабатывается