Andrey Epifancev 7a35b79d5e docs: change slice-0-skeleton под факты платформы — NATS без JetStream, Python 3.11, без CI и Nexus
Обмен с ядром на core NATS: подписка queue group, ack status=processing сразу по получении, process-local
LRU job_id против повторных запросов, публикация результата с повторами; удалены durable consumer / max_deliver /
ack_wait. Dockerfile python:3.11-slim; make check / make check-full вместо CI; raisa_* фильтруются при
dev-установке. Обновлены proposal, design (решения без обсуждения), specs, tasks, CLAUDE.md, openspec/config.yaml.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 16:21:45 +04:00

90 lines
9.2 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.

# Design: Срез 0 — скелет tabular-parser
## Context
См. `proposal.md`. Исходное состояние — копия каркаса `arm-core` (`124f63b`): FastAPI с lifespan (SMB, MinIO, NATS),
SPA-mount, Alembic, пустые пакеты `models/repositories/services`. Контракт v1.0, ack `processing` и правила `Nats-Msg-Id` —
родительский change в docs. Платформа: Python 3.11, NATS без JetStream, Nexus недоступен до прода, CI в Gitea нет.
Сервис должен остаться запускаемым платформой так же, как шаблон (`api/run_server.py`, `SERVER__*`, `raisa_config.yml`).
## Goals / Non-Goals
**Goals:** сервис без БД/фронта; транспорт NATS ↔ S3 проверен интеграционным тестом, включая повторный запрос; парсер —
заменяемый интерфейс.
**Non-Goals:** см. proposal; масштабирование (несколько инстансов — queue group даёт это бесплатно, но не тестируется).
## Decisions
### D-1. Вырезаем, а не переписываем
Удаляются каталоги и зависимости, перечисленные в proposal; `app.py` упрощается до: настройки, lifespan (MinIO, NATS,
запуск воркера), `/healthcheck`, обработчики ошибок, логирование. Остающиеся файлы шаблона не переименовываются —
чтобы diff с `arm-core` и будущие обновления шаблона были читаемы.
### D-2. Воркер — подписка core NATS с queue group, ack сразу, обработка в очереди задач
`nc.subscribe("recognition.tabular.request", queue="tabular-parser", cb=on_request)`. В `on_request`: валидация →
если `job_id` уже в process-local множестве «в работе/обработано» — только ack; иначе добавить в множество, опубликовать
ack `processing`, положить задание во внутреннюю `asyncio.Queue`. Рабочая корутина (одна на инстанцию, `WORKER_CONCURRENCY=1`)
берёт задания из очереди и выполняет конвейер S3 → парсер → S3 → конверт результата. Множество обработанных `job_id`
ограничено по размеру (`DONE_CACHE_SIZE=1000`, LRU) — защита от повторов ядра после результата. *Почему ack сразу:* ядро
без JetStream публикует повторно через 30 с без ack; долгий разбор не должен вызывать повторы. *Альтернатива* — pull-семантика
JetStream: недоступна.
### D-3. Парсер — протокол `Parser.parse(data: bytes, file_type) -> ParseOutcome`
`ParseOutcome` = `status`, `documents` (модели контракта), `metrics`, `error`. `NullParser` всегда отдаёт
`error NOT_IMPLEMENTED` с сообщением «разбор XLS/XLSX/XLSB реализуется в Срезе 2». Воркер не знает о реализации;
Срез 2 подменяет фабрику. Так транспорт и парсер тестируются раздельно, парсер — без NATS/S3.
### D-4. Ключи S3 и имя сервиса
Исходник читается по `request.s3.key` (роль `uploads`), результат пишется в `results/{file_id}/{job_id}/result.json`
того же бакета; `service = {name: "tabular-parser", version: settings.VERSION}`. Клиент S3 — `S3Storage` шаблона,
дополненный `get_bytes`/`put_json`.
### D-5. Любая ошибка транспорта — результат, не молчание
Таблица: запрос не проходит pydantic → `SCHEMA_MISMATCH` (без ack — нечего подтверждать, но результат публикуется с
`job_id`, если он читаем); `MAJOR` версии запроса ≠ 1 → `SCHEMA_MISMATCH`; S3 `get` упал → `FILE_READ_ERROR`;
исключение парсера → `UNKNOWN_ERROR` с текстом исключения в `error.message` (без трассировки). Публикуется конверт без
`result_s3`; `metrics.duration_ms` заполняется всегда. Публикация результата повторяется до `PUBLISH_RETRIES=3` раз с
паузой 2 с; если не удалось — лог уровня error, задание дойдёт до `timeout` в ядре.
### D-6. Соединение с NATS — как у ядра, без JetStream
Пустые `NATS_SEED/NATS_JWT` → без учётных данных (dev); иначе nkeys/JWT шаблона. Автопереподключение `nats-py`
(`max_reconnect_attempts=-1`); при разрыве подписка восстанавливается клиентом. `/healthcheck` отдаёт `nats = ok | down`.
Флаг `NATS_ENABLED` (по умолчанию `true`) — чтобы unit-тесты поднимали приложение без NATS.
### D-7. Тесты в два уровня
Unit (`tests/unit`): модели контракта и примеры docs; обработчик запроса с фейками `S3Storage`/`Publisher` (протоколы) —
сценарии D-5, повторный запрос, ack до обработки. Интеграционные (`tests/integration`, маркер `integration`):
compose-NATS/MinIO — реальный запрос из примера `request-tabular.json` → ack и результат в `recognition.result`, файл в бакете.
### D-8. Dev без Nexus, Python 3.11
`Dockerfile` — `python:3.11-slim`; `make venv` ставит `requirements.txt` без строк `raisa_*` (в этом сервисе их уже нет
после D-1, но цель одинакова во всех репозиториях). Конфигураций CI нет: `make check` / `make check-full`.
## Risks / Trade-offs
- [Повторный запрос ядра после ack при долгой обработке] → ack публикуется до чтения S3; ядро повторяет только без ack.
- [Инстанция упала после ack, до результата] → ядро закроет задание по `reply_deadline` (`timeout`), replay — Срез 3+.
- [`NOT_IMPLEMENTED` в проде, если Срез 2 задержится] → код ошибки честный, ядро показывает его аналитику.
- [Удалённые зависимости понадобятся позже] → возвращаются по одной строке в `requirements.txt`.
## Migration Plan
Нет данных. Удаление каталогов — одним коммитом «chore: вырезан фронт и БД из шаблона» до функциональных изменений,
чтобы ревью было читаемым. Откат — revert коммита.
## Open Questions
Общие с родительским change (версия Node не нужна этому сервису; параметры NATS банка — к деплою). Специфичных нет.
## Decisions made without discussion
| № | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| 1 | 2026-10-03 | D-1: вырезать фронт/БД/SMB, не переименовывая оставшееся | читаемый diff с arm-core | нет |
| 2 | 2026-10-03 | D-2: queue group `tabular-parser`, ack до обработки, одна рабочая корутина, LRU обработанных `job_id` | NATS без JetStream (ответ 03.10); ядро повторяет без ack | смена параметров; при JetStream — pull-consumer |
| 3 | 2026-10-03 | D-3: парсер-заглушка отвечает `NOT_IMPLEMENTED` (код добавлен в контракт родительским change'ем) | честный статус вместо маскировки под `UNSUPPORTED_FORMAT` | нет |
| 4 | 2026-10-03 | D-4: результат в `results/{file_id}/{job_id}/result.json` того же бакета | схема ключей `03-architecture.md`, один бакет | смена шаблона ключа в одном месте |
| 5 | 2026-10-03 | D-5: транспортные ошибки превращаются в результат; публикация с 3 повторами | ядро не должно ждать таймаута, если сервис жив | нет |
| 6 | 2026-10-03 | D-8: `python:3.11-slim`, `make check`/`check-full` вместо CI | ответы пользователя | смена тега |
| 7 | 2026-10-03 | Каталог словарей — `api/rules/` (относительно рабочего каталога `api/`) | все команды шаблона идут из `api/` | перенос каталога |