Andrey Epifancev 0c04127aec docs: OpenSpec change slice-0-skeleton — сквозной скелет Среза 0
openspec init (config с контекстом docs-репозитория) и change slice-0-skeleton: proposal, design
(11 решений без обсуждения, открытые вопросы к человеку), спеки recognition-contract,
dev-environment, ci-template, tasks в порядке зависимостей между репозиториями. 06-test-data.md §6:
справочники продукта — seed arm-core, не фикстуры. 09-delivery-plan.md: Срез 0 по факту (change'и,
compose в docs/dev, контракт копией без общего пакета, seed).

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

116 lines
13 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 — сквозной скелет
## Context
См. `proposal.md` — Why. Ограничения, формирующие решения:
- Шаблон команды (`03-architecture.md`, «Соглашения шаблона команды»): один бакет S3, NATS с nkeys/JWT, настройки
`OPENBAO__*`, `DEBUG=true` отключает внешние интеграции, `Dockerfile`/`docker-compose.yml` в `.gitignore` шаблона.
- DECISION-4 DBML: сервисы распознавания без БД; обмен только через `recognition_job` (outbox/inbox), дедуп по `msg_id`, DLQ.
- Внутренние пакеты банка (`raisa_*`) и зеркала Nexus недоступны вне сети банка; локальная разработка идёт на публичных
образах и PyPI/npm.
- Фикстуры — отдельный приватный репозиторий `arm-ka-fixtures`; в CI он нужен только read-only.
## Goals / Non-Goals
**Goals:** один файл контракта на каждое сообщение; один `docker compose up` для всего workspace; один шаблон CI, из
которого три репозитория берут свои пайплайны; документы контекста соответствуют реальному шаблону.
**Non-Goals:** см. proposal. Дополнительно: не проектируем prod-деплой (это платформа банка) и не решаем ретеншн `results/` (ARCH-010).
## Decisions
### D-1. Контракт — JSON Schema в docs + вендоренная копия в каждом сервисе, без общего пакета
`09-delivery-plan.md` оставлял выбор: пакет `arm-ka-contracts` или копия с проверкой идентичности. Выбрана **копия**:
`docs/contracts/*.schema.json` — источник истины; каждый сервис хранит копию в `api/contracts/` и pydantic-модели в
`api/src/contract/`; тест сервиса сравнивает копию с `../docs/contracts` байт в байт (пропускается с предупреждением,
если `docs` недоступен и не задан `ARM_KA_DOCS`), а pydantic-модели валидируют `docs/contracts/examples/*.json` и
генерируют JSON, проходящий схему. *Альтернатива* — публикуемый пакет: нужен внутренний PyPI (вопрос к платформе),
добавляет цикл релиза ради трёх потребителей. *Цена ошибки:* при переходе на пакет копии удаляются, тест идентичности
заменяется пином версии — полдня.
### D-2. Конверт результата не дублирует `group_schema`
`03-architecture.md` перечислял `group_schema` в сообщении `recognition.result`. В конверте остаются только поля для
маршрутизации и статуса; `group_schema` ядро берёт из `documents[].group_schema` файла результата при маппинге
(Срез 2). *Почему:* одна истина на результат, сообщение остаётся маленьким (лимит NATS 1 МБ не трогаем).
### D-3. Идемпотентность — `Nats-Msg-Id` по естественному ключу попытки
Запрос: `<job_id>:<attempt>`; результат: `result:<job_id>:<attempt>`. JetStream отбрасывает дубликаты в окне
`duplicate_window` (2 мин по умолчанию); ядро дополнительно хранит `recognition_job.result_msg_id UNIQUE`
(DBML) — повторная доставка после окна тоже безопасна.
### D-4. Один stream `RECOGNITION` с subjects `recognition.>`, два durable consumer'а запросов, один — результатов
Запросы: consumer `tabular-parser` (filter `recognition.tabular.request`), `scan-gateway` (`recognition.image.request`),
оба — pull, `ack_wait` ≥ `reply_deadline` сервиса, `max_deliver` = 3 (парсер) / 1 (OCR — ТЗ 3, OQ-15).
Результаты: consumer `arm-core-results` (`recognition.result`), `max_deliver` = 5, после — DLQ-поведение по DBML
(`status = dlq`). Кто создаёт stream: в dev — `arm-core` при старте (`NATS_MANAGE_STREAM=true`), в проде — по ответу
платформы (Open Questions).
### D-5. Один бакет `arm-ka`, роли — префиксы
Следствие шаблона (`MINIO_BUCKET`). Ключи: `uploads/{file_id}/{job_id}/{original_name}`,
`results/{file_id}/{job_id}/result.json`, `raw/{job_id}.json`, `exports/…`, `archive/…`. Lifecycle в MinIO dev не
настраиваем; в проде — по префиксам (ARCH-010 для `results/`).
### D-6. Эмулятор ИС Сканирование — часть `scan-gateway`, не отдельный репозиторий
Эмулятор знает формат горячих папок и ответы Content AI так же, как сам шлюз; живёт в `scan-gateway/api/src/scan_emulator/`,
в compose запускается тем же образом другой командой с общим volume `hotfolder` и смонтированным `../../fixtures/ocr`.
*Альтернатива* — скрипт в `docs/dev/`: пришлось бы дублировать соглашения об именах `UUID_имя` и структуре `Output/UUID/UUID.json`.
### D-7. Compose на уровне workspace, Dockerfile — в каждом сервисе
`docs/dev/docker-compose.yml` собирает сервисы из `../../<repo>` (работает и из `.wt/<срез>/docs/dev`). Для этого из
`.gitignore` сервисов убирается строка `Dockerfile` (единственная правка шаблона); `docker-compose.yml` в репозиториях
не появляется. Образ собирает платформа по своим правилам — наш `Dockerfile` для dev и CI.
### D-8. CI — Gitea Actions, синтаксис GitHub Actions
Шаблон `docs/dev/ci/python-service.yml` копируется в `<repo>/.gitea/workflows/ci.yml` и параметризуется именем
сервиса. Фикстуры — `actions/checkout` репозитория `RSHB/arm-ka-fixtures` в `../fixtures` c токеном из секрета
`FIXTURES_TOKEN`. Если раннер не поддерживает `services:` — fallback: `docker compose -f docs/dev/docker-compose.yml up`
внутри job (вопрос к человеку).
## Risks / Trade-offs
- [Публичные образы PostgreSQL/NATS/MinIO отличаются версиями от банковских] → версии фиксируются в compose (15 / 2.10 / RELEASE.2025-xx)
и выравниваются по ответу платформы; код не использует фич новее PostgreSQL 15.
- [Шаблон инициализирует nkeys при импорте `app.py`] → задача сервисам: пустые `NATS_SEED/NATS_JWT` = соединение без
учётных данных (dev); при заданных — как в шаблоне.
- [Копии контракта расходятся] → тест идентичности в каждом сервисе; PR в docs без PR в сервисы — красный CI сервисов.
- [Эмулятор отвечает только для файлов, у которых есть пара `aep_N.json` в фикстурах (сейчас две)] → для остальных имён
эмулятор возвращает ответ с `error.code = INVALID_DOCUMENT` в формате Content AI — это тоже проверяемый сценарий.
## Migration Plan
Нет данных для миграции. Порядок внедрения: контракт → compose инфраструктуры → arm-core → сервисы → compose профиль
`services` → CI → документы. Откат — удаление ветки; `docs/contracts/recognition-result.schema.json` остаётся совместимым
с текущим черновиком (добавлен только код ошибки).
## Open Questions (к человеку; не меняют структуру change'а, но нужны до первого деплоя)
1. Версия Python в базовом образе платформы (в шаблоне нет пина; локальный venv — 3.13) и версия Node для `web/`
(нет `engines`). Пока compose/CI берут `python:3.12-slim` и `node:22`.
2. Доступ к Nexus банка (`raisa_fastapi_protected_api`, `raisa-ui`, `raisa-fastapi-protected-web`) из dev-среды и CI:
прокси, вендоренные wheel/tgz, или оставить условные импорты до деплоя.
3. Gitea Actions на `git.aepif.ru`: включены ли, есть ли раннер с Docker (нужны `services:` и `docker build`), реестр
образов для push (адрес, учётные данные как секреты).
4. Доступ CI к `RSHB/arm-ka-fixtures`: токен/deploy key только на чтение (секрет `FIXTURES_TOKEN`).
5. NATS банка: включён ли JetStream; кто создаёт stream `RECOGNITION` и consumer'ы — мы при старте или платформа;
параметры nkeys/JWT для трёх сервисов.
6. PostgreSQL банка: версия (нужна ≥ 15 ради `NULLS NOT DISTINCT`), право на `CREATE SCHEMA dkb` и `CREATE TYPE`.
7. JWT РАИСА: алгоритм и источник ключей (JWKS URL / публичный ключ), имена claim'ов (email, subject), issuer/audience —
для `RaisaVerifier`; до ответа работает только dev-эмитент.
## Decisions made without discussion
| № | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| 1 | 2026-10-03 | D-1: контракт вендорится копией, общего пакета нет | нет внутреннего PyPI; три потребителя | полдня на переход к пакету |
| 2 | 2026-10-03 | D-2: `group_schema` убран из конверта результата | одна истина в файле результата | правка одной таблицы в docs и одной pydantic-модели |
| 3 | 2026-10-03 | D-3/D-4: именование `Nats-Msg-Id`, один stream, параметры consumer'ов | DECISION-4, OQ-15 | переименование констант в трёх сервисах |
| 4 | 2026-10-03 | D-5: один бакет `arm-ka`, роли как префиксы | шаблон команды (`MINIO_BUCKET`) | `s3_object.bucket` уже хранится отдельно — смена на несколько бакетов не ломает модель |
| 5 | 2026-10-03 | D-6: эмулятор ИС Сканирование живёт в `scan-gateway` | общие соглашения горячих папок | перенос каталога |
| 6 | 2026-10-03 | D-7: compose на уровне workspace в `docs/dev/`, `Dockerfile` разыгнорирован в сервисах | шаблон игнорирует Dockerfile, платформа собирает сама | вернуть строку в `.gitignore`, перенести Dockerfile в `docs/dev/` |
| 7 | 2026-10-03 | D-8: CI как Gitea Actions (GitHub-синтаксис) | Gitea у заказчика; синтаксис переносим | переписать YAML под другой раннер |
| 8 | 2026-10-03 | В контракт v1.0 добавлен код ошибки `NOT_IMPLEMENTED` | заглушки Среза 0 и будущие неподдерживаемые форматы не должны маскироваться под `UNSUPPORTED_FORMAT` | удалить одно значение enum в 1.1 |
| 9 | 2026-10-03 | Справочники продукта для seed (план счетов, правила элиминации, права) — в `arm-core/api/seed/`, не в `fixtures/directories/` | это данные продукта, нужны в проде; `fixtures` — приватные тестовые данные | перенос файлов и правка §6 `06-test-data.md` |
| 10 | 2026-10-03 | Версии образов dev: PostgreSQL 15, NATS 2.10, MinIO стабильный релиз, `python:3.12-slim`, `node:22` | минимумы из CLAUDE.md; версии банка неизвестны (Open Questions 1, 6) | смена тега в compose/CI |
| 11 | 2026-10-03 | Worktree среза собираются в зеркальный workspace `.wt/<срез>/` с симлинками | относительные пути `../docs`, `../fixtures` и хуки должны работать в ветке | переместить worktree |