Срез 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>
7.4 KiB
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 идентичность подтверждена, примеры валидны