- 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>
85 lines
10 KiB
Markdown
85 lines
10 KiB
Markdown
# Срез 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
|
||
|