Andrey Epifancev 49d9eca082 chore(openspec): архив change slice-0-skeleton
Срез 0 реализован и влит (PR #3), change перенесён в openspec/changes/archive, спеки — в openspec/specs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-04 19:44:29 +04:00

48 KiB
Raw Blame History

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 убрать две группы строк; сервисы с другим набором обязательных настроек получат лишние (игнорируемые) переменные Примечание: часть про обязательность SMB устарела, см. №44.
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:<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). Пауза — деталь реализации (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) правило файла: агент вопросы не закрывает, а помечает допущение; слово «закрыт» нигде не использовано различение статусов — рекомендация, не контракт; при несогласии префикс правится в реестре
43 2026-10-04 Сценарий spec dev-environment «Запуск из venv против compose без Nexus» (make venv + python run_server.py + healthcheck) в Срезе 0 не прогонялся; раздел «Запуск сервисов из venv без Nexus» в dev/README.md остаётся черновиком, вопрос вынесен в OQ-84 (кандидат в follow-up issue) проверка требует создать .venv внутри сервисных репозиториев (make venv пишет в api/.venv), а в фазе 2 они были доступны только для чтения (их worktree с открытыми PR не правятся); сквозная проверка шла на профиле services (контейнеры из тех же исходников) сценарий spec может оказаться неработоспособным (набор переменных, requirements.txt без raisa_*, порты) — разработчик без Docker для сервисов узнает об этом при первом запуске; правится README или .env.example
44 2026-10-04 Уточнение к №18: блок OPENBAO__SMB__* не обязателен. В arm-core OpenBAOSettings.SMB = SMBSettings() с пустыми значениями по умолчанию (api/src/core/config.py), в scan-gateway блок необязателен в режиме SCAN_HOTFOLDER_MODE=local (по умолчанию) и обязателен целиком при smb, в tabular-parser блока нет, и OpenBAOSettings отвергает неизвестные ключи (extra_forbidden; проверено на копии его config.py с .env из dev/.env.example: падает на OPENBAO.smb, так же упадёт на OPENBAO__DATABASE__*) — extra="ignore" относится только к верхнему уровню Settings. Заглушки в dev/.env.example оставлены; комментарий и README исправлены и говорят: .env.example годится для arm-core как есть; для scan-gateway из него нужно убрать блок DATABASE (в его OpenBAOSettings поля нет, extra_forbidden), для tabular-parser — блоки DATABASE и SMB (уточнено 04.10 по ревью, N-1) в момент решения №18 Settings arm-core требовал блок; затем в arm-core блок сделан необязательным (в Срезе 0 SMB ядру не нужен), а формулировка осталась в .env.example и README ложное утверждение «обязателен» заставляло бы переносить заглушки в окружение, где они не нужны; цена низкая — у arm-core заглушки безвредны, а scan-gateway и tabular-parser с неотредактированным .env.example не стартуют (описано в README, часть OQ-84)