arm-scan-gateway/openspec/changes/slice-0-skeleton/open-questions-proposal.md
Andrey Epifancev b127c02422 Срез 0 [7.5]: финальная волна, документы — T5.1, T5.3, T6.1, T6.3, T6.6, M-3
- README: верхняя граница задержки settle, шаг 5 «Полного круга»; критерии ошибочного файла (корень не объект)
- CLAUDE.md: по одному json на каждый распознанный документ; критерии ошибочного файла
- design.md: D-5 — оговорка про нестроковый error.code отдельным предложением; №79 — источник указания, SQL уборки в порядке FK
- open-questions-proposal.md п.20: замены ../docs/dev/README.md (185–187, 224–226, 252–253) и комментарий docker-compose.yml:184
- план: Task 6 — пометка про замену down -v (R-27)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:01:13 +04:00

179 lines
30 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`.
**Фрагмент рабочий только после исправления C-1** (ветка `feat/1-slice-0-skeleton`, коммит `ad3fb34`): раньше
`python -m src.scan_emulator` импортировал пакет `src`, а тот создавал приложение шлюза и требовал `OPENBAO__*`, поэтому
контейнер эмулятора без переменных MinIO/NATS падал с `ValidationError`. Теперь импорт пакета `src` настроек шлюза не
требует; проверено на пересобранном образе: `docker run --rm -e SCAN_HOTFOLDER_PATH=/nonexistent scan-gateway:dev
python -m src.scan_emulator` завершается кодом 2 с сообщением `Эмулятор не запущен: корень горячей папки /nonexistent не
существует или не каталог` (не `ValidationError`); шлюз из того же образа стартует, `/healthcheck` отвечает 200.
Проверить после применения фрагмента:
- `docker compose --profile services up -d --wait scan-gateway scan-emulator` возвращает код 0: `scan-gateway`
зависит от `minio-init` (`service_completed_successfully`), а в списке `--wait` одноразового `minio-init` нет (по
`../docs/dev/README.md` голый `docker compose up -d --wait` возвращает 1 из-за завершившегося `minio-init`);
- в логе эмулятора при старте — строка о каталогах и строка о числе фикстур (формат `run()` в
`api/src/scan_emulator/__init__.py`: `Эмулятор ИС Сканирование: корень {}, входящий каталог {}, исходящий каталог {},
задержка {} с, опрос {} с` и `Фикстуры: {}, фикстур: {}`), то есть `Эмулятор ИС Сканирование: корень /data/hotfolder,
входящий каталог /data/hotfolder/Input, исходящий каталог /data/hotfolder/Output, задержка 2.0 с, опрос 1.0 с` и
`Фикстуры: /fixtures/ocr, фикстур: 2`;
- `scan-gateway` — `healthy`, `/healthcheck` на `127.0.0.1:8002` даёт `nats: ok`, `hotfolder: ok`.
Примечания: (а) порт хоста 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 | **Решено человеком 04.10.2026 (OQ-76, частично):** исходно — реальные фикстуры `fixtures/ocr/aep_5.json`, `aep_8_form4.json` были ответами-ошибками ИС (`error.code = INVALID_DOCUMENT`, `data_ocr` — объект с пустыми полями и `table_structure: []`). Добавлены реальные успешные образцы: `aep_3.pdf` + `aep_3.json` (успех: «Бухгалтерский баланс» 42 строки + «Отчет о финансовых результатах» 18 строк) + `aep_3_ofr.json` (второй ответ ИС по тому же файлу, `INVALID_DOCUMENT`), `aep_6.pdf` + `aep_6.json` (успех: баланс 122 строки с шапкой дат + ОФР 49 строк); `aep_5.json`, `aep_8_form4.json` — ответы-ошибки | O-1, R-10 (design #32); OQ-76 | Транспорт Среза 0 проверяется на реальных образцах (склейка, `NOT_IMPLEMENTED`, `metrics.warnings`), синтетический успешный ответ не нужен; разбор успешных ответов — Срез 3 (нормализатор, OQ-13/14) |
| 4 | **Решено человеком 04.10.2026 (OQ-77):** исходно — §11 `05-domain-rules.md` описывал `data_ocr[]` (список), а в фикстурах `data_ocr` — объект. Факт по `aep_3.json`, `aep_6.json`: `data_ocr` — **объект**, один на json-файл (`label`, `organization`, `inn`, `units`, `date` — `{form_name, confidence}` с `confidence` 0–100; `table_structure` — **список таблиц** `{label, rows[]}`; в одном json может быть несколько таблиц разных форм (в `aep_3.json` и `aep_6.json` — баланс и ОФР), у каждой свой `label`; тип документа задаёт `label`; таблицы не интерпретируются в Срезе 0 (Срез 3)); успех — `error = {"code": "", "message": ""}` (не `null`), ошибка — непустой `code` и пустой `table_structure`. §11 исправлен в `docs` | O-1, T3.2; OQ-77 | В Срезе 0 успех файла определяется как `error.code == ""` (design #57); разбор таблиц — Срез 3 |
| 5 | **Решено человеком 04.10.2026 (OQ-78):** исходно шлюз ожидал единственный файл `<output>/<job_id>/<job_id>.json`, старый код читал все файлы каталога `Output/<id>/`. Факт: ИС кладёт **все распознанные документы одновременно** в каталог `<job_id>`, по одному json на документ (`aep_3.json` + `aep_3_ofr.json`); шлюз читает **все** `*.json` каталога и склеивает в один ответ; готовность — по стабилизации набора (`SCAN_SETTLE_SECONDS`, 2 с); ошибка одного документа не отменяет остальные (`metrics.warnings`) | T8.5; OQ-78 | Реализуется доработкой PR #3 (tasks 7.x, design #54–#58); подходящее значение settle и поведение ИС на реальной шаре — проверить на стенде в Срезе 3 |
| 6 | **Решено человеком 04.10.2026 (OQ-79):** исходно временный файл `.<имя>.part` лежал в `Input` (R-5), и ИС без маски могла забрать недописанный файл. Решение: временный файл уходит в служебный каталог `SCAN_TMP_DIR` (по умолчанию `Tmp`) рядом с `Input`/`Output`, на той же шаре/ФС, затем `rename` в `Input` (атомарно) | R-5, T7/T8; OQ-79 | Реализуется доработкой PR #3 (tasks 7.x, design #59); `Tmp` должен лежать на той же шаре, что `Input`; поведение ИС на реальной шаре — проверить в Срезе 3 |
| 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 | Порт шлюза на хосте задаётся `${SCAN_GATEWAY_PORT:-8002}` (фрагмент выше), но в `../docs/dev/.env.example` и в таблице переменных `../docs/dev/README.md` его нет | T16.1 | Владельцу `docs`: добавить `SCAN_GATEWAY_PORT=8002` в `../docs/dev/.env.example`, строку в таблицу переменных `../docs/dev/README.md` (рядом с `MINIO_CONSOLE_PORT`) и 8002 — в список свободных портов хоста |
| 17 | Replay с `attempt + 1` для `job_id`, который есть в LRU процесса, получает только ack и не обрабатывается (R-17, spec «обработан этим процессом»); по `../docs/context/03-architecture.md` replay — «сброс в `queued` с `attempt + 1`». На единственном инстансе такое задание повисает до `reply_deadline` -> `timeout` | I-3, R-17 | При одобрении: LRU `_done` с ключом `(job_id, attempt)` — более высокий `attempt` готового задания обрабатывать заново; `_active` — по `job_id` (повтор во время обработки — только ack). Решает человек |
| 18 | `make up` в tabular-parser (`../tabular-parser/Makefile`, цель `up`) тоже не запускает `minio-init`: на свежем томе бакета нет | I-1 | Чужой репозиторий — только сообщить владельцу; в scan-gateway исправлено (`run --rm minio-init` после `up -d --wait`) |
| 19 | Расхождения и наблюдения контроллера при исполнении (`Observation` в `progress.md`) | `progress.md` | См. ниже |
| 20 | Сквозная проверка 7.5 (Task 6, 04.10.2026) на профиле `services`: `aep_3.pdf` → `error NOT_IMPLEMENTED` + `metrics.warnings` про `<job_id>_ofr.json`, `aep_6.pdf` → `error NOT_IMPLEMENTED` без `metrics`, `aep_5.PDF` → `error INVALID_DOCUMENT` + одна строка `warnings`; **`../docs/dev/docker-compose.yml` функциональных правок не требует** (только устаревший комментарий, п. 11): умолчания `SCAN_TMP_DIR=Tmp` и `SCAN_SETTLE_SECONDS=2` подходят (`Tmp` создаётся шлюзом в томе `hotfolder`, после прогона пуст). Устарел раздел «Проверка окружения» `../docs/dev/README.md` (сценарий 2 и абзац «не покрывает») | Task 6, R-37 (design #78) | Владельцу `docs`, в `../docs/dev/README.md`: (1) строки 220–221 заменить на `# [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}]}`; (2) в строке 226 лог эмулятора — `<job_id>_aep_5.PDF -> /data/hotfolder/Output/<job_id>` (каталог набора, не файл); (3) строки 227–229 («успешный путь image … не проходит — нужен успешный ответ ИС») заменить на: «Успешные ответы ИС есть (`aep_3.json` + `aep_3_ofr.json`, `aep_6.json`); шлюз склеивает все `*.json` каталога ответа и отвечает `NOT_IMPLEMENTED` (нормализация — Срез 3), ошибочные файлы набора — в `metrics.warnings`»; (4) строку 230 заменить на «`processing` виден ~10 с: задержка эмулятора 2 с, затем до `SCAN_POLL_SECONDS + SCAN_SETTLE_SECONDS` (5 + 2 с) после первого опроса, увидевшего набор»; (5) в строке 255 «у ошибок `scan-gateway` `recognition_job.metrics` пустой» заменить на «у ошибок `scan-gateway` в `recognition_job.metrics` только `warnings` — по строке `<имя файла>: <код>: <сообщение>` на ошибочный файл ответа ИС, если такие есть»; (6) после сценария 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` пусто; (7) в той же строке 255 «полная очистка — `docker compose --profile services down -v`» дополнить оговоркой, что это удаляет и общие тома `postgres-data`/`minio-data` всех worktree (R-27, design #77); (8) строки 185–187 (абзац «Рецепт прогнан дословно при уже существовавшей в БД строке `legal_entity` … отдельно не проверялась.») заменить на «Рецепт прогнан дословно на чистой БД (вставка `legal_entity` без конфликта по `inn`) и повторно при уже существующей строке (`on conflict (inn) do nothing`); оба пути проверены.» (или удалить абзац: ветка вставки на чистой БД проверена в Task 6); (9) строки 224–226: «Код `INVALID_DOCUMENT` здесь — **настоящий ответ ИС из фикстуры**: обе пары в `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС Сканирование («Не удалось определить тип документа.», пустой `table_structure`). Эмулятор нашёл пару (в логе `scan-emulator`: `<job_id>_aep_5.PDF -> …/Output/<job_id>/<job_id>.json`; при отсутствии пары сообщение было бы «…: нет эталонного ответа в фикстурах»).» заменить на «Код `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>`; при отсутствии набора сообщение было бы «…: нет эталонного ответа в фикстурах»).» (строка 227 «Поэтому успешный путь `image` … не проходит — нужен успешный ответ ИС в `fixtures/ocr`.» заменяется по (3)); (10) строки 252–253: «успешные результаты (`done`/`needs_check`, регистрация файла `results/…` в `s3_object`) — заглушка парсера и фикстуры ИС дают только ошибки;» заменить на «успешные результаты (`done`/`needs_check`, регистрация файла `results/…` в `s3_object`) — заглушка парсера и нормализатор шлюза Среза 0 (`NOT_IMPLEMENTED`) дают только ошибки, хотя фикстуры ИС (`aep_3`, `aep_6`) успешные ответы содержат;»; (11) `../docs/dev/docker-compose.yml:184`: «# Фикстуры ответов ИС — ../../fixtures/ocr (только чтение): пара <имя>.json для входного файла UUID_<имя>.ext.» заменить на «# Фикстуры ответов ИС — ../../fixtures/ocr (только чтение): набор <имя>.json + <имя>_*.json для входного файла UUID_<имя>.ext.» (только комментарий, поведение compose не меняется) |
Строки `Observation` из `progress.md` (только пересказ, статусы — в ledger):
- **O-1** — **[закрыто 04.10.2026: образцы `aep_3.json`, `aep_6.json` (OQ-76), пункты 3 и 4 таблицы]**. Исходно: обе
реальные фикстуры (`aep_5.json`, `aep_8_form4.json`) сами являлись ответами-ошибками ИС (`INVALID_DOCUMENT`, `data_ocr`
— объект, не список), поэтому путь `NOT_IMPLEMENTED` проверялся синтетическим ответом (R-10, design №32, заменено D-9);
теперь он проверяется на реальных успешных ответах.
- **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`.
- [ ] Перенести правки раздела «Проверка окружения» `../docs/dev/README.md` по итогам сквозной проверки 7.5 (пункт 20) —
тем же PR репозитория `docs`; compose правки не требует.
- [ ] Согласовать с владельцем tabular-parser семантику `PUBLISH_RETRIES` (пункт 1) и решить судьбу правила про
`SCHEMA_MISMATCH` + ack (пункт 2).
- [x] Пункты 3-6 (OQ-76/77/78/79: образцы успешных ответов, формат `data_ocr`, состав файлов ответа, временный файл)
решены человеком 04.10.2026 и внесены в `docs/context/07-open-questions.md`; пункты 7, 8, 15 — в follow-up issue
(`Follow-ups from #1`).
- [ ] Не архивировать change `slice-0-skeleton`, пока compose-фрагмент не применён в `docs` и не проверена команда
`docker compose --profile services up -d --wait scan-gateway scan-emulator` (I-2).
- [ ] Решить I-3 (пункт 17: replay с `attempt + 1` для задания из LRU процесса).
- [ ] Определить приоритет follow-up по T12.5 (пункт 8): при каждом деплое задания в работе превращаются в
окончательный `timeout`.