arm-ka-docs/dev/README.md
Andrey Epifancev d643453dee Устаревшая документация: модель v5.17, диаграммы по частям, сделанное — в сделанное (#60)
- Ссылки на источник истины v5.13/v5.14/v5.15 → v5.17: openspec/config.yaml, KICKOFF-PROMPT, 00-workspace-layout,
  08-source-docs-index, заголовок 05-domain-rules, dev/checks.md; спека quality-checks — без привязки к версии
  (путь — переменная DBML Makefile).
- Диаграммы model/diagrams/part01…09 перерисованы по v5.17 (были v5.12): scripts/gen_model_diagrams.py
  (разбиение DBML на части + dbml-renderer + resvg через npx, версии закреплены), цель make diagrams;
  описание в 04-data-model-guide, 06-test-data, 08-source-docs-index.
- dev/frontend-api-guide.md: КПП (arm-core #52) и DB_SCHEMA (#53) — из §11 в §1, КПП в маршрутах §5.2/§5.3,
  схеме §5.8 и каталоге ошибок §9 (сверено с кодом 99669cd).
- Ссылки на заархивированные change'и (dev/README, dev/checks, api-contract-slice-3b/4), путь к v4 DBML в индексе.
- 09-delivery-plan: статусы Срезов 0, 2d, 3b, 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 23:35:44 +04:00

443 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Окружение разработки АРМ КА (`docs/dev/`)
Локальная инфраструктура workspace одной командой: PostgreSQL 16, NATS 2.10 без JetStream и MinIO с бакетом `arm-ka`.
Профиль `services` добавляет к ней сервисы системы, собранные из соседних репозиториев workspace: `arm-core`,
`tabular-parser`, `scan-gateway` и эмулятор ИС Сканирование `scan-emulator` (см. «Профиль `services`»). Без профиля
поднимается только инфраструктура, а сервисы можно запускать из venv (см. «Запуск сервисов из venv»).
Для ручного тестирования стенда (установка ПО, запуск, чек-лист сценариев по ролям) — [`qa-guide.md`](qa-guide.md).
Разработчику фронта: **начинать с [`frontend-api-guide.md`](frontend-api-guide.md)** — единый гайд по API `arm-core` (подключение,
общие правила, все маршруты, ошибки, сценарии экранов, что недавно изменилось и что изменится); как устроены вход и права `arm-core`, какие ручки когда дёргать, семантика 401/403/404 —
[`frontend-auth-guide.md`](frontend-auth-guide.md); файлы, документы и справочники — [`api-contract-files-directories.md`](api-contract-files-directories.md); контракт API консолидации — [`api-contract-slice-4.md`](api-contract-slice-4.md); контракт API форм ББ/ОФР и пути
пользователя до сводной ОСВ — [`api-contract-slice-3b.md`](api-contract-slice-3b.md).
Перенос сервисов в контур банка (снимки без истории, что вырезается, скрипт подготовки, проверки в контуре) —
[`contour-transfer-guide.md`](contour-transfer-guide.md).
## Предварительные требования
- Docker и compose v2 (проверено на docker 28 / compose 2.34).
- `uv` и Python 3.11 — для запуска сервисов из venv (версия Python — как на платформе).
- Свободные порты хоста (по умолчанию доступны только с `127.0.0.1`, см. `BIND_ADDR`): 5432 (PostgreSQL), 4222 и 8222 (NATS и его мониторинг), 9000 и 9001 (MinIO API и консоль);
для профиля `services` ещё 8000 (`arm-core`), 8001 (`tabular-parser`), 8002 (`scan-gateway`).
Занятые порты меняются в `.env` (см. «Типичные проблемы»).
- Для профиля `services`: соседние каталоги `../../arm-core`, `../../tabular-parser`, `../../scan-gateway` и
`../../fixtures` относительно `docs/dev/` (в workspace и в зеркальном worktree `.wt/<срез>/` это одни и те же соседи
`docs`). Сборка образов ставит зависимости из PyPI (строки `raisa_*` отфильтрованы, Nexus не нужен).
## Запуск инфраструктуры
Команды — из каталога `docs/dev/`:
```bash
cp .env.example .env # .env в git не попадает
docker compose up -d # вернётся сразу; healthy — примерно через 6 с
```
Ожидание готовности (для скриптов и `Makefile`):
```bash
docker compose up -d && docker compose up -d --wait postgres nats minio
```
Голый `docker compose up -d --wait` **возвращает код 1**: одноразовый `minio-init` (создаёт бакет и завершается с
кодом 0) compose 2.34 считает сбоем (`container arm-ka-minio-init-1 exited (0)`). Поэтому `--wait` нужно давать со
списком долгоживущих сервисов — как выше.
Проверка:
```bash
docker compose ps # postgres, nats, minio — Up (healthy); minio-init — Exited (0)
docker compose exec postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "select version()"'
curl -s localhost:8222/healthz # {"status":"ok"}
curl -s localhost:8222/varz | grep -A1 jetstream # "jetstream": {} — JetStream не включён
docker compose run --rm --entrypoint sh minio-init -c 'mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && mc ls local'
# [...] 0B arm-ka/
```
Консоль MinIO — <http://localhost:9001> (по умолчанию `minioadmin` / `minioadmin`); S3 API — `localhost:9000`.
Схема БД (`dkb`) создаётся миграциями `arm-core`, не compose.
## Профиль `services`
Сервисы системы в контейнерах против той же инфраструктуры. Образы собираются из `Dockerfile` в корне соседних
репозиториев (`../../arm-core`, `../../tabular-parser`, `../../scan-gateway`) и получают теги `arm-core:dev`,
`tabular-parser:dev`, `scan-gateway:dev`. Настройки сервисов задаёт сам compose (переменные `OPENBAO__*` шаблона с
адресами внутри сети compose — `postgres:5432`, `nats:4222`, `minio:9000`, `DEBUG=true`); второй блок `.env` и
`api/.env` сервисов на контейнеры не влияют.
Команды — из каталога `docs/dev/`:
```bash
cp .env.example .env # если ещё не сделано
docker compose --profile services up -d --build # сборка образов и запуск; первая сборка — около минуты
docker compose --profile services up -d --wait arm-core tabular-parser scan-gateway scan-emulator # ждать healthy
```
| Сервис | Порт хоста (`.env`) | Порт в контейнере | Что делает |
|---|---|---|---|
| `arm-core` | 8000 (`ARM_CORE_PORT`) | 8000 | ядро: API, outbox/inbox `recognition_job`, подписка `recognition.result` |
| `tabular-parser` | 8001 (`TABULAR_PARSER_PORT`) | 8000 | подписка `recognition.tabular.request` (queue group `tabular-parser`) |
| `scan-gateway` | 8002 (`SCAN_GATEWAY_PORT`) | 8000 | подписка `recognition.image.request` (queue group `scan-gateway`), горячая папка — том `hotfolder` |
| `scan-emulator` | — | — | эмулятор ИС Сканирование: следит за `Input/` тома `hotfolder`, ответы — из `../../fixtures/ocr` (только чтение) |
| `arm-core-migrate` | — | — | одноразовый: `alembic upgrade head` и `python -m scripts.seed --dev`, затем `arm-core` стартует |
Все три сервиса внутри контейнера слушают **8000** (значение по умолчанию `SERVER__PORT`); различаются только порты
хоста. Сервис, запущенный из venv, по умолчанию тоже слушает 8000 на хосте — он конфликтует с контейнером `arm-core`
(остановите контейнер или задайте `SERVER__PORT` в `api/.env`).
Порядок старта задан `depends_on`: `arm-core-migrate` ждёт healthy `postgres`; `arm-core` — `postgres`, `nats`, `minio`,
успешного завершения `minio-init` и `arm-core-migrate`; `tabular-parser` и `scan-gateway` — `nats`, `minio` и
`minio-init`. Одноразовые `minio-init` и `arm-core-migrate` выполняются при каждом `up` профиля (миграции и seed
идемпотентны: повторный seed пишет `0 / 0 / N` — без изменений) и остаются в `ps -a` как `Exited (0)`.
Проверка (фактический вывод на чистых томах, 2026-10-04, сокращён):
```bash
docker compose --profile services ps -a
# arm-core Up (healthy) 127.0.0.1:8000->8000/tcp
# arm-core-migrate Exited (0)
# minio-init Exited (0)
# scan-emulator Up 8000/tcp (healthcheck нет: процесс без HTTP)
# scan-gateway Up (healthy) 127.0.0.1:8002->8000/tcp
# tabular-parser Up (healthy) 127.0.0.1:8001->8000/tcp
curl -s localhost:8000/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok"}
curl -s localhost:8001/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok"}
curl -s localhost:8002/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok","hotfolder":"ok"}
docker compose --profile services logs arm-core-migrate # Running upgrade -> 0001_dbml_v5_13 …; Seed применён …
docker compose --profile services logs scan-emulator # … корень /data/hotfolder …; Фикстуры: /fixtures/ocr, фикстур: 2
```
`/healthcheck` всех трёх сервисов **всегда отвечает 200**, поэтому healthcheck контейнеров читает поля ответа:
`nats = ok` (и `hotfolder = ok` у `scan-gateway`). Статус `healthy` означает, что сервис подключён к NATS.
Dev-токен `arm-core` (в compose включён dev-эмитент, `AUTH_DEV_ISSUER=true`, `DEBUG=true`; пользователи — из
`seed --dev`, см. `../../arm-core/README.md`):
```bash
curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' -d '{"email":"ka_rf@armka.local"}'
curl -s localhost:8000/api/v1/me -H "Authorization: Bearer <JWT>" # email, роли KA_RF, права
```
Образ `arm-core` содержит только бэкенд: фронт (`web/`) в `Dockerfile` не собирается. `GET /` отвечает **307** с
редиректом на `//`, а тот — 404 (`curl -L` показывает 404 на `http://127.0.0.1:8000//`; редирект на `//` — особенность
`arm-core`, вынесена в его follow-up). UI — `npm run dev` из `../../arm-core/web` (см. README `arm-core`).
**Ожидание готовности и коды возврата `--wait`** (compose 2.34, проверено 2026-10-04):
| Команда | Код | Комментарий |
|---|---|---|
| `docker compose --profile services up -d --wait arm-core tabular-parser scan-gateway scan-emulator` | 0 | рекомендуемая форма |
| `docker compose --profile services up -d --wait scan-gateway scan-emulator` | 0 | и на чистых томах, и на работающем окружении: `minio-init` поднимается как зависимость `scan-gateway` (`service_completed_successfully`) и его выход с кодом 0 сбоем не считается |
| `docker compose --profile services up -d --wait` (без списка) | 0 | все одноразовые контейнеры профиля — зависимости с `service_completed_successfully` |
| `docker compose up -d --wait` (без профиля и списка) | 1 | `container arm-ka-minio-init-1 exited (0)`: без профиля у `minio-init` нет зависимых сервисов |
Для скриптов указывайте долгоживущие сервисы явно — форма не зависит от состава профиля.
**Пересборка после изменения кода сервиса:** `docker compose --profile services up -d --build` — пересобирает образы
и пересоздаёт все контейнеры, чей образ сменился. Образ `arm-core:dev` собирает только сервис `arm-core`, образ
`scan-gateway:dev` — только `scan-gateway`; `arm-core-migrate` и `scan-emulator` используют готовый тег
(`pull_policy: never`): две сборки одного тега давали разные ID образа, и каждый следующий `up` пересоздавал контейнеры.
Поэтому `arm-core-migrate` или `scan-emulator` в одиночку (`up -d scan-emulator`) на машине без образа не стартуют —
сначала соберите его (`docker compose --profile services build scan-gateway`) или поднимайте профиль целиком.
Форма `up -d --build <сервис>` пересоздаёт только сервис и его зависимости (проверено 2026-10-04 на образе,
пересобранном с `--no-cache`):
| Команда | Что пересоздано | Что осталось на старом образе |
|---|---|---|
| `up -d --build arm-core` | `arm-core` и `arm-core-migrate` (он — зависимость `arm-core`, миграции нового кода выполняются) | — |
| `up -d --build scan-gateway` | только `scan-gateway` | **`scan-emulator`** (не зависимость шлюза) — до следующего `docker compose --profile services up -d`, который его пересоздаёт |
После правок кода `scan-gateway`, затрагивающих эмулятор, пересобирайте профиль целиком.
**Остановка и сброс профиля** — только с `--profile services`: без него compose не видит контейнеры профиля
(см. «Сброс окружения»).
## Проверка окружения (сквозной сценарий)
Ручная проверка обмена `recognition_job` через NATS на поднятом профиле `services`: ядро публикует задание, сервис
отвечает ack и результатом, ядро меняет статус. UI и API загрузки файлов в Срезе 0 нет, поэтому файл кладётся в MinIO
через `mc`, а строки `legal_entity` / `s3_object` / `stored_file` / `recognition_job` вставляются через `psql`. Проверено 2026-10-04
(все три сценария ниже — фактический вывод, время UTC).
Помощники (bash/zsh, из `docs/dev/`; `uuidgen` есть в macOS и util-linux):
```bash
dpsql() { docker compose exec -T postgres sh -c 'psql -X -q -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" "$@"' sh "$@"; }
dmc() { docker compose run --rm -v "$PWD/../../fixtures:/fixtures:ro" --entrypoint sh minio-init \
-c 'mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && mc "$@"' sh "$@"; }
new_job() { # new_job <tabular|image> <file_type> <файл в fixtures> — кладёт файл в uploads/ и ставит задание queued
F=$(uuidgen | tr A-Z a-z); JOB=$(uuidgen | tr A-Z a-z); O=$(uuidgen | tr A-Z a-z); N=$(basename "$3")
dmc cp -q "/fixtures/$3" "local/arm-ka/uploads/$F/$JOB/$N" >/dev/null || return 1
dpsql <<SQL
insert into dkb.legal_entity (id, inn, name, valid_from) values (gen_random_uuid(), '7700000001', 'Проверка окружения', now())
on conflict (inn) do nothing;
insert into dkb.s3_object (id, bucket_role, bucket, key, created_at) values ('$O', 'uploads', 'arm-ka', 'uploads/$F/$JOB/$N', now());
insert into dkb.stored_file (id, legal_entity_id, file_type, original_name, s3_object_id, status, uploaded_at)
values ('$F', (select id from dkb.legal_entity where inn = '7700000001'), '$2', '$N', '$O', 'UPLOADED', now());
insert into dkb.recognition_job (id, file_id, pipeline, request_subject, schema_version, status, requested_at)
values ('$JOB', '$F', '$1', 'recognition.$1.request', '1.0', 'queued', now());
SQL
echo "JOB=$JOB"
}
poll() { # poll <секунд> — статус задания $JOB раз в секунду, до финального статуса
for _ in $(seq "$1"); do
s=$(dpsql -At -c "select status || ' ' || coalesce(error_code, '') || ' ' || coalesce(metrics->>'republish_count', '') from dkb.recognition_job where id = '$JOB'")
[ "$s" != "$p" ] && echo "$(date -u +%T) $s" && p=$s
case $s in error*|done*|needs_check*|timeout*|dlq*) p=; return;; esac
sleep 1
done; p=
}
```
Рецепт прогнан дословно на чистой БД (вставка `legal_entity` без конфликта по `inn`) и повторно при уже существующей
строке (`on conflict (inn) do nothing`); оба пути проверены.
Ключ исходника — `uploads/{file_id}/{job_id}/{имя}` (`../context/03-architecture.md`, «Схема ключей S3»). Сообщения на
шине можно смотреть параллельно: `docker run --rm --network arm-ka_default natsio/nats-box:0.17.0 nats sub -s
nats://nats:4222 'recognition.>'` (печатает `Nats-Msg-Id` и тело каждого запроса, ack и результата).
**1. `tabular` → `NOT_IMPLEMENTED`.** Заглушка парсера Среза 0 отвечает ошибкой на любой файл:
```bash
new_job tabular XLSX "osv/schastl/2025-Q3/Оборотносальдовая ведомость по счету 60 за 3 квартал 2025 г..xlsx" && poll 30
# JOB=e585f47b-1b38-44a6-84c5-9432c7e6d194
# 02:12:15 queued
# 02:12:20 error NOT_IMPLEMENTED
dpsql -c "select status, error_code, error_message, published_at::time(3), started_at::time(3), finished_at::time(3) from dkb.recognition_job where id = '$JOB'"
# error | NOT_IMPLEMENTED | разбор XLS/XLSX/XLSB реализуется в Срезе 2 | 02:12:18.98 | 02:12:19.021 | 02:12:19.024
```
`published → processing → error` проходит за десятки миллисекунд (в прогонах 14–51 мс), опросом его не поймать; последовательность видна по
`published_at < started_at < finished_at` (`started_at` пишет ack), в логе `arm-core` («Запрос распознавания
опубликован» → «Сервис подтвердил получение запроса (ack): задание в processing» → «Результат распознавания принят …
error_code=NOT_IMPLEMENTED») и на шине: `recognition.tabular.request` (`Nats-Msg-Id: <job_id>:1`) →
`recognition.result` `ack:<job_id>:1` (`status: processing`) → `result:<job_id>:1` (`status: error`). Публикатор
опрашивает outbox раз в 5 с, поэтому от вставки до публикации проходит до 5 с.
**2. `image` → эмулятор ИС → `INVALID_DOCUMENT`, сырой ответ в `raw/`:**
```bash
new_job image PDF ocr/aep_5.PDF && poll 30
# JOB=46b0c90b-5a1b-4ac0-9e8a-0cdad80d857e
# 02:12:27 queued
# 02:12:29 processing
# 02:12:34 error INVALID_DOCUMENT
dmc ls local/arm-ka/raw/
# [2026-10-04 11:23:58 UTC] 600B STANDARD 0ea16933-4cc1-4b50-bda9-a97089a918c5.json
dmc cat "local/arm-ka/raw/$JOB.json" # {"job_id": "<job_id>", "files": [{"name": "<job_id>.json", "response": {"file_uid": "<job_id>", "error": {"code": "INVALID_DOCUMENT", "message": "Не удалось определить тип документа."}, "data_ocr": {…}}, "parse_error": null}]}
```
Код `INVALID_DOCUMENT` здесь — **настоящий ответ ИС из фикстуры**: `aep_5.json` и `aep_8_form4.json` — ответы-ошибки
ИС Сканирование («Не удалось определить тип документа.», пустой `table_structure`); у `aep_3` и `aep_6` есть успешные
ответы (сценарий 2а). С Среза 3a шлюз кладёт файл во входящую папку как `<uuid>.<ext>` (расширение — из `original_name` в
нижнем регистре), а эмулятор находит пару **по sha256 содержимого**: имя и регистр расширения файла значения не имеют.
В логе `scan-emulator`: `Эмулятор: <job_id>.pdf -> /data/hotfolder/Output/<job_id> (пара: hash)`; для файла без пары ответ —
`INVALID_DOCUMENT` с текстом `<первые 12 символов sha256>: нет пары по содержимому в фикстурах` (в профиле `services`
`SCAN_EMULATOR_DEFAULT` пуст). Шлюз склеивает все `*.json` каталога ответа, нормализует их (Срез 3a) и публикует результат;
ответ-ошибка по единственному файлу набора даёт `error` с кодом ИС, ошибочные файлы набора — в `metrics.warnings`.
Текст сценария обновлён 07.10.2026 по спеке Среза 3a и на живом стенде не перепрогонялся (прогон 04.10.2026 был на Срезе 0).
`processing` виден ~10 с: задержка эмулятора 2 с, затем до `SCAN_POLL_SECONDS + SCAN_SETTLE_SECONDS` (5 + 2 с) после
первого опроса, увидевшего набор.
**2а. `image` → успешный ответ ИС → `needs_check` с документами (Срез 3a):**
```bash
new_job image PDF ocr/aep_3.pdf && poll 60
dpsql -c "select status, error_code, metrics from dkb.recognition_job where id = '$JOB'"
new_job image PDF ocr/aep_6.pdf && poll 60
```
Ожидание (по спеке и README `scan-gateway`; вывод не прогонялся): `aep_3.pdf` — результат `needs_check` с двумя документами
(`BALANCE`, `INCOME`), в `metrics.warnings` — предупреждения нормализации и строка
`<job_id>_ofr.json: INVALID_DOCUMENT: Не удалось определить тип документа.` (второй файл набора — ответ-ошибка, штатно);
`aep_6.pdf` — `needs_check` с двумя документами, в предупреждениях «редакция fsbu4 не заполнена». Маппинг результата в формы в
ядре — Срез 3b; что именно ядро делает с конвертом `needs_check` до этого, на стенде не проверялось.
Подробности и все ожидания — `qa-guide.md` §6.8.
**3. Сервис недоступен → три повторные публикации → `dlq` / `NO_ACK`:**
```bash
docker compose stop tabular-parser # имя сервиса указано явно — профиль не нужен
new_job tabular XLSX "osv/schastl/2025-Q3/Оборотносальдовая ведомость по счету 60 за 3 квартал 2025 г..xlsx" && poll 180
# 02:12:43 queued
# 02:12:44 published
# 02:13:14 published 1 ← republish_count: повтор через ACK_TIMEOUT_SECONDS = 30 с, тот же Nats-Msg-Id <job_id>:1
# 02:13:45 published 2
# 02:14:14 published 3
# 02:14:44 dlq NO_ACK 3 ← ещё через 30 с после третьего повтора (OUTBOX_MAX_REPUBLISH = 3)
dpsql -c "select status, error_code, error_message, metrics, request_msg_id from dkb.recognition_job where id = '$JOB'"
# dlq | NO_ACK | Сервис распознавания не подтвердил получение запроса (ack) после 3 повторных публикаций | {"republish_count": 3} | <job_id>:1
docker compose --profile services up -d --wait tabular-parser # вернуть сервис; rc=0, Healthy
```
От первой публикации до `dlq` — ~120 с (4 × 30 с). `published_at` при каждом повторе перезаписывается (время последней
публикации), `attempt` остаётся 1, `started_at` и `service_name` пусты. Задание в `dlq` после возврата сервиса само не
перезапускается (replay — вне Среза 0).
Что сквозная проверка **не** покрывает (Срез 0): успешные результаты (`done`/`needs_check`, регистрация файла
`results/…` в `s3_object`) — заглушка парсера Среза 0 (`NOT_IMPLEMENTED`) даёт только ошибки; `scan-gateway` с Среза 3a
публикует и успешные результаты (сценарий 2а), но их приём ядром (`done`/`needs_check`) здесь не проверяется;
`stored_file.status` остаётся `UPLOADED`, `current_recognition_job_id` не заполняется (маппинг результата — `NoopMapper`, Срез 2); у ошибок
`scan-gateway` в `recognition_job.metrics` только `warnings` — по строке `<имя файла>: <код>: <сообщение>` на ошибочный
файл ответа ИС, если такие есть (`tabular-parser` присылает `duration_ms`). Тестовые строки и объекты остаются в БД и
MinIO; полная очистка — `docker compose --profile services down -v` (удаляет и общие тома `postgres-data`/`minio-data`
всех worktree: проект compose `arm-ka` общий).
## Переменные `.env`
Файл читают `docker compose` (из этого каталога) и, вторым блоком, сервисы при запуске из venv. Значения по умолчанию —
только для dev.
Блок compose:
| Переменная | По умолчанию | Для чего |
|---|---|---|
| `BIND_ADDR` | `127.0.0.1` | адрес привязки всех портов хоста; `0.0.0.0` — доступ с других машин (небезопасно, см. «Примечания») |
| `POSTGRES_DB` | `armka` | имя БД |
| `POSTGRES_USER` | `armka` | пользователь БД |
| `POSTGRES_PASSWORD` | `armka` | пароль БД |
| `POSTGRES_PORT` | `5432` | порт PostgreSQL на хосте |
| `NATS_PORT` | `4222` | клиентский порт NATS на хосте |
| `NATS_MONITOR_PORT` | `8222` | порт мониторинга NATS (`/varz`, `/healthz`) |
| `MINIO_ROOT_USER` | `minioadmin` | администратор MinIO |
| `MINIO_ROOT_PASSWORD` | `minioadmin` | пароль администратора MinIO |
| `MINIO_API_PORT` | `9000` | порт S3 API на хосте |
| `MINIO_CONSOLE_PORT` | `9001` | порт консоли MinIO на хосте |
| `MINIO_BUCKET` | `arm-ka` | единственный бакет, создаётся `minio-init` |
| `ARM_CORE_PORT` | `8000` | порт хоста `arm-core` (профиль `services`; в контейнере 8000) |
| `TABULAR_PARSER_PORT` | `8001` | порт хоста `tabular-parser` (профиль `services`; в контейнере 8000) |
| `SCAN_GATEWAY_PORT` | `8002` | порт хоста `scan-gateway` (профиль `services`; в контейнере 8000) |
Блок сервисов (запуск из venv против compose по `localhost`, формат шаблона команды):
| Переменная | По умолчанию | Для чего |
|---|---|---|
| `DEBUG` | `true` | dev-режим: пакеты `raisa_*` импортируются условно |
| `OPENBAO__DATABASE__DATABASE_URL` | `postgresql+asyncpg://armka:armka@localhost:5432/armka` | подключение к PostgreSQL (asyncpg) |
| `OPENBAO__MINIO__MINIO_URL` | `localhost:9000` | адрес MinIO без схемы (`host:port`, так ждёт `miniopy-async`) |
| `OPENBAO__MINIO__MINIO_ADMIN_LOGIN` | `minioadmin` | логин MinIO |
| `OPENBAO__MINIO__MINIO_ADMIN_PASSWORD` | `minioadmin` | пароль MinIO |
| `OPENBAO__MINIO__MINIO_BUCKET` | `arm-ka` | бакет |
| `OPENBAO__NATS__NATS_URL` | `nats://localhost:4222` | адрес NATS |
| `OPENBAO__NATS__NATS_SEED` | пусто | nkey seed; пусто — подключение без nkeys |
| `OPENBAO__NATS__NATS_JWT` | пусто | JWT NATS; пусто — подключение без nkeys |
| `OPENBAO__SMB__SMB_HOST` | `localhost` | заглушка SMB (в Срезе 0 не используется); блок необязателен для `arm-core` и для `scan-gateway` в режиме `SCAN_HOTFOLDER_MODE=local` (по умолчанию); `tabular-parser` его не знает и отвергнет (см. ниже) |
| `OPENBAO__SMB__SMB_PATH` | `share` | заглушка SMB |
| `OPENBAO__SMB__SMB_LOGIN` | `user` | заглушка SMB |
| `OPENBAO__SMB__SMB_PASSWORD` | `password` | заглушка SMB |
| `NATS_ENABLED` | `true` | включает подключение arm-core к NATS; без него `/healthcheck` показывает `nats=off` |
Два блока описывают одни и те же ресурсы: **при смене порта, пароля, пользователя или бакета меняйте обе части.**
## Сброс окружения
```bash
docker compose --profile services down # останавливает все контейнеры проекта; тома сохраняются
docker compose --profile services down -v # полный сброс: тома postgres-data, minio-data и hotfolder (БД, бакет, горячая папка — с нуля)
```
Без `--profile services` команда `down` (с `-v` или без) **не трогает** контейнеры профиля: проверено — после
`docker compose down -v` контейнеры `arm-core`/`tabular-parser`/`scan-gateway`/`scan-emulator` продолжают работать,
том `hotfolder` и сеть `arm-ka_default` остаются (`Network arm-ka_default Resource is still in use`). Если профиль не
поднимался, `docker compose down [-v]` достаточно.
Имя проекта compose фиксировано (`name: arm-ka`), поэтому тома общие для любого worktree (`.wt/<срез>/docs/dev`):
сброс из одного worktree затрагивает все.
## Запуск сервисов из venv без Nexus (черновик)
> Черновик: запуск сервисов из venv в Срезе 0 не проверялся (сквозная проверка шла на профиле `services`); шаги ниже
> не прогонялись целиком. Сценарий spec `dev-environment` остаётся неподтверждённым — вопрос
> [OQ-84](../context/07-open-questions.md), решение design #43. `make venv` и `make test-integration` есть во всех трёх
> сервисах.
Пакеты `raisa_*` из Nexus банка вне банка недоступны (D-10). Поэтому `make venv` ставит `requirements.txt` без строк
`raisa_*` (фильтр `grep -v '^raisa_'`), код импортирует `raisa_*` условно (при `DEBUG=true`), а npm-пакеты `raisa-*`
в `web/package.json` вынесены в `optionalDependencies`.
Шаги для сервиса (пути — относительно каталога его репозитория; в workspace `docs/` лежит рядом, `../docs/dev/`):
```bash
# 1. поднять инфраструктуру (из docs/dev/, см. выше)
# 2. из корня репозитория сервиса:
cp ../docs/dev/.env.example api/.env
make venv # api/.venv: Python 3.11 + зависимости без raisa_*
cd api && .venv/bin/python run_server.py # сервис подключается к PostgreSQL/NATS/MinIO по localhost
# 3. в другом терминале:
curl -s localhost:<порт сервиса>/healthcheck
```
Из venv каждый сервис слушает порт из своих настроек (`SERVER__PORT`, по умолчанию **8000** у всех трёх) — порты
хоста 8001/8002 относятся только к контейнерам профиля `services` (см. таблицу портов выше). Два сервиса из venv
одновременно или сервис из venv рядом с профилем `services` требуют разных `SERVER__PORT`. Для `arm-core` в ответе
`/healthcheck` ожидаются `version` и `nats = ok` — при `NATS_ENABLED=true` (задан в `.env.example`; без него
`nats = off`). Поля `/healthcheck` в контейнерах: `tabular-parser` — `uptime`, `version`, `nats`; `scan-gateway` — ещё
`hotfolder` (шлюзу нужен `SCAN_HOTFOLDER_PATH`). Набор переменных сервисов для запуска из venv не проверялся (см. черновик выше и OQ-84). По чтению конфигов: `scan-gateway` и `tabular-parser` отвергают неизвестные ключи `OPENBAO__*`, а поле DATABASE есть только у `arm-core`. Поэтому в `.env` `scan-gateway` нужно удалить строки `OPENBAO__DATABASE__*` (SMB в режиме `local` допустим), а в `.env` `tabular-parser` — `OPENBAO__DATABASE__*` и `OPENBAO__SMB__*`; для MinIO по `http` нужен `OPENBAO__MINIO__MINIO_SECURE=false` (в `.env.example` его нет, по умолчанию `true`).
## Интеграционные тесты сервисов против compose (черновик)
> Черновик: состав целей и порядок проверок описаны в [`checks.md`](checks.md); открытые вопросы — OQ-83 (общий NATS
> у контейнеров и тестов) и OQ-84.
Общий набор целей (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`) есть во всех сервисных репозиториях
(`arm-core`, `tabular-parser`, `scan-gateway`); семантика, состав `check-full`, порядок перед PR и пропуск тестов без
`docs`/`fixtures` описаны в [`checks.md`](checks.md); эталон рецептов общих целей — [`Makefile.common`](Makefile.common). Кратко: `make check` — быстрые проверки без Docker, `make check-full` —
`check` + интеграционные тесты против этого compose + образ (для `arm-core` ещё сверка с DBML, миграции и `web`);
выполняется перед созданием PR.
Интеграционные тесты **требуют поднятой инфраструктуры**: сначала `docker compose up -d && docker compose up -d --wait
postgres nats minio`, затем `make test-integration` (или `make check-full`) в репозитории сервиса. Для `scan-gateway`
эмулятор ИС Сканирование поднимается в процессе интеграционного теста; контейнер `scan-emulator` профиля `services`
тестами не используется. Общее описание проверок — [`checks.md`](checks.md).
> **Не запускайте интеграционные тесты `tabular-parser` и `scan-gateway` при поднятом профиле `services`.** Их
> сквозные тесты (`api/tests/integration/test_roundtrip.py`, у `scan-gateway` ещё `test_nats_minio_integration.py`)
> публикуют запросы в боевые `recognition.tabular.request` / `recognition.image.request`, а экземпляр сервиса из теста
> подписан в ту же queue group (`tabular-parser` / `scan-gateway`), что и контейнер. NATS отдаёт каждое сообщение группы
> одному подписчику — запрос теста может уйти контейнеру, и тест упадёт по таймауту ожидания ack (так прямо сказано в
> docstring теста `scan-gateway`). Риск подтверждён чтением кода тестов, тесты при поднятом профиле не запускались.
> Интеграционные тесты `arm-core` от контейнеров изолированы: временная БД на сессию (рабочая `armka` не используется)
> и уникальный subject результатов на тест. Перед тестами сервисов:
> `docker compose --profile services stop tabular-parser scan-gateway scan-emulator`; проверить, что чужих подписчиков
> нет: `curl -s 'http://127.0.0.1:8222/subsz?subs=1' | grep -o '"subject": *"recognition[^"]*"'` — строк
> `recognition.tabular.request` / `recognition.image.request` быть не должно (`recognition.result` — подписка контейнера
> `arm-core`, тестам `arm-core` не мешает).
## Проверка контракта
Из корня репозитория `docs`: `python scripts/validate_contracts.py` — валидирует примеры в
[`contracts/examples/`](../contracts/examples/) тремя схемами из [`contracts/`](../contracts/) (3 схемы, 8 примеров;
нужен только пакет `jsonschema`: формат `date-time` скрипт проверяет собственной функцией, поэтому `rfc3339-validator`
не требуется). Или `make venv && make check` (`make venv` создаёт `.venv` и ставит `jsonschema`; `check` использует этот
`.venv` и ещё запускает `openspec validate`), см. [`checks.md`](checks.md).
## Примечания
- **Образы MinIO неофициальные.** Официальные `minio/minio` и `minio/mc` недоступны (Docker Hub — denied, на quay.io нет
нужных тегов), поэтому в compose стоят `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и
`bitnamilegacy/minio-client:2025.7.21`, а сервис `minio` запущен с `user: "0:0"` (иначе образ не пишет в свежий том) —
только для dev. Решения: [`design.md`](../openspec/changes/archive/2026-10-04-slice-0-skeleton/design.md), раздел
«Decisions made without discussion», решения №16–17. Замена на официальные образы — две строки `image:` в
`docker-compose.yml` (и удаление `user: "0:0"`).
- **Безопасность.** По умолчанию порты публикуются только на `127.0.0.1` (`BIND_ADDR`), снаружи машины они недоступны.
Чтобы открыть их для других машин, задайте в `.env` `BIND_ADDR=0.0.0.0` — но пароли по умолчанию (`armka`,
`minioadmin`) годятся только для dev, поэтому сначала смените их (в обоих блоках `.env`) и не делайте этого в
недоверенной сети.
## Типичные проблемы
- **Порт занят** (`address already in use`): измените `*_PORT` в `.env` и одновременно соответствующий порт в
`OPENBAO__*` (`DATABASE_URL`, `MINIO_URL`, `NATS_URL`); перезапустите `docker compose up -d`. Для портов сервисов
(`ARM_CORE_PORT`, `TABULAR_PARSER_PORT`, `SCAN_GATEWAY_PORT`) достаточно `.env`.
- **Сервис профиля не становится healthy:** `docker compose --profile services logs <сервис>`. `nats = down` —
NATS недоступен; `hotfolder = down` у `scan-gateway` — каталог `SCAN_HOTFOLDER_PATH` (том `hotfolder`) недоступен. `arm-core` не стартует, пока
`arm-core-migrate` не завершился с кодом 0: `docker compose --profile services logs arm-core-migrate`.
- **`No such image: arm-core:dev` / `scan-gateway:dev`** при запуске `arm-core-migrate` или `scan-emulator` по
отдельности: образ ещё не собран — `docker compose --profile services build` (см. «Профиль `services`»).
- **`minio-init` завис или завершился с ошибкой:** `docker compose logs minio-init`; чаще всего `minio` не стал healthy —
`docker compose logs minio`. Повторный `docker compose up -d` идемпотентен.
- **Остаточные тома от прошлых запусков** (старый пароль БД, бакет не тот): `docker compose --profile services down -v`
и запуск заново (без `--profile services` контейнеры профиля и том `hotfolder` остаются, см. «Сброс окружения»).
- **`up -d --wait` возвращает код 1:** см. раздел «Запуск инфраструктуры» — указывайте сервисы явно.