Andrey Epifancev dc64d9a214 docs(openspec): design.md — решения 34–38 (рулинги задачи 6.1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 06:41:55 +04:00

173 lines
40 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.

# Design: Срез 0 — сквозной скелет
## Context
См. `proposal.md` — Why. Ограничения, формирующие решения:
- Шаблон команды (`03-architecture.md`, «Соглашения шаблона команды»): один бакет S3, NATS с nkeys/JWT, настройки
`OPENBAO__*`, `DEBUG=true` отключает внешние интеграции, `Dockerfile`/`docker-compose.yml` в `.gitignore` шаблона.
- DECISION-4 DBML: сервисы распознавания без БД; обмен только через `recognition_job` (outbox/inbox), дедуп по `msg_id`, DLQ.
- **Факты платформы (ответы пользователя 03.10.2026):** Python **3.11**; версия Node неизвестна; доступа к Nexus банка
(`raisa_*`) не будет до деплоя в прод; CI в Gitea нет и не планируется — у платформы свой CI/CD, в него не лезем;
**NATS без JetStream**; PostgreSQL **16**; создавать схемы можно; параметры JWT РАИСА пока неизвестны.
- Фикстуры — отдельный приватный репозиторий `arm-ka-fixtures`; локально доступен как `../fixtures`.
## Goals / Non-Goals
**Goals:** один файл контракта на каждое сообщение; один `docker compose up` для всего workspace; одна команда проверки
качества, одинаковая для трёх репозиториев; документы контекста соответствуют реальному шаблону и реальной платформе.
**Non-Goals:** см. proposal. Дополнительно: не проектируем prod-деплой (это платформа банка), не решаем ретеншн `results/`
(ARCH-010), не пишем конфигурации CI ни для Gitea, ни для платформы.
## Decisions
### D-1. Контракт — JSON Schema в docs + вендоренная копия в каждом сервисе, без общего пакета
`09-delivery-plan.md` оставлял выбор: пакет `arm-ka-contracts` или копия с проверкой идентичности. Выбрана **копия**:
`docs/contracts/*.schema.json` — источник истины; каждый сервис хранит копию в `api/contracts/` и pydantic-модели в
`api/src/contract/`; тест сервиса сравнивает копию с `../docs/contracts` байт в байт (пропускается с предупреждением,
если `docs` недоступен и не задан `ARM_KA_DOCS`), а pydantic-модели валидируют `docs/contracts/examples/*.json` и
генерируют JSON, проходящий схему. *Альтернатива* — публикуемый пакет: нет внутреннего PyPI, добавляет цикл релиза ради
трёх потребителей. *Цена ошибки:* при переходе на пакет копии удаляются, тест идентичности заменяется пином — полдня.
### D-2. Конверт результата не дублирует `group_schema`
`03-architecture.md` перечислял `group_schema` в сообщении `recognition.result`. В конверте остаются только поля для
маршрутизации и статуса; `group_schema` ядро берёт из `documents[].group_schema` файла результата при маппинге
(Срез 2). *Почему:* одна истина на результат, сообщение остаётся маленьким (лимит NATS 1 МБ не трогаем).
### D-3. Доставка без JetStream: app-level ack, повторная публикация ядром, дедуп в БД
JetStream на платформе нет, значит нет durable consumer'ов, серверного дедупа по `Nats-Msg-Id`, redelivery и `max_deliver`.
Схема на core NATS:
- Запросы публикуются ядром из outbox (`recognition_job.status = queued → published`); сервисы подписаны **queue group**
(`tabular-parser`, `scan-gateway`) — несколько инстанций делят нагрузку, одно сообщение получает одна.
- Сервис **сразу по получении** публикует в `recognition.result` конверт со `status = processing` (ack: «взял в работу») —
это значение уже есть в `recognition_status` DBML («сервис подтвердил взятие в работу»). Ядро ставит `processing`,
`started_at`, `service_name/version`.
- Если ack не пришёл за `ACK_TIMEOUT_SECONDS` (30) — ядро **публикует запрос повторно** с тем же `Nats-Msg-Id`
(`<job_id>:<attempt>`, тот же `attempt`: попытка обработки не началась), не более `OUTBOX_MAX_REPUBLISH` (3) раз, затем
`status = dlq` (DLQ-поведение DBML без брокера). `attempt`/`max_attempts` остаются для replay после `error`/`timeout`
(ручной, Срез 3+).
- Результат, потерянный при простое ядра, обнаруживается по `reply_deadline` → `timeout`; `result_msg_id UNIQUE` в
`recognition_job` остаётся единственным дедупом результатов (`Nats-Msg-Id` сервисы проставляют сами).
- Сервис держит process-local множество `job_id` в работе: повторный запрос с тем же `job_id` (пришёл повторно, пока
первый обрабатывается или уже обработан в этом процессе) подтверждается ack'ом и не обрабатывается второй раз.
*Альтернатива* — ждать JetStream или ставить свою очередь (Redis/БД-поллинг у сервисов): первое не в нашей власти, второе
нарушает DECISION-4 (сервисы без БД). *Если JetStream появится:* меняются только publisher/subscriber (`js.publish`,
durable consumer), outbox/inbox и контракт остаются.
### D-4. Subjects и группы
Stream'ов и consumer'ов нет. Subjects: `recognition.tabular.request`, `recognition.image.request`, `recognition.result`.
Queue group'ы: `tabular-parser`, `scan-gateway` — на запросах; `arm-core` — на результатах (несколько инстанций ядра
получают результат по одному разу). Заголовки `Nats-Msg-Id` ставятся всегда (core NATS 2.2+ поддерживает заголовки) —
для трассировки и будущего JetStream.
### D-5. Один бакет `arm-ka`, роли — префиксы
Следствие шаблона (`MINIO_BUCKET`). Ключи: `uploads/{file_id}/{job_id}/{original_name}`,
`results/{file_id}/{job_id}/result.json`, `raw/{job_id}.json`, `exports/…`, `archive/…`. Lifecycle в MinIO dev не
настраиваем; в проде — по префиксам (ARCH-010 для `results/`).
### D-6. Эмулятор ИС Сканирование — часть `scan-gateway`, не отдельный репозиторий
Эмулятор знает формат горячих папок и ответы Content AI так же, как сам шлюз; живёт в `scan-gateway/api/src/scan_emulator/`,
в compose запускается тем же образом другой командой с общим volume `hotfolder` и смонтированным `../../fixtures/ocr`.
*Альтернатива* — скрипт в `docs/dev/`: пришлось бы дублировать соглашения об именах `UUID_имя` и структуре `Output/UUID/UUID.json`.
### D-7. Compose на уровне workspace, Dockerfile — в каждом сервисе
`docs/dev/docker-compose.yml` собирает сервисы из `../../<repo>` (работает и из `.wt/<срез>/docs/dev`). Для этого из
`.gitignore` сервисов убирается строка `Dockerfile` (единственная правка шаблона); `docker-compose.yml` в репозиториях
не появляется. Образ для прода собирает платформа по своим правилам — наш `Dockerfile` только для dev и `make check-full`.
### D-8. Проверки качества — локальная команда, без конфигураций CI
CI в Gitea нет, пайплайн платформы — вне репозиториев. Каждый репозиторий получает одинаковые цели `Makefile`:
`make check` (быстро, без Docker: `ruff check`, `ruff format --check`, `mypy`, `pytest -m "not integration"`, тест
идентичности контракта) и `make check-full` (`make check` + интеграционные тесты против compose; в `arm-core` — ещё
«`alembic upgrade head` → `downgrade base` → `upgrade head`» и `check_dbml_vs_orm.py`). `make check-full` обязателен
перед PR, результат вписывается в описание PR. Правила — `docs/dev/checks.md`. Фикстуры берутся из `../fixtures` или
`ARM_KA_FIXTURES`; при отсутствии тесты с маркером `fixtures` пропускаются с предупреждением. Что из этого попадёт в
пайплайн платформы — решает платформа; мы даём ей готовые команды.
### D-9. Версии инструментов
Python **3.11** (`python:3.11-slim` в Dockerfile; пины `requirements.txt` совместимы). PostgreSQL **16** в compose
(код не использует ничего новее 15; `NULLS NOT DISTINCT` доступен). NATS **2.10 без `--jetstream`** — паритет с прод.
Node — неизвестен; локально `node:22` LTS, только для сборки `web/` в compose; меняется одной строкой.
### D-10. Пакеты `raisa_*` без Nexus
`raisa_fastapi_protected_api` остаётся в `requirements.txt` для сборки платформой; dev-установка (`make venv`,
`Dockerfile`) ставит `requirements.txt` без строк `raisa_*` (фильтр `grep -v`), код импортирует пакет условно, как в
шаблоне (`DEBUG=false`). В `web/package.json` `raisa-ui` и `raisa-fastapi-protected-web` переносятся в
`optionalDependencies`: `npm install` без Nexus проходит, с Nexus — ставит их как раньше. *Цена ошибки:* если платформа
требует их в `dependencies` — вернуть две строки.
## Risks / Trade-offs
- [Без JetStream сообщение теряется при простое получателя] → запросы: ack + повторная публикация ядром (D-3);
результаты: `reply_deadline` → `timeout` и replay; для OCR повторная публикация до ack не повторяет распознавание
(сервис ещё не брал файл).
- [Повторный запрос пришёл в сервис, который уже обрабатывает `job_id`] → process-local множество в работе; имя файла в
горячей папке = `job_id`, повторный `put` перезаписывает тот же файл.
- [Публичные образы PostgreSQL/NATS/MinIO отличаются от банковских] → версии зафиксированы по ответам (16 / 2.10 без JS).
- [Шаблон инициализирует nkeys при импорте `app.py`] → задача сервисам: пустые `NATS_SEED/NATS_JWT` = соединение без
учётных данных (dev); при заданных — как в шаблоне.
- [Копии контракта расходятся] → тест идентичности в `make check` каждого сервиса.
- [Эмулятор отвечает только для файлов, у которых есть пара `aep_N.json` (сейчас две)] → для остальных имён —
ответ с `error.code = INVALID_DOCUMENT` в формате Content AI; это тоже проверяемый сценарий.
- [Нет CI — проверки можно забыть] → `make check-full` в DoD каждой задачи и в описании PR; ревьюер требует вывод.
## Migration Plan
Нет данных для миграции. Порядок внедрения: контракт → compose инфраструктуры → arm-core → сервисы → compose профиль
`services` → проверки качества → документы. Откат — удаление ветки; `docs/contracts/recognition-result.schema.json`
остаётся совместимым с текущим черновиком (добавлен только код ошибки).
## Open Questions
Отвечено 03.10.2026: Python 3.11; Nexus недоступен до прода; CI в Gitea нет, пайплайн платформы не трогаем; JetStream нет;
PostgreSQL 16, схемы создавать можно. Остаются (не блокируют старт):
1. Версия Node для `web/` (пока `node:22`).
2. JWT РАИСА: алгоритм, источник ключей, имена claim'ов, issuer/audience — нужны к первому деплою; до этого dev-эмитент.
3. Имена входящего/исходящего каталогов и путь SMB на стенде ИС Сканирование — к Срезу 3.
4. Появится ли JetStream на платформе позже — влияет только на publisher/subscriber (D-3).
## Decisions made without discussion
| № | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| 1 | 2026-10-03 | D-1: контракт вендорится копией, общего пакета нет | нет внутреннего PyPI; три потребителя | полдня на переход к пакету |
| 2 | 2026-10-03 | D-2: `group_schema` убран из конверта результата | одна истина в файле результата | правка одной таблицы в docs и одной pydantic-модели |
| 3 | 2026-10-03 | D-3/D-4: без JetStream — ack `processing`, повторная публикация ядром (30 с, 3 раза), queue group'ы, дедуп по `result_msg_id` | ответ пользователя «JetStream нет»; DECISION-4 требует не трогать БД из сервисов | при появлении JetStream — замена publisher/subscriber |
| 4 | 2026-10-03 | D-5: один бакет `arm-ka`, роли как префиксы | шаблон команды (`MINIO_BUCKET`) | `s3_object.bucket` хранится отдельно — смена на несколько бакетов не ломает модель |
| 5 | 2026-10-03 | D-6: эмулятор ИС Сканирование живёт в `scan-gateway` | общие соглашения горячих папок | перенос каталога |
| 6 | 2026-10-03 | D-7: compose в `docs/dev/`, `Dockerfile` разыгнорирован в сервисах | шаблон игнорирует Dockerfile, платформа собирает сама | вернуть строку в `.gitignore` |
| 7 | 2026-10-03 | D-8: проверки качества — `make check` / `make check-full`, конфигураций CI нет | CI в Gitea нет, пайплайн платформы не наш | добавить YAML под нужный раннер, команды уже есть |
| 8 | 2026-10-03 | В контракт v1.0 добавлен код ошибки `NOT_IMPLEMENTED` и статус конверта `processing` (ack) | заглушки Среза 0; доставка без JetStream | удалить значения enum в 1.1 |
| 9 | 2026-10-03 | Справочники продукта для seed — в `arm-core/api/seed/`, не в `fixtures/directories/` | данные продукта нужны в проде; `fixtures` — приватные тестовые данные | перенос файлов и правка §6 `06-test-data.md` |
| 10 | 2026-10-03 | D-9: `python:3.11-slim`, PostgreSQL 16, NATS 2.10 без JS, `node:22` до ответа | ответы пользователя; Node неизвестен | смена тега в compose/Dockerfile |
| 11 | 2026-10-03 | Worktree среза — зеркальный workspace `.wt/<срез>/` с симлинками | относительные пути `../docs`, `../fixtures` и хуки должны работать в ветке | переместить worktree |
| 12 | 2026-10-03 | D-10: `raisa_*` фильтруются при dev-установке, npm-пакеты `raisa-*` → `optionalDependencies` | Nexus недоступен до прода | вернуть две строки в `dependencies` |
| 13 | 2026-10-03 | Файл результата: пустой `documents` допустим только при `status = error` — выражено в схеме условием `if/then` (`minItems: 1`) | требование spec `recognition-contract` «Файл результата»; текст задачи 1.1 его не перечислял | ослабить/убрать условие в схеме, пересобрать копии контракта в сервисах |
| 14 | 2026-10-03 | Compose: фиксированное имя проекта `name: arm-ka`, порты хоста переопределяются через `.env` со значениями по умолчанию из spec | одинаковые тома/сети из любого worktree; порты 5432/4222 на машине разработчика могут быть заняты | смена имени проекта — потеря dev-томов (`down -v`), порты — правка `.env` |
| 15 | 2026-10-03 | Чекбоксы плана и `tasks.md` отмечает контроллер SDD отдельным коммитом после чистого ревью задачи | исполнитель не должен отмечать выполненным то, что ещё не прошло ревью | отметки правятся вручную |
| 16 | 2026-10-03 | Compose: MinIO берётся из неофициальных образов `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и `bitnamilegacy/minio-client:2025.7.21` | `minio/minio` и `minio/mc` недоступны на Docker Hub (denied), на quay.io нужных тегов нет; для dev достаточно любого S3-совместимого MinIO нужной версии API | образы сторонние: при появлении доступа к банковскому реестру/официальным образам — заменить две строки `image:` в `dev/docker-compose.yml`; в прод эти образы не попадают |
| 17 | 2026-10-03 | Compose: сервис `minio` запускается от `user: "0:0"` | образ `alpine/minio` по умолчанию работает от пользователя `minio` и падает на свежем именованном томе | только dev; удалить строку, когда образ/том позволят писать без root |
| 18 | 2026-10-03 | `dev/.env.example` дополнен заглушками `OPENBAO__SMB__*` и `NATS_ENABLED=true` | `Settings` шаблона (arm-core) требует блок `OPENBAO.SMB`, без него сервис из venv не стартует; `NATS_ENABLED` по умолчанию `false`, и `/healthcheck` показывает `nats = off`; spec велит копировать именно `docs/dev/.env.example` | убрать две группы строк; сервисы с другим набором обязательных настроек получат лишние (игнорируемые) переменные |
| 19 | 2026-10-03 | `schema_version` во всех трёх схемах: `const "1.0"` заменён на `type: string, pattern: ^1\.\d+$` (производитель v1.0 пишет `"1.0"`); в `$comment` и в 03-architecture.md добавлено правило «новый MINOR раскатывается сначала на потребителей» | spec «Версионирование контракта»: потребитель принимает тот же MAJOR и любой MINOR, сервис «поддерживающий 1.x» принимает 1.1 и отвергает только 2.0; с `const` это неисполнимо, а копии схем уйдут в сервисы побайтно (финальное ревью I-1) | вернуть `const "1.0"` и пересобрать копии схем в сервисах; побочный риск — копии в `arm-core`/`tabular-parser`/`scan-gateway` нужно обновить (тест идентичности контракта упадёт, пока копии не обновлены) |
| 20 | 2026-10-03 | Compose: порты хоста публикуются на `${BIND_ADDR:-127.0.0.1}`, а не на 0.0.0.0 | пароли по умолчанию (`armka`, `minioadmin`); все сценарии идут через localhost; spec bind-адрес не оговаривает (финальное ревью M-6) | если нужен доступ с другой машины — `BIND_ADDR=0.0.0.0` в `.env`; для запуска профиля `services` порты хоста не нужны |
| 21 | 2026-10-04 | В корне репозитория `docs` добавлен `Makefile` с целями `venv`, `validate-contracts`, `validate-openspec`, `check` (сервисных целей нет); `jsonschema` ставится в `.venv` (в `.gitignore`) | дополнение контроллера к задаче 4.1: проверки контракта и спеки должны запускаться одной командой, как в сервисах; `check` = `scripts/validate_contracts.py` + `openspec validate slice-0-skeleton --strict` | удалить `Makefile` и строку `.venv/` в `.gitignore`; команды из `dev/checks.md` останутся запускаемыми вручную |
| 22 | 2026-10-04 | `dev/checks.md` описывает общие цели по эталону `scan-gateway` (`up` с `--project-name arm-ka`, `--wait postgres nats minio` и `run --rm minio-init`; `check-full` как последовательность шагов), расхождения `arm-core`/`tabular-parser` и неполная поддержка `ARM_KA_DOCS`/`ARM_KA_FIXTURES` в `scan-gateway` вынесены в раздел «Известные расхождения реализаций», код сервисов не менялся | spec «Одинаковый набор целей» требует единой семантики, а фактические `Makefile` расходятся; правка сервисов вне задачи 4.1 и их PR | если эталон выбран неверно — переписать раздел `make up` в `checks.md`; расхождения останутся незакрытыми, пока сервисы не выровнены |
| 23 | 2026-10-04 | В `dev/README.md` раздел «Интеграционные тесты сервисов против compose» заменён ссылкой на `checks.md`; убрано утверждение «Makefile есть только у `arm-core`» | утверждение устарело (Makefile есть у всех трёх сервисов), единый источник описания проверок — `checks.md` | вернуть старый текст раздела |
| 24 | 2026-10-04 | `dev/Makefile.common` — эталон общих целей по `scan-gateway`; различия сервисов вынесены в переменные (`MYPY_PATHS`, `IMAGE`, `COMPOSE_*`, `CHECK_FULL_STEPS`), `check-full` реализован через `$(foreach …)` с вызовом `$(MAKE)` на каждый шаг; в docs-репозитории `Makefile.common` не `include`-ится, сервисы его пока не подключают | задача 4.2 требует эталонный фрагмент и сверку `diff`ом; Makefile сервисов в Срезе 0 не правятся (код готов, PR открыты) — `include` в них можно ввести без смены поведения следующим срезом | сервисы продолжают держать копии рецептов; расхождения копий вылезают только при правке одной из них (сверка — `make -n`, см. `checks.md`)
| 25 | 2026-10-04 | Замечания T1.1–T1.4 ревью задачи 4.1 исправлены в `dev/checks.md` (fixtures и «ARM_KA_FIXTURES» — только `arm-core`, `mypy src scripts`, «Инфраструктура compose», «одинаковая семантика» заменена ссылкой на расхождения) | прямое поручение контроллера; формулировки приведены в соответствие с фактическими Makefile и тестами | неточная документация; правится одной строкой
| 26 | 2026-10-04 | Профиль `services` в `dev/docker-compose.yml`: переменные сервисов — общие якоря YAML (`x-service-env`, `x-arm-core-env`) с адресами внутри сети compose и `DEBUG=true`; образы собираются локально с тегами `arm-core:dev`, `tabular-parser:dev`, `scan-gateway:dev`; healthcheck контейнеров — `python -c` с чтением поля `nats` (у `scan-gateway` ещё `hotfolder`) из `/healthcheck`; порты хоста `${ARM_CORE_PORT:-8000}`/`${TABULAR_PARSER_PORT:-8001}`/`${SCAN_GATEWAY_PORT:-8002}` на `${BIND_ADDR:-127.0.0.1}`, в контейнере 8000 | spec dev-environment («`OPENBAO__*` шаблона, `DEBUG=true`»), фрагмент `scan-gateway` (`open-questions-proposal.md`) как основа; `curl` в `python:3.11-slim` нет, а `/healthcheck` всегда 200; якоря убирают тройное дублирование учётных данных MinIO/NATS | правка одного блока compose; при смене формата `/healthcheck` сервиса — правка трёх строк `test` |
| 27 | 2026-10-04 | Одноразовый сервис `arm-core-migrate` (`alembic upgrade head && python -m scripts.seed --dev`, `depends_on` postgres `service_healthy`); `arm-core` ждёт его `service_completed_successfully`; выполняется при каждом `up` профиля | дополнение контроллера к задаче 5.3; без схемы `dkb` и справочников ядро не работает, оба шага идемпотентны (повторный seed — `0 / 0 / N`); `--dev` — нужны dev-пользователи для dev-токена | лишний запуск миграций при каждом `up` (~2 с); убрать `--dev` — одна строка |
| 28 | 2026-10-04 | Образ каждого репозитория собирает один сервис: `build` только у `arm-core` и `scan-gateway`; `arm-core-migrate` и `scan-emulator` используют тег `arm-core:dev`/`scan-gateway:dev` с `pull_policy: never` (отступление от фрагмента `scan-gateway`, где `build` у обоих) | проверено: при `build` у двух сервисов с одним тегом compose 2.34 собирает образ дважды с разными ID, метка `com.docker.compose.image` контейнера расходится с тегом, и каждый следующий `up` пересоздаёт `arm-core-migrate` и `arm-core` (флип-флоп); с одной сборкой три `up` подряд — 0 пересозданий | `up -d scan-emulator` или `arm-core-migrate` в одиночку на машине без образа падает `No such image` — сначала `docker compose --profile services build`; вернуть `build` — две строки |
| 29 | 2026-10-04 | В compose у `arm-core` включён dev-эмитент токенов (`AUTH_DEV_ISSUER=true`, публичный `AUTH_DEV_SECRET` из `arm-core/api/.env.example`) | ручные проверки `/api/v1/me` и сквозной сценарий (задача 5.4) без РАИСА; маршрут существует только при `DEBUG=true`; порт хоста по умолчанию на 127.0.0.1 | при `BIND_ADDR=0.0.0.0` токен любого dev-пользователя может получить любой в сети — только dev; выключить — одна строка |
| 30 | 2026-10-04 | Сквозная проверка окружения (задача 5.4) выполняется вставкой строк `legal_entity` (синтетическое, ИНН `7700000001`, `on conflict do nothing`) / `s3_object` / `stored_file` / `recognition_job(queued)` через `psql` в контейнере `postgres` и загрузкой исходника в `uploads/{file_id}/{job_id}/{имя}` через `mc` из образа `minio-init`; рецепт — функции `dpsql`/`dmc`/`new_job`/`poll` в `dev/README.md` («Проверка окружения»); тестовые строки и объекты не удаляются | API загрузки файлов в Срезе 0 нет, тестовых скриптов `arm-core` для рабочей БД тоже (его интеграционные тесты работают во временной БД); ключ — по «Схеме ключей S3» `03-architecture.md`; рецепт проверен прогоном дословно | вставка в обход будущего API может разойтись с ним (обязательные поля `stored_file`/`document`) — рецепт придётся обновить в Срезе 1/2; мусорные строки в dev-БД — сброс `down -v` |
| 31 | 2026-10-04 | Ожидаемый итог сценария `image` в README — `error` / `INVALID_DOCUMENT` (фактический ответ), а не `NOT_IMPLEMENTED`; файл — `ocr/aep_5.PDF` (так он назван в `fixtures`, не `aep_5.pdf`) | обе пары `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС, эмулятор отдаёт их как есть, шлюз пробрасывает код ИС; на регистрозависимой ФС `aep_5.pdf` не найдётся | успешный путь `image` (`done`/`needs_check`, `results/…`) сквозной проверкой не покрыт, пока в `fixtures/ocr` нет успешного ответа ИС |
| 32 | 2026-10-04 | Замечание T3.6 проверено эмпирически: образы `scan-gateway:dev` и `arm-core:dev` пересобраны `build --no-cache` (новые ID) на работающем профиле; после пересоздания контейнеров старые ID образов в `docker image ls -a` не остались (`No such image`) | формулировку README нужно было подтвердить, а без смены ID образа compose ничего не пересоздаёт | ~1 мин сборки и загрузка зависимостей из PyPI; на окружение не влияет (образы — те же исходники) |
| 33 | 2026-10-04 | В README для наблюдения за шиной предложен `natsio/nats-box:0.17.0` (`nats sub 'recognition.>'` в сети `arm-ka_default`) — разовый `docker run --rm`, не сервис compose | без него `published`/`processing` у `tabular` (десятки мс) не видны; образ доступен с Docker Hub (проверено), в compose не добавлялся, чтобы не менять состав окружения | при недоступности образа — только логи `arm-core` и метки времени задания; шаг опциональный |
| 34 | 2026-10-04 | Правим спеку и документ под фактическое поведение сервисов, а не код: при неизвестном MAJOR `schema_version` (и при запросе не по схеме) сервис публикует `error SCHEMA_MISMATCH` **без** отдельного ack `processing`; формулировка «и подтверждает ack'ом» (T12.13) из `spec.md` (сценарий «Несовместимая версия запроса») и `03-architecture.md` убрана. Конверт публикуется, только если извлекаются корректные `job_id`/`file_id`, и не публикуется для `job_id`, уже находящегося в работе/обработанного | `tabular-parser/api/src/core/worker.py` (`_reject`: «`SCHEMA_MISMATCH` без ack», строки 242–275) и `scan-gateway/api/src/core/worker.py` (`_reject`, R-16: строки 288–321) ведут себя одинаково; финальный конверт сам закрывает задание в ядре (`published → error` допустим, `recognition_exchange.py`: результат принимается из `queued/published/processing`), ack перед ним ничего не добавляет, а `Nats-Msg-Id` ack и результата разные; конверт для `job_id` в работе совпал бы по `result:<job_id>:<attempt>` с настоящим результатом | если ядро когда-нибудь начнёт требовать ack перед результатом, задание с `SCHEMA_MISMATCH` останется в `published` до повторов и `dlq`; правка — одна строка в воркерах и спеке |
| 35 | 2026-10-04 | Запрос, из которого нельзя извлечь `job_id` (не JSON, не объект, нет/не uuid `job_id`), остаётся без ответа: сервис пишет лог и отбрасывает его; ядро закрывает задание по `NO_ACK` / `dlq` после `OUTBOX_MAX_REPUBLISH` повторов. Записано в `spec.md` (новый сценарий «Нечитаемый запрос») и `03-architecture.md` | собрать валидный конверт без `job_id` нельзя (схема требует uuid), а выдуманный `job_id` ядро всё равно не сопоставит с заданием; оба сервиса логируют и сбрасывают (`worker.py`: tabular строки 253–263, scan-gateway 292–299); ядро и так закрывает «молчание» (`recognition_exchange.py`: `NO_ACK`, `dlq`) | нет худшего исхода, чем 120 с до `dlq` (4 × `ACK_TIMEOUT_SECONDS`) для заведомо битого запроса, которого ядро само не порождает; сокращать задержку можно только чтением `job_id` из заголовка `Nats-Msg-Id` — вне Среза 0 |
| 36 | 2026-10-04 | `PUBLISH_RETRIES` в спеке (новое требование «Повторы публикации ответов сервисом») = **одна первая попытка + до 3 повторов** (всего не более 4 попыток) с тем же `Nats-Msg-Id` и паузой между попытками; исчерпаны — error-лог, задание закрывает ядро (`reply_deadline`). Пауза — деталь реализации, в спеку не включена | значения в коде: `scan-gateway/api/src/core/config.py:63` (`PUBLISH_RETRIES = 3`, «повторы после первой попытки (всего до 4)») и `core/nats.py:245–256` (`attempts_total = retry_count + 1`); `tabular-parser/api/src/core/config.py:49` (`PUBLISH_RETRIES = 3`, `NonNegativeInt`) и `core/nats.py:306` (`attempts = publish_retries + 1`); паузы различаются (2,0 с и 0,5 с), семантика одна | если ядро/сервис договорятся считать `PUBLISH_RETRIES` общим числом попыток, расхождение «3 против 4» не ломает обмен (дедуп по `Nats-Msg-Id`); правка — одна фраза в спеке |
| 37 | 2026-10-04 | Пример `contracts/examples/request-image.json` с `reply_deadline` в прошлом **не меняется**; в `spec.md` (сценарий «Примеры контракта — иллюстрации») и `03-architecture.md` записано, что примеры — иллюстрации формы, схема проверяет у `reply_deadline` лишь формат `date-time`, а срок проверяет сервис (`scan-gateway` — `error TIMEOUT`, `scan-gateway/api/src/core/worker.py:469–475`; `tabular-parser` срок не проверяет: `reply_deadline` в его воркере не используется) | копии `examples/` в сервисах сверяются побайтно тестами идентичности: правка примера = пересинхронизация трёх репозиториев ради документальной оговорки; `validate_contracts.py` проходит (rc 0, 8 примеров) | если кто-то возьмёт `request-image.json` как готовое задание для `scan-gateway`, получит `TIMEOUT` — это зафиксировано оговоркой; обновить дату позже можно одним PR с пересинхронизацией |
| 38 | 2026-10-04 | `06-test-data.md` §6 и `09-delivery-plan.md` (Срез 0) приведены к факту: `fixtures/directories/` — только тестовые ЮЛ/ГК (сейчас пустой `.gitkeep`, наполнение — Срез 1); seed продукта лежит в `arm-core/api/seed/core/` (+ `seed/dev/` только для `--dev`); в плане Среза 0 добавлена ссылка на `dev/README.md` («Профиль `services`», «Проверка окружения»); `00-workspace-layout.md` не менялся (неактуальных фактов нет) | перечень файлов seed и число ролей/прав проверены по `arm-core/api/seed/` (7 ролей, 16 прав, 122 строки плана счетов); решение D-1, compose и проверки вместо CI в `09` уже были верны | только неточная ссылка в документах; правится правкой текста |