docs: Срез 0 под факты платформы — NATS без JetStream, Python 3.11, PostgreSQL 16, без CI и Nexus

По ответам пользователя 03.10.2026: обмен через recognition_job перестроен на core NATS (queue group'ы,
ack сервиса = конверт status=processing, повторная публикация ядром 30 с × 3 → dlq, дедуп по result_msg_id);
capability ci-template заменена на quality-checks (make check / make check-full, конфигураций CI нет);
compose: PostgreSQL 16, NATS без --jetstream, python:3.11-slim; пакеты raisa_* фильтруются при dev-установке,
npm raisa-* → optionalDependencies. Обновлены 03-architecture, 09-delivery-plan, 00-workspace-layout,
proposal/design/specs/tasks change'а; открытые вопросы к платформе закрыты, остались Node и JWT РАИСА.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-03 16:21:39 +04:00
parent 5cbb23f011
commit a0be5ab5a1
11 changed files with 315 additions and 213 deletions

View File

@ -28,8 +28,9 @@
- **Ветки и коммиты — всегда `git -C <repo>`** или с явным `cd`. Хук `guard-main` должен определять репозиторий по
пути изменяемого файла (`git -C "$(dirname "$FILE")" rev-parse --abbrev-ref HEAD`), а не по cwd workspace.
- **codegraph** — по одному индексу на репозиторий; workspace-уровня карта кода не нужна.
- **Относительные ссылки на контекст** — из любого репозитория: `../docs/context/...`, `../fixtures/...`. В CI сервисов
фикстуры подтягиваются checkout'ом `arm-ka-fixtures` в `../fixtures` (или переменная `ARM_KA_FIXTURES`).
- **Относительные ссылки на контекст** — из любого репозитория: `../docs/context/...`, `../fixtures/...`. Проверки
(`make check`, `make check-full`) берут фикстуры из `../fixtures` или `ARM_KA_FIXTURES`, docs — из `../docs` или
`ARM_KA_DOCS`; при отсутствии — пропуск с предупреждением. CI в Gitea нет; пайплайн платформы банка — вне репозиториев.
- **Вложенные `CLAUDE.md`** в каждом репозитории короткие: что это за сервис, его собственные соглашения, ссылка на
`../CLAUDE.md` и `../docs/`. Шаблоны — `docs/repo-templates/`.
- Один `claude`-сеанс может править несколько репозиториев, но PR — по одному на репозиторий; в описании PR указывать

View File

@ -11,7 +11,7 @@
│ │
PostgreSQL │ │ S3 (uploads/ results/ exports/ archive/)
│ │
NATS JetStream ◀─────────────────┘ outbox │
NATS (core) ◀─────────────────┘ outbox │
recognition.tabular.request │
recognition.image.request │
recognition.result ───────────────────▶ arm-core (inbox, дедуп по msg_id)
@ -47,14 +47,21 @@
| результат | `recognition.result` | tabular-parser / scan-gateway | `schema_version`, `job_id`, `file_id`, `status` (done / needs_check / error), `error {code, message}`, `service {name, version}`, `result_s3 {bucket, key}`, `result_schema_version`, `raw_s3_key` (только scan-gateway), `metrics` | `result:<job_id>:<attempt>` |
`group_schema` в конверт не входит — ядро берёт его из `documents[].group_schema` файла результата при маппинге.
Один stream `RECOGNITION` (subjects `recognition.>`); durable consumer'ы: `tabular-parser` и `scan-gateway` — запросы,
`arm-core-results` — результаты.
Конверт со `status = processing` — подтверждение взятия в работу (ack), `Nats-Msg-Id = ack:<job_id>:<attempt>`.
**На платформе NATS без JetStream** (ответ 03.10.2026): нет durable consumer'ов, серверного дедупа и redelivery.
Поэтому гарантии доставки — на уровне приложения: сервисы подписаны queue group'ами (`tabular-parser`, `scan-gateway`),
ядро — queue group `arm-core` на `recognition.result`; сервис сразу по получении запроса публикует ack `processing`;
ядро, не получив ack за `ACK_TIMEOUT_SECONDS` (30), публикует запрос повторно с тем же `Nats-Msg-Id`, не более
`OUTBOX_MAX_REPUBLISH` (3) раз, затем `dlq`. Сервис держит process-local множество `job_id` в работе и на повторный
запрос отвечает ack'ом без второй обработки. Появится JetStream — меняются только publisher/subscriber.
Правила:
- `recognition_job` создаётся в статусе `queued` **в одной транзакции** со `stored_file`; фоновый публикатор переводит в
`published` (`request_msg_id` = `Nats-Msg-Id`). Повтор публикации — по `queued` старше N секунд.
- Результат принимается через durable consumer; `result_msg_id UNIQUE` = дедуп. После `max_deliver` → `dlq`;
replay = сброс в `queued` с `attempt + 1`.
`published` (`request_msg_id` = `Nats-Msg-Id`), ack сервиса — в `processing` (`started_at`). Повтор публикации — по
`queued` старше N секунд и по `published` без ack.
- Результат принимается подпиской queue group; `result_msg_id UNIQUE` = дедуп. Нет ack после повторов → `dlq`;
потерянный результат → `reply_deadline` → `timeout`; replay = сброс в `queued` с `attempt + 1`.
- Результат одного файла содержит **N документов**; ядро на каждый создаёт `document` и маппит данные.
- Сервисы не знают про БД и про бизнес-смысл: они возвращают структуру файла; нормализация счетов, резолв
контрагентов, проверки — в ядре.
@ -184,8 +191,8 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро
- **S3** — `miniopy-async` (`Minio`, `cert_check=False`), **один бакет** `MINIO_BUCKET`; роли `s3_object.bucket_role`
становятся первым сегментом ключа (`uploads/…`, `results/…`, `raw/…`, `exports/…`, `archive/…`).
- **NATS** — `nats-py`; аутентификация nkeys-seed + JWT (`NATS_SEED`, `NATS_JWT`) через `user_jwt_cb`/`signature_cb`;
соединение и подписки создаются в lifespan и живут в `app.state.nats` / `app.state.nats_subs`. JetStream
(durable consumer, `Nats-Msg-Id`) — поверх того же соединения.
соединение и подписки создаются в lifespan и живут в `app.state.nats` / `app.state.nats_subs`. Core NATS без
JetStream: подписки с queue group, заголовки `Nats-Msg-Id` ставим сами, ack и повторы — на уровне приложения.
- **SMB** (горячие папки ИС Сканирование) — `smbprotocol`/`smbclient`, сессия регистрируется в lifespan; нужен только `scan-gateway`.
- **Внешняя аутентификация** — пакет `raisa_fastapi_protected_api` из внутреннего Nexus (`AuthorizationMiddleware`,
`ProtectedSettings`, …), импортируется только при `DEBUG=false`; фронтовая пара — `raisa-fastapi-protected-web`.
@ -218,13 +225,21 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро
### Сборка и деплой
- Платформа РАИСА запускает `api/run_server.py` с `SERVER__HOST`/`SERVER__PORT` из окружения; ресурсы — `raisa_config.yml`.
Python на платформе — **3.11** (ответ 03.10.2026); версия Node неизвестна (локально `node:22`).
- `Dockerfile`, `docker-compose.yml`, `.dockerignore` шаблон держит в `.gitignore` («Custom ignore»): образ собирает
платформа. Для локальной разработки и CI: compose — на уровне workspace (`docs/dev/docker-compose.yml`), `Dockerfile` —
в корне каждого сервиса, строка `Dockerfile` убирается из `.gitignore` (единственная правка шаблона; решение Среза 0).
платформа своим CI/CD, в который мы не лезем. Для локальной разработки: compose — на уровне workspace
(`docs/dev/docker-compose.yml`), `Dockerfile` (`python:3.11-slim`) — в корне каждого сервиса, строка `Dockerfile`
убирается из `.gitignore` (единственная правка шаблона; решение Среза 0).
- Фронт: `npm run build` (rsbuild) → `web/dist/` → копируется в `api/web/`, откуда FastAPI раздаёт SPA; роутеры
подключаются **до** mount'а `/`.
- Зеркала пакетов: pip и npm настроены на `nexus.dev.raisa.go.rshbank.ru` (`setup/pip.conf`, `setup/.npmrc`).
- CI-конфига в шаблоне нет; шаблон пайплайна — `docs/dev/ci/` (change `slice-0-skeleton` в docs).
- Зеркала пакетов: pip и npm настроены на `nexus.dev.raisa.go.rshbank.ru` (`setup/pip.conf`, `setup/.npmrc`). Вне банка
Nexus недоступен до прода: dev-установка ставит `requirements.txt` без строк `raisa_*` (`make venv`), в
`web/package.json` пакеты `raisa-*` переносятся в `optionalDependencies`; импорты `raisa_fastapi_protected_api` условные
(`DEBUG=false`), как в шаблоне.
- CI в Gitea нет и не планируется; конфигураций CI в репозиториях нет. Проверки качества — одинаковые цели `Makefile`
во всех репозиториях: `make check` (ruff, mypy, pytest без Docker, идентичность контракта) и `make check-full`
(+ интеграционные тесты против compose, для arm-core — цикл миграций и `check_dbml_vs_orm.py`); `make check-full`
обязателен перед PR. Описание — `docs/dev/checks.md`.
### Frontend (только arm-core)
@ -253,7 +268,8 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро
## Что ожидаем от платформы (для запроса ресурсов)
PostgreSQL 15+ (одна БД, схема `dkb` — как в шаблоне), NATS JetStream (один stream `RECOGNITION`, 3 subject;
аутентификация nkeys/JWT, как в шаблоне), S3-совместимое хранилище (один бакет; префиксы-роли + lifecycle по префиксам),
Nexus (pip/npm, пакеты `raisa_*`), Gitea + CI (lint, pytest, миграции с нуля, сборка образов), реестр образов.
Секреты — через переменные окружения (`OPENBAO__*`).
По ответам платформы (03.10.2026): PostgreSQL **16** (одна БД, схема `dkb` — создавать схемы можно), NATS **без
JetStream** (3 subject `recognition.*`; аутентификация nkeys/JWT, как в шаблоне), S3-совместимое хранилище (один бакет;
префиксы-роли + lifecycle по префиксам), Python 3.11 в базовом образе, Nexus (pip/npm, пакеты `raisa_*`) — только внутри
банка, CI/CD — пайплайн платформы (мы отдаём команды `make check` / `make check-full`). Секреты — через переменные
окружения (`OPENBAO__*`). Открыто: версия Node, параметры JWT РАИСА.

View File

@ -13,14 +13,15 @@
- Три репозитория из нулевого коммита шаблона команды; соглашения шаблона — в `03-architecture.md` и в `CLAUDE.md`
каждого репозитория. **Первая задача агента — прочитать шаблон и дополнить документы.** (Сделано: раздел «Соглашения
шаблона команды» в `03-architecture.md`; в `tabular-parser`/`scan-gateway` фронт и БД вырезаются задачами среза.)
- OpenSpec change'и среза: `docs/openspec/changes/slice-0-skeleton` (сквозной: контракт, compose, CI-шаблон, документы) и
- OpenSpec change'и среза: `docs/openspec/changes/slice-0-skeleton` (сквозной: контракт, compose, проверки качества, документы) и
по одному `openspec/changes/slice-0-skeleton` в `arm-core`, `tabular-parser`, `scan-gateway`; issue #1 в каждом репозитории.
- `docker-compose` workspace-уровня (`docs/dev/docker-compose.yml`): PostgreSQL 15, NATS 2.10 JetStream, MinIO (S3, один
бакет `arm-ka`), эмулятор ИС Сканирование (часть `scan-gateway`, отдаёт `fixtures/ocr/aep_N.json`), три сервиса (профиль
`services`). `Makefile` с `up/test/lint/migrate` в каждом репо (шаблон Makefile не содержит — добавляется).
- `docker-compose` workspace-уровня (`docs/dev/docker-compose.yml`): PostgreSQL 16, NATS 2.10 **без JetStream** (как на
платформе; ack и повторная публикация — на уровне приложения), MinIO (S3, один бакет `arm-ka`), эмулятор ИС
Сканирование (часть `scan-gateway`, отдаёт `fixtures/ocr/aep_N.json`), три сервиса (профиль `services`, Python 3.11).
`Makefile` с общими целями `up/test/test-integration/lint/check/check-full` (+ `migrate/seed` в ядре) в каждом репо.
- Контракт v1.0 — три схемы в `docs/contracts/`: `recognition-request`, `recognition-result-message` (конверты NATS) и
`recognition-result` (файл результата) + `examples/`. Общий пакет `arm-ka-contracts` **не публикуется**: каждый сервис
хранит копию схем в `api/contracts/` и pydantic-модели в `src/contract/`, идентичность с `docs/contracts` проверяется в CI
хранит копию схем в `api/contracts/` и pydantic-модели в `src/contract/`, идентичность с `docs/contracts` проверяется в `make check`
(решение D-1 change `slice-0-skeleton` в docs).
- Alembic: миграция 0001 — **все** таблицы и enum'ы DBML v5.13 (черновик из ORM, сверка с DBML скриптом
`api/scripts/check_dbml_vs_orm.py` — расхождение = ошибка CI).
@ -29,9 +30,12 @@
(План счетов 94н + типовые субсчета 1С, OQ-47), `elimination_rule` v1 из §4 (только ББ), `required_document_rule`
для BASIC/EXTENDED (OQ-46).
- Auth: валидация JWT + dev-эмитент; `app_user` seed (по одному на роль); зависимость `require(permission)`.
- `audit_event` middleware/helper; `s3_object` клиент; outbox-паблишер и consumer `recognition.result` (пустые обработчики).
- CI: lint, mypy, pytest, миграции с нуля и обратно, сборка образов.
- DoD: `GET /api/v1/me` возвращает пользователя и его эффективные права; все миграции применяются; CI зелёный.
- `audit_event` helper; `s3_object` клиент; outbox-паблишер (ack `processing`, повторная публикация без ack, `dlq`) и
подписка `recognition.result` (дедуп по `result_msg_id`, пустой маппер документов).
- Проверки качества вместо CI (в Gitea CI нет, пайплайн платформы вне репозиториев): `make check` (ruff, mypy, pytest,
контракт) и `make check-full` (+ интеграционные тесты против compose, цикл миграций, `check_dbml_vs_orm`) — `docs/dev/checks.md`.
- DoD: `GET /api/v1/me` возвращает пользователя и его эффективные права; все миграции применяются и откатываются;
`make check-full` зелёный в трёх репозиториях.
## Срез 1 — Справочники (ГК / ЮЛ / ГСК / ГСЗ / филиалы) + пользователи и роли

View File

@ -7,16 +7,18 @@
- Шаблон команды (`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.
- **Факты платформы (ответы пользователя 03.10.2026):** Python **3.11**; версия Node неизвестна; доступа к Nexus банка
(`raisa_*`) не будет до деплоя в прод; CI в Gitea нет и не планируется — у платформы свой CI/CD, в него не лезем;
**NATS без JetStream**; PostgreSQL **16**; создавать схемы можно; параметры JWT РАИСА пока неизвестны.
- Фикстуры — отдельный приватный репозиторий `arm-ka-fixtures`; локально доступен как `../fixtures`.
## Goals / Non-Goals
**Goals:** один файл контракта на каждое сообщение; один `docker compose up` для всего workspace; один шаблон CI, из
которого три репозитория берут свои пайплайны; документы контекста соответствуют реальному шаблону.
**Goals:** один файл контракта на каждое сообщение; один `docker compose up` для всего workspace; одна команда проверки
качества, одинаковая для трёх репозиториев; документы контекста соответствуют реальному шаблону и реальной платформе.
**Non-Goals:** см. proposal. Дополнительно: не проектируем prod-деплой (это платформа банка) и не решаем ретеншн `results/` (ARCH-010).
**Non-Goals:** см. proposal. Дополнительно: не проектируем prod-деплой (это платформа банка), не решаем ретеншн `results/`
(ARCH-010), не пишем конфигурации CI ни для Gitea, ни для платформы.
## Decisions
@ -25,26 +27,39 @@
`docs/contracts/*.schema.json` — источник истины; каждый сервис хранит копию в `api/contracts/` и pydantic-модели в
`api/src/contract/`; тест сервиса сравнивает копию с `../docs/contracts` байт в байт (пропускается с предупреждением,
если `docs` недоступен и не задан `ARM_KA_DOCS`), а pydantic-модели валидируют `docs/contracts/examples/*.json` и
генерируют JSON, проходящий схему. *Альтернатива* — публикуемый пакет: нужен внутренний PyPI (вопрос к платформе),
добавляет цикл релиза ради трёх потребителей. *Цена ошибки:* при переходе на пакет копии удаляются, тест идентичности
заменяется пином версии — полдня.
генерируют 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-3. Доставка без JetStream: app-level ack, повторная публикация ядром, дедуп в БД
JetStream на платформе нет, значит нет durable consumer'ов, серверного дедупа по `Nats-Msg-Id`, redelivery и `max_deliver`.
Схема на core NATS:
- Запросы публикуются ядром из outbox (`recognition_job.status = queued → published`); сервисы подписаны **queue group**
(`tabular-parser`, `scan-gateway`) — несколько инстанций делят нагрузку, одно сообщение получает одна.
- Сервис **сразу по получении** публикует в `recognition.result` конверт со `status = processing` (ack: «взял в работу») —
это значение уже есть в `recognition_status` DBML («сервис подтвердил взятие в работу»). Ядро ставит `processing`,
`started_at`, `service_name/version`.
- Если ack не пришёл за `ACK_TIMEOUT_SECONDS` (30) — ядро **публикует запрос повторно** с тем же `Nats-Msg-Id`
(`<job_id>:<attempt>`, тот же `attempt`: попытка обработки не началась), не более `OUTBOX_MAX_REPUBLISH` (3) раз, затем
`status = dlq` (DLQ-поведение DBML без брокера). `attempt`/`max_attempts` остаются для replay после `error`/`timeout`
(ручной, Срез 3+).
- Результат, потерянный при простое ядра, обнаруживается по `reply_deadline` → `timeout`; `result_msg_id UNIQUE` в
`recognition_job` остаётся единственным дедупом результатов (`Nats-Msg-Id` сервисы проставляют сами).
- Сервис держит process-local множество `job_id` в работе: повторный запрос с тем же `job_id` (пришёл повторно, пока
первый обрабатывается или уже обработан в этом процессе) подтверждается ack'ом и не обрабатывается второй раз.
*Альтернатива* — ждать JetStream или ставить свою очередь (Redis/БД-поллинг у сервисов): первое не в нашей власти, второе
нарушает DECISION-4 (сервисы без БД). *Если JetStream появится:* меняются только publisher/subscriber (`js.publish`,
durable consumer), outbox/inbox и контракт остаются.
### 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-4. Subjects и группы
Stream'ов и consumer'ов нет. Subjects: `recognition.tabular.request`, `recognition.image.request`, `recognition.result`.
Queue group'ы: `tabular-parser`, `scan-gateway` — на запросах; `arm-core` — на результатах (несколько инстанций ядра
получают результат по одному разу). Заголовки `Nats-Msg-Id` ставятся всегда (core NATS 2.2+ поддерживает заголовки) —
для трассировки и будущего JetStream.
### D-5. Один бакет `arm-ka`, роли — префиксы
Следствие шаблона (`MINIO_BUCKET`). Ключи: `uploads/{file_id}/{job_id}/{original_name}`,
@ -59,44 +74,59 @@
### D-7. Compose на уровне workspace, Dockerfile — в каждом сервисе
`docs/dev/docker-compose.yml` собирает сервисы из `../../<repo>` (работает и из `.wt/<срез>/docs/dev`). Для этого из
`.gitignore` сервисов убирается строка `Dockerfile` (единственная правка шаблона); `docker-compose.yml` в репозиториях
не появляется. Образ собирает платформа по своим правилам — наш `Dockerfile` для dev и CI.
не появляется. Образ для прода собирает платформа по своим правилам — наш `Dockerfile` только для dev и `make check-full`.
### 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 (вопрос к человеку).
### D-8. Проверки качества — локальная команда, без конфигураций CI
CI в Gitea нет, пайплайн платформы — вне репозиториев. Каждый репозиторий получает одинаковые цели `Makefile`:
`make check` (быстро, без Docker: `ruff check`, `ruff format --check`, `mypy`, `pytest -m "not integration"`, тест
идентичности контракта) и `make check-full` (`make check` + интеграционные тесты против compose; в `arm-core` — ещё
«`alembic upgrade head` → `downgrade base` → `upgrade head`» и `check_dbml_vs_orm.py`). `make check-full` обязателен
перед PR, результат вписывается в описание PR. Правила — `docs/dev/checks.md`. Фикстуры берутся из `../fixtures` или
`ARM_KA_FIXTURES`; при отсутствии тесты с маркером `fixtures` пропускаются с предупреждением. Что из этого попадёт в
пайплайн платформы — решает платформа; мы даём ей готовые команды.
### D-9. Версии инструментов
Python **3.11** (`python:3.11-slim` в Dockerfile; пины `requirements.txt` совместимы). PostgreSQL **16** в compose
(код не использует ничего новее 15; `NULLS NOT DISTINCT` доступен). NATS **2.10 без `--jetstream`** — паритет с прод.
Node — неизвестен; локально `node:22` LTS, только для сборки `web/` в compose; меняется одной строкой.
### D-10. Пакеты `raisa_*` без Nexus
`raisa_fastapi_protected_api` остаётся в `requirements.txt` для сборки платформой; dev-установка (`make venv`,
`Dockerfile`) ставит `requirements.txt` без строк `raisa_*` (фильтр `grep -v`), код импортирует пакет условно, как в
шаблоне (`DEBUG=false`). В `web/package.json` `raisa-ui` и `raisa-fastapi-protected-web` переносятся в
`optionalDependencies`: `npm install` без Nexus проходит, с Nexus — ставит их как раньше. *Цена ошибки:* если платформа
требует их в `dependencies` — вернуть две строки.
## Risks / Trade-offs
- [Публичные образы PostgreSQL/NATS/MinIO отличаются версиями от банковских] → версии фиксируются в compose (15 / 2.10 / RELEASE.2025-xx)
и выравниваются по ответу платформы; код не использует фич новее PostgreSQL 15.
- [Без JetStream сообщение теряется при простое получателя] → запросы: ack + повторная публикация ядром (D-3);
результаты: `reply_deadline` → `timeout` и replay; для OCR повторная публикация до ack не повторяет распознавание
(сервис ещё не брал файл).
- [Повторный запрос пришёл в сервис, который уже обрабатывает `job_id`] → process-local множество в работе; имя файла в
горячей папке = `job_id`, повторный `put` перезаписывает тот же файл.
- [Публичные образы PostgreSQL/NATS/MinIO отличаются от банковских] → версии зафиксированы по ответам (16 / 2.10 без JS).
- [Шаблон инициализирует nkeys при импорте `app.py`] → задача сервисам: пустые `NATS_SEED/NATS_JWT` = соединение без
учётных данных (dev); при заданных — как в шаблоне.
- [Копии контракта расходятся] → тест идентичности в каждом сервисе; PR в docs без PR в сервисы — красный CI сервисов.
- [Эмулятор отвечает только для файлов, у которых есть пара `aep_N.json` в фикстурах (сейчас две)] → для остальных имён
эмулятор возвращает ответ с `error.code = INVALID_DOCUMENT` в формате Content AI — это тоже проверяемый сценарий.
- [Копии контракта расходятся] → тест идентичности в `make check` каждого сервиса.
- [Эмулятор отвечает только для файлов, у которых есть пара `aep_N.json` (сейчас две)] → для остальных имён —
ответ с `error.code = INVALID_DOCUMENT` в формате Content AI; это тоже проверяемый сценарий.
- [Нет CI — проверки можно забыть] → `make check-full` в DoD каждой задачи и в описании PR; ревьюер требует вывод.
## Migration Plan
Нет данных для миграции. Порядок внедрения: контракт → compose инфраструктуры → arm-core → сервисы → compose профиль
`services` → CI → документы. Откат — удаление ветки; `docs/contracts/recognition-result.schema.json` остаётся совместимым
с текущим черновиком (добавлен только код ошибки).
`services` → проверки качества → документы. Откат — удаление ветки; `docs/contracts/recognition-result.schema.json`
остаётся совместимым с текущим черновиком (добавлен только код ошибки).
## Open Questions (к человеку; не меняют структуру change'а, но нужны до первого деплоя)
## Open Questions
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-эмитент.
Отвечено 03.10.2026: Python 3.11; Nexus недоступен до прода; CI в Gitea нет, пайплайн платформы не трогаем; JetStream нет;
PostgreSQL 16, схемы создавать можно. Остаются (не блокируют старт):
1. Версия Node для `web/` (пока `node:22`).
2. JWT РАИСА: алгоритм, источник ключей, имена claim'ов, issuer/audience — нужны к первому деплою; до этого dev-эмитент.
3. Имена входящего/исходящего каталогов и путь SMB на стенде ИС Сканирование — к Срезу 3.
4. Появится ли JetStream на платформе позже — влияет только на publisher/subscriber (D-3).
## Decisions made without discussion
@ -104,12 +134,13 @@
|---|---|---|---|---|
| 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` уже хранится отдельно — смена на несколько бакетов не ломает модель |
| 3 | 2026-10-03 | D-3/D-4: без JetStream — ack `processing`, повторная публикация ядром (30 с, 3 раза), queue group'ы, дедуп по `result_msg_id` | ответ пользователя «JetStream нет»; DECISION-4 требует не трогать БД из сервисов | при появлении JetStream — замена publisher/subscriber |
| 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 |
| 6 | 2026-10-03 | D-7: compose в `docs/dev/`, `Dockerfile` разыгнорирован в сервисах | шаблон игнорирует Dockerfile, платформа собирает сама | вернуть строку в `.gitignore` |
| 7 | 2026-10-03 | D-8: проверки качества — `make check` / `make check-full`, конфигураций CI нет | CI в Gitea нет, пайплайн платформы не наш | добавить YAML под нужный раннер, команды уже есть |
| 8 | 2026-10-03 | В контракт v1.0 добавлен код ошибки `NOT_IMPLEMENTED` и статус конверта `processing` (ack) | заглушки Среза 0; доставка без JetStream | удалить значения enum в 1.1 |
| 9 | 2026-10-03 | Справочники продукта для seed — в `arm-core/api/seed/`, не в `fixtures/directories/` | данные продукта нужны в проде; `fixtures` — приватные тестовые данные | перенос файлов и правка §6 `06-test-data.md` |
| 10 | 2026-10-03 | D-9: `python:3.11-slim`, PostgreSQL 16, NATS 2.10 без JS, `node:22` до ответа | ответы пользователя; Node неизвестен | смена тега в compose/Dockerfile |
| 11 | 2026-10-03 | Worktree среза — зеркальный workspace `.wt/<срез>/` с симлинками | относительные пути `../docs`, `../fixtures` и хуки должны работать в ветке | переместить worktree |
| 12 | 2026-10-03 | D-10: `raisa_*` фильтруются при dev-установке, npm-пакеты `raisa-*` → `optionalDependencies` | Nexus недоступен до прода | вернуть две строки в `dependencies` |

View File

@ -1,4 +1,4 @@
# Proposal: Срез 0 — сквозной скелет (контракт v1.0, окружение разработки, CI-шаблон)
# Proposal: Срез 0 — сквозной скелет (контракт v1.0, окружение разработки, проверки качества)
**Срез:** 0 «Скелет» (`docs/context/09-delivery-plan.md`). **Репозиторий:** arm-ka-docs (сквозная часть).
**Issue:** RSHB/arm-ka-docs#1. **Дочерние change'и:** `arm-core/openspec/changes/slice-0-skeleton` (arm-core#1),
@ -9,9 +9,10 @@
Три сервисных репозитория развёрнуты из шаблона команды, но между ними ещё нет ничего общего: контракт результата
распознавания помечен «черновик», сообщения NATS описаны только таблицей в `03-architecture.md`, локально поднять
PostgreSQL + NATS + MinIO + ИС Сканирование нечем, CI-конфигурации у шаблона нет. Без этого Срез 1 (справочники) и
Срез 2 (парсер) нельзя начать параллельно в трёх репозиториях: каждый сервис придумает свой формат сообщений и своё
окружение. Срез 0 фиксирует общие точки один раз и проверяет их end-to-end на пустых обработчиках.
PostgreSQL + NATS + MinIO + ИС Сканирование нечем, единого набора проверок перед PR нет. Без этого Срез 1 (справочники)
и Срез 2 (парсер) нельзя начать параллельно в трёх репозиториях: каждый сервис придумает свой формат сообщений и своё
окружение. Срез 0 фиксирует общие точки один раз и проверяет их end-to-end на пустых обработчиках — на той платформе,
которая есть: NATS без JetStream, PostgreSQL 16, Python 3.11, без доступа к Nexus и без CI в Gitea.
## What Changes
@ -21,52 +22,57 @@ PostgreSQL + NATS + MinIO + ИС Сканирование нечем, CI-кон
`statementDocument`) не меняется — она уточняется в Срезе 2/3 версией 1.x (additive) или 2.0 (breaking).
- **Новое:** `recognition-request.schema.json` — тело сообщения `recognition.{tabular|image}.request`
(`job_id`, `file_id`, `pipeline`, `file_type`, `original_name`, `s3 {bucket, key}`, `reply_deadline`, `attempt`).
- **Новое:** `recognition-result-message.schema.json` — тело сообщения `recognition.result` (конверт: `job_id`,
`file_id`, `status`, `error`, `service`, `result_s3 {bucket, key}`, `result_schema_version`, `raw_s3_key`, `metrics`).
Поле `group_schema` в конверт **не входит** — ядро берёт его из `documents[].group_schema` файла результата
(правка таблицы в `03-architecture.md`).
- Правила заголовков NATS: `Nats-Msg-Id` запроса = `<job_id>:<attempt>`, результата = `result:<job_id>:<attempt>`.
- Примеры сообщений и результата — `docs/contracts/examples/` (валидируются в CI сервисов).
- **Новое:** `recognition-result-message.schema.json` — тело сообщения `recognition.result`: конверт со `status`
`processing` (подтверждение взятия в работу — замена ack брокера, которого без JetStream нет) | `done` | `needs_check`
| `error`; поля `job_id`, `file_id`, `error`, `service`, `result_s3 {bucket, key}`, `result_schema_version`,
`raw_s3_key`, `metrics`. Поле `group_schema` в конверт **не входит** — ядро берёт его из `documents[].group_schema`
файла результата (правка таблицы в `03-architecture.md`).
- Заголовки NATS: `Nats-Msg-Id` запроса = `<job_id>:<attempt>`, результата = `result:<job_id>:<attempt>`, ack —
`ack:<job_id>:<attempt>`; без JetStream они нужны для трассировки, дедуп результатов — `recognition_job.result_msg_id`.
- Примеры сообщений и результата — `docs/contracts/examples/` (валидируются тестами сервисов).
- **Окружение разработки** `docs/dev/`:
- `docker-compose.yml` workspace-уровня: PostgreSQL 15, NATS 2.10 с JetStream, MinIO (+ init-контейнер: бакет
`arm-ka`), эмулятор ИС Сканирование (образ `scan-gateway`, команда эмулятора, общая папка горячих каталогов),
сервисы `arm-core`, `tabular-parser`, `scan-gateway` (профиль `services`; сборка из соседних каталогов workspace).
- `.env.example` c переменными шаблона (`OPENBAO__*`, `DEBUG`) и флагами разработки (`AUTH_DEV_ISSUER`, `SCAN_EMULATOR`).
- `README.md`: как поднять инфраструктуру, как запустить сервисы из venv против compose, как прогнать интеграционные тесты.
- **CI-шаблон** `docs/dev/ci/`: workflow Gitea Actions для Python-сервиса (ruff, mypy, pytest с сервисами PostgreSQL/NATS/MinIO,
проверка идентичности вендоренного контракта с `docs/contracts`, сборка образа; для arm-core — плюс миграции с нуля и
назад, `check_dbml_vs_orm.py`, job для `web/`: biome, tsc, rsbuild build). Checkout `arm-ka-fixtures` в `../fixtures`
(или `ARM_KA_FIXTURES`) — через секрет с правом чтения.
- `docker-compose.yml` workspace-уровня: PostgreSQL 16, NATS 2.10 **без JetStream** (паритет с платформой), MinIO
(+ init-контейнер: бакет `arm-ka`), эмулятор ИС Сканирование (образ `scan-gateway`, команда эмулятора, общая папка
горячих каталогов), сервисы `arm-core`, `tabular-parser`, `scan-gateway` (профиль `services`; сборка из соседних
каталогов workspace, `python:3.11-slim`).
- `.env.example` c переменными шаблона (`OPENBAO__*`, `DEBUG`) и флагами разработки (`AUTH_DEV_ISSUER`, `SCAN_EMULATOR`,
`NATS_ENABLED`).
- `README.md`: как поднять инфраструктуру, как запустить сервисы из venv против compose без Nexus, как прогнать
интеграционные тесты.
- **Проверки качества** `docs/dev/checks.md`: одинаковые цели `make check` (ruff, mypy, pytest без Docker, идентичность
контракта) и `make check-full` (+ интеграционные тесты против compose; для arm-core — цикл миграций и
`check_dbml_vs_orm.py`) в каждом репозитории; `make check-full` обязателен перед PR. Конфигураций CI в репозиториях
нет: в Gitea CI не будет, пайплайн платформы — вне нашей зоны.
- **Документы контекста:** `03-architecture.md` — раздел «Соглашения шаблона команды», раскладка `api/src/`, схема `dkb`,
один бакет S3, конверт результата без `group_schema`; `00-workspace-layout.md` — worktree-workspace `.wt/<срез>/`;
`06-test-data.md` §6 — справочники для seed продукта (план счетов, правила элиминации, права) живут в `arm-core/api/seed/`,
в `fixtures/directories/` остаются только тестовые справочники (ЮЛ и ГК группы «Русский бисквит», Срез 1);
`09-delivery-plan.md` — решение по пакету контрактов (без общего пакета, см. design); `07-open-questions.md` — новые OQ.
один бакет S3, обмен через NATS без JetStream (ack, повторная публикация), проверки качества вместо CI; `00-workspace-layout.md`
— worktree-workspace `.wt/<срез>/`, фикстуры для проверок; `06-test-data.md` §6 — справочники для seed продукта живут в
`arm-core/api/seed/`; `09-delivery-plan.md` — Срез 0 по факту; `07-open-questions.md` — новые OQ.
- **Сверка макета** (задача B kickoff): `10-user-journey.md`, `mockup/INDEX.md`, новые снимки `mockup/screens/`,
`scripts/mockup_crawl_extra.py`; расхождения — в `07-open-questions.md`.
## Capabilities
### New Capabilities
- `recognition-contract`: контракт обмена между ядром и сервисами распознавания — тела сообщений NATS, файл результата
в S3, правила версионирования и идемпотентности.
- `dev-environment`: воспроизводимое локальное окружение workspace (PostgreSQL, NATS JetStream, MinIO, эмулятор ИС
Сканирование, три сервиса) с одним `docker compose up`.
- `ci-template`: обязательный набор проверок CI для сервисных репозиториев и порядок их выполнения.
- `recognition-contract`: контракт обмена между ядром и сервисами распознавания — тела сообщений NATS (запрос, ack,
результат), файл результата в S3, правила версионирования и идемпотентности без JetStream.
- `dev-environment`: воспроизводимое локальное окружение workspace (PostgreSQL, NATS, MinIO, эмулятор ИС Сканирование,
три сервиса) с одним `docker compose up`, без доступа к Nexus.
- `quality-checks`: единый набор проверок качества перед PR для сервисных репозиториев (`make check`, `make check-full`).
### Modified Capabilities
<!-- openspec/specs пуст — изменяемых capability нет -->
## Impact
- Читают контракт: `arm-core` (outbox публикует запрос, consumer читает конверт результата и файл результата),
`tabular-parser` и `scan-gateway` (читают запрос, пишут результат и конверт). Каждый сервис вендорит копию схем в
`api/contracts/` и проверяет идентичность с `docs/contracts` в CI.
- Читают контракт: `arm-core` (outbox публикует запрос, consumer читает ack и конверт результата и файл результата),
`tabular-parser` и `scan-gateway` (читают запрос, пишут ack, результат и конверт). Каждый сервис вендорит копию схем
в `api/contracts/` и проверяет идентичность с `docs/contracts` в `make check`.
- Таблицы DBML v5.13, которые опираются на контракт: `recognition_job` (`request_msg_id`, `result_msg_id`,
`schema_version`, `result_schema_version`, `service_name/version`, `result_s3_object_id`, `metrics`, `group_schema`,
`error_code`), `s3_object` (`bucket_role`, `bucket`, `key`, `created_by`), `stored_file` (`file_type`). Новых полей нет.
- Внешние зависимости: Docker/Compose у разработчика; Gitea Actions с раннером, умеющим `services:` и `docker build`
(вопрос к человеку — см. design «Open Questions»).
`schema_version`, `result_schema_version`, `service_name/version`, `status` включая `processing` и `dlq`, `started_at`,
`result_s3_object_id`, `metrics`, `group_schema`, `error_code`), `s3_object` (`bucket_role`, `bucket`, `key`,
`created_by`), `stored_file` (`file_type`). Новых полей нет.
- Внешние зависимости: Docker/Compose у разработчика; публичные PyPI/npm (пакеты `raisa_*` недоступны — условные импорты
и `optionalDependencies`, см. design D-10).
## Non-goals
@ -75,4 +81,5 @@ PostgreSQL + NATS + MinIO + ИС Сканирование нечем, CI-кон
- Контракт `ex-payload-v1.schema.json` (Срез 7).
- Production-значения инфраструктуры банка (адреса NATS/S3/OpenBao, nkeys) — compose только для разработки.
- Публикация общего python-пакета контрактов (решено: не делать, см. design D-1).
- Конфигурации CI для Gitea или платформы банка.
- Какие-либо экраны фронта.

View File

@ -1,52 +0,0 @@
# Spec Delta: ci-template
## Purpose
Единый шаблон CI (Gitea Actions) для сервисных репозиториев АРМ КА: какие проверки обязательны, в каком порядке и с
какими внешними ресурсами они выполняются, чтобы PR в любой из трёх репозиториев проверялся одинаково.
## ADDED Requirements
### Requirement: Обязательные проверки Python-сервиса
Шаблон `docs/dev/ci/python-service.yml` SHALL выполнять на каждый push в ветку и PR в `main`: установку зависимостей из
`api/requirements.txt` и `api/requirements-dev.txt`; `ruff check` и `ruff format --check` по `api/`; `mypy api/src`;
`pytest` из `api/` с сервисами PostgreSQL 15, NATS 2.10 (JetStream) и MinIO, доступными по переменным `OPENBAO__*`;
тест идентичности вендоренного контракта с `docs/contracts`; сборку образа `docker build` без push.
#### Scenario: PR с нарушением формата
- **WHEN** в PR есть файл, не отформатированный `ruff format`
- **THEN** job `lint` падает до запуска тестов, остальные job'ы не стартуют
#### Scenario: Зелёный PR
- **WHEN** все проверки проходят
- **THEN** статус PR в Gitea — success, артефакт отчёта pytest (junit) прикреплён к run
### Requirement: Дополнительные проверки arm-core
Для `arm-core` шаблон SHALL дополнительно: применить миграции с нуля (`alembic upgrade head`) к пустой БД, откатить
(`alembic downgrade base`) и применить снова; запустить `api/scripts/check_dbml_vs_orm.py` против
`docs/model/data-model-v5.13.dbml`; выполнить job `web`: `npm ci`, `npx biome check src`, `npx tsc --noEmit`, `npm run build`.
#### Scenario: Расхождение ORM и DBML
- **WHEN** в ORM добавлена колонка, которой нет в DBML
- **THEN** шаг `check_dbml_vs_orm` падает с перечнем расхождений, PR красный
#### Scenario: Миграция без отката
- **WHEN** `downgrade` ревизии не удаляет созданный ею объект
- **THEN** шаг «upgrade → downgrade → upgrade» падает
### Requirement: Доступ к соседним репозиториям
Шаблон SHALL получать `arm-ka-docs` (для контракта и DBML) и `arm-ka-fixtures` (для тестов на реальных данных)
checkout'ом в `../docs` и `../fixtures` c токеном только на чтение из секретов репозитория (`DOCS_TOKEN`,
`FIXTURES_TOKEN`); при отсутствии секрета фикстурные тесты SHALL пропускаться с явным предупреждением, а не падать.
#### Scenario: Нет доступа к фикстурам
- **WHEN** секрет `FIXTURES_TOKEN` не задан
- **THEN** тесты с маркером `fixtures` помечены skipped с причиной «ARM_KA_FIXTURES недоступен», остальные выполняются
### Requirement: Шаблон параметризуется именем сервиса
Копия шаблона в `<repo>/.gitea/workflows/ci.yml` SHALL отличаться от `docs/dev/ci/python-service.yml` только блоком
`env:` (имя сервиса, порт, включение шагов arm-core); расхождение структуры шагов считается ошибкой ревью.
#### Scenario: Сверка с шаблоном
- **WHEN** ревьюер сравнивает `ci.yml` сервиса с шаблоном (`diff` без блока `env:`)
- **THEN** различий нет

View File

@ -2,21 +2,21 @@
## Purpose
Локальное окружение разработки всего workspace одной командой: инфраструктура (PostgreSQL, NATS JetStream, MinIO),
Локальное окружение разработки всего workspace одной командой: инфраструктура (PostgreSQL 16, NATS без JetStream, MinIO),
эмулятор внешней ИС Сканирование и три сервиса системы, собранные из соседних каталогов workspace.
## ADDED Requirements
### Requirement: Инфраструктура поднимается одной командой
`docker compose -f docs/dev/docker-compose.yml up -d` без профилей SHALL поднять PostgreSQL 15 (БД `armka`,
пользователь `armka`, порт 5432), NATS 2.10 с включённым JetStream (порты 4222, мониторинг 8222) и MinIO
`docker compose -f docs/dev/docker-compose.yml up -d` без профилей SHALL поднять PostgreSQL 16 (БД `armka`,
пользователь `armka`, порт 5432), NATS 2.10 **без JetStream** (как на платформе; порты 4222, мониторинг 8222) и MinIO
(API 9000, консоль 9001) с уже созданным бакетом `arm-ka`. Все учётные данные — в `docs/dev/.env.example`, копируемом
в `docs/dev/.env`.
#### Scenario: Чистый старт
- **WHEN** разработчик выполняет `cp .env.example .env && docker compose up -d` в `docs/dev/` на машине без томов
- **THEN** в течение 60 с все три контейнера проходят healthcheck, `psql` подключается к `armka`, `nats stream ls`
отвечает (JetStream включён), `mc ls local/arm-ka` возвращает пустой бакет
- **THEN** в течение 60 с все три контейнера проходят healthcheck, `psql` подключается к `armka`, `curl :8222/varz`
отвечает и `jetstream` в нём отсутствует/выключен, `mc ls local/arm-ka` возвращает пустой бакет
### Requirement: Профиль сервисов собирает приложения из workspace
Профиль `services` SHALL добавить контейнеры `arm-core`, `tabular-parser`, `scan-gateway` и `scan-emulator`, собранные
@ -27,12 +27,12 @@ workspace `.wt/<срез>/`). Сервисы читают настройки ч
#### Scenario: Сервисы стартуют против инфраструктуры compose
- **WHEN** выполняется `docker compose --profile services up -d --build`
- **THEN** `GET http://localhost:8000/healthcheck` (arm-core), `:8001/healthcheck` (tabular-parser),
`:8002/healthcheck` (scan-gateway) отвечают 200 с `version`; в логах нет ошибок подключения к NATS/S3; stream
`RECOGNITION` существует
`:8002/healthcheck` (scan-gateway) отвечают 200 с `version` и `nats = ok`; в логах нет ошибок подключения к NATS/S3
#### Scenario: Запуск из venv против compose
- **WHEN** разработчик поднял только инфраструктуру и запускает `python run_server.py` из `api/` любого сервиса с
`.env`, скопированным из `docs/dev/.env.example`
#### Scenario: Запуск из venv против compose без Nexus
- **WHEN** разработчик поднял только инфраструктуру, создал venv командой `make venv` (ставит `requirements.txt` без
строк `raisa_*`) и запускает `python run_server.py` из `api/` любого сервиса с `.env`, скопированным из
`docs/dev/.env.example`
- **THEN** сервис подключается к тем же PostgreSQL/NATS/MinIO по `localhost` и проходит healthcheck
### Requirement: Эмулятор ИС Сканирование воспроизводит горячие папки

View File

@ -0,0 +1,57 @@
# Spec Delta: quality-checks
## Purpose
Единый набор проверок качества для сервисных репозиториев АРМ КА, выполняемый локально одной командой перед каждым PR:
какие проверки обязательны, в каком порядке и с какими внешними ресурсами. Конфигураций CI в репозиториях нет — в Gitea
CI не используется, пайплайн платформы банка вне зоны проекта.
## ADDED Requirements
### Requirement: `make check` — быстрые проверки без Docker
Каждый сервисный репозиторий SHALL предоставлять цель `make check`, выполняющую из `api/`: `ruff check .`,
`ruff format --check .`, `mypy src`, `pytest -m "not integration"`, тест идентичности вендоренного контракта с
`../docs/contracts`. Цель SHALL завершаться ненулевым кодом при первой упавшей проверке и MUST NOT требовать запущенного
Docker, PostgreSQL или NATS.
#### Scenario: Файл не отформатирован
- **WHEN** в репозитории есть файл, не отформатированный `ruff format`
- **THEN** `make check` завершается с кодом 1 на шаге `ruff format --check`, следующие шаги не выполняются
#### Scenario: Чистый репозиторий
- **WHEN** все проверки проходят
- **THEN** `make check` завершается с кодом 0 за время, не превышающее нескольких минут, без обращения к сети
### Requirement: `make check-full` — полная проверка против compose
Каждый сервисный репозиторий SHALL предоставлять цель `make check-full`: `make check` плюс `pytest -m integration` против
инфраструктуры `docs/dev/docker-compose.yml` (PostgreSQL 16, NATS, MinIO, для `scan-gateway` — эмулятор). Для `arm-core`
цель SHALL дополнительно выполнять «`alembic upgrade head` → `alembic downgrade base` → `alembic upgrade head`» на чистой
БД и `python scripts/check_dbml_vs_orm.py` против `../docs/model/data-model-v5.13.dbml`. `make check-full` SHALL быть
выполнен перед созданием PR, его итог вписывается в описание PR.
#### Scenario: Расхождение ORM и DBML
- **WHEN** в ORM добавлена колонка, которой нет в DBML
- **THEN** шаг `check_dbml_vs_orm` падает с перечнем расхождений, `make check-full` завершается с кодом 1
#### Scenario: Миграция без отката
- **WHEN** `downgrade` ревизии не удаляет созданный ею объект
- **THEN** шаг «upgrade → downgrade → upgrade» падает
### Requirement: Фикстуры и соседние репозитории — опциональны с предупреждением
Проверки SHALL брать `docs` и `fixtures` из соседних каталогов workspace (`../docs`, `../fixtures`) или из переменных
`ARM_KA_DOCS`, `ARM_KA_FIXTURES`. При отсутствии фикстур тесты с маркером `fixtures` SHALL пропускаться с явной причиной;
при отсутствии docs тест идентичности контракта SHALL пропускаться с предупреждением. Отсутствие соседей MUST NOT делать
`make check` красным.
#### Scenario: Нет фикстур
- **WHEN** `../fixtures` отсутствует и `ARM_KA_FIXTURES` не задан
- **THEN** тесты с маркером `fixtures` помечены skipped с причиной «ARM_KA_FIXTURES недоступен», остальные выполняются
### Requirement: Одинаковый набор целей во всех репозиториях
`Makefile` каждого сервисного репозитория SHALL содержать цели `up`, `test`, `test-integration`, `lint`, `check`,
`check-full` с одинаковой семантикой (описана в `docs/dev/checks.md`); репозиторий MAY добавлять свои цели (`migrate`,
`seed`, `emulator`), но MUST NOT менять смысл общих.
#### Scenario: Сверка с описанием
- **WHEN** ревьюер сравнивает список общих целей `Makefile` сервиса с `docs/dev/checks.md`
- **THEN** все общие цели присутствуют и запускают описанные команды

View File

@ -24,12 +24,34 @@
- **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`)
- **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` и не падает
### Requirement: Подтверждение взятия в работу (ack без JetStream)
Получив запрос, сервис SHALL немедленно, до чтения файла из S3, опубликовать в `recognition.result` конверт со
`status = processing`, `job_id`, `file_id`, `service {name, version}` и заголовком `Nats-Msg-Id = ack:<job_id>:<attempt>`.
Ядро SHALL перевести задание `published → processing` и записать `started_at`, `service_name`, `service_version`. Если
ack не получен за `ACK_TIMEOUT_SECONDS` (по умолчанию 30), ядро SHALL опубликовать запрос повторно с тем же
`Nats-Msg-Id` и тем же `attempt`, не более `OUTBOX_MAX_REPUBLISH` (по умолчанию 3) раз, после чего перевести задание в `dlq`.
#### Scenario: Сервис жив
- **WHEN** запрос опубликован и сервис подписан
- **THEN** в течение `ACK_TIMEOUT_SECONDS` в `recognition.result` появляется конверт `processing`, задание получает
`status = processing` и `started_at`
#### Scenario: Сервис недоступен
- **WHEN** ни один инстанс сервиса не подписан на subject запроса
- **THEN** ядро публикует запрос повторно каждые `ACK_TIMEOUT_SECONDS`; после `OUTBOX_MAX_REPUBLISH` повторов задание
получает `status = dlq`, `request_msg_id` не меняется
#### Scenario: Повторный запрос пришёл в работающий сервис
- **WHEN** сервис получает запрос с `job_id`, который уже обрабатывается или обработан этим процессом
- **THEN** сервис публикует ack повторно и не начинает вторую обработку
### Requirement: Сообщение результата распознавания
Тело сообщения на subject `recognition.result` SHALL соответствовать `docs/contracts/recognition-result-message.schema.json`
v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `file_id`, `status` (`done` | `needs_check` | `error`),
`service {name, version}`; при `status ≠ error` обязательны `result_s3 {bucket, key}` и `result_schema_version`;
при `status = error` обязателен `error {code, message}`; опциональны `raw_s3_key` (только `scan-gateway`) и `metrics`.
Поле `group_schema` в конверт NOT включается — источник `recognition_job.group_schema` — файл результата.
v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `file_id`, `status` (`processing` | `done` | `needs_check` |
`error`), `service {name, version}`; при `status ∈ {done, needs_check}` обязательны `result_s3 {bucket, key}` и
`result_schema_version`; при `status = error` обязателен `error {code, message}`; опциональны `raw_s3_key`
(только `scan-gateway`) и `metrics`. Поле `group_schema` в конверт NOT включается — источник `recognition_job.group_schema`
— файл результата.
#### Scenario: Успешный результат
- **WHEN** сервис завершил обработку файла
@ -63,10 +85,20 @@ v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `f
- **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH`, запрос подтверждается (ack), чтобы не
зациклить доставку
### Requirement: Идемпотентность результатов без брокера
Публикующий SHALL ставить заголовок `Nats-Msg-Id` на каждое сообщение (запрос `<job_id>:<attempt>`, ack
`ack:<job_id>:<attempt>`, результат `result:<job_id>:<attempt>`). Поскольку серверного дедупа нет, ядро SHALL считать
результат дубликатом, если `recognition_job.result_msg_id` уже равен `Nats-Msg-Id` сообщения, и MUST NOT менять задание
повторно; потерянный результат SHALL обнаруживаться по `reply_deadline` (`timeout`).
#### Scenario: Результат доставлен дважды
- **WHEN** сервис опубликовал один и тот же результат два раза (повтор публикации после сетевой ошибки)
- **THEN** задание изменено один раз, второе сообщение проигнорировано
### Requirement: Копия контракта в сервисе идентична источнику
Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в CI их
байтовую идентичность файлам `docs/contracts/` (checkout `arm-ka-docs` или путь `ARM_KA_DOCS`).
Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check`
их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением).
#### Scenario: Копия устарела
- **WHEN** в `docs/contracts/` изменена схема, а копия в сервисе — нет
- **THEN** тест идентичности в CI сервиса падает с указанием имени файла
- **THEN** тест идентичности в `make check` сервиса падает с указанием имени файла

View File

@ -10,59 +10,65 @@
(метасхема 2020-12 валидна)
- [ ] 1.2 [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recognition-contract` (поля, enum,
форматы uuid/date-time); проверить метасхемой
- [ ] 1.3 [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт результата, условные
обязательные поля через `if/then`); проверить метасхемой
- [ ] 1.3 [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт: `status` ∈ processing | done |
needs_check | error; условные обязательные поля через `if/then`); проверить метасхемой
- [ ] 1.4 [docs] Примеры `docs/contracts/examples/`: `request-tabular.json`, `request-image.json`,
`result-message-done.json`, `result-message-error.json`, `result-osv-minimal.json`, `result-statement-minimal.json`,
`result-error.json`; скрипт `docs/scripts/validate_contracts.py` валидирует все примеры схемами и возвращает 0
`result-message-processing.json`, `result-message-done.json`, `result-message-error.json`, `result-osv-minimal.json`,
`result-statement-minimal.json`, `result-error.json`; скрипт `docs/scripts/validate_contracts.py` валидирует все
примеры схемами и возвращает 0
- [ ] 1.5 [docs] `03-architecture.md`: таблица «Контракт обмена через NATS» — ссылки на три файла схем, правила
`Nats-Msg-Id`, убрать `group_schema` из конверта; проверить, что текст ссылается на существующие файлы
`Nats-Msg-Id`, ack через `processing`, повторная публикация; проверить, что текст ссылается на существующие файлы
## 2. Инфраструктура разработки
- [ ] 2.1 [docs] `docs/dev/docker-compose.yml`: сервисы `postgres` (15), `nats` (2.10, `--jetstream`), `minio`,
- [ ] 2.1 [docs] `docs/dev/docker-compose.yml`: сервисы `postgres` (16), `nats` (2.10, без `--jetstream`), `minio`,
`minio-init` (создаёт бакет `arm-ka`), healthcheck'и, именованные тома; `docs/dev/.env.example`; проверка:
`docker compose up -d` → все healthy, `mc ls` видит бакет
- [ ] 2.2 [docs] `docs/dev/README.md`: запуск инфраструктуры, переменные, сброс томов, запуск сервисов из venv (черновик,
дописывается в 5.2)
`docker compose up -d` → все healthy, `mc ls` видит бакет, `curl :8222/varz` отвечает
- [ ] 2.2 [docs] `docs/dev/README.md`: запуск инфраструктуры, переменные, сброс томов, запуск сервисов из venv без Nexus
(`make venv` фильтрует `raisa_*`), черновик — дописывается в 5.4
## 3. Ядро (детали — `arm-core/openspec/changes/slice-0-skeleton/tasks.md`)
- [ ] 3.1 [arm-core] Инструменты: `api/pyproject.toml` (ruff, mypy), `requirements-dev.txt` с `-r requirements.txt`,
`Dockerfile` (+ строка убрана из `.gitignore`), `Makefile` (up/test/lint/migrate), `ENV_FILE` в Settings
- [ ] 3.2 [arm-core] ORM 1:1 с DBML v5.13 + миграция 0001 + `check_dbml_vs_orm.py`; проверка: upgrade → downgrade → upgrade на compose-PostgreSQL, скрипт сверки — 0 расхождений
`Dockerfile` (`python:3.11-slim`, + строка убрана из `.gitignore`), `Makefile` (общие цели + `migrate`, `seed`),
`ENV_FILE` в Settings, `raisa-*` в `web/package.json` → `optionalDependencies`
- [ ] 3.2 [arm-core] ORM 1:1 с DBML v5.13 + миграция 0001 + `check_dbml_vs_orm.py`; проверка: upgrade → downgrade → upgrade
на compose-PostgreSQL 16, скрипт сверки — 0 расхождений
- [ ] 3.3 [arm-core] Seed справочников/ролей/прав + dev-пользователи; auth (JWT, dev-эмитент, `require(permission)`), `GET /api/v1/me`
- [ ] 3.4 [arm-core] Вендоренный контракт + outbox-паблишер + consumer `recognition.result` + вахтёр `reply_deadline`;
интеграционный тест на compose: queued → published → результат → done, дубликат игнорируется
- [ ] 3.5 [arm-core] `.gitea/workflows/ci.yml` из шаблона 4.1 (после его готовности), job `web`
- [ ] 3.4 [arm-core] Вендоренный контракт + outbox-паблишер + подписка `recognition.result` (queue group `arm-core`): ack
`processing`, повторная публикация без ack, дедуп по `result_msg_id`, `dlq`, вахтёр `reply_deadline`; интеграционный
тест на compose: queued → published → processing → done; дубликат результата игнорируется; потерянный запрос
публикуется повторно
- [ ] 3.5 [arm-core] `make check-full` зелёный; итог — в описание PR
## 4. CI-шаблон (после того как хотя бы один сервис имеет тесты)
## 4. Проверки качества (параллельно с 3.x, после первых тестов)
- [ ] 4.1 [docs] `docs/dev/ci/python-service.yml`: job'ы `lint`, `test` (services: postgres/nats/minio), `contract`,
`build`; опциональные шаги arm-core (`migrations`, `dbml-check`); checkout `arm-ka-docs`/`arm-ka-fixtures` по
секретам с graceful skip; проверка: workflow проходит в `arm-core` на ветке среза
- [ ] 4.2 [docs] `docs/dev/ci/web.yml` (фрагмент job `web` для arm-core: npm ci, biome, tsc, build); проверка — зелёный run в arm-core
- [ ] 4.3 [docs] `docs/dev/ci/README.md`: как копировать шаблон, какие секреты нужны (`DOCS_TOKEN`, `FIXTURES_TOKEN`,
реестр образов), правило «отличия только в блоке env»
- [ ] 4.1 [docs] `docs/dev/checks.md`: общие цели `Makefile` (`up`, `test`, `test-integration`, `lint`, `check`,
`check-full`), что делает каждая, порядок перед PR, как пропускаются тесты без фикстур/docs; проверка: цели
`arm-core` соответствуют описанию
- [ ] 4.2 [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` сервисов через `include` или
копируется); проверка: `make check` в трёх репозиториях вызывает одинаковые команды (`diff` общих целей)
## 5. Сервисы распознавания (детали — в их change'ах) и профиль `services`
- [ ] 5.1 [tabular-parser] Вырезать фронт/БД, вендоренный контракт, worker NATS/S3 с заглушкой `NOT_IMPLEMENTED`, Dockerfile, CI
- [ ] 5.2 [scan-gateway] Вырезать фронт/БД, вендоренный контракт, горячие папки (local/smb), эмулятор, worker с
таймаутом, Dockerfile, CI
- [ ] 5.1 [tabular-parser] Вырезать фронт/БД, вендоренный контракт, подписчик queue group `tabular-parser` с ack
`processing` и заглушкой `NOT_IMPLEMENTED`, Dockerfile 3.11, `make check-full`
- [ ] 5.2 [scan-gateway] Вырезать фронт/БД, вендоренный контракт, горячие папки (local/smb), эмулятор, подписчик queue group
`scan-gateway` с ack и таймаутом, Dockerfile 3.11, `make check-full`
- [ ] 5.3 [docs] `docker-compose.yml`: профиль `services` — `arm-core` (8000), `tabular-parser` (8001), `scan-gateway`
(8002), `scan-emulator` (том `hotfolder`, `../../fixtures/ocr:ro`); проверка: `--profile services up --build`,
все `/healthcheck` 200, stream `RECOGNITION` создан
все `/healthcheck` 200 с `nats = ok`
- [ ] 5.4 [docs] Сквозная проверка руками по README: вставить `recognition_job` в БД через `psql` (или тестовый скрипт
arm-core) → паблишер публикует → `tabular-parser` отвечает `NOT_IMPLEMENTED` → `recognition_job.status = error`,
`error_code = NOT_IMPLEMENTED`; то же для `image` через эмулятор (`raw/{job_id}.json` появляется в MinIO);
зафиксировать в README как «проверка окружения»
arm-core) → паблишер публикует → `tabular-parser` отвечает ack и `NOT_IMPLEMENTED` → `recognition_job.status`
проходит `published → processing → error`, `error_code = NOT_IMPLEMENTED`; то же для `image` через эмулятор
(`raw/{job_id}.json` появляется в MinIO); остановить `tabular-parser`, вставить задание — через 30 с повторная
публикация, после трёх — `dlq`; зафиксировать в README как «проверка окружения»
## 6. Документы контекста и закрытие
- [ ] 6.1 [docs] `06-test-data.md` §6: `fixtures/directories/` — только тестовые ЮЛ/ГК (Срез 1); план счетов, правила
элиминации, права — `arm-core/api/seed/`; `09-delivery-plan.md` Срез 0: решение D-1 по пакету контрактов,
ссылка на `docs/dev/`
ссылка на `docs/dev/`, проверки вместо CI
- [ ] 6.2 [docs] `07-open-questions.md`: OQ по итогам Среза 0 (контракт `data_type = NU` vs enum `osv_data_type`;
обязательность ОСВ расширенного подхода; источник полного плана счетов 1С) и расхождения макета из задачи B;
статусы допущений

View File

@ -3,7 +3,7 @@ schema: spec-driven
context: |
Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.13), правила домена, контракты между сервисами
(docs/contracts/*.schema.json), макет UI и экспорт ТЗ системы АРМ КА (ДКБ, Россельхозбанк).
Здесь оформляются СКВОЗНЫЕ изменения: контракт + окружение разработки (docs/dev) + CI-шаблон + изменения
Здесь оформляются СКВОЗНЫЕ изменения: контракт + окружение разработки (docs/dev) + проверки качества (make check) + изменения
документов контекста. Код сервисов живёт в соседних репозиториях (../arm-core, ../tabular-parser, ../scan-gateway),
у каждого свой openspec/ и свой change; сквозной change ссылается на них.
Источники истины по приоритету: docs/model/data-model-v5.13.dbml → docs/model/entities-v5.13.md →