# 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` (`:`, тот же `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` собирает сервисы из `../../` (работает и из `.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`; рецепт проверен прогоном дословно, но при уже существовавшем `legal_entity` с этим ИНН: ветка вставки без конфликта на чистой БД не выполнялась | вставка в обход будущего 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::` с настоящим результатом | если ядро когда-нибудь начнёт требовать 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`). Пауза — деталь реализации (`tabular-parser` 2,0 с, `scan-gateway` 0,5 с), в спеку не включена | значения в коде: `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`); паузы различаются (`tabular-parser` — `PUBLISH_RETRY_PAUSE = 2.0` с в `core/nats.py:51`, `scan-gateway` — `RETRY_DELAY = 0.5` с в `core/nats.py:49`), семантика одна | если ядро/сервис договорятся считать `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` уже были верны | только неточная ссылка в документах; правится правкой текста | | 39 | 2026-10-04 | Реестр `07-open-questions.md`: 36 новых строк OQ-48…OQ-83 вместо ~60 пунктов трёх `open-questions-proposal.md` и находок контроллера — пересекающиеся пункты слиты в одну строку с перечнем источников: PUBLISH_RETRIES (scan-gateway п. 1 + O-25), `reply_deadline` (п. 12 + O-26), нечитаемый запрос (tabular P-1 + O-24), ack при `SCHEMA_MISMATCH` (tabular P-2 + scan-gateway п. 2), `attempt + 1` (tabular P-3 + scan-gateway п. 17), фикстуры OCR (п. 3 + O-15), `make up`/`check-full` (O-1, O-2, O-4, O-5 + scan-gateway п. 18), общий NATS (п. 11 + O-11), `stored_file.status` (arm-core предл. 19 + O-18), `republish_count` (предл. 10 + O-19), append-only аудит (предл. 9 + дополнение), срок ответа (предл. 11 + дополнение) | один вопрос — одна строка: иначе человек решает одно и то же дважды, а реестр несёт противоречивые статусы; перечень источников (репозиторий + маркер) сохраняет прослеживаемость, маркеры не менялись | если слияние оказалось неудачным, строка делится на две с новым номером; нумерация OQ-48…OQ-83 сплошная, на неё могут ссылаться только новые документы | | 40 | 2026-10-04 | Статусы OQ-45/46/47 обновлены в существующих строках, не продублированы: все три — «допущение принято и применено в Срезе 0» с фактами (NU: контракт оставляет, ORM `OsvDataType` 1:1 с DBML v5.13, `NoopMapper`; OQ-46: 27 строк EXTENDED + 10 BASIC в seed, `is_mandatory = true`; OQ-47: 122 строки плана счетов, названия нестандартных субсчетов выведены по роли в §4). Кандидаты DBML v5.14 (`NU`, `NULLS NOT DISTINCT`, note `article_2_line`, действие `audit_action`, счётчик republish, `lower(email)`) внесены только строками реестра, файл `data-model-v5.13.dbml` не менялся | факты проверены чтением `arm-core/api/seed/core/`, `api/src/models/enums.py` и `contract/common.py`; создание DBML-версии — отдельное решение человека (CLAUDE.md: версия не меняется задним числом) | если человек решит иначе (например, хранить НУ), статус строки меняется правкой реестра, код Среза 0 не затронут | | 41 | 2026-10-04 | В реестр **не** внесены, а оставлены в отчёте задачи 6 (кандидаты в follow-up issue при закрытии): scan-gateway п. 7 (рестарт NATS не автоматизирован), п. 8 (`stop()` не публикует конверты принятым заданиям), п. 9 (`/healthcheck` всегда 200), п. 13 (`DEBUG` не используется), п. 14 (`ResourceWarning` aiohttp), п. 15 (SMB без `connection_timeout`); tabular P-4, scan-gateway п. 10 и 16 (порты, `SCAN_GATEWAY_PORT`, устаревший README) — закрыты задачами 3–5 и `dev/README.md`; O-22 и T3.7/O-20 — процессные пункты задачи 7 | это инженерные доработки или уже исправленные расхождения документов, вопроса к человеку/аналитикам в них нет; реестр остаётся списком решений, которые принимает человек | пункты могут потеряться, если follow-up issue не будет заведена (чеклист закрытия, п. 6): они перечислены в отчёте с маркерами | | 42 | 2026-10-04 | Формулировка статусов в новых строках: «допущение принято: …» — в коде/спеке Среза 0 действует рабочее допущение, вопрос открыт для подтверждения; «вопрос: …» — человек должен принять решение, допущение Среза 0 указано после двоеточия (аудит на уровне БД, `attempt + 1`, образцы ответов ИС, формат `data_ocr`, имя файла ответа, `.part` в `Input`) | правило файла: агент вопросы не закрывает, а помечает допущение; слово «закрыт» нигде не использовано | различение статусов — рекомендация, не контракт; при несогласии префикс правится в реестре |