Refs #34 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
Окружение разработки АРМ КА (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.
Разработчику фронта: как устроены вход и права arm-core, какие ручки когда дёргать, семантика 401/403/404 —
frontend-auth-guide.md; контракт API консолидации — api-contract-slice-4.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/:
cp .env.example .env # .env в git не попадает
docker compose up -d # вернётся сразу; healthy — примерно через 6 с
Ожидание готовности (для скриптов и Makefile):
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 нужно давать со
списком долгоживущих сервисов — как выше.
Проверка:
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/:
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, сокращён):
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):
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):
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 отвечает ошибкой на любой файл:
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/:
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а). Эмулятор нашёл набор ответов (в логе scan-emulator: <job_id>_aep_5.PDF -> /data/hotfolder/Output/<job_id>; при отсутствии набора сообщение было бы «…: нет эталонного ответа в фикстурах»).
Успешные ответы ИС есть (aep_3.json + aep_3_ofr.json, aep_6.json); шлюз склеивает все *.json каталога ответа и
отвечает NOT_IMPLEMENTED (нормализация — Срез 3), ошибочные файлы набора — в metrics.warnings. Имя файла — aep_5.PDF (расширение в верхнем регистре; на macOS aep_5.pdf тоже находится, на
Linux — нет).
processing виден ~10 с: задержка эмулятора 2 с, затем до SCAN_POLL_SECONDS + SCAN_SETTLE_SECONDS (5 + 2 с) после
первого опроса, увидевшего набор.
2а. image → успешный ответ ИС → NOT_IMPLEMENTED:
new_job image PDF ocr/aep_3.pdf && poll 60
# 11:22:07 queued
# 11:22:08 processing
# 11:22:18 error NOT_IMPLEMENTED
dpsql -c "select status, error_code, metrics from dkb.recognition_job where id = '$JOB'"
# error | NOT_IMPLEMENTED | {"warnings": ["<job_id>_ofr.json: INVALID_DOCUMENT: Не удалось определить тип документа."]}
new_job image PDF ocr/aep_6.pdf && poll 60
# error NOT_IMPLEMENTED, metrics пусто
3. Сервис недоступен → три повторные публикации → dlq / NO_ACK:
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) дают только ошибки,
хотя фикстуры ИС (aep_3, aep_6) успешные ответы содержат; 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 |
Два блока описывают одни и те же ресурсы: при смене порта, пароля, пользователя или бакета меняйте обе части.
Сброс окружения
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); шаги ниже не прогонялись целиком. Сценарий specdev-environmentостаётся неподтверждённым — вопрос OQ-84, решение 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/):
# 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; открытые вопросы — 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; эталон рецептов общих целей — 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.
Не запускайте интеграционные тесты
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/ (3 схемы, 8 примеров;
нужен только пакет jsonschema: формат date-time скрипт проверяет собственной функцией, поэтому rfc3339-validator
не требуется). Или make venv && make check (make venv создаёт .venv и ставит jsonschema; check использует этот
.venv и ещё запускает openspec validate), см. 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, раздел «Decisions made without discussion», решения №16–17. Замена на официальные образы — две строкиimage:вdocker-compose.yml(и удалениеuser: "0:0"). - Безопасность. По умолчанию порты публикуются только на
127.0.0.1(BIND_ADDR), снаружи машины они недоступны. Чтобы открыть их для других машин, задайте в.envBIND_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: см. раздел «Запуск инфраструктуры» — указывайте сервисы явно.