arm-scan-gateway/openspec/changes/slice-0-skeleton/open-questions-proposal.md
Andrey Epifancev 58bcae2a57 Срез 0 [6.3]: прогон DoD, предложения для docs и правки .env.example
- open-questions-proposal.md: compose-фрагмент scan-emulator/scan-gateway, таблица предложений по контракту и OQ,
  чеклист для человека (файлы в ../docs не менялись)
- .env.example: SCAN_EMULATOR_POLL_SECONDS; уточнён комментарий PUBLISH_RETRIES (повторы после первой попытки)
- отмечена задача 16 плана и пункт 6.3 tasks.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 04:44:33 +04:00

15 KiB
Raw Blame History

Предложения по итогам Среза 0 scan-gateway: compose, контракт, открытые вопросы

Передача для владельца docs и человека. Файлы в ../docs этим change'ом не менялись; всё ниже — предложения. Источник: прогон DoD (задача 6.3), progress.md контроллера, ревью задач 1-15. Маркеры (T12.5, O-1, R-13) — из ledger.

Фрагмент compose для scan-emulator и scan-gateway

Вставить в ../docs/dev/docker-compose.yml в раздел services: (после minio-init) и добавить в раздел volumes: том hotfolder:. Профиль services: docker compose --profile services up -d --wait scan-gateway scan-emulator (в up -d --wait указывать сервисы явно, как для инфраструктуры). Образ собирается из корня репозитория scan-gateway (Dockerfile), эмулятор использует тот же образ (команда python -m src.scan_emulator). Эмулятору переменные MinIO/NATS не нужны (R-11). Контекст ../../scan-gateway верен и для workspace, и для worktree .wt/<срез>/docs/dev.

  # Эмулятор ИС Сканирование: тот же образ, что у шлюза; общий том hotfolder с шлюзом. Переменные MinIO/NATS не нужны.
  scan-emulator:
    profiles: ["services"]
    build:
      context: ../../scan-gateway
    image: scan-gateway:dev
    command: ["python", "-m", "src.scan_emulator"]
    restart: unless-stopped
    environment:
      SCAN_HOTFOLDER_PATH: /data/hotfolder
      SCAN_FIXTURES_DIR: /fixtures/ocr
      SCAN_EMULATOR_DELAY: "2"
    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:
      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"
      SCAN_HOTFOLDER_MODE: local
      SCAN_HOTFOLDER_PATH: /data/hotfolder
    volumes:
      - hotfolder:/data/hotfolder
    ports:
      - "${BIND_ADDR:-127.0.0.1}:${SCAN_GATEWAY_PORT:-8002}:8000"
    healthcheck:
      # curl в python:3.11-slim нет; /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:
  hotfolder:

Проверено: YAML разбирается (yaml.safe_load); docker compose -f <копия> --project-directory docs/dev --profile services config -q на временной копии файла с этими сервисами — код 0 (реальный проект arm-ka не затрагивался). Образ scan-gateway:dev запущен отдельным контейнером в сети arm-ka_default с теми же переменными окружения: /healthcheck -> nats: ok, hotfolder: ok, команда healthcheck из фрагмента возвращает 0 (контейнер удалён).

Что искать в логах: у эмулятора при старте — Эмулятор ИС Сканирование: корень /data/hotfolder, входящий каталог /data/hotfolder/Input, исходящий каталог /data/hotfolder/Output, задержка 2.0 с, опрос 1.0 с, затем Фикстуры: /fixtures/ocr, фикстур: 2 (при 0/«не найден» эмулятор отвечает INVALID_DOCUMENT на всё). У шлюза — Подписка NATS активна. subject=recognition.image.request, queue=scan-gateway и Воркер scan-gateway запущен. Имена переменных сверены с api/src/core/config.py и api/src/scan_emulator/__init__.py.

Примечания: (а) порт хоста 8002 — по spec docs, у сервиса порт по умолчанию 8000 (SERVER__PORT), поэтому маппинг 8002:8000; (б) /healthcheck всегда отвечает 200 (пункт 9 таблицы), поэтому healthcheck читает поля nats/hotfolder; (в) контейнеры работают от root, права на том hotfolder не настраиваются; (г) build задан у обоих сервисов с одним image: — compose соберёт образ один раз за счёт кэша.

Предложения по контракту, OQ и документам

# Что Откуда Предложение
1 PUBLISH_RETRIES = число повторов после первой попытки (1+3 = 4 попытки) R-13 (design #37), T11.1; api/src/core/config.py Согласовать с tabular-parser (там может быть «3 попытки всего»); зафиксировать в 03-architecture.md одной фразой
2 03-architecture.md («Схема schema_version»): «неизвестный MAJOR -> SCHEMA_MISMATCH и ack»; спека change'а и воркер — без ack R-16 (design #41), T12.13 Привести документ в соответствие (ack не публикуется, конверт error — если из тела извлекаются job_id и file_id) либо пересмотреть правило и воркер: ack на отвергнутый запрос защищает ядро от зацикливания повторов доставки, цена — лишнее сообщение и расхождение с R-16
3 Реальные фикстуры fixtures/ocr/aep_5.json, aep_8_form4.json — ответы-ошибки ИС (error.code = INVALID_DOCUMENT, поля data_ocr пусты) O-1, R-10 (design #32) Для Среза 3 (OQ-13/14) нужны образцы успешных ответов Content AI; пока успех проверяется синтетическим ответом, NullNormalizer на реальных фикстурах пропускает ошибку ИС
4 §11 05-domain-rules.md описывает data_ocr[] (список), а в фикстурах data_ocr — объект с label/organization/inn/units/date/table_structure O-1, T3.2 Выяснить фактический формат на стенде ИС и поправить §11 или фикстуры; решение нужно до нормализатора Среза 3
5 Шлюз читает ровно <output>/<job_id>/<job_id>.json (спека); старый код читал все файлы каталога Output/<id>/ T8.5 Проверить на стенде ИС в Срезе 3: имя ответа, наличие дополнительных файлов, момент готовности
6 Временный файл .<имя>.part лежит в Input (атомарная запись .part + rename, R-5) — реальная ИС без маски может забрать недописанный файл R-5, T7/T8 Вариант: служебный подкаталог рядом (Input/.tmp или корень) + rename в Input; либо уточнить у владельцев ИС маску/поведение
7 Сценарий «NATS перезапущен -> подписка восстановлена за <= 20 с» не автоматизирован: общий NATS нельзя перезапускать в проверках T11.7 Ручная проверка на отдельном nats-server (поднять, подключить шлюз, убить, поднять снова) либо отдельный compose-профиль для тестов
8 Штатная остановка не публикует конверты для принятых (ack) заданий; D-2 допускает потерю при падении, при rolling deploy задание теряется до reply_deadline T12.5, D-2 Публиковать UNKNOWN_ERROR «сервис остановлен» в stop() для всех принятых заданий; Срез 3+
9 /healthcheck всегда 200, даже если NATS постоянно недоступен (nats: down) T12.10 Платформе (readiness/liveness) читать поле nats/hotfolder, а не код ответа; либо отдельные /ready и /live
10 ../docs/dev/README.md устарел относительно шлюза: «у scan-gateway Makefile пока нет», nats = off (шлюз показывает disabled), порт 8002 против 8000, шлюз требует SCAN_HOTFOLDER_* и не использует OPENBAO__DATABASE__* Task 3, O-a Обновить раздел «Запуск сервисов из venv» и «Интеграционные тесты»; добавить профиль services (фрагмент выше); make up/make check-full/make emulator в scan-gateway есть
11 Интеграционные тесты шлюза и arm-core делят общий NATS: in-process шлюз подписывается на queue group scan-gateway на реальном subject и может забрать запросы тестов arm-core, идущих параллельно (и наоборот) T14.1, T11.8 Сделать queue group и/или subject-префикс настраиваемыми для тестов (например NATS_TEST_PREFIX), либо отдельный NATS на тесты; пока — не гонять make test-integration двух сервисов одновременно
12 Пример contracts/examples/request-image.json имеет reply_deadline в прошлом (2026-10-03T12:45:00Z) Task 3, O-e Для ручного прогона подставлять будущий срок (иначе сразу TIMEOUT); в README контракта отметить, либо сделать пример с датой далеко в будущем
13 DEBUG в настройках шлюза не используется (оставлен шаблоном; raisa_* не импортируются) Task 3 Принято; убрать вместе с остатками шаблона или оставить для единообразия с arm-core
14 ResourceWarning aiohttp при загрузке > 1 МиБ (miniopy-async) скрыт фильтром в одном модуле тестов; при ручном прогоне со скриптом тоже виден Unclosed client session T14.5 Если сырые ответы ИС будут > 1 МиБ — пересмотреть фильтр/закрывать сессию клиента явно
15 Зависший poll/put по SMB (до 60 с) не прерывается дедлайном; нет connection_timeout у smbclient T12.2, T8.4, design #46 Задать connection_timeout при регистрации сессии и ограничить вызовы wait_for остатком до дедлайна; проверить на стенде
16 Расхождения и наблюдения контроллера при исполнении (Observation в progress.md) progress.md См. ниже

Строки Observation из progress.md (только пересказ, статусы — в ledger):

  • O-1 — обе реальные фикстуры (aep_5.json, aep_8_form4.json) сами являются ответами-ошибками ИС (INVALID_DOCUMENT, data_ocr — объект, не список); пути NOT_IMPLEMENTED для реальных фикстур недостижимы; успех тестируется синтетическим ответом (R-10). Пункты 3 и 4 таблицы.
  • O-2 — исполнитель T11 вопреки инструкции поставил трейлер харнесса; история локальных коммитов переписана (filter-branch), в remote ничего не уходило. Процессное, к docs не относится.
  • O-a..O-e (по итогам Task 3) — docs/dev/README.md устарел (пункт 10); порт 8002 против 8000 (пункт 10 и примечание к compose); design.md D-9 описывал compose-интеграцию эмулятора — исправлено до R-7 (e228635); check-full без docker-сборки — закрыто задачей 15; DEBUG не используется (пункт 13); reply_deadline в примере в прошлом (пункт 12).

Дополнительно по итогам прогона DoD (реальные процессы шлюза и эмулятора, SIGTERM):

  • шлюз при SIGTERM корректно завершает lifespan (Приложение закрывается, Соединение с NATS закрыто), но код выхода процесса — 143 (uvicorn повторно поднимает полученный сигнал); для оркестраторов это штатно, скрипты, ожидающие 0, должны учитывать;
  • кэш hotfolder в /healthcheck — 5 с: после возврата каталога поле остаётся down до 5 с.

Что осталось человеку

  • Просмотреть и влить PR ветки feat/1-slice-0-skeleton (merge — только по явной команде); вывод make check-full — в описании PR (.superpowers/sdd/2026-10-03-slice-0-skeleton/make-check-full.log, локальный).
  • После merge: openspec archive slice-0-skeleton, коммит и push архива в main (чеклист закрытия).
  • Перенести compose-фрагмент выше в ../docs/dev/docker-compose.yml (и обновить README.md, пункт 10) — отдельным PR репозитория docs.
  • Согласовать с владельцем tabular-parser семантику PUBLISH_RETRIES (пункт 1) и решить судьбу правила про SCHEMA_MISMATCH + ack (пункт 2).
  • Занести пункты 3-5 (образцы успешных ответов Content AI, формат data_ocr, имя файла ответа) в docs/context/07-open-questions.md; пункты 6-8, 15 — в follow-up issue (Follow-ups from #1).