Andrey Epifancev ee4359d535 Срез 0 [7.1]: отметка выполнения Task 2 в плане и задачи 7.1 в tasks.md
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 14:01:39 +04:00

110 lines
12 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.

# Tasks: Срез 0 — скелет scan-gateway
Репозиторий — scan-gateway; команды — из `api/`. Зависимости: 3.1 ждёт контракт v1.0 (`docs` 1.1–1.4); интеграционные
тесты 6.x — compose (`docs` 2.1, 5.3). Каждая задача ≤ половины дня. CI нет — проверки локальные.
## 1. Вырезать фронт и БД из шаблона (SMB остаётся)
- [x] 1.1 Удалить `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`
- [x] 1.2 `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
- [x] 1.3 `README.md` — описание сервиса (назначение, протокол горячих папок, env, запуск против compose и эмулятора),
`CLAUDE.md` — актуализировать «Соглашения шаблона» по факту; проверка: упомянутые файлы существуют
## 2. Инструменты
- [x] 2.1 `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` зелёные
- [x] 2.2 `Dockerfile` (`python:3.11-slim`; `CMD python run_server.py`, эмулятор — через `command` в compose),
`.env.example`, `.env.test`; проверка: `docker build` успешен без Nexus
## 3. Контракт
- [x] 3.1 `api/contracts/` — копии схем и `examples/` из `../docs/contracts`; `src/contract/` — pydantic-модели (конверт со
статусами `processing | done | needs_check | error`); `tests/unit/test_contract.py` — идентичность (skip без docs),
примеры, round-trip; проверка: тесты зелёные
## 4. Горячие папки и эмулятор
- [x] 4.1 `src/gateway/hotfolder.py`: протокол `HotFolder`, `LocalHotFolder` (put/poll/healthy); `tests/unit/test_local_hotfolder.py`
на `tmp_path`; проверка: put пишет `<uuid>_<name>`, poll возвращает `None` до появления ответа и байты после
- [x] 4.2 `src/gateway/smb.py`: `SmbHotFolder` поверх `smbclient` (UNC `\\host\path\Input`), те же методы; unit-тест с
подменённым `smbclient` (вызовы `open_file`, `listdir`); проверка: тест зелёный, в режиме `local` модуль не импортируется
- [x] 4.3 `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 без фикстур); проверка: зелёный
## 5. Воркер и нормализатор-заглушка
- [x] 5.1 `src/normalizer/__init__.py`: протокол `Normalizer`, `NormalizeOutcome`, `NullNormalizer` (ошибка ИС → код из
ответа; успех → `NOT_IMPLEMENTED`; не JSON → `INVALID_FORMAT`); unit-тесты на `aep_5.json`, `aep_8_form4.json`,
синтетическом ответе с `error`; проверка: зелёный
- [x] 5.2 `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
- [x] 5.3 `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)
- [x] 5.4 Тест параллелизма: пять заданий при `SCAN_MAX_INFLIGHT = 4` — пять ack сразу, четыре файла во входящем каталоге,
пятый после освобождения слота (фейковые S3/NATS, `LocalHotFolder`); проверка: зелёный
## 6. Интеграция, проверки, завершение
- [x] 6.1 `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`; проверка: зелёный
- [x] 6.2 `make check-full`: `make check` + `pytest -m integration` + `docker build`; проверка: зелёный против compose docs
на ветке `feat/1-slice-0-skeleton`
- [x] 6.3 Прогон DoD: `make up` → `make check-full` → ручная проверка из `docs/dev/README.md`; `openspec validate
slice-0-skeleton --strict`; вывод `make check-full` — в описание PR
## 7. Доработка по реальным ответам ИС
Основание: решения человека 04.10.2026 (OQ-76/77/78/79), план `docs/superpowers/plans/2026-10-04-multi-file-response.md`,
design №54–66. Контракт v1.0 не меняется; нормализация в `statementDocument` — Срез 3.
Формулировки уже закрытых задач 4.3, 5.1, 5.3, 6.1 (поиск фикстуры по префиксу `<name>*`, `aep_5.pdf` как единственный
сценарий, единственный файл `<job_id>.json`, «не JSON → `INVALID_FORMAT`» без склейки) заменены задачами 7.x и
обновлёнными спеками; действует группа 7.
- [x] 7.0 Привести change в соответствие с решениями человека: `proposal.md`, `design.md` (D-1, D-3–D-6, решения №54–62),
`specs/image-recognition-gateway/spec.md`, `specs/scan-emulator/spec.md`, `open-questions-proposal.md`
(OQ-76/77/78/79 решены), эта группа; проверка: `openspec validate slice-0-skeleton --strict` — valid; в спеках нет
«читает ровно `<job_id>.json`», `.part` в `Input`, `data_ocr` как списка и успеха по `error != null`
- [x] 7.1 Горячие папки: `SCAN_TMP_DIR` (`Tmp`), `SCAN_SETTLE_SECONDS` (2); `HotFolder.poll(job_id)` возвращает `None` либо
список `AnswerFile(name, data)` — все `*.json` каталога ответа по имени после settle; `.part` создаётся в `<корень>/Tmp/`
(`LocalHotFolder`, `SmbHotFolder`), в `Input` — только итоговый файл; проверка: unit-тесты на `tmp_path` (нет `.part` в
`Input`; нет каталога → `None`; один json; два json по имени; дописываемый файл → `None` до стабилизации; посторонний файл
игнорируется; `SmbHotFolder` с подменённым `smbclient`), `make check` зелёный
- [ ] 7.2 Склейка, сырой файл, нормализатор, воркер: `merge_answers(job_id, files)`; `raw/{job_id}.json` = склеенный ответ;
`NullNormalizer` по правилам D-5 (успех = `error.code == ""`, per-file ошибки в `metrics.warnings`, `NOT_IMPLEMENTED`
при хотя бы одном успешном файле); проверка: unit-тесты `merge_answers` (порядок, невалидный json → `parse_error`),
нормализатор на реальных фикстурах `aep_6.json`, `aep_3.json` (не ошибка), `aep_3_ofr.json`, `aep_5.json`
(`INVALID_DOCUMENT`), `aep_3.json` + `aep_3_ofr.json` в склейке под именами `<job_id>.json` / `<job_id>_ofr.json` →
`NOT_IMPLEMENTED` с одной строкой warnings `<job_id>_ofr.json: INVALID_DOCUMENT: …`, воркер с фейками; `make check` зелёный
- [ ] 7.3 Эмулятор: набор файлов на задание — `<name>.json` и `<name>_*.json` в `Output/<uuid>/` как `<uuid>.json` /
`<uuid>_<suffix>.json` с `file_uid = <uuid>`, атомарная запись каталога; проверка: unit-тесты (маркер `fixtures`) —
`aep_3.pdf` → два файла, `aep_6.pdf` → один, `aep_5.PDF` → один с ошибкой, `aep_8.pdf` → `<uuid>_form4.json`, `aep_30*` не
попадает в набор `aep_3`, неизвестное имя → `INVALID_DOCUMENT`; сквозной «put → эмулятор → poll (с settle) → склейка» на
`aep_3`; `make check` зелёный
- [ ] 7.4 Интеграционные тесты и документация: реальные NATS и MinIO — `aep_3.pdf` → `NOT_IMPLEMENTED` с warnings про
`<job_id>_ofr.json` и двумя файлами в `raw/`, `aep_6.pdf` → `NOT_IMPLEMENTED` без warnings, `aep_5.PDF` → `INVALID_DOCUMENT`;
`README.md`, `CLAUDE.md`, `.env.example`, `.env.test` актуализированы (`SCAN_TMP_DIR`, `SCAN_SETTLE_SECONDS`, склеенный raw);
проверка: `make check-full` (при поднятом `make up`) — exit 0, вывод в отчёт
- [ ] 7.5 Проверка в реальном compose (без правок `../docs`): сервисы `scan-gateway` и `scan-emulator` собираются и стартуют,
сценарии `aep_3.pdf`, `aep_6.pdf`, `aep_5.PDF` проходят способом из `../docs/dev/README.md`, содержимое
`raw/<job_id>.json` зафиксировано; окружение остановлено (`down -v`); необходимые правки compose (`SCAN_TMP_DIR`,
`SCAN_SETTLE_SECONDS`) — точным фрагментом в `open-questions-proposal.md`; проверка: отчёт со снимками выводов