Andrey Epifancev 1661ec2fca docs: соглашения шаблона в CLAUDE.md и OpenSpec change slice-0-skeleton
Срез 0 (issue #1): раздел «Соглашения шаблона» (что оставлено от шаблона команды, что
вырезается, что добавляется); change openspec/changes/slice-0-skeleton — proposal, design
с таблицей «Decisions made without discussion», спеки и tasks; в openspec/config.yaml
строки с двоеточием взяты в кавычки (YAML разбирал их как словари).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 02:43:38 +04:00

81 lines
7.9 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 и правила `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/` | перенос каталога |