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>
116 lines
13 KiB
Markdown
116 lines
13 KiB
Markdown
# 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 |
|