# Design: Срез 0 — скелет tabular-parser ## Context См. `proposal.md`. Исходное состояние — копия каркаса `arm-core` (`124f63b`): FastAPI с lifespan (SMB, MinIO, NATS), SPA-mount, Alembic, пустые пакеты `models/repositories/services`. Контракт v1.0 и правила `Nats-Msg-Id` — родительский change в docs. Сервис должен остаться запускаемым платформой так же, как шаблон (`api/run_server.py`, `SERVER__*`, `raisa_config.yml`). ## Goals / Non-Goals **Goals:** сервис без БД/фронта; транспорт NATS ↔ S3 проверен интеграционным тестом; парсер — заменяемый интерфейс. **Non-Goals:** см. proposal; масштабирование (несколько инстансов — queue-семантика pull-consumer'а даёт это бесплатно, но не тестируется). ## Decisions ### D-1. Вырезаем, а не переписываем Удаляются каталоги и зависимости, перечисленные в proposal; `app.py` упрощается до: настройки, lifespan (MinIO, NATS, запуск воркера), `/healthcheck`, обработчики ошибок, логирование. Остающиеся файлы шаблона не переименовываются — чтобы diff с `arm-core` и будущие обновления шаблона были читаемы. ### D-2. Воркер — pull-consumer в lifespan, одно сообщение за раз на инстанцию `JetStream.pull_subscribe("recognition.tabular.request", durable="tabular-parser")`, цикл `fetch(1, timeout=5)`; `ack_wait` 10 мин (большие ОСВ), `max_deliver` 3 (OQ-15). Ack — после успешной публикации конверта результата; при падении до публикации сообщение вернётся (повтор ≤ 3), при третьем провале ядро получит таймаут по `reply_deadline`. *Альтернатива* — push-consumer с очередью: сложнее контролировать параллелизм и ack. ### 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, чтобы не зациклить); `MAJOR` версии запроса ≠ 1 → `SCHEMA_MISMATCH`; S3 `get` упал → `FILE_READ_ERROR`; исключение парсера → `UNKNOWN_ERROR` с текстом исключения в `error.message` (без трассировки). Публикуется конверт без `result_s3`; `metrics.duration_ms` заполняется всегда. Если не удалось даже опубликовать результат — nak с задержкой 5 с. ### D-6. Соединение с NATS — как у ядра Пустые `NATS_SEED/NATS_JWT` → без учётных данных (dev); иначе nkeys/JWT шаблона. Stream `RECOGNITION` сервис **не создаёт** (ответственность ядра / платформы); при отсутствии stream воркер логирует ошибку и повторяет подписку каждые 10 с, `/healthcheck` отдаёт `nats = down`. Флаг `NATS_ENABLED` (по умолчанию `true`) — чтобы unit-тесты поднимали приложение без NATS. ### D-7. Тесты в два уровня Unit (`tests/unit`): модели контракта и примеры docs; обработчик сообщения с фейками `S3Storage`/`Publisher` (протоколы) — сценарии D-5. Интеграционные (`tests/integration`, маркер `integration`): compose-NATS/MinIO — реальный запрос из примера `request-tabular.json` → результат в бакете и конверт в `recognition.result`. ## Risks / Trade-offs - [`ack_wait` 10 мин при max_deliver 3 → до 30 мин «висящего» задания при падении инстанции] → ядро ограничивает ожидание `reply_deadline`; значение `ack_wait` вынесено в настройку `NATS_ACK_WAIT_SECONDS`. - [`NOT_IMPLEMENTED` в проде, если Срез 2 задержится] → код ошибки честный, ядро показывает его аналитику как «формат пока не поддерживается». - [Удалённые зависимости понадобятся позже] → возвращаются по одной строке в `requirements.txt`. ## Migration Plan Нет данных. Удаление каталогов — одним коммитом «chore: вырезан фронт и БД из шаблона» до функциональных изменений, чтобы ревью было читаемым. Откат — revert коммита. ## Open Questions Общие с родительским change (Nexus, версии Python, параметры NATS банка). Специфичных нет. ## Decisions made without discussion | № | Дата | Решение | Обоснование | Цена ошибки | |---|---|---|---|---| | 1 | 2026-10-03 | D-1: вырезать фронт/БД/SMB, не переименовывая оставшееся | читаемый diff с arm-core | нет | | 2 | 2026-10-03 | D-2: pull-consumer, `fetch(1)`, `ack_wait` 10 мин, `max_deliver` 3 | OQ-15, простота контроля параллелизма | смена параметров 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: транспортные ошибки превращаются в результат с кодом | ядро не должно ждать таймаута, если сервис жив | нет | | 6 | 2026-10-03 | D-6: stream создаёт ядро, сервис только подписывается | одна точка управления инфраструктурой NATS | добавить `ensure_stream` и в сервис | | 7 | 2026-10-03 | Каталог словарей — `api/rules/` (относительно рабочего каталога `api/`) | все команды шаблона идут из `api/` | перенос каталога |