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

147 lines
15 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.

# Предложения по итогам Среза 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`.
```yaml
# Эмулятор ИС Сканирование: тот же образ, что у шлюза; общий том 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
```
И в конец файла:
```yaml
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`).