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>
13 KiB
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'а, но нужны до первого деплоя)
- Версия Python в базовом образе платформы (в шаблоне нет пина; локальный venv — 3.13) и версия Node для
web/(нетengines). Пока compose/CI берутpython:3.12-slimиnode:22. - Доступ к Nexus банка (
raisa_fastapi_protected_api,raisa-ui,raisa-fastapi-protected-web) из dev-среды и CI: прокси, вендоренные wheel/tgz, или оставить условные импорты до деплоя. - Gitea Actions на
git.aepif.ru: включены ли, есть ли раннер с Docker (нужныservices:иdocker build), реестр образов для push (адрес, учётные данные как секреты). - Доступ CI к
RSHB/arm-ka-fixtures: токен/deploy key только на чтение (секретFIXTURES_TOKEN). - NATS банка: включён ли JetStream; кто создаёт stream
RECOGNITIONи consumer'ы — мы при старте или платформа; параметры nkeys/JWT для трёх сервисов. - PostgreSQL банка: версия (нужна ≥ 15 ради
NULLS NOT DISTINCT), право наCREATE SCHEMA dkbиCREATE TYPE. - 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 |