Срез 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>
70 lines
7.0 KiB
Markdown
70 lines
7.0 KiB
Markdown
# 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"`); файл кладётся как `<job_id>_<original_name>`,
|
||
ответ ожидается в `<output>/<job_id>/<job_id>.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/<uuid>/<uuid>.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
|
||
<!-- openspec/specs пуст -->
|
||
|
||
## 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`.
|