# Spec Delta: tabular-recognition-service ## Purpose Stateless-сервис разбора табличных файлов (XLS/XLSX/XLSB): принимает задания из NATS, читает исходник из 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` durable pull-consumer'ом `tabular-parser`; для каждого запроса, проходящего `recognition-request.schema.json`, SHALL прочитать объект `s3.key` из бакета `s3.bucket`, вызвать парсер, записать файл результата (по `recognition-result.schema.json`) в `results/{file_id}/{job_id}/result.json`, затем опубликовать конверт в `recognition.result` (по `recognition-result-message.schema.json`) с заголовком `Nats-Msg-Id = result::` и `service.name = "tabular-parser"`, и только после публикации подтвердить запрос. #### Scenario: Запрос с заглушкой парсера (Срез 0) - **WHEN** опубликован запрос из `docs/contracts/examples/request-tabular.json` с существующим объектом в `uploads/` - **THEN** в `recognition.result` появляется конверт `status = error`, `error.code = NOT_IMPLEMENTED`, `job_id`/`file_id` из запроса, `metrics.duration_ms ≥ 0`; файл результата может отсутствовать; запрос подтверждён #### Scenario: Ack только после результата - **WHEN** публикация конверта завершается ошибкой - **THEN** запрос не подтверждается (nak с задержкой), следующая доставка повторяет обработку; дубликат конверта отбрасывается по `Nats-Msg-Id` ### Requirement: Ошибки транспорта превращаются в результат Сервис SHALL отвечать конвертом `status = error` и подтверждать запрос в случаях: тело не проходит схему или `MAJOR` версии ≠ 1 → `SCHEMA_MISMATCH`; объект 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`, запрос подтверждён ### 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, проходящий схему. #### Scenario: CI сервиса - **WHEN** выполняются тесты контракта при доступном `../docs` (или `ARM_KA_DOCS`) - **THEN** идентичность подтверждена, примеры валидны; при недоступном docs тест идентичности пропущен с предупреждением ### Requirement: Соединение с NATS по правилам шаблона При пустых `NATS_SEED`/`NATS_JWT` сервис SHALL подключаться без учётных данных, иначе — с nkeys/JWT. Отсутствие stream `RECOGNITION` SHALL приводить к повторным попыткам подписки каждые 10 с и `nats = down` в `/healthcheck`, а не к падению. #### Scenario: Stream появился позже - **WHEN** сервис стартовал раньше ядра, создавшего stream - **THEN** в течение 20 с после создания stream воркер подписан, `nats = ok`