Срез 0 (issue #1): раздел «Соглашения шаблона» (что оставлено от шаблона команды, что вырезается, что добавляется); change openspec/changes/slice-0-skeleton — proposal, design с таблицей «Decisions made without discussion», спеки и tasks; в openspec/config.yaml строки с двоеточием взяты в кавычки (YAML разбирал их как словари). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
87 lines
7.4 KiB
Markdown
87 lines
7.4 KiB
Markdown
# Spec Delta: image-recognition-gateway
|
||
|
||
## Purpose
|
||
|
||
Адаптер внешней ИС Сканирование (Content AI) для PDF/PNG/JPG: принимает задание из NATS, передаёт файл через горячие
|
||
папки, ждёт ответ не дольше таймаута, сохраняет сырой ответ в S3 и отвечает ядру по контракту v1.0. Без БД и фронта.
|
||
|
||
## ADDED Requirements
|
||
|
||
### Requirement: Сервис не зависит от БД и фронта
|
||
Репозиторий MUST NOT содержать `web/`, `alembic/`, `src/db/`, `src/models/`, `src/repositories/`; зависимости `SQLAlchemy`,
|
||
`asyncpg`, `alembic`, `raisa_fastapi_protected_api` MUST NOT присутствовать; `smbprotocol` SHALL остаться. Для старта
|
||
SHALL хватать `OPENBAO__MINIO__*`, `OPENBAO__NATS__NATS_URL`, `SCAN_HOTFOLDER_MODE=local`, `SCAN_HOTFOLDER_PATH`.
|
||
|
||
#### Scenario: Старт в режиме локальных папок
|
||
- **WHEN** сервис запущен с `SCAN_HOTFOLDER_MODE=local` и существующим каталогом `SCAN_HOTFOLDER_PATH`
|
||
- **THEN** `GET /healthcheck` → 200 с полями `uptime`, `version`, `nats`, `hotfolder = ok`; `OPENBAO__SMB__*` не требуются
|
||
|
||
### Requirement: Передача файла в ИС Сканирование через горячие папки
|
||
Для запроса `recognition.image.request`, проходящего схему, сервис SHALL прочитать исходник из S3 (`s3.bucket/s3.key`) и
|
||
записать его во входящий каталог как `<job_id>_<original_name>`; затем SHALL опрашивать исходящий каталог
|
||
`<SCAN_OUTPUT_DIR>/<job_id>/<job_id>.json` каждые `SCAN_POLL_SECONDS` до `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS)`.
|
||
|
||
#### Scenario: Файл положен во входящий каталог
|
||
- **WHEN** получен запрос для `aep_5.pdf` с `job_id = 3f1c…`
|
||
- **THEN** во входящем каталоге появляется `3f1c…_aep_5.pdf`, байт в байт равный объекту S3
|
||
|
||
#### Scenario: Ответ получен
|
||
- **WHEN** в `Output/3f1c…/3f1c….json` появляется ответ ИС до истечения таймаута
|
||
- **THEN** опрос прекращается не позднее чем через `SCAN_POLL_SECONDS`, ответ передаётся дальше по конвейеру
|
||
|
||
### Requirement: Сырой ответ сохраняется в S3 до нормализации
|
||
Полученный ответ ИС SHALL быть записан как есть в `raw/{job_id}.json` того же бакета **до** вызова нормализатора;
|
||
ключ SHALL передаваться в конверте результата полем `raw_s3_key`; в БД объект не регистрируется (ядро его не создаёт).
|
||
|
||
#### Scenario: Нормализатор упал
|
||
- **WHEN** ответ ИС получен, а нормализатор бросил исключение
|
||
- **THEN** объект `raw/{job_id}.json` существует в S3, конверт содержит `raw_s3_key` и `error.code = UNKNOWN_ERROR`
|
||
|
||
### Requirement: Результат по контракту и подтверждение
|
||
Сервис SHALL публиковать в `recognition.result` конверт по `recognition-result-message.schema.json` с
|
||
`service.name = "scan-gateway"`, `Nats-Msg-Id = result:<job_id>:<attempt>`; при `status ≠ error` — файл результата в
|
||
`results/{file_id}/{job_id}/result.json` и `result_s3`; запрос SHALL подтверждаться только после публикации конверта.
|
||
В Срезе 0 нормализатор SHALL возвращать: при `error ≠ null` в ответе ИС — `status = error` с кодом из ответа; иначе —
|
||
`error.code = NOT_IMPLEMENTED`.
|
||
|
||
#### Scenario: Успешный ответ ИС в Срезе 0
|
||
- **WHEN** ответ ИС содержит `data_ocr` без `error`
|
||
- **THEN** конверт `status = error`, `error.code = NOT_IMPLEMENTED`, `raw_s3_key = raw/<job_id>.json`, запрос подтверждён
|
||
|
||
#### Scenario: Ошибка ИС
|
||
- **WHEN** ответ ИС содержит `error.code = INVALID_DOCUMENT`
|
||
- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT`, `error.message` из ответа, `raw_s3_key` заполнен
|
||
|
||
### Requirement: Таймаут ожидания ответа
|
||
Если ответ не появился до дедлайна, сервис SHALL опубликовать конверт `status = error`, `error.code = TIMEOUT` и
|
||
подтвердить запрос; повторная отправка файла в ИС MUST NOT выполняться (consumer `max_deliver = 1`).
|
||
|
||
#### Scenario: ИС молчит
|
||
- **WHEN** запрос с `reply_deadline` через 10 с, ответ не появляется
|
||
- **THEN** через ≤ 15 с опубликован конверт `TIMEOUT`, входящий файл остаётся в каталоге, запрос подтверждён
|
||
|
||
### Requirement: Ошибки транспорта превращаются в результат
|
||
Сервис SHALL отвечать конвертом `status = error` с кодом по таблице: запрос не по схеме или `MAJOR` ≠ 1 →
|
||
`SCHEMA_MISMATCH`; объект S3 не читается → `FILE_READ_ERROR`; запись во входящий каталог не удалась →
|
||
`RECOGNITION_FAILED`; ответ ИС не разбирается как JSON → `INVALID_FORMAT`; иное исключение → `UNKNOWN_ERROR`.
|
||
Во всех случаях конверт SHALL быть опубликован, запрос подтверждён, процесс MUST NOT завершаться.
|
||
|
||
#### Scenario: Горячая папка недоступна
|
||
- **WHEN** каталог `SCAN_HOTFOLDER_PATH` не существует в момент обработки
|
||
- **THEN** конверт `RECOGNITION_FAILED` с сообщением о недоступности горячей папки; `/healthcheck.hotfolder = down`
|
||
|
||
### Requirement: Параллельная обработка ограничена
|
||
Сервис SHALL обрабатывать не более `SCAN_MAX_INFLIGHT` (по умолчанию 4) заданий одновременно; `ack_wait` consumer'а
|
||
SHALL превышать `SCAN_TIMEOUT_SECONDS` не менее чем на 120 с.
|
||
|
||
#### Scenario: Пять заданий подряд
|
||
- **WHEN** в очереди пять запросов при `SCAN_MAX_INFLIGHT = 4`
|
||
- **THEN** четыре файла появляются во входящем каталоге сразу, пятый — после завершения любого из четырёх
|
||
|
||
### Requirement: Копия контракта идентична источнику
|
||
`api/contracts/*.schema.json` SHALL быть идентичны `../docs/contracts/`; pydantic-модели SHALL валидировать примеры docs.
|
||
|
||
#### Scenario: CI сервиса
|
||
- **WHEN** выполняются тесты контракта при доступном `../docs`
|
||
- **THEN** идентичность подтверждена, примеры валидны
|