- 07-open-questions: OQ-149 переписан (пустой код у «БАЛАНС»/«Выручка», предупреждения №29, `|` экранирован), новый OQ-152 (повтор кода 2410 в aep_6), уточнены OQ-137 и OQ-144 (Input) - 05-domain-rules: §11 locator `file:<имя json>;table:<i>`; §11.1 п. 2 (длина даты №31), п. 4 (предупреждения о пропущенных строках), п. 7 (все точки удаляются, Money и confidence №30), п. 8 (ИНН неуверенно), п. 9 (№28), п. 10 (редакция не определена) - 03-architecture: формулировка про `.part`; 06-test-data, dev/README (сценарии 2/2а), repo-templates/scan-gateway.CLAUDE.md — раскладка `<job_id>.<ext>` и пары по sha256 - dev/compose, qa-guide §6.8/§8 (ожидаемые предупреждения по эталонам, OQ-151), 09-delivery-plan (статус 3a) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
247 lines
12 KiB
YAML
247 lines
12 KiB
YAML
# Инфраструктура разработки АРМ КА: PostgreSQL 16, NATS 2.10 (без JetStream, как на платформе), MinIO + бакет.
|
||
# Запуск: cp .env.example .env && docker compose up -d (из этого каталога)
|
||
# Ожидание готовности: docker compose up -d --wait postgres nats minio. Голый `up -d --wait` возвращает код 1, потому что
|
||
# одноразовый minio-init завершается (хотя и с кодом 0) — для скриптов указывайте сервисы явно.
|
||
# Все учётные данные, порты хоста и имя бакета берутся из .env (значения по умолчанию — dev).
|
||
# Порты публикуются только на 127.0.0.1; адрес привязки меняется через BIND_ADDR (см. .env.example).
|
||
# Профиль `services` — сервисы системы, собранные из соседних репозиториев workspace (../../<repo>, Dockerfile в корне):
|
||
# docker compose --profile services up -d --build
|
||
# docker compose --profile services up -d --wait arm-core tabular-parser scan-gateway scan-emulator
|
||
# Одноразовые minio-init и arm-core-migrate (миграции + seed) в список --wait не входят: они завершаются с кодом 0.
|
||
# Внутри контейнера каждый сервис слушает 8000; на хост публикуются 8000 (arm-core), 8001 (tabular-parser), 8002 (scan-gateway).
|
||
|
||
# Фиксированное имя проекта: одинаковые тома и сети из любого worktree (.wt/<срез>/docs/dev).
|
||
name: arm-ka
|
||
|
||
# Общие переменные сервисов профиля `services` (формат шаблона команды; адреса — внутри сети compose, не localhost).
|
||
x-service-env: &service-env
|
||
DEBUG: "true"
|
||
OPENBAO__MINIO__MINIO_URL: minio:9000
|
||
OPENBAO__MINIO__MINIO_ADMIN_LOGIN: ${MINIO_ROOT_USER:-minioadmin}
|
||
OPENBAO__MINIO__MINIO_ADMIN_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
|
||
OPENBAO__MINIO__MINIO_BUCKET: ${MINIO_BUCKET:-arm-ka}
|
||
OPENBAO__MINIO__MINIO_SECURE: "false"
|
||
OPENBAO__NATS__NATS_URL: nats://nats:4222
|
||
NATS_ENABLED: "true"
|
||
|
||
# arm-core и его одноразовые миграции: те же переменные плюс БД и dev-эмитент токенов.
|
||
x-arm-core-env: &arm-core-env
|
||
<<: *service-env
|
||
OPENBAO__DATABASE__DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-armka}:${POSTGRES_PASSWORD:-armka}@postgres:5432/${POSTGRES_DB:-armka}
|
||
# Dev-эмитент JWT (POST /api/v1/dev/token) — только при DEBUG=true; секрет публичный, только для dev.
|
||
# Порт хоста по умолчанию на 127.0.0.1 (BIND_ADDR): при BIND_ADDR=0.0.0.0 токен может получить любой в сети.
|
||
AUTH_DEV_ISSUER: "true"
|
||
AUTH_DEV_SECRET: dev-only-secret-do-not-use-in-production-0123456789
|
||
|
||
services:
|
||
postgres:
|
||
image: postgres:16.10-alpine
|
||
environment:
|
||
POSTGRES_DB: ${POSTGRES_DB:-armka}
|
||
POSTGRES_USER: ${POSTGRES_USER:-armka}
|
||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-armka}
|
||
ports:
|
||
- "${BIND_ADDR:-127.0.0.1}:${POSTGRES_PORT:-5432}:5432"
|
||
volumes:
|
||
- postgres-data:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||
interval: 3s
|
||
timeout: 3s
|
||
retries: 20
|
||
start_period: 5s
|
||
|
||
# Core NATS без JetStream: флаг --jetstream не передаётся. Мониторинг — порт 8222 (/varz, /healthz).
|
||
nats:
|
||
image: nats:2.10.29-alpine
|
||
command: ["-m", "8222"]
|
||
ports:
|
||
- "${BIND_ADDR:-127.0.0.1}:${NATS_PORT:-4222}:4222"
|
||
- "${BIND_ADDR:-127.0.0.1}:${NATS_MONITOR_PORT:-8222}:8222"
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8222/healthz || exit 1"]
|
||
interval: 3s
|
||
timeout: 3s
|
||
retries: 20
|
||
start_period: 2s
|
||
|
||
# Образы minio/minio и minio/mc сняты с Docker Hub и quay.io; используется сборка alpine/minio с RELEASE-тегом.
|
||
minio:
|
||
image: alpine/minio:RELEASE.2025-10-15T17-29-55Z
|
||
# Образ по умолчанию работает от пользователя minio и не может писать в свежий именованный том — в dev запускаем от root.
|
||
user: "0:0"
|
||
command: ["server", "/data", "--console-address", ":9001"]
|
||
environment:
|
||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
|
||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
|
||
ports:
|
||
- "${BIND_ADDR:-127.0.0.1}:${MINIO_API_PORT:-9000}:9000"
|
||
- "${BIND_ADDR:-127.0.0.1}:${MINIO_CONSOLE_PORT:-9001}:9001"
|
||
volumes:
|
||
- minio-data:/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:9000/minio/health/live || exit 1"]
|
||
interval: 3s
|
||
timeout: 3s
|
||
retries: 20
|
||
start_period: 3s
|
||
|
||
# Одноразовое создание бакета; идемпотентно (--ignore-existing), при повторном up снова завершается с кодом 0.
|
||
minio-init:
|
||
image: bitnamilegacy/minio-client:2025.7.21
|
||
depends_on:
|
||
minio:
|
||
condition: service_healthy
|
||
restart: "no"
|
||
environment:
|
||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
|
||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
|
||
MINIO_BUCKET: ${MINIO_BUCKET:-arm-ka}
|
||
entrypoint: ["/bin/sh", "-c"]
|
||
command:
|
||
- >-
|
||
mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" &&
|
||
mc mb --ignore-existing "local/$$MINIO_BUCKET"
|
||
|
||
# --- Профиль `services` -------------------------------------------------------------------------------------------
|
||
|
||
# Одноразово: миграции arm-core (alembic upgrade head) и справочники с dev-пользователями (seed --dev).
|
||
# Оба шага идемпотентны; при каждом `up` профиля контейнер снова выполняется и завершается с кодом 0.
|
||
# Образ собирает сервис arm-core (build только у него): две сборки одного тега дают разные ID образа, и каждый
|
||
# следующий `up` пересоздавал бы arm-core-migrate и arm-core. pull_policy: never — образ локальный, не из реестра.
|
||
arm-core-migrate:
|
||
profiles: ["services"]
|
||
image: arm-core:dev
|
||
pull_policy: never
|
||
restart: "no"
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
environment: *arm-core-env
|
||
command: ["sh", "-c", "alembic upgrade head && python -m scripts.seed --dev"]
|
||
|
||
arm-core:
|
||
profiles: ["services"]
|
||
build:
|
||
context: ../../arm-core
|
||
image: arm-core:dev
|
||
restart: unless-stopped
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
nats:
|
||
condition: service_healthy
|
||
minio:
|
||
condition: service_healthy
|
||
minio-init:
|
||
condition: service_completed_successfully
|
||
arm-core-migrate:
|
||
condition: service_completed_successfully
|
||
environment: *arm-core-env
|
||
ports:
|
||
- "${BIND_ADDR:-127.0.0.1}:${ARM_CORE_PORT:-8000}:8000"
|
||
healthcheck:
|
||
# curl в python:3.11-slim нет; /healthcheck всегда отвечает 200, поэтому читаем поле nats
|
||
test:
|
||
- CMD
|
||
- python
|
||
- -c
|
||
- "import json,sys,urllib.request as u; d=json.load(u.urlopen('http://127.0.0.1:8000/healthcheck', timeout=3)); sys.exit(0 if d['nats']=='ok' else 1)"
|
||
interval: 5s
|
||
timeout: 5s
|
||
retries: 12
|
||
start_period: 10s
|
||
|
||
tabular-parser:
|
||
profiles: ["services"]
|
||
build:
|
||
context: ../../tabular-parser
|
||
image: tabular-parser:dev
|
||
restart: unless-stopped
|
||
depends_on:
|
||
nats:
|
||
condition: service_healthy
|
||
minio:
|
||
condition: service_healthy
|
||
minio-init:
|
||
condition: service_completed_successfully
|
||
environment: *service-env
|
||
ports:
|
||
- "${BIND_ADDR:-127.0.0.1}:${TABULAR_PARSER_PORT:-8001}:8000"
|
||
healthcheck:
|
||
test:
|
||
- CMD
|
||
- python
|
||
- -c
|
||
- "import json,sys,urllib.request as u; d=json.load(u.urlopen('http://127.0.0.1:8000/healthcheck', timeout=3)); sys.exit(0 if d['nats']=='ok' else 1)"
|
||
interval: 5s
|
||
timeout: 5s
|
||
retries: 12
|
||
start_period: 10s
|
||
|
||
# Эмулятор ИС Сканирование: тот же образ, что у шлюза (собирает сервис scan-gateway, см. arm-core-migrate); общий
|
||
# том hotfolder с шлюзом. Переменные MinIO/NATS не нужны.
|
||
# Фикстуры ответов ИС — ../../fixtures/ocr (только чтение). Входной файл шлюз называет <uuid>.<ext> (имя файла
|
||
# фикстуры не несёт); эмулятор считает sha256 содержимого и ищет пару PDF + набор <имя>.json / <имя>_*.json по хешу
|
||
# (aep_3.pdf, aep_6.pdf, ...). Файл без пары по содержимому — ответ-ошибка INVALID_DOCUMENT, пока SCAN_EMULATOR_DEFAULT
|
||
# пуст; чтобы такой файл получал набор по умолчанию, задайте SCAN_EMULATOR_DEFAULT=<имя набора>, например aep_6.
|
||
# PDF из fixtures/ocr без JSON-набора (aep_4/7/9/30–34) остаются INVALID_DOCUMENT и при заданном default (OQ-151).
|
||
scan-emulator:
|
||
profiles: ["services"]
|
||
image: scan-gateway:dev
|
||
pull_policy: never
|
||
command: ["python", "-m", "src.scan_emulator"]
|
||
restart: unless-stopped
|
||
environment:
|
||
SCAN_HOTFOLDER_PATH: /data/hotfolder
|
||
SCAN_FIXTURES_DIR: /fixtures/ocr
|
||
SCAN_EMULATOR_DELAY: "2"
|
||
# Набор по умолчанию для файлов без пары по содержимому. Пусто — такой файл получает INVALID_DOCUMENT.
|
||
# Файл, совпавший по хешу с PDF без JSON-набора, default не получает (OQ-151).
|
||
SCAN_EMULATOR_DEFAULT: ""
|
||
volumes:
|
||
- hotfolder:/data/hotfolder
|
||
- ../../fixtures/ocr:/fixtures/ocr:ro
|
||
|
||
scan-gateway:
|
||
profiles: ["services"]
|
||
build:
|
||
context: ../../scan-gateway
|
||
image: scan-gateway:dev
|
||
restart: unless-stopped
|
||
depends_on:
|
||
nats:
|
||
condition: service_healthy
|
||
minio:
|
||
condition: service_healthy
|
||
minio-init:
|
||
condition: service_completed_successfully
|
||
environment:
|
||
<<: *service-env
|
||
SCAN_HOTFOLDER_MODE: local
|
||
SCAN_HOTFOLDER_PATH: /data/hotfolder
|
||
# Очистка Output/<job_id>/ и входящего файла после чтения ответа (dev-том принадлежит нам). На шаре контура по
|
||
# умолчанию выключена: права на удаление не подтверждены (OQ-144).
|
||
SCAN_CLEANUP: "true"
|
||
volumes:
|
||
- hotfolder:/data/hotfolder
|
||
ports:
|
||
- "${BIND_ADDR:-127.0.0.1}:${SCAN_GATEWAY_PORT:-8002}:8000"
|
||
healthcheck:
|
||
# /healthcheck всегда отвечает 200, поэтому читаем поля nats и hotfolder
|
||
test:
|
||
- CMD
|
||
- python
|
||
- -c
|
||
- "import json,sys,urllib.request as u; d=json.load(u.urlopen('http://127.0.0.1:8000/healthcheck', timeout=3)); sys.exit(0 if d['nats']=='ok' and d['hotfolder']=='ok' else 1)"
|
||
interval: 5s
|
||
timeout: 5s
|
||
retries: 12
|
||
start_period: 10s
|
||
|
||
volumes:
|
||
postgres-data:
|
||
minio-data:
|
||
# Общая горячая папка scan-gateway и scan-emulator (Input/, Output/)
|
||
hotfolder:
|