Andrey Epifancev 5dbb4dab20 Срез 0 [7.0]: change обновлён под решения человека 04.10.2026
Склейка всех json каталога ответа (settle), SCAN_TMP_DIR вне Input, успех = error.code == "",
per-file ошибки в metrics.warnings, набор файлов в эмуляторе; решения №54-62 в design.md;
OQ-76/77/78/79 отмечены решёнными; группа 7 в tasks.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 13:38:48 +04:00

86 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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, проверки качества).
**Соседние:** `../arm-core/openspec/changes/slice-0-skeleton`, `../tabular-parser/openspec/changes/slice-0-skeleton`.
## Why
Репозиторий — копия каркаса `arm-core` с фронтом и БД, тогда как по DECISION-4 это stateless-адаптер внешней ИС
Сканирование. Самая рискованная часть адаптера — не разбор JSON (Срез 3), а обмен через горячие папки с таймаутом и
без доступа к БД: положить файл, дождаться ответа, не потерять задание — причём на NATS без JetStream. Срез 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` (`python:3.11-slim`; строка убрана из `.gitignore`), `Makefile` с общими целями + `venv`, `emulator`,
`.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(job_id) -> list[AnswerFile] | None`,
`healthy()`) с двумя реализациями — `LocalHotFolder` (каталог; dev и тесты) и `SmbHotFolder` (`smbclient` шаблона;
прод). Имена подкаталогов настраиваются (`SCAN_INPUT_DIR = "Input"`, `SCAN_OUTPUT_DIR = "Output"`, `SCAN_TMP_DIR =
"Tmp"`); файл кладётся как `<job_id>_<original_name>`; временный файл атомарной записи (`.part`) лежит в служебном
каталоге `<корень>/<SCAN_TMP_DIR>/` рядом с `Input`/`Output` (на той же шаре/ФС) и переименовывается в `Input`, так что
в `Input` недописанных файлов не бывает (OQ-79). ИС кладёт **все** распознанные документы задания одновременно в
`<output>/<job_id>/` — по одному `*.json` на документ (OQ-78); `poll` возвращает все `*.json` каталога по возрастанию
имени, когда набор готов: есть хотя бы один `*.json`, а набор имён и размеров не менялся `SCAN_SETTLE_SECONDS` (2).
- **Воркер** `src/core/worker.py` (NATS без JetStream): подписка на `recognition.image.request` в queue group
`scan-gateway`; по получении — сразу ack `processing` в `recognition.result`; до `SCAN_MAX_INFLIGHT` (4) заданий
параллельно: S3 `uploads/…` → горячая папка → опрос каждые `SCAN_POLL_SECONDS` (5) до
`min(reply_deadline, now + SCAN_TIMEOUT_SECONDS=300)` → склейка всех `*.json` ответа в один сырой ответ
`{"job_id", "files": [{"name", "response", "parse_error"}]}` в S3 `raw/{job_id}.json` → нормализатор →
`results/{file_id}/{job_id}/result.json` → конверт результата (`raw_s3_key` заполнен). Таймаут → `error.code = TIMEOUT`.
Process-local множество `job_id` в работе: повторный запрос получает ack и не создаёт второй файл в горячей папке.
- **Нормализатор как интерфейс** `src/normalizer/`: протокол `Normalizer(merged) -> documents | error`, вход — склеенный
ответ; в Срезе 0 — `NullNormalizer`. Успех файла ИС — `error.code == ""` (поле `error` не `null`: у реального успешного
ответа `error = {"code": "", "message": ""}`); ошибка — непустой `error.code` (`FILE_READ_ERROR`, `INVALID_FORMAT`,
`INVALID_DOCUMENT`, `RECOGNITION_FAILED`, `UNKNOWN_ERROR`) либо `parse_error` (файл не разобран как JSON). Если ошибочны
**все** файлы — результат `error` с кодом первого ошибочного файла по имени (для `parse_error` — `INVALID_FORMAT`),
каждый ошибочный файл — строка `<имя>: <код>: <сообщение>` в `metrics.warnings`. Есть хотя бы один успешный файл →
`NOT_IMPLEMENTED` (Срез 3 реализует §11), ошибочные файлы — в `metrics.warnings` так же.
- **Эмулятор ИС Сканирование** `src/scan_emulator/` (`python -m src.scan_emulator`): следит за `Input/`, через
`SCAN_EMULATOR_DELAY` (2 с) кладёт в `Output/<uuid>/` **набор** ответов: все `SCAN_FIXTURES_DIR/<name>.json` и
`<name>_*.json` (`<name>` — имя без uuid и расширения; не префикс `<name>*`) под именами `<uuid>.json` и
`<uuid>_<suffix>.json`, с `file_uid = <uuid>` (`aep_3.pdf` → `<uuid>.json` + `<uuid>_ofr.json`, `aep_6.pdf` → один
файл, `aep_8.pdf` → `<uuid>_form4.json`); набор появляется в `Output/<uuid>/` атомарно целиком (временный каталог +
переименование каталога). Для неизвестных имён — один ответ Content AI с `error.code = INVALID_DOCUMENT`. Запускается
тем же образом в compose (`scan-emulator`).
- **Ошибки транспорта** → результат: запрос не по схеме → `SCHEMA_MISMATCH`; S3 не читается → `FILE_READ_ERROR`;
горячая папка недоступна → `RECOGNITION_FAILED`; ответ ИС не JSON (после готовности набора) → `parse_error` файла,
при ошибочности всех файлов — `INVALID_FORMAT`; исключение → `UNKNOWN_ERROR`.
- **Healthcheck** дополняется полями `nats` и `hotfolder` (`ok`/`down`).
- **Проверки качества** вместо CI: `make check` и `make check-full` по `../docs/dev/checks.md`.
## Capabilities
### New Capabilities
- `image-recognition-gateway`: адаптер ИС Сканирование — приём и подтверждение задания из NATS без JetStream, обмен
файлами через горячие папки с таймаутом, сохранение сырого ответа, результат по контракту v1.0.
- `scan-emulator`: эмулятор ИС Сканирование для разработки и тестов, воспроизводящий протокол горячих папок на фикстурах `../fixtures/ocr`.
### Modified Capabilities
<!-- openspec/specs пуст -->
## Impact
- **Таблицы DBML:** сервис не пишет в БД. Косвенно: `recognition_job` ядра получает `status = processing` (`started_at`),
затем `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`.
- Конфигурации CI.