# Proposal: Срез 0 — скелет scan-gateway **Срез:** 0 «Скелет» (`../docs/context/09-delivery-plan.md`). **Репозиторий:** scan-gateway. **Issue:** RSHB/arm-scan-gateway#1. **Родительский change:** `../docs/openspec/changes/slice-0-skeleton` (контракт v1.0, compose, CI-шаблон). **Соседние:** `../arm-core/openspec/changes/slice-0-skeleton`, `../tabular-parser/openspec/changes/slice-0-skeleton`. ## Why Репозиторий — копия каркаса `arm-core` с фронтом и БД, тогда как по DECISION-4 это stateless-адаптер внешней ИС Сканирование. Самая рискованная часть адаптера — не разбор JSON (Срез 3), а обмен через горячие папки с таймаутом и без доступа к БД: положить файл, дождаться ответа, не потерять задание. Срез 0 строит этот конвейер и эмулятор ИС Сканирование, чтобы он работал в compose без доступа к банковской системе. ## What Changes - **Вырезать из шаблона** (список — `CLAUDE.md`, «Соглашения шаблона»): `web/`, `setup/.npmrc` и npm-шаги `setup/setup.sh`; из `app.py` — mount SPA, CORS, импорт `raisa_fastapi_protected_api`; `db/`, `models/`, `repositories/`, `alembic/`, `alembic.ini`; настройка `OPENBAO.DATABASE`; зависимости `asyncpg`, `SQLAlchemy`, `alembic`, `argon2-cffi`, `raisa_fastapi_protected_api`; шаблонный `README.md`. **SMB остаётся** (`smbprotocol`, `OPENBAO.SMB`, сессия в lifespan). - **Инструменты**: `api/pyproject.toml` (ruff, mypy), `requirements-dev.txt` = `-r requirements.txt` + инструменты, `Dockerfile` (строка убрана из `.gitignore`), `Makefile`, `.env.example`, `.env.test`; `Settings` читает `ENV_FILE`; `SERVICE_NAME = "scan-gateway"`. - **Контракт**: вендоренные схемы `api/contracts/` + pydantic-модели `src/contract/`; тест идентичности с `../docs/contracts`. - **Горячие папки** `src/gateway/`: протокол `HotFolder` (`put(name, data)`, `poll(uuid) -> bytes | None`) с двумя реализациями — `LocalHotFolder` (каталог; dev и тесты) и `SmbHotFolder` (`smbclient` шаблона; прод). Имена подкаталогов настраиваются (`SCAN_INPUT_DIR = "Input"`, `SCAN_OUTPUT_DIR = "Output"`); файл кладётся как `_`, ответ ожидается в `//.json`. - **Воркер** `src/core/worker.py`: durable pull-consumer `scan-gateway` на `recognition.image.request` (`max_deliver` 1 — OQ-15), до `SCAN_MAX_INFLIGHT` (4) заданий параллельно: S3 `uploads/…` → горячая папка → опрос каждые `SCAN_POLL_SECONDS` (5) до `min(reply_deadline, now + SCAN_TIMEOUT_SECONDS=300)` → сырой ответ в S3 `raw/{job_id}.json` → нормализатор → `results/{file_id}/{job_id}/result.json` → конверт `recognition.result` (`raw_s3_key` заполнен) → ack. Таймаут → `error.code = TIMEOUT`. - **Нормализатор как интерфейс** `src/normalizer/`: протокол `Normalizer(raw_json) -> documents | error`; в Срезе 0 — `NullNormalizer`: ошибки Content AI (`error.code` из ответа: `FILE_READ_ERROR`, `INVALID_FORMAT`, `INVALID_DOCUMENT`, `RECOGNITION_FAILED`, `UNKNOWN_ERROR`) пробрасываются как есть; успешный ответ → `NOT_IMPLEMENTED` (Срез 3 реализует §11). - **Эмулятор ИС Сканирование** `src/scan_emulator/` (`python -m src.scan_emulator`): следит за `Input/`, через `SCAN_EMULATOR_DELAY` (2 с) кладёт в `Output//.json` содержимое `SCAN_FIXTURES_DIR/<имя без uuid и расширения>*.json` (`aep_5.pdf` → `aep_5.json`, `aep_8.pdf` → `aep_8_form4.json`), для неизвестных имён — ответ Content AI с `error.code = INVALID_DOCUMENT`. Запускается тем же образом в compose (`scan-emulator`). - **Ошибки транспорта** → результат: запрос не по схеме → `SCHEMA_MISMATCH`; S3 не читается → `FILE_READ_ERROR`; горячая папка недоступна → `RECOGNITION_FAILED`; ответ ИС не JSON → `INVALID_FORMAT`; исключение → `UNKNOWN_ERROR`. - **Healthcheck** дополняется полями `nats` и `hotfolder` (`ok`/`down`). - **CI**: `.gitea/workflows/ci.yml` из шаблона docs. ## Capabilities ### New Capabilities - `image-recognition-gateway`: адаптер ИС Сканирование — приём задания из NATS, обмен файлами через горячие папки с таймаутом, сохранение сырого ответа, результат по контракту v1.0. - `scan-emulator`: эмулятор ИС Сканирование для разработки и тестов, воспроизводящий протокол горячих папок на фикстурах `../fixtures/ocr`. ### Modified Capabilities ## Impact - **Таблицы DBML:** сервис не пишет в БД. Косвенно: `recognition_job` ядра получает `status`, `error_code` (`TIMEOUT`, `NOT_IMPLEMENTED`, коды Content AI), `service_name = scan-gateway`, `external_task_id` — не передаётся (у ИС Сканирование идентификатор = наш `job_id` в имени файла). Сырой ответ `raw/{job_id}.json` в БД не регистрируется (v5.13 п. 5). - **Правила:** §11 — только как контракт нормализатора; §9.4 — статусы задания; OQ-13/14/15 — допущения в коде нормализатора (Срез 3). - **Зависимости:** удаляются БД/фронт; остаётся `smbprotocol`; добавляются `ruff`, `mypy`, `pytest*`, `jsonschema`. - **Внешние системы:** NATS JetStream, MinIO/S3, SMB-шара ИС Сканирование (прод) / локальный каталог (dev). ## Non-goals - Нормализация ответа Content AI в `statementDocument` (§11, OQ-13, OQ-14) — Срез 3. - PNG/JPG — транспорт не различает типы, но проверяется только PDF (пары в `fixtures/ocr`). - Повторы OCR (`max_attempts` = 1 по OQ-15), heartbeat `processing`. - Реальная SMB-шара в тестах — `SmbHotFolder` покрыт только unit-тестами с подменой `smbclient`.