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

9.6 KiB
Raw Blame History

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

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.