Срез 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>
7.9 KiB
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_wait10 мин при 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/ |
перенос каталога |