From a0be5ab5a1a1a3b16d79410ba7a25c877a4bf4e9 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 16:21:39 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=A1=D1=80=D0=B5=D0=B7=200=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=20=D1=84=D0=B0=D0=BA=D1=82=D1=8B=20=D0=BF=D0=BB?= =?UTF-8?q?=D0=B0=D1=82=D1=84=D0=BE=D1=80=D0=BC=D1=8B=20=E2=80=94=20NATS?= =?UTF-8?q?=20=D0=B1=D0=B5=D0=B7=20JetStream,=20Python=203.11,=20PostgreSQ?= =?UTF-8?q?L=2016,=20=D0=B1=D0=B5=D0=B7=20CI=20=D0=B8=20Nexus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По ответам пользователя 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 --- context/00-workspace-layout.md | 5 +- context/03-architecture.md | 48 ++++-- context/09-delivery-plan.md | 20 ++- openspec/changes/slice-0-skeleton/design.md | 137 +++++++++++------- openspec/changes/slice-0-skeleton/proposal.md | 77 +++++----- .../specs/ci-template/spec.md | 52 ------- .../specs/dev-environment/spec.md | 20 +-- .../specs/quality-checks/spec.md | 57 ++++++++ .../specs/recognition-contract/spec.md | 46 +++++- openspec/changes/slice-0-skeleton/tasks.md | 64 ++++---- openspec/config.yaml | 2 +- 11 files changed, 315 insertions(+), 213 deletions(-) delete mode 100644 openspec/changes/slice-0-skeleton/specs/ci-template/spec.md create mode 100644 openspec/changes/slice-0-skeleton/specs/quality-checks/spec.md diff --git a/context/00-workspace-layout.md b/context/00-workspace-layout.md index 567713b..56501c0 100644 --- a/context/00-workspace-layout.md +++ b/context/00-workspace-layout.md @@ -28,8 +28,9 @@ - **Ветки и коммиты — всегда `git -C `** или с явным `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 указывать diff --git a/context/03-architecture.md b/context/03-architecture.md index b277b30..9420f84 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -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::` | `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::`. + +**На платформе 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 РАИСА. diff --git a/context/09-delivery-plan.md b/context/09-delivery-plan.md index 8c51a5c..19eb5bc 100644 --- a/context/09-delivery-plan.md +++ b/context/09-delivery-plan.md @@ -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 — Справочники (ГК / ЮЛ / ГСК / ГСЗ / филиалы) + пользователи и роли diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index b00b507..fc3dc0a 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -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` по естественному ключу попытки -Запрос: `:`; результат: `result::`. 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` + (`:`, тот же `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` собирает сервисы из `../../` (работает и из `.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` копируется в `/.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` | diff --git a/openspec/changes/slice-0-skeleton/proposal.md b/openspec/changes/slice-0-skeleton/proposal.md index 88fc410..5141467 100644 --- a/openspec/changes/slice-0-skeleton/proposal.md +++ b/openspec/changes/slice-0-skeleton/proposal.md @@ -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` запроса = `:`, результата = `result::`. - - Примеры сообщений и результата — `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` запроса = `:`, результата = `result::`, ack — + `ack::`; без 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 ## 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 или платформы банка. - Какие-либо экраны фронта. diff --git a/openspec/changes/slice-0-skeleton/specs/ci-template/spec.md b/openspec/changes/slice-0-skeleton/specs/ci-template/spec.md deleted file mode 100644 index a65d0ac..0000000 --- a/openspec/changes/slice-0-skeleton/specs/ci-template/spec.md +++ /dev/null @@ -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: Шаблон параметризуется именем сервиса -Копия шаблона в `/.gitea/workflows/ci.yml` SHALL отличаться от `docs/dev/ci/python-service.yml` только блоком -`env:` (имя сервиса, порт, включение шагов arm-core); расхождение структуры шагов считается ошибкой ревью. - -#### Scenario: Сверка с шаблоном -- **WHEN** ревьюер сравнивает `ci.yml` сервиса с шаблоном (`diff` без блока `env:`) -- **THEN** различий нет diff --git a/openspec/changes/slice-0-skeleton/specs/dev-environment/spec.md b/openspec/changes/slice-0-skeleton/specs/dev-environment/spec.md index 604775e..72557ad 100644 --- a/openspec/changes/slice-0-skeleton/specs/dev-environment/spec.md +++ b/openspec/changes/slice-0-skeleton/specs/dev-environment/spec.md @@ -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: Эмулятор ИС Сканирование воспроизводит горячие папки diff --git a/openspec/changes/slice-0-skeleton/specs/quality-checks/spec.md b/openspec/changes/slice-0-skeleton/specs/quality-checks/spec.md new file mode 100644 index 0000000..6afc84e --- /dev/null +++ b/openspec/changes/slice-0-skeleton/specs/quality-checks/spec.md @@ -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** все общие цели присутствуют и запускают описанные команды diff --git a/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md b/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md index 8e26f37..219679d 100644 --- a/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md +++ b/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md @@ -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::`. +Ядро 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` на каждое сообщение (запрос `:`, ack +`ack::`, результат `result::`). Поскольку серверного дедупа нет, ядро 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` сервиса падает с указанием имени файла diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 7ca3ae6..9b098f5 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -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; статусы допущений diff --git a/openspec/config.yaml b/openspec/config.yaml index fc42e4c..fb4f6a8 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -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 →