From da9b9f66cfe62c1e2cb5f850159fe18a19a677ed Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 12 Oct 2026 21:40:35 +0400 Subject: [PATCH 01/11] =?UTF-8?q?openspec:=20change=20nats-env-config=20?= =?UTF-8?q?=E2=80=94=20subject'=D1=8B=20=D0=B8=20JWT=20NATS=20=D0=BF=D0=BE?= =?UTF-8?q?=20=D0=BE=D0=BA=D1=80=D1=83=D0=B6=D0=B5=D0=BD=D0=B8=D1=8E=20?= =?UTF-8?q?=D0=B8=D0=B7=20OpenBao?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../changes/nats-env-config/.openspec.yaml | 2 + openspec/changes/nats-env-config/design.md | 79 +++++++++++++++ openspec/changes/nats-env-config/proposal.md | 60 ++++++++++++ .../specs/recognition-contract/spec.md | 96 +++++++++++++++++++ openspec/changes/nats-env-config/tasks.md | 26 +++++ 5 files changed, 263 insertions(+) create mode 100644 openspec/changes/nats-env-config/.openspec.yaml create mode 100644 openspec/changes/nats-env-config/design.md create mode 100644 openspec/changes/nats-env-config/proposal.md create mode 100644 openspec/changes/nats-env-config/specs/recognition-contract/spec.md create mode 100644 openspec/changes/nats-env-config/tasks.md diff --git a/openspec/changes/nats-env-config/.openspec.yaml b/openspec/changes/nats-env-config/.openspec.yaml new file mode 100644 index 0000000..b9ebfc7 --- /dev/null +++ b/openspec/changes/nats-env-config/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-12 diff --git a/openspec/changes/nats-env-config/design.md b/openspec/changes/nats-env-config/design.md new file mode 100644 index 0000000..fadc2ae --- /dev/null +++ b/openspec/changes/nats-env-config/design.md @@ -0,0 +1,79 @@ +## Context + +Решение владельца 12.10.2026: «топики можно выставлять через секреты и jwt токен тоже, т.к. у нас будут различные наборы +топиков и разные jwt для прод и тест окружения». Выбран подход 1 из трёх обсуждённых (12.10.2026): + +1. **Выбран:** три ключа в группе `OPENBAO__NATS__`, одинаковые в трёх сервисах. +2. `OPENBAO__APP__*` (`settings_sources.py`) — отвергнут: разносит JWT и subject'ы, на которые он даёт права, по разным + группам. +3. Один префикс `NATS_SUBJECT_PREFIX` — отвергнут: работает, только если тестовые subject'ы платформы отличаются от + боевых префиксом. + +Образец — `RSHB/armka_file_worker` (проверен в контуре): `NATSSettings` с `NATS_URL`, `NATS_JWT: SecretStr`, +`NATS_SEED: SecretStr`, `NATS_COMMAND_SUBJECT`, `NATS_EVENT_SUBJECT`; `Settings(hide_input_in_errors=True)`. + +| Ключ | Умолчание | arm-core | tabular-parser | scan-gateway | +|---|---|---|---|---| +| `OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT` | `files.tabular_uploaded` | публикует | подписан (queue `tabular-parser`) | — | +| `OPENBAO__NATS__NATS_IMAGE_REQUEST_SUBJECT` | `files.img_uploaded` | публикует | — | подписан (queue `scan-gateway`) | +| `OPENBAO__NATS__NATS_RESULT_SUBJECT` | `files.tabular_parsed` | подписан (queue `arm-core`) | публикует | публикует | + +## Goals / Non-Goals + +Goals: subject'ы и учётные данные NATS задаются секретами окружения; разъехавшиеся настройки видны в `/healthcheck`; +тесты не делят subject'ы со стендом. Non-goals: см. proposal. + +## Decisions + +1. **Ключи одинаковые во всех трёх сервисах, проверяются все три.** На окружение — один набор секретов, который + платформа кладёт всем сервисам; сервис использует только свои subject'ы, но валидирует все три, чтобы ошибка набора + проявилась в любом сервисе одинаково. +2. **Валидация при старте.** Имя subject'а — непустое, без пробельных символов, без шаблонов `*` и `>`, без пустых + токенов (`a..b`, ведущая или завершающая точка). Три имени попарно различны: совпадение запроса и ответа — сервис + получает собственные ответы; совпадение двух запросов — каждый запрос получают оба сервиса. Ошибка — сервис не + стартует (ошибка конфигурации, как у `DB_SCHEMA`). +3. **Константы `contract/subjects.py` — только умолчания.** Код публикации и подписок получает имена из настроек; + функция выбора subject'а по `pipeline` принимает настройки (форма — на усмотрение плана сервиса). +4. **Секреты.** `NATS_JWT` и `NATS_SEED` — `SecretStr`; в колбэки nkeys значение передаётся через + `get_secret_value()`; `Settings` с `hide_input_in_errors=True`, чтобы ошибка валидации конфигурации не печатала + входные значения. +5. **`/healthcheck`.** Новый объект `subjects` с subject'ами, которые использует сервис: arm-core — + `tabular_request`, `image_request`, `result`; tabular-parser — `tabular_request`, `result`; scan-gateway — + `image_request`, `result`. Отдаётся всегда, в том числе при `nats = off | down` (это конфигурация, не состояние). + Решение владельца 12.10.2026: «пусть будет в хэлзчеке». +6. **Лог при старте.** Сервис пишет свои subject'ы одной строкой при старте (существующая строка «Подписка NATS + активна» сохраняется). +7. **Тесты (OQ-83).** Интеграционные тесты `tabular-parser` и `scan-gateway`, где обе стороны обмена — в самом тесте, + задают уникальные subject'ы через настройки и не пересекаются с контейнерами стенда. e2e ядра (`make e2e`) остаётся + на subject'ах по умолчанию: он гоняет настоящий контейнер `tabular-parser` стенда, который слушает умолчания, а + конфликт там — в общей queue group `arm-core` на subject'е ответа; требование останавливать ядро стенда сохраняется + (R-7). Интеграционные тесты NATS ядра уже работают на уникальных subject'ах. + +## Risks / Trade-offs + +- Разъехавшиеся настройки ядра и сервиса — тишина на шине: ядро переведёт задания в `dlq` по `NO_ACK`. Смягчение: + `subjects` в `/healthcheck` и лог при старте; в `03-architecture.md` — требование задавать набор всем трём сервисам. +- JWT окружения без прав на заданные subject'ы — ошибка прав NATS при подписке/публикации (в логе сервиса); смягчение + — перечень секретов окружения в `03-architecture.md` и `contour-transfer-guide.md`. +- `/healthcheck` публичен и теперь раскрывает имена subject'ов. Имена не секретны (без JWT ими не воспользоваться), + JWT и seed в ответ не попадают. + +## Decisions made without discussion + +1. 12.10.2026 — **R-1:** имена ключей `NATS_TABULAR_REQUEST_SUBJECT`, `NATS_IMAGE_REQUEST_SUBJECT`, + `NATS_RESULT_SUBJECT` (по образцу FileWorker — префикс `NATS_`, суффикс `_SUBJECT`), а не `NATS_COMMAND_SUBJECT` / + `NATS_EVENT_SUBJECT` FileWorker: у нас два subject'а запроса. Имена показаны владельцу в дизайне и приняты вместе + с подходом. Цена ошибки: низкая — переименование ключа до выката в контур. +2. 12.10.2026 — **R-2:** `schema_version` не меняется: тела сообщений те же, subject'ы — окружение (как R-2 в + `nats-files-subjects`). Цена ошибки: низкая. +3. 12.10.2026 — **R-3:** проверяются все три subject'а в каждом сервисе, включая неиспользуемые им (решение 1). + Цена ошибки: низкая — лишний отказ старта при кривом наборе, который всё равно сломал бы обмен. +4. 12.10.2026 — **R-4:** `subjects` в `/healthcheck` отдаётся и при `nats = off`. Цена ошибки: низкая. +5. 12.10.2026 — **R-5:** пароли MinIO/SMB в `SecretStr` не переводятся — вне запроса владельца; `hide_input_in_errors` + всё равно скрывает их в ошибках валидации. Цена ошибки: пароли видны в `repr(settings)` до отдельной задачи. +6. 12.10.2026 — **R-6:** маршрут — OpenSpec change `nats-env-config` в каждом из четырёх репозиториев, один план SDD на + все четыре (больше пяти задач — SDD-контроллер). Цена ошибки: низкая, процессная. +7. 12.10.2026 — **R-7:** e2e ядра не переводится на свои subject'ы (решение 7): ему нужен контейнер `tabular-parser` + стенда на умолчаниях, а развести ядро теста и ядро стенда может только своя queue group, которая в этом change + не настраивается. OQ-83 для `make e2e` остаётся открытым. Цена ошибки: перед `make e2e` по-прежнему останавливать + ядро стенда. diff --git a/openspec/changes/nats-env-config/proposal.md b/openspec/changes/nats-env-config/proposal.md new file mode 100644 index 0000000..15042bd --- /dev/null +++ b/openspec/changes/nats-env-config/proposal.md @@ -0,0 +1,60 @@ +## Why + +Прод и тест будут работать с разными наборами subject'ов NATS и разными JWT. Платформа РАИСА передаёт в под только +переменные `OPENBAO__*`, поэтому окружение должно настраиваться только секретами OpenBao, без пересборки и релиза +сервисов. Сейчас JWT и seed уже приходят как `OPENBAO__NATS__NATS_JWT` / `NATS_SEED`, а subject'ы зашиты константами +`api/src/contract/subjects.py` трёх сервисов (решение R-4 в #74) и уже раз менялись (`recognition.*` → `files.*`, +08.10.2026). Решение владельца 12.10.2026: subject'ы и JWT задаются по окружению секретами группы `OPENBAO__NATS__` — +так устроен `RSHB/armka_file_worker`, проверенный в контуре (`NATS_COMMAND_SUBJECT`, `NATS_EVENT_SUBJECT` рядом с +`NATS_JWT`). + +Срез: вне плана срезов `09-delivery-plan.md` — подготовка к контуру (инфраструктура NATS платформы). Issue +RSHB/arm-ka-docs#80. + +## What Changes + +- Три настройки окружения, одинаковые ключи во всех трёх сервисах, умолчания — текущие имена: + `OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT` (`files.tabular_uploaded`), + `OPENBAO__NATS__NATS_IMAGE_REQUEST_SUBJECT` (`files.img_uploaded`), + `OPENBAO__NATS__NATS_RESULT_SUBJECT` (`files.tabular_parsed`). +- Контракт: subject'ы — конфигурация окружения; у ядра и сервисов одного окружения значения MUST совпадать; имена + проверяются при старте (непустые, без пробелов и шаблонов `*`/`>`, три разных). +- `NATS_JWT` / `NATS_SEED` — секреты: значения не попадают в `repr`, логи и тексты ошибок валидации конфигурации. +- Каждый сервис пишет свои subject'ы в лог при старте и отдаёт их в `/healthcheck` (объект `subjects`). +- Интеграционные тесты `tabular-parser` и `scan-gateway` берут собственные subject'ы через настройки и не делят их с + контейнерами стенда (OQ-83 в части этих тестов; e2e ядра — без изменений, design R-7). +- Документы: `context/03-architecture.md` (NATS платформы, перечень секретов), `context/07-open-questions.md` (OQ-83), + `dev/README.md`, `dev/contour-transfer-guide.md`, `dev/qa-guide.md`, `dev/frontend-api-guide.md` (ответ + `/healthcheck`, если описан), `dev/.env.example`. + +Читают и пишут контракт: `arm-core` (публикует запросы в оба subject'а запроса, подписан на subject ответа), +`tabular-parser` (подписан на subject запроса `tabular`, публикует в subject ответа), `scan-gateway` (подписан на +subject запроса `image`, публикует в subject ответа). Дочерние change'и `nats-env-config`: RSHB/arm-core#106, +RSHB/arm-tabular-parser#31, RSHB/arm-scan-gateway#32. + +## Non-goals + +- `schema_version` контракта не меняется: тела сообщений, `Nats-Msg-Id`, файл результата — те же. +- Queue group'ы (`arm-core`, `tabular-parser`, `scan-gateway`) остаются константами. +- Механизм аутентификации NATS не меняется (nkeys seed + JWT, оба пусты — без учётных данных). +- Пароли MinIO и SMB в `SecretStr` не переводятся. +- Subject'ы не настраиваются через `OPENBAO__APP__*`: это группа `NATS`, а не настройки верхнего уровня. +- DBML не поднимается; заметки v5.17 про subject'ы — follow-up RSHB/arm-ka-docs#79 (R-3). +- Отдельный NATS для тестов не поднимается. + +## Capabilities + +### New Capabilities + +### Modified Capabilities +- `recognition-contract`: subject'ы запроса и ответа — настройки окружения с умолчаниями `files.*`. + +## Impact + +- Сервисы: `arm-core`, `tabular-parser`, `scan-gateway` — `core/config.py` (`NATSSettings`), `contract/subjects.py`, + `core/nats.py`, публикация и подписки, `/healthcheck`, тесты, `.env.example`, спеки, README. +- Выкат обратно совместим: без новых ключей всё работает на `files.*`. Для теста платформа кладёт свои `NATS_JWT`, + `NATS_SEED` и три subject'а одинаково всем трём сервисам; JWT окружения должен давать права publish/subscribe на эти + subject'ы. +- Ядро вычисляет subject при каждой публикации по `pipeline`, поэтому после смены настроек повторная публикация + заданий в работе уходит в новый subject; `recognition_job.request_subject` записывает фактический subject. diff --git a/openspec/changes/nats-env-config/specs/recognition-contract/spec.md b/openspec/changes/nats-env-config/specs/recognition-contract/spec.md new file mode 100644 index 0000000..3205397 --- /dev/null +++ b/openspec/changes/nats-env-config/specs/recognition-contract/spec.md @@ -0,0 +1,96 @@ +## ADDED Requirements + +### Requirement: Subject'ы NATS — настройки окружения +Subject'ы обмена SHALL задаваться секретами группы `OPENBAO__NATS__` с одинаковыми ключами во всех трёх сервисах: +`NATS_TABULAR_REQUEST_SUBJECT` (по умолчанию `files.tabular_uploaded`), `NATS_IMAGE_REQUEST_SUBJECT` (по умолчанию +`files.img_uploaded`), `NATS_RESULT_SUBJECT` (по умолчанию `files.tabular_parsed`). У ядра и сервисов одного окружения +значения MUST совпадать. Каждый сервис SHALL при старте проверять все три имени: непустое, без пробельных символов, без +шаблонов `*` и `>`, без пустых токенов, три имени попарно различны; при нарушении сервис MUST NOT стартовать. +Учётные данные NATS (`OPENBAO__NATS__NATS_JWT`, `OPENBAO__NATS__NATS_SEED`) SHALL задаваться по окружению теми же +секретами, и их значения MUST NOT попадать в логи, `repr` настроек и тексты ошибок валидации конфигурации. Каждый +сервис SHALL писать используемые subject'ы в лог при старте и отдавать их в `/healthcheck` объектом `subjects` +(ключи `tabular_request`, `image_request`, `result` — только те, что использует сервис) независимо от состояния NATS. +Queue group'ы и `schema_version` от настроек subject'ов не зависят. + +#### Scenario: Умолчания +- **WHEN** сервис стартует без ключей `OPENBAO__NATS__NATS_*_SUBJECT` +- **THEN** обмен идёт по `files.tabular_uploaded`, `files.img_uploaded`, `files.tabular_parsed` + +#### Scenario: Набор тестового окружения +- **WHEN** всем трём сервисам заданы одинаковые `OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT`, + `NATS_IMAGE_REQUEST_SUBJECT`, `NATS_RESULT_SUBJECT`, `NATS_JWT`, `NATS_SEED` тестового окружения +- **THEN** ядро публикует запросы и получает ack и результаты только по этим subject'ам; subject'ы по умолчанию не + используются, `recognition_job.request_subject` содержит subject тестового окружения + +#### Scenario: Недопустимый набор +- **WHEN** `NATS_RESULT_SUBJECT` совпадает с одним из subject'ов запроса, или имя содержит `*`, `>` или пробел +- **THEN** сервис не стартует с ошибкой конфигурации, называющей ключ + +#### Scenario: Разъехавшиеся настройки видны +- **WHEN** у ядра и сервиса заданы разные subject'ы ответа +- **THEN** `/healthcheck.subjects.result` ядра и сервиса различаются; JWT и seed в ответе `/healthcheck` отсутствуют + +#### Scenario: Ошибка конфигурации не раскрывает JWT +- **WHEN** сервис не стартует из-за ошибки валидации настроек при заданных `NATS_JWT` и `NATS_SEED` +- **THEN** текст ошибки и лог не содержат значений JWT и seed + +## MODIFIED Requirements + +### Requirement: Сообщение запроса распознавания +Тело сообщения на subject запроса `tabular` или `image` (настройки окружения `NATS_TABULAR_REQUEST_SUBJECT` / +`NATS_IMAGE_REQUEST_SUBJECT`, по умолчанию `files.tabular_uploaded` / `files.img_uploaded`) SHALL соответствовать +`docs/contracts/recognition-request.schema.json` v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id` (uuid = +`recognition_job.id`), `file_id` (uuid = `stored_file.id`), `pipeline` (`tabular` | `image`), `file_type` +(`XLS` | `XLSX` | `XLSB` | `PDF` | `PNG` | `JPG`), `original_name`, `s3 {bucket, key}` исходника в роли `uploads`, +`reply_deadline` (date-time, UTC), `attempt` (≥ 1). Subject SHALL соответствовать `pipeline`. + +#### Scenario: Публикация запроса ядром +- **WHEN** ядро переводит `recognition_job` из `queued` в `published` +- **THEN** в subject, соответствующий `recognition_job.pipeline`, публикуется сообщение, проходящее схему запроса, с + заголовком `Nats-Msg-Id` = `:` + +#### Scenario: Сообщение не по схеме +- **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`), но из тела извлекаются + корректные `job_id` и `file_id` +- **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` **без предшествующего ack + `processing`** и не падает; `attempt` для `Nats-Msg-Id` результата берётся из тела запроса, при его отсутствии + реализации расходятся (`tabular-parser` берёт `attempt` из заголовка `Nats-Msg-Id` запроса или не отвечает, + `scan-gateway` подставляет 1) — см. OQ-71 + +#### Scenario: Нечитаемый запрос +- **WHEN** сервис получает сообщение, из которого нельзя извлечь `job_id` (не JSON, не объект, `job_id` отсутствует или + не uuid), а значит нельзя собрать валидный конверт +- **THEN** сервис ответа не публикует: запрос записывается в лог и отбрасывается; ядро не получит ack, повторит + публикацию `OUTBOX_MAX_REPUBLISH` раз и переведёт задание в `dlq` (`error_code = NO_ACK`) + +#### Scenario: Невалидный повтор запроса, уже находящегося в работе +- **WHEN** сервис получает запрос, не проходящий схему, но с `job_id`, который уже в работе или обработан этим процессом +- **THEN** конверт `SCHEMA_MISMATCH` не публикуется (его `Nats-Msg-Id` `result::` совпал бы с настоящим + результатом, и ядро отбросило бы настоящий как дубликат); запрос записывается в лог и отбрасывается + +#### Scenario: Примеры контракта — иллюстрации +- **WHEN** запрос берётся из `docs/contracts/examples/request-*.json` (в `request-image.json` `reply_deadline` лежит в + прошлом) +- **THEN** он проходит схему: схема проверяет для `reply_deadline` лишь формат `date-time`, но не сравнивает срок с + текущим временем; пример — иллюстрация формы сообщения, а не готовое к исполнению задание; проверка срока — дело + сервиса (`scan-gateway` отвечает `error TIMEOUT`, если `reply_deadline` истёк до отправки файла в ИС Сканирование) + +### Requirement: Сообщение результата распознавания +Тело сообщения на subject ответа (настройка окружения `NATS_RESULT_SUBJECT`, по умолчанию `files.tabular_parsed`) +SHALL соответствовать `docs/contracts/recognition-result-message.schema.json` +v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `file_id`, `status` (`processing` | `done` | `needs_check` | +`error`), `service {name, version}`; при `status ∈ {done, needs_check}` обязательны `result_s3 {bucket, key}` и +`result_schema_version`; при `status = error` обязателен `error {code, message}`; опциональны `raw_s3_key` +(только `scan-gateway`) и `metrics`. Поле `group_schema` в конверт NOT включается — источник `recognition_job.group_schema` +— файл результата. + +#### Scenario: Успешный результат +- **WHEN** сервис завершил обработку файла +- **THEN** файл результата записан в S3 по ключу `results/{file_id}/{job_id}/result.json`, а затем опубликован конверт + со `status = done | needs_check`, `result_s3` указывает на этот файл, заголовок `Nats-Msg-Id` = `result::` + +#### Scenario: Ошибка обработки +- **WHEN** сервис не смог обработать файл (нечитаем, таймаут внешней системы, тип не поддерживается) +- **THEN** публикуется конверт со `status = error` и `error.code` из перечня схемы (`UNSUPPORTED_FORMAT`, `UNREADABLE`, + `TIMEOUT`, `SCHEMA_MISMATCH`, `NOT_IMPLEMENTED`, `FILE_READ_ERROR`, `INVALID_FORMAT`, `INVALID_DOCUMENT`, + `RECOGNITION_FAILED`, `UNKNOWN_ERROR`); `result_s3` может отсутствовать diff --git a/openspec/changes/nats-env-config/tasks.md b/openspec/changes/nats-env-config/tasks.md new file mode 100644 index 0000000..4f8d767 --- /dev/null +++ b/openspec/changes/nats-env-config/tasks.md @@ -0,0 +1,26 @@ +## 1. Контракт (docs) + +- [ ] 1.1 Дельта спеки `recognition-contract` (новое требование «Subject'ы NATS — настройки окружения», два изменённых); + Purpose спеки при архивации: subject'ы — настройки с умолчаниями `files.*` +- [ ] 1.2 `contracts/recognition-request.schema.json`, `contracts/recognition-result-message.schema.json`: в описаниях + subject'ов — «настройка окружения, по умолчанию …» (только `description`, `schema_version` не меняется; копии в + сервисах обновляются побайтно) + +## 2. Сервисы (дочерние change'и `nats-env-config`) + +- [ ] 2.1 RSHB/arm-core#106: `NATSSettings` (3 subject'а, `SecretStr`, валидация), `hide_input_in_errors`, публикация и + подписка по настройкам, `/healthcheck.subjects`, лог при старте, unit-тесты (e2e — на умолчаниях, design R-7), `.env.example`, спека `recognition-job-exchange`, README +- [ ] 2.2 RSHB/arm-tabular-parser#31: то же для подписки `tabular_request` и публикации `result`, спека + `tabular-recognition-service` +- [ ] 2.3 RSHB/arm-scan-gateway#32: то же для подписки `image_request` и публикации `result`, спека + `image-recognition-gateway` + +## 3. Документы контекста + +- [ ] 3.1 `context/03-architecture.md`: subject'ы — настройки окружения (таблица потоков, NATS платформы), перечень + секретов `OPENBAO__NATS__*` по сервисам, требование одинакового набора для всех трёх сервисов +- [ ] 3.2 `dev/README.md` (таблица переменных, `/healthcheck`, интеграционные тесты), `dev/.env.example`, + `dev/contour-transfer-guide.md` (секреты окружения), `dev/qa-guide.md` и `dev/frontend-api-guide.md` (ответ + `/healthcheck`, если описан), `repo-templates/*.CLAUDE.md` при упоминании констант subject'ов +- [ ] 3.3 `context/07-open-questions.md`: OQ-83 — интеграционные тесты `tabular-parser`/`scan-gateway` на своих + subject'ах; для e2e ядра вопрос остаётся (общая queue group `arm-core`, design R-7) -- 2.47.2 From 2f7b65e0a1c49660d9f5de51f8602f63b8ca7119 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 10:36:47 +0400 Subject: [PATCH 02/11] =?UTF-8?q?=D0=9F=D0=BB=D0=B0=D0=BD=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20nats-env-con?= =?UTF-8?q?fig=20(#80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-13-nats-env-config.md | 781 ++++++++++++++++++ 1 file changed, 781 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-13-nats-env-config.md diff --git a/docs/superpowers/plans/2026-10-13-nats-env-config.md b/docs/superpowers/plans/2026-10-13-nats-env-config.md new file mode 100644 index 0000000..dc8fea8 --- /dev/null +++ b/docs/superpowers/plans/2026-10-13-nats-env-config.md @@ -0,0 +1,781 @@ +# Subject'ы и JWT NATS по окружению (OpenBao) — план реализации + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** subject'ы обмена NATS и учётные данные NATS задаются секретами `OPENBAO__NATS__*` по окружению (прод/тест) во +всех трёх сервисах; subject'ы видны в логе старта и в `/healthcheck`. + +**Architecture:** в `NATSSettings` каждого сервиса — три поля subject'ов с умолчаниями из констант +`contract/subjects.py`, валидация при старте, `SecretStr` для JWT/seed, `hide_input_in_errors` во всех классах настроек. +Код публикации и подписок берёт имена из настроек; функция `used_subjects(nats_settings)` в `core/nats.py` каждого +сервиса — единый источник для лога и `/healthcheck.subjects`. Контракт (`schema_version`) не меняется. + +**Tech Stack:** Python 3.11, pydantic v2 / pydantic-settings, nats-py (core NATS), FastAPI, pytest. + +**Spec:** `docs/openspec/changes/nats-env-config/` (proposal, design, `specs/recognition-contract/spec.md`) и дочерние +change'и `nats-env-config` в `arm-core/`, `tabular-parser/`, `scan-gateway/` (`openspec/changes/nats-env-config/`). + +## Рабочее место + +Workspace-worktree: `/Users/andrey.epifancev/projects/awp-ca/.wt/nats-env/` (дальше `$W`). + +| Репозиторий | Путь | Ветка | Issue | +|---|---|---|---| +| docs | `$W/docs` | `feat/80-nats-env-config` | RSHB/arm-ka-docs#80 | +| arm-core | `$W/arm-core` | `feat/106-nats-env-config` | RSHB/arm-core#106 | +| tabular-parser | `$W/tabular-parser` | `feat/31-nats-env-config` | RSHB/arm-tabular-parser#31 | +| scan-gateway | `$W/scan-gateway` | `feat/32-nats-env-config` | RSHB/arm-scan-gateway#32 | + +- git — только `git -C <абсолютный путь worktree>`; никогда не `cd` в основные checkout'ы `awp-ca/` (они на `main`, + хук guard-main заблокирует всю сессию). +- В каждом сервисном worktree перед первой задачей: `make -C $W/ venv` (uv, Python 3.11). +- Перед изменением кода — `codegraph_explore` с `projectPath` = абсолютный путь worktree сервиса. +- Коммиты на русском, с трейлером `Co-Authored-By: Claude Opus 5.5 `. +- Инфраструктура для интеграционных тестов — compose-проект `arm-ka` (`make -C $W/docs/dev up` или уже поднятый стенд). + +## Global Constraints + +- Ключи (одинаковые в трёх сервисах): `OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT` (умолчание `files.tabular_uploaded`), + `OPENBAO__NATS__NATS_IMAGE_REQUEST_SUBJECT` (умолчание `files.img_uploaded`), `OPENBAO__NATS__NATS_RESULT_SUBJECT` + (умолчание `files.tabular_parsed`). +- Каждый сервис проверяет все три имени: непустое, без пробельных символов, без `*` и `>`, без пустых токенов + (`a..b`, ведущая/завершающая точка); три имени попарно различны; нарушение — сервис не стартует, ошибка называет ключ. +- `NATS_JWT`, `NATS_SEED` — `SecretStr`; значения не попадают в логи, `repr` настроек и тексты ошибок валидации. +- `/healthcheck` — новый объект `subjects`: arm-core `{tabular_request, image_request, result}`, tabular-parser + `{tabular_request, result}`, scan-gateway `{image_request, result}`; отдаётся при любом состоянии NATS. +- Queue group'ы (`arm-core`, `tabular-parser`, `scan-gateway`) — константы, не меняются. +- `schema_version` контракта не меняется; копии схем в сервисах — побайтно равны `docs/contracts/`. +- `contract/subjects.py` — одинаковый модуль в трёх репозиториях: менять одинаково. +- Пароли MinIO/SMB в `SecretStr` не переводить (design R-5). +- e2e ядра (`tests/e2e/`) остаётся на subject'ах по умолчанию (design R-7). +- Язык комментариев, docstring'ов, сообщений об ошибках — русский; идентификаторы — английские. + +## Review Focus + +1. **Утечка JWT через ошибку «группа не полная».** Задан `OPENBAO__NATS__NATS_JWT`, но нет `NATS_URL` (или нет группы + `MINIO`) → pydantic печатает `input_value` всей группы. Ожидание: ни JWT, ни seed в `str(ValidationError)`. + Тест — в задачах 2, 4, 6 (`test_validation_error_hides_nats_secrets`). +2. **Пробелы по краям значения из OpenBao** (`"files.x "` — частая ошибка копирования в секрет). Ожидание: отказ старта + с именем ключа, а не тихая подписка на несуществующий subject. Тест — параметр `"files.tabular_uploaded "` в + `test_invalid_subject_rejected` (задачи 2, 4, 6). +3. **Совпадение subject'а ответа с subject'ом запроса** — сервис получал бы собственные ответы. Ожидание: отказ старта, + в сообщении оба ключа. Тест — `test_equal_subjects_rejected` (задачи 2, 4, 6). +4. **Ядро после смены секретов повторно публикует задание в новый subject**, а `recognition_job.request_subject` + отражает фактический subject публикации. Тест — `test_publish_uses_configured_request_subject` (задача 3). +5. **`/healthcheck` при выключенном или упавшем NATS** всё равно показывает `subjects` и не показывает JWT. Тест — + `test_healthcheck_subjects_*` (задачи 3, 5, 7). + +--- + +## Раздел A. Общий код настроек NATS (одинаковый в трёх сервисах) + +Задачи 2, 4, 6 вставляют этот код дословно (отличаются только окружающие файлы). + +**A.1. Импорты в начало `api/src/core/config.py`** (добавить к существующим; `re` — если ещё нет): + +```python +import re + +from pydantic import SecretStr, ValidationInfo, field_validator, model_validator + +from src.contract.subjects import IMAGE_REQUEST_SUBJECT, RESULT_SUBJECT, TABULAR_REQUEST_SUBJECT +``` + +**A.2. Регулярное выражение** (на уровне модуля, рядом с прочими константами модуля): + +```python +# Subject NATS без шаблонов: непустые токены через точку, без пробельных символов, `*` и `>` (docs nats-env-config) +NATS_SUBJECT_RE = re.compile(r"[^\s.*>]+(?:\.[^\s.*>]+)*") +NATS_SUBJECT_FIELDS = ("NATS_TABULAR_REQUEST_SUBJECT", "NATS_IMAGE_REQUEST_SUBJECT", "NATS_RESULT_SUBJECT") +``` + +**A.3. Класс `NATSSettings`** (заменяет существующий целиком): + +```python +class NATSSettings(BaseSettings): + """Подключение и subject'ы NATS — секреты окружения `OPENBAO__NATS__*` (прод и тест — свои наборы). + + Пустые `NATS_SEED` и `NATS_JWT` — подключение без nkeys (локальная разработка, тесты). Subject'ы по умолчанию — + имена контракта v1.0; у ядра и сервисов одного окружения они обязаны совпадать (docs, change nats-env-config). + """ + + model_config = SettingsConfigDict(hide_input_in_errors=True) + + NATS_URL: str + NATS_SEED: SecretStr = SecretStr("") + NATS_JWT: SecretStr = SecretStr("") + NATS_TABULAR_REQUEST_SUBJECT: str = TABULAR_REQUEST_SUBJECT + NATS_IMAGE_REQUEST_SUBJECT: str = IMAGE_REQUEST_SUBJECT + NATS_RESULT_SUBJECT: str = RESULT_SUBJECT + + @field_validator(*NATS_SUBJECT_FIELDS) + @classmethod + def _check_subject(cls, value: str, info: ValidationInfo) -> str: + """Конкретный subject без шаблонов: иначе подписка молча слушала бы чужой трафик или ничего.""" + if not NATS_SUBJECT_RE.fullmatch(value): + raise ValueError( + f"{info.field_name}={value!r}: нужен subject NATS из непустых токенов через точку, " + "без пробелов, '*' и '>'" + ) + return value + + @model_validator(mode="after") + def _check_distinct_subjects(self) -> "NATSSettings": + """Три subject'а попарно различны: совпадение запроса и ответа — сервис получает свои ответы, двух запросов — + каждый запрос получают оба сервиса.""" + seen: dict[str, str] = {} + for field in NATS_SUBJECT_FIELDS: + subject = getattr(self, field) + if subject in seen: + raise ValueError(f"{seen[subject]} и {field} совпадают ({subject!r}): subject'ы NATS должны различаться") + seen[subject] = field + return self + + def request_subjects(self) -> dict[str, str]: + """Subject запроса по `pipeline` (`tabular`, `image`) — аргумент `request_subject` контракта.""" + return {"tabular": self.NATS_TABULAR_REQUEST_SUBJECT, "image": self.NATS_IMAGE_REQUEST_SUBJECT} +``` + +**A.4. `hide_input_in_errors=True`** — в `model_config` **каждого** класса настроек `config.py` (`MinioSettings`, +`OpenBAOSettings`, `DBSettings`/`SMBSettings`/`ServerSettings` — какие есть, и `Settings`). У классов без +`model_config` добавить `model_config = SettingsConfigDict(hide_input_in_errors=True)`; у `Settings` — дописать ключ в +существующий `SettingsConfigDict(...)`. Причина: ошибка «нет поля» печатает `input_value` всей группы, а в ней JWT. + +**A.5. `contract/subjects.py` — `request_subject` с картой subject'ов** (заменить функцию; docstring модуля не трогать): + +```python +def request_subject(pipeline: str, subjects: Mapping[str, str] = REQUEST_SUBJECTS) -> str: + """Subject запроса по pipeline. `subjects` — карта `pipeline → subject` окружения + (`NATSSettings.request_subjects()`); по умолчанию — имена контракта: `tabular` → `files.tabular_uploaded`, + `image` → `files.img_uploaded`.""" + try: + return subjects[pipeline] + except KeyError: + raise ValueError(f"неизвестный pipeline {pipeline!r}: допустимы {', '.join(get_args(Pipeline))}") from None +``` + +**A.6. Колбэки nkeys** — в `build_nats_auth` (`api/src/core/nats.py`) читать значения через `get_secret_value()`: +`seed = settings.NATS_SEED.get_secret_value()` и `jwt = settings.NATS_JWT.get_secret_value()` (в scan-gateway +сохранить существующий `.strip()`: `settings.NATS_SEED.get_secret_value().strip()`). Остальная логика функции не меняется. + +**A.7. Общие тесты настроек** — добавить в тест-файл настроек сервиса (имя файла и фабрика настроек — в задаче). +Фабрика `make(**env)` в каждом репозитории своя; в коде ниже она называется `settings_from_env`: + +```python +JWT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.secret-payload.sig" +SEED = "SUACSSL3UAHUDXKFSNVUZRF5UHPMWZ6BFDTJ7M6USDXIEDNPPQYYYCU3VY" + + +def test_nats_subjects_default_to_contract_names(settings_from_env) -> None: + nats = settings_from_env().OPENBAO.NATS + assert nats.NATS_TABULAR_REQUEST_SUBJECT == "files.tabular_uploaded" + assert nats.NATS_IMAGE_REQUEST_SUBJECT == "files.img_uploaded" + assert nats.NATS_RESULT_SUBJECT == "files.tabular_parsed" + assert nats.request_subjects() == {"tabular": "files.tabular_uploaded", "image": "files.img_uploaded"} + + +def test_nats_subjects_from_openbao(settings_from_env) -> None: + nats = settings_from_env( + OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT="test.files.tabular_uploaded", + OPENBAO__NATS__NATS_IMAGE_REQUEST_SUBJECT="test.files.img_uploaded", + OPENBAO__NATS__NATS_RESULT_SUBJECT="test.files.tabular_parsed", + ).OPENBAO.NATS + assert nats.request_subjects() == {"tabular": "test.files.tabular_uploaded", "image": "test.files.img_uploaded"} + assert nats.NATS_RESULT_SUBJECT == "test.files.tabular_parsed" + + +@pytest.mark.parametrize( + "value", ["", "files.*", "files.>", ">", "files..tabular", ".files", "files.", "files tabular", "files.tabular_uploaded "] +) +def test_invalid_subject_rejected(settings_from_env, value: str) -> None: + with pytest.raises(ValidationError, match="NATS_RESULT_SUBJECT"): + settings_from_env(OPENBAO__NATS__NATS_RESULT_SUBJECT=value) + + +def test_equal_subjects_rejected(settings_from_env) -> None: + with pytest.raises(ValidationError, match="NATS_TABULAR_REQUEST_SUBJECT и NATS_RESULT_SUBJECT совпадают"): + settings_from_env(OPENBAO__NATS__NATS_RESULT_SUBJECT="files.tabular_uploaded") + + +def test_nats_secrets_hidden_in_repr(settings_from_env) -> None: + settings = settings_from_env(OPENBAO__NATS__NATS_JWT=JWT, OPENBAO__NATS__NATS_SEED=SEED) + assert settings.OPENBAO.NATS.NATS_JWT.get_secret_value() == JWT + assert JWT not in repr(settings) and SEED not in repr(settings) + assert JWT not in str(settings) and SEED not in str(settings) + + +MINIO_KEYS = ("MINIO_URL", "MINIO_ADMIN_LOGIN", "MINIO_ADMIN_PASSWORD", "MINIO_BUCKET", "MINIO_SECURE") + + +@pytest.mark.parametrize( + "missing", + [ + ("OPENBAO__NATS__NATS_URL",), # нет поля в группе NATS: в ошибке — вход группы NATS (с JWT) + tuple(f"OPENBAO__MINIO__{key}" for key in MINIO_KEYS), # нет группы MINIO: в ошибке — вход всего OPENBAO + ], +) +def test_validation_error_hides_nats_secrets(settings_from_env, missing: tuple[str, ...]) -> None: + """Ошибка «нет поля» печатает вход всей группы — JWT и seed туда попадать не должны (Review Focus 1).""" + with pytest.raises(ValidationError) as exc_info: + settings_from_env(OPENBAO__NATS__NATS_JWT=JWT, OPENBAO__NATS__NATS_SEED=SEED, **dict.fromkeys(missing)) + assert JWT not in str(exc_info.value) + assert SEED not in str(exc_info.value) +``` + +Проверка теста на честность (обязательна): временно убрать `hide_input_in_errors` из всех классов — оба параметра +должны упасть (JWT виден в тексте ошибки). Если какой-то параметр проходит и без скрытия — тест не ловит утечку, +доработать параметр под фактическое поведение pydantic, а не ослаблять проверку. + +Семантика фабрики `settings_from_env(**env)`: собирает `Settings(_env_file=None)` из минимального набора обязательных +`OPENBAO__*` сервиса, поверх — `env`; значение `None` означает «удалить переменную». Фабрика строится на `monkeypatch` +и существующих фикстурах тест-файла (см. задачу). + +--- + +### Task 1: docs — описания subject'ов в схемах контракта + +**Model:** haiku (готовый текст). + +**Files:** +- Modify: `$W/docs/contracts/recognition-request.schema.json` (поле верхнего уровня `description`) +- Modify: `$W/docs/contracts/recognition-result-message.schema.json` (поле верхнего уровня `description`) + +**Interfaces:** +- Produces: новые байты двух схем — задачи 2, 4, 6 копируют их в сервисы (`api/contracts/`). + +- [ ] **Step 1: request.** В `description` заменить начало + `Тело сообщения на subject files.tabular_uploaded или files.img_uploaded; subject должен соответствовать полю pipeline (tabular → files.tabular_uploaded, image → files.img_uploaded).` + на + `Тело сообщения на subject запроса; subject — настройка окружения и должен соответствовать полю pipeline (tabular → NATS_TABULAR_REQUEST_SUBJECT, по умолчанию files.tabular_uploaded; image → NATS_IMAGE_REQUEST_SUBJECT, по умолчанию files.img_uploaded).` + Остальной текст `description`, `$id`, `schema_version` — без изменений. + +- [ ] **Step 2: result-message.** В `description` заменить `Тело сообщения на subject files.tabular_parsed.` на + `Тело сообщения на subject ответа (настройка окружения NATS_RESULT_SUBJECT, по умолчанию files.tabular_parsed).` + +- [ ] **Step 3: проверка.** Run: `make -C $W/docs check`. Expected: зелёный (JSON валиден, примеры проходят схемы). + +- [ ] **Step 4: Commit.** + +```bash +git -C $W/docs add contracts/recognition-request.schema.json contracts/recognition-result-message.schema.json +git -C $W/docs commit -m "contracts: subject'ы запроса и ответа — настройки окружения (nats-env-config)" +``` + +--- + +### Task 2: arm-core — настройки NATS, контракт, nkeys + +**Model:** haiku (код в разделе A). + +**Files:** +- Modify: `$W/arm-core/api/src/core/config.py` (A.1–A.4) +- Modify: `$W/arm-core/api/src/contract/subjects.py` (A.5) +- Modify: `$W/arm-core/api/src/core/nats.py` — `build_nats_auth` (A.6) +- Modify: `$W/arm-core/api/contracts/recognition-request.schema.json`, `recognition-result-message.schema.json` + (копия из `$W/docs/contracts/`, Task 1) +- Test: `$W/arm-core/api/tests/test_config.py`, `$W/arm-core/api/tests/test_contract.py`, + `$W/arm-core/api/tests/test_nats_core.py` + +**Interfaces:** +- Consumes: схемы из Task 1. +- Produces: `NATSSettings.NATS_TABULAR_REQUEST_SUBJECT/NATS_IMAGE_REQUEST_SUBJECT/NATS_RESULT_SUBJECT: str`, + `NATSSettings.request_subjects() -> dict[str, str]`, `NATS_SEED/NATS_JWT: SecretStr`, + `request_subject(pipeline: str, subjects: Mapping[str, str] = REQUEST_SUBJECTS) -> str`. + +- [ ] **Step 1: фабрика и тесты.** В `tests/test_config.py` добавить фикстуру на основе существующей `clean_env` и + словаря `required` из `test_defaults` (вынести словарь в константу модуля `REQUIRED_ENV`): + +```python +@pytest.fixture +def settings_from_env(clean_env: pytest.MonkeyPatch): + def make(**env: str | None) -> Settings: + for key, value in {**REQUIRED_ENV, **env}.items(): + if value is None: + clean_env.delenv(key, raising=False) + else: + clean_env.setenv(key, value) + return Settings(_env_file=None) # type: ignore[call-arg] + + return make +``` + + и тесты A.7 дословно. Существующие `assert s.OPENBAO.NATS.NATS_SEED == ""` / `NATS_JWT == ""` заменить на + `.get_secret_value() == ""`. В `tests/test_contract.py` добавить: + +```python +def test_request_subject_uses_environment_map() -> None: + subjects = {"tabular": "test.files.tabular_uploaded", "image": "test.files.img_uploaded"} + assert request_subject("tabular", subjects) == "test.files.tabular_uploaded" + assert request_subject("image", subjects) == "test.files.img_uploaded" +``` + + В `tests/test_nats_core.py` хелпер `NATSSettings(NATS_URL=..., NATS_SEED=seed, NATS_JWT=jwt)` оставить (pydantic + примет `str` в `SecretStr`). + +- [ ] **Step 2: тесты падают.** Run: `cd $W/arm-core/api && ../.venv/bin/pytest tests/test_config.py tests/test_contract.py -q` + Expected: FAIL (`NATS_RESULT_SUBJECT` нет, `request_subject` не принимает второй аргумент). + +- [ ] **Step 3: реализация.** Применить A.1–A.6 к файлам задачи; скопировать схемы: + `cp $W/docs/contracts/recognition-request.schema.json $W/docs/contracts/recognition-result-message.schema.json $W/arm-core/api/contracts/`. + +- [ ] **Step 4: тесты проходят.** Run: `make -C $W/arm-core lint test` Expected: PASS, ruff/mypy чистые. + +- [ ] **Step 5: Commit.** + +```bash +git -C $W/arm-core add api/src/core/config.py api/src/contract/subjects.py api/src/core/nats.py api/contracts api/tests +git -C $W/arm-core commit -m "NATS: subject'ы и JWT из OPENBAO__NATS__*, SecretStr, скрытие входа в ошибках (#106)" +``` + +--- + +### Task 3: arm-core — публикация, подписка, лог и `/healthcheck` по настройкам + +**Model:** sonnet (интеграция в существующий код). + +**Files:** +- Modify: `$W/arm-core/api/src/core/nats.py` — `NatsConnection.__init__`, новая `used_subjects` +- Modify: `$W/arm-core/api/src/services/recognition_exchange.py` — `_process_one`, `publish_pending`, + `run_publisher_loop`, `ExchangeTasks.start` +- Modify: `$W/arm-core/api/src/services/primary_files.py:483` — `request_subject` при создании задания +- Modify: `$W/arm-core/api/src/app.py` — `lifespan` (лог), `healthcheck`, docstring модуля +- Modify: `$W/arm-core/api/.env.example`, `$W/arm-core/README.md` (раздел переменных NATS / healthcheck — найти grep'ом + `NATS_JWT` и `healthcheck`) +- Test: `$W/arm-core/api/tests/test_nats_core.py`, `$W/arm-core/api/tests/test_recognition_exchange.py`, + `$W/arm-core/api/tests/test_app.py`, `$W/arm-core/api/tests/test_api_files_upload.py` + +**Interfaces:** +- Consumes: Task 2 (`NATSSettings`, `request_subjects()`, `request_subject(pipeline, subjects)`). +- Produces: `used_subjects(settings: NATSSettings) -> dict[str, str]` в `src/core/nats.py` (ключи + `tabular_request`, `image_request`, `result`); `publish_pending(..., request_subjects: Mapping[str, str] = REQUEST_SUBJECTS)`; + `NatsConnection(settings, *, result_subject: str | None = None, ...)` — `None` → `settings.NATS_RESULT_SUBJECT`. + +- [ ] **Step 1: тесты.** + + `tests/test_nats_core.py`: + +```python +def test_used_subjects_core() -> None: + nats = NATSSettings(NATS_URL="nats://x:4222", NATS_RESULT_SUBJECT="test.files.tabular_parsed") + assert used_subjects(nats) == { + "tabular_request": "files.tabular_uploaded", + "image_request": "files.img_uploaded", + "result": "test.files.tabular_parsed", + } + + +def test_connection_subscribes_to_configured_result_subject() -> None: + nats = NATSSettings(NATS_URL="nats://x:4222", NATS_RESULT_SUBJECT="test.files.tabular_parsed") + assert NatsConnection(nats).result_subject == "test.files.tabular_parsed" + assert NatsConnection(nats, result_subject="override").result_subject == "override" +``` + + `tests/test_recognition_exchange.py` — по образцу существующего теста первой публикации (строки ~235–262: фикстуры + `jobs`, `RequestSubscriber`, `publish_pending`), но `RequestSubscriber` подписывается на свои subject'ы: + добавить в `RequestSubscriber.__init__` параметр `subjects: tuple[str, ...] = REQUEST_SUBJECTS` и использовать его в + `start()`; новый тест: + +```python +async def test_publish_uses_configured_request_subject(jobs: CommittedJobs, nats_conn) -> None: + """Review Focus 4: subject — из настроек окружения, `request_subject` задания — фактический subject публикации.""" + run = uuid.uuid4().hex[:8] + subjects = {"tabular": f"test-{run}.files.tabular_uploaded", "image": f"test-{run}.files.img_uploaded"} + subscriber = RequestSubscriber(subjects=tuple(subjects.values())) + await subscriber.start() + try: + job_id = await jobs.create(pipeline=RecognitionPipeline.tabular) + await publish_pending(jobs.session_factory, nats_conn, ack_timeout=ACK, max_republish=MAX_REPUBLISH, + request_subjects=subjects) + (msg,) = await subscriber.wait_for(job_id, 1) + finally: + await subscriber.stop() + assert msg.subject == subjects["tabular"] + assert (await jobs.get(job_id)).request_subject == subjects["tabular"] +``` + + Имена фикстур (`jobs`, соединения NATS, метода создания задания) взять из существующих тестов файла — если они + называются иначе, использовать существующие, не создавая новых. + + `tests/test_app.py`: + +```python +async def test_healthcheck_subjects_when_nats_off(client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(app_module.settings, "NATS_ENABLED", False) + body = (await client.get("/healthcheck")).json() + assert body["subjects"] == { + "tabular_request": app_module.settings.OPENBAO.NATS.NATS_TABULAR_REQUEST_SUBJECT, + "image_request": app_module.settings.OPENBAO.NATS.NATS_IMAGE_REQUEST_SUBJECT, + "result": app_module.settings.OPENBAO.NATS.NATS_RESULT_SUBJECT, + } + assert "NATS_JWT" not in str(body) and "jwt" not in str(body).lower() +``` + + и такой же `test_healthcheck_subjects_when_nats_down` с `NATS_ENABLED=True` без соединения. + + `tests/test_api_files_upload.py` — тест создания задания с subject'ами окружения: подменить + `settings.OPENBAO.NATS` на `NATSSettings(..., NATS_TABULAR_REQUEST_SUBJECT="test.files.tabular_uploaded")` через + `monkeypatch.setattr` на объекте настроек, которым пользуется сервис загрузки в этом тест-файле, и проверить + `job.request_subject == "test.files.tabular_uploaded"` (по образцу существующей проверки на строке ~251). + +- [ ] **Step 2: тесты падают.** Run: `make -C $W/arm-core test` и + `cd $W/arm-core/api && ../.venv/bin/pytest tests/test_recognition_exchange.py -q -k configured` (нужен NATS стенда). + Expected: FAIL (`used_subjects` нет, `publish_pending` не принимает `request_subjects`, нет `subjects` в healthcheck). + +- [ ] **Step 3: реализация.** + + `src/core/nats.py`: + +```python +def used_subjects(settings: NATSSettings) -> dict[str, str]: + """Subject'ы ядра для лога старта и `/healthcheck.subjects`: публикует оба запроса, подписано на ответ.""" + return { + "tabular_request": settings.NATS_TABULAR_REQUEST_SUBJECT, + "image_request": settings.NATS_IMAGE_REQUEST_SUBJECT, + "result": settings.NATS_RESULT_SUBJECT, + } +``` + + В `NatsConnection.__init__`: `result_subject: str | None = None`, `self.result_subject = result_subject or + settings.NATS_RESULT_SUBJECT`; импорт `RESULT_SUBJECT` из `src.contract.subjects` убрать, если больше не используется; + docstring класса и модуля: «по умолчанию — `NATS_RESULT_SUBJECT` настроек». + + `src/services/recognition_exchange.py`: `_process_one(..., request_subjects: Mapping[str, str])` → + `subject = request_subject(row.pipeline.value, request_subjects)`; `publish_pending(..., request_subjects: + Mapping[str, str] = REQUEST_SUBJECTS)` передаёт в `_process_one`; `run_publisher_loop(..., request_subjects: + Mapping[str, str] = REQUEST_SUBJECTS)` передаёт в `publish_pending`; `ExchangeTasks.start` передаёт + `request_subjects=settings.OPENBAO.NATS.request_subjects()`. `REQUEST_SUBJECTS` импортировать из `src.contract`. + + `src/services/primary_files.py:483`: `request_subject=request_subject(detected.pipeline.value, + self._settings.OPENBAO.NATS.request_subjects())`. + + `src/app.py`: в `lifespan` перед `_register_smb()` — + `logger.info("Subject'ы NATS. {subjects}", subjects=used_subjects(settings.OPENBAO.NATS))`; в `healthcheck` — + `return {"uptime": UP_TIME, "version": VERSION, "nats": nats_status, "subjects": used_subjects(app_settings.OPENBAO.NATS)}`; + docstring модуля: healthcheck `(uptime, version, nats: ok|down|off, subjects)`. + + `.env.example`: после `OPENBAO__NATS__NATS_JWT=` три строки с умолчаниями и комментарием + `# subject'ы обмена — по окружению (прод/тест), у трёх сервисов одинаковые; умолчания — имена контракта`. + README: в таблицу/список переменных NATS — три ключа; в описание `/healthcheck` — поле `subjects`. + +- [ ] **Step 4: тесты проходят.** Run: `make -C $W/arm-core lint test test-integration` Expected: PASS. + +- [ ] **Step 5: Commit.** + +```bash +git -C $W/arm-core add api/src api/tests api/.env.example README.md +git -C $W/arm-core commit -m "NATS: публикация, подписка и /healthcheck.subjects по настройкам окружения (#106)" +``` + +--- + +### Task 4: tabular-parser — настройки NATS, контракт, nkeys + +**Model:** haiku (код в разделе A). + +**Files:** +- Modify: `$W/tabular-parser/api/src/core/config.py` (A.1–A.4) +- Modify: `$W/tabular-parser/api/src/contract/subjects.py` (A.5) +- Modify: `$W/tabular-parser/api/src/core/nats.py` — `build_nats_auth` (A.6) +- Modify: `$W/tabular-parser/api/contracts/recognition-request.schema.json`, `recognition-result-message.schema.json` + (копия из `$W/docs/contracts/`) +- Test: `$W/tabular-parser/api/tests/unit/test_config.py`, `$W/tabular-parser/api/tests/unit/test_contract.py` + (или файл, где сейчас тестируется `request_subject`; найти `grep -rn request_subject tests`) + +**Interfaces:** +- Consumes: Task 1. +- Produces: то же, что Task 2, в tabular-parser. + +- [ ] **Step 1: фабрика и тесты.** В `tests/unit/test_config.py` фабрика `settings_from_env` поверх существующей + фикстуры `minimal_settings` (conftest): если `minimal_settings(**env)` уже принимает переопределения — обёртка + `settings_from_env = minimal_settings` с поддержкой `None` → `monkeypatch.delenv`; иначе — по образцу Task 2 Step 1 с + обязательными для tabular-parser `OPENBAO__MINIO__*` и `OPENBAO__NATS__NATS_URL`. Тесты A.7 дословно; существующие + `NATS_SEED == ""`/`NATS_JWT == ""` → `.get_secret_value() == ""`. Тест `request_subject(pipeline, subjects)` — как в + Task 2 Step 1 (`test_request_subject_uses_environment_map`). + +- [ ] **Step 2: тесты падают.** Run: `make -C $W/tabular-parser test` Expected: FAIL. + +- [ ] **Step 3: реализация.** A.1–A.6; `cp $W/docs/contracts/recognition-request.schema.json + $W/docs/contracts/recognition-result-message.schema.json $W/tabular-parser/api/contracts/`. + +- [ ] **Step 4: тесты проходят.** Run: `make -C $W/tabular-parser lint test` Expected: PASS (тест идентичности + контракта видит `../docs` = `$W/docs`). + +- [ ] **Step 5: Commit.** + +```bash +git -C $W/tabular-parser add api/src/core/config.py api/src/contract/subjects.py api/src/core/nats.py api/contracts api/tests +git -C $W/tabular-parser commit -m "NATS: subject'ы и JWT из OPENBAO__NATS__*, SecretStr, скрытие входа в ошибках (#31)" +``` + +--- + +### Task 5: tabular-parser — подписка, публикация, лог, `/healthcheck`, roundtrip на своих subject'ах + +**Model:** sonnet. + +**Files:** +- Modify: `$W/tabular-parser/api/src/core/nats.py` — `subscribe_requests` (умолчание subject'а), новая `used_subjects` +- Modify: `$W/tabular-parser/api/src/core/worker.py` — `RecognitionWorker.__init__` (`result_subject`), публикация + (`:467`) +- Modify: `$W/tabular-parser/api/src/app.py` — `_build_worker`, `lifespan` (лог), `healthcheck`, docstring модуля +- Modify: `$W/tabular-parser/api/.env.example`, `$W/tabular-parser/README.md` +- Test: `$W/tabular-parser/api/tests/unit/test_nats.py`, `tests/unit/test_worker.py`, `tests/unit/test_app.py`, + `tests/integration/test_roundtrip.py` + +**Interfaces:** +- Consumes: Task 4. +- Produces: `used_subjects(settings: NATSSettings) -> dict[str, str]` (ключи `tabular_request`, `result`); + `RecognitionWorker(..., result_subject: str = RESULT_SUBJECT)`; + `NatsConnection.subscribe_requests(cb, *, queue=REQUEST_QUEUE_GROUP, subject: str | None = None)` — `None` → + `settings.NATS_TABULAR_REQUEST_SUBJECT`. + +- [ ] **Step 1: тесты.** + + `tests/unit/test_nats.py`: + +```python +def test_used_subjects_tabular_parser() -> None: + nats = NATSSettings(NATS_URL="nats://x:4222", NATS_TABULAR_REQUEST_SUBJECT="test.files.tabular_uploaded") + assert used_subjects(nats) == {"tabular_request": "test.files.tabular_uploaded", "result": "files.tabular_parsed"} +``` + + и тест, что `subscribe_requests(cb)` без `subject` подписывается на `NATS_TABULAR_REQUEST_SUBJECT` настроек — по + образцу существующего unit-теста подписки в этом файле (фейковый клиент, проверка аргумента `subscribe`). + + `tests/unit/test_worker.py` — по образцу существующего теста публикации ack/результата (фейковый publisher с + записью `(subject, payload, msg_id)`): воркер с `result_subject="test.files.tabular_parsed"` публикует ack и + результат в этот subject. + + `tests/unit/test_app.py` — `test_healthcheck_subjects_when_nats_off` / `_down` по образцу существующих тестов + healthcheck файла: `body["subjects"] == {"tabular_request": ..., "result": ...}` из `app_module.settings`, JWT в + ответе нет. + + `tests/integration/test_roundtrip.py` — уникальные subject'ы на прогон (OQ-83): в фикстуре `service` перед + `lifespan_context`: + +```python +RUN = uuid.uuid4().hex[:8] +TEST_SUBJECTS = { + "NATS_TABULAR_REQUEST_SUBJECT": f"test-{RUN}.files.tabular_uploaded", + "NATS_IMAGE_REQUEST_SUBJECT": f"test-{RUN}.files.img_uploaded", + "NATS_RESULT_SUBJECT": f"test-{RUN}.files.tabular_parsed", +} +# в фикстуре service: +monkeypatch.setattr(app_module.settings.OPENBAO, "NATS", app_module.settings.OPENBAO.NATS.model_copy(update=TEST_SUBJECTS)) +``` + + (`RUN` и `TEST_SUBJECTS` — константы модуля); класс `Core` публикует в `TEST_SUBJECTS["NATS_TABULAR_REQUEST_SUBJECT"]`, + фикстура `core` подписывается на `TEST_SUBJECTS["NATS_RESULT_SUBJECT"]`; docstring модуля — убрать оговорку про общую + шину, если она есть, добавить «subject'ы прогона уникальны — контейнер стенда сообщения теста не получает (OQ-83)». + +- [ ] **Step 2: тесты падают.** Run: `make -C $W/tabular-parser test` Expected: FAIL. + +- [ ] **Step 3: реализация.** + + `src/core/nats.py`: + +```python +def used_subjects(settings: NATSSettings) -> dict[str, str]: + """Subject'ы сервиса для лога старта и `/healthcheck.subjects`: подписан на запрос `tabular`, публикует ответ.""" + return {"tabular_request": settings.NATS_TABULAR_REQUEST_SUBJECT, "result": settings.NATS_RESULT_SUBJECT} +``` + + `subscribe_requests`: параметр `subject: str | None = None`, в теле `subject = subject or + self._settings.NATS_TABULAR_REQUEST_SUBJECT` (имя атрибута настроек — как в классе); импорт + `TABULAR_REQUEST_SUBJECT` убрать, если не используется; docstring модуля — «подписка на subject запроса `tabular` + из настроек (по умолчанию `files.tabular_uploaded`)». + + `src/core/worker.py`: `RecognitionWorker.__init__(..., result_subject: str = RESULT_SUBJECT)` → + `self._result_subject = result_subject`; строка `:467` — `await self._publisher.publish(self._result_subject, payload, msg_id)`. + + `src/app.py`: в `_build_worker` — `RecognitionWorker(..., result_subject=settings.OPENBAO.NATS.NATS_RESULT_SUBJECT)`; + в `lifespan` первым действием — `logger.info("Subject'ы NATS. {subjects}", subjects=used_subjects(settings.OPENBAO.NATS))`; + `healthcheck` — `{"uptime": ..., "version": ..., "nats": ..., "subjects": used_subjects(settings.OPENBAO.NATS)}`. + + `.env.example`, README — как в Task 3 Step 3 (три ключа, поле `subjects`). + +- [ ] **Step 4: тесты проходят.** Run: `make -C $W/tabular-parser lint test test-integration` Expected: PASS + (интеграционные — при поднятом compose `arm-ka`; контейнер `tabular-parser` стенда останавливать не нужно). + +- [ ] **Step 5: Commit.** + +```bash +git -C $W/tabular-parser add api/src api/tests api/.env.example README.md +git -C $W/tabular-parser commit -m "NATS: подписка, публикация и /healthcheck.subjects по настройкам; roundtrip на своих subject'ах (#31)" +``` + +--- + +### Task 6: scan-gateway — настройки NATS, контракт, nkeys + +**Model:** haiku (код в разделе A). + +**Files:** +- Modify: `$W/scan-gateway/api/src/core/config.py` (A.1–A.4; у scan-gateway ещё `SMBSettings`, `ServerSettings`) +- Modify: `$W/scan-gateway/api/src/contract/subjects.py` (A.5) +- Modify: `$W/scan-gateway/api/src/core/nats.py` — `build_nats_auth` (A.6, сохранить `.strip()`) +- Modify: `$W/scan-gateway/api/contracts/recognition-request.schema.json`, `recognition-result-message.schema.json` +- Test: `$W/scan-gateway/api/tests/unit/test_config.py`, `tests/unit/test_contract.py`, `tests/unit/test_nats.py` + +**Interfaces:** +- Consumes: Task 1. +- Produces: то же, что Task 2, в scan-gateway. + +- [ ] **Step 1: фабрика и тесты.** В `tests/unit/test_config.py` фабрика `settings_from_env` поверх существующих + `_clean_env(monkeypatch)` и фикстуры `minimal_env_file` (сначала `_clean_env`, затем `setenv`/`delenv` по `env`, + затем `get_settings.cache_clear()` если `get_settings` кэширован, и `Settings()`); тесты A.7 дословно (для + `missing="OPENBAO__MINIO__MINIO_URL"` переменная должна отсутствовать и в `minimal_env_file` — если она задаётся + файлом, тест строит `Settings(_env_file=None)` с явным набором env). Существующие сравнения `NATS_SEED == ""` / + `NATS_JWT == ""` → `.get_secret_value() == ""`. Тест `request_subject(pipeline, subjects)` — как в Task 2. + +- [ ] **Step 2: тесты падают.** Run: `make -C $W/scan-gateway test` Expected: FAIL. + +- [ ] **Step 3: реализация.** A.1–A.6; копия схем в `$W/scan-gateway/api/contracts/`. + +- [ ] **Step 4: тесты проходят.** Run: `make -C $W/scan-gateway lint test` Expected: PASS. + +- [ ] **Step 5: Commit.** + +```bash +git -C $W/scan-gateway add api/src/core/config.py api/src/contract/subjects.py api/src/core/nats.py api/contracts api/tests +git -C $W/scan-gateway commit -m "NATS: subject'ы и JWT из OPENBAO__NATS__*, SecretStr, скрытие входа в ошибках (#32)" +``` + +--- + +### Task 7: scan-gateway — подписка, публикация, лог, `/healthcheck`, интеграционные тесты на своих subject'ах + +**Model:** sonnet. + +**Files:** +- Modify: `$W/scan-gateway/api/src/core/nats.py` — `NatsGateway.subscribe_requests` (`:211–237`), `publish_result` + (`:294–298`), новая `used_subjects` +- Modify: `$W/scan-gateway/api/src/core/health.py` — `snapshot` +- Modify: `$W/scan-gateway/api/src/app.py` — лог при старте, docstring модуля +- Modify: `$W/scan-gateway/api/.env.example`, `$W/scan-gateway/README.md` +- Test: `$W/scan-gateway/api/tests/unit/test_nats.py`, `tests/unit/test_health.py`, `tests/unit/test_app.py`, + `tests/integration/test_nats_minio_integration.py`, `tests/integration/test_roundtrip.py` + +**Interfaces:** +- Consumes: Task 6. +- Produces: `used_subjects(settings: NATSSettings) -> dict[str, str]` (ключи `image_request`, `result`); + `NatsGateway` подписывается на `settings.OPENBAO.NATS.NATS_IMAGE_REQUEST_SUBJECT` и публикует в + `settings.OPENBAO.NATS.NATS_RESULT_SUBJECT`. + +- [ ] **Step 1: тесты.** + + `tests/unit/test_nats.py`: + +```python +def test_used_subjects_scan_gateway() -> None: + nats = NATSSettings(NATS_URL="nats://x:4222", NATS_IMAGE_REQUEST_SUBJECT="test.files.img_uploaded") + assert used_subjects(nats) == {"image_request": "test.files.img_uploaded", "result": "files.tabular_parsed"} +``` + + и по образцу существующих unit-тестов `NatsGateway` с фейковым клиентом: при + `OPENBAO__NATS__NATS_IMAGE_REQUEST_SUBJECT=test.files.img_uploaded` и `OPENBAO__NATS__NATS_RESULT_SUBJECT=test.files.tabular_parsed` + (`make_settings(monkeypatch, ...)`) `subscribe_requests` вызывает `subscribe("test.files.img_uploaded", queue="scan-gateway", ...)`, + а `publish_result` публикует в `"test.files.tabular_parsed"`. + + `tests/unit/test_health.py` — `snapshot()` содержит `"subjects": {"image_request": ..., "result": ...}` при + `nats_status` `ok`, `down` и `disabled`. + + `tests/unit/test_app.py` — `/healthcheck` отдаёт `subjects`, JWT в ответе нет. + + Интеграционные (`tests/integration/`): по образцу Task 5 Step 1 — константы `RUN`, `TEST_SUBJECTS` и настройки + тестовых `NatsGateway` с этими subject'ами (через `make_settings`/`monkeypatch.setenv` на `OPENBAO__NATS__NATS_*_SUBJECT` + до создания `Settings`), публикация запросов и подписка на результат — в `TEST_SUBJECTS`; убрать фильтрацию + «чужие сообщения на общем subject пропускаем», если после перехода она больше не нужна (оставить, если сообщения + фильтруются по `job_id` внутри теста по другим причинам). + +- [ ] **Step 2: тесты падают.** Run: `make -C $W/scan-gateway test` Expected: FAIL. + +- [ ] **Step 3: реализация.** + + `src/core/nats.py`: + +```python +def used_subjects(settings: NATSSettings) -> dict[str, str]: + """Subject'ы сервиса для лога старта и `/healthcheck.subjects`: подписан на запрос `image`, публикует ответ.""" + return {"image_request": settings.NATS_IMAGE_REQUEST_SUBJECT, "result": settings.NATS_RESULT_SUBJECT} +``` + + `subscribe_requests`: `subject = self._settings.OPENBAO.NATS.NATS_IMAGE_REQUEST_SUBJECT` вместо + `IMAGE_REQUEST_SUBJECT` (в сообщении `NatsNotConnectedError`, в `subscribe`, в логе); `publish_result`: + `await self.publish(self._settings.OPENBAO.NATS.NATS_RESULT_SUBJECT, payload, ...)`; неиспользуемые импорты констант + убрать; docstring'и — «subject из настроек (по умолчанию …)». + + `src/core/health.py`: `snapshot()` возвращает `{"nats": ..., "hotfolder": ..., "subjects": self._subjects}`, где + `self._subjects` — новый параметр `__init__` `subjects: Mapping[str, str]` (передать в `app.py` при создании + `Health...`: `subjects=used_subjects(settings.OPENBAO.NATS)`). + + `src/app.py`: лог `logger.info("Subject'ы NATS. {subjects}", subjects=used_subjects(settings.OPENBAO.NATS))` в начале + lifespan; docstring модуля — `/healthcheck` отдаёт `uptime`, `version`, `nats`, `hotfolder`, `subjects`. + + `.env.example`, README — как в Task 3 Step 3. + +- [ ] **Step 4: тесты проходят.** Run: `make -C $W/scan-gateway lint test test-integration` Expected: PASS. + +- [ ] **Step 5: Commit.** + +```bash +git -C $W/scan-gateway add api/src api/tests api/.env.example README.md +git -C $W/scan-gateway commit -m "NATS: подписка, публикация и /healthcheck.subjects по настройкам; интеграционные тесты на своих subject'ах (#32)" +``` + +--- + +### Task 8: docs — документы контекста и окружения разработки + +**Model:** sonnet. + +**Files:** +- Modify: `$W/docs/context/03-architecture.md` — схема/таблица потоков (строки ~17, ~68–76), раздел секретов + (~306), раздел NATS платформы (~334) +- Modify: `$W/docs/context/07-open-questions.md` — OQ-83 (строка ~91) +- Modify: `$W/docs/dev/README.md` — таблица переменных (~323–325), раздел интеграционных тестов/e2e (~405), + описание `/healthcheck` +- Modify: `$W/docs/dev/.env.example`, `$W/docs/dev/contour-transfer-guide.md` (~180–188), + `$W/docs/dev/qa-guide.md` (~955: `/healthcheck`), `$W/docs/dev/frontend-api-guide.md` (если описан ответ `/healthcheck`) +- Modify: `$W/docs/repo-templates/*.CLAUDE.md` — только если там названы константы subject'ов как неизменные +- Modify: `$W/docs/openspec/changes/nats-env-config/tasks.md` — отметить выполненные пункты + +**Interfaces:** +- Consumes: итог задач 1–7 (имена ключей, поле `subjects`, поведение тестов). + +- [ ] **Step 1: `03-architecture.md`.** В таблице потоков и на схеме имена `files.*` оставить, пометив «subject'ы — + настройки окружения `OPENBAO__NATS__NATS_{TABULAR_REQUEST,IMAGE_REQUEST,RESULT}_SUBJECT`, здесь — умолчания». + В перечне секретов — три ключа рядом с `NATS_URL/NATS_SEED/NATS_JWT` для всех трёх сервисов. В разделе NATS + платформы — абзац: «Прод и тест — свои наборы: JWT, seed и три subject'а окружения задаются одинаково всем трём + сервисам; JWT окружения должен давать publish/subscribe на эти subject'ы. Разъехавшиеся настройки — тишина на шине + (dlq по NO_ACK); сверять `/healthcheck.subjects` ядра и сервисов.» (решение владельца 12.10.2026). + +- [ ] **Step 2: `07-open-questions.md`, OQ-83.** В колонку решения дописать: «13.10.2026 (nats-env-config): subject'ы — + настройки; интеграционные тесты tabular-parser и scan-gateway идут на уникальных subject'ах прогона и не пересекаются + со стендом. Для `make e2e` ядра вопрос остаётся: ему нужен контейнер tabular-parser стенда на умолчаниях, конфликт — + общая queue group `arm-core` (design R-7)». Статус строки — открыт (частично). + +- [ ] **Step 3: `dev/README.md`, `dev/.env.example`, `contour-transfer-guide.md`, `qa-guide.md`, + `frontend-api-guide.md`.** Три ключа в таблицах переменных (умолчание, смысл); в `contour-transfer-guide.md` — в + перечень секретов каждого сервиса и пункт «для тестового контура — свой набор JWT/seed/subject'ов»; ответ + `/healthcheck` — с полем `subjects` там, где он описан; в README — интеграционные тесты сервисов больше не требуют + останавливать контейнеры стенда, `make e2e` ядра — по-прежнему требует (R-7). + +- [ ] **Step 4: проверка.** Run: `make -C $W/docs check` и `cd $W/docs && openspec validate nats-env-config --strict`. + Expected: зелёный / valid. + +- [ ] **Step 5: Commit.** + +```bash +git -C $W/docs add context dev repo-templates openspec/changes/nats-env-config/tasks.md +git -C $W/docs commit -m "docs: subject'ы и JWT NATS по окружению — архитектура, окружение, OQ-83 (#80)" +``` + +--- + +## После задач (контроллер) + +- Отметить пункты `tasks.md` дочерних change'ей в трёх сервисах (каждый — коммитом в своей ветке). +- `make -C $W/ check-full` в трёх сервисах (без `| tail`: код возврата не терять). Для `make e2e` ядра — + остановить ядро стенда на время прогона (R-7). +- Финальное ревью ветки (opus) по четырём репозиториям, таблица находок с маркерами и статусами. -- 2.47.2 From e197bd53e33d5d1d8b2df010f101a9026c54eab8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 10:36:53 +0400 Subject: [PATCH 03/11] =?UTF-8?q?=D0=9F=D0=BB=D0=B0=D0=BD=20nats-env-confi?= =?UTF-8?q?g:=20=D0=BF=D1=83=D1=82=D1=8C=20=D0=BA=20venv=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B8=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/superpowers/plans/2026-10-13-nats-env-config.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-13-nats-env-config.md b/docs/superpowers/plans/2026-10-13-nats-env-config.md index dc8fea8..1445599 100644 --- a/docs/superpowers/plans/2026-10-13-nats-env-config.md +++ b/docs/superpowers/plans/2026-10-13-nats-env-config.md @@ -312,7 +312,7 @@ def test_request_subject_uses_environment_map() -> None: В `tests/test_nats_core.py` хелпер `NATSSettings(NATS_URL=..., NATS_SEED=seed, NATS_JWT=jwt)` оставить (pydantic примет `str` в `SecretStr`). -- [ ] **Step 2: тесты падают.** Run: `cd $W/arm-core/api && ../.venv/bin/pytest tests/test_config.py tests/test_contract.py -q` +- [ ] **Step 2: тесты падают.** Run: `cd $W/arm-core/api && .venv/bin/pytest tests/test_config.py tests/test_contract.py -q` Expected: FAIL (`NATS_RESULT_SUBJECT` нет, `request_subject` не принимает второй аргумент). - [ ] **Step 3: реализация.** Применить A.1–A.6 к файлам задачи; скопировать схемы: @@ -418,7 +418,7 @@ async def test_healthcheck_subjects_when_nats_off(client: httpx.AsyncClient, mon `job.request_subject == "test.files.tabular_uploaded"` (по образцу существующей проверки на строке ~251). - [ ] **Step 2: тесты падают.** Run: `make -C $W/arm-core test` и - `cd $W/arm-core/api && ../.venv/bin/pytest tests/test_recognition_exchange.py -q -k configured` (нужен NATS стенда). + `cd $W/arm-core/api && .venv/bin/pytest tests/test_recognition_exchange.py -q -k configured` (нужен NATS стенда). Expected: FAIL (`used_subjects` нет, `publish_pending` не принимает `request_subjects`, нет `subjects` в healthcheck). - [ ] **Step 3: реализация.** -- 2.47.2 From 1462553cb6404abd5fd74f1e3f7e13133fab4778 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 12:02:20 +0400 Subject: [PATCH 04/11] =?UTF-8?q?contracts:=20subject'=D1=8B=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20=D0=B8=20=D0=BE=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D0=B0=20=E2=80=94=20=D0=BD=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=B9=D0=BA=D0=B8=20=D0=BE=D0=BA=D1=80=D1=83=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20(nats-env-config)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- contracts/recognition-request.schema.json | 2 +- contracts/recognition-result-message.schema.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/recognition-request.schema.json b/contracts/recognition-request.schema.json index 9d59f2c..afd5077 100644 --- a/contracts/recognition-request.schema.json +++ b/contracts/recognition-request.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "arm-ka/recognition-request/v1", "title": "Запрос распознавания файла (arm-core → tabular-parser / scan-gateway), контракт v1.0", - "description": "Тело сообщения на subject files.tabular_uploaded или files.img_uploaded; subject должен соответствовать полю pipeline (tabular → files.tabular_uploaded, image → files.img_uploaded). Заголовок Nats-Msg-Id = : (не поле тела). Один файл (stored_file) → один job.", + "description": "Тело сообщения на subject запроса; subject — настройка окружения и должен соответствовать полю pipeline (tabular → NATS_TABULAR_REQUEST_SUBJECT, по умолчанию files.tabular_uploaded; image → NATS_IMAGE_REQUEST_SUBJECT, по умолчанию files.img_uploaded). Заголовок Nats-Msg-Id = : (не поле тела). Один файл (stored_file) → один job.", "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.", "type": "object", "required": ["schema_version", "job_id", "file_id", "pipeline", "file_type", "original_name", "s3", "reply_deadline", "attempt"], diff --git a/contracts/recognition-result-message.schema.json b/contracts/recognition-result-message.schema.json index 7c1cfb5..4d9f401 100644 --- a/contracts/recognition-result-message.schema.json +++ b/contracts/recognition-result-message.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "arm-ka/recognition-result-message/v1", "title": "Сообщение результата распознавания (tabular-parser / scan-gateway → arm-core), контракт v1.0", - "description": "Тело сообщения на subject files.tabular_parsed. Заголовок Nats-Msg-Id (не поле тела) на этом subject имеет две формы: ack:: — для status = processing; result:: — для done, needs_check и error. Форма запроса : описана в схеме recognition-request. status = processing — ack «взял в работу», публикуется до чтения файла из S3. group_schema в конверт не входит: источник — файл результата (recognition-result).", + "description": "Тело сообщения на subject ответа (настройка окружения NATS_RESULT_SUBJECT, по умолчанию files.tabular_parsed). Заголовок Nats-Msg-Id (не поле тела) на этом subject имеет две формы: ack:: — для status = processing; result:: — для done, needs_check и error. Форма запроса : описана в схеме recognition-request. status = processing — ack «взял в работу», публикуется до чтения файла из S3. group_schema в конверт не входит: источник — файл результата (recognition-result).", "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.", "type": "object", "required": ["schema_version", "job_id", "file_id", "status", "service"], -- 2.47.2 From 4d9e81c04bda05db858b57b17db8e4d5b7e1307a Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 12:31:45 +0400 Subject: [PATCH 05/11] =?UTF-8?q?docs:=20subject'=D1=8B=20=D0=B8=20JWT=20N?= =?UTF-8?q?ATS=20=D0=BF=D0=BE=20=D0=BE=D0=BA=D1=80=D1=83=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8E=20=E2=80=94=20=D0=B0=D1=80=D1=85=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D1=83=D1=80=D0=B0,=20=D0=BE=D0=BA=D1=80=D1=83?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5,=20OQ-83=20(#80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- context/03-architecture.md | 17 +++++++++++---- context/07-open-questions.md | 2 +- dev/.env.example | 5 +++++ dev/README.md | 26 +++++++++++------------ dev/contour-transfer-guide.md | 11 +++++++--- dev/frontend-api-guide.md | 2 +- dev/qa-guide.md | 1 + openspec/changes/nats-env-config/tasks.md | 6 +++--- 8 files changed, 45 insertions(+), 25 deletions(-) diff --git a/context/03-architecture.md b/context/03-architecture.md index 42c5046..86d6f8c 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -64,9 +64,13 @@ | Поток | Subject | Кто пишет | Полезная нагрузка | `Nats-Msg-Id` | Схема | |---|---|---|---|---|---| -| запрос | `files.tabular_uploaded` / `files.img_uploaded` | arm-core (outbox из `recognition_job`) | `schema_version`, `job_id`, `file_id`, `pipeline`, `file_type`, `original_name`, `s3 {bucket, key}` исходника, `reply_deadline`, `attempt` | `:` | [request](../contracts/recognition-request.schema.json) | -| ack | `files.tabular_parsed` | tabular-parser / scan-gateway | конверт со `schema_version`, `status = processing`, `job_id`, `file_id`, `service {name, version}` | `ack::` | [message](../contracts/recognition-result-message.schema.json) | -| результат | `files.tabular_parsed` | tabular-parser / scan-gateway | `schema_version`, `job_id`, `file_id`, `status` (done / needs_check / error), `service {name, version}`, `metrics` (опц.), `raw_s3_key` (опц., только scan-gateway); при `done` / `needs_check` обязательны `result_s3 {bucket, key}` и `result_schema_version`, при `error` — `error {code, message}` | `result::` | [message](../contracts/recognition-result-message.schema.json); файл в S3 — [result](../contracts/recognition-result.schema.json) | +| запрос | `files.tabular_uploaded` / `files.img_uploaded` (умолчания) | arm-core (outbox из `recognition_job`) | `schema_version`, `job_id`, `file_id`, `pipeline`, `file_type`, `original_name`, `s3 {bucket, key}` исходника, `reply_deadline`, `attempt` | `:` | [request](../contracts/recognition-request.schema.json) | +| ack | `files.tabular_parsed` (умолчание) | tabular-parser / scan-gateway | конверт со `schema_version`, `status = processing`, `job_id`, `file_id`, `service {name, version}` | `ack::` | [message](../contracts/recognition-result-message.schema.json) | +| результат | `files.tabular_parsed` (умолчание) | tabular-parser / scan-gateway | `schema_version`, `job_id`, `file_id`, `status` (done / needs_check / error), `service {name, version}`, `metrics` (опц.), `raw_s3_key` (опц., только scan-gateway); при `done` / `needs_check` обязательны `result_s3 {bucket, key}` и `result_schema_version`, при `error` — `error {code, message}` | `result::` | [message](../contracts/recognition-result-message.schema.json); файл в S3 — [result](../contracts/recognition-result.schema.json) | + +Имена `files.*` на схеме и в таблице выше — умолчания: subject'ы являются настройками окружения +`OPENBAO__NATS__NATS_{TABULAR_REQUEST,IMAGE_REQUEST,RESULT}_SUBJECT` (`files.tabular_uploaded`, `files.img_uploaded`, +`files.tabular_parsed`), одинаковыми у всех трёх сервисов. Queue group'ы — константы. `group_schema` в конверт не входит — ядро берёт его из `documents[].group_schema` файла результата при маппинге. @@ -303,7 +307,9 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро - `pydantic-settings`, файл `api/.env`, вложенность через `__`, лишние переменные игнорируются. Все инфраструктурные секреты — под префиксом `OPENBAO` (в проде заполняет OpenBao): `OPENBAO__DATABASE__DATABASE_URL`, `OPENBAO__MINIO__{MINIO_URL, MINIO_ADMIN_LOGIN, MINIO_ADMIN_PASSWORD, MINIO_BUCKET}`, - `OPENBAO__NATS__{NATS_URL, NATS_SEED, NATS_JWT}`, `OPENBAO__SMB__{SMB_HOST, SMB_PATH, SMB_LOGIN, SMB_PASSWORD}`. + `OPENBAO__NATS__{NATS_URL, NATS_SEED, NATS_JWT, NATS_TABULAR_REQUEST_SUBJECT, NATS_IMAGE_REQUEST_SUBJECT, + NATS_RESULT_SUBJECT}` (во всех трёх сервисах; `NATS_SEED` и `NATS_JWT` — `SecretStr`, три subject'а — настройки + с умолчаниями `files.*`), `OPENBAO__SMB__{SMB_HOST, SMB_PATH, SMB_LOGIN, SMB_PASSWORD}`. Прочее: `DEBUG`, `ROOT_PATH`, `VERSION`, `SERVER__HOST`, `SERVER__PORT`. - Платформа РАИСА передаёт в под только секреты OpenBao, поэтому любая настройка верхнего уровня `Settings` в контуре задаётся как `OPENBAO__APP__<ИМЯ>` (приоритет: обычный env → `OPENBAO__APP__*` → `.env` → умолчание; неизвестное @@ -334,6 +340,9 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро `files.img_uploaded`, `files.tabular_parsed`, `files.img_parsed`; используем три — запросы в `files.tabular_uploaded` и `files.img_uploaded`, ack и результаты обоих сервисов в общий `files.tabular_parsed`; `files.img_parsed` не используется. Старый АРМ гасится, общих подписчиков на этих subject'ах у нас с ним не будет. + Прод и тест — свои наборы: JWT, seed и три subject'а окружения задаются одинаково всем трём сервисам; JWT окружения + должен давать publish/subscribe на эти subject'ы. Разъехавшиеся настройки — тишина на шине (dlq по NO_ACK); сверять + `/healthcheck.subjects` ядра и сервисов (решение владельца 12.10.2026). - **SMB** (горячие папки ИС Сканирование) — `smbprotocol`/`smbclient`, сессия регистрируется в lifespan; нужен только `scan-gateway`. - **Внешняя аутентификация** — пакет `raisa_fastapi_protected_api` из внутреннего Nexus (`AuthorizationMiddleware`, `ProtectedSettings`, …), импортируется только при `DEBUG=false` (в контуре временно `DEBUG=true`, см. «Аутентификация и авторизация», п. 6); фронтовая пара — `raisa-fastapi-protected-web`. diff --git a/context/07-open-questions.md b/context/07-open-questions.md index 11632cb..7249380 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -88,7 +88,7 @@ | OQ-80 | `make up` расходится с эталоном (scan-gateway и `dev/Makefile.common`): в `tabular-parser` нет `minio-init` — на свежем томе бакета `arm-ka` нет, `test_s3_integration` даёт `NoSuchBucket`, хотя README сервиса обещает бакет; в `arm-core` `up -d` без `--wait` и без `--project-name` (имя в compose фиксировано, безвредно). Также `check-full` в arm-core и tabular-parser — через пререквизиты (порядок при `make -j` не гарантирован), в scan-gateway — через `$(MAKE)`; `lint` arm-core включает `mypy scripts`. Подключить `include ../docs/dev/Makefile.common` в сервисах? | tabular-parser: `Makefile` (цель `up`); scan-gateway: п. 18 таблицы предложений (I-1); docs: O-1 и O-2 (задача 1 фазы 2), O-4 и O-5 (задача 2 фазы 2), `dev/checks.md`, `dev/Makefile.common` | срез 0 (процесс), все сервисы | допущение принято: эталон — scan-gateway, расхождения описаны в `dev/checks.md`, Makefile сервисов не правились (design #22, #24); выровнять — правкой Makefile сервисов (follow-up) | | OQ-81 | Переменные `ARM_KA_DOCS` / `ARM_KA_FIXTURES` поддержаны неравномерно: `arm-core` — обе; `tabular-parser` — только `ARM_KA_DOCS`; `scan-gateway` — ни одной (жёстко `../docs`, `../../fixtures/ocr`). Текст причины skip («fixtures недоступен: положите репозиторий…») не содержит строки «ARM_KA_FIXTURES недоступен» из сценария «Нет фикстур» spec `quality-checks`; для docs-теста текст тоже без `ARM_KA_DOCS`. Нарушение spec — править сервисы? | scan-gateway и tabular-parser (тесты, Makefile); docs: O-3 (задача 1 фазы 2), `dev/checks.md`; spec `quality-checks` | срез 0 (процесс), все сервисы | допущение принято: расхождение описано в `dev/checks.md`; правка тестов и Makefile в scan-gateway и tabular-parser — follow-up человеку (сервисы не правились); **частично закрыт (Срез 2a, tabular-parser):** хелпер `tests/fixtures_helpers.py` поддерживает `ARM_KA_FIXTURES` (текст skip называет переменную и путь), `ARM_KA_DOCS` поддержан в тесте идентичности контракта; `scan-gateway` — по-прежнему ни одной переменной, остаётся follow-up человеку | | OQ-82 | `.dockerignore` в `arm-core` не исключает `api/.env.test`: файл попадает в образ (у `tabular-parser` и `scan-gateway` исключён). Исправлять? | arm-core `.dockerignore`; docs: T3.2 (задача 3 фазы 2) | срез 0 (образы), прод-сборка | **закрыт реализацией (arm-core PR #10, техдолг Среза 1, 05.10.2026):** `.dockerignore` исключает `api/.env.*` (включая `.env.test` и `.env.example`); настройки контейнера — только из окружения | -| OQ-83 | Контейнеры профиля `services` подписаны на те же subject и queue group общего NATS, что и экземпляры в интеграционных тестах сервисов (`recognition.result` / `arm-core`, `*.request`): NATS может отдать сообщение теста контейнеру и наоборот; интеграционные тесты `scan-gateway` и `arm-core` одновременно тоже делят NATS. Сделать queue group и subject-префикс настраиваемыми для тестов (например `NATS_TEST_PREFIX`) или поднимать отдельный NATS на тесты? | scan-gateway: п. 11 таблицы предложений (T14.1, T11.8); docs: O-11 (задача 3 фазы 2; подтверждено ревью для tabular-parser и scan-gateway), `dev/README.md` | срез 0 (процесс), интеграционные тесты | допущение принято: в `dev/README.md` предупреждение и команда `stop` для контейнеров профиля; не гонять `make test-integration` двух сервисов одновременно | +| OQ-83 | Контейнеры профиля `services` подписаны на те же subject и queue group общего NATS, что и экземпляры в интеграционных тестах сервисов (`recognition.result` / `arm-core`, `*.request`): NATS может отдать сообщение теста контейнеру и наоборот; интеграционные тесты `scan-gateway` и `arm-core` одновременно тоже делят NATS. Сделать queue group и subject-префикс настраиваемыми для тестов (например `NATS_TEST_PREFIX`) или поднимать отдельный NATS на тесты? | scan-gateway: п. 11 таблицы предложений (T14.1, T11.8); docs: O-11 (задача 3 фазы 2; подтверждено ревью для tabular-parser и scan-gateway), `dev/README.md` | срез 0 (процесс), интеграционные тесты | допущение принято: в `dev/README.md` предупреждение и команда `stop` для контейнеров профиля; не гонять `make test-integration` двух сервисов одновременно. 13.10.2026 (nats-env-config): subject'ы — настройки; интеграционные тесты tabular-parser и scan-gateway идут на уникальных subject'ах прогона и не пересекаются со стендом. Для `make e2e` ядра вопрос остаётся: ему нужен контейнер tabular-parser стенда на умолчаниях, конфликт — общая queue group `arm-core` (design R-7). Статус: открыт (частично) | | OQ-84 | Запуск сервисов из venv без Nexus (`make venv` + `python run_server.py` из `api/` с `.env` из `docs/dev/.env.example`, healthcheck по `localhost`) в Срезе 0 не проверялся ни в одном из трёх сервисов. Известно по чтению конфигов (design #44): `.env.example` в нынешнем виде отвергнут `scan-gateway` (`OPENBAO__DATABASE__*`) и `tabular-parser` (`OPENBAO__DATABASE__*`, `OPENBAO__SMB__*` — `extra_forbidden`), в нём нет `OPENBAO__MINIO__MINIO_SECURE=false` (по умолчанию `true` — https к http-MinIO). Прогнать сценарий, зафиксировать набор переменных и портов (`SERVER__PORT`, `SCAN_HOTFOLDER_PATH` для `scan-gateway`), решить, нужен ли отдельный `.env.example` на сервис, и снять пометку «черновик» с раздела `dev/README.md`? | spec `dev-environment`, сценарий «Запуск из venv против compose без Nexus»; docs: design #43, `dev/README.md` («Запуск сервисов из venv без Nexus»); финальное ревью фазы 2: I-1 | срез 0 (процесс, окружение разработки); кандидат в follow-up issue | вопрос: допущение Среза 0 — раздел README остаётся черновиком, сценарий не подтверждён (проверка требовала `.venv` внутри сервисных репозиториев, в фазе 2 доступных только для чтения) | | OQ-85 | **Возможен ли JetStream на платформе** (серверный дедуп по `Nats-Msg-Id`, durable consumer'ы, redelivery вместо ack и повторной публикации на уровне приложения)? Сейчас NATS без JetStream (ответ платформы 03.10.2026); Срез 0 строит доставку на ack `processing`, повторной публикации ядром и дедупе по `recognition_job.result_msg_id`. Если JetStream подключить можно — меняются только publisher/subscriber (`core/nats.py` в трёх сервисах), outbox/inbox и контракт остаются; исчезают потеря результата при простое ядра и риск повторной обработки. | решение человека 04.10.2026: «узнать у платформы и проверить позже»; `design.md` slice-0-skeleton (D-3, Open Questions п. 4); OQ-72, OQ-73 | инфраструктура NATS (платформа); `core/nats.py`, воркеры сервисов | **вопрос к платформе (поставить напоминание, проверить после ревью Среза 0 / до Среза 3)**: допущение — без JetStream. 06.10.2026: `RSHB/armka_file_worker` (PDFWorker старого АРМ, проверен в контуре) тоже работает на core NATS (`subscribe`/`publish` + `flush`, без `jetstream()`), один JWT на все сервисы АРМ с правами по subject'ам; возможность включить JetStream по-прежнему не выяснена | | OQ-86 | Фильтр видимости по ЗО (`business_segment`): сегмент есть только у `app_user`, у ГК/ЮЛ поля нет (DBML v5.14). Откуда брать сегмент ГК (из ЕЦП CRM? из РФ?), нужен ли вообще фильтр по ЗО в первой очереди? | §10.1 правил; DBML v5.14 | срез 1 (policy), DBML v5.15 | допущение: в Срезе 1 ЗО не фильтрует; видимость — только по филиалу (ГО видит всё) | diff --git a/dev/.env.example b/dev/.env.example index c50f3ff..66d4208 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -47,6 +47,11 @@ OPENBAO__MINIO__MINIO_BUCKET=arm-ka OPENBAO__NATS__NATS_URL=nats://localhost:4222 OPENBAO__NATS__NATS_SEED= OPENBAO__NATS__NATS_JWT= +# Subject'ы окружения — одинаковые у arm-core, tabular-parser и scan-gateway (значения ниже — умолчания). +# Прод и тест имеют свои наборы; JWT должен давать publish/subscribe на эти subject'ы. +OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT=files.tabular_uploaded +OPENBAO__NATS__NATS_IMAGE_REQUEST_SUBJECT=files.img_uploaded +OPENBAO__NATS__NATS_RESULT_SUBJECT=files.tabular_parsed # SMB — заглушки (в Срезе 0 не используется); блок необязателен для arm-core и scan-gateway (режим local). # Блок OPENBAO__DATABASE__* знает только arm-core: scan-gateway и tabular-parser неизвестные ключи OPENBAO__* отвергают diff --git a/dev/README.md b/dev/README.md index 410dd44..e3e0adf 100644 --- a/dev/README.md +++ b/dev/README.md @@ -323,12 +323,17 @@ MinIO; полная очистка — `docker compose --profile services down - | `OPENBAO__NATS__NATS_URL` | `nats://localhost:4222` | адрес NATS | | `OPENBAO__NATS__NATS_SEED` | пусто | nkey seed; пусто — подключение без nkeys | | `OPENBAO__NATS__NATS_JWT` | пусто | JWT NATS; пусто — подключение без nkeys | +| `OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT` | `files.tabular_uploaded` | subject запросов на разбор таблиц (ядро публикует, tabular-parser слушает) | +| `OPENBAO__NATS__NATS_IMAGE_REQUEST_SUBJECT` | `files.img_uploaded` | subject запросов на распознавание сканов (ядро публикует, scan-gateway слушает) | +| `OPENBAO__NATS__NATS_RESULT_SUBJECT` | `files.tabular_parsed` | subject ack и результатов обоих сервисов (слушает ядро) | | `OPENBAO__SMB__SMB_HOST` | `localhost` | заглушка SMB (в Срезе 0 не используется); блок необязателен для `arm-core` и для `scan-gateway` в режиме `SCAN_HOTFOLDER_MODE=local` (по умолчанию); `tabular-parser` его не знает и отвергнет (см. ниже) | | `OPENBAO__SMB__SMB_PATH` | `share` | заглушка SMB | | `OPENBAO__SMB__SMB_LOGIN` | `user` | заглушка SMB | | `OPENBAO__SMB__SMB_PASSWORD` | `password` | заглушка SMB | | `NATS_ENABLED` | `true` | включает подключение arm-core к NATS; без него `/healthcheck` показывает `nats=off` | +Три subject'а одинаковы у всех трёх сервисов (непустые, без пробелов, `*`, `>` и пустых токенов, попарно различны; `NATS_JWT` и `NATS_SEED` — `SecretStr`). Прод и тест — свои наборы; разъехавшиеся настройки дают тишину на шине, сверяйте `/healthcheck.subjects`. + Два блока описывают одни и те же ресурсы: **при смене порта, пароля, пользователя или бакета меняйте обе части.** ## Сброс окружения @@ -374,7 +379,7 @@ curl -s localhost:<порт сервиса>/healthcheck одновременно или сервис из venv рядом с профилем `services` требуют разных `SERVER__PORT`. Для `arm-core` в ответе `/healthcheck` ожидаются `version` и `nats = ok` — при `NATS_ENABLED=true` (задан в `.env.example`; без него `nats = off`). Поля `/healthcheck` в контейнерах: `tabular-parser` — `uptime`, `version`, `nats`; `scan-gateway` — ещё -`hotfolder` (шлюзу нужен `SCAN_HOTFOLDER_PATH`). Набор переменных сервисов для запуска из venv не проверялся (см. черновик выше и OQ-84). По чтению конфигов: `scan-gateway` и `tabular-parser` отвергают неизвестные ключи `OPENBAO__*`, а поле DATABASE есть только у `arm-core`. Поэтому в `.env` `scan-gateway` нужно удалить строки `OPENBAO__DATABASE__*` (SMB в режиме `local` допустим), а в `.env` `tabular-parser` — `OPENBAO__DATABASE__*` и `OPENBAO__SMB__*`; для MinIO по `http` нужен `OPENBAO__MINIO__MINIO_SECURE=false` (в `.env.example` его нет, по умолчанию `true`). +`hotfolder` (шлюзу нужен `SCAN_HOTFOLDER_PATH`). Во всех трёх ответах есть объект `subjects` с фактическими subject'ами сервиса (при любом состоянии NATS): `arm-core` — `{tabular_request, image_request, result}`, `tabular-parser` — `{tabular_request, result}`, `scan-gateway` — `{image_request, result}`. Набор переменных сервисов для запуска из venv не проверялся (см. черновик выше и OQ-84). По чтению конфигов: `scan-gateway` и `tabular-parser` отвергают неизвестные ключи `OPENBAO__*`, а поле DATABASE есть только у `arm-core`. Поэтому в `.env` `scan-gateway` нужно удалить строки `OPENBAO__DATABASE__*` (SMB в режиме `local` допустим), а в `.env` `tabular-parser` — `OPENBAO__DATABASE__*` и `OPENBAO__SMB__*`; для MinIO по `http` нужен `OPENBAO__MINIO__MINIO_SECURE=false` (в `.env.example` его нет, по умолчанию `true`). ## Интеграционные тесты сервисов против compose (черновик) @@ -392,18 +397,13 @@ postgres nats minio`, затем `make test-integration` (или `make check-ful эмулятор ИС Сканирование поднимается в процессе интеграционного теста; контейнер `scan-emulator` профиля `services` тестами не используется. Общее описание проверок — [`checks.md`](checks.md). -> **Не запускайте интеграционные тесты `tabular-parser` и `scan-gateway` при поднятом профиле `services`.** Их -> сквозные тесты (`api/tests/integration/test_roundtrip.py`, у `scan-gateway` ещё `test_nats_minio_integration.py`) -> публикуют запросы в боевые `files.tabular_uploaded` / `files.img_uploaded`, а экземпляр сервиса из теста -> подписан в ту же queue group (`tabular-parser` / `scan-gateway`), что и контейнер. NATS отдаёт каждое сообщение группы -> одному подписчику — запрос теста может уйти контейнеру, и тест упадёт по таймауту ожидания ack (так прямо сказано в -> docstring теста `scan-gateway`). Риск подтверждён чтением кода тестов, тесты при поднятом профиле не запускались. -> Интеграционные тесты `arm-core` от контейнеров изолированы: временная БД на сессию (рабочая `armka` не используется) -> и уникальный subject результатов на тест. Перед тестами сервисов: -> `docker compose --profile services stop tabular-parser scan-gateway scan-emulator`; проверить, что чужих подписчиков -> нет: `curl -s 'http://127.0.0.1:8222/subsz?subs=1' | grep -o '"subject": *"recognition[^"]*"'` — строк -> `files.tabular_uploaded` / `files.img_uploaded` быть не должно (`files.tabular_parsed` — подписка контейнера -> `arm-core`, тестам `arm-core` не мешает). +> Интеграционные тесты `tabular-parser` и `scan-gateway` (`api/tests/integration/`) идут на уникальных subject'ах +> прогона (префиксы `itest.` / `test-.`), поэтому не пересекаются с контейнерами профиля `services` и друг с +> другом: **останавливать контейнеры стенда больше не нужно.** Интеграционные тесты `arm-core` от контейнеров +> изолированы: временная БД на сессию и уникальный subject результатов на тест. Исключение — `make e2e` ядра +> (`tests/e2e/`): он идёт на subject'ах по умолчанию (design R-7, OQ-83), поэтому ядро стенда (`arm-core` на +> `:8000`) на время прогона нужно остановить — общая queue group `arm-core`; контейнер `tabular-parser` стенда на +> умолчаниях ему, наоборот, нужен. ## Проверка контракта diff --git a/dev/contour-transfer-guide.md b/dev/contour-transfer-guide.md index 367f1c7..169acdd 100644 --- a/dev/contour-transfer-guide.md +++ b/dev/contour-transfer-guide.md @@ -183,9 +183,14 @@ issue #22; файл `api/src/core/settings_sources.py` одинаков в тр | Сервис | Секреты `OPENBAO__<ГРУППА>__*` | Настройки `OPENBAO__APP__*` | |---|---|---| -| arm-core | `OPENBAO__DATABASE__DATABASE_URL` (asyncpg, схема `dkb`), `OPENBAO__MINIO__{MINIO_URL, MINIO_ADMIN_LOGIN, MINIO_ADMIN_PASSWORD, MINIO_BUCKET}` (`MINIO_SECURE=true` по умолчанию), `OPENBAO__NATS__{NATS_URL, NATS_SEED, NATS_JWT}` | `NATS_ENABLED=true` (по умолчанию `false`: без него NATS не подключается и обмен с сервисами распознавания не работает), `DEBUG=true`, `AUTH_DEV_ISSUER=true`, `AUTH_DEV_SECRET=<случайная строка от 32 байт>` (поле верхнего уровня `Settings`, поэтому тоже через `OPENBAO__APP__`), `FORWARDED_ALLOW_IPS=<адреса прокси платформы>` (иначе IP в аудите — адрес прокси); `CORS_ORIGINS` при `DEBUG=true` не нужен. `SERVER__HOST` не задавать (платформа задаёт сама). **Временно, на период тестов:** `CONSOLIDATION_PERIOD_FROM_VERSION=true` — ОСВ, загруженная в консолидацию, берёт её период (arm-core#103, OQ-161; `qa-guide.md` §7.4); после тестов убрать | -| scan-gateway | `OPENBAO__MINIO__*`, `OPENBAO__NATS__*`, `OPENBAO__SMB__{SMB_HOST, SMB_PATH, SMB_LOGIN, SMB_PASSWORD}` | `SCAN_HOTFOLDER_MODE=smb` (`NATS_ENABLED` по умолчанию `true`, задавать не нужно). `SCAN_HOTFOLDER_PATH` в контуре не нужен (только режим `local`; режим `local` и эмулятор `make emulator` — для разработки) | -| tabular-parser | `OPENBAO__MINIO__*`, `OPENBAO__NATS__*` | сверх секретов ничего (`NATS_ENABLED` по умолчанию `true`); при необходимости `PARSE_TIMEOUT_SECONDS` (и `PARSE_MAX_BYTES`) | +| arm-core | `OPENBAO__DATABASE__DATABASE_URL` (asyncpg, схема `dkb`), `OPENBAO__MINIO__{MINIO_URL, MINIO_ADMIN_LOGIN, MINIO_ADMIN_PASSWORD, MINIO_BUCKET}` (`MINIO_SECURE=true` по умолчанию), `OPENBAO__NATS__{NATS_URL, NATS_SEED, NATS_JWT, NATS_TABULAR_REQUEST_SUBJECT, NATS_IMAGE_REQUEST_SUBJECT, NATS_RESULT_SUBJECT}` | `NATS_ENABLED=true` (по умолчанию `false`: без него NATS не подключается и обмен с сервисами распознавания не работает), `DEBUG=true`, `AUTH_DEV_ISSUER=true`, `AUTH_DEV_SECRET=<случайная строка от 32 байт>` (поле верхнего уровня `Settings`, поэтому тоже через `OPENBAO__APP__`), `FORWARDED_ALLOW_IPS=<адреса прокси платформы>` (иначе IP в аудите — адрес прокси); `CORS_ORIGINS` при `DEBUG=true` не нужен. `SERVER__HOST` не задавать (платформа задаёт сама). **Временно, на период тестов:** `CONSOLIDATION_PERIOD_FROM_VERSION=true` — ОСВ, загруженная в консолидацию, берёт её период (arm-core#103, OQ-161; `qa-guide.md` §7.4); после тестов убрать | +| scan-gateway | `OPENBAO__MINIO__*`, `OPENBAO__NATS__*` (включая три subject'а), `OPENBAO__SMB__{SMB_HOST, SMB_PATH, SMB_LOGIN, SMB_PASSWORD}` | `SCAN_HOTFOLDER_MODE=smb` (`NATS_ENABLED` по умолчанию `true`, задавать не нужно). `SCAN_HOTFOLDER_PATH` в контуре не нужен (только режим `local`; режим `local` и эмулятор `make emulator` — для разработки) | +| tabular-parser | `OPENBAO__MINIO__*`, `OPENBAO__NATS__*` (включая три subject'а) | сверх секретов ничего (`NATS_ENABLED` по умолчанию `true`); при необходимости `PARSE_TIMEOUT_SECONDS` (и `PARSE_MAX_BYTES`) | + +Для тестового контура — свой набор JWT, seed и трёх subject'ов (`NATS_TABULAR_REQUEST_SUBJECT` по умолчанию +`files.tabular_uploaded`, `NATS_IMAGE_REQUEST_SUBJECT` — `files.img_uploaded`, `NATS_RESULT_SUBJECT` — +`files.tabular_parsed`): одинаковый у всех трёх сервисов; JWT должен давать publish/subscribe на эти subject'ы. +Разъехавшиеся настройки — тишина на шине (dlq по NO_ACK); сверяйте `/healthcheck.subjects`. В отличие от arm-core, в `scan-gateway` и `tabular-parser` `NATS_ENABLED=true` по умолчанию, а `DEBUG` влияет только на уровень логов; платформенной аутентификации у них нет (внутренние сервисы без HTTP-входа для пользователей). diff --git a/dev/frontend-api-guide.md b/dev/frontend-api-guide.md index 08eff45..5cb1cf8 100644 --- a/dev/frontend-api-guide.md +++ b/dev/frontend-api-guide.md @@ -68,7 +68,7 @@ | Токен в проде | SSO РАИСА; параметры неизвестны (OQ-54) — держите слой «получить токен» отдельно от слоя «использовать токен» | | Dev-пользователи | `admin@`, `ka_go@`, `curator_go@`, `expert@`, `ka_rf@`, `rka@`, `client_manager@armka.local` — по одному на роль (таблица — `frontend-auth-guide.md` §3.1) | | Схема и Swagger | `/openapi.json`, `/docs`, `/redoc` — **только** при `DEBUG=true`; в проде 404 | -| Живость | `GET /healthcheck` (без префикса и без токена) → `{"uptime": "<время старта процесса, ISO>", "version": "1.0.0", "nats": "ok" \| "down" \| "off"}` — это не признак «вошёл» | +| Живость | `GET /healthcheck` (без префикса и без токена) → `{"uptime": "<время старта процесса, ISO>", "version": "1.0.0", "nats": "ok" \| "down" \| "off", "subjects": {"tabular_request": "…", "image_request": "…", "result": "…"}}` (`subjects` — служебное поле для диагностики, фронту не нужно) — это не признак «вошёл» | | CORS | при `DEBUG=true` — `*` без credentials; в проде — источники из `CORS_ORIGINS` | | Стенд целиком | `qa-guide.md` (docker compose, сиды, запуск бэка и фронта) | diff --git a/dev/qa-guide.md b/dev/qa-guide.md index 90503fe..048a973 100644 --- a/dev/qa-guide.md +++ b/dev/qa-guide.md @@ -952,6 +952,7 @@ Gitea: бэкенд и API — `RSHB/arm-core`, экраны — `RSHB/arm-web`, | PDF завис в «Обработка» | не запущены `scan-gateway`/`scan-emulator` (раздел 8, предусловия) | | PDF в статусе «Ошибка» (подсказка `INVALID_DOCUMENT`) | эмулятор знает только PDF из `fixtures/ocr` с JSON-ответом (6.8); свой PDF так и должен падать | | Импорт падает на записи файла в S3 | MinIO не поднят или не создан бакет: `docker compose logs minio-init` | +| `/healthcheck.subjects` ядра и сервиса различаются (или файл зависает в `UPLOADED` при `nats: ok`) | у сервисов разные `OPENBAO__NATS__NATS_*_SUBJECT`; выровняйте настройки у всех трёх | | `/healthcheck` показывает `nats: down` | NATS не поднят; на сценарии Среза 1 не влияет, но загрузка ОСВ (6.6) без NATS не дойдёт до парсера | | Файл завис в `UPLOADED`/`PROCESSING` | не запущен tabular-parser (`curl localhost:8001/healthcheck`) или ядро запущено без `NATS_ENABLED=true`; без ack файл через ~2 мин станет `ERROR` | diff --git a/openspec/changes/nats-env-config/tasks.md b/openspec/changes/nats-env-config/tasks.md index 4f8d767..0719fba 100644 --- a/openspec/changes/nats-env-config/tasks.md +++ b/openspec/changes/nats-env-config/tasks.md @@ -17,10 +17,10 @@ ## 3. Документы контекста -- [ ] 3.1 `context/03-architecture.md`: subject'ы — настройки окружения (таблица потоков, NATS платформы), перечень +- [x] 3.1 `context/03-architecture.md`: subject'ы — настройки окружения (таблица потоков, NATS платформы), перечень секретов `OPENBAO__NATS__*` по сервисам, требование одинакового набора для всех трёх сервисов -- [ ] 3.2 `dev/README.md` (таблица переменных, `/healthcheck`, интеграционные тесты), `dev/.env.example`, +- [x] 3.2 `dev/README.md` (таблица переменных, `/healthcheck`, интеграционные тесты), `dev/.env.example`, `dev/contour-transfer-guide.md` (секреты окружения), `dev/qa-guide.md` и `dev/frontend-api-guide.md` (ответ `/healthcheck`, если описан), `repo-templates/*.CLAUDE.md` при упоминании констант subject'ов -- [ ] 3.3 `context/07-open-questions.md`: OQ-83 — интеграционные тесты `tabular-parser`/`scan-gateway` на своих +- [x] 3.3 `context/07-open-questions.md`: OQ-83 — интеграционные тесты `tabular-parser`/`scan-gateway` на своих subject'ах; для e2e ядра вопрос остаётся (общая queue group `arm-core`, design R-7) -- 2.47.2 From b60ed850a0104b33448c8a9fd913765586ba876c Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 12:55:48 +0400 Subject: [PATCH 06/11] =?UTF-8?q?docs(dev):=20=D1=83=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D1=8C=20OQ-83=20=D0=B8=20SecretStr=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20NATS=5FJWT/NATS=5FSEED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Шапка черновика интеграционных тестов: OQ-83 закрыт частично (тесты tabular-parser и scan-gateway идут на своих subject'ах), make e2e ядра остаётся открытым (design R-7). - Утверждение про SecretStr вынесено в отдельное предложение. Co-Authored-By: Claude Opus 5.5 --- dev/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dev/README.md b/dev/README.md index e3e0adf..e147ffb 100644 --- a/dev/README.md +++ b/dev/README.md @@ -332,7 +332,9 @@ MinIO; полная очистка — `docker compose --profile services down - | `OPENBAO__SMB__SMB_PASSWORD` | `password` | заглушка SMB | | `NATS_ENABLED` | `true` | включает подключение arm-core к NATS; без него `/healthcheck` показывает `nats=off` | -Три subject'а одинаковы у всех трёх сервисов (непустые, без пробелов, `*`, `>` и пустых токенов, попарно различны; `NATS_JWT` и `NATS_SEED` — `SecretStr`). Прод и тест — свои наборы; разъехавшиеся настройки дают тишину на шине, сверяйте `/healthcheck.subjects`. +Три subject'а одинаковы у всех трёх сервисов (непустые, без пробелов, `*`, `>` и пустых токенов, попарно различны). + +`NATS_JWT` и `NATS_SEED` — `SecretStr`. Прод и тест — свои наборы; разъехавшиеся настройки дают тишину на шине, сверяйте `/healthcheck.subjects`. Два блока описывают одни и те же ресурсы: **при смене порта, пароля, пользователя или бакета меняйте обе части.** @@ -384,7 +386,8 @@ curl -s localhost:<порт сервиса>/healthcheck ## Интеграционные тесты сервисов против compose (черновик) > Черновик: состав целей и порядок проверок описаны в [`checks.md`](checks.md); открытые вопросы — OQ-83 (общий NATS -> у контейнеров и тестов) и OQ-84. +> у контейнеров и тестов; закрыто частично: интеграционные тесты `tabular-parser` и `scan-gateway` идут на своих +> subject'ах прогона, `make e2e` ядра по-прежнему открыт, design R-7) и OQ-84. Общий набор целей (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`) есть во всех сервисных репозиториях (`arm-core`, `tabular-parser`, `scan-gateway`); семантика, состав `check-full`, порядок перед PR и пропуск тестов без -- 2.47.2 From 0103c6fea7e371355d2e51b98124172896f520fd Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 13:18:15 +0400 Subject: [PATCH 07/11] =?UTF-8?q?openspec:=20nats-env-config=20=E2=80=94?= =?UTF-8?q?=20=D0=BE=D1=82=D0=BC=D0=B5=D1=87=D0=B5=D0=BD=D1=8B=20=D0=BF?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=82=D1=8B=201.x=20=D0=B8=202.x=20tasks.md?= =?UTF-8?q?=20(#80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/nats-env-config/tasks.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openspec/changes/nats-env-config/tasks.md b/openspec/changes/nats-env-config/tasks.md index 0719fba..1447d63 100644 --- a/openspec/changes/nats-env-config/tasks.md +++ b/openspec/changes/nats-env-config/tasks.md @@ -1,18 +1,18 @@ ## 1. Контракт (docs) -- [ ] 1.1 Дельта спеки `recognition-contract` (новое требование «Subject'ы NATS — настройки окружения», два изменённых); +- [x] 1.1 Дельта спеки `recognition-contract` (новое требование «Subject'ы NATS — настройки окружения», два изменённых); Purpose спеки при архивации: subject'ы — настройки с умолчаниями `files.*` -- [ ] 1.2 `contracts/recognition-request.schema.json`, `contracts/recognition-result-message.schema.json`: в описаниях +- [x] 1.2 `contracts/recognition-request.schema.json`, `contracts/recognition-result-message.schema.json`: в описаниях subject'ов — «настройка окружения, по умолчанию …» (только `description`, `schema_version` не меняется; копии в сервисах обновляются побайтно) ## 2. Сервисы (дочерние change'и `nats-env-config`) -- [ ] 2.1 RSHB/arm-core#106: `NATSSettings` (3 subject'а, `SecretStr`, валидация), `hide_input_in_errors`, публикация и +- [x] 2.1 RSHB/arm-core#106: `NATSSettings` (3 subject'а, `SecretStr`, валидация), `hide_input_in_errors`, публикация и подписка по настройкам, `/healthcheck.subjects`, лог при старте, unit-тесты (e2e — на умолчаниях, design R-7), `.env.example`, спека `recognition-job-exchange`, README -- [ ] 2.2 RSHB/arm-tabular-parser#31: то же для подписки `tabular_request` и публикации `result`, спека +- [x] 2.2 RSHB/arm-tabular-parser#31: то же для подписки `tabular_request` и публикации `result`, спека `tabular-recognition-service` -- [ ] 2.3 RSHB/arm-scan-gateway#32: то же для подписки `image_request` и публикации `result`, спека +- [x] 2.3 RSHB/arm-scan-gateway#32: то же для подписки `image_request` и публикации `result`, спека `image-recognition-gateway` ## 3. Документы контекста -- 2.47.2 From 32fa327621298d2d45ce6c3a5e59b8a4a39821f3 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 13:26:47 +0400 Subject: [PATCH 08/11] =?UTF-8?q?docs:=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=D1=83=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20nat?= =?UTF-8?q?s-env-config=20(M-1,=20M-2,=20M-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dev/README.md: примеры /healthcheck дополнены subjects по сервисам - design.md (решение 7) и dev/README.md: уточнено, что запросы в тестах ядра идут на умолчательные subject'ы - repo-templates: имена files.* помечены как умолчания Co-Authored-By: Claude Opus 5.5 --- dev/README.md | 10 ++++++---- openspec/changes/nats-env-config/design.md | 4 +++- repo-templates/scan-gateway.CLAUDE.md | 2 +- repo-templates/tabular-parser.CLAUDE.md | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/dev/README.md b/dev/README.md index e147ffb..bfd7017 100644 --- a/dev/README.md +++ b/dev/README.md @@ -101,9 +101,9 @@ docker compose --profile services ps -a # scan-emulator Up 8000/tcp (healthcheck нет: процесс без HTTP) # scan-gateway Up (healthy) 127.0.0.1:8002->8000/tcp # tabular-parser Up (healthy) 127.0.0.1:8001->8000/tcp -curl -s localhost:8000/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok"} -curl -s localhost:8001/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok"} -curl -s localhost:8002/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok","hotfolder":"ok"} +curl -s localhost:8000/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok","subjects":{"tabular_request":"…","image_request":"…","result":"…"}} +curl -s localhost:8001/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok","subjects":{"tabular_request":"…","result":"…"}} +curl -s localhost:8002/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok","hotfolder":"ok","subjects":{"image_request":"…","result":"…"}} docker compose --profile services logs arm-core-migrate # Running upgrade -> 0001_dbml_v5_13 …; Seed применён … docker compose --profile services logs scan-emulator # … корень /data/hotfolder …; Фикстуры: /fixtures/ocr, фикстур: 2 ``` @@ -403,7 +403,9 @@ postgres nats minio`, затем `make test-integration` (или `make check-ful > Интеграционные тесты `tabular-parser` и `scan-gateway` (`api/tests/integration/`) идут на уникальных subject'ах > прогона (префиксы `itest.` / `test-.`), поэтому не пересекаются с контейнерами профиля `services` и друг с > другом: **останавливать контейнеры стенда больше не нужно.** Интеграционные тесты `arm-core` от контейнеров -> изолированы: временная БД на сессию и уникальный subject результатов на тест. Исключение — `make e2e` ядра +> изолированы: временная БД на сессию и уникальный subject ответа на тест; публикации запросов в тестах ядра +> (`test_recognition_exchange.py`, `test_api_files_upload.py`) идут на умолчательные subject'ы запросов и достигают +> контейнеров стенда. Исключение — `make e2e` ядра > (`tests/e2e/`): он идёт на subject'ах по умолчанию (design R-7, OQ-83), поэтому ядро стенда (`arm-core` на > `:8000`) на время прогона нужно остановить — общая queue group `arm-core`; контейнер `tabular-parser` стенда на > умолчаниях ему, наоборот, нужен. diff --git a/openspec/changes/nats-env-config/design.md b/openspec/changes/nats-env-config/design.md index fadc2ae..083ddc6 100644 --- a/openspec/changes/nats-env-config/design.md +++ b/openspec/changes/nats-env-config/design.md @@ -47,7 +47,9 @@ Goals: subject'ы и учётные данные NATS задаются секр задают уникальные subject'ы через настройки и не пересекаются с контейнерами стенда. e2e ядра (`make e2e`) остаётся на subject'ах по умолчанию: он гоняет настоящий контейнер `tabular-parser` стенда, который слушает умолчания, а конфликт там — в общей queue group `arm-core` на subject'е ответа; требование останавливать ядро стенда сохраняется - (R-7). Интеграционные тесты NATS ядра уже работают на уникальных subject'ах. + (R-7). Интеграционные тесты NATS ядра идут на уникальном subject'е ответа; публикации запросов в тестах ядра + (`test_recognition_exchange.py`, `test_api_files_upload.py`) идут на умолчательные subject'ы запросов и достигают + контейнеров стенда. ## Risks / Trade-offs diff --git a/repo-templates/scan-gateway.CLAUDE.md b/repo-templates/scan-gateway.CLAUDE.md index 042252b..5753dd3 100644 --- a/repo-templates/scan-gateway.CLAUDE.md +++ b/repo-templates/scan-gateway.CLAUDE.md @@ -7,7 +7,7 @@ (временный `.part` — в `Tmp`), забрать все `Исходящие//*.json` (по одному json на документ), сохранить склеенный сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить горячую папку (только при `SCAN_CLEANUP=true`), нормализовать в `../docs/contracts/recognition-result.schema.json` (`statementDocument`), опубликовать -`files.tabular_parsed`. Таймаут 5 мин → `TIMEOUT`. **Нет доступа к БД.** +`files.tabular_parsed` (по умолчанию; задаётся OPENBAO__NATS__NATS_*_SUBJECT). Таймаут 5 мин → `TIMEOUT`. **Нет доступа к БД.** ## Правила - Нормализация по фактическим ответам `../fixtures/ocr/aep_*.json`, а не по ТЗ (OQ-13, OQ-14): confidence → 0–100, diff --git a/repo-templates/tabular-parser.CLAUDE.md b/repo-templates/tabular-parser.CLAUDE.md index 912dfeb..d8d47cf 100644 --- a/repo-templates/tabular-parser.CLAUDE.md +++ b/repo-templates/tabular-parser.CLAUDE.md @@ -4,8 +4,8 @@ ## Что это Stateless-сервис разбора XLS/XLSX/XLSB: ОСВ по счетам (§1 правил) и ББ/ОФР из Excel (§12). Вход — NATS -`files.tabular_uploaded` + файл в S3 `uploads/`; выход — JSON по `../docs/contracts/recognition-result.schema.json` -в S3 `results/` + NATS `files.tabular_parsed`. **Нет доступа к БД.** Не знает про организации, периметры, план счетов — +`files.tabular_uploaded` (по умолчанию; задаётся OPENBAO__NATS__NATS_*_SUBJECT) + файл в S3 `uploads/`; выход — JSON по `../docs/contracts/recognition-result.schema.json` +в S3 `results/` + NATS `files.tabular_parsed` (по умолчанию; задаётся OPENBAO__NATS__NATS_*_SUBJECT). **Нет доступа к БД.** Не знает про организации, периметры, план счетов — возвращает структуру файла; нормализация и резолв — в arm-core. ## Главное правило -- 2.47.2 From 62d7fd780e150afee4dde5fbbc537f6ec05d7b3c Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 13:36:06 +0400 Subject: [PATCH 09/11] =?UTF-8?q?openspec:=20nats-env-config=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20R-8=E2=80=A6R-1?= =?UTF-8?q?3=20=D0=B2=20design.md=20(#80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/nats-env-config/design.md | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openspec/changes/nats-env-config/design.md b/openspec/changes/nats-env-config/design.md index 083ddc6..c582207 100644 --- a/openspec/changes/nats-env-config/design.md +++ b/openspec/changes/nats-env-config/design.md @@ -79,3 +79,26 @@ Goals: subject'ы и учётные данные NATS задаются секр стенда на умолчаниях, а развести ядро теста и ядро стенда может только своя queue group, которая в этом change не настраивается. OQ-83 для `make e2e` остаётся открытым. Цена ошибки: перед `make e2e` по-прежнему останавливать ядро стенда. +8. 13.10.2026 — **R-8:** задачи 2/4/6 (настройки сервисов) и затем 3/5/7 выполнялись параллельно по репозиториям + (общих файлов нет); ревью — по каждой задаче отдельно. Цена ошибки: нулевая (разные репозитории). +9. 13.10.2026 — **R-9:** трейлер коммитов — `Co-Authored-By: Claude Opus 5.5` по вводной; у двух коммитов + исполнителей (haiku) был Haiku 5.5 — исправлено `commit --amend` до публикации ветки. Цена ошибки: нулевая. +10. 13.10.2026 — **R-10:** minor-находки задач, усиливающие тесты (T2.1, T2.2, T3.1, T3.2, T4.1, T5.1–T5.3, T5.6, T6.1, + T6.3, T8.2, T8.3), исправлены одной волной после задач 1–8; косметика (T7.1 — позже исправлена, T7.2, T5.4 — + позже исправлена) и T8.1 отложены/отклонены в ledger. Тест T2.2 (утечка JWT при невалидном subject) не ловит + отсутствие `hide_input_in_errors` (ошибка field-validator не печатает вход группы) — оставлен как регрессионный; + защиту несёт `test_validation_error_hides_nats_secrets`. Цена ошибки: низкая. +11. 13.10.2026 — **R-11:** по финальному ревью (opus) исправлены I-1 (повторная публикация обновляет + `recognition_job.request_subject`), M-1 (примеры `/healthcheck`), M-2 (формулировка про интеграционные тесты ядра — + запросы тестов ядра идут на умолчательные subject'ы), M-3 (docstring'и и логи не называют умолчание фактическим + subject'ом), M-5 (пометки «по умолчанию» в CLAUDE.md и шаблонах). **M-4** (базовые спеки с `SHALL … files.*` + формально противоречат ADDED-требованию) — отложено: решение при архивации (MODIFIED) или отдельным change'ем; + **M-7** (ключи в `.env.example` сервисов закомментированы, у ядра — нет) отклонено: у ядра паритет + `.env.example`/`.env.test` проверяет тест, в сервисах умолчание работает без ключей. Цена ошибки M-4: после + архивации в базовых спеках остаются два формально разных требования о subject'ах; M-7: нулевая. +12. 13.10.2026 — **R-12:** `make check` в docs запускался с `CHANGE=nats-env-config` — дефолт `CHANGE ?= slice-0-skeleton` + указывает на архивный change (O-2, вне объёма). Цена ошибки: нулевая. +13. 13.10.2026 — **R-13:** `check-full` в tabular-parser проверен на временной ветке `main` + feat/31: ветка отстаёт от + main (tp#33), общий `fixtures/` уже содержит golden после fixtures#18; на ветке без merge main 2 теста + `tests/fixtures` красные, после merge — 2313 passed. Ветку не перебазировал. Цена ошибки: перед merge PR — тот же + расклад, решение (rebase/merge main) за владельцем. -- 2.47.2 From 31d3742dd5ee8c93f7d4a9af8e8037edabbc1378 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 13:39:22 +0400 Subject: [PATCH 10/11] =?UTF-8?q?design=20nats-env-config:=20R-14=20?= =?UTF-8?q?=E2=80=94=20=D0=B2=D0=B5=D1=82=D0=BA=D0=B8=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B1=D0=B0=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B0=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/nats-env-config/design.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openspec/changes/nats-env-config/design.md b/openspec/changes/nats-env-config/design.md index c582207..aaff9d3 100644 --- a/openspec/changes/nats-env-config/design.md +++ b/openspec/changes/nats-env-config/design.md @@ -102,3 +102,6 @@ Goals: subject'ы и учётные данные NATS задаются секр main (tp#33), общий `fixtures/` уже содержит golden после fixtures#18; на ветке без merge main 2 теста `tests/fixtures` красные, после merge — 2313 passed. Ветку не перебазировал. Цена ошибки: перед merge PR — тот же расклад, решение (rebase/merge main) за владельцем. +14. 13.10.2026 — **R-14:** после отчёта контроллера ветки docs `feat/80` и tabular-parser `feat/31` (ещё не + опубликованные) перебазированы на свежий `main` (docs#98, tp#33) — снимает расклад R-13 без merge-коммита; force-push + не нужен. `check-full` сервисов и `make check` docs перезапущены на перебазированных ветках. Цена ошибки: нулевая. -- 2.47.2 From 3a3167441de6eea51b213f4b8fc3a88ee5cb8772 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 13:40:35 +0400 Subject: [PATCH 11/11] =?UTF-8?q?qa-guide:=20=D1=81=D0=B8=D0=BC=D0=BF?= =?UTF-8?q?=D1=82=D0=BE=D0=BC=20=D1=80=D0=B0=D0=B7=D1=8A=D0=B5=D1=85=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=B8=D1=85=D1=81=D1=8F=20subject'=D0=BE=D0=B2=20?= =?UTF-8?q?=D1=83=D1=82=D0=BE=D1=87=D0=BD=D1=91=D0=BD=20(T8.1);=20design?= =?UTF-8?q?=20R-15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- dev/qa-guide.md | 2 +- openspec/changes/nats-env-config/design.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dev/qa-guide.md b/dev/qa-guide.md index 048a973..dcb2fa1 100644 --- a/dev/qa-guide.md +++ b/dev/qa-guide.md @@ -952,7 +952,7 @@ Gitea: бэкенд и API — `RSHB/arm-core`, экраны — `RSHB/arm-web`, | PDF завис в «Обработка» | не запущены `scan-gateway`/`scan-emulator` (раздел 8, предусловия) | | PDF в статусе «Ошибка» (подсказка `INVALID_DOCUMENT`) | эмулятор знает только PDF из `fixtures/ocr` с JSON-ответом (6.8); свой PDF так и должен падать | | Импорт падает на записи файла в S3 | MinIO не поднят или не создан бакет: `docker compose logs minio-init` | -| `/healthcheck.subjects` ядра и сервиса различаются (или файл зависает в `UPLOADED` при `nats: ok`) | у сервисов разные `OPENBAO__NATS__NATS_*_SUBJECT`; выровняйте настройки у всех трёх | +| `/healthcheck.subjects` ядра и сервиса различаются (или файл при `nats: ok` стоит в `UPLOADED`, а через ~2 мин становится `ERROR` — ack не пришёл) | у сервисов разные `OPENBAO__NATS__NATS_*_SUBJECT`; выровняйте настройки у всех трёх | | `/healthcheck` показывает `nats: down` | NATS не поднят; на сценарии Среза 1 не влияет, но загрузка ОСВ (6.6) без NATS не дойдёт до парсера | | Файл завис в `UPLOADED`/`PROCESSING` | не запущен tabular-parser (`curl localhost:8001/healthcheck`) или ядро запущено без `NATS_ENABLED=true`; без ack файл через ~2 мин станет `ERROR` | diff --git a/openspec/changes/nats-env-config/design.md b/openspec/changes/nats-env-config/design.md index aaff9d3..324fc1a 100644 --- a/openspec/changes/nats-env-config/design.md +++ b/openspec/changes/nats-env-config/design.md @@ -105,3 +105,7 @@ Goals: subject'ы и учётные данные NATS задаются секр 14. 13.10.2026 — **R-14:** после отчёта контроллера ветки docs `feat/80` и tabular-parser `feat/31` (ещё не опубликованные) перебазированы на свежий `main` (docs#98, tp#33) — снимает расклад R-13 без merge-коммита; force-push не нужен. `check-full` сервисов и `make check` docs перезапущены на перебазированных ветках. Цена ошибки: нулевая. +15. 13.10.2026 — **R-15:** отложенные контроллером RM-1 (лог `file_mismatch` с литералом `files.tabular_parsed`) и RM-2 + (переносы docstring'ов в arm-core) исправлены сразу — правка в три строки, follow-up не нужен. T8.1 сверено с кодом + ядра: без ack задание после повторных публикаций уходит в `dlq`, файл — `UPLOADED` → `ERROR`; строка `qa-guide.md` + уточнена. Цена ошибки: нулевая. -- 2.47.2