Срез 0: скелет ядра — ORM и миграция 0001 по DBML v5.13, seed, auth с dev-эмитентом, обмен через recognition_job #3

Merged
aep merged 97 commits from feat/1-slice-0-skeleton into main 2026-10-04 18:40:48 +03:00
Owner

Issue #1 — реализация change slice-0-skeleton (97 коммитов после merge proposal #2). Не закрывает issue: закрытие — после openspec archive командой «закрывай».

Что сделано

  • Инструменты шаблона: api/pyproject.toml (ruff, mypy), requirements-dev.txt, Dockerfile (python:3.11-slim), Makefile (venv без raisa_*, lint, test, test-integration, check, check-full, up, migrate, seed, seed-dev), ENV_FILE в Settings, NATS без nkeys при пустых учётных данных, /healthcheck.nats, префикс /api/v1; web/package.json: raisa-* → optionalDependencies.
  • Модель данных: ORM 1:1 с DBML v5.13 — 43 таблицы, 27 enum, схема dkb, partial unique, NULLS NOT DISTINCT, FK-цикл через use_alter; миграция 0001 с полным откатом; api/scripts/check_dbml_vs_orm.py (pydbml) — 0 расхождений; тест паритета мигрированной схемы с ORM.
  • Seed: api/seed/core (7 ролей, 16 прав, матрица §10 — 52 строки, план счетов 94н + субсчета 1С — 122, правила элиминации v1 ББ — 8 + 59 счетов, чек-лист BASIC/EXTENDED — 37) и api/seed/dev (3 РФ, 7 пользователей); идемпотентно.
  • Auth: TokenVerifier (dev HS256 за AUTH_DEV_ISSUER + DEBUG, JWKS-каркас), current_user/require(permission), GET /api/v1/me, POST /api/v1/dev/token только в dev.
  • Обмен через recognition_job на core NATS (без JetStream): outbox, ack processing, повторная публикация без ack (30 с × 3 → dlq, NO_ACK), inbox с дедупом по result_msg_id, регистрация результата в s3_object, вахтёр reply_deadline; NoopMapper как точка расширения Среза 2. AuditService (append-only).
  • Вендоренный контракт v1.0 (api/contracts/, src/contract/) с тестом идентичности к docs/contracts.

Проверки

make check: ruff, format, mypy чисто; 590 unit passed. make check-full против compose (docs/dev): exit 0 — 590 unit + 153 integration (цикл миграций upgrade → downgrade → upgrade, пустой autogenerate, сверка схемы), dbml-check 0 расхождений, docker build ок, web: npm install, biome, tsc, rsbuild build ок. DoD-прогон: make up → migrate → seed-dev → /api/v1/me (KA_RF 10 прав, ADMIN 7, KA_GO 11; без токена 401; при DEBUG=false dev-маршрут 404) и e2e обмен через NATS (published → ack → done + s3_object, повтор результата без изменений). OpenAPI покрыт тестом test_full_app_me_dev_token_and_openapi. openspec validate --strict — valid.

Rulings I made

Полный список — openspec/changes/slice-0-skeleton/design.md, таблица «Decisions made without discussion» (№14–105: пред-проверка, рулинги по ходу, решения исполнителей, fix-wave). Ключевые: R-1 Makefile в корне, backend-цели через cd api; R-4 required_document_rule в audit_ex.py; R-7 обмен recognition_job не пишет audit_event в Срезе 0 (нет подходящего audit_action); R-9 naming_convention для ограничений; R-11 PyJWT[crypto] + cryptography в прод-requirements; R-15 CREATE SCHEMA только при отсутствии схемы; R-16 тесты в отдельной БД armcore_pytest_<hex>; R-17 email нормализуется (lower), >1 совпадения → 403; R-19 .env.test на порты compose; R-20 MINIO_SECURE (false для http); R-22 финальные статусы inbox {done, needs_check, error, timeout, dlq}; R-23 web/package-lock.json в .gitignore (решение о commit — за человеком). Трейлеры трёх коммитов исправлены до push.

Deferred (в follow-up issue при закрытии)

T1.2, T8.3 (required_document_rule NULLS NOT DISTINCT → DBML v5.14), T12.2, T13.5 (OQ-47), T14.1–T14.3, T15.4 (--dev без guard), T16.2 (aud/iss, параметры JWT РАИСА), T17.2 (аудит LOGIN_FAILED), T20.7, T22.2, T22.9/T24.9/I-6 (разнести быстрый inbox и маппер Среза 2), T23.1, T23.9, T24.3, T24.6, T26.2 (npm audit 6 high в шаблонных зависимостях), M-1, M-5, M-6 (recognition_exchange.py 786 строк), M-8, M-10, O-8. Предложения в docs (OQ, DBML v5.14, контракт) — openspec/changes/slice-0-skeleton/open-questions-proposal.md.

Что нужно решить человеку

Коммитить ли web/package-lock.json; 404 vs 405 под /api/v1; обязательный aud в проде; параметры JWT РАИСА.

Связанные

RSHB/arm-ka-docs ветка feat/1-slice-0-skeleton (контракт v1.0, compose) — PR после фазы 2; RSHB/arm-tabular-parser, RSHB/arm-scan-gateway — в работе.

🤖 Generated with Claude Code

https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2

Issue #1 — реализация change `slice-0-skeleton` (97 коммитов после merge proposal #2). Не закрывает issue: закрытие — после `openspec archive` командой «закрывай». ## Что сделано - **Инструменты шаблона:** `api/pyproject.toml` (ruff, mypy), `requirements-dev.txt`, `Dockerfile` (`python:3.11-slim`), `Makefile` (`venv` без `raisa_*`, `lint`, `test`, `test-integration`, `check`, `check-full`, `up`, `migrate`, `seed`, `seed-dev`), `ENV_FILE` в Settings, NATS без nkeys при пустых учётных данных, `/healthcheck.nats`, префикс `/api/v1`; `web/package.json`: `raisa-*` → `optionalDependencies`. - **Модель данных:** ORM 1:1 с DBML v5.13 — 43 таблицы, 27 enum, схема `dkb`, partial unique, `NULLS NOT DISTINCT`, FK-цикл через `use_alter`; миграция `0001` с полным откатом; `api/scripts/check_dbml_vs_orm.py` (pydbml) — 0 расхождений; тест паритета мигрированной схемы с ORM. - **Seed:** `api/seed/core` (7 ролей, 16 прав, матрица §10 — 52 строки, план счетов 94н + субсчета 1С — 122, правила элиминации v1 ББ — 8 + 59 счетов, чек-лист BASIC/EXTENDED — 37) и `api/seed/dev` (3 РФ, 7 пользователей); идемпотентно. - **Auth:** `TokenVerifier` (dev HS256 за `AUTH_DEV_ISSUER` + `DEBUG`, JWKS-каркас), `current_user`/`require(permission)`, `GET /api/v1/me`, `POST /api/v1/dev/token` только в dev. - **Обмен через `recognition_job` на core NATS (без JetStream):** outbox, ack `processing`, повторная публикация без ack (30 с × 3 → `dlq`, `NO_ACK`), inbox с дедупом по `result_msg_id`, регистрация результата в `s3_object`, вахтёр `reply_deadline`; `NoopMapper` как точка расширения Среза 2. `AuditService` (append-only). - Вендоренный контракт v1.0 (`api/contracts/`, `src/contract/`) с тестом идентичности к `docs/contracts`. ## Проверки `make check`: ruff, format, mypy чисто; 590 unit passed. `make check-full` против compose (`docs/dev`): exit 0 — 590 unit + 153 integration (цикл миграций upgrade → downgrade → upgrade, пустой autogenerate, сверка схемы), `dbml-check` 0 расхождений, `docker build` ок, web: `npm install`, `biome`, `tsc`, `rsbuild build` ок. DoD-прогон: `make up → migrate → seed-dev → /api/v1/me` (KA_RF 10 прав, ADMIN 7, KA_GO 11; без токена 401; при `DEBUG=false` dev-маршрут 404) и e2e обмен через NATS (published → ack → done + `s3_object`, повтор результата без изменений). OpenAPI покрыт тестом `test_full_app_me_dev_token_and_openapi`. `openspec validate --strict` — valid. ## Rulings I made Полный список — `openspec/changes/slice-0-skeleton/design.md`, таблица «Decisions made without discussion» (№14–105: пред-проверка, рулинги по ходу, решения исполнителей, fix-wave). Ключевые: R-1 Makefile в корне, backend-цели через `cd api`; R-4 `required_document_rule` в `audit_ex.py`; R-7 обмен `recognition_job` не пишет `audit_event` в Срезе 0 (нет подходящего `audit_action`); R-9 `naming_convention` для ограничений; R-11 `PyJWT[crypto]` + `cryptography` в прод-requirements; R-15 `CREATE SCHEMA` только при отсутствии схемы; R-16 тесты в отдельной БД `armcore_pytest_<hex>`; R-17 email нормализуется (`lower`), >1 совпадения → 403; R-19 `.env.test` на порты compose; R-20 `MINIO_SECURE` (false для http); R-22 финальные статусы inbox {done, needs_check, error, timeout, dlq}; R-23 `web/package-lock.json` в `.gitignore` (решение о commit — за человеком). Трейлеры трёх коммитов исправлены до push. ## Deferred (в follow-up issue при закрытии) T1.2, T8.3 (`required_document_rule` NULLS NOT DISTINCT → DBML v5.14), T12.2, T13.5 (OQ-47), T14.1–T14.3, T15.4 (`--dev` без guard), T16.2 (`aud`/`iss`, параметры JWT РАИСА), T17.2 (аудит LOGIN_FAILED), T20.7, T22.2, T22.9/T24.9/I-6 (разнести быстрый inbox и маппер Среза 2), T23.1, T23.9, T24.3, T24.6, T26.2 (`npm audit` 6 high в шаблонных зависимостях), M-1, M-5, M-6 (`recognition_exchange.py` 786 строк), M-8, M-10, O-8. Предложения в docs (OQ, DBML v5.14, контракт) — `openspec/changes/slice-0-skeleton/open-questions-proposal.md`. ## Что нужно решить человеку Коммитить ли `web/package-lock.json`; 404 vs 405 под `/api/v1`; обязательный `aud` в проде; параметры JWT РАИСА. ## Связанные `RSHB/arm-ka-docs` ветка `feat/1-slice-0-skeleton` (контракт v1.0, compose) — PR после фазы 2; `RSHB/arm-tabular-parser`, `RSHB/arm-scan-gateway` — в работе. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 97 commits 2026-10-04 01:20:23 +03:00
docs/superpowers/plans/2026-10-03-slice-0-skeleton*.md сгенерирован 1:1 из openspec/changes/slice-0-skeleton/tasks.md
(заголовки «Task N» для скриптов subagent-driven-development, Global Constraints из спеки и CLAUDE.md).
.gitignore: .codegraph/, .superpowers/, .bounded, .DS_Store.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Без смены поведения: форматирование, сортировка импортов, удаление неиспользуемых
импортов, __all__ в src/db, datetime.UTC, HTTPStatus.NOT_FOUND вместо 404,
аннотация lvl в logger, noqa F401 на проверке доступности raisa_fastapi_protected_api,
mypy: miniopy_async не подтягивается (follow_imports=skip).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NATS_SEED/NATS_JWT необязательны (пусто — подключение без nkeys); в app.py
разбор seed защищён от пустого значения (подключение переедет в задаче 22).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Тест сверяет имена и упорядоченные значения с блоками Enum {} DBML через pydbml.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
16 таблиц: directories, access, accounting; схема dkb, детерминированные имена ограничений.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
files.py: s3_object, stored_file, document (NULLS NOT DISTINCT), recognition_job;
FK-цикл stored_file <-> recognition_job разорван через use_alter с явным именем.
osv.py: osv_entry. statements.py: financial_statement_meta/column/line/value.
Тест частей 01–03 не привязан к полному составу metadata.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ядро сверки перенесено из tests/dbml_parity.py в scripts/dbml_parity.py (единственная
реализация, R-14); tests/dbml_parity.py и dbml_helpers.py — тонкие обёртки с пропуском
теста без docs. Единый формат строк: `<объект>: ожидалось X / фактически Y`,
`<объект>: отсутствует в ORM|DBML`; тип и nullability — одной строкой в нотации DBML.
Версия из шапки DBML сверяется с DBML_VERSION до сравнения таблиц. Коды: 0/1/2.
Без рабочего .env скрипт берёт api/.env.example. Makefile: dbml-check (в check-full),
mypy проверяет scripts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
I-1: табличный UniqueConstraint ORM сверяется как уникальный индекс по колонкам
(флаг колонки unique=True — по-прежнему в свойствах колонки); любое CHECK в ORM —
«отсутствует в DBML».
I-2: у совпавших целей FK сравниваются ON DELETE / ON UPDATE (нет в DBML = NO ACTION),
DEFERRABLE и INITIALLY.
I-3: неизвестный класс типа отрисовывается repr с параметрами (VARCHAR(length=50),
Uuid(as_uuid=False)) и никогда не совпадает с именем типа DBML.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Схема dkb создаётся и фиксируется до context.configure (онлайн) и печатается первой в офлайн-SQL.
URL: -x dburl > sqlalchemy.url, заданный вызывающим кодом (не заглушка driver://) > настройки.
Async-движок из URL напрямую (без configparser-интерполяции %).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Явный DDL без импорта src.models: схема dkb, 27 enum-типов (явный CREATE/DROP TYPE), 43 таблицы с индексами
(partial unique, NULLS NOT DISTINCT), FK цикла stored_file -> recognition_job отдельным шагом; downgrade —
FK цикла, таблицы в обратном порядке, типы. Тесты (integration, scratch-БД, alembic подпроцессом):
upgrade -> downgrade -> upgrade, пустой compare_metadata и alembic check, каталог = create_all ORM,
поведение partial unique/NULLS NOT DISTINCT/FK-цикла; офлайн-SQL — CREATE SCHEMA первым.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PostgreSQL проверяет право CREATE на базу раньше IF NOT EXISTS: роль миграций, которой платформа выдала готовую
схему dkb без этого права, падала на CREATE SCHEMA IF NOT EXISTS. env.py (онлайн) проверяет pg_namespace и создаёт
схему только при отсутствии; офлайн-SQL и upgrade() ревизии 0001 — DO-блок с той же проверкой. Тест: схема заранее
во владении отдельной роли, CREATE на базу отозван у PUBLIC — upgrade/check/downgrade/upgrade проходят.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Двухфазный загрузчик: валидация yaml целиком до записи (SeedError), затем upsert по
натуральному ключу без удалений; run_seed(session, *, dev, seed_dir) без commit, CLI коммитит.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dev HS256 включается только при AUTH_DEV_ISSUER и DEBUG одновременно; РАИСА — RS256/ES256 по
публичному ключу или JWKS с кэшем. PyJWT[crypto] добавлен в requirements.txt как прод-зависимость.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
auth.py импортирует cryptography напрямую, а requirements.txt — полный freeze.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
I-1: в jwt.decode передаётся PyJWK (а не голый ключ), рассогласование alg и типа ключа даёт 401,
а не TypeError/500; страховка на (TypeError, ValueError).
I-2: cache_keys=False — кэш ключей по kid не имеет срока жизни и держал бы отозванный ключ;
остаётся кэш набора JWKS на 300 с.
R-17: Claims.email = strip().lower().
R-18: запредельный exp не даёт 500; пустой sub отвергается; kid из заголовка пишется в лог через repr и усекается.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Пользователь, филиал, роли и права читаются одним запросом; роль is_active=false прав не даёт;
email без учёта регистра, неоднозначность — 403 (R-17).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Без jsonable_encoder ValueError из field_validator давал 500 вместо 422;
HTTP_422_UNPROCESSABLE_ENTITY помечена устаревшей в starlette.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dev-эмитент подключается только при AUTH_DEV_ISSUER и DEBUG; статический тест
«нет кодов ролей» расширен на src/api/** и src/services/**.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
При выключенном dev-эмитенте POST /api/v1/dev/token отвечал 405 от StaticFiles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Назначение, предварительные требования (Python 3.11, uv, Docker), быстрый старт до зелёного /api/v1/me
(make venv/up/migrate/seed-dev, dev-токен), таблица env, команды make, тесты, проверка ORM и DBML, миграции,
раскладка репозитория, работа без Nexus, безопасность dev-режима, ссылки на соседние документы.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
I-1: предупреждение dev-эмитента появляется при первом запросе к /me, а не при старте. I-2: добавлен трейсбэк SMB
(заглушка). S3-трейсбэк помечен «до исправления в задаче 22». M-3: дождаться healthy перед make migrate.
M-5: таблица env с полными именами OPENBAO__MINIO__*, __NATS__*, __SMB__*. M-2: migration-cycle и check-full читают .env.test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- NatsConnection: supervisor первого подключения в фоне (старт приложения не ждёт NATS), nats-py
  переподключается сам (max_reconnect_attempts=-1, reconnect_time_wait=2) и восстанавливает подписки;
  экспоненциальный backoff 1→30 с, если connect завершился исключением; логирование ошибок без шума
  (первая ошибка серии — warning, дальше не чаще раза в минуту).
- publish(subject, payload, msg_id): заголовок Nats-Msg-Id + flush; нет соединения или нет подтверждения —
  NatsPublishError. subscribe_results(handler): queue group arm-core, исключения обработчика логируются.
- build_nats_auth перенесён из app.py; ошибка учётных данных — NatsConfigError, категория config_error,
  без повторов (T4.1). Первое подключение повторяется (T4.2): прежний await connect в lifespan при
  недоступном NATS висел бесконечно (nats-py с max_reconnect_attempts=-1 повторяет первое подключение сам).
- app.py: подключение переехало в core/nats.py (R-3), lifespan запускает/останавливает NatsConnection,
  подписка — заглушка services/recognition_exchange.handle_message (полная обработка — задача 24).
- Тесты: юнит (поддельный клиент, инжектируемый sleep, отсутствие JetStream в src/) и интеграционные
  против NATS без JetStream (заголовок, queue group без дублей, недоступный сервер).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MinioSettings.MINIO_SECURE (по умолчанию true — https, как в шаблоне и на платформе) передаётся в Minio(secure=...);
.env.example и .env.test — false (MinIO из docker compose слушает http). README: трейсбэк S3 из раздела
«что в логе» убран, переменная добавлена в таблицу конфигурации.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- services/recognition_exchange.py: publish_pending — одно задание на короткую транзакцию
  (SELECT … LIMIT 1 FOR UPDATE OF recognition_job SKIP LOCKED → публикация → UPDATE → COMMIT):
  queued → published (request_msg_id = <job_id>:<attempt>, request_subject, published_at, reply_deadline —
  5 мин по умолчанию, requested_at); published без ack дольше ACK_TIMEOUT → повтор с тем же Nats-Msg-Id и
  attempt, metrics.republish_count (jsonb ||, прочие ключи сохраняются); после OUTBOX_MAX_REPUBLISH → dlq/NO_ACK;
  запрос не собирается по контракту → error/INVALID_REQUEST; первая NatsPublishError откатывает задание и
  прерывает проход; нет соединения — проход пропускается. published с прошедшим reply_deadline оставлен вахтёру.
- expire_deadlines — один UPDATE processing/published с reply_deadline < now → timeout/TIMEOUT.
- run_publisher_loop / run_watchdog_loop (30 с), ExchangeTasks: запуск в lifespan вместе с NatsConnection,
  остановка до nats.stop(). audit_event не пишется (R-7).
- Тесты: интеграционные против PostgreSQL и NATS (схема запроса, повторы и dlq на инжектируемых часах, ack после
  первого повтора, вахтёр, ошибка публикации, два конкурентных прохода, циклы), lifespan; фабрика CommittedJobs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
handle_message(msg, *, session_factory, mapper, now): разбор конверта по контракту v1.0, одна короткая транзакция
с SELECT … FOR UPDATE (ждёт блокировку публикатора/вахтёра); ack published → processing; результат — дедуп по
result_msg_id, финальные статусы (R-22) не трогаются, регистрация s3_object (results), слияние метрик с сохранением
republish_count. Подписка app.py получает handler с фабрикой сессий и NoopMapper; шаблонный core/nats_callback.py
удалён (R-3). audit_event не пишется (R-7).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Цели docker-build, web-check, check-infra (подсказка «make up»); .env.test переведён на 5432/4222/9000 (R-19),
test_config и README обновлены. Цикл миграций входит в test-integration и отдельным шагом не дублируется.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLAUDE.md: цели make из корня, NATS_ENABLED vs DEBUG, обмен recognition_job, аудит, аутентификация, seed,
сверка ORM-DBML, тесты; Dockerfile для dev и check-full; skip reason у fixtures.
tasks.md/plan: required_document_rule в audit_ex.py (R-4); spec data-model: 43 таблицы модели + alembic_version.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Строка «Контракты» перечисляет три реальные схемы recognition-* и копии в api/contracts/; ex-payload-v1 помечен
как будущий (Срез 7). В раскладке — src/api/endpoints. В docstring app.py убрано утверждение, что middleware
РАИСА подключается при DEBUG=False (пакет лишь условно импортируется).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Все nullable jsonb-колонки ORM (audit_event.before_value/after_value,
ex_snapshot_item.payload, recognition_job.group_schema/metrics,
osv_entry.original_values) объявлены через nullable_jsonb() =
JSONB(none_as_null=True): Python None сохраняется как SQL NULL, IS NULL
находит строку. DDL не меняется; сверка с DBML и миграции зелёные.

Обходы для не-объекта в metrics (CASE по jsonb_typeof в публикаторе,
merge_metrics в inbox) оставлены: строки до исправления и сырой SQL
могут содержать jsonb null. Тесты: SQL NULL после AuditService.record
без before/after, metrics фабрики None -> SQL NULL, повтор публикации и
слияние метрик для SQL NULL и jsonb null, перечень колонок с флагом.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Обработчики IntegrityError (409) и SQLAlchemyError (500) молча отдавали
ответ. Теперь каждая такая ошибка — одна запись ERROR: путь и метод
запроса, классы ошибок SQLAlchemy и драйвера, SQLSTATE, SQL-текст с
плейсхолдерами (усечён до 200 символов) и стек вызовов. Текст исключения
в лог не идёт: str() ошибки asyncpg/SQLAlchemy содержит параметры запроса
и DETAIL PostgreSQL со значениями ключей (email, ИНН). Ответы прежние.

JwksVerifier.verify: TypeError/ValueError, превращаемые в AuthError,
теперь пишут warning в том же санированном виде repr(str(exc))[:200].

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- OPENBAO__SMB__* необязательны (по умолчанию пусто); при пустом SMB_HOST
  сессия SMB не регистрируется (DEBUG «SMB не настроен»), в .env.example
  и .env.test SMB выключен — в логе dev-запуска больше нет трейсбэка SMB.
- Задан SMB_HOST: register_session и reset_connection_cache вызываются
  через asyncio.to_thread и не блокируют цикл событий.
- Остановка: задачи обмена -> NATS -> S3 -> SMB -> engine.dispose();
  каждый шаг в своём try/except с логом step=..., ошибка одного шага не
  отменяет следующие. Пул соединений БД закрывается последним (M-3).
- README: «что в логе при запуске», таблица настроек SMB, порядок
  остановки.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
OUTBOX_POLL_SECONDS и ACK_TIMEOUT_SECONDS — PositiveInt, OUTBOX_MAX_REPUBLISH —
NonNegativeInt: 0 и отрицательные значения периода и таймаута, отрицательное
число повторов отвергаются при старте. Значения по умолчанию прежние.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
job_id из сообщения, не прошедшего контракт, — данные отправителя; без
экранирования перевод строки в нём позволял подделать строку лога.
Теперь repr(job_id[:64]), как в рулинге R-18 для kid токена.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Команды уровня кластера в тестах (создание и удаление временных БД, роль
в тесте предвыданной схемы) подключались к рабочей БД armka. Теперь —
к служебной БД postgres того же сервера (service_url): тестам не нужна
существующая armka. Проверено прогоном с несуществующей БД в
OPENBAO__DATABASE__DATABASE_URL.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
В api/.env.example задан SERVER__HOST=127.0.0.1 (и SERVER__PORT=8000):
при DEBUG и AUTH_DEV_ISSUER эндпоинт dev-токенов выдаёт токен любому, кто
знает email. Значение приложения по умолчанию (0.0.0.0) не меняется.
.env.test — те же ключи; новый тест сверяет набор ключей .env.example и
.env.test. README: рекомендация в быстром старте и строка таблицы.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Вывод loguru из негативных тестов больше не засоряет прогон: pytest
перехватывает stderr и показывает его только для упавших тестов (секция
«Captured stderr call»). Ни один тест не зависел от отключённого
перехвата; полный прогон зелёный.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tests/test_exchange_lifespan_nats.py (integration, NATS и PostgreSQL из
.env.test, уникальный subject результатов, queue group arm-core):
- настоящий lifespan при NATS_ENABLED=true: /healthcheck -> nats=ok,
  подписка видна в мониторинге NATS (/connz) с qgroup arm-core, публикатор
  lifespan переводит задание в published, inbox принимает ack и результат;
  остановка гасит задачи обмена до nats.stop(), закрывает пул БД и не
  оставляет висящих задач;
- два NatsConnection с настоящим handle_message: результат обрабатывает
  ровно один инстанс, задание и s3_object меняются один раз; повторная
  доставка того же сообщения ничего не меняет.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
openspec/changes/slice-0-skeleton/open-questions-proposal.md — носитель
15 предложенных вопросов, правок статусов OQ-46/OQ-47 и кандидатов в DBML
v5.14 для docs-PR (открывает владелец ../docs); архивируется вместе с
change. Добавлены пункты финального ревью: аудит LOGIN/LOGIN_FAILED,
CORS * и доверенные прокси, открытые /docs и /openapi.json, статус
stored_file из inbox, срок ответа tabular, append-only на уровне БД,
последовательный callback NATS и длинный маппер, проверка ключа
results/{file_id}/{job_id}/ до чтения результата.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
audit-trail: WHEN сценария «Системное событие» — «системный процесс (без
пользователя) вызывает сервис аудита»; NOTE: обмен через recognition_job
в Срезе 0 аудит не пишет (R-7, design #20). design.md: R-25, R-26 и
решения fix-волны финального ревью (№100–105). openspec validate --strict
проходит.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
aep merged commit adb9ba47dc into main 2026-10-04 18:40:48 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/arm-core#3
No description provided.