arm-scan-gateway/docs/superpowers/plans/2026-10-03-slice-0-skeleton.md
Andrey Epifancev 58bcae2a57 Срез 0 [6.3]: прогон DoD, предложения для docs и правки .env.example
- open-questions-proposal.md: compose-фрагмент scan-emulator/scan-gateway, таблица предложений по контракту и OQ,
  чеклист для человека (файлы в ../docs не менялись)
- .env.example: SCAN_EMULATOR_POLL_SECONDS; уточнён комментарий PUBLISH_RETRIES (повторы после первой попытки)
- отмечена задача 16 плана и пункт 6.3 tasks.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 04:44:33 +04:00

85 lines
10 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.

# Срез 0 — скелет scan-gateway — план исполнения (Срез 0)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Branch:** `feat/1-slice-0-skeleton` (worktree `.wt/slice-0/scan-gateway`). **Issue:** #1.
**Spec:** `openspec/changes/slice-0-skeleton/` — proposal.md, design.md, specs/*/spec.md этого change'а. Спека — единственный источник истины при рулингах; этот план сгенерирован из её `tasks.md` 1:1 (номера в скобках — исходные номера задач tasks.md; по завершении задачи отметить её `- [x]` и в `tasks.md`).
## Global Constraints
- Сервис без БД и фронта (DECISION-4), **SMB остаётся**. Раскладка шаблона, команды из `api/`.
- Платформа: Python 3.11, NATS **без JetStream** (queue group `scan-gateway`, ack `processing` до чтения S3, LRU `job_id`, публикация с повторами), один бакет S3, Nexus недоступен, CI нет (`make check` / `make check-full`).
- Горячие папки: протокол `HotFolder`, режимы `local` / `smb`, каталоги `SCAN_INPUT_DIR`/`SCAN_OUTPUT_DIR` (по умолчанию `Input`/`Output`), файл `<job_id>_<original_name>`, ответ `<output>/<job_id>/<job_id>.json`; таймаут `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS=300)`, опрос 5 с, `SCAN_MAX_INFLIGHT=4`; повторной отправки в ИС нет (OQ-15).
- Сырой ответ → S3 `raw/{job_id}.json` **до** нормализации, `raw_s3_key` в конверте; в БД не регистрируется. `NullNormalizer`: ошибка ИС → её код, успех → `NOT_IMPLEMENTED`.
- Эмулятор: `python -m src.scan_emulator`, фикстуры `../fixtures/ocr/<name>*.json`, подстановка `file_uid`, неизвестное имя → `INVALID_DOCUMENT`, `.done` после обработки.
- Контракт: копии схем из `../docs/contracts/` байт в байт; `service.name = "scan-gateway"`; `Nats-Msg-Id` по правилам контракта.
- Линтеры: ruff (120, E,F,I,UP,B,PL) + mypy; pytest-asyncio auto; маркеры `integration`/`fixtures`.
- Язык комментариев/коммитов — русский, идентификаторы — английские; коммит заканчивается строкой `Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>`.
## Задачи
### Task 1: [1.1] Удалить `web/`, `setup/.npmrc`, npm-шаги `setup/setup.sh`, `api/alembic/`, `api/
- [x] Удалить `web/`, `setup/.npmrc`, npm-шаги `setup/setup.sh`, `api/alembic/`, `api/alembic.ini`, `api/src/db/`, `api/src/models/`, `api/src/repositories/`; убрать `Dockerfile` из `.gitignore`; проверка: `git status` — только удаления и `.gitignore`
### Task 2: [1.2] `src/app.py`
- [x] `src/app.py`: убрать mount SPA/`os.makedirs("web")`, CORS, импорт `raisa_fastapi_protected_api`; оставить SMB-сессию (только при `SCAN_HOTFOLDER_MODE=smb`); `src/core/config.py`: убрать `DBSettings`; добавить `ENV_FILE`, `SERVICE_NAME="scan-gateway"`, `NATS_ENABLED=true`, `PUBLISH_RETRIES=3`, `DONE_CACHE_SIZE=1000`, `SCAN_HOTFOLDER_MODE="local"`, `SCAN_HOTFOLDER_PATH`, `SCAN_INPUT_DIR="Input"`, `SCAN_OUTPUT_DIR="Output"`, `SCAN_POLL_SECONDS=5`, `SCAN_TIMEOUT_SECONDS=300`, `SCAN_MAX_INFLIGHT=4`; `requirements.txt` без `asyncpg`, `SQLAlchemy`, `alembic`, `argon2-cffi`, `raisa_fastapi_protected_api`; проверка: чистый venv (python3.11), `python run_server.py` с `.env.example`, `/healthcheck` 200
### Task 3: [1.3] `README.md` — описание сервиса (назначение, протокол горячих папок, env, запуск
- [x] `README.md` — описание сервиса (назначение, протокол горячих папок, env, запуск против compose и эмулятора), `CLAUDE.md` — актуализировать «Соглашения шаблона» по факту; проверка: упомянутые файлы существуют
### Task 4: [2.1] `api/pyproject.toml` (ruff, mypy), `requirements-dev.txt` = `-r requirements.txt
- [x] `api/pyproject.toml` (ruff, mypy), `requirements-dev.txt` = `-r requirements.txt` + ruff, mypy, pytest, pytest-asyncio, pytest-env, httpx, jsonschema; `Makefile`: `venv` (python3.11, без `raisa_*`), `lint`, `test`, `test-integration`, `check`, `check-full`, `up`, `emulator`; проверка: `make venv && make lint` зелёные
### Task 5: [2.2] `Dockerfile` (`python
- [x] `Dockerfile` (`python:3.11-slim`; `CMD python run_server.py`, эмулятор — через `command` в compose), `.env.example`, `.env.test`; проверка: `docker build` успешен без Nexus
### Task 6: [3.1] `api/contracts/` — копии схем и `examples/` из `../docs/contracts`
- [x] `api/contracts/` — копии схем и `examples/` из `../docs/contracts`; `src/contract/` — pydantic-модели (конверт со статусами `processing | done | needs_check | error`); `tests/unit/test_contract.py` — идентичность (skip без docs), примеры, round-trip; проверка: тесты зелёные
### Task 7: [4.1] `src/gateway/hotfolder.py`
- [x] `src/gateway/hotfolder.py`: протокол `HotFolder`, `LocalHotFolder` (put/poll/healthy); `tests/unit/test_local_hotfolder.py` на `tmp_path`; проверка: put пишет `<uuid>_<name>`, poll возвращает `None` до появления ответа и байты после
### Task 8: [4.2] `src/gateway/smb.py`
- [x] `src/gateway/smb.py`: `SmbHotFolder` поверх `smbclient` (UNC `\\host\path\Input`), те же методы; unit-тест с подменённым `smbclient` (вызовы `open_file`, `listdir`); проверка: тест зелёный, в режиме `local` модуль не импортируется
### Task 9: [4.3] `src/scan_emulator/__init__.py` + `__main__.py`
- [x] `src/scan_emulator/__init__.py` + `__main__.py`: функция `process_once(root, fixtures, delay)` и цикл; поиск фикстуры по имени/префиксу, подстановка `file_uid`, ошибка `INVALID_DOCUMENT`, переименование в `.done`; `tests/unit/test_emulator.py` — сценарии spec «Точное имя», «Префикс», «Нет пары» на `tmp_path` с копиями `../fixtures/ocr/*.json` (маркер `fixtures`, skip без фикстур); проверка: зелёный
### Task 10: [5.1] `src/normalizer/__init__.py`
- [x] `src/normalizer/__init__.py`: протокол `Normalizer`, `NormalizeOutcome`, `NullNormalizer` (ошибка ИС → код из ответа; успех → `NOT_IMPLEMENTED`; не JSON → `INVALID_FORMAT`); unit-тесты на `aep_5.json`, `aep_8_form4.json`, синтетическом ответе с `error`; проверка: зелёный
### Task 11: [5.2] `src/core/clients.py` (`get_bytes`, `put_json`, `put_bytes`), `src/core/nats.py`
- [x] `src/core/clients.py` (`get_bytes`, `put_json`, `put_bytes`), `src/core/nats.py` (подключение с/без nkeys, автопереподключение, `subscribe_requests(queue="scan-gateway", cb)`, `publish` с `Nats-Msg-Id` и повторами); проверка: unit с фейками, интеграционный тест подключения к compose-NATS без JetStream
### Task 12: [5.3] `src/core/worker.py`
- [x] `src/core/worker.py`: `on_request(msg)`: валидация → LRU `job_id` → ack `processing` → очередь; семафор `SCAN_MAX_INFLIGHT`; `handle(job)`: S3 get → `hotfolder.put` → опрос до дедлайна → `raw/{job_id}.json` → `Normalizer` → результат/конверт → publish; таблица ошибок D-7; `/healthcheck.nats`, `.hotfolder`; проверка: unit-тесты `handle` с фейками и `LocalHotFolder` + `process_once` эмулятора — сценарии spec (ack раньше файла, повторный запрос → один файл, успех → NOT_IMPLEMENTED с `raw_s3_key`, INVALID_DOCUMENT, TIMEOUT с укороченными таймаутами, RECOGNITION_FAILED при отсутствии каталога, SCHEMA_MISMATCH без ack, FILE_READ_ERROR)
### Task 13: [5.4] Тест параллелизма
- [x] Тест параллелизма: пять заданий при `SCAN_MAX_INFLIGHT = 4` — пять ack сразу, четыре файла во входящем каталоге, пятый после освобождения слота (фейковые S3/NATS, `LocalHotFolder`); проверка: зелёный
### Task 14: [6.1] `tests/integration/test_roundtrip.py` (маркер `integration`)
- [x] `tests/integration/test_roundtrip.py` (маркер `integration`): compose с `scan-emulator` — положить `aep_5.pdf` в MinIO `uploads/…`, опубликовать `request-image.json`, собрать `recognition.result`: `processing`, затем `NOT_IMPLEMENTED` с `raw_s3_key`; проверить объект `raw/<job_id>.json` в MinIO; второй запрос с `unknown.pdf` → `INVALID_DOCUMENT`; проверка: зелёный
### Task 15: [6.2] `make check-full`
- [x] `make check-full`: `make check` + `pytest -m integration` + `docker build`; проверка: зелёный против compose docs на ветке `feat/1-slice-0-skeleton`
### Task 16: [6.3] Прогон DoD
- [x] Прогон DoD: `make up` → `make check-full` → ручная проверка из `docs/dev/README.md`; `openspec validate slice-0-skeleton --strict`; вывод `make check-full` — в описание PR