From 22f55e5f7d32952301f612c779f3b31dbed8ad07 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 17:53:56 +0400 Subject: [PATCH 01/68] =?UTF-8?q?chore:=20=D0=BF=D0=BB=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?SDD=20=D0=B8=D0=B7=20tasks.md=20change'=D0=B0,=20=D1=81=D0=BB?= =?UTF-8?q?=D1=83=D0=B6=D0=B5=D0=B1=D0=BD=D1=8B=D0=B5=20=D0=BA=D0=B0=D1=82?= =?UTF-8?q?=D0=B0=D0=BB=D0=BE=D0=B3=D0=B8=20=D0=B2=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/superpowers/plans/2026-10-03-slice-0-skeleton*.md сгенерирован 1:1 из openspec/changes/slice-0-skeleton/tasks.md (заголовки «Task N» для скриптов subagent-driven-development, Global Constraints из спеки и CLAUDE.md). .gitignore: .codegraph/, .superpowers/, .bounded, .DS_Store. Co-Authored-By: Claude Fable 5.1 --- .gitignore | 3 ++ .../2026-10-03-slice-0-skeleton-phase1.md | 45 +++++++++++++++++++ .../2026-10-03-slice-0-skeleton-phase2.md | 45 +++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md create mode 100644 docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md diff --git a/.gitignore b/.gitignore index e43b0f9..a46bd45 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .DS_Store +.codegraph/ +.superpowers/ +.bounded diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md new file mode 100644 index 0000000..5749967 --- /dev/null +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md @@ -0,0 +1,45 @@ +# Срез 0 — сквозной скелет (docs) — фаза 1: контракт и инфраструктура — план исполнения (Срез 0) + +> **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. + +**Branch:** `feat/1-slice-0-skeleton` (worktree `.wt/slice-0/docs`). **Issue:** #1. +**Spec:** `openspec/changes/slice-0-skeleton/` — proposal.md, design.md, specs/*/spec.md этого change'а. Спека — единственный источник истины при рулингах; этот план сгенерирован из её `tasks.md` 1:1 (номера в скобках — исходные номера задач tasks.md; по завершении задачи отметить её `- [x]` и в `tasks.md`). + +## Global Constraints + +- Репозиторий `arm-ka-docs`: документы, контракты, `docs/dev/`. `docs/source/**` не редактировать. +- Контракт v1.0: три схемы JSON Schema 2020-12 (`recognition-request`, `recognition-result-message`, `recognition-result`), `schema_version` = `"1.0"`; статусы конверта `processing | done | needs_check | error`; коды ошибок — перечень из spec `recognition-contract` (включая `NOT_IMPLEMENTED`); деньги — строки `^-?\d+(\.\d{1,2})?$`; `Nats-Msg-Id`: `:`, `ack::`, `result::`. +- Платформа: Python 3.11, PostgreSQL 16, NATS 2.10 **без JetStream**, один бакет S3 `arm-ka`; Nexus недоступен; CI нет — `make check` / `make check-full`. +- Compose собирает сервисы из `../../` относительно `docs/dev/`; версии образов фиксированы. +- Язык документов и коммитов — русский; коммит заканчивается строкой `Co-Authored-By: Claude Fable 5.1 `. + +## Задачи + +### Task 1: [1.1] [docs] `docs/contracts/recognition-result.schema.json` + +- [ ] [docs] `docs/contracts/recognition-result.schema.json`: снять «ЧЕРНОВИК» из title, добавить `$comment` о версионировании, добавить `NOT_IMPLEMENTED` в `error.code`; проверить `python -m jsonschema` на файле схемы (метасхема 2020-12 валидна) + +### Task 2: [1.2] [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recogniti + +- [ ] [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recognition-contract` (поля, enum, форматы uuid/date-time); проверить метасхемой + +### Task 3: [1.3] [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт + +- [ ] [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт: `status` ∈ processing | done | needs_check | error; условные обязательные поля через `if/then`); проверить метасхемой + +### Task 4: [1.4] [docs] Примеры `docs/contracts/examples/` + +- [ ] [docs] Примеры `docs/contracts/examples/`: `request-tabular.json`, `request-image.json`, `result-message-processing.json`, `result-message-done.json`, `result-message-error.json`, `result-osv-minimal.json`, `result-statement-minimal.json`, `result-error.json`; скрипт `docs/scripts/validate_contracts.py` валидирует все примеры схемами и возвращает 0 + +### Task 5: [1.5] [docs] `03-architecture.md` + +- [ ] [docs] `03-architecture.md`: таблица «Контракт обмена через NATS» — ссылки на три файла схем, правила `Nats-Msg-Id`, ack через `processing`, повторная публикация; проверить, что текст ссылается на существующие файлы + +### Task 6: [2.1] [docs] `docs/dev/docker-compose.yml` + +- [ ] [docs] `docs/dev/docker-compose.yml`: сервисы `postgres` (16), `nats` (2.10, без `--jetstream`), `minio`, `minio-init` (создаёт бакет `arm-ka`), healthcheck'и, именованные тома; `docs/dev/.env.example`; проверка: `docker compose up -d` → все healthy, `mc ls` видит бакет, `curl :8222/varz` отвечает + +### Task 7: [2.2] [docs] `docs/dev/README.md` + +- [ ] [docs] `docs/dev/README.md`: запуск инфраструктуры, переменные, сброс томов, запуск сервисов из venv без Nexus (`make venv` фильтрует `raisa_*`), черновик — дописывается в 5.4 + diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md new file mode 100644 index 0000000..815038a --- /dev/null +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md @@ -0,0 +1,45 @@ +# Срез 0 — сквозной скелет (docs) — фаза 2: проверки, профиль services, документы — план исполнения (Срез 0) + +> **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. + +**Branch:** `feat/1-slice-0-skeleton` (worktree `.wt/slice-0/docs`). **Issue:** #1. +**Spec:** `openspec/changes/slice-0-skeleton/` — proposal.md, design.md, specs/*/spec.md этого change'а. Спека — единственный источник истины при рулингах; этот план сгенерирован из её `tasks.md` 1:1 (номера в скобках — исходные номера задач tasks.md; по завершении задачи отметить её `- [x]` и в `tasks.md`). + +## Global Constraints + +- Репозиторий `arm-ka-docs`: документы, контракты, `docs/dev/`. `docs/source/**` не редактировать. +- Контракт v1.0: три схемы JSON Schema 2020-12 (`recognition-request`, `recognition-result-message`, `recognition-result`), `schema_version` = `"1.0"`; статусы конверта `processing | done | needs_check | error`; коды ошибок — перечень из spec `recognition-contract` (включая `NOT_IMPLEMENTED`); деньги — строки `^-?\d+(\.\d{1,2})?$`; `Nats-Msg-Id`: `:`, `ack::`, `result::`. +- Платформа: Python 3.11, PostgreSQL 16, NATS 2.10 **без JetStream**, один бакет S3 `arm-ka`; Nexus недоступен; CI нет — `make check` / `make check-full`. +- Compose собирает сервисы из `../../` относительно `docs/dev/`; версии образов фиксированы. +- Язык документов и коммитов — русский; коммит заканчивается строкой `Co-Authored-By: Claude Fable 5.1 `. + +## Задачи + +### Task 1: [4.1] [docs] `docs/dev/checks.md` + +- [ ] [docs] `docs/dev/checks.md`: общие цели `Makefile` (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`), что делает каждая, порядок перед PR, как пропускаются тесты без фикстур/docs; проверка: цели `arm-core` соответствуют описанию + +### Task 2: [4.2] [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` се + +- [ ] [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` сервисов через `include` или копируется); проверка: `make check` в трёх репозиториях вызывает одинаковые команды (`diff` общих целей) + +### Task 3: [5.3] [docs] `docker-compose.yml` + +- [ ] [docs] `docker-compose.yml`: профиль `services` — `arm-core` (8000), `tabular-parser` (8001), `scan-gateway` (8002), `scan-emulator` (том `hotfolder`, `../../fixtures/ocr:ro`); проверка: `--profile services up --build`, все `/healthcheck` 200 с `nats = ok` + +### Task 4: [5.4] [docs] Сквозная проверка руками по README + +- [ ] [docs] Сквозная проверка руками по README: вставить `recognition_job` в БД через `psql` (или тестовый скрипт arm-core) → паблишер публикует → `tabular-parser` отвечает ack и `NOT_IMPLEMENTED` → `recognition_job.status` проходит `published → processing → error`, `error_code = NOT_IMPLEMENTED`; то же для `image` через эмулятор (`raw/{job_id}.json` появляется в MinIO); остановить `tabular-parser`, вставить задание — через 30 с повторная публикация, после трёх — `dlq`; зафиксировать в README как «проверка окружения» + +### Task 5: [6.1] [docs] `06-test-data.md` §6 + +- [ ] [docs] `06-test-data.md` §6: `fixtures/directories/` — только тестовые ЮЛ/ГК (Срез 1); план счетов, правила элиминации, права — `arm-core/api/seed/`; `09-delivery-plan.md` Срез 0: решение D-1 по пакету контрактов, ссылка на `docs/dev/`, проверки вместо CI + +### Task 6: [6.2] [docs] `07-open-questions.md` + +- [ ] [docs] `07-open-questions.md`: OQ по итогам Среза 0 (контракт `data_type = NU` vs enum `osv_data_type`; обязательность ОСВ расширенного подхода; источник полного плана счетов 1С) и расхождения макета из задачи B; статусы допущений + +### Task 7: [6.3] [docs] Итоговая проверка + +- [ ] [docs] Итоговая проверка: `openspec validate slice-0-skeleton --strict` зелёный в четырёх репозиториях; ссылки между документами ведут на существующие файлы (`grep` по именам файлов) + -- 2.47.2 From 8e1a1ce4f587c7e968355a0acada15343569c4cb Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 17:55:37 +0400 Subject: [PATCH 02/68] =?UTF-8?q?docs:=20design.md=20=E2=80=94=20=D1=80?= =?UTF-8?q?=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=2013=E2=80=9315=20(SDD,=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B7=D0=B0=201=20=D1=81=D1=80=D0=B5=D0=B7=D0=B0?= =?UTF-8?q?=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- openspec/changes/slice-0-skeleton/design.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index fc3dc0a..ae4c948 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -144,3 +144,6 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 10 | 2026-10-03 | D-9: `python:3.11-slim`, PostgreSQL 16, NATS 2.10 без JS, `node:22` до ответа | ответы пользователя; Node неизвестен | смена тега в compose/Dockerfile | | 11 | 2026-10-03 | Worktree среза — зеркальный workspace `.wt/<срез>/` с симлинками | относительные пути `../docs`, `../fixtures` и хуки должны работать в ветке | переместить worktree | | 12 | 2026-10-03 | D-10: `raisa_*` фильтруются при dev-установке, npm-пакеты `raisa-*` → `optionalDependencies` | Nexus недоступен до прода | вернуть две строки в `dependencies` | +| 13 | 2026-10-03 | Файл результата: пустой `documents` допустим только при `status = error` — выражено в схеме условием `if/then` (`minItems: 1`) | требование spec `recognition-contract` «Файл результата»; текст задачи 1.1 его не перечислял | ослабить/убрать условие в схеме, пересобрать копии контракта в сервисах | +| 14 | 2026-10-03 | Compose: фиксированное имя проекта `name: arm-ka`, порты хоста переопределяются через `.env` со значениями по умолчанию из spec | одинаковые тома/сети из любого worktree; порты 5432/4222 на машине разработчика могут быть заняты | смена имени проекта — потеря dev-томов (`down -v`), порты — правка `.env` | +| 15 | 2026-10-03 | Чекбоксы плана и `tasks.md` отмечает контроллер SDD отдельным коммитом после чистого ревью задачи | исполнитель не должен отмечать выполненным то, что ещё не прошло ревью | отметки правятся вручную | -- 2.47.2 From 65c3809efe04561f6cc42e248401fa0edd2490fd Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 17:58:47 +0400 Subject: [PATCH 03/68] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=20v1.0:=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D0=B0=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D0=BF=D0=BE=D0=B7=D0=BD=D0=B0=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B1=D0=B5=D0=B7=20=D0=BF=D0=BE=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D0=BA=D0=B8=20=C2=AB=D1=87=D0=B5=D1=80=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D0=BA=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - title без «ЧЕРНОВИК v1, уточнить в Срезе 2» - $comment о правилах версионирования (MAJOR.MINOR) - NOT_IMPLEMENTED в перечне error.code - пустой documents допустим только при status = error (if/then, minItems: 1 для done и needs_check) Co-Authored-By: Claude Fable 5.1 --- contracts/recognition-result.schema.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/contracts/recognition-result.schema.json b/contracts/recognition-result.schema.json index 010429d..0ef9333 100644 --- a/contracts/recognition-result.schema.json +++ b/contracts/recognition-result.schema.json @@ -1,8 +1,9 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "arm-ka/recognition-result/v1", - "title": "Результат распознавания файла (tabular-parser / scan-gateway) — ЧЕРНОВИК v1, уточнить в Срезе 2", + "title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v1.0", "description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Нормализация счетов, резолв контрагентов, проверки — в arm-core.", + "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR.", "type": "object", "required": ["schema_version", "job_id", "file_id", "service", "status", "documents"], "properties": { @@ -21,7 +22,7 @@ "error": { "type": ["object", "null"], "properties": { - "code": { "enum": ["UNSUPPORTED_FORMAT", "UNREADABLE", "TIMEOUT", "SCHEMA_MISMATCH", "FILE_READ_ERROR", "INVALID_FORMAT", "INVALID_DOCUMENT", "RECOGNITION_FAILED", "UNKNOWN_ERROR"] }, + "code": { "enum": ["UNSUPPORTED_FORMAT", "UNREADABLE", "TIMEOUT", "SCHEMA_MISMATCH", "NOT_IMPLEMENTED", "FILE_READ_ERROR", "INVALID_FORMAT", "INVALID_DOCUMENT", "RECOGNITION_FAILED", "UNKNOWN_ERROR"] }, "message": { "type": "string" } } }, @@ -43,6 +44,13 @@ "items": { "oneOf": [ { "$ref": "#/$defs/osvDocument" }, { "$ref": "#/$defs/statementDocument" } ] } } }, + "if": { + "properties": { "status": { "enum": ["done", "needs_check"] } }, + "required": ["status"] + }, + "then": { + "properties": { "documents": { "minItems": 1 } } + }, "$defs": { "money": { "type": ["string", "null"], "pattern": "^-?\\d+(\\.\\d{1,2})?$", "description": "Decimal как строка, чтобы не терять копейки в JSON" }, "osvDocument": { -- 2.47.2 From 1166abe42bd71364942a9a74bc5a5625abb26835 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 17:59:57 +0400 Subject: [PATCH 04/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=201?= =?UTF-8?q?=20(1.1)=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md index 5749967..f58635d 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md @@ -17,7 +17,7 @@ ### Task 1: [1.1] [docs] `docs/contracts/recognition-result.schema.json` -- [ ] [docs] `docs/contracts/recognition-result.schema.json`: снять «ЧЕРНОВИК» из title, добавить `$comment` о версионировании, добавить `NOT_IMPLEMENTED` в `error.code`; проверить `python -m jsonschema` на файле схемы (метасхема 2020-12 валидна) +- [x] [docs] `docs/contracts/recognition-result.schema.json`: снять «ЧЕРНОВИК» из title, добавить `$comment` о версионировании, добавить `NOT_IMPLEMENTED` в `error.code`; проверить `python -m jsonschema` на файле схемы (метасхема 2020-12 валидна) ### Task 2: [1.2] [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recogniti diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 9b098f5..377e9a5 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -5,7 +5,7 @@ ## 1. Контракт v1.0 (первым — от него зависят все сервисы) -- [ ] 1.1 [docs] `docs/contracts/recognition-result.schema.json`: снять «ЧЕРНОВИК» из title, добавить `$comment` о +- [x] 1.1 [docs] `docs/contracts/recognition-result.schema.json`: снять «ЧЕРНОВИК» из title, добавить `$comment` о версионировании, добавить `NOT_IMPLEMENTED` в `error.code`; проверить `python -m jsonschema` на файле схемы (метасхема 2020-12 валидна) - [ ] 1.2 [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recognition-contract` (поля, enum, -- 2.47.2 From 8efaba47bf757daff36efb3ebe0be84c8e8af736 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:00:35 +0400 Subject: [PATCH 05/68] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BA=D1=82:=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20=D1=80=D0=B0=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D0=B7=D0=BD=D0=B0=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20recognition-?= =?UTF-8?q?request=20v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSON Schema 2020-12 по spec recognition-contract: schema_version, job_id, file_id (uuid), pipeline, file_type, original_name, s3 {bucket, key}, reply_deadline (date-time), attempt (>= 1). Co-Authored-By: Claude Fable 5.1 --- contracts/recognition-request.schema.json | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 contracts/recognition-request.schema.json diff --git a/contracts/recognition-request.schema.json b/contracts/recognition-request.schema.json new file mode 100644 index 0000000..4545ef7 --- /dev/null +++ b/contracts/recognition-request.schema.json @@ -0,0 +1,28 @@ +{ + "$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 recognition.tabular.request или recognition.image.request; subject должен соответствовать полю pipeline (tabular → recognition.tabular.request, image → recognition.image.request). Заголовок Nats-Msg-Id = : (не поле тела). Один файл (stored_file) → один job.", + "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR.", + "type": "object", + "required": ["schema_version", "job_id", "file_id", "pipeline", "file_type", "original_name", "s3", "reply_deadline", "attempt"], + "properties": { + "schema_version": { "const": "1.0" }, + "job_id": { "type": "string", "format": "uuid", "description": "recognition_job.id" }, + "file_id": { "type": "string", "format": "uuid", "description": "stored_file.id" }, + "pipeline": { "enum": ["tabular", "image"], "description": "конвейер распознавания; определяет subject запроса" }, + "file_type": { "enum": ["XLS", "XLSX", "XLSB", "PDF", "PNG", "JPG"] }, + "original_name": { "type": "string", "minLength": 1, "description": "имя файла, как загрузил пользователь" }, + "s3": { + "type": "object", + "description": "исходный файл в роли uploads", + "required": ["bucket", "key"], + "properties": { + "bucket": { "type": "string", "minLength": 1 }, + "key": { "type": "string", "minLength": 1 } + } + }, + "reply_deadline": { "type": "string", "format": "date-time", "description": "срок ответа, UTC" }, + "attempt": { "type": "integer", "minimum": 1, "description": "номер попытки, с 1" } + } +} -- 2.47.2 From 4be67392abdf4eac9ee3766a4bfee9f20f12cfae Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:01:18 +0400 Subject: [PATCH 06/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=202?= =?UTF-8?q?=20(1.2)=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md index f58635d..b5f0292 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md @@ -21,7 +21,7 @@ ### Task 2: [1.2] [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recogniti -- [ ] [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recognition-contract` (поля, enum, форматы uuid/date-time); проверить метасхемой +- [x] [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recognition-contract` (поля, enum, форматы uuid/date-time); проверить метасхемой ### Task 3: [1.3] [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 377e9a5..502b983 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -8,7 +8,7 @@ - [x] 1.1 [docs] `docs/contracts/recognition-result.schema.json`: снять «ЧЕРНОВИК» из title, добавить `$comment` о версионировании, добавить `NOT_IMPLEMENTED` в `error.code`; проверить `python -m jsonschema` на файле схемы (метасхема 2020-12 валидна) -- [ ] 1.2 [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recognition-contract` (поля, enum, +- [x] 1.2 [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recognition-contract` (поля, enum, форматы uuid/date-time); проверить метасхемой - [ ] 1.3 [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт: `status` ∈ processing | done | needs_check | error; условные обязательные поля через `if/then`); проверить метасхемой -- 2.47.2 From a57cf2db49a7087e44084602bd4ca500ecfbcc7b Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:02:05 +0400 Subject: [PATCH 07/68] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BA=D1=82:=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0=20=D1=81=D0=BE?= =?UTF-8?q?=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D0=B0=20recognition-result?= =?UTF-8?q?-message=20v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Конверт на subject recognition.result: status processing | done | needs_check | error, условные обязательные поля через if/then (done/needs_check → result_s3 и result_schema_version; error → error). group_schema в конверт не входит. Co-Authored-By: Claude Fable 5.1 --- .../recognition-result-message.schema.json | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 contracts/recognition-result-message.schema.json diff --git a/contracts/recognition-result-message.schema.json b/contracts/recognition-result-message.schema.json new file mode 100644 index 0000000..2a19195 --- /dev/null +++ b/contracts/recognition-result-message.schema.json @@ -0,0 +1,70 @@ +{ + "$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 recognition.result. Заголовок Nats-Msg-Id (не поле тела) бывает трёх видов: : — запрос; ack:: — для status = processing; result:: — для done, needs_check и error. status = processing — ack «взял в работу», публикуется до чтения файла из S3. group_schema в конверт не входит: источник — файл результата (recognition-result).", + "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR.", + "type": "object", + "required": ["schema_version", "job_id", "file_id", "status", "service"], + "properties": { + "schema_version": { "const": "1.0" }, + "job_id": { "type": "string", "format": "uuid", "description": "recognition_job.id" }, + "file_id": { "type": "string", "format": "uuid", "description": "stored_file.id" }, + "status": { "enum": ["processing", "done", "needs_check", "error"] }, + "service": { + "type": "object", + "required": ["name", "version"], + "properties": { + "name": { "enum": ["tabular-parser", "scan-gateway"] }, + "version": { "type": "string" } + } + }, + "result_s3": { + "type": "object", + "description": "файл результата (recognition-result): results/{file_id}/{job_id}/result.json", + "required": ["bucket", "key"], + "properties": { + "bucket": { "type": "string", "minLength": 1 }, + "key": { "type": "string", "minLength": 1 } + } + }, + "result_schema_version": { "type": "string", "pattern": "^\\d+\\.\\d+$", "description": "schema_version файла результата, MAJOR.MINOR" }, + "error": { + "type": "object", + "required": ["code", "message"], + "properties": { + "code": { "enum": ["UNSUPPORTED_FORMAT", "UNREADABLE", "TIMEOUT", "SCHEMA_MISMATCH", "NOT_IMPLEMENTED", "FILE_READ_ERROR", "INVALID_FORMAT", "INVALID_DOCUMENT", "RECOGNITION_FAILED", "UNKNOWN_ERROR"] }, + "message": { "type": "string" } + } + }, + "raw_s3_key": { "type": ["string", "null"], "description": "scan-gateway: raw/{job_id}.json — сырой ответ ИС Сканирование; в БД не регистрируется" }, + "metrics": { + "type": "object", + "properties": { + "duration_ms": { "type": "integer" }, + "pages": { "type": "integer" }, + "rows": { "type": "integer" }, + "fields_total": { "type": "integer" }, + "fields_low_confidence": { "type": "integer" }, + "fields_unrecognized": { "type": "integer" }, + "warnings": { "type": "array", "items": { "type": "string" } } + } + } + }, + "allOf": [ + { + "if": { + "properties": { "status": { "enum": ["done", "needs_check"] } }, + "required": ["status"] + }, + "then": { "required": ["result_s3", "result_schema_version"] } + }, + { + "if": { + "properties": { "status": { "const": "error" } }, + "required": ["status"] + }, + "then": { "required": ["error"] } + } + ] +} -- 2.47.2 From faae87c9c775234b2b2c707c07b0ded16cb35a91 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:03:16 +0400 Subject: [PATCH 08/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=203?= =?UTF-8?q?=20(1.3)=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md index b5f0292..20cc7f0 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md @@ -25,7 +25,7 @@ ### Task 3: [1.3] [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт -- [ ] [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт: `status` ∈ processing | done | needs_check | error; условные обязательные поля через `if/then`); проверить метасхемой +- [x] [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт: `status` ∈ processing | done | needs_check | error; условные обязательные поля через `if/then`); проверить метасхемой ### Task 4: [1.4] [docs] Примеры `docs/contracts/examples/` diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 502b983..8af1822 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -10,7 +10,7 @@ (метасхема 2020-12 валидна) - [x] 1.2 [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recognition-contract` (поля, enum, форматы uuid/date-time); проверить метасхемой -- [ ] 1.3 [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт: `status` ∈ processing | done | +- [x] 1.3 [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт: `status` ∈ processing | done | needs_check | error; условные обязательные поля через `if/then`); проверить метасхемой - [ ] 1.4 [docs] Примеры `docs/contracts/examples/`: `request-tabular.json`, `request-image.json`, `result-message-processing.json`, `result-message-done.json`, `result-message-error.json`, `result-osv-minimal.json`, -- 2.47.2 From 38c50c21016a1175fda04e2e221ab19f4e955fb2 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:04:49 +0400 Subject: [PATCH 09/68] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BA=D1=82:=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B8?= =?UTF-8?q?=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D1=80=D0=B0=D1=81=D0=BF=D0=BE=D0=B7=D0=BD=D0=B0=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- contracts/examples/request-image.json | 14 ++++ contracts/examples/request-tabular.json | 14 ++++ contracts/examples/result-error.json | 12 +++ contracts/examples/result-message-done.json | 21 +++++ contracts/examples/result-message-error.json | 11 +++ .../examples/result-message-processing.json | 7 ++ contracts/examples/result-osv-minimal.json | 79 +++++++++++++++++++ .../examples/result-statement-minimal.json | 50 ++++++++++++ 8 files changed, 208 insertions(+) create mode 100644 contracts/examples/request-image.json create mode 100644 contracts/examples/request-tabular.json create mode 100644 contracts/examples/result-error.json create mode 100644 contracts/examples/result-message-done.json create mode 100644 contracts/examples/result-message-error.json create mode 100644 contracts/examples/result-message-processing.json create mode 100644 contracts/examples/result-osv-minimal.json create mode 100644 contracts/examples/result-statement-minimal.json diff --git a/contracts/examples/request-image.json b/contracts/examples/request-image.json new file mode 100644 index 0000000..2de9cd5 --- /dev/null +++ b/contracts/examples/request-image.json @@ -0,0 +1,14 @@ +{ + "schema_version": "1.0", + "job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03", + "file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04", + "pipeline": "image", + "file_type": "PDF", + "original_name": "bb_2025_q3.pdf", + "s3": { + "bucket": "arm-ka", + "key": "uploads/c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03/bb_2025_q3.pdf" + }, + "reply_deadline": "2026-10-03T12:45:00Z", + "attempt": 1 +} diff --git a/contracts/examples/request-tabular.json b/contracts/examples/request-tabular.json new file mode 100644 index 0000000..e1a26c1 --- /dev/null +++ b/contracts/examples/request-tabular.json @@ -0,0 +1,14 @@ +{ + "schema_version": "1.0", + "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", + "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", + "pipeline": "tabular", + "file_type": "XLSX", + "original_name": "osv_60_2025_q1.xlsx", + "s3": { + "bucket": "arm-ka", + "key": "uploads/a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02/3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01/osv_60_2025_q1.xlsx" + }, + "reply_deadline": "2026-10-03T12:30:00Z", + "attempt": 1 +} diff --git a/contracts/examples/result-error.json b/contracts/examples/result-error.json new file mode 100644 index 0000000..54af7cb --- /dev/null +++ b/contracts/examples/result-error.json @@ -0,0 +1,12 @@ +{ + "schema_version": "1.0", + "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", + "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", + "service": { "name": "tabular-parser", "version": "0.1.0" }, + "status": "error", + "error": { + "code": "NOT_IMPLEMENTED", + "message": "Разбор ОСВ пока не реализован / OSV parsing is not implemented yet" + }, + "documents": [] +} diff --git a/contracts/examples/result-message-done.json b/contracts/examples/result-message-done.json new file mode 100644 index 0000000..6d56f17 --- /dev/null +++ b/contracts/examples/result-message-done.json @@ -0,0 +1,21 @@ +{ + "schema_version": "1.0", + "job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03", + "file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04", + "status": "done", + "service": { "name": "scan-gateway", "version": "0.1.0" }, + "result_s3": { + "bucket": "arm-ka", + "key": "results/c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03/result.json" + }, + "result_schema_version": "1.0", + "raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json", + "metrics": { + "duration_ms": 18400, + "pages": 2, + "fields_total": 4, + "fields_low_confidence": 0, + "fields_unrecognized": 0, + "warnings": [] + } +} diff --git a/contracts/examples/result-message-error.json b/contracts/examples/result-message-error.json new file mode 100644 index 0000000..f5ec77a --- /dev/null +++ b/contracts/examples/result-message-error.json @@ -0,0 +1,11 @@ +{ + "schema_version": "1.0", + "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", + "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", + "status": "error", + "service": { "name": "tabular-parser", "version": "0.1.0" }, + "error": { + "code": "NOT_IMPLEMENTED", + "message": "Разбор ОСВ пока не реализован / OSV parsing is not implemented yet" + } +} diff --git a/contracts/examples/result-message-processing.json b/contracts/examples/result-message-processing.json new file mode 100644 index 0000000..2087150 --- /dev/null +++ b/contracts/examples/result-message-processing.json @@ -0,0 +1,7 @@ +{ + "schema_version": "1.0", + "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", + "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", + "status": "processing", + "service": { "name": "tabular-parser", "version": "0.1.0" } +} diff --git a/contracts/examples/result-osv-minimal.json b/contracts/examples/result-osv-minimal.json new file mode 100644 index 0000000..6b6d122 --- /dev/null +++ b/contracts/examples/result-osv-minimal.json @@ -0,0 +1,79 @@ +{ + "schema_version": "1.0", + "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", + "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", + "service": { "name": "tabular-parser", "version": "0.1.0" }, + "status": "done", + "documents": [ + { + "document_type": "OSV", + "locator": "sheet:Лист_1", + "header": { + "organization_name": "ООО Ромашка", + "organization_inn": "7700000000", + "account_code_raw": "60", + "period_text": "1 квартал 2025 г.", + "period_year": 2025, + "period_quarter": "Q1", + "is_cumulative": false, + "data_type": "BU", + "currency": "RUB", + "header_variant": 1 + }, + "group_schema": [ + { "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true } + ], + "columns": { + "opening_debit": "C", + "opening_credit": "D", + "turnover_debit": "E", + "turnover_credit": "F", + "closing_debit": "G", + "closing_credit": "H" + }, + "rows": [ + { + "row_number": 1, + "level": 0, + "parent_row_number": null, + "row_value": "60", + "group_type": "ACCOUNT", + "group_name": null, + "account_code_raw": "60", + "counterparty_name": null, + "counterparty_inn": null, + "data_type": "BU", + "opening_debit": "0.00", + "opening_credit": "1000.00", + "turnover_debit": "500.00", + "turnover_credit": "200.50", + "closing_debit": "0.00", + "closing_credit": "700.50" + }, + { + "row_number": 2, + "level": 1, + "parent_row_number": 1, + "row_value": "ООО Лютик, ИНН 7711111111", + "group_type": "COUNTERPARTY", + "group_name": "Контрагенты", + "account_code_raw": "60", + "counterparty_name": "ООО Лютик", + "counterparty_inn": "7711111111", + "data_type": "BU", + "opening_debit": "0.00", + "opening_credit": "1000.00", + "turnover_debit": "500.00", + "turnover_credit": "200.50", + "closing_debit": "0.00", + "closing_credit": "700.50" + } + ], + "checks": { + "total_row_matches_root": true, + "balance_identity_violations": [], + "parent_sum_violations": [] + } + } + ] +} diff --git a/contracts/examples/result-statement-minimal.json b/contracts/examples/result-statement-minimal.json new file mode 100644 index 0000000..484f222 --- /dev/null +++ b/contracts/examples/result-statement-minimal.json @@ -0,0 +1,50 @@ +{ + "schema_version": "1.0", + "job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03", + "file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04", + "service": { "name": "scan-gateway", "version": "0.1.0" }, + "status": "done", + "raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json", + "documents": [ + { + "document_type": "BALANCE", + "locator": "pages:1-2", + "header": { + "form_name": "Бухгалтерский баланс", + "organization_name": "ООО Ромашка", + "organization_name_confidence": 97.5, + "inn": "7700000000", + "inn_confidence": 99, + "report_date_text": "30 сентября 2025 г.", + "report_date": "2025-09-30", + "units": "тыс. руб." + }, + "columns": [ + { "column_index": 0, "label": "На 30 сентября 2025 г.", "period_date": "2025-09-30" }, + { "column_index": 1, "label": "На 31 декабря 2024 г.", "period_date": "2024-12-31" } + ], + "lines": [ + { + "line_code": "1100", + "line_code_confidence": 99, + "line_name": "Итого по разделу I", + "line_name_confidence": 96, + "values": [ + { "column_index": 0, "amount": "1234.50", "confidence": 95, "is_recognized": true }, + { "column_index": 1, "amount": "1100", "confidence": 93, "is_recognized": true } + ] + }, + { + "line_code": "1600", + "line_code_confidence": 99, + "line_name": "БАЛАНС (актив)", + "line_name_confidence": 98, + "values": [ + { "column_index": 0, "amount": "5678.00", "confidence": 94.5, "is_recognized": true }, + { "column_index": 1, "amount": "-12.30", "confidence": 90, "is_recognized": true } + ] + } + ] + } + ] +} -- 2.47.2 From af0f25fbd88f7a840fa0cd47e9276a901f5540dd Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:04:49 +0400 Subject: [PATCH 10/68] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BA=D1=82:=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=20validate?= =?UTF-8?q?=5Fcontracts.py=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B8=D1=80?= =?UTF-8?q?=D1=83=D0=B5=D1=82=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B?= =?UTF-8?q?=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- scripts/validate_contracts.py | 116 ++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 scripts/validate_contracts.py diff --git a/scripts/validate_contracts.py b/scripts/validate_contracts.py new file mode 100644 index 0000000..a34c2d6 --- /dev/null +++ b/scripts/validate_contracts.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Валидация примеров контракта распознавания схемами JSON Schema 2020-12. + +Запуск из любого cwd: python scripts/validate_contracts.py +Зависимости: стандартная библиотека + jsonschema. +Код возврата: 0 — всё валидно, 1 — любая ошибка. +""" +import datetime +import json +import re +import sys +from pathlib import Path + +from jsonschema import Draft202012Validator, FormatChecker + +ROOT = Path(__file__).resolve().parent.parent +CONTRACTS = ROOT / "contracts" +EXAMPLES = CONTRACTS / "examples" + +SCHEMAS = { + "request": CONTRACTS / "recognition-request.schema.json", + "message": CONTRACTS / "recognition-result-message.schema.json", + "result": CONTRACTS / "recognition-result.schema.json", +} + +# Порядок важен: result-message-* проверяется раньше result-*. +PATTERNS = [ + ("request-", "request"), + ("result-message-", "message"), + ("result-", "result"), +] + +_DATE_TIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$") + + +def _check_date_time(value): + # Собственная проверка вместо rfc3339-validator: нужна и дата, и время, и часовой пояс. + if not isinstance(value, str): + return True + if not _DATE_TIME_RE.match(value): + return False + datetime.datetime.fromisoformat(value.replace("z", "Z")) # ValueError -> формат не прошёл + return True + + +def make_format_checker(): + checker = FormatChecker() + checker.checks("date-time", raises=ValueError)(_check_date_time) + return checker + + +def short_path(error): + path = "/".join(str(p) for p in error.absolute_path) + return f"{path or ''}: {error.message[:120]}" + + +def main(): + failed = False + validators = {} + format_checker = make_format_checker() + + for kind, path in SCHEMAS.items(): + if not path.is_file(): + print(f"FAIL схема {path.name}: файл не найден") + failed = True + continue + try: + schema = json.loads(path.read_text(encoding="utf-8")) + Draft202012Validator.check_schema(schema) + except Exception as exc: # битый JSON или нарушение метасхемы + print(f"FAIL схема {path.name}: {str(exc).splitlines()[0][:150]}") + failed = True + continue + validators[kind] = Draft202012Validator(schema, format_checker=format_checker) + print(f"OK схема {path.name}") + + counts = {kind: 0 for kind in SCHEMAS} + for example in sorted(EXAMPLES.glob("*.json")): + kind = next((k for prefix, k in PATTERNS if example.name.startswith(prefix)), None) + if kind is None: + print(f"FAIL {example.name}: имя не подходит ни под один шаблон (request-*, result-message-*, result-*)") + failed = True + continue + counts[kind] += 1 + validator = validators.get(kind) + if validator is None: + print(f"FAIL {example.name} [{SCHEMAS[kind].name}]: схема недоступна") + failed = True + continue + try: + instance = json.loads(example.read_text(encoding="utf-8")) + except ValueError as exc: + print(f"FAIL {example.name} [{SCHEMAS[kind].name}]: не JSON: {exc}") + failed = True + continue + errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.absolute_path)) + if errors: + failed = True + print(f"FAIL {example.name} [{SCHEMAS[kind].name}]") + for error in errors: + print(f" {short_path(error)}") + else: + print(f"OK {example.name} [{SCHEMAS[kind].name}]") + + for kind, count in counts.items(): + if count == 0: + print(f"FAIL нет ни одного примера для схемы {SCHEMAS[kind].name}") + failed = True + + total = sum(counts.values()) + print(f"Итого: примеров {total}, {'есть ошибки' if failed else 'всё валидно'}") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) -- 2.47.2 From 7f0de551c1785c27df57fa04c2f3fc37ab197856 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:06:41 +0400 Subject: [PATCH 11/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=204?= =?UTF-8?q?=20(1.4)=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md index 20cc7f0..f9a3f0a 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md @@ -29,7 +29,7 @@ ### Task 4: [1.4] [docs] Примеры `docs/contracts/examples/` -- [ ] [docs] Примеры `docs/contracts/examples/`: `request-tabular.json`, `request-image.json`, `result-message-processing.json`, `result-message-done.json`, `result-message-error.json`, `result-osv-minimal.json`, `result-statement-minimal.json`, `result-error.json`; скрипт `docs/scripts/validate_contracts.py` валидирует все примеры схемами и возвращает 0 +- [x] [docs] Примеры `docs/contracts/examples/`: `request-tabular.json`, `request-image.json`, `result-message-processing.json`, `result-message-done.json`, `result-message-error.json`, `result-osv-minimal.json`, `result-statement-minimal.json`, `result-error.json`; скрипт `docs/scripts/validate_contracts.py` валидирует все примеры схемами и возвращает 0 ### Task 5: [1.5] [docs] `03-architecture.md` diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 8af1822..bcfe93d 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -12,7 +12,7 @@ форматы uuid/date-time); проверить метасхемой - [x] 1.3 [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт: `status` ∈ processing | done | needs_check | error; условные обязательные поля через `if/then`); проверить метасхемой -- [ ] 1.4 [docs] Примеры `docs/contracts/examples/`: `request-tabular.json`, `request-image.json`, +- [x] 1.4 [docs] Примеры `docs/contracts/examples/`: `request-tabular.json`, `request-image.json`, `result-message-processing.json`, `result-message-done.json`, `result-message-error.json`, `result-osv-minimal.json`, `result-statement-minimal.json`, `result-error.json`; скрипт `docs/scripts/validate_contracts.py` валидирует все примеры схемами и возвращает 0 -- 2.47.2 From 3074632fb1390420b483a92038473c7a3518d2a1 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:07:27 +0400 Subject: [PATCH 12/68] =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D1=83=D1=80=D0=B0:=20=D1=82=D0=B0=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8=D1=86=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B0=20NATS=20=D1=81=D0=BE=20=D1=81=D1=81=D1=8B=D0=BB?= =?UTF-8?q?=D0=BA=D0=B0=D0=BC=D0=B8=20=D0=BD=D0=B0=20=D1=81=D1=85=D0=B5?= =?UTF-8?q?=D0=BC=D1=8B,=20ack=20=D0=B8=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=D0=B0=20Nats-Msg-Id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- context/03-architecture.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/context/03-architecture.md b/context/03-architecture.md index 9420f84..6a1bcb1 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -38,16 +38,19 @@ ## Контракт обмена через NATS (DECISION-4) -Схемы — `docs/contracts/` (v1.0): `recognition-request.schema.json` и `recognition-result-message.schema.json` — тела -сообщений, `recognition-result.schema.json` — файл результата в S3; примеры — `docs/contracts/examples/`. +Схемы — `docs/contracts/` (v1.0), JSON Schema 2020-12: [`recognition-request.schema.json`](../contracts/recognition-request.schema.json) +и [`recognition-result-message.schema.json`](../contracts/recognition-result-message.schema.json) — тела сообщений, +[`recognition-result.schema.json`](../contracts/recognition-result.schema.json) — файл результата в S3; примеры — +[`docs/contracts/examples/`](../contracts/examples/). Примеры проверяются схемами: `python scripts/validate_contracts.py` +(из корня репозитория docs). -| Поток | Subject | Кто пишет | Полезная нагрузка | `Nats-Msg-Id` | -|---|---|---|---|---| -| запрос | `recognition.tabular.request` / `recognition.image.request` | arm-core (outbox из `recognition_job`) | `schema_version`, `job_id`, `file_id`, `pipeline`, `file_type`, `original_name`, `s3 {bucket, key}` исходника, `reply_deadline`, `attempt` | `:` | -| результат | `recognition.result` | tabular-parser / scan-gateway | `schema_version`, `job_id`, `file_id`, `status` (done / needs_check / error), `error {code, message}`, `service {name, version}`, `result_s3 {bucket, key}`, `result_schema_version`, `raw_s3_key` (только scan-gateway), `metrics` | `result::` | +| Поток | Subject | Кто пишет | Полезная нагрузка | `Nats-Msg-Id` | Схема | +|---|---|---|---|---|---| +| запрос | `recognition.tabular.request` / `recognition.image.request` | 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 | `recognition.result` | tabular-parser / scan-gateway | конверт со `status = processing`, `job_id`, `file_id`, `service {name, version}` | `ack::` | [message](../contracts/recognition-result-message.schema.json) | +| результат | `recognition.result` | 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) | `group_schema` в конверт не входит — ядро берёт его из `documents[].group_schema` файла результата при маппинге. -Конверт со `status = processing` — подтверждение взятия в работу (ack), `Nats-Msg-Id = ack::`. **На платформе NATS без JetStream** (ответ 03.10.2026): нет durable consumer'ов, серверного дедупа и redelivery. Поэтому гарантии доставки — на уровне приложения: сервисы подписаны queue group'ами (`tabular-parser`, `scan-gateway`), @@ -63,6 +66,11 @@ - Результат принимается подпиской queue group; `result_msg_id UNIQUE` = дедуп. Нет ack после повторов → `dlq`; потерянный результат → `reply_deadline` → `timeout`; replay = сброс в `queued` с `attempt + 1`. - Результат одного файла содержит **N документов**; ядро на каждый создаёт `document` и маппит данные. +- Три формы `Nats-Msg-Id`: запрос `:`, ack `ack::`, результат `result::`. + Ack (`processing`) сервис публикует **до чтения файла из S3**; повторная публикация запроса ядром идёт с тем же + `Nats-Msg-Id` и тем же `attempt`. Дубликат результата ядро определяет по `recognition_job.result_msg_id`. +- `schema_version` — `MAJOR.MINOR`; сервис отвергает запрос с неизвестным MAJOR результатом `error` с кодом `SCHEMA_MISMATCH` и подтверждает его + ack'ом, чтобы не зациклить доставку. - Сервисы не знают про БД и про бизнес-смысл: они возвращают структуру файла; нормализация счетов, резолв контрагентов, проверки — в ядре. -- 2.47.2 From a22a9c31001cfe954cba9f10eaf05e234248aeb9 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:18:09 +0400 Subject: [PATCH 13/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=205?= =?UTF-8?q?=20(1.5)=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md index f9a3f0a..16b75c4 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md @@ -33,7 +33,7 @@ ### Task 5: [1.5] [docs] `03-architecture.md` -- [ ] [docs] `03-architecture.md`: таблица «Контракт обмена через NATS» — ссылки на три файла схем, правила `Nats-Msg-Id`, ack через `processing`, повторная публикация; проверить, что текст ссылается на существующие файлы +- [x] [docs] `03-architecture.md`: таблица «Контракт обмена через NATS» — ссылки на три файла схем, правила `Nats-Msg-Id`, ack через `processing`, повторная публикация; проверить, что текст ссылается на существующие файлы ### Task 6: [2.1] [docs] `docs/dev/docker-compose.yml` diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index bcfe93d..6f5f641 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -16,7 +16,7 @@ `result-message-processing.json`, `result-message-done.json`, `result-message-error.json`, `result-osv-minimal.json`, `result-statement-minimal.json`, `result-error.json`; скрипт `docs/scripts/validate_contracts.py` валидирует все примеры схемами и возвращает 0 -- [ ] 1.5 [docs] `03-architecture.md`: таблица «Контракт обмена через NATS» — ссылки на три файла схем, правила +- [x] 1.5 [docs] `03-architecture.md`: таблица «Контракт обмена через NATS» — ссылки на три файла схем, правила `Nats-Msg-Id`, ack через `processing`, повторная публикация; проверить, что текст ссылается на существующие файлы ## 2. Инфраструктура разработки -- 2.47.2 From 4de51361a372c97e198ec2b119987893f41e3127 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:29:57 +0400 Subject: [PATCH 14/68] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20docker-compose=20=D0=B8=D0=BD=D1=84=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80=D1=8B=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20(dev/)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostgreSQL 16, NATS 2.10 без JetStream, MinIO и minio-init с бакетом arm-ka; имя проекта arm-ka, порты и учётные данные из .env, версии образов зафиксированы. Co-Authored-By: Claude Fable 5.1 --- dev/.env.example | 39 ++++++++++++++++++++ dev/docker-compose.yml | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 dev/.env.example create mode 100644 dev/docker-compose.yml diff --git a/dev/.env.example b/dev/.env.example new file mode 100644 index 0000000..2c347e6 --- /dev/null +++ b/dev/.env.example @@ -0,0 +1,39 @@ +# Локальное окружение разработки. Скопируйте в .env: cp .env.example .env (.env в git не попадает) +# Файл читают docker compose (из этого каталога) и, блоком ниже, сервисы при запуске из venv. + +# --- Переменные docker-compose: учётные данные, порты хоста, бакет --- +# PostgreSQL 16 (схема dkb создаётся миграциями arm-core, не compose) +POSTGRES_DB=armka +POSTGRES_USER=armka +POSTGRES_PASSWORD=armka +POSTGRES_PORT=5432 + +# NATS 2.10 без JetStream: клиентский порт и порт мониторинга (/varz, /healthz) +NATS_PORT=4222 +NATS_MONITOR_PORT=8222 + +# MinIO: учётная запись администратора, порты API и консоли, единственный бакет +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin +MINIO_API_PORT=9000 +MINIO_CONSOLE_PORT=9001 +MINIO_BUCKET=arm-ka + +# --- Настройки сервисов при запуске из venv против compose по localhost (формат шаблона команды) --- +# Значения согласованы с блоком выше: при смене порта/пароля/бакета меняйте обе части. +# Если порт хоста изменён, поправьте и порты ниже. +DEBUG=true + +# PostgreSQL (драйвер asyncpg) +OPENBAO__DATABASE__DATABASE_URL=postgresql+asyncpg://armka:armka@localhost:5432/armka + +# MinIO: адрес без схемы (host:port) — так ждёт miniopy-async +OPENBAO__MINIO__MINIO_URL=localhost:9000 +OPENBAO__MINIO__MINIO_ADMIN_LOGIN=minioadmin +OPENBAO__MINIO__MINIO_ADMIN_PASSWORD=minioadmin +OPENBAO__MINIO__MINIO_BUCKET=arm-ka + +# NATS без JetStream; пустые NATS_SEED/NATS_JWT — подключение без nkeys +OPENBAO__NATS__NATS_URL=nats://localhost:4222 +OPENBAO__NATS__NATS_SEED= +OPENBAO__NATS__NATS_JWT= diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml new file mode 100644 index 0000000..82b9c7a --- /dev/null +++ b/dev/docker-compose.yml @@ -0,0 +1,83 @@ +# Инфраструктура разработки АРМ КА: PostgreSQL 16, NATS 2.10 (без JetStream, как на платформе), MinIO + бакет. +# Запуск: cp .env.example .env && docker compose up -d (из этого каталога) +# Ожидание готовности: docker compose up -d --wait postgres nats minio. Голый `up -d --wait` возвращает код 1, потому что +# одноразовый minio-init завершается (хотя и с кодом 0) — для скриптов указывайте сервисы явно. +# Все учётные данные, порты хоста и имя бакета берутся из .env (значения по умолчанию — dev). +# Профиль `services` (arm-core, tabular-parser, scan-gateway, scan-emulator) добавляется в фазе 2. + +# Фиксированное имя проекта: одинаковые тома и сети из любого worktree (.wt/<срез>/docs/dev). +name: arm-ka + +services: + postgres: + image: postgres:16.10-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-armka} + POSTGRES_USER: ${POSTGRES_USER:-armka} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-armka} + ports: + - "${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 3s + timeout: 3s + retries: 20 + start_period: 5s + + # Core NATS без JetStream: флаг --jetstream не передаётся. Мониторинг — порт 8222 (/varz, /healthz). + nats: + image: nats:2.10.29-alpine + command: ["-m", "8222"] + ports: + - "${NATS_PORT:-4222}:4222" + - "${NATS_MONITOR_PORT:-8222}:8222" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8222/healthz || exit 1"] + interval: 3s + timeout: 3s + retries: 20 + start_period: 2s + + # Образы minio/minio и minio/mc сняты с Docker Hub и quay.io; используется сборка alpine/minio с RELEASE-тегом. + minio: + image: alpine/minio:RELEASE.2025-10-15T17-29-55Z + # Образ по умолчанию работает от пользователя minio и не может писать в свежий именованный том — в dev запускаем от root. + user: "0:0" + command: ["server", "/data", "--console-address", ":9001"] + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} + ports: + - "${MINIO_API_PORT:-9000}:9000" + - "${MINIO_CONSOLE_PORT:-9001}:9001" + volumes: + - minio-data:/data + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:9000/minio/health/live || exit 1"] + interval: 3s + timeout: 3s + retries: 20 + start_period: 3s + + # Одноразовое создание бакета; идемпотентно (--ignore-existing), при повторном up снова завершается с кодом 0. + minio-init: + image: bitnamilegacy/minio-client:2025.7.21 + depends_on: + minio: + condition: service_healthy + restart: "no" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} + MINIO_BUCKET: ${MINIO_BUCKET:-arm-ka} + entrypoint: ["/bin/sh", "-c"] + command: + - >- + mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" && + mc mb --ignore-existing "local/$$MINIO_BUCKET" + +volumes: + postgres-data: + minio-data: -- 2.47.2 From 944296250e57aaf35421fc8e4467e83f57891306 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:29:57 +0400 Subject: [PATCH 15/68] =?UTF-8?q?=D0=98=D0=B3=D0=BD=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20dev/.env=20=D1=81=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=BC=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=20compose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a46bd45..1ffed2b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .codegraph/ .superpowers/ .bounded +dev/.env -- 2.47.2 From 64c103da5fc3b30879fc5b4e952ee87aa543fb05 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:30:59 +0400 Subject: [PATCH 16/68] =?UTF-8?q?docs:=20design.md=20=E2=80=94=20=D1=80?= =?UTF-8?q?=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=2016=E2=80=9317=20(=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B7=D1=8B=20MinIO=20=D0=B2=20dev-compose)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- openspec/changes/slice-0-skeleton/design.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index ae4c948..a784251 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -147,3 +147,5 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 13 | 2026-10-03 | Файл результата: пустой `documents` допустим только при `status = error` — выражено в схеме условием `if/then` (`minItems: 1`) | требование spec `recognition-contract` «Файл результата»; текст задачи 1.1 его не перечислял | ослабить/убрать условие в схеме, пересобрать копии контракта в сервисах | | 14 | 2026-10-03 | Compose: фиксированное имя проекта `name: arm-ka`, порты хоста переопределяются через `.env` со значениями по умолчанию из spec | одинаковые тома/сети из любого worktree; порты 5432/4222 на машине разработчика могут быть заняты | смена имени проекта — потеря dev-томов (`down -v`), порты — правка `.env` | | 15 | 2026-10-03 | Чекбоксы плана и `tasks.md` отмечает контроллер SDD отдельным коммитом после чистого ревью задачи | исполнитель не должен отмечать выполненным то, что ещё не прошло ревью | отметки правятся вручную | +| 16 | 2026-10-03 | Compose: MinIO берётся из неофициальных образов `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и `bitnamilegacy/minio-client:2025.7.21` | `minio/minio` и `minio/mc` недоступны на Docker Hub (denied), на quay.io нужных тегов нет; для dev достаточно любого S3-совместимого MinIO нужной версии API | образы сторонние: при появлении доступа к банковскому реестру/официальным образам — заменить две строки `image:` в `dev/docker-compose.yml`; в прод эти образы не попадают | +| 17 | 2026-10-03 | Compose: сервис `minio` запускается от `user: "0:0"` | образ `alpine/minio` по умолчанию работает от пользователя `minio` и падает на свежем именованном томе | только dev; удалить строку, когда образ/том позволят писать без root | -- 2.47.2 From 34c43be881b9d186ac68a5b2a8658cab41fc07f6 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:34:49 +0400 Subject: [PATCH 17/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=206?= =?UTF-8?q?=20(2.1)=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md index 16b75c4..9f7b20e 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md @@ -37,7 +37,7 @@ ### Task 6: [2.1] [docs] `docs/dev/docker-compose.yml` -- [ ] [docs] `docs/dev/docker-compose.yml`: сервисы `postgres` (16), `nats` (2.10, без `--jetstream`), `minio`, `minio-init` (создаёт бакет `arm-ka`), healthcheck'и, именованные тома; `docs/dev/.env.example`; проверка: `docker compose up -d` → все healthy, `mc ls` видит бакет, `curl :8222/varz` отвечает +- [x] [docs] `docs/dev/docker-compose.yml`: сервисы `postgres` (16), `nats` (2.10, без `--jetstream`), `minio`, `minio-init` (создаёт бакет `arm-ka`), healthcheck'и, именованные тома; `docs/dev/.env.example`; проверка: `docker compose up -d` → все healthy, `mc ls` видит бакет, `curl :8222/varz` отвечает ### Task 7: [2.2] [docs] `docs/dev/README.md` diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 6f5f641..817aff7 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -21,7 +21,7 @@ ## 2. Инфраструктура разработки -- [ ] 2.1 [docs] `docs/dev/docker-compose.yml`: сервисы `postgres` (16), `nats` (2.10, без `--jetstream`), `minio`, +- [x] 2.1 [docs] `docs/dev/docker-compose.yml`: сервисы `postgres` (16), `nats` (2.10, без `--jetstream`), `minio`, `minio-init` (создаёт бакет `arm-ka`), healthcheck'и, именованные тома; `docs/dev/.env.example`; проверка: `docker compose up -d` → все healthy, `mc ls` видит бакет, `curl :8222/varz` отвечает - [ ] 2.2 [docs] `docs/dev/README.md`: запуск инфраструктуры, переменные, сброс томов, запуск сервисов из venv без Nexus -- 2.47.2 From 6b97d87d8e57a8e365372ceafbadbb0c7bed2438 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:36:17 +0400 Subject: [PATCH 18/68] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20dev/README.md:=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81?= =?UTF-8?q?=D0=BA=20=D0=B8=D0=BD=D1=84=D1=80=D0=B0=D1=81=D1=82=D1=80=D1=83?= =?UTF-8?q?=D0=BA=D1=82=D1=83=D1=80=D1=8B,=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5,=20=D1=81=D0=B1=D1=80?= =?UTF-8?q?=D0=BE=D1=81,=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20=D0=B8?= =?UTF-8?q?=D0=B7=20venv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Черновик: профиль services, запуск из venv и интеграционные тесты помечены как планируемые и дописываются в задачах 5.x. Co-Authored-By: Claude Fable 5.1 --- dev/README.md | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 dev/README.md diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 0000000..16f7b4f --- /dev/null +++ b/dev/README.md @@ -0,0 +1,160 @@ +# Окружение разработки АРМ КА (`docs/dev/`) + +Локальная инфраструктура workspace одной командой: PostgreSQL 16, NATS 2.10 без JetStream и MinIO с бакетом `arm-ka`. + +**Статус (фаза 1):** в compose сейчас только инфраструктура (`postgres`, `nats`, `minio`, `minio-init`). Профиль +`services` (контейнеры `arm-core`, `tabular-parser`, `scan-gateway`, `scan-emulator`) **планируется** — добавляется позже +(задача 5.3), здесь не описан. До этого сервисы запускаются из venv против этой инфраструктуры (см. ниже). + +## Предварительные требования + +- Docker и compose v2 (проверено на docker 28 / compose 2.34). +- `uv` и Python 3.11 — для запуска сервисов из venv (версия Python — как на платформе). +- Свободные порты хоста: 5432 (PostgreSQL), 4222 и 8222 (NATS и его мониторинг), 9000 и 9001 (MinIO API и консоль). + Занятые порты меняются в `.env` (см. «Типичные проблемы»). + +## Запуск инфраструктуры + +Команды — из каталога `docs/dev/`: + +```bash +cp .env.example .env # .env в git не попадает +docker compose up -d # вернётся сразу; healthy — примерно через 6 с +``` + +Ожидание готовности (для скриптов и `Makefile`): + +```bash +docker compose up -d && docker compose up -d --wait postgres nats minio +``` + +Голый `docker compose up -d --wait` **возвращает код 1**: одноразовый `minio-init` (создаёт бакет и завершается с +кодом 0) compose 2.34 считает сбоем (`container arm-ka-minio-init-1 exited (0)`). Поэтому `--wait` нужно давать со +списком долгоживущих сервисов — как выше. + +Проверка: + +```bash +docker compose ps # postgres, nats, minio — Up (healthy); minio-init — Exited (0) +docker compose exec postgres psql -U armka -d armka -c 'select version()' +curl -s localhost:8222/healthz # {"status":"ok"} +curl -s localhost:8222/varz | grep -A1 jetstream # "jetstream": {} — JetStream не включён +docker compose run --rm --entrypoint sh minio-init -c 'mc alias set local http://minio:9000 minioadmin minioadmin >/dev/null && mc ls local' + # [...] 0B arm-ka/ +``` + +Консоль MinIO — (по умолчанию `minioadmin` / `minioadmin`); S3 API — `localhost:9000`. +Схема БД (`dkb`) создаётся миграциями `arm-core`, не compose. + +## Переменные `.env` + +Файл читают `docker compose` (из этого каталога) и, вторым блоком, сервисы при запуске из venv. Значения по умолчанию — +только для dev. + +Блок compose: + +| Переменная | По умолчанию | Для чего | +|---|---|---| +| `POSTGRES_DB` | `armka` | имя БД | +| `POSTGRES_USER` | `armka` | пользователь БД | +| `POSTGRES_PASSWORD` | `armka` | пароль БД | +| `POSTGRES_PORT` | `5432` | порт PostgreSQL на хосте | +| `NATS_PORT` | `4222` | клиентский порт NATS на хосте | +| `NATS_MONITOR_PORT` | `8222` | порт мониторинга NATS (`/varz`, `/healthz`) | +| `MINIO_ROOT_USER` | `minioadmin` | администратор MinIO | +| `MINIO_ROOT_PASSWORD` | `minioadmin` | пароль администратора MinIO | +| `MINIO_API_PORT` | `9000` | порт S3 API на хосте | +| `MINIO_CONSOLE_PORT` | `9001` | порт консоли MinIO на хосте | +| `MINIO_BUCKET` | `arm-ka` | единственный бакет, создаётся `minio-init` | + +Блок сервисов (запуск из venv против compose по `localhost`, формат шаблона команды): + +| Переменная | По умолчанию | Для чего | +|---|---|---| +| `DEBUG` | `true` | dev-режим: пакеты `raisa_*` импортируются условно | +| `OPENBAO__DATABASE__DATABASE_URL` | `postgresql+asyncpg://armka:armka@localhost:5432/armka` | подключение к PostgreSQL (asyncpg) | +| `OPENBAO__MINIO__MINIO_URL` | `localhost:9000` | адрес MinIO без схемы (`host:port`, так ждёт `miniopy-async`) | +| `OPENBAO__MINIO__MINIO_ADMIN_LOGIN` | `minioadmin` | логин MinIO | +| `OPENBAO__MINIO__MINIO_ADMIN_PASSWORD` | `minioadmin` | пароль MinIO | +| `OPENBAO__MINIO__MINIO_BUCKET` | `arm-ka` | бакет | +| `OPENBAO__NATS__NATS_URL` | `nats://localhost:4222` | адрес NATS | +| `OPENBAO__NATS__NATS_SEED` | пусто | nkey seed; пусто — подключение без nkeys | +| `OPENBAO__NATS__NATS_JWT` | пусто | JWT NATS; пусто — подключение без nkeys | + +Два блока описывают одни и те же ресурсы: **при смене порта, пароля, пользователя или бакета меняйте обе части.** + +## Сброс окружения + +```bash +docker compose down # останавливает контейнеры; тома postgres-data и minio-data сохраняются +docker compose down -v # полный сброс: удаляет тома postgres-data и minio-data (БД и бакет — с нуля) +``` + +Имя проекта compose фиксировано (`name: arm-ka`), поэтому тома общие для любого worktree (`.wt/<срез>/docs/dev`): +сброс из одного worktree затрагивает все. + +## Запуск сервисов из venv без Nexus (черновик) + +> Черновик: будет дописан и проверен вручную в задаче 5.4 (сквозная проверка). `make venv` и `make test-integration` +> в `tabular-parser` и `scan-gateway` будут доступны после задач этих сервисов; сейчас они есть в `arm-core`. + +Пакеты `raisa_*` из Nexus банка вне банка недоступны (D-10). Поэтому `make venv` ставит `requirements.txt` без строк +`raisa_*` (фильтр `grep -v '^raisa_'`), код импортирует `raisa_*` условно (при `DEBUG=true`), а npm-пакеты `raisa-*` +в `web/package.json` вынесены в `optionalDependencies`. + +Шаги для сервиса (пути — относительно каталога его репозитория; в workspace `docs/` лежит рядом, `../docs/dev/`): + +```bash +# 1. поднять инфраструктуру (из docs/dev/, см. выше) +# 2. из корня репозитория сервиса: +cp ../docs/dev/.env.example api/.env +make venv # api/.venv: Python 3.11 + зависимости без raisa_* +cd api && .venv/bin/python run_server.py # сервис подключается к PostgreSQL/NATS/MinIO по localhost +# 3. в другом терминале: +curl -s localhost:<порт сервиса>/healthcheck +``` + +Порт сервиса задаётся его настройками; в профиле `services` по spec это 8000 (`arm-core`), 8001 (`tabular-parser`), +8002 (`scan-gateway`). В ответе `/healthcheck` ожидаются `version` и `nats = ok`. + +## Интеграционные тесты сервисов против compose (черновик) + +> Черновик: уточняется после задач 3.x и 5.x. + +Цели `Makefile` одинаковы во всех сервисных репозиториях (spec `quality-checks`): + +- `make test-integration` — `pytest -m integration` против инфраструктуры из этого compose; +- `make check` — быстрые проверки без Docker (ruff, mypy, `pytest -m "not integration"`, идентичность контракта); +- `make check-full` — `make check` + интеграционные тесты; для `arm-core` — ещё «`alembic upgrade head` → + `downgrade base` → `upgrade head`» и `scripts/check_dbml_vs_orm.py`. Выполняется перед созданием PR. + +Интеграционные тесты **требуют поднятой инфраструктуры**: сначала `docker compose up -d && docker compose up -d --wait +postgres nats minio`, затем `make test-integration` (или `make check-full`) в репозитории сервиса. Для `scan-gateway` +дополнительно нужен эмулятор ИС Сканирование (появится вместе с профилем `services`). Общее описание проверок — +`docs/dev/checks.md` (будет добавлено). + +## Проверка контракта + +Из корня репозитория `docs`: `python scripts/validate_contracts.py` — валидирует примеры в +[`contracts/examples/`](../contracts/examples/) тремя схемами из [`contracts/`](../contracts/) (3 схемы, 8 примеров; +нужен `jsonschema`). + +## Примечания + +- **Образы MinIO неофициальные.** Официальные `minio/minio` и `minio/mc` недоступны (Docker Hub — denied, на quay.io нет + нужных тегов), поэтому в compose стоят `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и + `bitnamilegacy/minio-client:2025.7.21`, а сервис `minio` запущен с `user: "0:0"` (иначе образ не пишет в свежий том) — + только для dev. Решения R-5/R-6: [`design.md`](../openspec/changes/slice-0-skeleton/design.md), раздел + «Decisions made without discussion», решения №16–17. Замена на официальные образы — две строки `image:` в + `docker-compose.yml` (и удаление `user: "0:0"`). +- **Безопасность.** Порты публикуются на `0.0.0.0` с паролями по умолчанию: это dev-окружение, не запускайте его в + недоверенной сети. + +## Типичные проблемы + +- **Порт занят** (`address already in use`): измените `*_PORT` в `.env` и одновременно соответствующий порт в + `OPENBAO__*` (`DATABASE_URL`, `MINIO_URL`, `NATS_URL`); перезапустите `docker compose up -d`. +- **`minio-init` завис или завершился с ошибкой:** `docker compose logs minio-init`; чаще всего `minio` не стал healthy — + `docker compose logs minio`. Повторный `docker compose up -d` идемпотентен. +- **Остаточные тома от прошлых запусков** (старый пароль БД, бакет не тот): `docker compose down -v` и запуск заново. +- **`up -d --wait` возвращает код 1:** см. раздел «Запуск инфраструктуры» — указывайте сервисы явно. -- 2.47.2 From de116ed0c13e9426f686a10f2a07adf829ba02ab Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:41:31 +0400 Subject: [PATCH 19/68] =?UTF-8?q?docs:=20design.md=20=E2=80=94=20=D1=80?= =?UTF-8?q?=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=2018=20(SMB-=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D0=BB=D1=83=D1=88=D0=BA=D0=B8=20=D0=B8=20NATS=5FENABLED?= =?UTF-8?q?=20=D0=B2=20dev/.env.example)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- openspec/changes/slice-0-skeleton/design.md | 1 + 1 file changed, 1 insertion(+) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index a784251..7d8712f 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -149,3 +149,4 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 15 | 2026-10-03 | Чекбоксы плана и `tasks.md` отмечает контроллер SDD отдельным коммитом после чистого ревью задачи | исполнитель не должен отмечать выполненным то, что ещё не прошло ревью | отметки правятся вручную | | 16 | 2026-10-03 | Compose: MinIO берётся из неофициальных образов `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и `bitnamilegacy/minio-client:2025.7.21` | `minio/minio` и `minio/mc` недоступны на Docker Hub (denied), на quay.io нужных тегов нет; для dev достаточно любого S3-совместимого MinIO нужной версии API | образы сторонние: при появлении доступа к банковскому реестру/официальным образам — заменить две строки `image:` в `dev/docker-compose.yml`; в прод эти образы не попадают | | 17 | 2026-10-03 | Compose: сервис `minio` запускается от `user: "0:0"` | образ `alpine/minio` по умолчанию работает от пользователя `minio` и падает на свежем именованном томе | только dev; удалить строку, когда образ/том позволят писать без root | +| 18 | 2026-10-03 | `dev/.env.example` дополнен заглушками `OPENBAO__SMB__*` и `NATS_ENABLED=true` | `Settings` шаблона (arm-core) требует блок `OPENBAO.SMB`, без него сервис из venv не стартует; `NATS_ENABLED` по умолчанию `false`, и `/healthcheck` показывает `nats = off`; spec велит копировать именно `docs/dev/.env.example` | убрать две группы строк; сервисы с другим набором обязательных настроек получат лишние (игнорируемые) переменные | -- 2.47.2 From 45d45e17f727724fde956dbf46713e9a3e813ef6 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:42:17 +0400 Subject: [PATCH 20/68] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20dev/.env.example=20=D0=B8=20README=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E:=20SMB-=D0=B7?= =?UTF-8?q?=D0=B0=D0=B3=D0=BB=D1=83=D1=88=D0=BA=D0=B8,=20NATS=5FENABLED,?= =?UTF-8?q?=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=20=D1=86=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D0=B9=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Спека и R-7 (design №18): dev/.env.example остаётся единственным источником для запуска из venv, поэтому в него добавлены OPENBAO__SMB__* и NATS_ENABLED=true; README отделяет текущие цели Makefile arm-core от планируемых по spec quality-checks. Co-Authored-By: Claude Fable 5.1 --- dev/.env.example | 9 +++++++++ dev/README.md | 19 ++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/dev/.env.example b/dev/.env.example index 2c347e6..2e4a7eb 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -37,3 +37,12 @@ OPENBAO__MINIO__MINIO_BUCKET=arm-ka OPENBAO__NATS__NATS_URL=nats://localhost:4222 OPENBAO__NATS__NATS_SEED= OPENBAO__NATS__NATS_JWT= + +# SMB — заглушки (в Срезе 0 не используется); блок обязателен для Settings arm-core +OPENBAO__SMB__SMB_HOST=localhost +OPENBAO__SMB__SMB_PATH=share +OPENBAO__SMB__SMB_LOGIN=user +OPENBAO__SMB__SMB_PASSWORD=password + +# Включает подключение arm-core к NATS (без него /healthcheck показывает nats=off) +NATS_ENABLED=true diff --git a/dev/README.md b/dev/README.md index 16f7b4f..ccd4da4 100644 --- a/dev/README.md +++ b/dev/README.md @@ -80,6 +80,11 @@ docker compose run --rm --entrypoint sh minio-init -c 'mc alias set local http:/ | `OPENBAO__NATS__NATS_URL` | `nats://localhost:4222` | адрес NATS | | `OPENBAO__NATS__NATS_SEED` | пусто | nkey seed; пусто — подключение без nkeys | | `OPENBAO__NATS__NATS_JWT` | пусто | JWT NATS; пусто — подключение без nkeys | +| `OPENBAO__SMB__SMB_HOST` | `localhost` | заглушка SMB (в Срезе 0 не используется); блок обязателен для `Settings` arm-core | +| `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` | Два блока описывают одни и те же ресурсы: **при смене порта, пароля, пользователя или бакета меняйте обе части.** @@ -115,18 +120,26 @@ curl -s localhost:<порт сервиса>/healthcheck ``` Порт сервиса задаётся его настройками; в профиле `services` по spec это 8000 (`arm-core`), 8001 (`tabular-parser`), -8002 (`scan-gateway`). В ответе `/healthcheck` ожидаются `version` и `nats = ok`. +8002 (`scan-gateway`). Для `arm-core` в ответе `/healthcheck` ожидаются `version` и `nats = ok` — при `NATS_ENABLED=true` +(задан в `.env.example`; без него `nats = off`). Для `tabular-parser` и `scan-gateway` ожидаемые поля и переменные +зависят от их настроек — будет уточнено в задачах 5.x. ## Интеграционные тесты сервисов против compose (черновик) > Черновик: уточняется после задач 3.x и 5.x. -Цели `Makefile` одинаковы во всех сервисных репозиториях (spec `quality-checks`): +**Сейчас.** Makefile есть только у `arm-core`: `make test-integration` (`pytest -m integration`), +`make check` = `lint` + `test` (ruff, mypy, `pytest -m "not integration"`), `make check-full` = `check` + +`test-integration`. У `tabular-parser` и `scan-gateway` Makefile пока нет. + +**Планируется** (spec `quality-checks`, реализуется в задачах 3.x/4.x/5.x): одинаковый набор целей во всех сервисных +репозиториях со следующей семантикой: - `make test-integration` — `pytest -m integration` против инфраструктуры из этого compose; - `make check` — быстрые проверки без Docker (ruff, mypy, `pytest -m "not integration"`, идентичность контракта); - `make check-full` — `make check` + интеграционные тесты; для `arm-core` — ещё «`alembic upgrade head` → - `downgrade base` → `upgrade head`» и `scripts/check_dbml_vs_orm.py`. Выполняется перед созданием PR. + `downgrade base` → `upgrade head`» и `scripts/check_dbml_vs_orm.py` (этих шагов и скрипта пока нет). Выполняется +перед созданием PR. Интеграционные тесты **требуют поднятой инфраструктуры**: сначала `docker compose up -d && docker compose up -d --wait postgres nats minio`, затем `make test-integration` (или `make check-full`) в репозитории сервиса. Для `scan-gateway` -- 2.47.2 From 0ee1d41da78043b601f65e1e0a7d11e86921dcb7 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:47:31 +0400 Subject: [PATCH 21/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=207?= =?UTF-8?q?=20(2.2)=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md index 9f7b20e..177b468 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md @@ -41,5 +41,5 @@ ### Task 7: [2.2] [docs] `docs/dev/README.md` -- [ ] [docs] `docs/dev/README.md`: запуск инфраструктуры, переменные, сброс томов, запуск сервисов из venv без Nexus (`make venv` фильтрует `raisa_*`), черновик — дописывается в 5.4 +- [x] [docs] `docs/dev/README.md`: запуск инфраструктуры, переменные, сброс томов, запуск сервисов из venv без Nexus (`make venv` фильтрует `raisa_*`), черновик — дописывается в 5.4 diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 817aff7..71cbd24 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -24,7 +24,7 @@ - [x] 2.1 [docs] `docs/dev/docker-compose.yml`: сервисы `postgres` (16), `nats` (2.10, без `--jetstream`), `minio`, `minio-init` (создаёт бакет `arm-ka`), healthcheck'и, именованные тома; `docs/dev/.env.example`; проверка: `docker compose up -d` → все healthy, `mc ls` видит бакет, `curl :8222/varz` отвечает -- [ ] 2.2 [docs] `docs/dev/README.md`: запуск инфраструктуры, переменные, сброс томов, запуск сервисов из venv без Nexus +- [x] 2.2 [docs] `docs/dev/README.md`: запуск инфраструктуры, переменные, сброс томов, запуск сервисов из venv без Nexus (`make venv` фильтрует `raisa_*`), черновик — дописывается в 5.4 ## 3. Ядро (детали — `arm-core/openspec/changes/slice-0-skeleton/tasks.md`) -- 2.47.2 From 92d642d64a26faceb0ce5ac464b00eb4ba53afe8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:57:15 +0400 Subject: [PATCH 22/68] =?UTF-8?q?docs:=20design.md=20=E2=80=94=20=D1=80?= =?UTF-8?q?=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=2019=E2=80=9320=20(schema?= =?UTF-8?q?=5Fversion=20=D0=BF=D0=BE=20MAJOR,=20bind-=D0=B0=D0=B4=D1=80?= =?UTF-8?q?=D0=B5=D1=81=20=D0=BF=D0=BE=D1=80=D1=82=D0=BE=D0=B2=20compose)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- openspec/changes/slice-0-skeleton/design.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index 7d8712f..453f3e4 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -150,3 +150,5 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 16 | 2026-10-03 | Compose: MinIO берётся из неофициальных образов `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и `bitnamilegacy/minio-client:2025.7.21` | `minio/minio` и `minio/mc` недоступны на Docker Hub (denied), на quay.io нужных тегов нет; для dev достаточно любого S3-совместимого MinIO нужной версии API | образы сторонние: при появлении доступа к банковскому реестру/официальным образам — заменить две строки `image:` в `dev/docker-compose.yml`; в прод эти образы не попадают | | 17 | 2026-10-03 | Compose: сервис `minio` запускается от `user: "0:0"` | образ `alpine/minio` по умолчанию работает от пользователя `minio` и падает на свежем именованном томе | только dev; удалить строку, когда образ/том позволят писать без root | | 18 | 2026-10-03 | `dev/.env.example` дополнен заглушками `OPENBAO__SMB__*` и `NATS_ENABLED=true` | `Settings` шаблона (arm-core) требует блок `OPENBAO.SMB`, без него сервис из venv не стартует; `NATS_ENABLED` по умолчанию `false`, и `/healthcheck` показывает `nats = off`; spec велит копировать именно `docs/dev/.env.example` | убрать две группы строк; сервисы с другим набором обязательных настроек получат лишние (игнорируемые) переменные | +| 19 | 2026-10-03 | `schema_version` во всех трёх схемах: `const "1.0"` заменён на `type: string, pattern: ^1\.\d+$` (производитель v1.0 пишет `"1.0"`); в `$comment` и в 03-architecture.md добавлено правило «новый MINOR раскатывается сначала на потребителей» | spec «Версионирование контракта»: потребитель принимает тот же MAJOR и любой MINOR, сервис «поддерживающий 1.x» принимает 1.1 и отвергает только 2.0; с `const` это неисполнимо, а копии схем уйдут в сервисы побайтно (финальное ревью I-1) | вернуть `const "1.0"` и пересобрать копии схем в сервисах; побочный риск — копии в `arm-core`/`tabular-parser`/`scan-gateway` нужно обновить (тест идентичности контракта упадёт, пока копии не обновлены) | +| 20 | 2026-10-03 | Compose: порты хоста публикуются на `${BIND_ADDR:-127.0.0.1}`, а не на 0.0.0.0 | пароли по умолчанию (`armka`, `minioadmin`); все сценарии идут через localhost; spec bind-адрес не оговаривает (финальное ревью M-6) | если нужен доступ с другой машины — `BIND_ADDR=0.0.0.0` в `.env`; для запуска профиля `services` порты хоста не нужны | -- 2.47.2 From 991a152454c89fd2025e506bd52b10d5e05e65ce Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:58:48 +0400 Subject: [PATCH 23/68] =?UTF-8?q?fix(contracts):=20schema=5Fversion=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=BD=D0=B8=D0=BC=D0=B0=D0=B5=D1=82=20=D0=BB?= =?UTF-8?q?=D1=8E=D0=B1=D0=BE=D0=B9=20MINOR,=20=D0=B3=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=86=D1=8B=20confidence,=20=D0=BE=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20Nats-Msg-Id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - schema_version: const "1.0" заменён на pattern ^1\.\d+$ во всех трёх схемах (I-1, R-8); в $comment добавлено правило раскатки нового MINOR сначала на потребителей - границы 0–100 для organization_name_confidence, inn_confidence, line_code_confidence, line_name_confidence (I-2) - description recognition-result-message: две формы Nats-Msg-Id на recognition.result (M-5) Co-Authored-By: Claude Fable 5.1 --- contracts/recognition-request.schema.json | 4 ++-- contracts/recognition-result-message.schema.json | 6 +++--- contracts/recognition-result.schema.json | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/recognition-request.schema.json b/contracts/recognition-request.schema.json index 4545ef7..780573c 100644 --- a/contracts/recognition-request.schema.json +++ b/contracts/recognition-request.schema.json @@ -3,11 +3,11 @@ "$id": "arm-ka/recognition-request/v1", "title": "Запрос распознавания файла (arm-core → tabular-parser / scan-gateway), контракт v1.0", "description": "Тело сообщения на subject recognition.tabular.request или recognition.image.request; subject должен соответствовать полю pipeline (tabular → recognition.tabular.request, image → recognition.image.request). Заголовок Nats-Msg-Id = : (не поле тела). Один файл (stored_file) → один job.", - "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR.", + "$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"], "properties": { - "schema_version": { "const": "1.0" }, + "schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.0 пишет \"1.0\"" }, "job_id": { "type": "string", "format": "uuid", "description": "recognition_job.id" }, "file_id": { "type": "string", "format": "uuid", "description": "stored_file.id" }, "pipeline": { "enum": ["tabular", "image"], "description": "конвейер распознавания; определяет subject запроса" }, diff --git a/contracts/recognition-result-message.schema.json b/contracts/recognition-result-message.schema.json index 2a19195..1f20727 100644 --- a/contracts/recognition-result-message.schema.json +++ b/contracts/recognition-result-message.schema.json @@ -2,12 +2,12 @@ "$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 recognition.result. Заголовок Nats-Msg-Id (не поле тела) бывает трёх видов: : — запрос; ack:: — для status = processing; result:: — для done, needs_check и error. status = processing — ack «взял в работу», публикуется до чтения файла из S3. group_schema в конверт не входит: источник — файл результата (recognition-result).", - "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR.", + "description": "Тело сообщения на subject recognition.result. Заголовок 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"], "properties": { - "schema_version": { "const": "1.0" }, + "schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.0 пишет \"1.0\"" }, "job_id": { "type": "string", "format": "uuid", "description": "recognition_job.id" }, "file_id": { "type": "string", "format": "uuid", "description": "stored_file.id" }, "status": { "enum": ["processing", "done", "needs_check", "error"] }, diff --git a/contracts/recognition-result.schema.json b/contracts/recognition-result.schema.json index 0ef9333..c5eea87 100644 --- a/contracts/recognition-result.schema.json +++ b/contracts/recognition-result.schema.json @@ -3,11 +3,11 @@ "$id": "arm-ka/recognition-result/v1", "title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v1.0", "description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Нормализация счетов, резолв контрагентов, проверки — в arm-core.", - "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR.", + "$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", "service", "status", "documents"], "properties": { - "schema_version": { "const": "1.0" }, + "schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.0 пишет \"1.0\"" }, "job_id": { "type": "string", "format": "uuid" }, "file_id": { "type": "string", "format": "uuid" }, "service": { @@ -139,9 +139,9 @@ "properties": { "form_name": { "type": "string" }, "organization_name": { "type": ["string", "null"] }, - "organization_name_confidence": { "type": ["number", "null"] }, + "organization_name_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 }, "inn": { "type": ["string", "null"] }, - "inn_confidence": { "type": ["number", "null"] }, + "inn_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 }, "report_date_text": { "type": ["string", "null"] }, "report_date": { "type": ["string", "null"], "format": "date" }, "units": { "type": ["string", "null"], "examples": ["тыс. руб."] }, @@ -171,9 +171,9 @@ "required": ["line_code", "line_name", "values"], "properties": { "line_code": { "type": "string" }, - "line_code_confidence": { "type": ["number", "null"] }, + "line_code_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 }, "line_name": { "type": "string" }, - "line_name_confidence": { "type": ["number", "null"] }, + "line_name_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 }, "values": { "type": "array", "items": { -- 2.47.2 From 5bebdfe9fccd6bcdb4a5b14b736d4ba0395ad67d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:59:03 +0400 Subject: [PATCH 24/68] =?UTF-8?q?docs(architecture):=20=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D1=83=D0=B0=D0=BB=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D0=BF=D0=BE=D0=B7=D0=BD=D0=B0=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - убрана устаревшая пометка «черновик» у recognition-result.schema.json (M-1) - в строку ack добавлен schema_version, metrics и raw_s3_key помечены как опциональные, правило версионирования дополнено принятием любого MINOR и порядком раскатки (M-2) Co-Authored-By: Claude Fable 5.1 --- context/03-architecture.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/context/03-architecture.md b/context/03-architecture.md index 6a1bcb1..9bc4e22 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -28,7 +28,7 @@ (вычисления), согласование, ЕХ, аудит, права. Один Alembic, одна схема. - **tabular-parser** — читает файл из S3 `uploads/`, находит на листах ОСВ (§1) и формы ББ/ОФР (§12) **по содержимому**, пишет нормализованный JSON в S3 `results/`, публикует `recognition.result`. Контракт результата — - `docs/contracts/recognition-result.schema.json` (черновик; финализировать в срезе 2). Правила распознавания + `docs/contracts/recognition-result.schema.json` (контракт v1.0). Правила распознавания заголовков/группировок — `rules/*.yaml`. - **scan-gateway** — адаптер ИС Сканирование: переименовать файл `UUID_имя.pdf`, положить в «Входящие», опросить «Исходящие/UUID/UUID.json», сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), нормализовать @@ -47,8 +47,8 @@ | Поток | Subject | Кто пишет | Полезная нагрузка | `Nats-Msg-Id` | Схема | |---|---|---|---|---|---| | запрос | `recognition.tabular.request` / `recognition.image.request` | 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 | `recognition.result` | tabular-parser / scan-gateway | конверт со `status = processing`, `job_id`, `file_id`, `service {name, version}` | `ack::` | [message](../contracts/recognition-result-message.schema.json) | -| результат | `recognition.result` | 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) | +| ack | `recognition.result` | tabular-parser / scan-gateway | конверт со `schema_version`, `status = processing`, `job_id`, `file_id`, `service {name, version}` | `ack::` | [message](../contracts/recognition-result-message.schema.json) | +| результат | `recognition.result` | 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) | `group_schema` в конверт не входит — ядро берёт его из `documents[].group_schema` файла результата при маппинге. @@ -69,8 +69,11 @@ - Три формы `Nats-Msg-Id`: запрос `:`, ack `ack::`, результат `result::`. Ack (`processing`) сервис публикует **до чтения файла из S3**; повторная публикация запроса ядром идёт с тем же `Nats-Msg-Id` и тем же `attempt`. Дубликат результата ядро определяет по `recognition_job.result_msg_id`. -- `schema_version` — `MAJOR.MINOR`; сервис отвергает запрос с неизвестным MAJOR результатом `error` с кодом `SCHEMA_MISMATCH` и подтверждает его - ack'ом, чтобы не зациклить доставку. +- `schema_version` — `MAJOR.MINOR`; ядро и сервисы принимают тот же MAJOR и любой MINOR. Сервис отвергает запрос с + неизвестным MAJOR результатом `error` с кодом `SCHEMA_MISMATCH` и подтверждает его ack'ом, чтобы не зациклить + доставку. Новый MINOR раскатывается сначала на потребителей + (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают + незнакомые значения. - Сервисы не знают про БД и про бизнес-смысл: они возвращают структуру файла; нормализация счетов, резолв контрагентов, проверки — в ядре. -- 2.47.2 From 5f2be96e114da5381a531cd1fdbe190a3f634bd8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:59:56 +0400 Subject: [PATCH 25/68] =?UTF-8?q?fix(dev):=20healthcheck=20PostgreSQL=20?= =?UTF-8?q?=D0=BF=D0=BE=20TCP,=20=D0=BF=D0=BE=D1=80=D1=82=D1=8B=20=D0=BD?= =?UTF-8?q?=D0=B0=20127.0.0.1,=20=D0=BF=D0=B0=D1=80=D0=BE=D0=BB=D0=B8=20Mi?= =?UTF-8?q?nIO=20=D0=B8=D0=B7=20.env=20=D0=B2=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pg_isready с -h 127.0.0.1: временный сервер initdb слушает только сокет (M-3) - README: mc и psql берут учётные данные из переменных окружения контейнера, а не зашитые значения (M-4) - порты postgres/nats/minio публикуются на ${BIND_ADDR:-127.0.0.1}; BIND_ADDR добавлен в .env.example и README, примечание про безопасность обновлено (M-6, R-9) Co-Authored-By: Claude Fable 5.1 --- dev/.env.example | 3 +++ dev/README.md | 11 +++++++---- dev/docker-compose.yml | 13 +++++++------ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/dev/.env.example b/dev/.env.example index 2e4a7eb..446ee8a 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -2,6 +2,9 @@ # Файл читают docker compose (из этого каталога) и, блоком ниже, сервисы при запуске из venv. # --- Переменные docker-compose: учётные данные, порты хоста, бакет --- +# Адрес привязки портов хоста: 127.0.0.1 — только эта машина; 0.0.0.0 — доступ с других машин (пароли по умолчанию dev) +BIND_ADDR=127.0.0.1 + # PostgreSQL 16 (схема dkb создаётся миграциями arm-core, не compose) POSTGRES_DB=armka POSTGRES_USER=armka diff --git a/dev/README.md b/dev/README.md index ccd4da4..5a507f0 100644 --- a/dev/README.md +++ b/dev/README.md @@ -10,7 +10,7 @@ - Docker и compose v2 (проверено на docker 28 / compose 2.34). - `uv` и Python 3.11 — для запуска сервисов из venv (версия Python — как на платформе). -- Свободные порты хоста: 5432 (PostgreSQL), 4222 и 8222 (NATS и его мониторинг), 9000 и 9001 (MinIO API и консоль). +- Свободные порты хоста (по умолчанию доступны только с `127.0.0.1`, см. `BIND_ADDR`): 5432 (PostgreSQL), 4222 и 8222 (NATS и его мониторинг), 9000 и 9001 (MinIO API и консоль). Занятые порты меняются в `.env` (см. «Типичные проблемы»). ## Запуск инфраструктуры @@ -36,10 +36,10 @@ docker compose up -d && docker compose up -d --wait postgres nats minio ```bash docker compose ps # postgres, nats, minio — Up (healthy); minio-init — Exited (0) -docker compose exec postgres psql -U armka -d armka -c 'select version()' +docker compose exec postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "select version()"' curl -s localhost:8222/healthz # {"status":"ok"} curl -s localhost:8222/varz | grep -A1 jetstream # "jetstream": {} — JetStream не включён -docker compose run --rm --entrypoint sh minio-init -c 'mc alias set local http://minio:9000 minioadmin minioadmin >/dev/null && mc ls local' +docker compose run --rm --entrypoint sh minio-init -c 'mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && mc ls local' # [...] 0B arm-ka/ ``` @@ -55,6 +55,7 @@ docker compose run --rm --entrypoint sh minio-init -c 'mc alias set local http:/ | Переменная | По умолчанию | Для чего | |---|---|---| +| `BIND_ADDR` | `127.0.0.1` | адрес привязки всех портов хоста; `0.0.0.0` — доступ с других машин (небезопасно, см. «Примечания») | | `POSTGRES_DB` | `armka` | имя БД | | `POSTGRES_USER` | `armka` | пользователь БД | | `POSTGRES_PASSWORD` | `armka` | пароль БД | @@ -160,7 +161,9 @@ postgres nats minio`, затем `make test-integration` (или `make check-ful только для dev. Решения R-5/R-6: [`design.md`](../openspec/changes/slice-0-skeleton/design.md), раздел «Decisions made without discussion», решения №16–17. Замена на официальные образы — две строки `image:` в `docker-compose.yml` (и удаление `user: "0:0"`). -- **Безопасность.** Порты публикуются на `0.0.0.0` с паролями по умолчанию: это dev-окружение, не запускайте его в +- **Безопасность.** По умолчанию порты публикуются только на `127.0.0.1` (`BIND_ADDR`), снаружи машины они недоступны. + Чтобы открыть их для других машин, задайте в `.env` `BIND_ADDR=0.0.0.0` — но пароли по умолчанию (`armka`, + `minioadmin`) годятся только для dev, поэтому сначала смените их (в обоих блоках `.env`) и не делайте этого в недоверенной сети. ## Типичные проблемы diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 82b9c7a..7949922 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -3,6 +3,7 @@ # Ожидание готовности: docker compose up -d --wait postgres nats minio. Голый `up -d --wait` возвращает код 1, потому что # одноразовый minio-init завершается (хотя и с кодом 0) — для скриптов указывайте сервисы явно. # Все учётные данные, порты хоста и имя бакета берутся из .env (значения по умолчанию — dev). +# Порты публикуются только на 127.0.0.1; адрес привязки меняется через BIND_ADDR (см. .env.example). # Профиль `services` (arm-core, tabular-parser, scan-gateway, scan-emulator) добавляется в фазе 2. # Фиксированное имя проекта: одинаковые тома и сети из любого worktree (.wt/<срез>/docs/dev). @@ -16,11 +17,11 @@ services: POSTGRES_USER: ${POSTGRES_USER:-armka} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-armka} ports: - - "${POSTGRES_PORT:-5432}:5432" + - "${BIND_ADDR:-127.0.0.1}:${POSTGRES_PORT:-5432}:5432" volumes: - postgres-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 3s timeout: 3s retries: 20 @@ -31,8 +32,8 @@ services: image: nats:2.10.29-alpine command: ["-m", "8222"] ports: - - "${NATS_PORT:-4222}:4222" - - "${NATS_MONITOR_PORT:-8222}:8222" + - "${BIND_ADDR:-127.0.0.1}:${NATS_PORT:-4222}:4222" + - "${BIND_ADDR:-127.0.0.1}:${NATS_MONITOR_PORT:-8222}:8222" healthcheck: test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8222/healthz || exit 1"] interval: 3s @@ -50,8 +51,8 @@ services: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} ports: - - "${MINIO_API_PORT:-9000}:9000" - - "${MINIO_CONSOLE_PORT:-9001}:9001" + - "${BIND_ADDR:-127.0.0.1}:${MINIO_API_PORT:-9000}:9000" + - "${BIND_ADDR:-127.0.0.1}:${MINIO_CONSOLE_PORT:-9001}:9001" volumes: - minio-data:/data healthcheck: -- 2.47.2 From 039bb4ed7b131a84462727220e91e19ebb57ea26 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:19:39 +0400 Subject: [PATCH 26/68] =?UTF-8?q?build:=20Makefile=20=D1=80=D0=B5=D0=BF?= =?UTF-8?q?=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D1=8F=20docs=20?= =?UTF-8?q?=D1=81=20=D1=86=D0=B5=D0=BB=D1=8C=D1=8E=20check=20(=D0=B2=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BA=D1=82=D0=B0=20=D0=B8=20OpenSpec)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 1 + Makefile | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index 1ffed2b..e44f625 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .superpowers/ .bounded dev/.env +.venv/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b280266 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +# Команды проверки репозитория arm-ka-docs. Запуск из корня репозитория. +# Сервисных целей (lint/test/up...) здесь нет: описание общих целей сервисов — dev/checks.md. +VENV := .venv +# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должен быть jsonschema) +PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3) +CHANGE ?= slice-0-skeleton + +.PHONY: venv validate-contracts validate-openspec check + +# Единственная зависимость скриптов проверки контракта — jsonschema +venv: + uv venv --python 3.11 --allow-existing $(VENV) + uv pip install --python $(VENV)/bin/python jsonschema + +# Примеры contracts/examples/ против трёх схем JSON Schema 2020-12 +validate-contracts: + $(PY) scripts/validate_contracts.py + +# Спека текущего изменения OpenSpec (нужен CLI openspec в PATH) +validate-openspec: + openspec validate $(CHANGE) --strict + +check: validate-contracts validate-openspec -- 2.47.2 From 401cdbc362cb35ca6234132ed8466e5c250d0552 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:20:27 +0400 Subject: [PATCH 27/68] =?UTF-8?q?docs(dev):=20checks.md=20=E2=80=94=20?= =?UTF-8?q?=D1=86=D0=B5=D0=BB=D0=B8=20make,=20=D0=BF=D0=BE=D1=80=D1=8F?= =?UTF-8?q?=D0=B4=D0=BE=D0=BA=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20PR,=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=BF=D1=83=D1=81=D0=BA=20=D0=B1=D0=B5=D0=B7=20docs/fixt?= =?UTF-8?q?ures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- dev/checks.md | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 dev/checks.md diff --git a/dev/checks.md b/dev/checks.md new file mode 100644 index 0000000..3262196 --- /dev/null +++ b/dev/checks.md @@ -0,0 +1,151 @@ +# Проверки качества: цели `make` и порядок перед PR + +Конфигураций CI в репозиториях нет (в Gitea CI не используется, пайплайн платформы банка вне зоны проекта), поэтому +проверки выполняются локально одной командой перед каждым PR. Требования — spec `quality-checks` +(`openspec/changes/slice-0-skeleton/specs/quality-checks/spec.md`), решение D-8. + +Все цели запускаются **из корня репозитория сервиса** (`arm-core`, `tabular-parser`, `scan-gateway`); рецепты сами +переходят в `api/` и используют окружение `api/.venv`. Компоуз-инфраструктура — `docs/dev/docker-compose.yml` +(см. [`README.md`](README.md)); в workspace и в worktree `.wt/<срез>/` она лежит рядом как `../docs/dev/`. + +## Общие цели + +Одинаковый набор и одинаковая семантика во всех трёх сервисных репозиториях. Репозиторий вправе добавлять свои цели +(см. ниже), но не меняет смысл общих. + +| Цель | Что делает | Нужен Docker / compose | +|---|---|---| +| `make venv` | `api/.venv` на Python 3.11 через `uv`; ставит `requirements.txt` без строк `raisa_*` (пакеты Nexus банка вне банка недоступны, D-10) и `requirements-dev.txt` | нет | +| `make lint` | `ruff check .`, `ruff format --check .`, `mypy src` (в `arm-core` — `mypy src scripts`) | нет | +| `make test` | `pytest -m "not integration"`: юнит-тесты; **включает тест идентичности вендоренного контракта** `api/contracts/` с `../docs/contracts` | нет | +| `make test-integration` | `pytest -m integration` против инфраструктуры compose (PostgreSQL 16, NATS, MinIO; бакет `arm-ka` должен существовать, его создаёт `make up`) | да, `make up` | +| `make check` | `make lint` затем `make test`; останавливается на первой упавшей проверке | нет | +| `make check-full` | `make check` + `make test-integration` + сборка docker-образа (`docker build`); для `arm-core` — расширенный набор, см. ниже | да, `make up` и Docker | +| `make up` | поднимает инфраструктуру из `../docs/dev/docker-compose.yml`; постоянные сервисы `postgres`, `nats`, `minio` | Docker | + +Сводка шагов `make check`: `ruff check .` → `ruff format --check .` → `mypy src` → `pytest -m "not integration"`. +Любой ненулевой код останавливает цель на этом шаге, следующие не выполняются. `make check` не требует Docker, +PostgreSQL, NATS и сети. + +### `make up` + +Поднимает compose-проект `arm-ka` (имя зафиксировано в compose, поэтому тома общие для любого worktree): + +```bash +docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka up -d --wait postgres nats minio +docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka run --rm minio-init # бакет arm-ka (идемпотентно) +``` + +Почему так: голый `docker compose up -d --wait` возвращает код 1 (одноразовый `minio-init` завершается кодом 0, compose +считает это сбоем), поэтому `--wait` даётся со списком долгоживущих сервисов, а бакет создаётся отдельным шагом. Без +бакета интеграционные тесты на свежем томе падают на `NoSuchBucket`. Целей `down`/`stop`/`restart` нет намеренно: проект +`arm-ka` общий для всех сервисов; остановка и сброс — вручную (`docker compose down`, `down -v`), см. `README.md`. + +> Эталонная реализация `make up` — в `scan-gateway`. Реализации в `arm-core` и `tabular-parser` отличаются +> (см. «Известные расхождения реализаций»). + +### Состав `make check-full` по репозиториям + +| Репозиторий | Шаги по порядку | +|---|---| +| `scan-gateway` | `check` → `test-integration` → `docker-build` (образ `scan-gateway:dev`); интеграционный тест поднимает эмулятор ИС Сканирование в процессе теста | +| `tabular-parser` | `check` → `test-integration` → `docker-build` (образ `tabular-parser`) | +| `arm-core` | `check` → `dbml-check` → `check-infra` → `test-integration` → `docker-build` → `web-check` | + +Для `arm-core` дополнительно: + +- `dbml-check` — `python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.13.dbml` (путь от `api/`; + другой — `make dbml-check DBML=<путь относительно api/>`): ORM обязана совпадать с DBML; расхождения печатаются + построчно, код 1. +- Цикл миграций «`alembic upgrade head` → `alembic downgrade base` → `alembic upgrade head`» на чистой БД, пустой + autogenerate и сверка схемы с `create_all` ORM выполняет `tests/test_migrations.py`; он входит в `test-integration`, + поэтому отдельным шагом в `check-full` не гоняется. Для быстрой проверки одних миграций: `make migration-cycle`. +- `check-infra` — TCP-проверка доступности PostgreSQL и NATS из `api/.env.test`; вместо длинного трейсбека pytest + подсказывает «поднимите инфраструктуру: `make up`». +- `web-check` — `npm install`, `biome check src`, `tsc --noEmit`, `npm run build` в `web/`. + +Дополнительные цели репозиториев (не общие): `arm-core` — `migrate`, `seed`, `seed-dev`, `dbml-check`, +`migration-cycle`, `check-infra`, `docker-build`, `web-check`; `scan-gateway` — `help`, `docker-build`, `emulator`; +`tabular-parser` — `docker-build`. + +## Внешние ресурсы: docs и fixtures опциональны + +Проверки сервисов обращаются к соседним репозиториям workspace и **не должны падать**, если их нет: тест +пропускается (`skipped`) с явной причиной, остальные выполняются, `make check` остаётся зелёным. + +| Что | Где ищется | Что пропускается без него | +|---|---|---| +| `docs` | `../docs` или каталог из `ARM_KA_DOCS` | тест идентичности контракта `api/contracts/` с `docs/contracts/`; в `arm-core` ещё сверка с DBML в тестах (`dbml-check` как цель `make` требует DBML и падает без него) | +| `fixtures` | `../fixtures` или каталог из `ARM_KA_FIXTURES` | тесты с маркером `fixtures` (реальные данные, внутренний доступ): причина пропуска — «ARM_KA_FIXTURES недоступен …» | + +Пример (docs и fixtures лежат не рядом): + +```bash +export ARM_KA_DOCS=/path/to/arm-ka-docs +export ARM_KA_FIXTURES=/path/to/arm-ka-fixtures +make check +``` + +Причины пропусков видно в выводе (`pytest --verbose` в `addopts`: строки `SKIPPED (…)`); полный список — +`cd api && .venv/bin/pytest -m "not integration" -rs`. + +Важно: зелёный `make check` с пропусками **не** доказывает идентичность контракта и прохождение на реальных данных. Перед +PR убедитесь, что `docs` и `fixtures` доступны (пропусков по этим причинам нет) либо явно укажите в описании PR, что +пропущено. + +> Фактическая поддержка переменных различается по репозиториям — см. «Известные расхождения реализаций». + +## Проверки этого репозитория (`docs`) + +В репозитории `arm-ka-docs` нет сервисных целей; `Makefile` в его корне содержит только проверки спеки и контракта: + +| Цель | Что делает | +|---|---| +| `make venv` | `.venv` (Python 3.11, `uv`) с `jsonschema` — единственной зависимостью скриптов | +| `make validate-contracts` | `python scripts/validate_contracts.py`: примеры `contracts/examples/` против трёх схем JSON Schema 2020-12 (3 схемы, 8 примеров); код 1 при любой ошибке | +| `make validate-openspec` | `openspec validate slice-0-skeleton --strict` (другое изменение: `make validate-openspec CHANGE=`) | +| `make check` | обе проверки выше | + +Без `make`: `python scripts/validate_contracts.py` (нужен `jsonschema`) и `openspec validate --strict`. +Менять `contracts/**` нужно осознанно: байтовые копии схем лежат в `api/contracts/` каждого сервиса, после правки их +нужно пересинхронизировать (`cp`), иначе тест идентичности в сервисах упадёт. + +## Порядок перед PR + +1. Поднять инфраструктуру: `make up` (в репозитории сервиса). Убедиться, что `docs` и `fixtures` доступны (рядом или через + `ARM_KA_DOCS`, `ARM_KA_FIXTURES`). +2. `make check` — быстрые проверки. Исправлять до зелёного. +3. `make check-full` — полная проверка (интеграционные тесты, образ; для `arm-core` — ещё DBML, миграции, `web`). +4. Если менялись `contracts/`, `model/` или спека в репозитории `docs`: в нём `make check`, затем в каждом затронутом + сервисе — пересинхронизация копий контракта и пункты 2–3. +5. Итог `make check-full` (зелёный/красный, число тестов, **что пропущено и почему**) вписать в описание PR. + +Правила: PR создаётся только после зелёного `make check-full`; один OpenSpec change и один PR — на один репозиторий; +`make check-full` не заменяет ревью. + +## Типичные проблемы + +- **`make test-integration` падает на соединении** — не поднята инфраструктура: `make up`; порты заняты — поменять в + `docs/dev/.env` и в `api/.env.test`/`api/.env` сервиса (`README.md`). +- **`NoSuchBucket`** — бакета нет на свежем томе: `docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka + run --rm minio-init`. +- **`tabular-parser`: интеграционный тест получает чужие ответы** — на шине compose не должно работать другого + экземпляра сервиса из той же queue group (интеграционный тест поднимает сервис в процессе теста). +- **Нет `uv`/Python 3.11** — `make venv` их требует (версия Python — как на платформе). +- **`docker build` падает без сети** — это единственные шаги `check-full`, которым нужна сеть; `make check` её не требует. + +## Известные расхождения реализаций + +Фактические реализации в `Makefile` трёх сервисов расходятся с описанной выше семантикой. Сервисы в рамках Среза 0 +не менялись; расхождения занесены как находки ревью. + +- **`make up`.** Эталон — `scan-gateway` (`--project-name arm-ka`, `--wait` со списком сервисов, затем `run --rm + minio-init`). `tabular-parser` не создаёт бакет (нет шага `minio-init`) и на свежем томе даёт `NoSuchBucket`; + `arm-core` выполняет `docker compose up -d` без `--wait`, то есть возвращается до готовности сервисов. +- **Структура `check-full`.** В `scan-gateway` шаги вызываются через `$(MAKE)` (порядок и остановка на первой ошибке + гарантированы и при `make -j`); в `arm-core` и `tabular-parser` — пререквизитами (при последовательном запуске + эквивалентно, при `make -j` порядок не гарантирован). Запускайте без `-j`. +- **`ARM_KA_DOCS`/`ARM_KA_FIXTURES`.** `arm-core` учитывает обе переменные; `tabular-parser` — только `ARM_KA_DOCS` + (тестов с маркером `fixtures` пока нет); `scan-gateway` — ни одну (ищет `../docs` и `../fixtures/ocr` по фиксированному + пути; тексты причин пропуска не содержат «ARM_KA_FIXTURES недоступен»). Пока расхождение не устранено, для + `scan-gateway` docs/fixtures нужно класть рядом. -- 2.47.2 From b6e2be9f875b5ce929eec766061c9a6e2c5801fc Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:20:41 +0400 Subject: [PATCH 28/68] =?UTF-8?q?docs(dev):=20README=20=D1=81=D1=81=D1=8B?= =?UTF-8?q?=D0=BB=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=BD=D0=B0=20checks.md?= =?UTF-8?q?,=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D0=BE=20=D1=83=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=B5=D0=B2=D1=88=D0=B5=D0=B5=20=C2=ABMakefile=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D1=83=20arm-core=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- dev/README.md | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/dev/README.md b/dev/README.md index 5a507f0..e4ca357 100644 --- a/dev/README.md +++ b/dev/README.md @@ -129,29 +129,22 @@ curl -s localhost:<порт сервиса>/healthcheck > Черновик: уточняется после задач 3.x и 5.x. -**Сейчас.** Makefile есть только у `arm-core`: `make test-integration` (`pytest -m integration`), -`make check` = `lint` + `test` (ruff, mypy, `pytest -m "not integration"`), `make check-full` = `check` + -`test-integration`. У `tabular-parser` и `scan-gateway` Makefile пока нет. - -**Планируется** (spec `quality-checks`, реализуется в задачах 3.x/4.x/5.x): одинаковый набор целей во всех сервисных -репозиториях со следующей семантикой: - -- `make test-integration` — `pytest -m integration` против инфраструктуры из этого compose; -- `make check` — быстрые проверки без Docker (ruff, mypy, `pytest -m "not integration"`, идентичность контракта); -- `make check-full` — `make check` + интеграционные тесты; для `arm-core` — ещё «`alembic upgrade head` → - `downgrade base` → `upgrade head`» и `scripts/check_dbml_vs_orm.py` (этих шагов и скрипта пока нет). Выполняется -перед созданием PR. +Общий набор целей (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`) есть во всех сервисных репозиториях +(`arm-core`, `tabular-parser`, `scan-gateway`); семантика, состав `check-full`, порядок перед PR и пропуск тестов без +`docs`/`fixtures` описаны в [`checks.md`](checks.md). Кратко: `make check` — быстрые проверки без Docker, `make check-full` — +`check` + интеграционные тесты против этого compose + образ (для `arm-core` ещё сверка с DBML, миграции и `web`); +выполняется перед созданием PR. Интеграционные тесты **требуют поднятой инфраструктуры**: сначала `docker compose up -d && docker compose up -d --wait postgres nats minio`, затем `make test-integration` (или `make check-full`) в репозитории сервиса. Для `scan-gateway` -дополнительно нужен эмулятор ИС Сканирование (появится вместе с профилем `services`). Общее описание проверок — -`docs/dev/checks.md` (будет добавлено). +эмулятор ИС Сканирование поднимается в процессе интеграционного теста (отдельный контейнер появится вместе с профилем +`services`). Общее описание проверок — [`checks.md`](checks.md). ## Проверка контракта Из корня репозитория `docs`: `python scripts/validate_contracts.py` — валидирует примеры в [`contracts/examples/`](../contracts/examples/) тремя схемами из [`contracts/`](../contracts/) (3 схемы, 8 примеров; -нужен `jsonschema`). +нужен `jsonschema`). Или `make venv && make check` (ещё `openspec validate`), см. [`checks.md`](checks.md). ## Примечания -- 2.47.2 From eaf2489e9809de5c05c75b48dc1944ebe6959ff7 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:20:53 +0400 Subject: [PATCH 29/68] =?UTF-8?q?docs(openspec):=20design.md=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=2021=E2=80=9323?= =?UTF-8?q?=20(Makefile=20docs,=20=D1=8D=D1=82=D0=B0=D0=BB=D0=BE=D0=BD=20?= =?UTF-8?q?=D1=86=D0=B5=D0=BB=D0=B5=D0=B9,=20README=20dev);=20checks.md=20?= =?UTF-8?q?=E2=80=94=20=D1=83=D1=82=D0=BE=D1=87=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D1=80=D0=BE=20=D1=81=D0=B5=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- dev/checks.md | 2 +- openspec/changes/slice-0-skeleton/design.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dev/checks.md b/dev/checks.md index 3262196..678c7b5 100644 --- a/dev/checks.md +++ b/dev/checks.md @@ -132,7 +132,7 @@ PR убедитесь, что `docs` и `fixtures` доступны (пропу - **`tabular-parser`: интеграционный тест получает чужие ответы** — на шине compose не должно работать другого экземпляра сервиса из той же queue group (интеграционный тест поднимает сервис в процессе теста). - **Нет `uv`/Python 3.11** — `make venv` их требует (версия Python — как на платформе). -- **`docker build` падает без сети** — это единственные шаги `check-full`, которым нужна сеть; `make check` её не требует. +- **`docker build` (и `npm install` в `web-check` у `arm-core`) падает без сети** — это шаги `check-full`, которым нужна сеть (образы, пакеты); `make check` сети не требует. ## Известные расхождения реализаций diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index 453f3e4..e6aec00 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -152,3 +152,6 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 18 | 2026-10-03 | `dev/.env.example` дополнен заглушками `OPENBAO__SMB__*` и `NATS_ENABLED=true` | `Settings` шаблона (arm-core) требует блок `OPENBAO.SMB`, без него сервис из venv не стартует; `NATS_ENABLED` по умолчанию `false`, и `/healthcheck` показывает `nats = off`; spec велит копировать именно `docs/dev/.env.example` | убрать две группы строк; сервисы с другим набором обязательных настроек получат лишние (игнорируемые) переменные | | 19 | 2026-10-03 | `schema_version` во всех трёх схемах: `const "1.0"` заменён на `type: string, pattern: ^1\.\d+$` (производитель v1.0 пишет `"1.0"`); в `$comment` и в 03-architecture.md добавлено правило «новый MINOR раскатывается сначала на потребителей» | spec «Версионирование контракта»: потребитель принимает тот же MAJOR и любой MINOR, сервис «поддерживающий 1.x» принимает 1.1 и отвергает только 2.0; с `const` это неисполнимо, а копии схем уйдут в сервисы побайтно (финальное ревью I-1) | вернуть `const "1.0"` и пересобрать копии схем в сервисах; побочный риск — копии в `arm-core`/`tabular-parser`/`scan-gateway` нужно обновить (тест идентичности контракта упадёт, пока копии не обновлены) | | 20 | 2026-10-03 | Compose: порты хоста публикуются на `${BIND_ADDR:-127.0.0.1}`, а не на 0.0.0.0 | пароли по умолчанию (`armka`, `minioadmin`); все сценарии идут через localhost; spec bind-адрес не оговаривает (финальное ревью M-6) | если нужен доступ с другой машины — `BIND_ADDR=0.0.0.0` в `.env`; для запуска профиля `services` порты хоста не нужны | +| 21 | 2026-10-04 | В корне репозитория `docs` добавлен `Makefile` с целями `venv`, `validate-contracts`, `validate-openspec`, `check` (сервисных целей нет); `jsonschema` ставится в `.venv` (в `.gitignore`) | дополнение контроллера к задаче 4.1: проверки контракта и спеки должны запускаться одной командой, как в сервисах; `check` = `scripts/validate_contracts.py` + `openspec validate slice-0-skeleton --strict` | удалить `Makefile` и строку `.venv/` в `.gitignore`; команды из `dev/checks.md` останутся запускаемыми вручную | +| 22 | 2026-10-04 | `dev/checks.md` описывает общие цели по эталону `scan-gateway` (`up` с `--project-name arm-ka`, `--wait postgres nats minio` и `run --rm minio-init`; `check-full` как последовательность шагов), расхождения `arm-core`/`tabular-parser` и неполная поддержка `ARM_KA_DOCS`/`ARM_KA_FIXTURES` в `scan-gateway` вынесены в раздел «Известные расхождения реализаций», код сервисов не менялся | spec «Одинаковый набор целей» требует единой семантики, а фактические `Makefile` расходятся; правка сервисов вне задачи 4.1 и их PR | если эталон выбран неверно — переписать раздел `make up` в `checks.md`; расхождения останутся незакрытыми, пока сервисы не выровнены | +| 23 | 2026-10-04 | В `dev/README.md` раздел «Интеграционные тесты сервисов против compose» заменён ссылкой на `checks.md`; убрано утверждение «Makefile есть только у `arm-core`» | утверждение устарело (Makefile есть у всех трёх сервисов), единый источник описания проверок — `checks.md` | вернуть старый текст раздела | -- 2.47.2 From 69fc0cc5810353163f8309e95e4b360a3af3f8dd Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:25:21 +0400 Subject: [PATCH 30/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=204.1?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md index 815038a..50fcc4a 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md @@ -17,7 +17,7 @@ ### Task 1: [4.1] [docs] `docs/dev/checks.md` -- [ ] [docs] `docs/dev/checks.md`: общие цели `Makefile` (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`), что делает каждая, порядок перед PR, как пропускаются тесты без фикстур/docs; проверка: цели `arm-core` соответствуют описанию +- [x] [docs] `docs/dev/checks.md`: общие цели `Makefile` (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`), что делает каждая, порядок перед PR, как пропускаются тесты без фикстур/docs; проверка: цели `arm-core` соответствуют описанию ### Task 2: [4.2] [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` се diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 71cbd24..212fc01 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -43,7 +43,7 @@ ## 4. Проверки качества (параллельно с 3.x, после первых тестов) -- [ ] 4.1 [docs] `docs/dev/checks.md`: общие цели `Makefile` (`up`, `test`, `test-integration`, `lint`, `check`, +- [x] 4.1 [docs] `docs/dev/checks.md`: общие цели `Makefile` (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`), что делает каждая, порядок перед PR, как пропускаются тесты без фикстур/docs; проверка: цели `arm-core` соответствуют описанию - [ ] 4.2 [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` сервисов через `include` или -- 2.47.2 From b08df040e94927bc3b37546cf73e55df0b45a2ad Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:26:51 +0400 Subject: [PATCH 31/68] =?UTF-8?q?build(dev):=20Makefile.common=20=E2=80=94?= =?UTF-8?q?=20=D1=8D=D1=82=D0=B0=D0=BB=D0=BE=D0=BD=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?=D1=84=D1=80=D0=B0=D0=B3=D0=BC=D0=B5=D0=BD=D1=82=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=89=D0=B8=D1=85=20=D1=86=D0=B5=D0=BB=D0=B5=D0=B9=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Цели venv, lint, test, test-integration, check, docker-build, check-full, up по эталону scan-gateway; различия сервисов вынесены в переменные (MYPY_PATHS, IMAGE, COMPOSE_*, CHECK_FULL_STEPS). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- dev/Makefile.common | 74 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 dev/Makefile.common diff --git a/dev/Makefile.common b/dev/Makefile.common new file mode 100644 index 0000000..37b03ce --- /dev/null +++ b/dev/Makefile.common @@ -0,0 +1,74 @@ +# Эталонный фрагмент общих целей разработки для сервисов АРМ КА (arm-core, tabular-parser, scan-gateway). +# Описание целей и порядок проверок перед PR — dev/checks.md. Источник эталона: Makefile scan-gateway. +# +# Подключение (два способа, смысл одинаков): +# 1. include (в workspace и в worktree .wt/<срез>/ docs лежит рядом с сервисом): +# include ../docs/dev/Makefile.common +# Ставьте include ПОСЛЕ первой собственной цели сервиса или задайте `.DEFAULT_GOAL := check`, +# иначе целью по умолчанию станет `venv` (первая цель этого файла). +# 2. копирование: скопируйте содержимое в Makefile сервиса, не меняя рецепты общих целей. +# +# Запуск — из корня репозитория сервиса; рецепты сами переходят в api/ и используют окружение api/.venv. +# Все переменные заданы через `?=`: сервис переопределяет их до include (или в командной строке: make up COMPOSE_PROJECT=x). +# +# VENV каталог окружения относительно корня сервиса (api/.venv) +# BIN каталог исполняемых файлов окружения (абсолютный путь, строится от $(CURDIR)) +# MYPY_PATHS что проверяет mypy (src; у arm-core: src scripts) +# COMPOSE_FILE compose-файл инфраструктуры (../docs/dev/docker-compose.yml) +# COMPOSE_PROJECT имя compose-проекта; общее для всех сервисов workspace (arm-ka), поэтому целей down/stop/restart нет +# IMAGE тег docker-образа (по умолчанию — имя каталога сервиса) +# CHECK_FULL_STEPS цели `check-full` по порядку; сервис дописывает свои (arm-core: check dbml-check check-infra +# test-integration docker-build web-check). Собственные цели сервиса (migrate, seed, web-check ...) +# определяются в его Makefile, не здесь. +# +# Запускайте без `make -j`: шаги check-full вызываются последовательно через $(MAKE) и останавливаются на первой ошибке. + +VENV ?= api/.venv +BIN ?= $(CURDIR)/$(VENV)/bin +MYPY_PATHS ?= src +COMPOSE_FILE ?= ../docs/dev/docker-compose.yml +COMPOSE_PROJECT ?= arm-ka +IMAGE ?= $(notdir $(CURDIR)) +CHECK_FULL_STEPS ?= check test-integration docker-build + +# Перевод строки для $(foreach ...) в рецепте: каждый шаг становится отдельной строкой рецепта (две пустые строки обязательны) +define newline + + +endef + +.PHONY: venv lint test test-integration check docker-build check-full up + +# Python 3.11 (версия платформы); внутренние пакеты raisa_* вне банка недоступны — пропускаем их +venv: + cd api && uv venv --python 3.11 --allow-existing .venv + cd api && grep -v '^raisa_' requirements.txt | uv pip install --python .venv/bin/python -r - + cd api && grep -v '^-r' requirements-dev.txt | uv pip install --python .venv/bin/python -r - + +lint: + cd api && $(BIN)/ruff check . + cd api && $(BIN)/ruff format --check . + cd api && $(BIN)/mypy $(MYPY_PATHS) + +test: + cd api && $(BIN)/pytest -m "not integration" + +test-integration: + cd api && $(BIN)/pytest -m integration + +check: lint test + +# Образ собирается из корня репозитория; тесты в образе не запускаются +docker-build: + docker build -t $(IMAGE) . + +# Полная проверка перед PR. Шаги вызываются последовательно через $(MAKE), а не как пререквизиты: +# порядок и остановка на первой ошибке гарантированы и при make -j. +check-full: + $(foreach step,$(CHECK_FULL_STEPS),$(MAKE) $(step)$(newline)) + +# Общий compose-проект workspace. --wait — только со списком долгоживущих сервисов: одноразовый minio-init +# завершается кодом 0, и compose считает это сбоем. Бакет arm-ka создаётся отдельным идемпотентным шагом. +up: + docker compose -f $(COMPOSE_FILE) --project-name $(COMPOSE_PROJECT) up -d --wait postgres nats minio + docker compose -f $(COMPOSE_FILE) --project-name $(COMPOSE_PROJECT) run --rm minio-init -- 2.47.2 From 792042ffad9c77c611cd3e0ab3788259cfc8d270 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:27:00 +0400 Subject: [PATCH 32/68] =?UTF-8?q?docs(dev):=20checks.md=20=E2=80=94=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BC=D0=B5=D1=87=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D1=8C=D1=8E=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B8?= =?UTF-8?q?=204.1=20(T1.1=E2=80=93T1.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Уточнены: fixtures только в arm-core, mypy scripts у arm-core, формулировка про инфраструктуру compose и «одинаковую семантику» целей. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- dev/checks.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/dev/checks.md b/dev/checks.md index 678c7b5..4d53699 100644 --- a/dev/checks.md +++ b/dev/checks.md @@ -5,13 +5,14 @@ (`openspec/changes/slice-0-skeleton/specs/quality-checks/spec.md`), решение D-8. Все цели запускаются **из корня репозитория сервиса** (`arm-core`, `tabular-parser`, `scan-gateway`); рецепты сами -переходят в `api/` и используют окружение `api/.venv`. Компоуз-инфраструктура — `docs/dev/docker-compose.yml` +переходят в `api/` и используют окружение `api/.venv`. Инфраструктура compose — `docs/dev/docker-compose.yml` (см. [`README.md`](README.md)); в workspace и в worktree `.wt/<срез>/` она лежит рядом как `../docs/dev/`. ## Общие цели -Одинаковый набор и одинаковая семантика во всех трёх сервисных репозиториях. Репозиторий вправе добавлять свои цели -(см. ниже), но не меняет смысл общих. +Одинаковый набор целей во всех трёх сервисных репозиториях; семантика, описанная ниже, — эталонная (реализации +расходятся в деталях, см. «Известные расхождения реализаций»). Репозиторий вправе добавлять свои цели (см. ниже), но +не меняет смысл общих. | Цель | Что делает | Нужен Docker / compose | |---|---|---| @@ -23,7 +24,8 @@ | `make check-full` | `make check` + `make test-integration` + сборка docker-образа (`docker build`); для `arm-core` — расширенный набор, см. ниже | да, `make up` и Docker | | `make up` | поднимает инфраструктуру из `../docs/dev/docker-compose.yml`; постоянные сервисы `postgres`, `nats`, `minio` | Docker | -Сводка шагов `make check`: `ruff check .` → `ruff format --check .` → `mypy src` → `pytest -m "not integration"`. +Сводка шагов `make check`: `ruff check .` → `ruff format --check .` → `mypy src` (в `arm-core` — `mypy src scripts`) → +`pytest -m "not integration"`. Любой ненулевой код останавливает цель на этом шаге, следующие не выполняются. `make check` не требует Docker, PostgreSQL, NATS и сети. @@ -76,7 +78,7 @@ docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka run --rm | Что | Где ищется | Что пропускается без него | |---|---|---| | `docs` | `../docs` или каталог из `ARM_KA_DOCS` | тест идентичности контракта `api/contracts/` с `docs/contracts/`; в `arm-core` ещё сверка с DBML в тестах (`dbml-check` как цель `make` требует DBML и падает без него) | -| `fixtures` | `../fixtures` или каталог из `ARM_KA_FIXTURES` | тесты с маркером `fixtures` (реальные данные, внутренний доступ): причина пропуска — «ARM_KA_FIXTURES недоступен …» | +| `fixtures` | `../fixtures` или каталог из `ARM_KA_FIXTURES` (в `arm-core`; в остальных сервисах — см. «Известные расхождения реализаций») | тесты с маркером `fixtures` (реальные данные, внутренний доступ): причина пропуска — «ARM_KA_FIXTURES недоступен …» (в `arm-core`) | Пример (docs и fixtures лежат не рядом): @@ -113,7 +115,8 @@ PR убедитесь, что `docs` и `fixtures` доступны (пропу ## Порядок перед PR 1. Поднять инфраструктуру: `make up` (в репозитории сервиса). Убедиться, что `docs` и `fixtures` доступны (рядом или через - `ARM_KA_DOCS`, `ARM_KA_FIXTURES`). + `ARM_KA_DOCS`, `ARM_KA_FIXTURES` — последняя поддерживается только в `arm-core`, см. «Известные расхождения + реализаций»). 2. `make check` — быстрые проверки. Исправлять до зелёного. 3. `make check-full` — полная проверка (интеграционные тесты, образ; для `arm-core` — ещё DBML, миграции, `web`). 4. Если менялись `contracts/`, `model/` или спека в репозитории `docs`: в нём `make check`, затем в каждом затронутом -- 2.47.2 From 9acd2f90c42e66846facef3d0d2a5e32b14746bf Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:27:21 +0400 Subject: [PATCH 33/68] =?UTF-8?q?docs(dev):=20checks.md=20=D0=B8=20README?= =?UTF-8?q?=20=D0=BE=D0=BF=D0=B8=D1=81=D1=8B=D0=B2=D0=B0=D1=8E=D1=82=20Mak?= =?UTF-8?q?efile.common;=20design.md=20=E2=80=94=20=D1=80=D0=B5=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=2024=E2=80=9325?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- dev/README.md | 2 +- dev/checks.md | 25 +++++++++++++++++++++ openspec/changes/slice-0-skeleton/design.md | 2 ++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/dev/README.md b/dev/README.md index e4ca357..e6553eb 100644 --- a/dev/README.md +++ b/dev/README.md @@ -131,7 +131,7 @@ curl -s localhost:<порт сервиса>/healthcheck Общий набор целей (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`) есть во всех сервисных репозиториях (`arm-core`, `tabular-parser`, `scan-gateway`); семантика, состав `check-full`, порядок перед PR и пропуск тестов без -`docs`/`fixtures` описаны в [`checks.md`](checks.md). Кратко: `make check` — быстрые проверки без Docker, `make check-full` — +`docs`/`fixtures` описаны в [`checks.md`](checks.md); эталон рецептов общих целей — [`Makefile.common`](Makefile.common). Кратко: `make check` — быстрые проверки без Docker, `make check-full` — `check` + интеграционные тесты против этого compose + образ (для `arm-core` ещё сверка с DBML, миграции и `web`); выполняется перед созданием PR. diff --git a/dev/checks.md b/dev/checks.md index 4d53699..69a4ae1 100644 --- a/dev/checks.md +++ b/dev/checks.md @@ -70,6 +70,31 @@ docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka run --rm `migration-cycle`, `check-infra`, `docker-build`, `web-check`; `scan-gateway` — `help`, `docker-build`, `emulator`; `tabular-parser` — `docker-build`. +## Эталонный фрагмент `dev/Makefile.common` + +Рецепты общих целей вынесены в [`Makefile.common`](Makefile.common) — это эталон (источник — `Makefile` `scan-gateway`), +с которым сервис сверяется `diff`ом или который подключает напрямую: + +```make +# Makefile сервиса: свои переменные и цели; include — после первой цели (иначе целью по умолчанию станет venv) +MYPY_PATHS := src scripts # arm-core; по умолчанию src +CHECK_FULL_STEPS := check dbml-check check-infra test-integration docker-build web-check # arm-core +include ../docs/dev/Makefile.common +``` + +Переменные (все `?=`): `VENV`, `BIN`, `MYPY_PATHS`, `COMPOSE_FILE`, `COMPOSE_PROJECT`, `IMAGE` (по умолчанию имя каталога +сервиса; у `scan-gateway` — `scan-gateway:dev`), `CHECK_FULL_STEPS` (по умолчанию `check test-integration docker-build`). +Определяет цели `venv`, `lint`, `test`, `test-integration`, `check`, `docker-build`, `check-full`, `up`. +Цели, специфичные для сервиса (`migrate`, `seed`, `dbml-check`, `check-infra`, `web-check`, `emulator`, `help`), +остаются в его `Makefile`. + +Проверка «`make check` вызывает одинаковые команды»: `make -n <цель>` в каждом сервисе и в Makefile, подключающем +`Makefile.common` с теми же переменными, даёт одинаковый вывод. Текущее состояние (Срез 0, сервисы не менялись): +`scan-gateway` совпадает с эталоном полностью; `arm-core` и `tabular-parser` — по `venv`, `lint`, `test`, +`test-integration`, `check`, `docker-build` и составу команд `check-full`, но не по `up` и по структуре `check-full` +(пререквизиты вместо вызовов `$(MAKE)`); см. «Известные расхождения реализаций». Сервисы переходят на `include` в +следующих срезах (решение 24 в `design.md`). + ## Внешние ресурсы: docs и fixtures опциональны Проверки сервисов обращаются к соседним репозиториям workspace и **не должны падать**, если их нет: тест diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index e6aec00..a4a40a0 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -155,3 +155,5 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 21 | 2026-10-04 | В корне репозитория `docs` добавлен `Makefile` с целями `venv`, `validate-contracts`, `validate-openspec`, `check` (сервисных целей нет); `jsonschema` ставится в `.venv` (в `.gitignore`) | дополнение контроллера к задаче 4.1: проверки контракта и спеки должны запускаться одной командой, как в сервисах; `check` = `scripts/validate_contracts.py` + `openspec validate slice-0-skeleton --strict` | удалить `Makefile` и строку `.venv/` в `.gitignore`; команды из `dev/checks.md` останутся запускаемыми вручную | | 22 | 2026-10-04 | `dev/checks.md` описывает общие цели по эталону `scan-gateway` (`up` с `--project-name arm-ka`, `--wait postgres nats minio` и `run --rm minio-init`; `check-full` как последовательность шагов), расхождения `arm-core`/`tabular-parser` и неполная поддержка `ARM_KA_DOCS`/`ARM_KA_FIXTURES` в `scan-gateway` вынесены в раздел «Известные расхождения реализаций», код сервисов не менялся | spec «Одинаковый набор целей» требует единой семантики, а фактические `Makefile` расходятся; правка сервисов вне задачи 4.1 и их PR | если эталон выбран неверно — переписать раздел `make up` в `checks.md`; расхождения останутся незакрытыми, пока сервисы не выровнены | | 23 | 2026-10-04 | В `dev/README.md` раздел «Интеграционные тесты сервисов против compose» заменён ссылкой на `checks.md`; убрано утверждение «Makefile есть только у `arm-core`» | утверждение устарело (Makefile есть у всех трёх сервисов), единый источник описания проверок — `checks.md` | вернуть старый текст раздела | +| 24 | 2026-10-04 | `dev/Makefile.common` — эталон общих целей по `scan-gateway`; различия сервисов вынесены в переменные (`MYPY_PATHS`, `IMAGE`, `COMPOSE_*`, `CHECK_FULL_STEPS`), `check-full` реализован через `$(foreach …)` с вызовом `$(MAKE)` на каждый шаг; в docs-репозитории `Makefile.common` не `include`-ится, сервисы его пока не подключают | задача 4.2 требует эталонный фрагмент и сверку `diff`ом; Makefile сервисов в Срезе 0 не правятся (код готов, PR открыты) — `include` в них можно ввести без смены поведения следующим срезом | сервисы продолжают держать копии рецептов; расхождения копий вылезают только при правке одной из них (сверка — `make -n`, см. `checks.md`) +| 25 | 2026-10-04 | Замечания T1.1–T1.4 ревью задачи 4.1 исправлены в `dev/checks.md` (fixtures и «ARM_KA_FIXTURES» — только `arm-core`, `mypy src scripts`, «Инфраструктура compose», «одинаковая семантика» заменена ссылкой на расхождения) | прямое поручение контроллера; формулировки приведены в соответствие с фактическими Makefile и тестами | неточная документация; правится одной строкой -- 2.47.2 From 22d7c10bb228fd82474c868cdc6606096bc96e66 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:33:15 +0400 Subject: [PATCH 34/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=204.2?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md index 50fcc4a..82fbe90 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md @@ -21,7 +21,7 @@ ### Task 2: [4.2] [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` се -- [ ] [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` сервисов через `include` или копируется); проверка: `make check` в трёх репозиториях вызывает одинаковые команды (`diff` общих целей) +- [x] [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` сервисов через `include` или копируется); проверка: `make check` в трёх репозиториях вызывает одинаковые команды (`diff` общих целей) ### Task 3: [5.3] [docs] `docker-compose.yml` diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 212fc01..1f0e33e 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -46,7 +46,7 @@ - [x] 4.1 [docs] `docs/dev/checks.md`: общие цели `Makefile` (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`), что делает каждая, порядок перед PR, как пропускаются тесты без фикстур/docs; проверка: цели `arm-core` соответствуют описанию -- [ ] 4.2 [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` сервисов через `include` или +- [x] 4.2 [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` сервисов через `include` или копируется); проверка: `make check` в трёх репозиториях вызывает одинаковые команды (`diff` общих целей) ## 5. Сервисы распознавания (детали — в их change'ах) и профиль `services` -- 2.47.2 From 4171ecc410be57da190115bb68edae46d0411168 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:42:00 +0400 Subject: [PATCH 35/68] =?UTF-8?q?feat(dev):=20=D0=BF=D1=80=D0=BE=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=20services=20=D0=B2=20docker-compose=20?= =?UTF-8?q?=E2=80=94=20arm-core,=20tabular-parser,=20scan-gateway,=20scan-?= =?UTF-8?q?emulator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сервисы собираются из соседних репозиториев (../../), образы :dev; порты хоста 8000/8001/8002 на ${BIND_ADDR:-127.0.0.1}, внутри контейнера все слушают 8000. Одноразовый arm-core-migrate выполняет alembic upgrade head и seed --dev до старта arm-core. Фрагмент scan-gateway/scan-emulator взят из open-questions-proposal.md scan-gateway; образ каждого репозитория собирается одним сервисом (arm-core, scan-gateway), второй использует тег с pull_policy: never. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- dev/docker-compose.yml | 154 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 7949922..a3d72c1 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -4,11 +4,35 @@ # одноразовый minio-init завершается (хотя и с кодом 0) — для скриптов указывайте сервисы явно. # Все учётные данные, порты хоста и имя бакета берутся из .env (значения по умолчанию — dev). # Порты публикуются только на 127.0.0.1; адрес привязки меняется через BIND_ADDR (см. .env.example). -# Профиль `services` (arm-core, tabular-parser, scan-gateway, scan-emulator) добавляется в фазе 2. +# Профиль `services` — сервисы системы, собранные из соседних репозиториев workspace (../../, Dockerfile в корне): +# docker compose --profile services up -d --build +# docker compose --profile services up -d --wait arm-core tabular-parser scan-gateway scan-emulator +# Одноразовые minio-init и arm-core-migrate (миграции + seed) в список --wait не входят: они завершаются с кодом 0. +# Внутри контейнера каждый сервис слушает 8000; на хост публикуются 8000 (arm-core), 8001 (tabular-parser), 8002 (scan-gateway). # Фиксированное имя проекта: одинаковые тома и сети из любого worktree (.wt/<срез>/docs/dev). name: arm-ka +# Общие переменные сервисов профиля `services` (формат шаблона команды; адреса — внутри сети compose, не localhost). +x-service-env: &service-env + DEBUG: "true" + OPENBAO__MINIO__MINIO_URL: minio:9000 + OPENBAO__MINIO__MINIO_ADMIN_LOGIN: ${MINIO_ROOT_USER:-minioadmin} + OPENBAO__MINIO__MINIO_ADMIN_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} + OPENBAO__MINIO__MINIO_BUCKET: ${MINIO_BUCKET:-arm-ka} + OPENBAO__MINIO__MINIO_SECURE: "false" + OPENBAO__NATS__NATS_URL: nats://nats:4222 + NATS_ENABLED: "true" + +# arm-core и его одноразовые миграции: те же переменные плюс БД и dev-эмитент токенов. +x-arm-core-env: &arm-core-env + <<: *service-env + OPENBAO__DATABASE__DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-armka}:${POSTGRES_PASSWORD:-armka}@postgres:5432/${POSTGRES_DB:-armka} + # Dev-эмитент JWT (POST /api/v1/dev/token) — только при DEBUG=true; секрет публичный, только для dev. + # Порт хоста по умолчанию на 127.0.0.1 (BIND_ADDR): при BIND_ADDR=0.0.0.0 токен может получить любой в сети. + AUTH_DEV_ISSUER: "true" + AUTH_DEV_SECRET: dev-only-secret-do-not-use-in-production-0123456789 + services: postgres: image: postgres:16.10-alpine @@ -79,6 +103,134 @@ services: mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" && mc mb --ignore-existing "local/$$MINIO_BUCKET" + # --- Профиль `services` ------------------------------------------------------------------------------------------- + + # Одноразово: миграции arm-core (alembic upgrade head) и справочники с dev-пользователями (seed --dev). + # Оба шага идемпотентны; при каждом `up` профиля контейнер снова выполняется и завершается с кодом 0. + # Образ собирает сервис arm-core (build только у него): две сборки одного тега дают разные ID образа, и каждый + # следующий `up` пересоздавал бы arm-core-migrate и arm-core. pull_policy: never — образ локальный, не из реестра. + arm-core-migrate: + profiles: ["services"] + image: arm-core:dev + pull_policy: never + restart: "no" + depends_on: + postgres: + condition: service_healthy + environment: *arm-core-env + command: ["sh", "-c", "alembic upgrade head && python -m scripts.seed --dev"] + + arm-core: + profiles: ["services"] + build: + context: ../../arm-core + image: arm-core:dev + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + nats: + condition: service_healthy + minio: + condition: service_healthy + minio-init: + condition: service_completed_successfully + arm-core-migrate: + condition: service_completed_successfully + environment: *arm-core-env + ports: + - "${BIND_ADDR:-127.0.0.1}:${ARM_CORE_PORT:-8000}:8000" + healthcheck: + # curl в python:3.11-slim нет; /healthcheck всегда отвечает 200, поэтому читаем поле nats + test: + - CMD + - python + - -c + - "import json,sys,urllib.request as u; d=json.load(u.urlopen('http://127.0.0.1:8000/healthcheck', timeout=3)); sys.exit(0 if d['nats']=='ok' else 1)" + interval: 5s + timeout: 5s + retries: 12 + start_period: 10s + + tabular-parser: + profiles: ["services"] + build: + context: ../../tabular-parser + image: tabular-parser:dev + restart: unless-stopped + depends_on: + nats: + condition: service_healthy + minio: + condition: service_healthy + minio-init: + condition: service_completed_successfully + environment: *service-env + ports: + - "${BIND_ADDR:-127.0.0.1}:${TABULAR_PARSER_PORT:-8001}:8000" + healthcheck: + test: + - CMD + - python + - -c + - "import json,sys,urllib.request as u; d=json.load(u.urlopen('http://127.0.0.1:8000/healthcheck', timeout=3)); sys.exit(0 if d['nats']=='ok' else 1)" + interval: 5s + timeout: 5s + retries: 12 + start_period: 10s + + # Эмулятор ИС Сканирование: тот же образ, что у шлюза (собирает сервис scan-gateway, см. arm-core-migrate); общий + # том hotfolder с шлюзом. Переменные MinIO/NATS не нужны. + # Фикстуры ответов ИС — ../../fixtures/ocr (только чтение): пара <имя>.json для входного файла UUID_<имя>.ext. + scan-emulator: + profiles: ["services"] + image: scan-gateway:dev + pull_policy: never + command: ["python", "-m", "src.scan_emulator"] + restart: unless-stopped + environment: + SCAN_HOTFOLDER_PATH: /data/hotfolder + SCAN_FIXTURES_DIR: /fixtures/ocr + SCAN_EMULATOR_DELAY: "2" + volumes: + - hotfolder:/data/hotfolder + - ../../fixtures/ocr:/fixtures/ocr:ro + + scan-gateway: + profiles: ["services"] + build: + context: ../../scan-gateway + image: scan-gateway:dev + restart: unless-stopped + depends_on: + nats: + condition: service_healthy + minio: + condition: service_healthy + minio-init: + condition: service_completed_successfully + environment: + <<: *service-env + SCAN_HOTFOLDER_MODE: local + SCAN_HOTFOLDER_PATH: /data/hotfolder + volumes: + - hotfolder:/data/hotfolder + ports: + - "${BIND_ADDR:-127.0.0.1}:${SCAN_GATEWAY_PORT:-8002}:8000" + healthcheck: + # /healthcheck всегда отвечает 200, поэтому читаем поля nats и hotfolder + test: + - CMD + - python + - -c + - "import json,sys,urllib.request as u; d=json.load(u.urlopen('http://127.0.0.1:8000/healthcheck', timeout=3)); sys.exit(0 if d['nats']=='ok' and d['hotfolder']=='ok' else 1)" + interval: 5s + timeout: 5s + retries: 12 + start_period: 10s + volumes: postgres-data: minio-data: + # Общая горячая папка scan-gateway и scan-emulator (Input/, Output/) + hotfolder: -- 2.47.2 From f1b648e7b782c4da82ab7ed4aa4420b8de26b30b Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:43:41 +0400 Subject: [PATCH 36/68] =?UTF-8?q?docs(dev):=20README=20=D0=B8=20.env.examp?= =?UTF-8?q?le=20=E2=80=94=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8C=20s?= =?UTF-8?q?ervices,=20=D0=BF=D0=BE=D1=80=D1=82=D1=8B=20=D1=85=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=20=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9?= =?UTF-8?q?=D0=BD=D0=B5=D1=80=D0=B0,=20=D0=BA=D0=BE=D0=B4=D1=8B=20--wait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Порты хоста ARM_CORE_PORT/TABULAR_PARSER_PORT/SCAN_GATEWAY_PORT (8000/8001/8002, в контейнере 8000); раздел «Профиль services»: команды, таблица портов, порядок старта, фактический вывод healthcheck, коды возврата up -d --wait (в т.ч. scan-gateway scan-emulator — 0), пересборка; сброс только с --profile services; предупреждение об интеграционных тестах при поднятом профиле. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- dev/.env.example | 7 +++ dev/README.md | 137 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 130 insertions(+), 14 deletions(-) diff --git a/dev/.env.example b/dev/.env.example index 446ee8a..271708d 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -22,6 +22,13 @@ MINIO_API_PORT=9000 MINIO_CONSOLE_PORT=9001 MINIO_BUCKET=arm-ka +# Порты хоста сервисов профиля `services` (docker compose --profile services ...). Внутри контейнера каждый сервис +# слушает 8000; эти значения меняют только порт на хосте. Настройки сервисов в контейнерах compose задаёт сам +# (docker-compose.yml, x-service-env), блок ниже на них не влияет. +ARM_CORE_PORT=8000 +TABULAR_PARSER_PORT=8001 +SCAN_GATEWAY_PORT=8002 + # --- Настройки сервисов при запуске из venv против compose по localhost (формат шаблона команды) --- # Значения согласованы с блоком выше: при смене порта/пароля/бакета меняйте обе части. # Если порт хоста изменён, поправьте и порты ниже. diff --git a/dev/README.md b/dev/README.md index e6553eb..8d2eccb 100644 --- a/dev/README.md +++ b/dev/README.md @@ -1,17 +1,20 @@ # Окружение разработки АРМ КА (`docs/dev/`) Локальная инфраструктура workspace одной командой: PostgreSQL 16, NATS 2.10 без JetStream и MinIO с бакетом `arm-ka`. - -**Статус (фаза 1):** в compose сейчас только инфраструктура (`postgres`, `nats`, `minio`, `minio-init`). Профиль -`services` (контейнеры `arm-core`, `tabular-parser`, `scan-gateway`, `scan-emulator`) **планируется** — добавляется позже -(задача 5.3), здесь не описан. До этого сервисы запускаются из venv против этой инфраструктуры (см. ниже). +Профиль `services` добавляет к ней сервисы системы, собранные из соседних репозиториев workspace: `arm-core`, +`tabular-parser`, `scan-gateway` и эмулятор ИС Сканирование `scan-emulator` (см. «Профиль `services`»). Без профиля +поднимается только инфраструктура, а сервисы можно запускать из venv (см. «Запуск сервисов из venv»). ## Предварительные требования - Docker и compose v2 (проверено на docker 28 / compose 2.34). - `uv` и Python 3.11 — для запуска сервисов из venv (версия Python — как на платформе). -- Свободные порты хоста (по умолчанию доступны только с `127.0.0.1`, см. `BIND_ADDR`): 5432 (PostgreSQL), 4222 и 8222 (NATS и его мониторинг), 9000 и 9001 (MinIO API и консоль). +- Свободные порты хоста (по умолчанию доступны только с `127.0.0.1`, см. `BIND_ADDR`): 5432 (PostgreSQL), 4222 и 8222 (NATS и его мониторинг), 9000 и 9001 (MinIO API и консоль); + для профиля `services` ещё 8000 (`arm-core`), 8001 (`tabular-parser`), 8002 (`scan-gateway`). Занятые порты меняются в `.env` (см. «Типичные проблемы»). +- Для профиля `services`: соседние каталоги `../../arm-core`, `../../tabular-parser`, `../../scan-gateway` и + `../../fixtures` относительно `docs/dev/` (в workspace и в зеркальном worktree `.wt/<срез>/` это одни и те же соседи + `docs`). Сборка образов ставит зависимости из PyPI (строки `raisa_*` отфильтрованы, Nexus не нужен). ## Запуск инфраструктуры @@ -46,6 +49,91 @@ docker compose run --rm --entrypoint sh minio-init -c 'mc alias set local http:/ Консоль MinIO — (по умолчанию `minioadmin` / `minioadmin`); S3 API — `localhost:9000`. Схема БД (`dkb`) создаётся миграциями `arm-core`, не compose. +## Профиль `services` + +Сервисы системы в контейнерах против той же инфраструктуры. Образы собираются из `Dockerfile` в корне соседних +репозиториев (`../../arm-core`, `../../tabular-parser`, `../../scan-gateway`) и получают теги `arm-core:dev`, +`tabular-parser:dev`, `scan-gateway:dev`. Настройки сервисов задаёт сам compose (переменные `OPENBAO__*` шаблона с +адресами внутри сети compose — `postgres:5432`, `nats:4222`, `minio:9000`, `DEBUG=true`); второй блок `.env` и +`api/.env` сервисов на контейнеры не влияют. + +Команды — из каталога `docs/dev/`: + +```bash +cp .env.example .env # если ещё не сделано +docker compose --profile services up -d --build # сборка образов и запуск; первая сборка — около минуты +docker compose --profile services up -d --wait arm-core tabular-parser scan-gateway scan-emulator # ждать healthy +``` + +| Сервис | Порт хоста (`.env`) | Порт в контейнере | Что делает | +|---|---|---|---| +| `arm-core` | 8000 (`ARM_CORE_PORT`) | 8000 | ядро: API, outbox/inbox `recognition_job`, подписка `recognition.result` | +| `tabular-parser` | 8001 (`TABULAR_PARSER_PORT`) | 8000 | подписка `recognition.tabular.request` (queue group `tabular-parser`) | +| `scan-gateway` | 8002 (`SCAN_GATEWAY_PORT`) | 8000 | подписка `recognition.image.request` (queue group `scan-gateway`), горячая папка — том `hotfolder` | +| `scan-emulator` | — | — | эмулятор ИС Сканирование: следит за `Input/` тома `hotfolder`, ответы — из `../../fixtures/ocr` (только чтение) | +| `arm-core-migrate` | — | — | одноразовый: `alembic upgrade head` и `python -m scripts.seed --dev`, затем `arm-core` стартует | + +Все три сервиса внутри контейнера слушают **8000** (значение по умолчанию `SERVER__PORT`); различаются только порты +хоста. Сервис, запущенный из venv, по умолчанию тоже слушает 8000 на хосте — он конфликтует с контейнером `arm-core` +(остановите контейнер или задайте `SERVER__PORT` в `api/.env`). + +Порядок старта задан `depends_on`: `arm-core-migrate` ждёт healthy `postgres`; `arm-core` — `postgres`, `nats`, `minio`, +успешного завершения `minio-init` и `arm-core-migrate`; `tabular-parser` и `scan-gateway` — `nats`, `minio` и +`minio-init`. Одноразовые `minio-init` и `arm-core-migrate` выполняются при каждом `up` профиля (миграции и seed +идемпотентны: повторный seed пишет `0 / 0 / N` — без изменений) и остаются в `ps -a` как `Exited (0)`. + +Проверка (фактический вывод на чистых томах, 2026-10-04, сокращён): + +```bash +docker compose --profile services ps -a +# arm-core Up (healthy) 127.0.0.1:8000->8000/tcp +# arm-core-migrate Exited (0) +# minio-init Exited (0) +# 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"} +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 +``` + +`/healthcheck` всех трёх сервисов **всегда отвечает 200**, поэтому healthcheck контейнеров читает поля ответа: +`nats = ok` (и `hotfolder = ok` у `scan-gateway`). Статус `healthy` означает, что сервис подключён к NATS. + +Dev-токен `arm-core` (в compose включён dev-эмитент, `AUTH_DEV_ISSUER=true`, `DEBUG=true`; пользователи — из +`seed --dev`, см. `../../arm-core/README.md`): + +```bash +curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' -d '{"email":"ka_rf@armka.local"}' +curl -s localhost:8000/api/v1/me -H "Authorization: Bearer " # email, роли KA_RF, права +``` + +Образ `arm-core` содержит только бэкенд: фронт (`web/`) в `Dockerfile` не собирается, `GET /` отвечает 404. UI — +`npm run dev` из `../../arm-core/web` (см. README `arm-core`). + +**Ожидание готовности и коды возврата `--wait`** (compose 2.34, проверено 2026-10-04): + +| Команда | Код | Комментарий | +|---|---|---| +| `docker compose --profile services up -d --wait arm-core tabular-parser scan-gateway scan-emulator` | 0 | рекомендуемая форма | +| `docker compose --profile services up -d --wait scan-gateway scan-emulator` | 0 | и на чистых томах, и на работающем окружении: `minio-init` поднимается как зависимость `scan-gateway` (`service_completed_successfully`) и его выход с кодом 0 сбоем не считается | +| `docker compose --profile services up -d --wait` (без списка) | 0 | все одноразовые контейнеры профиля — зависимости с `service_completed_successfully` | +| `docker compose up -d --wait` (без профиля и списка) | 1 | `container arm-ka-minio-init-1 exited (0)`: без профиля у `minio-init` нет зависимых сервисов | + +Для скриптов указывайте долгоживущие сервисы явно — форма не зависит от состава профиля. + +**Пересборка после изменения кода сервиса:** `docker compose --profile services up -d --build` (или +`up -d --build <сервис>`). Образ `arm-core:dev` собирает только сервис `arm-core`, образ `scan-gateway:dev` — только +`scan-gateway`; `arm-core-migrate` и `scan-emulator` используют готовый тег (`pull_policy: never`): две сборки одного +тега давали разные ID образа, и каждый следующий `up` пересоздавал контейнеры. Поэтому `arm-core-migrate` или +`scan-emulator` в одиночку (`up -d scan-emulator`) на машине без образа не стартуют — сначала соберите его +(`docker compose --profile services build scan-gateway`) или поднимайте профиль целиком. + +**Остановка и сброс профиля** — только с `--profile services`: без него compose не видит контейнеры профиля +(см. «Сброс окружения»). + ## Переменные `.env` Файл читают `docker compose` (из этого каталога) и, вторым блоком, сервисы при запуске из venv. Значения по умолчанию — @@ -67,6 +155,9 @@ docker compose run --rm --entrypoint sh minio-init -c 'mc alias set local http:/ | `MINIO_API_PORT` | `9000` | порт S3 API на хосте | | `MINIO_CONSOLE_PORT` | `9001` | порт консоли MinIO на хосте | | `MINIO_BUCKET` | `arm-ka` | единственный бакет, создаётся `minio-init` | +| `ARM_CORE_PORT` | `8000` | порт хоста `arm-core` (профиль `services`; в контейнере 8000) | +| `TABULAR_PARSER_PORT` | `8001` | порт хоста `tabular-parser` (профиль `services`; в контейнере 8000) | +| `SCAN_GATEWAY_PORT` | `8002` | порт хоста `scan-gateway` (профиль `services`; в контейнере 8000) | Блок сервисов (запуск из venv против compose по `localhost`, формат шаблона команды): @@ -92,10 +183,15 @@ docker compose run --rm --entrypoint sh minio-init -c 'mc alias set local http:/ ## Сброс окружения ```bash -docker compose down # останавливает контейнеры; тома postgres-data и minio-data сохраняются -docker compose down -v # полный сброс: удаляет тома postgres-data и minio-data (БД и бакет — с нуля) +docker compose --profile services down # останавливает все контейнеры проекта; тома сохраняются +docker compose --profile services down -v # полный сброс: тома postgres-data, minio-data и hotfolder (БД, бакет, горячая папка — с нуля) ``` +Без `--profile services` команда `down` (с `-v` или без) **не трогает** контейнеры профиля: проверено — после +`docker compose down -v` контейнеры `arm-core`/`tabular-parser`/`scan-gateway`/`scan-emulator` продолжают работать, +том `hotfolder` и сеть `arm-ka_default` остаются (`Network arm-ka_default Resource is still in use`). Если профиль не +поднимался, `docker compose down [-v]` достаточно. + Имя проекта compose фиксировано (`name: arm-ka`), поэтому тома общие для любого worktree (`.wt/<срез>/docs/dev`): сброс из одного worktree затрагивает все. @@ -120,10 +216,12 @@ cd api && .venv/bin/python run_server.py # сервис подключаетс curl -s localhost:<порт сервиса>/healthcheck ``` -Порт сервиса задаётся его настройками; в профиле `services` по spec это 8000 (`arm-core`), 8001 (`tabular-parser`), -8002 (`scan-gateway`). Для `arm-core` в ответе `/healthcheck` ожидаются `version` и `nats = ok` — при `NATS_ENABLED=true` -(задан в `.env.example`; без него `nats = off`). Для `tabular-parser` и `scan-gateway` ожидаемые поля и переменные -зависят от их настроек — будет уточнено в задачах 5.x. +Из venv каждый сервис слушает порт из своих настроек (`SERVER__PORT`, по умолчанию **8000** у всех трёх) — порты +хоста 8001/8002 относятся только к контейнерам профиля `services` (см. таблицу портов выше). Два сервиса из venv +одновременно или сервис из 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 уточняются в задаче 5.4. ## Интеграционные тесты сервисов против compose (черновик) @@ -137,8 +235,13 @@ curl -s localhost:<порт сервиса>/healthcheck Интеграционные тесты **требуют поднятой инфраструктуры**: сначала `docker compose up -d && docker compose up -d --wait postgres nats minio`, затем `make test-integration` (или `make check-full`) в репозитории сервиса. Для `scan-gateway` -эмулятор ИС Сканирование поднимается в процессе интеграционного теста (отдельный контейнер появится вместе с профилем -`services`). Общее описание проверок — [`checks.md`](checks.md). +эмулятор ИС Сканирование поднимается в процессе интеграционного теста; контейнер `scan-emulator` профиля `services` +тестами не используется. Общее описание проверок — [`checks.md`](checks.md). + +> **Не запускайте интеграционные тесты при поднятом профиле `services`** (не проверялось, вывод из устройства обмена): +> контейнеры подписаны на те же subject'ы общего NATS в тех же queue group (`arm-core` на `recognition.result`, +> `tabular-parser` и `scan-gateway` на свои `*.request`), поэтому NATS может отдать сообщение теста контейнеру, а не +> экземпляру из теста. Перед тестами: `docker compose --profile services stop arm-core tabular-parser scan-gateway scan-emulator`. ## Проверка контракта @@ -162,7 +265,13 @@ postgres nats minio`, затем `make test-integration` (или `make check-ful ## Типичные проблемы - **Порт занят** (`address already in use`): измените `*_PORT` в `.env` и одновременно соответствующий порт в - `OPENBAO__*` (`DATABASE_URL`, `MINIO_URL`, `NATS_URL`); перезапустите `docker compose up -d`. + `OPENBAO__*` (`DATABASE_URL`, `MINIO_URL`, `NATS_URL`); перезапустите `docker compose up -d`. Для портов сервисов + (`ARM_CORE_PORT`, `TABULAR_PARSER_PORT`, `SCAN_GATEWAY_PORT`) достаточно `.env`. +- **Сервис профиля не становится healthy:** `docker compose --profile services logs <сервис>`. `nats = down` — + NATS недоступен; `hotfolder = down` у `scan-gateway` — каталог `SCAN_HOTFOLDER_PATH` (том `hotfolder`) недоступен. `arm-core` не стартует, пока + `arm-core-migrate` не завершился с кодом 0: `docker compose --profile services logs arm-core-migrate`. +- **`No such image: arm-core:dev` / `scan-gateway:dev`** при запуске `arm-core-migrate` или `scan-emulator` по + отдельности: образ ещё не собран — `docker compose --profile services build` (см. «Профиль `services`»). - **`minio-init` завис или завершился с ошибкой:** `docker compose logs minio-init`; чаще всего `minio` не стал healthy — `docker compose logs minio`. Повторный `docker compose up -d` идемпотентен. - **Остаточные тома от прошлых запусков** (старый пароль БД, бакет не тот): `docker compose down -v` и запуск заново. -- 2.47.2 From f6e14c2195f5098bd2779bdbea3725ea23e5fdce Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 05:44:05 +0400 Subject: [PATCH 37/68] =?UTF-8?q?docs(openspec):=20design.md=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=2026=E2=80=9329?= =?UTF-8?q?=20(=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8C=20services=20?= =?UTF-8?q?=D0=B2=20compose)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- openspec/changes/slice-0-skeleton/design.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index a4a40a0..e821829 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -157,3 +157,7 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 23 | 2026-10-04 | В `dev/README.md` раздел «Интеграционные тесты сервисов против compose» заменён ссылкой на `checks.md`; убрано утверждение «Makefile есть только у `arm-core`» | утверждение устарело (Makefile есть у всех трёх сервисов), единый источник описания проверок — `checks.md` | вернуть старый текст раздела | | 24 | 2026-10-04 | `dev/Makefile.common` — эталон общих целей по `scan-gateway`; различия сервисов вынесены в переменные (`MYPY_PATHS`, `IMAGE`, `COMPOSE_*`, `CHECK_FULL_STEPS`), `check-full` реализован через `$(foreach …)` с вызовом `$(MAKE)` на каждый шаг; в docs-репозитории `Makefile.common` не `include`-ится, сервисы его пока не подключают | задача 4.2 требует эталонный фрагмент и сверку `diff`ом; Makefile сервисов в Срезе 0 не правятся (код готов, PR открыты) — `include` в них можно ввести без смены поведения следующим срезом | сервисы продолжают держать копии рецептов; расхождения копий вылезают только при правке одной из них (сверка — `make -n`, см. `checks.md`) | 25 | 2026-10-04 | Замечания T1.1–T1.4 ревью задачи 4.1 исправлены в `dev/checks.md` (fixtures и «ARM_KA_FIXTURES» — только `arm-core`, `mypy src scripts`, «Инфраструктура compose», «одинаковая семантика» заменена ссылкой на расхождения) | прямое поручение контроллера; формулировки приведены в соответствие с фактическими Makefile и тестами | неточная документация; правится одной строкой +| 26 | 2026-10-04 | Профиль `services` в `dev/docker-compose.yml`: переменные сервисов — общие якоря YAML (`x-service-env`, `x-arm-core-env`) с адресами внутри сети compose и `DEBUG=true`; образы собираются локально с тегами `arm-core:dev`, `tabular-parser:dev`, `scan-gateway:dev`; healthcheck контейнеров — `python -c` с чтением поля `nats` (у `scan-gateway` ещё `hotfolder`) из `/healthcheck`; порты хоста `${ARM_CORE_PORT:-8000}`/`${TABULAR_PARSER_PORT:-8001}`/`${SCAN_GATEWAY_PORT:-8002}` на `${BIND_ADDR:-127.0.0.1}`, в контейнере 8000 | spec dev-environment («`OPENBAO__*` шаблона, `DEBUG=true`»), фрагмент `scan-gateway` (`open-questions-proposal.md`) как основа; `curl` в `python:3.11-slim` нет, а `/healthcheck` всегда 200; якоря убирают тройное дублирование учётных данных MinIO/NATS | правка одного блока compose; при смене формата `/healthcheck` сервиса — правка трёх строк `test` | +| 27 | 2026-10-04 | Одноразовый сервис `arm-core-migrate` (`alembic upgrade head && python -m scripts.seed --dev`, `depends_on` postgres `service_healthy`); `arm-core` ждёт его `service_completed_successfully`; выполняется при каждом `up` профиля | дополнение контроллера к задаче 5.3; без схемы `dkb` и справочников ядро не работает, оба шага идемпотентны (повторный seed — `0 / 0 / N`); `--dev` — нужны dev-пользователи для dev-токена | лишний запуск миграций при каждом `up` (~2 с); убрать `--dev` — одна строка | +| 28 | 2026-10-04 | Образ каждого репозитория собирает один сервис: `build` только у `arm-core` и `scan-gateway`; `arm-core-migrate` и `scan-emulator` используют тег `arm-core:dev`/`scan-gateway:dev` с `pull_policy: never` (отступление от фрагмента `scan-gateway`, где `build` у обоих) | проверено: при `build` у двух сервисов с одним тегом compose 2.34 собирает образ дважды с разными ID, метка `com.docker.compose.image` контейнера расходится с тегом, и каждый следующий `up` пересоздаёт `arm-core-migrate` и `arm-core` (флип-флоп); с одной сборкой три `up` подряд — 0 пересозданий | `up -d scan-emulator` или `arm-core-migrate` в одиночку на машине без образа падает `No such image` — сначала `docker compose --profile services build`; вернуть `build` — две строки | +| 29 | 2026-10-04 | В compose у `arm-core` включён dev-эмитент токенов (`AUTH_DEV_ISSUER=true`, публичный `AUTH_DEV_SECRET` из `arm-core/api/.env.example`) | ручные проверки `/api/v1/me` и сквозной сценарий (задача 5.4) без РАИСА; маршрут существует только при `DEBUG=true`; порт хоста по умолчанию на 127.0.0.1 | при `BIND_ADDR=0.0.0.0` токен любого dev-пользователя может получить любой в сети — только dev; выключить — одна строка | -- 2.47.2 From e705080e5533fcce4a7bb5f8974515a8b9a9aa59 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:01:46 +0400 Subject: [PATCH 38/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=205.3?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md index 82fbe90..abe0854 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md @@ -25,7 +25,7 @@ ### Task 3: [5.3] [docs] `docker-compose.yml` -- [ ] [docs] `docker-compose.yml`: профиль `services` — `arm-core` (8000), `tabular-parser` (8001), `scan-gateway` (8002), `scan-emulator` (том `hotfolder`, `../../fixtures/ocr:ro`); проверка: `--profile services up --build`, все `/healthcheck` 200 с `nats = ok` +- [x] [docs] `docker-compose.yml`: профиль `services` — `arm-core` (8000), `tabular-parser` (8001), `scan-gateway` (8002), `scan-emulator` (том `hotfolder`, `../../fixtures/ocr:ro`); проверка: `--profile services up --build`, все `/healthcheck` 200 с `nats = ok` ### Task 4: [5.4] [docs] Сквозная проверка руками по README diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 1f0e33e..e0f3b48 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -55,7 +55,7 @@ `processing` и заглушкой `NOT_IMPLEMENTED`, Dockerfile 3.11, `make check-full` - [ ] 5.2 [scan-gateway] Вырезать фронт/БД, вендоренный контракт, горячие папки (local/smb), эмулятор, подписчик queue group `scan-gateway` с ack и таймаутом, Dockerfile 3.11, `make check-full` -- [ ] 5.3 [docs] `docker-compose.yml`: профиль `services` — `arm-core` (8000), `tabular-parser` (8001), `scan-gateway` +- [x] 5.3 [docs] `docker-compose.yml`: профиль `services` — `arm-core` (8000), `tabular-parser` (8001), `scan-gateway` (8002), `scan-emulator` (том `hotfolder`, `../../fixtures/ocr:ro`); проверка: `--profile services up --build`, все `/healthcheck` 200 с `nats = ok` - [ ] 5.4 [docs] Сквозная проверка руками по README: вставить `recognition_job` в БД через `psql` (или тестовый скрипт -- 2.47.2 From e98a9239bf4b0968d38cca6528b46d6a2eeaaf4d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:16:59 +0400 Subject: [PATCH 39/68] =?UTF-8?q?docs(dev):=20README=20=E2=80=94=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BC=D0=B5=D1=87=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D1=8C=D1=8E=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B8=205.3?= =?UTF-8?q?=20(T3.3=E2=80=93T3.6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - T3.3: риск общих queue group подтверждён чтением тестов tabular-parser и scan-gateway (test_roundtrip, боевые *.request); тесты arm-core изолированы (временная БД, уникальный subject результатов); команда проверки подписчиков. - T3.4: «Остаточные тома» — down -v только с --profile services. - T3.5: GET / отвечает 307 на // и затем 404. - T3.6: up -d --build <сервис> пересоздаёт сервис и зависимости: arm-core-migrate пересоздаётся вместе с arm-core, scan-emulator со scan-gateway — нет (проверено). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- dev/README.md | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/dev/README.md b/dev/README.md index 8d2eccb..168ecf7 100644 --- a/dev/README.md +++ b/dev/README.md @@ -110,8 +110,9 @@ curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/js curl -s localhost:8000/api/v1/me -H "Authorization: Bearer " # email, роли KA_RF, права ``` -Образ `arm-core` содержит только бэкенд: фронт (`web/`) в `Dockerfile` не собирается, `GET /` отвечает 404. UI — -`npm run dev` из `../../arm-core/web` (см. README `arm-core`). +Образ `arm-core` содержит только бэкенд: фронт (`web/`) в `Dockerfile` не собирается. `GET /` отвечает **307** с +редиректом на `//`, а тот — 404 (`curl -L` показывает 404 на `http://127.0.0.1:8000//`; редирект на `//` — особенность +`arm-core`, вынесена в его follow-up). UI — `npm run dev` из `../../arm-core/web` (см. README `arm-core`). **Ожидание готовности и коды возврата `--wait`** (compose 2.34, проверено 2026-10-04): @@ -124,12 +125,22 @@ curl -s localhost:8000/api/v1/me -H "Authorization: Bearer " # email, Для скриптов указывайте долгоживущие сервисы явно — форма не зависит от состава профиля. -**Пересборка после изменения кода сервиса:** `docker compose --profile services up -d --build` (или -`up -d --build <сервис>`). Образ `arm-core:dev` собирает только сервис `arm-core`, образ `scan-gateway:dev` — только -`scan-gateway`; `arm-core-migrate` и `scan-emulator` используют готовый тег (`pull_policy: never`): две сборки одного -тега давали разные ID образа, и каждый следующий `up` пересоздавал контейнеры. Поэтому `arm-core-migrate` или -`scan-emulator` в одиночку (`up -d scan-emulator`) на машине без образа не стартуют — сначала соберите его -(`docker compose --profile services build scan-gateway`) или поднимайте профиль целиком. +**Пересборка после изменения кода сервиса:** `docker compose --profile services up -d --build` — пересобирает образы +и пересоздаёт все контейнеры, чей образ сменился. Образ `arm-core:dev` собирает только сервис `arm-core`, образ +`scan-gateway:dev` — только `scan-gateway`; `arm-core-migrate` и `scan-emulator` используют готовый тег +(`pull_policy: never`): две сборки одного тега давали разные ID образа, и каждый следующий `up` пересоздавал контейнеры. +Поэтому `arm-core-migrate` или `scan-emulator` в одиночку (`up -d scan-emulator`) на машине без образа не стартуют — +сначала соберите его (`docker compose --profile services build scan-gateway`) или поднимайте профиль целиком. + +Форма `up -d --build <сервис>` пересоздаёт только сервис и его зависимости (проверено 2026-10-04 на образе, +пересобранном с `--no-cache`): + +| Команда | Что пересоздано | Что осталось на старом образе | +|---|---|---| +| `up -d --build arm-core` | `arm-core` и `arm-core-migrate` (он — зависимость `arm-core`, миграции нового кода выполняются) | — | +| `up -d --build scan-gateway` | только `scan-gateway` | **`scan-emulator`** (не зависимость шлюза) — до следующего `docker compose --profile services up -d`, который его пересоздаёт | + +После правок кода `scan-gateway`, затрагивающих эмулятор, пересобирайте профиль целиком. **Остановка и сброс профиля** — только с `--profile services`: без него compose не видит контейнеры профиля (см. «Сброс окружения»). @@ -238,10 +249,18 @@ postgres nats minio`, затем `make test-integration` (или `make check-ful эмулятор ИС Сканирование поднимается в процессе интеграционного теста; контейнер `scan-emulator` профиля `services` тестами не используется. Общее описание проверок — [`checks.md`](checks.md). -> **Не запускайте интеграционные тесты при поднятом профиле `services`** (не проверялось, вывод из устройства обмена): -> контейнеры подписаны на те же subject'ы общего NATS в тех же queue group (`arm-core` на `recognition.result`, -> `tabular-parser` и `scan-gateway` на свои `*.request`), поэтому NATS может отдать сообщение теста контейнеру, а не -> экземпляру из теста. Перед тестами: `docker compose --profile services stop arm-core tabular-parser scan-gateway scan-emulator`. +> **Не запускайте интеграционные тесты `tabular-parser` и `scan-gateway` при поднятом профиле `services`.** Их +> сквозные тесты (`api/tests/integration/test_roundtrip.py`, у `scan-gateway` ещё `test_nats_minio_integration.py`) +> публикуют запросы в боевые `recognition.tabular.request` / `recognition.image.request`, а экземпляр сервиса из теста +> подписан в ту же 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[^"]*"'` — строк +> `recognition.tabular.request` / `recognition.image.request` быть не должно (`recognition.result` — подписка контейнера +> `arm-core`, тестам `arm-core` не мешает). ## Проверка контракта @@ -274,5 +293,6 @@ postgres nats minio`, затем `make test-integration` (или `make check-ful отдельности: образ ещё не собран — `docker compose --profile services build` (см. «Профиль `services`»). - **`minio-init` завис или завершился с ошибкой:** `docker compose logs minio-init`; чаще всего `minio` не стал healthy — `docker compose logs minio`. Повторный `docker compose up -d` идемпотентен. -- **Остаточные тома от прошлых запусков** (старый пароль БД, бакет не тот): `docker compose down -v` и запуск заново. +- **Остаточные тома от прошлых запусков** (старый пароль БД, бакет не тот): `docker compose --profile services down -v` + и запуск заново (без `--profile services` контейнеры профиля и том `hotfolder` остаются, см. «Сброс окружения»). - **`up -d --wait` возвращает код 1:** см. раздел «Запуск инфраструктуры» — указывайте сервисы явно. -- 2.47.2 From 2e2e50accbfb755653c96d41e27b0e86c7f37b5d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:17:08 +0400 Subject: [PATCH 40/68] =?UTF-8?q?docs(dev):=20README=20=E2=80=94=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=20=C2=AB=D0=9F=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BA=D0=B0=20=D0=BE=D0=BA=D1=80=D1=83=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=C2=BB=20=D1=81=D0=BE=20=D1=81=D0=BA=D0=B2?= =?UTF-8?q?=D0=BE=D0=B7=D0=BD=D1=8B=D0=BC=20=D1=81=D1=86=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Помощники dpsql/dmc/new_job/poll и фактический вывод трёх сценариев на профиле services (2026-10-04): tabular → error/NOT_IMPLEMENTED; image → эмулятор ИС → error/INVALID_DOCUMENT (ответ-ошибка из фикстуры aep_5) и raw/{job_id}.json в MinIO; tabular-parser остановлен → 3 повтора через 30 с → dlq/NO_ACK (~120 с). Что сквозная проверка Среза 0 не покрывает. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- dev/README.md | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/dev/README.md b/dev/README.md index 168ecf7..ad4f10e 100644 --- a/dev/README.md +++ b/dev/README.md @@ -145,6 +145,112 @@ curl -s localhost:8000/api/v1/me -H "Authorization: Bearer " # email, **Остановка и сброс профиля** — только с `--profile services`: без него compose не видит контейнеры профиля (см. «Сброс окружения»). +## Проверка окружения (сквозной сценарий) + +Ручная проверка обмена `recognition_job` через NATS на поднятом профиле `services`: ядро публикует задание, сервис +отвечает ack и результатом, ядро меняет статус. UI и API загрузки файлов в Срезе 0 нет, поэтому файл кладётся в MinIO +через `mc`, а строки `stored_file` / `s3_object` / `recognition_job` вставляются через `psql`. Проверено 2026-10-04 +(все три сценария ниже — фактический вывод, время UTC). + +Помощники (bash/zsh, из `docs/dev/`; `uuidgen` есть в macOS и util-linux): + +```bash +dpsql() { docker compose exec -T postgres sh -c 'psql -X -q -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" "$@"' sh "$@"; } +dmc() { docker compose run --rm -v "$PWD/../../fixtures:/fixtures:ro" --entrypoint sh minio-init \ + -c 'mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && mc "$@"' sh "$@"; } +new_job() { # new_job <файл в fixtures> — кладёт файл в uploads/ и ставит задание queued + F=$(uuidgen | tr A-Z a-z); JOB=$(uuidgen | tr A-Z a-z); O=$(uuidgen | tr A-Z a-z); N=$(basename "$3") + dmc cp -q "/fixtures/$3" "local/arm-ka/uploads/$F/$JOB/$N" >/dev/null || return 1 + dpsql < — статус задания $JOB раз в секунду, до финального статуса + for _ in $(seq "$1"); do + s=$(dpsql -At -c "select status || ' ' || coalesce(error_code, '') || ' ' || coalesce(metrics->>'republish_count', '') from dkb.recognition_job where id = '$JOB'") + [ "$s" != "$p" ] && echo "$(date -u +%T) $s" && p=$s + case $s in error*|done*|needs_check*|timeout*|dlq*) p=; return;; esac + sleep 1 + done; p= +} +``` + +Ключ исходника — `uploads/{file_id}/{job_id}/{имя}` (`../context/03-architecture.md`, «Схема ключей S3»). Сообщения на +шине можно смотреть параллельно: `docker run --rm --network arm-ka_default natsio/nats-box:0.17.0 nats sub -s +nats://nats:4222 'recognition.>'` (печатает `Nats-Msg-Id` и тело каждого запроса, ack и результата). + +**1. `tabular` → `NOT_IMPLEMENTED`.** Заглушка парсера Среза 0 отвечает ошибкой на любой файл: + +```bash +new_job tabular XLSX "osv/schastl/2025-Q3/Оборотносальдовая ведомость по счету 60 за 3 квартал 2025 г..xlsx" && poll 30 +# JOB=e585f47b-1b38-44a6-84c5-9432c7e6d194 +# 02:12:15 queued +# 02:12:20 error NOT_IMPLEMENTED +dpsql -c "select status, error_code, error_message, published_at::time(3), started_at::time(3), finished_at::time(3) from dkb.recognition_job where id = '$JOB'" +# error | NOT_IMPLEMENTED | разбор XLS/XLSX/XLSB реализуется в Срезе 2 | 02:12:18.98 | 02:12:19.021 | 02:12:19.024 +``` + +`published → processing → error` проходит за десятки миллисекунд (в прогонах 14–51 мс), опросом его не поймать; последовательность видна по +`published_at < started_at < finished_at` (`started_at` пишет ack), в логе `arm-core` («Запрос распознавания +опубликован» → «Сервис подтвердил получение запроса (ack): задание в processing» → «Результат распознавания принят … +error_code=NOT_IMPLEMENTED») и на шине: `recognition.tabular.request` (`Nats-Msg-Id: :1`) → +`recognition.result` `ack::1` (`status: processing`) → `result::1` (`status: error`). Публикатор +опрашивает outbox раз в 5 с, поэтому от вставки до публикации проходит до 5 с. + +**2. `image` → эмулятор ИС → `INVALID_DOCUMENT`, сырой ответ в `raw/`:** + +```bash +new_job image PDF ocr/aep_5.PDF && poll 30 +# JOB=46b0c90b-5a1b-4ac0-9e8a-0cdad80d857e +# 02:12:27 queued +# 02:12:29 processing +# 02:12:34 error INVALID_DOCUMENT +dmc ls local/arm-ka/raw/ +# [2026-10-04 02:12:34 UTC] 588B STANDARD 46b0c90b-5a1b-4ac0-9e8a-0cdad80d857e.json +dmc cat "local/arm-ka/raw/$JOB.json" # {"file_uid": "", "error": {"code": "INVALID_DOCUMENT", "message": "Не удалось определить тип документа."}, "data_ocr": {…}} +``` + +Код `INVALID_DOCUMENT` здесь — **настоящий ответ ИС из фикстуры**: обе пары в `fixtures/ocr` (`aep_5.json`, +`aep_8_form4.json`) — ответы-ошибки ИС Сканирование («Не удалось определить тип документа.», пустой `table_structure`). +Эмулятор нашёл пару (в логе `scan-emulator`: `_aep_5.PDF -> …/Output//.json`; при +отсутствии пары сообщение было бы «…: нет эталонного ответа в фикстурах»). Поэтому успешный путь `image` +(`done`/`needs_check`, файл `results/…`) сквозной проверкой на текущих фикстурах не проходит — нужен успешный ответ ИС +в `fixtures/ocr`. Имя файла — `aep_5.PDF` (расширение в верхнем регистре; на macOS `aep_5.pdf` тоже находится, на Linux — нет). +`processing` виден ~5 с: задержка эмулятора 2 с плюс опрос горячей папки шлюзом. + +**3. Сервис недоступен → три повторные публикации → `dlq` / `NO_ACK`:** + +```bash +docker compose stop tabular-parser # имя сервиса указано явно — профиль не нужен +new_job tabular XLSX "osv/schastl/2025-Q3/Оборотносальдовая ведомость по счету 60 за 3 квартал 2025 г..xlsx" && poll 180 +# 02:12:43 queued +# 02:12:44 published +# 02:13:14 published 1 ← republish_count: повтор через ACK_TIMEOUT_SECONDS = 30 с, тот же Nats-Msg-Id :1 +# 02:13:45 published 2 +# 02:14:14 published 3 +# 02:14:44 dlq NO_ACK 3 ← ещё через 30 с после третьего повтора (OUTBOX_MAX_REPUBLISH = 3) +dpsql -c "select status, error_code, error_message, metrics, request_msg_id from dkb.recognition_job where id = '$JOB'" +# dlq | NO_ACK | Сервис распознавания не подтвердил получение запроса (ack) после 3 повторных публикаций | {"republish_count": 3} | :1 +docker compose --profile services up -d --wait tabular-parser # вернуть сервис; rc=0, Healthy +``` + +От первой публикации до `dlq` — ~120 с (4 × 30 с). `published_at` при каждом повторе перезаписывается (время последней +публикации), `attempt` остаётся 1, `started_at` и `service_name` пусты. Задание в `dlq` после возврата сервиса само не +перезапускается (replay — вне Среза 0). + +Что сквозная проверка **не** покрывает (Срез 0): успешные результаты (`done`/`needs_check`, регистрация файла +`results/…` в `s3_object`) — заглушка парсера и фикстуры ИС дают только ошибки; `stored_file.status` остаётся +`UPLOADED`, `current_recognition_job_id` не заполняется (маппинг результата — `NoopMapper`, Срез 2); у ошибок +`scan-gateway` `recognition_job.metrics` пустой (`tabular-parser` присылает `duration_ms`). Тестовые строки и объекты +остаются в БД и MinIO; полная очистка — `docker compose --profile services down -v`. + ## Переменные `.env` Файл читают `docker compose` (из этого каталога) и, вторым блоком, сервисы при запуске из venv. Значения по умолчанию — -- 2.47.2 From 29e74b875fb636902ee9edce0eef437b5f682e79 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:17:45 +0400 Subject: [PATCH 41/68] =?UTF-8?q?docs(openspec):=20design.md=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=2030=E2=80=9333?= =?UTF-8?q?=20(=D1=81=D0=BA=D0=B2=D0=BE=D0=B7=D0=BD=D0=B0=D1=8F=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0=20=D0=BE=D0=BA=D1=80?= =?UTF-8?q?=D1=83=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- openspec/changes/slice-0-skeleton/design.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index e821829..6a591a8 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -161,3 +161,7 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 27 | 2026-10-04 | Одноразовый сервис `arm-core-migrate` (`alembic upgrade head && python -m scripts.seed --dev`, `depends_on` postgres `service_healthy`); `arm-core` ждёт его `service_completed_successfully`; выполняется при каждом `up` профиля | дополнение контроллера к задаче 5.3; без схемы `dkb` и справочников ядро не работает, оба шага идемпотентны (повторный seed — `0 / 0 / N`); `--dev` — нужны dev-пользователи для dev-токена | лишний запуск миграций при каждом `up` (~2 с); убрать `--dev` — одна строка | | 28 | 2026-10-04 | Образ каждого репозитория собирает один сервис: `build` только у `arm-core` и `scan-gateway`; `arm-core-migrate` и `scan-emulator` используют тег `arm-core:dev`/`scan-gateway:dev` с `pull_policy: never` (отступление от фрагмента `scan-gateway`, где `build` у обоих) | проверено: при `build` у двух сервисов с одним тегом compose 2.34 собирает образ дважды с разными ID, метка `com.docker.compose.image` контейнера расходится с тегом, и каждый следующий `up` пересоздаёт `arm-core-migrate` и `arm-core` (флип-флоп); с одной сборкой три `up` подряд — 0 пересозданий | `up -d scan-emulator` или `arm-core-migrate` в одиночку на машине без образа падает `No such image` — сначала `docker compose --profile services build`; вернуть `build` — две строки | | 29 | 2026-10-04 | В compose у `arm-core` включён dev-эмитент токенов (`AUTH_DEV_ISSUER=true`, публичный `AUTH_DEV_SECRET` из `arm-core/api/.env.example`) | ручные проверки `/api/v1/me` и сквозной сценарий (задача 5.4) без РАИСА; маршрут существует только при `DEBUG=true`; порт хоста по умолчанию на 127.0.0.1 | при `BIND_ADDR=0.0.0.0` токен любого dev-пользователя может получить любой в сети — только dev; выключить — одна строка | +| 30 | 2026-10-04 | Сквозная проверка окружения (задача 5.4) выполняется вставкой строк `legal_entity` (синтетическое, ИНН `7700000001`, `on conflict do nothing`) / `s3_object` / `stored_file` / `recognition_job(queued)` через `psql` в контейнере `postgres` и загрузкой исходника в `uploads/{file_id}/{job_id}/{имя}` через `mc` из образа `minio-init`; рецепт — функции `dpsql`/`dmc`/`new_job`/`poll` в `dev/README.md` («Проверка окружения»); тестовые строки и объекты не удаляются | API загрузки файлов в Срезе 0 нет, тестовых скриптов `arm-core` для рабочей БД тоже (его интеграционные тесты работают во временной БД); ключ — по «Схеме ключей S3» `03-architecture.md`; рецепт проверен прогоном дословно | вставка в обход будущего API может разойтись с ним (обязательные поля `stored_file`/`document`) — рецепт придётся обновить в Срезе 1/2; мусорные строки в dev-БД — сброс `down -v` | +| 31 | 2026-10-04 | Ожидаемый итог сценария `image` в README — `error` / `INVALID_DOCUMENT` (фактический ответ), а не `NOT_IMPLEMENTED`; файл — `ocr/aep_5.PDF` (так он назван в `fixtures`, не `aep_5.pdf`) | обе пары `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС, эмулятор отдаёт их как есть, шлюз пробрасывает код ИС; на регистрозависимой ФС `aep_5.pdf` не найдётся | успешный путь `image` (`done`/`needs_check`, `results/…`) сквозной проверкой не покрыт, пока в `fixtures/ocr` нет успешного ответа ИС | +| 32 | 2026-10-04 | Замечание T3.6 проверено эмпирически: образы `scan-gateway:dev` и `arm-core:dev` пересобраны `build --no-cache` (новые ID) на работающем профиле; после пересоздания контейнеров старые ID образов в `docker image ls -a` не остались (`No such image`) | формулировку README нужно было подтвердить, а без смены ID образа compose ничего не пересоздаёт | ~1 мин сборки и загрузка зависимостей из PyPI; на окружение не влияет (образы — те же исходники) | +| 33 | 2026-10-04 | В README для наблюдения за шиной предложен `natsio/nats-box:0.17.0` (`nats sub 'recognition.>'` в сети `arm-ka_default`) — разовый `docker run --rm`, не сервис compose | без него `published`/`processing` у `tabular` (десятки мс) не видны; образ доступен с Docker Hub (проверено), в compose не добавлялся, чтобы не менять состав окружения | при недоступности образа — только логи `arm-core` и метки времени задания; шаг опциональный | -- 2.47.2 From a11eba028f7df0d38dea7013ff93f3fd6d85839b Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:38:58 +0400 Subject: [PATCH 42/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=205.4?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md index abe0854..3d2f538 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md @@ -29,7 +29,7 @@ ### Task 4: [5.4] [docs] Сквозная проверка руками по README -- [ ] [docs] Сквозная проверка руками по README: вставить `recognition_job` в БД через `psql` (или тестовый скрипт arm-core) → паблишер публикует → `tabular-parser` отвечает ack и `NOT_IMPLEMENTED` → `recognition_job.status` проходит `published → processing → error`, `error_code = NOT_IMPLEMENTED`; то же для `image` через эмулятор (`raw/{job_id}.json` появляется в MinIO); остановить `tabular-parser`, вставить задание — через 30 с повторная публикация, после трёх — `dlq`; зафиксировать в README как «проверка окружения» +- [x] [docs] Сквозная проверка руками по README: вставить `recognition_job` в БД через `psql` (или тестовый скрипт arm-core) → паблишер публикует → `tabular-parser` отвечает ack и `NOT_IMPLEMENTED` → `recognition_job.status` проходит `published → processing → error`, `error_code = NOT_IMPLEMENTED`; то же для `image` через эмулятор (`raw/{job_id}.json` появляется в MinIO); остановить `tabular-parser`, вставить задание — через 30 с повторная публикация, после трёх — `dlq`; зафиксировать в README как «проверка окружения» ### Task 5: [6.1] [docs] `06-test-data.md` §6 diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index e0f3b48..08c3698 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -58,7 +58,7 @@ - [x] 5.3 [docs] `docker-compose.yml`: профиль `services` — `arm-core` (8000), `tabular-parser` (8001), `scan-gateway` (8002), `scan-emulator` (том `hotfolder`, `../../fixtures/ocr:ro`); проверка: `--profile services up --build`, все `/healthcheck` 200 с `nats = ok` -- [ ] 5.4 [docs] Сквозная проверка руками по README: вставить `recognition_job` в БД через `psql` (или тестовый скрипт +- [x] 5.4 [docs] Сквозная проверка руками по README: вставить `recognition_job` в БД через `psql` (или тестовый скрипт arm-core) → паблишер публикует → `tabular-parser` отвечает ack и `NOT_IMPLEMENTED` → `recognition_job.status` проходит `published → processing → error`, `error_code = NOT_IMPLEMENTED`; то же для `image` через эмулятор (`raw/{job_id}.json` появляется в MinIO); остановить `tabular-parser`, вставить задание — через 30 с повторная -- 2.47.2 From bf0a0aef7b4527f12e918a58cfb80ab360366500 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:40:26 +0400 Subject: [PATCH 43/68] =?UTF-8?q?docs(context):=2006-test-data=20=C2=A76?= =?UTF-8?q?=20=E2=80=94=20seed=20=D0=BF=D1=80=D0=BE=D0=B4=D1=83=D0=BA?= =?UTF-8?q?=D1=82=D0=B0=20=D0=B2=20arm-core/api/seed,=20directories=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D1=85=20=D0=AE=D0=9B?= =?UTF-8?q?/=D0=93=D0=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- context/06-test-data.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/context/06-test-data.md b/context/06-test-data.md index c3f6539..1256054 100644 --- a/context/06-test-data.md +++ b/context/06-test-data.md @@ -146,15 +146,22 @@ fixtures/ statements-xlsx/ # ББ/ОФР в Excel (§12): лист «Отчетность» из Анализ*.xlsx, # рекомендуемые формы банка, выгрузки 1С — собрать у аналитиков ocr/ - aep_N.pdf + aep_N.json # пары вход/выход ИС Сканирование + aep_N.pdf|PDF + aep_N.json # пары вход/выход ИС Сканирование (эмулятор ИС + # в scan-gateway находит пару по имени, регистр + # расширения значим на Linux) golden/ Консолидация_Группа РБ.xlsx # эталон КФО по датам expected_osv_parse/*.json # ожидаемый результат парсера (зафиксировать после ревью) directories/ legal_entities.csv, company_groups.csv # тестовые справочники группы «Русский бисквит» (Срез 1) ``` -Справочники **продукта** — план счетов, правила элиминации, права и роли, состав обязательных документов — не фикстуры, -а seed ядра: `arm-core/api/seed/` (Срез 0, change `slice-0-skeleton`). Здесь остаются только тестовые данные группы. +`directories/` содержит только тестовые справочники группы (ЮЛ и ГК «Русский бисквит»); до Среза 1 в нём лежит пустой +`.gitkeep`. Справочники **продукта** — план счетов, правила элиминации, права и роли, состав обязательных документов, +региональные офисы — не фикстуры, а seed ядра: `arm-core/api/seed/core/` (обязательные данные: `roles.yaml`, +`permissions.yaml`, `role_permissions.yaml`, `regional_office.yaml`, `chart_of_accounts.csv`, `elimination_rules_v1.yaml`, +`required_document_rules.yaml`) и `arm-core/api/seed/dev/` (только `python -m scripts.seed --dev`: dev-пользователи и +примеры РФ). Загрузчик — `python -m scripts.seed [--dev]` из `arm-core/api/` (идемпотентен, upsert по натуральному ключу; +в compose его выполняет одноразовый сервис `arm-core-migrate`). Здесь остаются только тестовые данные группы. Полный план счетов 1С из бандла макета не передан — seed строится на Плане счетов 94н и типовых субсчетах 1С (OQ-47). Первый прогон парсера на `osv/**` сохранить как `expected_osv_parse/*.json` после ручной сверки итогов со строкой «Итого» — дальше это regression-набор. -- 2.47.2 From a977ab7792bb682245088ff60291f098423214dc Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:40:35 +0400 Subject: [PATCH 44/68] =?UTF-8?q?docs(context):=2009-delivery-plan,=20?= =?UTF-8?q?=D0=A1=D1=80=D0=B5=D0=B7=200=20=E2=80=94=20=D1=81=D1=81=D1=8B?= =?UTF-8?q?=D0=BB=D0=BA=D0=B0=20=D0=BD=D0=B0=20dev/README=20=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BA=D0=B2=D0=BE=D0=B7=D0=BD=D1=83=D1=8E=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83=20=D0=BE=D0=BA=D1=80=D1=83?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- context/09-delivery-plan.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/context/09-delivery-plan.md b/context/09-delivery-plan.md index 19eb5bc..eda18db 100644 --- a/context/09-delivery-plan.md +++ b/context/09-delivery-plan.md @@ -19,6 +19,9 @@ платформе; ack и повторная публикация — на уровне приложения), MinIO (S3, один бакет `arm-ka`), эмулятор ИС Сканирование (часть `scan-gateway`, отдаёт `fixtures/ocr/aep_N.json`), три сервиса (профиль `services`, Python 3.11). `Makefile` с общими целями `up/test/test-integration/lint/check/check-full` (+ `migrate/seed` в ядре) в каждом репо. + Порты, запуск профиля `services` и ручная сквозная проверка обмена `recognition_job` через NATS (сценарии `tabular` → + `NOT_IMPLEMENTED`, `image` → эмулятор ИС → `INVALID_DOCUMENT`, потеря ack → `dlq`) — `docs/dev/README.md`, + разделы «Профиль `services`» и «Проверка окружения». - Контракт v1.0 — три схемы в `docs/contracts/`: `recognition-request`, `recognition-result-message` (конверты NATS) и `recognition-result` (файл результата) + `examples/`. Общий пакет `arm-ka-contracts` **не публикуется**: каждый сервис хранит копию схем в `api/contracts/` и pydantic-модели в `src/contract/`, идентичность с `docs/contracts` проверяется в `make check` -- 2.47.2 From 2423854f27e1947539bd6a5cb799ae66196366e4 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:41:29 +0400 Subject: [PATCH 45/68] =?UTF-8?q?docs(openspec):=20recognition-contract=20?= =?UTF-8?q?=E2=80=94=20SCHEMA=5FMISMATCH=20=D0=B1=D0=B5=D0=B7=20ack,=20?= =?UTF-8?q?=D0=BD=D0=B5=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81,=20=D0=BF=D0=BE=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D1=8B=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8,=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=D1=8B=20=D0=BA=D0=B0=D0=BA=20=D0=B8=D0=BB=D0=BB=D1=8E?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../specs/recognition-contract/spec.md | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md b/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md index 219679d..fbc0f56 100644 --- a/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md +++ b/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md @@ -21,8 +21,28 @@ заголовком `Nats-Msg-Id` = `:` #### Scenario: Сообщение не по схеме -- **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`) -- **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` и не падает +- **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`), но из тела извлекаются + корректные `job_id` и `file_id` (и `attempt` для `Nats-Msg-Id`) +- **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` **без предшествующего ack + `processing`** и не падает + +#### 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: Подтверждение взятия в работу (ack без JetStream) Получив запрос, сервис SHALL немедленно, до чтения файла из S3, опубликовать в `recognition.result` конверт со @@ -82,8 +102,10 @@ v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `f #### Scenario: Несовместимая версия запроса - **WHEN** сервис, поддерживающий `1.x`, получает запрос `schema_version = "2.0"` -- **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH`, запрос подтверждается (ack), чтобы не - зациклить доставку +- **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH` **без ack `processing`**: ответом на + запрос служит сам финальный конверт, ядро закрывает им задание (`published → error`), и повторной доставки запроса не + будет. Конверт публикуется только если из тела извлекаются корректные `job_id` и `file_id`; иначе — как для нечитаемого + запроса (ответа нет, ядро закроет задание по `NO_ACK` / `dlq`) ### Requirement: Идемпотентность результатов без брокера Публикующий SHALL ставить заголовок `Nats-Msg-Id` на каждое сообщение (запрос `:`, ack @@ -95,6 +117,23 @@ v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `f - **WHEN** сервис опубликовал один и тот же результат два раза (повтор публикации после сетевой ошибки) - **THEN** задание изменено один раз, второе сообщение проигнорировано +### Requirement: Повторы публикации ответов сервисом +Сервис SHALL публиковать ack и результат с заголовком `Nats-Msg-Id` и при временной ошибке публикации (потеря соединения, +таймаут подтверждения сервера) повторять публикацию с тем же `Nats-Msg-Id`: **одна первая попытка и до +`PUBLISH_RETRIES` повторов** (значение по умолчанию 3, то есть не более 4 попыток), с паузой между попытками; пауза — +деталь реализации. Повторы исчерпаны — сервис пишет error-лог и не публикует это сообщение снова; задание закрывается +на стороне ядра (не пришёл результат — `reply_deadline` → `timeout`; не пришёл ack — повтор запроса и `dlq`, но результат, +дошедший без ack, ядро принимает и в статусе `published`). Дубликаты, появившиеся из-за повтора, +безопасны: ядро дедуплицирует их по `Nats-Msg-Id`. + +#### Scenario: Временный сбой публикации +- **WHEN** публикация результата упала с временной ошибкой, а следующая попытка успешна +- **THEN** сообщение опубликовано с прежним `Nats-Msg-Id`; ядро изменит задание один раз, даже если первая попытка тоже дошла + +#### Scenario: Повторы исчерпаны +- **WHEN** все 1 + `PUBLISH_RETRIES` попыток публикации результата неуспешны +- **THEN** сервис пишет error-лог и продолжает работу (процесс не падает), а ядро закрывает задание по `reply_deadline` + ### Requirement: Копия контракта в сервисе идентична источнику Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check` их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением). -- 2.47.2 From 3e1f39cd9e2314ddbc4301fe5c748060db7991d2 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:41:29 +0400 Subject: [PATCH 46/68] =?UTF-8?q?docs(context):=2003-architecture=20?= =?UTF-8?q?=E2=80=94=20SCHEMA=5FMISMATCH=20=D0=B1=D0=B5=D0=B7=20ack,=20?= =?UTF-8?q?=D0=BD=D0=B5=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=20=D0=B1=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0,=20PUBLISH=5FRETRIES,=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- context/03-architecture.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/context/03-architecture.md b/context/03-architecture.md index 9bc4e22..a11ac65 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -42,7 +42,11 @@ и [`recognition-result-message.schema.json`](../contracts/recognition-result-message.schema.json) — тела сообщений, [`recognition-result.schema.json`](../contracts/recognition-result.schema.json) — файл результата в S3; примеры — [`docs/contracts/examples/`](../contracts/examples/). Примеры проверяются схемами: `python scripts/validate_contracts.py` -(из корня репозитория docs). +(из корня репозитория docs). Примеры — иллюстрации формы сообщений, а не готовые к исполнению задания: +схема проверяет у `reply_deadline` только формат `date-time` и не сравнивает срок с текущим временем, поэтому +`request-image.json` проходит схему при `reply_deadline` в прошлом. Срок проверяет сервис: `scan-gateway` отвечает +`error TIMEOUT`, если `reply_deadline` истёк до отправки файла в ИС Сканирование; `tabular-parser` срок не проверяет. +Файлы `examples/` копируются в сервисы побайтно, поэтому пример не правится ради «свежей» даты. | Поток | Subject | Кто пишет | Полезная нагрузка | `Nats-Msg-Id` | Схема | |---|---|---|---|---|---| @@ -57,7 +61,9 @@ ядро — queue group `arm-core` на `recognition.result`; сервис сразу по получении запроса публикует ack `processing`; ядро, не получив ack за `ACK_TIMEOUT_SECONDS` (30), публикует запрос повторно с тем же `Nats-Msg-Id`, не более `OUTBOX_MAX_REPUBLISH` (3) раз, затем `dlq`. Сервис держит process-local множество `job_id` в работе и на повторный -запрос отвечает ack'ом без второй обработки. Появится JetStream — меняются только publisher/subscriber. +запрос отвечает ack'ом без второй обработки. Публикация ack и результата сервисом при временной ошибке повторяется +с тем же `Nats-Msg-Id`: первая попытка плюс `PUBLISH_RETRIES` повторов (по умолчанию 3, всего до 4 попыток); повторы +исчерпаны — error-лог, задание закроет ядро (по `reply_deadline`). Появится JetStream — меняются только publisher/subscriber. Правила: - `recognition_job` создаётся в статусе `queued` **в одной транзакции** со `stored_file`; фоновый публикатор переводит в @@ -70,8 +76,12 @@ Ack (`processing`) сервис публикует **до чтения файла из S3**; повторная публикация запроса ядром идёт с тем же `Nats-Msg-Id` и тем же `attempt`. Дубликат результата ядро определяет по `recognition_job.result_msg_id`. - `schema_version` — `MAJOR.MINOR`; ядро и сервисы принимают тот же MAJOR и любой MINOR. Сервис отвергает запрос с - неизвестным MAJOR результатом `error` с кодом `SCHEMA_MISMATCH` и подтверждает его ack'ом, чтобы не зациклить - доставку. Новый MINOR раскатывается сначала на потребителей + неизвестным MAJOR (как и запрос не по схеме) результатом `error` с кодом `SCHEMA_MISMATCH` **без предшествующего ack + `processing`**: финальный конверт сам закрывает задание в ядре (`published → error`), повторной доставки не будет. + Конверт публикуется, только если из тела извлекаются корректные `job_id` и `file_id`; для `job_id`, уже находящегося в + работе или обработанного, он не публикуется (его `Nats-Msg-Id` совпал бы с настоящим результатом). Нечитаемый запрос + (не JSON, нет `job_id`) остаётся без ответа: сервис пишет лог и отбрасывает его, ядро не получает ack, повторяет + публикацию `OUTBOX_MAX_REPUBLISH` раз и закрывает задание в `dlq` с `error_code = NO_ACK`. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения. - Сервисы не знают про БД и про бизнес-смысл: они возвращают структуру файла; нормализация счетов, резолв -- 2.47.2 From dc64d9a2145c73c559624bd204b7014bbf9899bc Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:41:55 +0400 Subject: [PATCH 47/68] =?UTF-8?q?docs(openspec):=20design.md=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=2034=E2=80=9338?= =?UTF-8?q?=20(=D1=80=D1=83=D0=BB=D0=B8=D0=BD=D0=B3=D0=B8=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D1=87=D0=B8=206.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- openspec/changes/slice-0-skeleton/design.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index 6a591a8..2eefe17 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -165,3 +165,8 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 31 | 2026-10-04 | Ожидаемый итог сценария `image` в README — `error` / `INVALID_DOCUMENT` (фактический ответ), а не `NOT_IMPLEMENTED`; файл — `ocr/aep_5.PDF` (так он назван в `fixtures`, не `aep_5.pdf`) | обе пары `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС, эмулятор отдаёт их как есть, шлюз пробрасывает код ИС; на регистрозависимой ФС `aep_5.pdf` не найдётся | успешный путь `image` (`done`/`needs_check`, `results/…`) сквозной проверкой не покрыт, пока в `fixtures/ocr` нет успешного ответа ИС | | 32 | 2026-10-04 | Замечание T3.6 проверено эмпирически: образы `scan-gateway:dev` и `arm-core:dev` пересобраны `build --no-cache` (новые ID) на работающем профиле; после пересоздания контейнеров старые ID образов в `docker image ls -a` не остались (`No such image`) | формулировку README нужно было подтвердить, а без смены ID образа compose ничего не пересоздаёт | ~1 мин сборки и загрузка зависимостей из PyPI; на окружение не влияет (образы — те же исходники) | | 33 | 2026-10-04 | В README для наблюдения за шиной предложен `natsio/nats-box:0.17.0` (`nats sub 'recognition.>'` в сети `arm-ka_default`) — разовый `docker run --rm`, не сервис compose | без него `published`/`processing` у `tabular` (десятки мс) не видны; образ доступен с Docker Hub (проверено), в compose не добавлялся, чтобы не менять состав окружения | при недоступности образа — только логи `arm-core` и метки времени задания; шаг опциональный | +| 34 | 2026-10-04 | Правим спеку и документ под фактическое поведение сервисов, а не код: при неизвестном MAJOR `schema_version` (и при запросе не по схеме) сервис публикует `error SCHEMA_MISMATCH` **без** отдельного ack `processing`; формулировка «и подтверждает ack'ом» (T12.13) из `spec.md` (сценарий «Несовместимая версия запроса») и `03-architecture.md` убрана. Конверт публикуется, только если извлекаются корректные `job_id`/`file_id`, и не публикуется для `job_id`, уже находящегося в работе/обработанного | `tabular-parser/api/src/core/worker.py` (`_reject`: «`SCHEMA_MISMATCH` без ack», строки 242–275) и `scan-gateway/api/src/core/worker.py` (`_reject`, R-16: строки 288–321) ведут себя одинаково; финальный конверт сам закрывает задание в ядре (`published → error` допустим, `recognition_exchange.py`: результат принимается из `queued/published/processing`), ack перед ним ничего не добавляет, а `Nats-Msg-Id` ack и результата разные; конверт для `job_id` в работе совпал бы по `result::` с настоящим результатом | если ядро когда-нибудь начнёт требовать ack перед результатом, задание с `SCHEMA_MISMATCH` останется в `published` до повторов и `dlq`; правка — одна строка в воркерах и спеке | +| 35 | 2026-10-04 | Запрос, из которого нельзя извлечь `job_id` (не JSON, не объект, нет/не uuid `job_id`), остаётся без ответа: сервис пишет лог и отбрасывает его; ядро закрывает задание по `NO_ACK` / `dlq` после `OUTBOX_MAX_REPUBLISH` повторов. Записано в `spec.md` (новый сценарий «Нечитаемый запрос») и `03-architecture.md` | собрать валидный конверт без `job_id` нельзя (схема требует uuid), а выдуманный `job_id` ядро всё равно не сопоставит с заданием; оба сервиса логируют и сбрасывают (`worker.py`: tabular строки 253–263, scan-gateway 292–299); ядро и так закрывает «молчание» (`recognition_exchange.py`: `NO_ACK`, `dlq`) | нет худшего исхода, чем 120 с до `dlq` (4 × `ACK_TIMEOUT_SECONDS`) для заведомо битого запроса, которого ядро само не порождает; сокращать задержку можно только чтением `job_id` из заголовка `Nats-Msg-Id` — вне Среза 0 | +| 36 | 2026-10-04 | `PUBLISH_RETRIES` в спеке (новое требование «Повторы публикации ответов сервисом») = **одна первая попытка + до 3 повторов** (всего не более 4 попыток) с тем же `Nats-Msg-Id` и паузой между попытками; исчерпаны — error-лог, задание закрывает ядро (`reply_deadline`). Пауза — деталь реализации, в спеку не включена | значения в коде: `scan-gateway/api/src/core/config.py:63` (`PUBLISH_RETRIES = 3`, «повторы после первой попытки (всего до 4)») и `core/nats.py:245–256` (`attempts_total = retry_count + 1`); `tabular-parser/api/src/core/config.py:49` (`PUBLISH_RETRIES = 3`, `NonNegativeInt`) и `core/nats.py:306` (`attempts = publish_retries + 1`); паузы различаются (2,0 с и 0,5 с), семантика одна | если ядро/сервис договорятся считать `PUBLISH_RETRIES` общим числом попыток, расхождение «3 против 4» не ломает обмен (дедуп по `Nats-Msg-Id`); правка — одна фраза в спеке | +| 37 | 2026-10-04 | Пример `contracts/examples/request-image.json` с `reply_deadline` в прошлом **не меняется**; в `spec.md` (сценарий «Примеры контракта — иллюстрации») и `03-architecture.md` записано, что примеры — иллюстрации формы, схема проверяет у `reply_deadline` лишь формат `date-time`, а срок проверяет сервис (`scan-gateway` — `error TIMEOUT`, `scan-gateway/api/src/core/worker.py:469–475`; `tabular-parser` срок не проверяет: `reply_deadline` в его воркере не используется) | копии `examples/` в сервисах сверяются побайтно тестами идентичности: правка примера = пересинхронизация трёх репозиториев ради документальной оговорки; `validate_contracts.py` проходит (rc 0, 8 примеров) | если кто-то возьмёт `request-image.json` как готовое задание для `scan-gateway`, получит `TIMEOUT` — это зафиксировано оговоркой; обновить дату позже можно одним PR с пересинхронизацией | +| 38 | 2026-10-04 | `06-test-data.md` §6 и `09-delivery-plan.md` (Срез 0) приведены к факту: `fixtures/directories/` — только тестовые ЮЛ/ГК (сейчас пустой `.gitkeep`, наполнение — Срез 1); seed продукта лежит в `arm-core/api/seed/core/` (+ `seed/dev/` только для `--dev`); в плане Среза 0 добавлена ссылка на `dev/README.md` («Профиль `services`», «Проверка окружения»); `00-workspace-layout.md` не менялся (неактуальных фактов нет) | перечень файлов seed и число ролей/прав проверены по `arm-core/api/seed/` (7 ролей, 16 прав, 122 строки плана счетов); решение D-1, compose и проверки вместо CI в `09` уже были верны | только неточная ссылка в документах; правится правкой текста | -- 2.47.2 From f8111cc3832e65b9ff31537feadabf57ec6e7a62 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:49:08 +0400 Subject: [PATCH 48/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=206.1?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md index 3d2f538..db0002f 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md @@ -33,7 +33,7 @@ ### Task 5: [6.1] [docs] `06-test-data.md` §6 -- [ ] [docs] `06-test-data.md` §6: `fixtures/directories/` — только тестовые ЮЛ/ГК (Срез 1); план счетов, правила элиминации, права — `arm-core/api/seed/`; `09-delivery-plan.md` Срез 0: решение D-1 по пакету контрактов, ссылка на `docs/dev/`, проверки вместо CI +- [x] [docs] `06-test-data.md` §6: `fixtures/directories/` — только тестовые ЮЛ/ГК (Срез 1); план счетов, правила элиминации, права — `arm-core/api/seed/`; `09-delivery-plan.md` Срез 0: решение D-1 по пакету контрактов, ссылка на `docs/dev/`, проверки вместо CI ### Task 6: [6.2] [docs] `07-open-questions.md` diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 08c3698..369b734 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -66,7 +66,7 @@ ## 6. Документы контекста и закрытие -- [ ] 6.1 [docs] `06-test-data.md` §6: `fixtures/directories/` — только тестовые ЮЛ/ГК (Срез 1); план счетов, правила +- [x] 6.1 [docs] `06-test-data.md` §6: `fixtures/directories/` — только тестовые ЮЛ/ГК (Срез 1); план счетов, правила элиминации, права — `arm-core/api/seed/`; `09-delivery-plan.md` Срез 0: решение D-1 по пакету контрактов, ссылка на `docs/dev/`, проверки вместо CI - [ ] 6.2 [docs] `07-open-questions.md`: OQ по итогам Среза 0 (контракт `data_type = NU` vs enum `osv_data_type`; -- 2.47.2 From f7c8c6c8838522be39c140e3e9b4d2ff4c529b33 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:52:49 +0400 Subject: [PATCH 49/68] =?UTF-8?q?docs(context):=2007-open-questions=20?= =?UTF-8?q?=E2=80=94=20OQ-48=E2=80=A6OQ-83=20=D0=BF=D0=BE=20=D0=B8=D1=82?= =?UTF-8?q?=D0=BE=D0=B3=D0=B0=D0=BC=20=D0=A1=D1=80=D0=B5=D0=B7=D0=B0=200,?= =?UTF-8?q?=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D1=8B=20OQ-45/46/47?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вопросы из open-questions-proposal.md трёх сервисов (с дедупликацией), кандидаты DBML v5.14 (только как строки реестра) и находки сквозной проверки окружения. У каждой строки — репозиторий и маркер источника. OQ-45/46/47 переведены в «допущение принято и применено» с фактами seed и ORM. Co-Authored-By: Claude Sonnet 5.5 --- context/07-open-questions.md | 42 +++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/context/07-open-questions.md b/context/07-open-questions.md index 625164e..de8102d 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -50,9 +50,45 @@ | OQ-42 | «Проверка распознавания» для ОСВ (xlsx) в макете отсутствует (строки `doc-row--readonly`, `39`). Нужен ли отдельный экран проверки ОСВ (дерево строк, подсветка контрагентов) или проверка ОСВ только через сводную ОСВ (`30`)? | макет [39] | срез 2 (UI) | допущение: минимальный экран таблицы ОСВ по §1 правил без левой панели исходника | | OQ-43 | Кнопка «К отчетности» неактивна у консолидаций в статусах «Инициация» и «Верификация РФ», активна у «Консолидация РФ» и «Верифицирован» (`34`). Каково правило доступности отчёта по статусу / роли? | макет [34] | срез 4, 7 (policy) | допущение: отчёт доступен с `CONSOLIDATION_RF` и далее для всех ролей с `CONSOLIDATION_VIEW`; «Инициация» — только карточка | | OQ-44 | «Добавить версию» создаёт версию сразу без модалки (`43-01`), копируя периметр; нужно ли подтверждение и выбор сценария/подхода новой версии? | макет [43-01] | срез 7 | допущение: без модалки, периметр и сценарий копируются из предыдущей версии | -| OQ-45 | Контракт `recognition-result.schema.json` допускает `data_type = NU` (строки налогового учёта в ОСВ «БУ, НУ»), а enum DBML `osv_data_type` — только BU, CURRENCY, QUANTITY, SUM. Добавлять NU в DBML (v5.14) или не хранить строки НУ? | контракт v1.0 vs DBML | срез 2 (маппинг `osv_entry`) | допущение: контракт оставляет NU; решение по хранению — до начала Среза 2; код Среза 0 не затрагивает | -| OQ-46 | Обязательны ли все ОСВ расширенного подхода (02, 03, 08, 44, 26, 91, 51, 52, 59, 80, 83, 86, 97, 001, 008, 009, 011) при `approach = EXTENDED`, или часть — «при наличии»? | ТЗ 5.1; `required_document_rule.is_mandatory` | срез 0 (seed), срез 3 (чек-лист) | допущение: все `is_mandatory = true` для EXTENDED | -| OQ-47 | Источник полного плана счетов 1С для seed `chart_of_account`: в фикстурах файла нет, «план счетов из бандла макета» не передан | `06-test-data.md` §6, `fixtures/README.md` | срез 0 (seed) | допущение: План счетов 94н (01–99, 001–011) + типовые субсчета 1С БП 3.0, упомянутые в §4/§6; остальное добавляет администратор (ARCH-004) | +| OQ-45 | Контракт `recognition-result.schema.json` допускает `data_type = NU` (строки налогового учёта в ОСВ «БУ, НУ»), а enum DBML `osv_data_type` — только BU, CURRENCY, QUANTITY, SUM. Добавлять NU в DBML (v5.14) или не хранить строки НУ? | контракт v1.0 vs DBML | срез 2 (маппинг `osv_entry`) | допущение принято и применено в Срезе 0: контракт v1.0 оставляет `NU` (в копиях схем трёх сервисов `DataType` содержит `NU`), ORM `OsvDataType` в arm-core 1:1 с DBML v5.13 (BU, CURRENCY, QUANTITY, SUM), маппинг результата в `osv_entry` — `NoopMapper`, поэтому расхождение Срез 0 не затрагивает; решение (кандидат DBML v5.14: добавить `NU` в `osv_data_type`, либо не хранить строки НУ) — до начала Среза 2; источник: arm-core, предложения, раздел C | +| OQ-46 | Обязательны ли все ОСВ расширенного подхода (02, 03, 08, 44, 26, 91, 51, 52, 59, 80, 83, 86, 97, 001, 008, 009, 011) при `approach = EXTENDED`, или часть — «при наличии»? | ТЗ 5.1; `required_document_rule.is_mandatory` | срез 0 (seed), срез 3 (чек-лист) | допущение принято и применено в Срезе 0: все 27 строк EXTENDED в seed `arm-core/api/seed/core/required_document_rules.yaml` (маркер `ASSUMPTION(OQ-46)`) — `is_mandatory = true`; BASIC — 10 строк, все обязательные; смена правила — правка одного yaml; вопрос к методологам остаётся открытым до Среза 3 (чек-лист); источник: arm-core, предложения, раздел A | +| OQ-47 | Источник полного плана счетов 1С для seed `chart_of_account`: в фикстурах файла нет, «план счетов из бандла макета» не передан | `06-test-data.md` §6, `fixtures/README.md` | срез 0 (seed) | допущение принято и применено в Срезе 0: seed `arm-core/api/seed/core/chart_of_accounts.csv` — 122 строки (62 синтетических счёта Плана счетов 94н, 11 забалансовых, 49 субсчетов и подсубсчетов 1С БП 3.0, упомянутых в §4/§6); названия нестандартных субсчетов (66/67 `.06/.22/.24/.26`, 76.07.x, 76.08/.28/.38) выведены по их роли в §4 правил, а не взяты из справочника 1С; названия-заглушки заменить по справочнику 1С клиента, остальное добавляет администратор (ARCH-004); источник: arm-core, предложения, раздел A | +| OQ-48 | Правило элиминации № 1 (§4), сторона Б «76.07.1 − 76.07.5 − 76.07.9»: 76.07.5 и 76.07.9 не потомки 76.07.1, поэтому «вычитание» чисто информативно. Так и задумано? | `05-domain-rules.md` §4 (правило 1); seed `elimination_rules_v1.yaml`; arm-core: предл. 1 (T14.1) | срез 5 (пары ВГО) | допущение принято: исключения 76.07.5/76.07.9 на стороне Б хранятся как `is_excluded` и не влияют на сумму (76.07.1 их не содержит); алгоритм на них не опирается; подтвердить с методологами | +| OQ-49 | Правило № 1, сторона А: резервный счёт 01 лежит в одном списке с 03 и 76.07.9 — приоритета нет. Как выбирать «03, а если у А нет ОСВ 03 — 01»? Что значит «результат в строку ППА» (см. OQ-11)? | `05-domain-rules.md` §4 (правило 1: «01 — резервный»); seed `elimination_rules_v1.yaml`; arm-core: предл. 2 (T14.2) | срез 5–6 | допущение принято: в seed `accounts: ["03", "76.07.9", "01"]` без порядка; fallback «берём 01, если нет ОСВ 03» — в `comment` правила; логику приоритета реализует срез 5, до решения она не реализуется | +| OQ-50 | Составные коды статей КФО `1450/1550` (правило 1) и `1310/1350` (правило 7, гудвилл) хранятся в `elimination_rule.article_2_line` одной строкой с «/». Как их потреблять: делить по «/» и подбирать строку, которая есть в ББ Б, или хранить две строки? | `05-domain-rules.md` §4 (правила 1, 7); seed `elimination_rules_v1.yaml`; arm-core: предл. 3 (T14.3) | срез 5–6; кандидат DBML v5.14 (note колонки) | допущение принято: хранится как в §4, потребитель делит по «/»; предложение — зафиксировать это в note `article_2_line` в DBML v5.14 (версию не создавали) | +| OQ-51 | `required_document_rule`: уникальный индекс `(approach, document_type, account_id)` при nullable `account_id` допускает дубли строк для не-ОСВ документов (NULL в PostgreSQL различны). Добавить `NULLS NOT DISTINCT`? | DBML v5.13 (`required_document_rule`, Indexes); arm-core: предл. 4 (T8.3) | срез 3 (чек-лист); кандидат DBML v5.14 | допущение принято: ORM и миграция 1:1 с DBML v5.13 (без `NULLS NOT DISTINCT`); предложение v5.14: `[unique, note: 'NULLS NOT DISTINCT']`, как у `document` | +| OQ-52 | Контракт v1.0, `recognition-result-message`: `schema_version` принимает только `1.x` (`^1\.\d+$`), а `result_schema_version` — любой MAJOR (`^\d+\.\d+$`). Намеренно (файл результата версионируется отдельно) или небрежность? | `contracts/recognition-result-message.schema.json`; arm-core: предл. 5 (T20.7) | контракт v1.x; срез 2 (inbox, маппинг) | допущение принято: ядро повторяет схему буквально (любой MAJOR проходит валидацию); совместимость MAJOR результата проверяет маппинг Среза 2; уточнить у владельца контракта | +| OQ-53 | Контракт v1.0: схемы нигде не задают `additionalProperties: false` (кроме `columns` ОСВ). Считать ли неизвестные поля допустимыми расширениями? | `contracts/*.schema.json`; arm-core: предл. 6 (T20) | контракт v1.x; все три сервиса | допущение принято: модели ядра `extra = "ignore"` (строже схемы быть нельзя — ядро отвергнет совместимого пира); изменение — через MINOR контракта | +| OQ-54 | Параметры JWT РАИСА неизвестны: JWKS URL / публичный ключ, `iss`, `aud`, имя claim с email. Что в продуктивном контуре? Делать ли проверку `aud`/`iss` обязательной в проде? | ТЗ 8.1, `03-architecture.md` (SSO РАИСА); `core/auth.py`; arm-core: предл. 7 (T16.2) | срез 1+ (прод-конфигурация) | допущение принято: настраиваются `AUTH_JWKS_URL`/`AUTH_PUBLIC_KEY` (взаимоисключающие), `AUTH_ISSUER`, `AUTH_AUDIENCE`, `AUTH_EMAIL_CLAIM` (по умолчанию `email`); при пустых `AUTH_AUDIENCE`/`AUTH_ISSUER` `aud`/`iss` не проверяются; предложение: в проде сделать `aud` обязательным и предупреждать при старте, если пусто | +| OQ-55 | В enum `audit_action` нет действия для смены статуса задания распознавания (`recognition_job`). Добавлять ли (DBML v5.14) и нужен ли аудит обмена вообще? | DBML v5.13 `audit_action`; arm-core: предл. 8 (R-7, design #20 arm-core) | срез 2 (обмен); кандидат DBML v5.14 | допущение принято: Срез 0 не пишет `audit_event` из обмена `recognition_job`; сервис аудита (`services/audit.py`) готов и покрыт тестами; если нужно — действие в v5.14 и вызовы в `recognition_exchange.py` | +| OQ-56 | Append-only аудит обеспечен только на уровне кода (единственная точка записи, статический тест на UPDATE/DELETE по `AuditEvent`). Нужна ли защита на уровне БД (триггер или `REVOKE UPDATE, DELETE` для роли приложения)? Вопрос поднят дважды (в том числе финальным ревью ветки). | инвариант «Append-only аудит» (`../CLAUDE.md`); `services/audit.py`; arm-core: предл. 9 и дополнение к нему (задача 21) | миграции, права БД; платформа | вопрос: допущение принято на Срез 0 — только код и тесты; решение по триггеру/правам — с командой платформы до прода | +| OQ-57 | Счётчик повторных публикаций запроса (`ACK_TIMEOUT_SECONDS` × `OUTBOX_MAX_REPUBLISH`) хранится в `recognition_job.metrics.republish_count` (jsonb), колонки в DBML нет; при каждой повторной публикации `published_at` перезаписывается временем последней, время первой не сохраняется. Нужны ли колонка счётчика и время первой публикации (v5.14)? | `services/recognition_exchange.py` (design #6 arm-core); arm-core: предл. 10 (T23, T23.1); docs: O-19 (`task-4-report.md`) | срез replay; кандидат DBML v5.14 | допущение принято: счётчик в `metrics.republish_count`, `attempt` при повторе не растёт; `published_at` — время последней публикации (`dlq` через (`OUTBOX_MAX_REPUBLISH`+1)×`ACK_TIMEOUT_SECONDS` ≈ 120 с, описано в `dev/README.md`); при будущем replay счётчик и `reply_deadline` надо сбрасывать | +| OQ-58 | Семантика сроков и ошибок обмена в ядре: (а) `reply_deadline` по умолчанию = `now + 5 мин` для обоих pipeline (`tabular`, `image`), если у задания NULL; в DBML срок есть только для OCR («OCR — 5 мин»), для парсера значения нет; (б) при невозможности собрать запрос по контракту задание уходит в `error` / `INVALID_REQUEST`; (в) при `ACK_TIMEOUT × (MAX_REPUBLISH+1) ≥ 5 мин` статус `dlq` недостижим (раньше сработает `timeout`). Верно ли? Какой срок у `tabular-parser`? | DBML `recognition_job.reply_deadline` note; `recognition_exchange.py` (`DEFAULT_REPLY_TIMEOUT`); arm-core: предл. 11 и дополнение (T23.4) | срез 2–3; настройка таймаутов | допущение принято: как описано (дефолты 30 с × 3 укладываются в 5 мин); сроки по pipeline уточнить с командами парсера и шлюза | +| OQ-59 | Email пользователя сопоставляется с `app_user.email` по `lower(email)`; при двух записях, различающихся регистром, — 403 (fail-closed). Добавить `CHECK`/уникальный индекс по `lower(email)` в DBML? | `core/auth.py`, `api/dependencies/auth.py`; arm-core: предл. 12 (R-17, design #29 arm-core) | срез 1 (админ-экран); кандидат DBML v5.14 | допущение принято: уникальность `email` регистрозависимая (как в DBML); регистронезависимая — в v5.14 | +| OQ-60 | Seed `--dev` не защищён флагом `DEBUG`: создаёт dev-пользователей (в том числе `ADMIN`) на любой БД, на которую указывает `.env`, и перезаписывает существующего `app_user` с совпадающим email. Нужна ли защита? | arm-core `scripts/seed.py`; arm-core: предл. 13 (T15.2, T15.4) | срез 0 (процесс); прод-развёртывание | допущение принято: спека защиты не требует, `--dev` помечен «только для разработки» (в compose запускается `arm-core-migrate`); предложение — отказ при `DEBUG=false` или явный флаг подтверждения | +| OQ-61 | Неверный метод на существующий API-маршрут (например `POST /api/v1/me`) отдаёт 404 вместо 405: SPA-монтирование на `/` перехватывает частичное совпадение, а `/api/*` в нём принудительно 404. Приемлемо? | arm-core `app.py` (`SPAStaticFiles`); arm-core: предл. 14 (T18.2) | API-контракт (клиенты, фронт) | допущение принято: оставлено как есть (честный 404 для несуществующих путей важнее 405); при необходимости — отдельная правка | +| OQ-62 | В образе `arm-core` нет фронта (`Dockerfile` не собирает `web/`), `GET /` отдаёт 307 на `//` и затем 404; редирект на `//` похож на дефект монтирования SPA при `ROOT_PATH=/`. Собирать `web/` в образ в Срезе 0/1 и чинить редирект? | arm-core `Dockerfile`, `app.py`; docs: O-10 (`task-3-report.md`, `dev/README.md`) | срез 0 (compose), срез 1 (фронт) | допущение принято: README фиксирует отсутствие UI в образе; редирект на `//` не исследовался (сервис не правился) — follow-up arm-core | +| OQ-63 | `web/package-lock.json`: шаблон lock не содержит, `raisa-*` из Nexus — lock вне банка отличался бы; в Срезе 0 lock в `.gitignore`. `npm install` сообщает 6 high (npm audit) в зависимостях шаблона web. Коммитить ли lock; кто обновляет зависимости? | arm-core `web/`; arm-core: предл. 15 (T25.1, R-23, design #35 arm-core) | платформа, фронт; до прода | допущение принято: lock не в репозитории, зависимости не менялись; решение — с платформенной командой (Nexus, политика audit) | +| OQ-64 | Аудит `LOGIN` / `LOGIN_FAILED` (действия есть в `audit_action`) для JWT-аутентификации: в каком срезе и что считать «входом» — первый запрос с токеном, каждый запрос, отказ проверки токена? | DBML `audit_action`; `core/auth.py`; arm-core: предл. 16 (финальное ревью) | срез 1 (вход, админ-экран); аудит | допущение принято: в Срезе 0 вход не аудируется; сервис аудита готов | +| OQ-65 | Шаблонные CORS `allow_origins=["*"]` + `allow_credentials=True` и `ProxyHeadersMiddleware(trusted_hosts=["*"])`: в проде любой клиент может подставить `X-Forwarded-For`, и `audit_event.ip_address` недостоверен. Какие origins и доверенные прокси на платформе? | arm-core `app.py` (шаблон команды); arm-core: предл. 17 (финальное ревью) | прод-конфигурация; достоверность аудита | допущение принято: оставлено как в шаблоне; перечни origins и доверенных прокси — с платформенной командой до прода | +| OQ-66 | `/docs` и `/openapi.json` FastAPI открыты без аутентификации, в том числе в проде. Закрывать их (выключать при `DEBUG=false`) или оставить? | arm-core `app.py` (шаблон); arm-core: предл. 18 (финальное ревью) | прод-конфигурация; безопасность | допущение принято: открыты, как в шаблоне; решение — с платформой и ИБ | +| OQ-67 | Inbox не обновляет `stored_file.status` (`UPLOADED → PROCESSING → PROCESSED / ERROR`) и `stored_file.current_recognition_job_id` по результату задания: после результата файл остаётся `UPLOADED`, `current_recognition_job_id` пуст. Кто и когда это делает в Срезе 2 — inbox или маппер, в одной транзакции? | DBML `stored_file`, `file_status`; `recognition_exchange.py`; arm-core: предл. 19 (финальное ревью); docs: O-18 (`task-4-report.md`) | срез 2 (загрузка и маппинг) | допущение принято: в Срезе 0 (`NoopMapper`) файл остаётся `UPLOADED`, в `dev/README.md` записано как «не покрывается»; решение — при проектировании маппера Среза 2 | +| OQ-68 | Конверт `error` у `scan-gateway` публикуется без `metrics`, поэтому `recognition_job.metrics` = NULL (у `tabular-parser` в `metrics` есть `duration_ms`). Требовать ли `metrics` в конверте ошибки и единые поля метрик у обоих сервисов? | scan-gateway `core/worker.py`; docs: O-18 (`task-4-report.md`, сквозная проверка) | срез 2–3 (наблюдаемость, контракт v1.x) | допущение принято: `metrics` в контракте необязательны, ядро хранит NULL; требование — решение владельца контракта | +| OQ-69 | Обработчик `recognition.result` выполняется последовательно в callback подписки NATS; длинный маппер Среза 2 задержит следующие сообщения, ack сервиса не успеет за `ACK_TIMEOUT_SECONDS` (30 с) — ложный republish, затем `dlq`. Предложение: развести быстрый inbox (фиксация сообщения и статуса) и отдельный цикл маппинга результата. | arm-core `core/nats.py` (`subscribe_results`), `recognition_exchange.py` (`handle_message`, `ResultMapper`); arm-core: предл. 20 (финальное ревью) | срез 2 (маппинг); архитектура обмена (D-6) | допущение принято: в Срезе 0 `NoopMapper` — проблема не проявляется; решение — до реализации маппера Среза 2 | +| OQ-70 | Перед чтением файла результата маппер должен проверять, что ключ начинается с `results/{file_id}/{job_id}/`, а бакет — бакет роли `results` (иначе сервис может «подсунуть» чужой объект). Закрепить это в контракте и `03-architecture.md`? | контракт `recognition-result-message` (`result_s3`); arm-core `_register_result_object` (`recognition_exchange.py`); arm-core: предл. 21 (финальное ревью) | срез 2 (маппинг); контракт v1.x | допущение принято: в Срезе 0 результат только регистрируется в `s3_object`, не читается; проверку реализует маппер Среза 2 | +| OQ-71 | Запрос без читаемых `job_id` / `file_id` / `attempt` остаётся без ответа (конверт собрать нельзя): сервис логирует ошибку и отбрасывает запрос, ядро закрывает задание `dlq` / `NO_ACK` примерно через 120 с. Расходятся и реализации: при нечитаемом `attempt` `tabular-parser` берёт его из заголовка `Nats-Msg-Id` запроса или ничего не отправляет, `scan-gateway` подставляет `attempt = 1` и публикует конверт. Оставить так, выровнять реализации или сделать `file_id` необязательным в конверте `error` (MINOR 1.1)? | `recognition-contract` spec, сценарий «Нечитаемый запрос»; tabular-parser: P-1 (задача 4.3); docs: O-24 (= O-1 `task-5-report.md`), design #35 | контракт v1.x; срез 2–3 | допущение принято: молчание сервиса (design #35); реализации выровнять в Срезе 2/3, влияние только на битые запросы | +| OQ-72 | Ack при несовместимой версии запроса: ранняя формулировка контракта и `03-architecture.md` требовали «`SCHEMA_MISMATCH` и ack», оба сервиса публикуют `error SCHEMA_MISMATCH` без ack (ядро принимает результат из `published`, ack перед ним ничего не добавляет). Спека и документ приведены к поведению сервисов — подтвердить. | tabular-parser: P-2; scan-gateway: п. 2 таблицы предложений (T12.13, R-16); docs: design #34 | контракт v1.x; срез 2 | допущение принято: без ack, результат `SCHEMA_MISMATCH` закрывает задание (design #34); если ядро начнёт требовать ack перед результатом — правка одной строки в воркерах и спеке | +| OQ-73 | Повторная попытка (`attempt + 1`) того же `job_id` после ошибки или результата: оба сервиса дедуплицируют по `job_id` (LRU `DONE_CACHE_SIZE`), поэтому replay с новым `attempt` получает только ack и не обрабатывается, на единственном инстансе задание повисает до `reply_deadline` → `timeout`. По `03-architecture.md` replay — «сброс в `queued` с `attempt + 1`». Сделать ключ дедупликации `(job_id, attempt)`; повтор во время обработки — только ack? | tabular-parser: P-3 (задача 4.3); scan-gateway: п. 17 таблицы предложений (I-3, R-17); `03-architecture.md` (replay) | срез 3+ (replay); контракт v1.x | вопрос: допущение принято на Срез 0 — дедуп по `job_id`, ядро повторяет только тем же `attempt`; при одобрении — одна строка в воркере (ключ LRU) | +| OQ-74 | `PUBLISH_RETRIES` = одна первая попытка + до 3 повторов (всего до 4) с тем же `Nats-Msg-Id`; у `scan-gateway` тип `PositiveInt` (повторы нельзя отключить настройкой), у `tabular-parser` `NonNegativeInt` (0 допустим); паузы 0,5 с и 2,0 с. Согласовать единую семантику (повторы или всего попыток) и тип? | scan-gateway: п. 1 таблицы предложений (R-13, T11.1); docs: O-25 (= O-2 `task-5-report.md`), design #36 | контракт v1.x; настройки сервисов | допущение принято: «1 + 3 повтора» (design #36), расхождение «3 против 4» обмен не ломает (дедуп по `Nats-Msg-Id`); выровнять тип — косметика конфигурации | +| OQ-75 | `reply_deadline` проверяет только `scan-gateway` (`error TIMEOUT` до отправки в ИС и потолок ожидания ответа); `tabular-parser` обрабатывает просроченный запрос. Пример `contracts/examples/request-image.json` имеет `reply_deadline` в прошлом (`2026-10-03T12:45:00Z`) и даёт `TIMEOUT` при ручном прогоне. Единая обязанность сервисов и свежая дата примера? | scan-gateway: п. 12 таблицы предложений (O-e); docs: O-26 (= O-3 `task-5-report.md`), design #37 | контракт v1.x; срез 2 | допущение принято: срок — дело сервиса, пример — иллюстрация формы (оговорка в spec и `03-architecture.md`, design #37); обновить дату можно одним PR с пересинхронизацией копий в трёх репозиториях | +| OQ-76 | Обе реальные фикстуры `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС (`error.code = INVALID_DOCUMENT`, пустой `table_structure`). Успешный путь `image` (`done` / `needs_check`, файл в `results/…`, `s3_object` с `bucket_role = results`) сквозной проверкой не покрыт; в `scan-gateway` проверяется только синтетическим ответом. Нужны образцы успешных ответов Content AI. | scan-gateway: п. 3 таблицы предложений (O-1, R-10, design #32); docs: O-15 (`task-4-report.md`), design #31 | срез 3 (OQ-13/14, нормализатор); fixtures | вопрос: допущение принято на Срез 0 — успех проверяется синтетическим ответом, `NullNormalizer` пропускает ошибку ИС; образцы запросить у владельцев ИС Сканирование | +| OQ-77 | §11 `05-domain-rules.md` описывает `data_ocr[]` (список), а в фикстурах `data_ocr` — объект с полями `label/organization/inn/units/date/table_structure`. Какой формат фактический на стенде ИС (связано с OQ-13, OQ-14)? | `05-domain-rules.md` §11; scan-gateway: п. 4 таблицы предложений (O-1, T3.2) | срез 3 (нормализатор) | вопрос: допущение принято на Срез 0 — формат не интерпретируется; решение нужно до нормализатора Среза 3 | +| OQ-78 | Шлюз читает ровно `//.json` (спека); старый код читал все файлы каталога `Output//`. Какие файлы и когда реальная ИС кладёт в `Output`, в каком порядке (момент готовности)? | scan-gateway: п. 5 таблицы предложений (T8.5) | срез 3 (стенд ИС) | вопрос: допущение принято на Срез 0 — имя ответа по спеке; проверить на стенде ИС | +| OQ-79 | Временный файл `.<имя>.part` лежит в `Input` (атомарная запись `.part` + rename): реальная ИС без маски может забрать недописанный файл. Уточнить у владельцев ИС маску и поведение или перенести временный файл в служебный подкаталог рядом с `Input`? | scan-gateway: п. 6 таблицы предложений (R-5, T7/T8) | срез 3 (стенд ИС) | вопрос: допущение принято на Срез 0 — `.part` + rename в `Input`; вариант с подкаталогом `Input/.tmp` или корнем — после ответа владельцев ИС | +| 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 (`task-1-report.md`), O-4 и O-5 (`task-2-report.md`), `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 (`task-1-report.md`), `dev/checks.md`; spec `quality-checks` | срез 0 (процесс), все сервисы | допущение принято: расхождение описано в `dev/checks.md`; правка тестов и Makefile в scan-gateway и tabular-parser — follow-up человеку (сервисы не правились) | +| OQ-82 | `.dockerignore` в `arm-core` не исключает `api/.env.test`: файл попадает в образ (у `tabular-parser` и `scan-gateway` исключён). Исправлять? | arm-core `.dockerignore`; docs: T3.2 (`task-3-report.md`) | срез 0 (образы), прод-сборка | допущение принято: безвредно (в файле стандартные dev-учётные данные compose, не секреты), в Срезе 0 не правилось; добавить исключение в arm-core — follow-up | +| 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 (`task-3-report.md`; подтверждено ревью для tabular-parser и scan-gateway), `dev/README.md` | срез 0 (процесс), интеграционные тесты | допущение принято: в `dev/README.md` предупреждение и команда `stop` для контейнеров профиля; не гонять `make test-integration` двух сервисов одновременно | Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения; `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. -- 2.47.2 From 55118120ad98548f339dc1a4c51ac61e4c8852d7 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 06:52:50 +0400 Subject: [PATCH 50/68] =?UTF-8?q?docs(openspec):=20design.md=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=2039=E2=80=9342?= =?UTF-8?q?=20(=D1=80=D1=83=D0=BB=D0=B8=D0=BD=D0=B3=D0=B8=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D1=87=D0=B8=206.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- openspec/changes/slice-0-skeleton/design.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index 2eefe17..83a420e 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -170,3 +170,7 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 36 | 2026-10-04 | `PUBLISH_RETRIES` в спеке (новое требование «Повторы публикации ответов сервисом») = **одна первая попытка + до 3 повторов** (всего не более 4 попыток) с тем же `Nats-Msg-Id` и паузой между попытками; исчерпаны — error-лог, задание закрывает ядро (`reply_deadline`). Пауза — деталь реализации, в спеку не включена | значения в коде: `scan-gateway/api/src/core/config.py:63` (`PUBLISH_RETRIES = 3`, «повторы после первой попытки (всего до 4)») и `core/nats.py:245–256` (`attempts_total = retry_count + 1`); `tabular-parser/api/src/core/config.py:49` (`PUBLISH_RETRIES = 3`, `NonNegativeInt`) и `core/nats.py:306` (`attempts = publish_retries + 1`); паузы различаются (2,0 с и 0,5 с), семантика одна | если ядро/сервис договорятся считать `PUBLISH_RETRIES` общим числом попыток, расхождение «3 против 4» не ломает обмен (дедуп по `Nats-Msg-Id`); правка — одна фраза в спеке | | 37 | 2026-10-04 | Пример `contracts/examples/request-image.json` с `reply_deadline` в прошлом **не меняется**; в `spec.md` (сценарий «Примеры контракта — иллюстрации») и `03-architecture.md` записано, что примеры — иллюстрации формы, схема проверяет у `reply_deadline` лишь формат `date-time`, а срок проверяет сервис (`scan-gateway` — `error TIMEOUT`, `scan-gateway/api/src/core/worker.py:469–475`; `tabular-parser` срок не проверяет: `reply_deadline` в его воркере не используется) | копии `examples/` в сервисах сверяются побайтно тестами идентичности: правка примера = пересинхронизация трёх репозиториев ради документальной оговорки; `validate_contracts.py` проходит (rc 0, 8 примеров) | если кто-то возьмёт `request-image.json` как готовое задание для `scan-gateway`, получит `TIMEOUT` — это зафиксировано оговоркой; обновить дату позже можно одним PR с пересинхронизацией | | 38 | 2026-10-04 | `06-test-data.md` §6 и `09-delivery-plan.md` (Срез 0) приведены к факту: `fixtures/directories/` — только тестовые ЮЛ/ГК (сейчас пустой `.gitkeep`, наполнение — Срез 1); seed продукта лежит в `arm-core/api/seed/core/` (+ `seed/dev/` только для `--dev`); в плане Среза 0 добавлена ссылка на `dev/README.md` («Профиль `services`», «Проверка окружения»); `00-workspace-layout.md` не менялся (неактуальных фактов нет) | перечень файлов seed и число ролей/прав проверены по `arm-core/api/seed/` (7 ролей, 16 прав, 122 строки плана счетов); решение D-1, compose и проверки вместо CI в `09` уже были верны | только неточная ссылка в документах; правится правкой текста | +| 39 | 2026-10-04 | Реестр `07-open-questions.md`: 36 новых строк OQ-48…OQ-83 вместо ~60 пунктов трёх `open-questions-proposal.md` и находок контроллера — пересекающиеся пункты слиты в одну строку с перечнем источников: PUBLISH_RETRIES (scan-gateway п. 1 + O-25), `reply_deadline` (п. 12 + O-26), нечитаемый запрос (tabular P-1 + O-24), ack при `SCHEMA_MISMATCH` (tabular P-2 + scan-gateway п. 2), `attempt + 1` (tabular P-3 + scan-gateway п. 17), фикстуры OCR (п. 3 + O-15), `make up`/`check-full` (O-1, O-2, O-4, O-5 + scan-gateway п. 18), общий NATS (п. 11 + O-11), `stored_file.status` (arm-core предл. 19 + O-18), `republish_count` (предл. 10 + O-19), append-only аудит (предл. 9 + дополнение), срок ответа (предл. 11 + дополнение) | один вопрос — одна строка: иначе человек решает одно и то же дважды, а реестр несёт противоречивые статусы; перечень источников (репозиторий + маркер) сохраняет прослеживаемость, маркеры не менялись | если слияние оказалось неудачным, строка делится на две с новым номером; нумерация OQ-48…OQ-83 сплошная, на неё могут ссылаться только новые документы | +| 40 | 2026-10-04 | Статусы OQ-45/46/47 обновлены в существующих строках, не продублированы: все три — «допущение принято и применено в Срезе 0» с фактами (NU: контракт оставляет, ORM `OsvDataType` 1:1 с DBML v5.13, `NoopMapper`; OQ-46: 27 строк EXTENDED + 10 BASIC в seed, `is_mandatory = true`; OQ-47: 122 строки плана счетов, названия нестандартных субсчетов выведены по роли в §4). Кандидаты DBML v5.14 (`NU`, `NULLS NOT DISTINCT`, note `article_2_line`, действие `audit_action`, счётчик republish, `lower(email)`) внесены только строками реестра, файл `data-model-v5.13.dbml` не менялся | факты проверены чтением `arm-core/api/seed/core/`, `api/src/models/enums.py` и `contract/common.py`; создание DBML-версии — отдельное решение человека (CLAUDE.md: версия не меняется задним числом) | если человек решит иначе (например, хранить НУ), статус строки меняется правкой реестра, код Среза 0 не затронут | +| 41 | 2026-10-04 | В реестр **не** внесены, а оставлены в отчёте задачи 6 (кандидаты в follow-up issue при закрытии): scan-gateway п. 7 (рестарт NATS не автоматизирован), п. 8 (`stop()` не публикует конверты принятым заданиям), п. 9 (`/healthcheck` всегда 200), п. 13 (`DEBUG` не используется), п. 14 (`ResourceWarning` aiohttp), п. 15 (SMB без `connection_timeout`); tabular P-4, scan-gateway п. 10 и 16 (порты, `SCAN_GATEWAY_PORT`, устаревший README) — закрыты задачами 3–5 и `dev/README.md`; O-22 и T3.7/O-20 — процессные пункты задачи 7 | это инженерные доработки или уже исправленные расхождения документов, вопроса к человеку/аналитикам в них нет; реестр остаётся списком решений, которые принимает человек | пункты могут потеряться, если follow-up issue не будет заведена (чеклист закрытия, п. 6): они перечислены в отчёте с маркерами | +| 42 | 2026-10-04 | Формулировка статусов в новых строках: «допущение принято: …» — в коде/спеке Среза 0 действует рабочее допущение, вопрос открыт для подтверждения; «вопрос: …» — человек должен принять решение, допущение Среза 0 указано после двоеточия (аудит на уровне БД, `attempt + 1`, образцы ответов ИС, формат `data_ocr`, имя файла ответа, `.part` в `Input`) | правило файла: агент вопросы не закрывает, а помечает допущение; слово «закрыт» нигде не использовано | различение статусов — рекомендация, не контракт; при несогласии префикс правится в реестре | -- 2.47.2 From c32c90419736d4a7e81789ffcb03b351e814c9d0 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:02:15 +0400 Subject: [PATCH 51/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=206.2?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md index db0002f..8949bf1 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md @@ -37,7 +37,7 @@ ### Task 6: [6.2] [docs] `07-open-questions.md` -- [ ] [docs] `07-open-questions.md`: OQ по итогам Среза 0 (контракт `data_type = NU` vs enum `osv_data_type`; обязательность ОСВ расширенного подхода; источник полного плана счетов 1С) и расхождения макета из задачи B; статусы допущений +- [x] [docs] `07-open-questions.md`: OQ по итогам Среза 0 (контракт `data_type = NU` vs enum `osv_data_type`; обязательность ОСВ расширенного подхода; источник полного плана счетов 1С) и расхождения макета из задачи B; статусы допущений ### Task 7: [6.3] [docs] Итоговая проверка diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 369b734..0a2f271 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -69,7 +69,7 @@ - [x] 6.1 [docs] `06-test-data.md` §6: `fixtures/directories/` — только тестовые ЮЛ/ГК (Срез 1); план счетов, правила элиминации, права — `arm-core/api/seed/`; `09-delivery-plan.md` Срез 0: решение D-1 по пакету контрактов, ссылка на `docs/dev/`, проверки вместо CI -- [ ] 6.2 [docs] `07-open-questions.md`: OQ по итогам Среза 0 (контракт `data_type = NU` vs enum `osv_data_type`; +- [x] 6.2 [docs] `07-open-questions.md`: OQ по итогам Среза 0 (контракт `data_type = NU` vs enum `osv_data_type`; обязательность ОСВ расширенного подхода; источник полного плана счетов 1С) и расхождения макета из задачи B; статусы допущений - [ ] 6.3 [docs] Итоговая проверка: `openspec validate slice-0-skeleton --strict` зелёный в четырёх репозиториях; ссылки -- 2.47.2 From c7c03616ce42143df9c685563d484cfddc8a1a8c Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:02:41 +0400 Subject: [PATCH 52/68] =?UTF-8?q?docs(dev):=20checks.md=20=E2=80=94=20.DEF?= =?UTF-8?q?AULT=5FGOAL=20=D0=B2=20=D1=8D=D1=82=D0=B0=D0=BB=D0=BE=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=20=D1=84=D1=80=D0=B0=D0=B3=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B5,=20--profile=20services=20=D0=B2=20down?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- dev/checks.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dev/checks.md b/dev/checks.md index 69a4ae1..f53c388 100644 --- a/dev/checks.md +++ b/dev/checks.md @@ -41,7 +41,8 @@ docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka run --rm Почему так: голый `docker compose up -d --wait` возвращает код 1 (одноразовый `minio-init` завершается кодом 0, compose считает это сбоем), поэтому `--wait` даётся со списком долгоживущих сервисов, а бакет создаётся отдельным шагом. Без бакета интеграционные тесты на свежем томе падают на `NoSuchBucket`. Целей `down`/`stop`/`restart` нет намеренно: проект -`arm-ka` общий для всех сервисов; остановка и сброс — вручную (`docker compose down`, `down -v`), см. `README.md`. +`arm-ka` общий для всех сервисов; остановка и сброс — вручную (`docker compose --profile services down`, при сбросе данных `down -v` с тем же `--profile services`, +иначе контейнеры сервисов останутся), см. `README.md`. > Эталонная реализация `make up` — в `scan-gateway`. Реализации в `arm-core` и `tabular-parser` отличаются > (см. «Известные расхождения реализаций»). @@ -76,7 +77,8 @@ docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka run --rm с которым сервис сверяется `diff`ом или который подключает напрямую: ```make -# Makefile сервиса: свои переменные и цели; include — после первой цели (иначе целью по умолчанию станет venv) +# Makefile сервиса: свои переменные и цели; цель по умолчанию задана явно, иначе ею станет первая цель include (venv) +.DEFAULT_GOAL := check MYPY_PATHS := src scripts # arm-core; по умолчанию src CHECK_FULL_STEPS := check dbml-check check-infra test-integration docker-build web-check # arm-core include ../docs/dev/Makefile.common -- 2.47.2 From 04cdbd6cc436c7d13b4fa1276bbe960eb20d7a8a Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:02:56 +0400 Subject: [PATCH 53/68] =?UTF-8?q?docs(openspec):=20design.md=20=E2=80=94?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BC=D1=8B=D0=BA=D0=B0=D1=8E=D1=89=D0=B8=D0=B9?= =?UTF-8?q?=20|=20=D1=83=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B9=2024?= =?UTF-8?q?=E2=80=9325?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- openspec/changes/slice-0-skeleton/design.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index 83a420e..e294818 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -155,8 +155,8 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 21 | 2026-10-04 | В корне репозитория `docs` добавлен `Makefile` с целями `venv`, `validate-contracts`, `validate-openspec`, `check` (сервисных целей нет); `jsonschema` ставится в `.venv` (в `.gitignore`) | дополнение контроллера к задаче 4.1: проверки контракта и спеки должны запускаться одной командой, как в сервисах; `check` = `scripts/validate_contracts.py` + `openspec validate slice-0-skeleton --strict` | удалить `Makefile` и строку `.venv/` в `.gitignore`; команды из `dev/checks.md` останутся запускаемыми вручную | | 22 | 2026-10-04 | `dev/checks.md` описывает общие цели по эталону `scan-gateway` (`up` с `--project-name arm-ka`, `--wait postgres nats minio` и `run --rm minio-init`; `check-full` как последовательность шагов), расхождения `arm-core`/`tabular-parser` и неполная поддержка `ARM_KA_DOCS`/`ARM_KA_FIXTURES` в `scan-gateway` вынесены в раздел «Известные расхождения реализаций», код сервисов не менялся | spec «Одинаковый набор целей» требует единой семантики, а фактические `Makefile` расходятся; правка сервисов вне задачи 4.1 и их PR | если эталон выбран неверно — переписать раздел `make up` в `checks.md`; расхождения останутся незакрытыми, пока сервисы не выровнены | | 23 | 2026-10-04 | В `dev/README.md` раздел «Интеграционные тесты сервисов против compose» заменён ссылкой на `checks.md`; убрано утверждение «Makefile есть только у `arm-core`» | утверждение устарело (Makefile есть у всех трёх сервисов), единый источник описания проверок — `checks.md` | вернуть старый текст раздела | -| 24 | 2026-10-04 | `dev/Makefile.common` — эталон общих целей по `scan-gateway`; различия сервисов вынесены в переменные (`MYPY_PATHS`, `IMAGE`, `COMPOSE_*`, `CHECK_FULL_STEPS`), `check-full` реализован через `$(foreach …)` с вызовом `$(MAKE)` на каждый шаг; в docs-репозитории `Makefile.common` не `include`-ится, сервисы его пока не подключают | задача 4.2 требует эталонный фрагмент и сверку `diff`ом; Makefile сервисов в Срезе 0 не правятся (код готов, PR открыты) — `include` в них можно ввести без смены поведения следующим срезом | сервисы продолжают держать копии рецептов; расхождения копий вылезают только при правке одной из них (сверка — `make -n`, см. `checks.md`) -| 25 | 2026-10-04 | Замечания T1.1–T1.4 ревью задачи 4.1 исправлены в `dev/checks.md` (fixtures и «ARM_KA_FIXTURES» — только `arm-core`, `mypy src scripts`, «Инфраструктура compose», «одинаковая семантика» заменена ссылкой на расхождения) | прямое поручение контроллера; формулировки приведены в соответствие с фактическими Makefile и тестами | неточная документация; правится одной строкой +| 24 | 2026-10-04 | `dev/Makefile.common` — эталон общих целей по `scan-gateway`; различия сервисов вынесены в переменные (`MYPY_PATHS`, `IMAGE`, `COMPOSE_*`, `CHECK_FULL_STEPS`), `check-full` реализован через `$(foreach …)` с вызовом `$(MAKE)` на каждый шаг; в docs-репозитории `Makefile.common` не `include`-ится, сервисы его пока не подключают | задача 4.2 требует эталонный фрагмент и сверку `diff`ом; Makefile сервисов в Срезе 0 не правятся (код готов, PR открыты) — `include` в них можно ввести без смены поведения следующим срезом | сервисы продолжают держать копии рецептов; расхождения копий вылезают только при правке одной из них (сверка — `make -n`, см. `checks.md`) | +| 25 | 2026-10-04 | Замечания T1.1–T1.4 ревью задачи 4.1 исправлены в `dev/checks.md` (fixtures и «ARM_KA_FIXTURES» — только `arm-core`, `mypy src scripts`, «Инфраструктура compose», «одинаковая семантика» заменена ссылкой на расхождения) | прямое поручение контроллера; формулировки приведены в соответствие с фактическими Makefile и тестами | неточная документация; правится одной строкой | | 26 | 2026-10-04 | Профиль `services` в `dev/docker-compose.yml`: переменные сервисов — общие якоря YAML (`x-service-env`, `x-arm-core-env`) с адресами внутри сети compose и `DEBUG=true`; образы собираются локально с тегами `arm-core:dev`, `tabular-parser:dev`, `scan-gateway:dev`; healthcheck контейнеров — `python -c` с чтением поля `nats` (у `scan-gateway` ещё `hotfolder`) из `/healthcheck`; порты хоста `${ARM_CORE_PORT:-8000}`/`${TABULAR_PARSER_PORT:-8001}`/`${SCAN_GATEWAY_PORT:-8002}` на `${BIND_ADDR:-127.0.0.1}`, в контейнере 8000 | spec dev-environment («`OPENBAO__*` шаблона, `DEBUG=true`»), фрагмент `scan-gateway` (`open-questions-proposal.md`) как основа; `curl` в `python:3.11-slim` нет, а `/healthcheck` всегда 200; якоря убирают тройное дублирование учётных данных MinIO/NATS | правка одного блока compose; при смене формата `/healthcheck` сервиса — правка трёх строк `test` | | 27 | 2026-10-04 | Одноразовый сервис `arm-core-migrate` (`alembic upgrade head && python -m scripts.seed --dev`, `depends_on` postgres `service_healthy`); `arm-core` ждёт его `service_completed_successfully`; выполняется при каждом `up` профиля | дополнение контроллера к задаче 5.3; без схемы `dkb` и справочников ядро не работает, оба шага идемпотентны (повторный seed — `0 / 0 / N`); `--dev` — нужны dev-пользователи для dev-токена | лишний запуск миграций при каждом `up` (~2 с); убрать `--dev` — одна строка | | 28 | 2026-10-04 | Образ каждого репозитория собирает один сервис: `build` только у `arm-core` и `scan-gateway`; `arm-core-migrate` и `scan-emulator` используют тег `arm-core:dev`/`scan-gateway:dev` с `pull_policy: never` (отступление от фрагмента `scan-gateway`, где `build` у обоих) | проверено: при `build` у двух сервисов с одним тегом compose 2.34 собирает образ дважды с разными ID, метка `com.docker.compose.image` контейнера расходится с тегом, и каждый следующий `up` пересоздаёт `arm-core-migrate` и `arm-core` (флип-флоп); с одной сборкой три `up` подряд — 0 пересозданий | `up -d scan-emulator` или `arm-core-migrate` в одиночку на машине без образа падает `No such image` — сначала `docker compose --profile services build`; вернуть `build` — две строки | -- 2.47.2 From 6000cab8921dd4d277c121031fb8bbaa629ca6e3 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:03:13 +0400 Subject: [PATCH 54/68] =?UTF-8?q?docs(dev):=20README=20=E2=80=94=20=D0=B2?= =?UTF-8?q?=D0=B5=D0=BD=D0=B2-=D1=87=D0=B5=D1=80=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BA=20=D0=B1=D0=B5=D0=B7=20=D1=81=D1=81=D1=8B=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=BD=D0=B0=205.4,=20legal=5Fentity=20=D0=B2=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B5=20=D0=BE=D0=BA=D1=80?= =?UTF-8?q?=D1=83=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- dev/README.md | 13 +++++++++---- openspec/changes/slice-0-skeleton/design.md | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/dev/README.md b/dev/README.md index ad4f10e..071c5f7 100644 --- a/dev/README.md +++ b/dev/README.md @@ -149,7 +149,7 @@ curl -s localhost:8000/api/v1/me -H "Authorization: Bearer " # email, Ручная проверка обмена `recognition_job` через NATS на поднятом профиле `services`: ядро публикует задание, сервис отвечает ack и результатом, ядро меняет статус. UI и API загрузки файлов в Срезе 0 нет, поэтому файл кладётся в MinIO -через `mc`, а строки `stored_file` / `s3_object` / `recognition_job` вставляются через `psql`. Проверено 2026-10-04 +через `mc`, а строки `legal_entity` / `s3_object` / `stored_file` / `recognition_job` вставляются через `psql`. Проверено 2026-10-04 (все три сценария ниже — фактический вывод, время UTC). Помощники (bash/zsh, из `docs/dev/`; `uuidgen` есть в macOS и util-linux): @@ -182,6 +182,10 @@ poll() { # poll <секунд> — статус задания $JOB раз в } ``` +Рецепт прогнан дословно при уже существовавшей в БД строке `legal_entity` с ИНН `7700000001`, поэтому ветка вставки +(без конфликта по `inn`) на чистой БД не выполнялась; `on conflict (inn) do nothing` рассчитан на оба случая, но первая +вставка на пустой БД отдельно не проверялась. + Ключ исходника — `uploads/{file_id}/{job_id}/{имя}` (`../context/03-architecture.md`, «Схема ключей S3»). Сообщения на шине можно смотреть параллельно: `docker run --rm --network arm-ka_default natsio/nats-box:0.17.0 nats sub -s nats://nats:4222 'recognition.>'` (печатает `Nats-Msg-Id` и тело каждого запроса, ack и результата). @@ -314,8 +318,9 @@ docker compose --profile services down -v # полный сброс: тома ## Запуск сервисов из venv без Nexus (черновик) -> Черновик: будет дописан и проверен вручную в задаче 5.4 (сквозная проверка). `make venv` и `make test-integration` -> в `tabular-parser` и `scan-gateway` будут доступны после задач этих сервисов; сейчас они есть в `arm-core`. +> Черновик: запуск сервисов из venv в Срезе 0 не проверялся (сквозная проверка задачи 5.4 шла на профиле `services`); +> шаги ниже не прогонялись целиком. Проверка из venv вынесена за пределы Среза 0 (фаза 2), отдельного вопроса в +> `07-open-questions.md` нет. `make venv` и `make test-integration` есть во всех трёх сервисах. Пакеты `raisa_*` из Nexus банка вне банка недоступны (D-10). Поэтому `make venv` ставит `requirements.txt` без строк `raisa_*` (фильтр `grep -v '^raisa_'`), код импортирует `raisa_*` условно (при `DEBUG=true`), а npm-пакеты `raisa-*` @@ -338,7 +343,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 уточняются в задаче 5.4. +`hotfolder` (шлюзу нужен `SCAN_HOTFOLDER_PATH`). Набор переменных сервисов для запуска из venv не проверялся (см. черновик выше). ## Интеграционные тесты сервисов против compose (черновик) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index e294818..15cfb4d 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -161,7 +161,7 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 27 | 2026-10-04 | Одноразовый сервис `arm-core-migrate` (`alembic upgrade head && python -m scripts.seed --dev`, `depends_on` postgres `service_healthy`); `arm-core` ждёт его `service_completed_successfully`; выполняется при каждом `up` профиля | дополнение контроллера к задаче 5.3; без схемы `dkb` и справочников ядро не работает, оба шага идемпотентны (повторный seed — `0 / 0 / N`); `--dev` — нужны dev-пользователи для dev-токена | лишний запуск миграций при каждом `up` (~2 с); убрать `--dev` — одна строка | | 28 | 2026-10-04 | Образ каждого репозитория собирает один сервис: `build` только у `arm-core` и `scan-gateway`; `arm-core-migrate` и `scan-emulator` используют тег `arm-core:dev`/`scan-gateway:dev` с `pull_policy: never` (отступление от фрагмента `scan-gateway`, где `build` у обоих) | проверено: при `build` у двух сервисов с одним тегом compose 2.34 собирает образ дважды с разными ID, метка `com.docker.compose.image` контейнера расходится с тегом, и каждый следующий `up` пересоздаёт `arm-core-migrate` и `arm-core` (флип-флоп); с одной сборкой три `up` подряд — 0 пересозданий | `up -d scan-emulator` или `arm-core-migrate` в одиночку на машине без образа падает `No such image` — сначала `docker compose --profile services build`; вернуть `build` — две строки | | 29 | 2026-10-04 | В compose у `arm-core` включён dev-эмитент токенов (`AUTH_DEV_ISSUER=true`, публичный `AUTH_DEV_SECRET` из `arm-core/api/.env.example`) | ручные проверки `/api/v1/me` и сквозной сценарий (задача 5.4) без РАИСА; маршрут существует только при `DEBUG=true`; порт хоста по умолчанию на 127.0.0.1 | при `BIND_ADDR=0.0.0.0` токен любого dev-пользователя может получить любой в сети — только dev; выключить — одна строка | -| 30 | 2026-10-04 | Сквозная проверка окружения (задача 5.4) выполняется вставкой строк `legal_entity` (синтетическое, ИНН `7700000001`, `on conflict do nothing`) / `s3_object` / `stored_file` / `recognition_job(queued)` через `psql` в контейнере `postgres` и загрузкой исходника в `uploads/{file_id}/{job_id}/{имя}` через `mc` из образа `minio-init`; рецепт — функции `dpsql`/`dmc`/`new_job`/`poll` в `dev/README.md` («Проверка окружения»); тестовые строки и объекты не удаляются | API загрузки файлов в Срезе 0 нет, тестовых скриптов `arm-core` для рабочей БД тоже (его интеграционные тесты работают во временной БД); ключ — по «Схеме ключей S3» `03-architecture.md`; рецепт проверен прогоном дословно | вставка в обход будущего API может разойтись с ним (обязательные поля `stored_file`/`document`) — рецепт придётся обновить в Срезе 1/2; мусорные строки в dev-БД — сброс `down -v` | +| 30 | 2026-10-04 | Сквозная проверка окружения (задача 5.4) выполняется вставкой строк `legal_entity` (синтетическое, ИНН `7700000001`, `on conflict do nothing`) / `s3_object` / `stored_file` / `recognition_job(queued)` через `psql` в контейнере `postgres` и загрузкой исходника в `uploads/{file_id}/{job_id}/{имя}` через `mc` из образа `minio-init`; рецепт — функции `dpsql`/`dmc`/`new_job`/`poll` в `dev/README.md` («Проверка окружения»); тестовые строки и объекты не удаляются | API загрузки файлов в Срезе 0 нет, тестовых скриптов `arm-core` для рабочей БД тоже (его интеграционные тесты работают во временной БД); ключ — по «Схеме ключей S3» `03-architecture.md`; рецепт проверен прогоном дословно, но при уже существовавшем `legal_entity` с этим ИНН: ветка вставки без конфликта на чистой БД не выполнялась | вставка в обход будущего API может разойтись с ним (обязательные поля `stored_file`/`document`) — рецепт придётся обновить в Срезе 1/2; мусорные строки в dev-БД — сброс `down -v` | | 31 | 2026-10-04 | Ожидаемый итог сценария `image` в README — `error` / `INVALID_DOCUMENT` (фактический ответ), а не `NOT_IMPLEMENTED`; файл — `ocr/aep_5.PDF` (так он назван в `fixtures`, не `aep_5.pdf`) | обе пары `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС, эмулятор отдаёт их как есть, шлюз пробрасывает код ИС; на регистрозависимой ФС `aep_5.pdf` не найдётся | успешный путь `image` (`done`/`needs_check`, `results/…`) сквозной проверкой не покрыт, пока в `fixtures/ocr` нет успешного ответа ИС | | 32 | 2026-10-04 | Замечание T3.6 проверено эмпирически: образы `scan-gateway:dev` и `arm-core:dev` пересобраны `build --no-cache` (новые ID) на работающем профиле; после пересоздания контейнеров старые ID образов в `docker image ls -a` не остались (`No such image`) | формулировку README нужно было подтвердить, а без смены ID образа compose ничего не пересоздаёт | ~1 мин сборки и загрузка зависимостей из PyPI; на окружение не влияет (образы — те же исходники) | | 33 | 2026-10-04 | В README для наблюдения за шиной предложен `natsio/nats-box:0.17.0` (`nats sub 'recognition.>'` в сети `arm-ka_default`) — разовый `docker run --rm`, не сервис compose | без него `published`/`processing` у `tabular` (десятки мс) не видны; образ доступен с Docker Hub (проверено), в compose не добавлялся, чтобы не менять состав окружения | при недоступности образа — только логи `arm-core` и метки времени задания; шаг опциональный | -- 2.47.2 From f928982058aa1ef33810e805a71cf6103bd0f856 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:03:23 +0400 Subject: [PATCH 55/68] =?UTF-8?q?docs(dev):=20README=20=E2=80=94=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82=D0=B0=20(jsonschema,=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20rfc3339-validator)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- dev/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev/README.md b/dev/README.md index 071c5f7..ca8b601 100644 --- a/dev/README.md +++ b/dev/README.md @@ -377,7 +377,9 @@ postgres nats minio`, затем `make test-integration` (или `make check-ful Из корня репозитория `docs`: `python scripts/validate_contracts.py` — валидирует примеры в [`contracts/examples/`](../contracts/examples/) тремя схемами из [`contracts/`](../contracts/) (3 схемы, 8 примеров; -нужен `jsonschema`). Или `make venv && make check` (ещё `openspec validate`), см. [`checks.md`](checks.md). +нужен только пакет `jsonschema`: формат `date-time` скрипт проверяет собственной функцией, поэтому `rfc3339-validator` +не требуется). Или `make venv && make check` (`make venv` создаёт `.venv` и ставит `jsonschema`; `check` использует этот +`.venv` и ещё запускает `openspec validate`), см. [`checks.md`](checks.md). ## Примечания -- 2.47.2 From 172d30a8b139e1f38c12dbe7ce49c286db95bf31 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:03:35 +0400 Subject: [PATCH 56/68] =?UTF-8?q?docs(openspec):=20=D1=81=D0=BF=D0=B5?= =?UTF-8?q?=D0=BA=D0=B0=20recognition-contract=20=E2=80=94=20ack=20=D1=82?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BD=D0=B0=20=D0=BA=D0=BE?= =?UTF-8?q?=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D1=8B=D0=B9=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81,=20=D0=B8=D1=81=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=20attempt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../slice-0-skeleton/specs/recognition-contract/spec.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md b/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md index fbc0f56..cc90ead 100644 --- a/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md +++ b/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md @@ -22,7 +22,8 @@ #### Scenario: Сообщение не по схеме - **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`), но из тела извлекаются - корректные `job_id` и `file_id` (и `attempt` для `Nats-Msg-Id`) + корректные `job_id` и `file_id`; `attempt` для `Nats-Msg-Id` берётся из тела или из заголовка `Nats-Msg-Id` запроса, + а если его нет ни там, ни там — `scan-gateway` подставляет 1, `tabular-parser` не отвечает - **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` **без предшествующего ack `processing`** и не падает @@ -45,7 +46,7 @@ сервиса (`scan-gateway` отвечает `error TIMEOUT`, если `reply_deadline` истёк до отправки файла в ИС Сканирование) ### Requirement: Подтверждение взятия в работу (ack без JetStream) -Получив запрос, сервис SHALL немедленно, до чтения файла из S3, опубликовать в `recognition.result` конверт со +Получив корректный запрос (прошедший проверку схемы и `MAJOR`), сервис SHALL немедленно, до чтения файла из S3, опубликовать в `recognition.result` конверт со `status = processing`, `job_id`, `file_id`, `service {name, version}` и заголовком `Nats-Msg-Id = ack::`. Ядро SHALL перевести задание `published → processing` и записать `started_at`, `service_name`, `service_version`. Если ack не получен за `ACK_TIMEOUT_SECONDS` (по умолчанию 30), ядро SHALL опубликовать запрос повторно с тем же @@ -105,7 +106,8 @@ v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `f - **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH` **без ack `processing`**: ответом на запрос служит сам финальный конверт, ядро закрывает им задание (`published → error`), и повторной доставки запроса не будет. Конверт публикуется только если из тела извлекаются корректные `job_id` и `file_id`; иначе — как для нечитаемого - запроса (ответа нет, ядро закроет задание по `NO_ACK` / `dlq`) + запроса (ответа нет, ядро закроет задание по `NO_ACK` / `dlq`). `attempt` для `Nats-Msg-Id` берётся из тела или из + заголовка `Nats-Msg-Id` запроса; если его нет — `scan-gateway` подставляет 1, `tabular-parser` не отвечает ### Requirement: Идемпотентность результатов без брокера Публикующий SHALL ставить заголовок `Nats-Msg-Id` на каждое сообщение (запрос `:`, ack -- 2.47.2 From cdfac6c74bdea2d9ed74d905a5213cf581d969fa Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:03:47 +0400 Subject: [PATCH 57/68] =?UTF-8?q?docs:=2006-test-data=20=E2=80=94=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B8=D1=81=D0=BA=20=D0=BF=D0=B0=D1=80=D1=8B=20=D1=8D?= =?UTF-8?q?=D0=BC=D1=83=D0=BB=D1=8F=D1=82=D0=BE=D1=80=D0=BE=D0=BC,=20desig?= =?UTF-8?q?n=20#36=20=E2=80=94=20=D0=BF=D0=B0=D1=83=D0=B7=D1=8B=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- context/06-test-data.md | 7 +++++-- openspec/changes/slice-0-skeleton/design.md | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/context/06-test-data.md b/context/06-test-data.md index 1256054..3c3916b 100644 --- a/context/06-test-data.md +++ b/context/06-test-data.md @@ -147,8 +147,11 @@ fixtures/ # рекомендуемые формы банка, выгрузки 1С — собрать у аналитиков ocr/ aep_N.pdf|PDF + aep_N.json # пары вход/выход ИС Сканирование (эмулятор ИС - # в scan-gateway находит пару по имени, регистр - # расширения значим на Linux) + # в scan-gateway ищет `.json` по имени входного + # файла без расширения, регистр его расширения для + # эмулятора неважен; важен путь самого входного файла + # `aep_5.PDF`: на регистрозависимой ФС (Linux) он ищется + # буквально) golden/ Консолидация_Группа РБ.xlsx # эталон КФО по датам expected_osv_parse/*.json # ожидаемый результат парсера (зафиксировать после ревью) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index 15cfb4d..75d5aea 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -167,7 +167,7 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 33 | 2026-10-04 | В README для наблюдения за шиной предложен `natsio/nats-box:0.17.0` (`nats sub 'recognition.>'` в сети `arm-ka_default`) — разовый `docker run --rm`, не сервис compose | без него `published`/`processing` у `tabular` (десятки мс) не видны; образ доступен с Docker Hub (проверено), в compose не добавлялся, чтобы не менять состав окружения | при недоступности образа — только логи `arm-core` и метки времени задания; шаг опциональный | | 34 | 2026-10-04 | Правим спеку и документ под фактическое поведение сервисов, а не код: при неизвестном MAJOR `schema_version` (и при запросе не по схеме) сервис публикует `error SCHEMA_MISMATCH` **без** отдельного ack `processing`; формулировка «и подтверждает ack'ом» (T12.13) из `spec.md` (сценарий «Несовместимая версия запроса») и `03-architecture.md` убрана. Конверт публикуется, только если извлекаются корректные `job_id`/`file_id`, и не публикуется для `job_id`, уже находящегося в работе/обработанного | `tabular-parser/api/src/core/worker.py` (`_reject`: «`SCHEMA_MISMATCH` без ack», строки 242–275) и `scan-gateway/api/src/core/worker.py` (`_reject`, R-16: строки 288–321) ведут себя одинаково; финальный конверт сам закрывает задание в ядре (`published → error` допустим, `recognition_exchange.py`: результат принимается из `queued/published/processing`), ack перед ним ничего не добавляет, а `Nats-Msg-Id` ack и результата разные; конверт для `job_id` в работе совпал бы по `result::` с настоящим результатом | если ядро когда-нибудь начнёт требовать ack перед результатом, задание с `SCHEMA_MISMATCH` останется в `published` до повторов и `dlq`; правка — одна строка в воркерах и спеке | | 35 | 2026-10-04 | Запрос, из которого нельзя извлечь `job_id` (не JSON, не объект, нет/не uuid `job_id`), остаётся без ответа: сервис пишет лог и отбрасывает его; ядро закрывает задание по `NO_ACK` / `dlq` после `OUTBOX_MAX_REPUBLISH` повторов. Записано в `spec.md` (новый сценарий «Нечитаемый запрос») и `03-architecture.md` | собрать валидный конверт без `job_id` нельзя (схема требует uuid), а выдуманный `job_id` ядро всё равно не сопоставит с заданием; оба сервиса логируют и сбрасывают (`worker.py`: tabular строки 253–263, scan-gateway 292–299); ядро и так закрывает «молчание» (`recognition_exchange.py`: `NO_ACK`, `dlq`) | нет худшего исхода, чем 120 с до `dlq` (4 × `ACK_TIMEOUT_SECONDS`) для заведомо битого запроса, которого ядро само не порождает; сокращать задержку можно только чтением `job_id` из заголовка `Nats-Msg-Id` — вне Среза 0 | -| 36 | 2026-10-04 | `PUBLISH_RETRIES` в спеке (новое требование «Повторы публикации ответов сервисом») = **одна первая попытка + до 3 повторов** (всего не более 4 попыток) с тем же `Nats-Msg-Id` и паузой между попытками; исчерпаны — error-лог, задание закрывает ядро (`reply_deadline`). Пауза — деталь реализации, в спеку не включена | значения в коде: `scan-gateway/api/src/core/config.py:63` (`PUBLISH_RETRIES = 3`, «повторы после первой попытки (всего до 4)») и `core/nats.py:245–256` (`attempts_total = retry_count + 1`); `tabular-parser/api/src/core/config.py:49` (`PUBLISH_RETRIES = 3`, `NonNegativeInt`) и `core/nats.py:306` (`attempts = publish_retries + 1`); паузы различаются (2,0 с и 0,5 с), семантика одна | если ядро/сервис договорятся считать `PUBLISH_RETRIES` общим числом попыток, расхождение «3 против 4» не ломает обмен (дедуп по `Nats-Msg-Id`); правка — одна фраза в спеке | +| 36 | 2026-10-04 | `PUBLISH_RETRIES` в спеке (новое требование «Повторы публикации ответов сервисом») = **одна первая попытка + до 3 повторов** (всего не более 4 попыток) с тем же `Nats-Msg-Id` и паузой между попытками; исчерпаны — error-лог, задание закрывает ядро (`reply_deadline`). Пауза — деталь реализации (`tabular-parser` 2,0 с, `scan-gateway` 0,5 с), в спеку не включена | значения в коде: `scan-gateway/api/src/core/config.py:63` (`PUBLISH_RETRIES = 3`, «повторы после первой попытки (всего до 4)») и `core/nats.py:245–256` (`attempts_total = retry_count + 1`); `tabular-parser/api/src/core/config.py:49` (`PUBLISH_RETRIES = 3`, `NonNegativeInt`) и `core/nats.py:306` (`attempts = publish_retries + 1`); паузы различаются (`tabular-parser` — `PUBLISH_RETRY_PAUSE = 2.0` с в `core/nats.py:51`, `scan-gateway` — `RETRY_DELAY = 0.5` с в `core/nats.py:49`), семантика одна | если ядро/сервис договорятся считать `PUBLISH_RETRIES` общим числом попыток, расхождение «3 против 4» не ломает обмен (дедуп по `Nats-Msg-Id`); правка — одна фраза в спеке | | 37 | 2026-10-04 | Пример `contracts/examples/request-image.json` с `reply_deadline` в прошлом **не меняется**; в `spec.md` (сценарий «Примеры контракта — иллюстрации») и `03-architecture.md` записано, что примеры — иллюстрации формы, схема проверяет у `reply_deadline` лишь формат `date-time`, а срок проверяет сервис (`scan-gateway` — `error TIMEOUT`, `scan-gateway/api/src/core/worker.py:469–475`; `tabular-parser` срок не проверяет: `reply_deadline` в его воркере не используется) | копии `examples/` в сервисах сверяются побайтно тестами идентичности: правка примера = пересинхронизация трёх репозиториев ради документальной оговорки; `validate_contracts.py` проходит (rc 0, 8 примеров) | если кто-то возьмёт `request-image.json` как готовое задание для `scan-gateway`, получит `TIMEOUT` — это зафиксировано оговоркой; обновить дату позже можно одним PR с пересинхронизацией | | 38 | 2026-10-04 | `06-test-data.md` §6 и `09-delivery-plan.md` (Срез 0) приведены к факту: `fixtures/directories/` — только тестовые ЮЛ/ГК (сейчас пустой `.gitkeep`, наполнение — Срез 1); seed продукта лежит в `arm-core/api/seed/core/` (+ `seed/dev/` только для `--dev`); в плане Среза 0 добавлена ссылка на `dev/README.md` («Профиль `services`», «Проверка окружения»); `00-workspace-layout.md` не менялся (неактуальных фактов нет) | перечень файлов seed и число ролей/прав проверены по `arm-core/api/seed/` (7 ролей, 16 прав, 122 строки плана счетов); решение D-1, compose и проверки вместо CI в `09` уже были верны | только неточная ссылка в документах; правится правкой текста | | 39 | 2026-10-04 | Реестр `07-open-questions.md`: 36 новых строк OQ-48…OQ-83 вместо ~60 пунктов трёх `open-questions-proposal.md` и находок контроллера — пересекающиеся пункты слиты в одну строку с перечнем источников: PUBLISH_RETRIES (scan-gateway п. 1 + O-25), `reply_deadline` (п. 12 + O-26), нечитаемый запрос (tabular P-1 + O-24), ack при `SCHEMA_MISMATCH` (tabular P-2 + scan-gateway п. 2), `attempt + 1` (tabular P-3 + scan-gateway п. 17), фикстуры OCR (п. 3 + O-15), `make up`/`check-full` (O-1, O-2, O-4, O-5 + scan-gateway п. 18), общий NATS (п. 11 + O-11), `stored_file.status` (arm-core предл. 19 + O-18), `republish_count` (предл. 10 + O-19), append-only аудит (предл. 9 + дополнение), срок ответа (предл. 11 + дополнение) | один вопрос — одна строка: иначе человек решает одно и то же дважды, а реестр несёт противоречивые статусы; перечень источников (репозиторий + маркер) сохраняет прослеживаемость, маркеры не менялись | если слияние оказалось неудачным, строка делится на две с новым номером; нумерация OQ-48…OQ-83 сплошная, на неё могут ссылаться только новые документы | -- 2.47.2 From 5630ffadb79d6be2d6d60505f390057fa5e55506 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:16:10 +0400 Subject: [PATCH 58/68] =?UTF-8?q?docs(context):=20=D1=80=D0=B5=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D1=80=20OQ=20=E2=80=94=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=20=D0=BE=D1=82=D1=87=D1=91=D1=82=D1=8B=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BC=D0=B0=D1=80=D0=BA=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8=20=D0=BD=D0=B0=D1=85=D0=BE=D0=B4=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- context/07-open-questions.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/context/07-open-questions.md b/context/07-open-questions.md index de8102d..b7140fa 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -62,33 +62,33 @@ | OQ-54 | Параметры JWT РАИСА неизвестны: JWKS URL / публичный ключ, `iss`, `aud`, имя claim с email. Что в продуктивном контуре? Делать ли проверку `aud`/`iss` обязательной в проде? | ТЗ 8.1, `03-architecture.md` (SSO РАИСА); `core/auth.py`; arm-core: предл. 7 (T16.2) | срез 1+ (прод-конфигурация) | допущение принято: настраиваются `AUTH_JWKS_URL`/`AUTH_PUBLIC_KEY` (взаимоисключающие), `AUTH_ISSUER`, `AUTH_AUDIENCE`, `AUTH_EMAIL_CLAIM` (по умолчанию `email`); при пустых `AUTH_AUDIENCE`/`AUTH_ISSUER` `aud`/`iss` не проверяются; предложение: в проде сделать `aud` обязательным и предупреждать при старте, если пусто | | OQ-55 | В enum `audit_action` нет действия для смены статуса задания распознавания (`recognition_job`). Добавлять ли (DBML v5.14) и нужен ли аудит обмена вообще? | DBML v5.13 `audit_action`; arm-core: предл. 8 (R-7, design #20 arm-core) | срез 2 (обмен); кандидат DBML v5.14 | допущение принято: Срез 0 не пишет `audit_event` из обмена `recognition_job`; сервис аудита (`services/audit.py`) готов и покрыт тестами; если нужно — действие в v5.14 и вызовы в `recognition_exchange.py` | | OQ-56 | Append-only аудит обеспечен только на уровне кода (единственная точка записи, статический тест на UPDATE/DELETE по `AuditEvent`). Нужна ли защита на уровне БД (триггер или `REVOKE UPDATE, DELETE` для роли приложения)? Вопрос поднят дважды (в том числе финальным ревью ветки). | инвариант «Append-only аудит» (`../CLAUDE.md`); `services/audit.py`; arm-core: предл. 9 и дополнение к нему (задача 21) | миграции, права БД; платформа | вопрос: допущение принято на Срез 0 — только код и тесты; решение по триггеру/правам — с командой платформы до прода | -| OQ-57 | Счётчик повторных публикаций запроса (`ACK_TIMEOUT_SECONDS` × `OUTBOX_MAX_REPUBLISH`) хранится в `recognition_job.metrics.republish_count` (jsonb), колонки в DBML нет; при каждой повторной публикации `published_at` перезаписывается временем последней, время первой не сохраняется. Нужны ли колонка счётчика и время первой публикации (v5.14)? | `services/recognition_exchange.py` (design #6 arm-core); arm-core: предл. 10 (T23, T23.1); docs: O-19 (`task-4-report.md`) | срез replay; кандидат DBML v5.14 | допущение принято: счётчик в `metrics.republish_count`, `attempt` при повторе не растёт; `published_at` — время последней публикации (`dlq` через (`OUTBOX_MAX_REPUBLISH`+1)×`ACK_TIMEOUT_SECONDS` ≈ 120 с, описано в `dev/README.md`); при будущем replay счётчик и `reply_deadline` надо сбрасывать | +| OQ-57 | Счётчик повторных публикаций запроса (`ACK_TIMEOUT_SECONDS` × `OUTBOX_MAX_REPUBLISH`) хранится в `recognition_job.metrics.republish_count` (jsonb), колонки в DBML нет; при каждой повторной публикации `published_at` перезаписывается временем последней, время первой не сохраняется. Нужны ли колонка счётчика и время первой публикации (v5.14)? | `services/recognition_exchange.py` (design #6 arm-core); arm-core: предл. 10 (T23, T23.1); docs: O-19 (задача 4 фазы 2) | срез replay; кандидат DBML v5.14 | допущение принято: счётчик в `metrics.republish_count`, `attempt` при повторе не растёт; `published_at` — время последней публикации (`dlq` через (`OUTBOX_MAX_REPUBLISH`+1)×`ACK_TIMEOUT_SECONDS` ≈ 120 с, описано в `dev/README.md`); при будущем replay счётчик и `reply_deadline` надо сбрасывать | | OQ-58 | Семантика сроков и ошибок обмена в ядре: (а) `reply_deadline` по умолчанию = `now + 5 мин` для обоих pipeline (`tabular`, `image`), если у задания NULL; в DBML срок есть только для OCR («OCR — 5 мин»), для парсера значения нет; (б) при невозможности собрать запрос по контракту задание уходит в `error` / `INVALID_REQUEST`; (в) при `ACK_TIMEOUT × (MAX_REPUBLISH+1) ≥ 5 мин` статус `dlq` недостижим (раньше сработает `timeout`). Верно ли? Какой срок у `tabular-parser`? | DBML `recognition_job.reply_deadline` note; `recognition_exchange.py` (`DEFAULT_REPLY_TIMEOUT`); arm-core: предл. 11 и дополнение (T23.4) | срез 2–3; настройка таймаутов | допущение принято: как описано (дефолты 30 с × 3 укладываются в 5 мин); сроки по pipeline уточнить с командами парсера и шлюза | | OQ-59 | Email пользователя сопоставляется с `app_user.email` по `lower(email)`; при двух записях, различающихся регистром, — 403 (fail-closed). Добавить `CHECK`/уникальный индекс по `lower(email)` в DBML? | `core/auth.py`, `api/dependencies/auth.py`; arm-core: предл. 12 (R-17, design #29 arm-core) | срез 1 (админ-экран); кандидат DBML v5.14 | допущение принято: уникальность `email` регистрозависимая (как в DBML); регистронезависимая — в v5.14 | | OQ-60 | Seed `--dev` не защищён флагом `DEBUG`: создаёт dev-пользователей (в том числе `ADMIN`) на любой БД, на которую указывает `.env`, и перезаписывает существующего `app_user` с совпадающим email. Нужна ли защита? | arm-core `scripts/seed.py`; arm-core: предл. 13 (T15.2, T15.4) | срез 0 (процесс); прод-развёртывание | допущение принято: спека защиты не требует, `--dev` помечен «только для разработки» (в compose запускается `arm-core-migrate`); предложение — отказ при `DEBUG=false` или явный флаг подтверждения | | OQ-61 | Неверный метод на существующий API-маршрут (например `POST /api/v1/me`) отдаёт 404 вместо 405: SPA-монтирование на `/` перехватывает частичное совпадение, а `/api/*` в нём принудительно 404. Приемлемо? | arm-core `app.py` (`SPAStaticFiles`); arm-core: предл. 14 (T18.2) | API-контракт (клиенты, фронт) | допущение принято: оставлено как есть (честный 404 для несуществующих путей важнее 405); при необходимости — отдельная правка | -| OQ-62 | В образе `arm-core` нет фронта (`Dockerfile` не собирает `web/`), `GET /` отдаёт 307 на `//` и затем 404; редирект на `//` похож на дефект монтирования SPA при `ROOT_PATH=/`. Собирать `web/` в образ в Срезе 0/1 и чинить редирект? | arm-core `Dockerfile`, `app.py`; docs: O-10 (`task-3-report.md`, `dev/README.md`) | срез 0 (compose), срез 1 (фронт) | допущение принято: README фиксирует отсутствие UI в образе; редирект на `//` не исследовался (сервис не правился) — follow-up arm-core | +| OQ-62 | В образе `arm-core` нет фронта (`Dockerfile` не собирает `web/`), `GET /` отдаёт 307 на `//` и затем 404; редирект на `//` похож на дефект монтирования SPA при `ROOT_PATH=/`. Собирать `web/` в образ в Срезе 0/1 и чинить редирект? | arm-core `Dockerfile`, `app.py`; docs: O-10 (задача 3 фазы 2), `dev/README.md` | срез 0 (compose), срез 1 (фронт) | допущение принято: README фиксирует отсутствие UI в образе; редирект на `//` не исследовался (сервис не правился) — follow-up arm-core | | OQ-63 | `web/package-lock.json`: шаблон lock не содержит, `raisa-*` из Nexus — lock вне банка отличался бы; в Срезе 0 lock в `.gitignore`. `npm install` сообщает 6 high (npm audit) в зависимостях шаблона web. Коммитить ли lock; кто обновляет зависимости? | arm-core `web/`; arm-core: предл. 15 (T25.1, R-23, design #35 arm-core) | платформа, фронт; до прода | допущение принято: lock не в репозитории, зависимости не менялись; решение — с платформенной командой (Nexus, политика audit) | | OQ-64 | Аудит `LOGIN` / `LOGIN_FAILED` (действия есть в `audit_action`) для JWT-аутентификации: в каком срезе и что считать «входом» — первый запрос с токеном, каждый запрос, отказ проверки токена? | DBML `audit_action`; `core/auth.py`; arm-core: предл. 16 (финальное ревью) | срез 1 (вход, админ-экран); аудит | допущение принято: в Срезе 0 вход не аудируется; сервис аудита готов | | OQ-65 | Шаблонные CORS `allow_origins=["*"]` + `allow_credentials=True` и `ProxyHeadersMiddleware(trusted_hosts=["*"])`: в проде любой клиент может подставить `X-Forwarded-For`, и `audit_event.ip_address` недостоверен. Какие origins и доверенные прокси на платформе? | arm-core `app.py` (шаблон команды); arm-core: предл. 17 (финальное ревью) | прод-конфигурация; достоверность аудита | допущение принято: оставлено как в шаблоне; перечни origins и доверенных прокси — с платформенной командой до прода | | OQ-66 | `/docs` и `/openapi.json` FastAPI открыты без аутентификации, в том числе в проде. Закрывать их (выключать при `DEBUG=false`) или оставить? | arm-core `app.py` (шаблон); arm-core: предл. 18 (финальное ревью) | прод-конфигурация; безопасность | допущение принято: открыты, как в шаблоне; решение — с платформой и ИБ | -| OQ-67 | Inbox не обновляет `stored_file.status` (`UPLOADED → PROCESSING → PROCESSED / ERROR`) и `stored_file.current_recognition_job_id` по результату задания: после результата файл остаётся `UPLOADED`, `current_recognition_job_id` пуст. Кто и когда это делает в Срезе 2 — inbox или маппер, в одной транзакции? | DBML `stored_file`, `file_status`; `recognition_exchange.py`; arm-core: предл. 19 (финальное ревью); docs: O-18 (`task-4-report.md`) | срез 2 (загрузка и маппинг) | допущение принято: в Срезе 0 (`NoopMapper`) файл остаётся `UPLOADED`, в `dev/README.md` записано как «не покрывается»; решение — при проектировании маппера Среза 2 | -| OQ-68 | Конверт `error` у `scan-gateway` публикуется без `metrics`, поэтому `recognition_job.metrics` = NULL (у `tabular-parser` в `metrics` есть `duration_ms`). Требовать ли `metrics` в конверте ошибки и единые поля метрик у обоих сервисов? | scan-gateway `core/worker.py`; docs: O-18 (`task-4-report.md`, сквозная проверка) | срез 2–3 (наблюдаемость, контракт v1.x) | допущение принято: `metrics` в контракте необязательны, ядро хранит NULL; требование — решение владельца контракта | +| OQ-67 | Inbox не обновляет `stored_file.status` (`UPLOADED → PROCESSING → PROCESSED / ERROR`) и `stored_file.current_recognition_job_id` по результату задания: после результата файл остаётся `UPLOADED`, `current_recognition_job_id` пуст. Кто и когда это делает в Срезе 2 — inbox или маппер, в одной транзакции? | DBML `stored_file`, `file_status`; `recognition_exchange.py`; arm-core: предл. 19 (финальное ревью); docs: O-18 (задача 4 фазы 2) | срез 2 (загрузка и маппинг) | допущение принято: в Срезе 0 (`NoopMapper`) файл остаётся `UPLOADED`, в `dev/README.md` записано как «не покрывается»; решение — при проектировании маппера Среза 2 | +| OQ-68 | Конверт `error` у `scan-gateway` публикуется без `metrics`, поэтому `recognition_job.metrics` = NULL (у `tabular-parser` в `metrics` есть `duration_ms`). Требовать ли `metrics` в конверте ошибки и единые поля метрик у обоих сервисов? | scan-gateway `core/worker.py`; docs: O-18 (задача 4 фазы 2, сквозная проверка) | срез 2–3 (наблюдаемость, контракт v1.x) | допущение принято: `metrics` в контракте необязательны, ядро хранит NULL; требование — решение владельца контракта | | OQ-69 | Обработчик `recognition.result` выполняется последовательно в callback подписки NATS; длинный маппер Среза 2 задержит следующие сообщения, ack сервиса не успеет за `ACK_TIMEOUT_SECONDS` (30 с) — ложный republish, затем `dlq`. Предложение: развести быстрый inbox (фиксация сообщения и статуса) и отдельный цикл маппинга результата. | arm-core `core/nats.py` (`subscribe_results`), `recognition_exchange.py` (`handle_message`, `ResultMapper`); arm-core: предл. 20 (финальное ревью) | срез 2 (маппинг); архитектура обмена (D-6) | допущение принято: в Срезе 0 `NoopMapper` — проблема не проявляется; решение — до реализации маппера Среза 2 | | OQ-70 | Перед чтением файла результата маппер должен проверять, что ключ начинается с `results/{file_id}/{job_id}/`, а бакет — бакет роли `results` (иначе сервис может «подсунуть» чужой объект). Закрепить это в контракте и `03-architecture.md`? | контракт `recognition-result-message` (`result_s3`); arm-core `_register_result_object` (`recognition_exchange.py`); arm-core: предл. 21 (финальное ревью) | срез 2 (маппинг); контракт v1.x | допущение принято: в Срезе 0 результат только регистрируется в `s3_object`, не читается; проверку реализует маппер Среза 2 | -| OQ-71 | Запрос без читаемых `job_id` / `file_id` / `attempt` остаётся без ответа (конверт собрать нельзя): сервис логирует ошибку и отбрасывает запрос, ядро закрывает задание `dlq` / `NO_ACK` примерно через 120 с. Расходятся и реализации: при нечитаемом `attempt` `tabular-parser` берёт его из заголовка `Nats-Msg-Id` запроса или ничего не отправляет, `scan-gateway` подставляет `attempt = 1` и публикует конверт. Оставить так, выровнять реализации или сделать `file_id` необязательным в конверте `error` (MINOR 1.1)? | `recognition-contract` spec, сценарий «Нечитаемый запрос»; tabular-parser: P-1 (задача 4.3); docs: O-24 (= O-1 `task-5-report.md`), design #35 | контракт v1.x; срез 2–3 | допущение принято: молчание сервиса (design #35); реализации выровнять в Срезе 2/3, влияние только на битые запросы | +| OQ-71 | Запрос без читаемых `job_id` / `file_id` / `attempt` остаётся без ответа (конверт собрать нельзя): сервис логирует ошибку и отбрасывает запрос, ядро закрывает задание `dlq` / `NO_ACK` примерно через 120 с. Расходятся и реализации: при нечитаемом `attempt` `tabular-parser` берёт его из заголовка `Nats-Msg-Id` запроса или ничего не отправляет, `scan-gateway` подставляет `attempt = 1` и публикует конверт. Оставить так, выровнять реализации или сделать `file_id` необязательным в конверте `error` (MINOR 1.1)? | `recognition-contract` spec, сценарий «Нечитаемый запрос»; tabular-parser: P-1 (задача 4.3); docs: O-24, design #35 | контракт v1.x; срез 2–3 | допущение принято: молчание сервиса (design #35); реализации выровнять в Срезе 2/3, влияние только на битые запросы | | OQ-72 | Ack при несовместимой версии запроса: ранняя формулировка контракта и `03-architecture.md` требовали «`SCHEMA_MISMATCH` и ack», оба сервиса публикуют `error SCHEMA_MISMATCH` без ack (ядро принимает результат из `published`, ack перед ним ничего не добавляет). Спека и документ приведены к поведению сервисов — подтвердить. | tabular-parser: P-2; scan-gateway: п. 2 таблицы предложений (T12.13, R-16); docs: design #34 | контракт v1.x; срез 2 | допущение принято: без ack, результат `SCHEMA_MISMATCH` закрывает задание (design #34); если ядро начнёт требовать ack перед результатом — правка одной строки в воркерах и спеке | | OQ-73 | Повторная попытка (`attempt + 1`) того же `job_id` после ошибки или результата: оба сервиса дедуплицируют по `job_id` (LRU `DONE_CACHE_SIZE`), поэтому replay с новым `attempt` получает только ack и не обрабатывается, на единственном инстансе задание повисает до `reply_deadline` → `timeout`. По `03-architecture.md` replay — «сброс в `queued` с `attempt + 1`». Сделать ключ дедупликации `(job_id, attempt)`; повтор во время обработки — только ack? | tabular-parser: P-3 (задача 4.3); scan-gateway: п. 17 таблицы предложений (I-3, R-17); `03-architecture.md` (replay) | срез 3+ (replay); контракт v1.x | вопрос: допущение принято на Срез 0 — дедуп по `job_id`, ядро повторяет только тем же `attempt`; при одобрении — одна строка в воркере (ключ LRU) | -| OQ-74 | `PUBLISH_RETRIES` = одна первая попытка + до 3 повторов (всего до 4) с тем же `Nats-Msg-Id`; у `scan-gateway` тип `PositiveInt` (повторы нельзя отключить настройкой), у `tabular-parser` `NonNegativeInt` (0 допустим); паузы 0,5 с и 2,0 с. Согласовать единую семантику (повторы или всего попыток) и тип? | scan-gateway: п. 1 таблицы предложений (R-13, T11.1); docs: O-25 (= O-2 `task-5-report.md`), design #36 | контракт v1.x; настройки сервисов | допущение принято: «1 + 3 повтора» (design #36), расхождение «3 против 4» обмен не ломает (дедуп по `Nats-Msg-Id`); выровнять тип — косметика конфигурации | -| OQ-75 | `reply_deadline` проверяет только `scan-gateway` (`error TIMEOUT` до отправки в ИС и потолок ожидания ответа); `tabular-parser` обрабатывает просроченный запрос. Пример `contracts/examples/request-image.json` имеет `reply_deadline` в прошлом (`2026-10-03T12:45:00Z`) и даёт `TIMEOUT` при ручном прогоне. Единая обязанность сервисов и свежая дата примера? | scan-gateway: п. 12 таблицы предложений (O-e); docs: O-26 (= O-3 `task-5-report.md`), design #37 | контракт v1.x; срез 2 | допущение принято: срок — дело сервиса, пример — иллюстрация формы (оговорка в spec и `03-architecture.md`, design #37); обновить дату можно одним PR с пересинхронизацией копий в трёх репозиториях | -| OQ-76 | Обе реальные фикстуры `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС (`error.code = INVALID_DOCUMENT`, пустой `table_structure`). Успешный путь `image` (`done` / `needs_check`, файл в `results/…`, `s3_object` с `bucket_role = results`) сквозной проверкой не покрыт; в `scan-gateway` проверяется только синтетическим ответом. Нужны образцы успешных ответов Content AI. | scan-gateway: п. 3 таблицы предложений (O-1, R-10, design #32); docs: O-15 (`task-4-report.md`), design #31 | срез 3 (OQ-13/14, нормализатор); fixtures | вопрос: допущение принято на Срез 0 — успех проверяется синтетическим ответом, `NullNormalizer` пропускает ошибку ИС; образцы запросить у владельцев ИС Сканирование | +| OQ-74 | `PUBLISH_RETRIES` = одна первая попытка + до 3 повторов (всего до 4) с тем же `Nats-Msg-Id`; у `scan-gateway` тип `PositiveInt` (повторы нельзя отключить настройкой), у `tabular-parser` `NonNegativeInt` (0 допустим); паузы 0,5 с и 2,0 с. Согласовать единую семантику (повторы или всего попыток) и тип? | scan-gateway: п. 1 таблицы предложений (R-13, T11.1); docs: O-25, design #36 | контракт v1.x; настройки сервисов | допущение принято: «1 + 3 повтора» (design #36), расхождение «3 против 4» обмен не ломает (дедуп по `Nats-Msg-Id`); выровнять тип — косметика конфигурации | +| OQ-75 | `reply_deadline` проверяет только `scan-gateway` (`error TIMEOUT` до отправки в ИС и потолок ожидания ответа); `tabular-parser` обрабатывает просроченный запрос. Пример `contracts/examples/request-image.json` имеет `reply_deadline` в прошлом (`2026-10-03T12:45:00Z`) и даёт `TIMEOUT` при ручном прогоне. Единая обязанность сервисов и свежая дата примера? | scan-gateway: п. 12 таблицы предложений (O-e); docs: O-26, design #37 | контракт v1.x; срез 2 | допущение принято: срок — дело сервиса, пример — иллюстрация формы (оговорка в spec и `03-architecture.md`, design #37); обновить дату можно одним PR с пересинхронизацией копий в трёх репозиториях | +| OQ-76 | Обе реальные фикстуры `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС (`error.code = INVALID_DOCUMENT`, пустой `table_structure`). Успешный путь `image` (`done` / `needs_check`, файл в `results/…`, `s3_object` с `bucket_role = results`) сквозной проверкой не покрыт; в `scan-gateway` проверяется только синтетическим ответом. Нужны образцы успешных ответов Content AI. | scan-gateway: п. 3 таблицы предложений (O-1, R-10, design #32); docs: O-15 (задача 4 фазы 2), design #31 | срез 3 (OQ-13/14, нормализатор); fixtures | вопрос: допущение принято на Срез 0 — успех проверяется синтетическим ответом, `NullNormalizer` пропускает ошибку ИС; образцы запросить у владельцев ИС Сканирование | | OQ-77 | §11 `05-domain-rules.md` описывает `data_ocr[]` (список), а в фикстурах `data_ocr` — объект с полями `label/organization/inn/units/date/table_structure`. Какой формат фактический на стенде ИС (связано с OQ-13, OQ-14)? | `05-domain-rules.md` §11; scan-gateway: п. 4 таблицы предложений (O-1, T3.2) | срез 3 (нормализатор) | вопрос: допущение принято на Срез 0 — формат не интерпретируется; решение нужно до нормализатора Среза 3 | | OQ-78 | Шлюз читает ровно `//.json` (спека); старый код читал все файлы каталога `Output//`. Какие файлы и когда реальная ИС кладёт в `Output`, в каком порядке (момент готовности)? | scan-gateway: п. 5 таблицы предложений (T8.5) | срез 3 (стенд ИС) | вопрос: допущение принято на Срез 0 — имя ответа по спеке; проверить на стенде ИС | | OQ-79 | Временный файл `.<имя>.part` лежит в `Input` (атомарная запись `.part` + rename): реальная ИС без маски может забрать недописанный файл. Уточнить у владельцев ИС маску и поведение или перенести временный файл в служебный подкаталог рядом с `Input`? | scan-gateway: п. 6 таблицы предложений (R-5, T7/T8) | срез 3 (стенд ИС) | вопрос: допущение принято на Срез 0 — `.part` + rename в `Input`; вариант с подкаталогом `Input/.tmp` или корнем — после ответа владельцев ИС | -| 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 (`task-1-report.md`), O-4 и O-5 (`task-2-report.md`), `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 (`task-1-report.md`), `dev/checks.md`; spec `quality-checks` | срез 0 (процесс), все сервисы | допущение принято: расхождение описано в `dev/checks.md`; правка тестов и Makefile в scan-gateway и tabular-parser — follow-up человеку (сервисы не правились) | -| OQ-82 | `.dockerignore` в `arm-core` не исключает `api/.env.test`: файл попадает в образ (у `tabular-parser` и `scan-gateway` исключён). Исправлять? | arm-core `.dockerignore`; docs: T3.2 (`task-3-report.md`) | срез 0 (образы), прод-сборка | допущение принято: безвредно (в файле стандартные dev-учётные данные compose, не секреты), в Срезе 0 не правилось; добавить исключение в arm-core — follow-up | -| 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 (`task-3-report.md`; подтверждено ревью для tabular-parser и scan-gateway), `dev/README.md` | срез 0 (процесс), интеграционные тесты | допущение принято: в `dev/README.md` предупреждение и команда `stop` для контейнеров профиля; не гонять `make test-integration` двух сервисов одновременно | +| 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 человеку (сервисы не правились) | +| OQ-82 | `.dockerignore` в `arm-core` не исключает `api/.env.test`: файл попадает в образ (у `tabular-parser` и `scan-gateway` исключён). Исправлять? | arm-core `.dockerignore`; docs: T3.2 (задача 3 фазы 2) | срез 0 (образы), прод-сборка | допущение принято: безвредно (в файле стандартные dev-учётные данные compose, не секреты), в Срезе 0 не правилось; добавить исключение в arm-core — follow-up | +| 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` двух сервисов одновременно | Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения; `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. -- 2.47.2 From 050f59a384615098207871d68cad656916f90748 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:17:21 +0400 Subject: [PATCH 59/68] =?UTF-8?q?docs:=20=D1=81=D1=86=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=B9=20venv=20=D0=BF=D1=80=D0=BE=D1=82=D0=B8?= =?UTF-8?q?=D0=B2=20compose=20=D0=BD=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BD=20=E2=80=94=20design=20#43/#44,=20OQ-8?= =?UTF-8?q?4,=20README=20=D0=B8=20.env.example=20=D0=B1=D0=B5=D0=B7=20?= =?UTF-8?q?=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2=D1=88=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=8F=D0=B7=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8=20SMB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- context/07-open-questions.md | 1 + dev/.env.example | 3 ++- dev/README.md | 16 +++++++++------- openspec/changes/slice-0-skeleton/design.md | 4 +++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/context/07-open-questions.md b/context/07-open-questions.md index b7140fa..526c9a4 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -89,6 +89,7 @@ | 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 человеку (сервисы не правились) | | OQ-82 | `.dockerignore` в `arm-core` не исключает `api/.env.test`: файл попадает в образ (у `tabular-parser` и `scan-gateway` исключён). Исправлять? | arm-core `.dockerignore`; docs: T3.2 (задача 3 фазы 2) | срез 0 (образы), прод-сборка | допущение принято: безвредно (в файле стандартные dev-учётные данные compose, не секреты), в Срезе 0 не правилось; добавить исключение в arm-core — follow-up | | 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-84 | Запуск сервисов из venv без Nexus (`make venv` + `python run_server.py` из `api/` с `.env` из `docs/dev/.env.example`, healthcheck по `localhost`) в Срезе 0 не проверялся ни в одном из трёх сервисов. Известно по чтению конфигов (design #44): `.env.example` в нынешнем виде отвергнет `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 доступных только для чтения) | Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения; `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. diff --git a/dev/.env.example b/dev/.env.example index 271708d..79a296c 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -48,7 +48,8 @@ OPENBAO__NATS__NATS_URL=nats://localhost:4222 OPENBAO__NATS__NATS_SEED= OPENBAO__NATS__NATS_JWT= -# SMB — заглушки (в Срезе 0 не используется); блок обязателен для Settings arm-core +# SMB — заглушки (в Срезе 0 не используется); блок необязателен для arm-core и scan-gateway (режим local). +# tabular-parser неизвестные ключи OPENBAO__* отвергает: для него удалите блоки DATABASE и SMB (см. dev/README.md) OPENBAO__SMB__SMB_HOST=localhost OPENBAO__SMB__SMB_PATH=share OPENBAO__SMB__SMB_LOGIN=user diff --git a/dev/README.md b/dev/README.md index ca8b601..630f05a 100644 --- a/dev/README.md +++ b/dev/README.md @@ -293,7 +293,7 @@ docker compose --profile services up -d --wait tabular-parser # вернут | `OPENBAO__NATS__NATS_URL` | `nats://localhost:4222` | адрес NATS | | `OPENBAO__NATS__NATS_SEED` | пусто | nkey seed; пусто — подключение без nkeys | | `OPENBAO__NATS__NATS_JWT` | пусто | JWT NATS; пусто — подключение без nkeys | -| `OPENBAO__SMB__SMB_HOST` | `localhost` | заглушка SMB (в Срезе 0 не используется); блок обязателен для `Settings` arm-core | +| `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 | @@ -318,9 +318,10 @@ docker compose --profile services down -v # полный сброс: тома ## Запуск сервисов из venv без Nexus (черновик) -> Черновик: запуск сервисов из venv в Срезе 0 не проверялся (сквозная проверка задачи 5.4 шла на профиле `services`); -> шаги ниже не прогонялись целиком. Проверка из venv вынесена за пределы Среза 0 (фаза 2), отдельного вопроса в -> `07-open-questions.md` нет. `make venv` и `make test-integration` есть во всех трёх сервисах. +> Черновик: запуск сервисов из venv в Срезе 0 не проверялся (сквозная проверка шла на профиле `services`); шаги ниже +> не прогонялись целиком. Сценарий spec `dev-environment` остаётся неподтверждённым — вопрос +> [OQ-84](../context/07-open-questions.md), решение design #43. `make venv` и `make test-integration` есть во всех трёх +> сервисах. Пакеты `raisa_*` из Nexus банка вне банка недоступны (D-10). Поэтому `make venv` ставит `requirements.txt` без строк `raisa_*` (фильтр `grep -v '^raisa_'`), код импортирует `raisa_*` условно (при `DEBUG=true`), а npm-пакеты `raisa-*` @@ -343,11 +344,12 @@ 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 не проверялся (см. черновик выше). +`hotfolder` (шлюзу нужен `SCAN_HOTFOLDER_PATH`). Набор переменных сервисов для запуска из venv не проверялся (см. черновик выше и OQ-84). По чтению конфигов: `tabular-parser` отвергает неизвестные ключи `OPENBAO__*`, поэтому в его `.env` нужно удалить строки `OPENBAO__DATABASE__*` и `OPENBAO__SMB__*`; для MinIO по `http` нужен `OPENBAO__MINIO__MINIO_SECURE=false` (в `.env.example` его нет, по умолчанию `true`). ## Интеграционные тесты сервисов против compose (черновик) -> Черновик: уточняется после задач 3.x и 5.x. +> Черновик: состав целей и порядок проверок описаны в [`checks.md`](checks.md); открытые вопросы — OQ-83 (общий NATS +> у контейнеров и тестов) и OQ-84. Общий набор целей (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`) есть во всех сервисных репозиториях (`arm-core`, `tabular-parser`, `scan-gateway`); семантика, состав `check-full`, порядок перед PR и пропуск тестов без @@ -386,7 +388,7 @@ postgres nats minio`, затем `make test-integration` (или `make check-ful - **Образы MinIO неофициальные.** Официальные `minio/minio` и `minio/mc` недоступны (Docker Hub — denied, на quay.io нет нужных тегов), поэтому в compose стоят `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и `bitnamilegacy/minio-client:2025.7.21`, а сервис `minio` запущен с `user: "0:0"` (иначе образ не пишет в свежий том) — - только для dev. Решения R-5/R-6: [`design.md`](../openspec/changes/slice-0-skeleton/design.md), раздел + только для dev. Решения: [`design.md`](../openspec/changes/slice-0-skeleton/design.md), раздел «Decisions made without discussion», решения №16–17. Замена на официальные образы — две строки `image:` в `docker-compose.yml` (и удаление `user: "0:0"`). - **Безопасность.** По умолчанию порты публикуются только на `127.0.0.1` (`BIND_ADDR`), снаружи машины они недоступны. diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index 75d5aea..9b3cdc9 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -149,7 +149,7 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 15 | 2026-10-03 | Чекбоксы плана и `tasks.md` отмечает контроллер SDD отдельным коммитом после чистого ревью задачи | исполнитель не должен отмечать выполненным то, что ещё не прошло ревью | отметки правятся вручную | | 16 | 2026-10-03 | Compose: MinIO берётся из неофициальных образов `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и `bitnamilegacy/minio-client:2025.7.21` | `minio/minio` и `minio/mc` недоступны на Docker Hub (denied), на quay.io нужных тегов нет; для dev достаточно любого S3-совместимого MinIO нужной версии API | образы сторонние: при появлении доступа к банковскому реестру/официальным образам — заменить две строки `image:` в `dev/docker-compose.yml`; в прод эти образы не попадают | | 17 | 2026-10-03 | Compose: сервис `minio` запускается от `user: "0:0"` | образ `alpine/minio` по умолчанию работает от пользователя `minio` и падает на свежем именованном томе | только dev; удалить строку, когда образ/том позволят писать без root | -| 18 | 2026-10-03 | `dev/.env.example` дополнен заглушками `OPENBAO__SMB__*` и `NATS_ENABLED=true` | `Settings` шаблона (arm-core) требует блок `OPENBAO.SMB`, без него сервис из venv не стартует; `NATS_ENABLED` по умолчанию `false`, и `/healthcheck` показывает `nats = off`; spec велит копировать именно `docs/dev/.env.example` | убрать две группы строк; сервисы с другим набором обязательных настроек получат лишние (игнорируемые) переменные | +| 18 | 2026-10-03 | `dev/.env.example` дополнен заглушками `OPENBAO__SMB__*` и `NATS_ENABLED=true` | `Settings` шаблона (arm-core) требует блок `OPENBAO.SMB`, без него сервис из venv не стартует; `NATS_ENABLED` по умолчанию `false`, и `/healthcheck` показывает `nats = off`; spec велит копировать именно `docs/dev/.env.example` | убрать две группы строк; сервисы с другим набором обязательных настроек получат лишние (игнорируемые) переменные Примечание: часть про обязательность SMB устарела, см. №44. | | 19 | 2026-10-03 | `schema_version` во всех трёх схемах: `const "1.0"` заменён на `type: string, pattern: ^1\.\d+$` (производитель v1.0 пишет `"1.0"`); в `$comment` и в 03-architecture.md добавлено правило «новый MINOR раскатывается сначала на потребителей» | spec «Версионирование контракта»: потребитель принимает тот же MAJOR и любой MINOR, сервис «поддерживающий 1.x» принимает 1.1 и отвергает только 2.0; с `const` это неисполнимо, а копии схем уйдут в сервисы побайтно (финальное ревью I-1) | вернуть `const "1.0"` и пересобрать копии схем в сервисах; побочный риск — копии в `arm-core`/`tabular-parser`/`scan-gateway` нужно обновить (тест идентичности контракта упадёт, пока копии не обновлены) | | 20 | 2026-10-03 | Compose: порты хоста публикуются на `${BIND_ADDR:-127.0.0.1}`, а не на 0.0.0.0 | пароли по умолчанию (`armka`, `minioadmin`); все сценарии идут через localhost; spec bind-адрес не оговаривает (финальное ревью M-6) | если нужен доступ с другой машины — `BIND_ADDR=0.0.0.0` в `.env`; для запуска профиля `services` порты хоста не нужны | | 21 | 2026-10-04 | В корне репозитория `docs` добавлен `Makefile` с целями `venv`, `validate-contracts`, `validate-openspec`, `check` (сервисных целей нет); `jsonschema` ставится в `.venv` (в `.gitignore`) | дополнение контроллера к задаче 4.1: проверки контракта и спеки должны запускаться одной командой, как в сервисах; `check` = `scripts/validate_contracts.py` + `openspec validate slice-0-skeleton --strict` | удалить `Makefile` и строку `.venv/` в `.gitignore`; команды из `dev/checks.md` останутся запускаемыми вручную | @@ -174,3 +174,5 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 40 | 2026-10-04 | Статусы OQ-45/46/47 обновлены в существующих строках, не продублированы: все три — «допущение принято и применено в Срезе 0» с фактами (NU: контракт оставляет, ORM `OsvDataType` 1:1 с DBML v5.13, `NoopMapper`; OQ-46: 27 строк EXTENDED + 10 BASIC в seed, `is_mandatory = true`; OQ-47: 122 строки плана счетов, названия нестандартных субсчетов выведены по роли в §4). Кандидаты DBML v5.14 (`NU`, `NULLS NOT DISTINCT`, note `article_2_line`, действие `audit_action`, счётчик republish, `lower(email)`) внесены только строками реестра, файл `data-model-v5.13.dbml` не менялся | факты проверены чтением `arm-core/api/seed/core/`, `api/src/models/enums.py` и `contract/common.py`; создание DBML-версии — отдельное решение человека (CLAUDE.md: версия не меняется задним числом) | если человек решит иначе (например, хранить НУ), статус строки меняется правкой реестра, код Среза 0 не затронут | | 41 | 2026-10-04 | В реестр **не** внесены, а оставлены в отчёте задачи 6 (кандидаты в follow-up issue при закрытии): scan-gateway п. 7 (рестарт NATS не автоматизирован), п. 8 (`stop()` не публикует конверты принятым заданиям), п. 9 (`/healthcheck` всегда 200), п. 13 (`DEBUG` не используется), п. 14 (`ResourceWarning` aiohttp), п. 15 (SMB без `connection_timeout`); tabular P-4, scan-gateway п. 10 и 16 (порты, `SCAN_GATEWAY_PORT`, устаревший README) — закрыты задачами 3–5 и `dev/README.md`; O-22 и T3.7/O-20 — процессные пункты задачи 7 | это инженерные доработки или уже исправленные расхождения документов, вопроса к человеку/аналитикам в них нет; реестр остаётся списком решений, которые принимает человек | пункты могут потеряться, если follow-up issue не будет заведена (чеклист закрытия, п. 6): они перечислены в отчёте с маркерами | | 42 | 2026-10-04 | Формулировка статусов в новых строках: «допущение принято: …» — в коде/спеке Среза 0 действует рабочее допущение, вопрос открыт для подтверждения; «вопрос: …» — человек должен принять решение, допущение Среза 0 указано после двоеточия (аудит на уровне БД, `attempt + 1`, образцы ответов ИС, формат `data_ocr`, имя файла ответа, `.part` в `Input`) | правило файла: агент вопросы не закрывает, а помечает допущение; слово «закрыт» нигде не использовано | различение статусов — рекомендация, не контракт; при несогласии префикс правится в реестре | +| 43 | 2026-10-04 | Сценарий spec `dev-environment` «Запуск из venv против compose без Nexus» (`make venv` + `python run_server.py` + healthcheck) в Срезе 0 **не прогонялся**; раздел «Запуск сервисов из venv без Nexus» в `dev/README.md` остаётся черновиком, вопрос вынесен в OQ-84 (кандидат в follow-up issue) | проверка требует создать `.venv` внутри сервисных репозиториев (`make venv` пишет в `api/.venv`), а в фазе 2 они были доступны только для чтения (их worktree с открытыми PR не правятся); сквозная проверка шла на профиле `services` (контейнеры из тех же исходников) | сценарий spec может оказаться неработоспособным (набор переменных, `requirements.txt` без `raisa_*`, порты) — разработчик без Docker для сервисов узнает об этом при первом запуске; правится README или `.env.example` | +| 44 | 2026-10-04 | Уточнение к №18: блок `OPENBAO__SMB__*` **не обязателен**. В `arm-core` `OpenBAOSettings.SMB = SMBSettings()` с пустыми значениями по умолчанию (`api/src/core/config.py`), в `scan-gateway` блок необязателен в режиме `SCAN_HOTFOLDER_MODE=local` (по умолчанию) и обязателен целиком при `smb`, в `tabular-parser` блока нет, и `OpenBAOSettings` отвергает неизвестные ключи (`extra_forbidden`; проверено на копии его `config.py` с `.env` из `dev/.env.example`: падает на `OPENBAO.smb`, так же упадёт на `OPENBAO__DATABASE__*`) — `extra="ignore"` относится только к верхнему уровню `Settings`. Заглушки в `dev/.env.example` оставлены; комментарий и README исправлены и говорят, что `.env.example` годится для `arm-core` и `scan-gateway`, а для `tabular-parser` из него нужно убрать блоки `DATABASE` и `SMB` | в момент решения №18 `Settings` arm-core требовал блок; затем в arm-core блок сделан необязательным (в Срезе 0 SMB ядру не нужен), а формулировка осталась в `.env.example` и README | ложное утверждение «обязателен» заставляло бы переносить заглушки в окружение, где они не нужны; цена низкая — у `arm-core` и `scan-gateway` заглушки безвредны, а `tabular-parser` с неотредактированным `.env.example` не стартует (описано в README, часть OQ-84) | -- 2.47.2 From bf893959dff453ec04b92538447afb8bf9b25946 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:17:53 +0400 Subject: [PATCH 60/68] =?UTF-8?q?docs(openspec):=20recognition-contract=20?= =?UTF-8?q?=E2=80=94=20=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=20?= =?UTF-8?q?attempt=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=B8=D0=B7?= =?UTF-8?q?=20=D1=82=D0=B5=D0=BB=D0=B0,=20=D1=80=D0=B0=D1=81=D1=85=D0=BE?= =?UTF-8?q?=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B8=D1=81=D0=BE=D0=B2=20=D0=B2=20THEN=20(OQ-71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../specs/recognition-contract/spec.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md b/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md index cc90ead..c7c8b10 100644 --- a/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md +++ b/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md @@ -22,10 +22,11 @@ #### Scenario: Сообщение не по схеме - **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`), но из тела извлекаются - корректные `job_id` и `file_id`; `attempt` для `Nats-Msg-Id` берётся из тела или из заголовка `Nats-Msg-Id` запроса, - а если его нет ни там, ни там — `scan-gateway` подставляет 1, `tabular-parser` не отвечает + корректные `job_id` и `file_id` - **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` **без предшествующего ack - `processing`** и не падает + `processing`** и не падает; `attempt` для `Nats-Msg-Id` результата берётся из тела запроса, при его отсутствии + реализации расходятся (`tabular-parser` берёт `attempt` из заголовка `Nats-Msg-Id` запроса или не отвечает, + `scan-gateway` подставляет 1) — см. OQ-71 #### Scenario: Нечитаемый запрос - **WHEN** сервис получает сообщение, из которого нельзя извлечь `job_id` (не JSON, не объект, `job_id` отсутствует или @@ -106,8 +107,9 @@ v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `f - **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH` **без ack `processing`**: ответом на запрос служит сам финальный конверт, ядро закрывает им задание (`published → error`), и повторной доставки запроса не будет. Конверт публикуется только если из тела извлекаются корректные `job_id` и `file_id`; иначе — как для нечитаемого - запроса (ответа нет, ядро закроет задание по `NO_ACK` / `dlq`). `attempt` для `Nats-Msg-Id` берётся из тела или из - заголовка `Nats-Msg-Id` запроса; если его нет — `scan-gateway` подставляет 1, `tabular-parser` не отвечает + запроса (ответа нет, ядро закроет задание по `NO_ACK` / `dlq`). `attempt` для `Nats-Msg-Id` берётся из тела запроса; при его + отсутствии реализации расходятся (`tabular-parser` берёт `attempt` из заголовка `Nats-Msg-Id` запроса или не отвечает, + `scan-gateway` подставляет 1) — см. OQ-71 ### Requirement: Идемпотентность результатов без брокера Публикующий SHALL ставить заголовок `Nats-Msg-Id` на каждое сообщение (запрос `:`, ack -- 2.47.2 From 56740972675eacf52d9981c32aa023670178af40 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:17:53 +0400 Subject: [PATCH 61/68] =?UTF-8?q?docs(dev):=20=D0=BF=D1=80=D0=B8=D1=87?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=20=D0=BD=D0=B5=20=D0=B7=D0=B0=D0=BF=D1=83?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=D1=82=D1=8C=20make=20-j=20=E2=80=94=20=D0=BF?= =?UTF-8?q?=D0=B0=D1=80=D0=B0=D0=BB=D0=BB=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BF=D1=80=D0=B5=D1=80=D0=B5=D0=BA=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=82=D1=8B=20check,=20=D0=B0=20=D0=BD=D0=B5=20check-ful?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- dev/Makefile.common | 3 ++- dev/checks.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dev/Makefile.common b/dev/Makefile.common index 37b03ce..f9be8ee 100644 --- a/dev/Makefile.common +++ b/dev/Makefile.common @@ -21,7 +21,8 @@ # test-integration docker-build web-check). Собственные цели сервиса (migrate, seed, web-check ...) # определяются в его Makefile, не здесь. # -# Запускайте без `make -j`: шаги check-full вызываются последовательно через $(MAKE) и останавливаются на первой ошибке. +# Запускайте без `make -j`: шаги check-full вызываются последовательно через $(MAKE) и при -j, но `check: lint test` — +# пререквизиты, и при -j `lint` и `test` идут параллельно, поэтому остановка на первой упавшей проверке не гарантируется. VENV ?= api/.venv BIN ?= $(CURDIR)/$(VENV)/bin diff --git a/dev/checks.md b/dev/checks.md index f53c388..37176ff 100644 --- a/dev/checks.md +++ b/dev/checks.md @@ -20,13 +20,13 @@ | `make lint` | `ruff check .`, `ruff format --check .`, `mypy src` (в `arm-core` — `mypy src scripts`) | нет | | `make test` | `pytest -m "not integration"`: юнит-тесты; **включает тест идентичности вендоренного контракта** `api/contracts/` с `../docs/contracts` | нет | | `make test-integration` | `pytest -m integration` против инфраструктуры compose (PostgreSQL 16, NATS, MinIO; бакет `arm-ka` должен существовать, его создаёт `make up`) | да, `make up` | -| `make check` | `make lint` затем `make test`; останавливается на первой упавшей проверке | нет | +| `make check` | `make lint` затем `make test`; останавливается на первой упавшей проверке (только при последовательном запуске: `lint` и `test` — пререквизиты, при `make -j` они идут параллельно) | нет | | `make check-full` | `make check` + `make test-integration` + сборка docker-образа (`docker build`); для `arm-core` — расширенный набор, см. ниже | да, `make up` и Docker | | `make up` | поднимает инфраструктуру из `../docs/dev/docker-compose.yml`; постоянные сервисы `postgres`, `nats`, `minio` | Docker | Сводка шагов `make check`: `ruff check .` → `ruff format --check .` → `mypy src` (в `arm-core` — `mypy src scripts`) → `pytest -m "not integration"`. -Любой ненулевой код останавливает цель на этом шаге, следующие не выполняются. `make check` не требует Docker, +Любой ненулевой код останавливает цель на этом шаге, следующие не выполняются. Запускайте без `-j`: при параллельной сборке шаги идут одновременно. `make check` не требует Docker, PostgreSQL, NATS и сети. ### `make up` -- 2.47.2 From 5456a7df3be0f3cd8424a86b9141ea516e8bf630 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:17:53 +0400 Subject: [PATCH 62/68] =?UTF-8?q?docs(context):=2003-architecture=20?= =?UTF-8?q?=E2=80=94=20ack=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D1=83?= =?UTF-8?q?=D0=B5=D1=82=D1=81=D1=8F=20=D0=BD=D0=B0=20=D0=BA=D0=BE=D1=80?= =?UTF-8?q?=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D1=8B=D0=B9=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- context/03-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context/03-architecture.md b/context/03-architecture.md index a11ac65..fa73c7f 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -58,7 +58,7 @@ **На платформе NATS без JetStream** (ответ 03.10.2026): нет durable consumer'ов, серверного дедупа и redelivery. Поэтому гарантии доставки — на уровне приложения: сервисы подписаны queue group'ами (`tabular-parser`, `scan-gateway`), -ядро — queue group `arm-core` на `recognition.result`; сервис сразу по получении запроса публикует ack `processing`; +ядро — queue group `arm-core` на `recognition.result`; сервис сразу по получении корректного запроса публикует ack `processing`; ядро, не получив ack за `ACK_TIMEOUT_SECONDS` (30), публикует запрос повторно с тем же `Nats-Msg-Id`, не более `OUTBOX_MAX_REPUBLISH` (3) раз, затем `dlq`. Сервис держит process-local множество `job_id` в работе и на повторный запрос отвечает ack'ом без второй обработки. Публикация ack и результата сервисом при временной ошибке повторяется -- 2.47.2 From 1568d49cd999dd3f343ae08a623d14971e069e0e Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:24:35 +0400 Subject: [PATCH 63/68] =?UTF-8?q?chore:=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=206.3?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md | 2 +- openspec/changes/slice-0-skeleton/tasks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md index 8949bf1..5d7ae4b 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md @@ -41,5 +41,5 @@ ### Task 7: [6.3] [docs] Итоговая проверка -- [ ] [docs] Итоговая проверка: `openspec validate slice-0-skeleton --strict` зелёный в четырёх репозиториях; ссылки между документами ведут на существующие файлы (`grep` по именам файлов) +- [x] [docs] Итоговая проверка: `openspec validate slice-0-skeleton --strict` зелёный в четырёх репозиториях; ссылки между документами ведут на существующие файлы (`grep` по именам файлов) diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 0a2f271..5f710f7 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -72,5 +72,5 @@ - [x] 6.2 [docs] `07-open-questions.md`: OQ по итогам Среза 0 (контракт `data_type = NU` vs enum `osv_data_type`; обязательность ОСВ расширенного подхода; источник полного плана счетов 1С) и расхождения макета из задачи B; статусы допущений -- [ ] 6.3 [docs] Итоговая проверка: `openspec validate slice-0-skeleton --strict` зелёный в четырёх репозиториях; ссылки +- [x] 6.3 [docs] Итоговая проверка: `openspec validate slice-0-skeleton --strict` зелёный в четырёх репозиториях; ссылки между документами ведут на существующие файлы (`grep` по именам файлов) -- 2.47.2 From b2f8aaa17e2d31800ea3393020e295bb72f28653 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 07:26:37 +0400 Subject: [PATCH 64/68] =?UTF-8?q?docs:=20.env.example=20=E2=80=94=20scan-g?= =?UTF-8?q?ateway=20=D1=82=D0=BE=D0=B6=D0=B5=20=D0=BE=D1=82=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B3=D0=B0=D0=B5=D1=82=20OPENBAO=5F=5FDATABASE=5F=5F*?= =?UTF-8?q?=20(N-1=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В OpenBAOSettings scan-gateway нет поля DATABASE (extra_forbidden), поэтому общий dev/.env.example годится только arm-core как есть. Исправлены комментарий в .env.example, раздел dev/README.md, решение №44 в design.md и OQ-84. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- context/07-open-questions.md | 2 +- dev/.env.example | 3 ++- dev/README.md | 2 +- openspec/changes/slice-0-skeleton/design.md | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/context/07-open-questions.md b/context/07-open-questions.md index 526c9a4..50e6ae4 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -89,7 +89,7 @@ | 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 человеку (сервисы не правились) | | OQ-82 | `.dockerignore` в `arm-core` не исключает `api/.env.test`: файл попадает в образ (у `tabular-parser` и `scan-gateway` исключён). Исправлять? | arm-core `.dockerignore`; docs: T3.2 (задача 3 фазы 2) | срез 0 (образы), прод-сборка | допущение принято: безвредно (в файле стандартные dev-учётные данные compose, не секреты), в Срезе 0 не правилось; добавить исключение в arm-core — follow-up | | 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-84 | Запуск сервисов из venv без Nexus (`make venv` + `python run_server.py` из `api/` с `.env` из `docs/dev/.env.example`, healthcheck по `localhost`) в Срезе 0 не проверялся ни в одном из трёх сервисов. Известно по чтению конфигов (design #44): `.env.example` в нынешнем виде отвергнет `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-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 доступных только для чтения) | Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения; `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. diff --git a/dev/.env.example b/dev/.env.example index 79a296c..c50f3ff 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -49,7 +49,8 @@ OPENBAO__NATS__NATS_SEED= OPENBAO__NATS__NATS_JWT= # SMB — заглушки (в Срезе 0 не используется); блок необязателен для arm-core и scan-gateway (режим local). -# tabular-parser неизвестные ключи OPENBAO__* отвергает: для него удалите блоки DATABASE и SMB (см. dev/README.md) +# Блок OPENBAO__DATABASE__* знает только arm-core: scan-gateway и tabular-parser неизвестные ключи OPENBAO__* отвергают +# (extra_forbidden) — для scan-gateway удалите блок DATABASE, для tabular-parser удалите блоки DATABASE и SMB (см. dev/README.md) OPENBAO__SMB__SMB_HOST=localhost OPENBAO__SMB__SMB_PATH=share OPENBAO__SMB__SMB_LOGIN=user diff --git a/dev/README.md b/dev/README.md index 630f05a..0b94c19 100644 --- a/dev/README.md +++ b/dev/README.md @@ -344,7 +344,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). По чтению конфигов: `tabular-parser` отвергает неизвестные ключи `OPENBAO__*`, поэтому в его `.env` нужно удалить строки `OPENBAO__DATABASE__*` и `OPENBAO__SMB__*`; для MinIO по `http` нужен `OPENBAO__MINIO__MINIO_SECURE=false` (в `.env.example` его нет, по умолчанию `true`). +`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`). ## Интеграционные тесты сервисов против compose (черновик) diff --git a/openspec/changes/slice-0-skeleton/design.md b/openspec/changes/slice-0-skeleton/design.md index 9b3cdc9..07bc89d 100644 --- a/openspec/changes/slice-0-skeleton/design.md +++ b/openspec/changes/slice-0-skeleton/design.md @@ -175,4 +175,4 @@ PostgreSQL 16, схемы создавать можно. Остаются (не | 41 | 2026-10-04 | В реестр **не** внесены, а оставлены в отчёте задачи 6 (кандидаты в follow-up issue при закрытии): scan-gateway п. 7 (рестарт NATS не автоматизирован), п. 8 (`stop()` не публикует конверты принятым заданиям), п. 9 (`/healthcheck` всегда 200), п. 13 (`DEBUG` не используется), п. 14 (`ResourceWarning` aiohttp), п. 15 (SMB без `connection_timeout`); tabular P-4, scan-gateway п. 10 и 16 (порты, `SCAN_GATEWAY_PORT`, устаревший README) — закрыты задачами 3–5 и `dev/README.md`; O-22 и T3.7/O-20 — процессные пункты задачи 7 | это инженерные доработки или уже исправленные расхождения документов, вопроса к человеку/аналитикам в них нет; реестр остаётся списком решений, которые принимает человек | пункты могут потеряться, если follow-up issue не будет заведена (чеклист закрытия, п. 6): они перечислены в отчёте с маркерами | | 42 | 2026-10-04 | Формулировка статусов в новых строках: «допущение принято: …» — в коде/спеке Среза 0 действует рабочее допущение, вопрос открыт для подтверждения; «вопрос: …» — человек должен принять решение, допущение Среза 0 указано после двоеточия (аудит на уровне БД, `attempt + 1`, образцы ответов ИС, формат `data_ocr`, имя файла ответа, `.part` в `Input`) | правило файла: агент вопросы не закрывает, а помечает допущение; слово «закрыт» нигде не использовано | различение статусов — рекомендация, не контракт; при несогласии префикс правится в реестре | | 43 | 2026-10-04 | Сценарий spec `dev-environment` «Запуск из venv против compose без Nexus» (`make venv` + `python run_server.py` + healthcheck) в Срезе 0 **не прогонялся**; раздел «Запуск сервисов из venv без Nexus» в `dev/README.md` остаётся черновиком, вопрос вынесен в OQ-84 (кандидат в follow-up issue) | проверка требует создать `.venv` внутри сервисных репозиториев (`make venv` пишет в `api/.venv`), а в фазе 2 они были доступны только для чтения (их worktree с открытыми PR не правятся); сквозная проверка шла на профиле `services` (контейнеры из тех же исходников) | сценарий spec может оказаться неработоспособным (набор переменных, `requirements.txt` без `raisa_*`, порты) — разработчик без Docker для сервисов узнает об этом при первом запуске; правится README или `.env.example` | -| 44 | 2026-10-04 | Уточнение к №18: блок `OPENBAO__SMB__*` **не обязателен**. В `arm-core` `OpenBAOSettings.SMB = SMBSettings()` с пустыми значениями по умолчанию (`api/src/core/config.py`), в `scan-gateway` блок необязателен в режиме `SCAN_HOTFOLDER_MODE=local` (по умолчанию) и обязателен целиком при `smb`, в `tabular-parser` блока нет, и `OpenBAOSettings` отвергает неизвестные ключи (`extra_forbidden`; проверено на копии его `config.py` с `.env` из `dev/.env.example`: падает на `OPENBAO.smb`, так же упадёт на `OPENBAO__DATABASE__*`) — `extra="ignore"` относится только к верхнему уровню `Settings`. Заглушки в `dev/.env.example` оставлены; комментарий и README исправлены и говорят, что `.env.example` годится для `arm-core` и `scan-gateway`, а для `tabular-parser` из него нужно убрать блоки `DATABASE` и `SMB` | в момент решения №18 `Settings` arm-core требовал блок; затем в arm-core блок сделан необязательным (в Срезе 0 SMB ядру не нужен), а формулировка осталась в `.env.example` и README | ложное утверждение «обязателен» заставляло бы переносить заглушки в окружение, где они не нужны; цена низкая — у `arm-core` и `scan-gateway` заглушки безвредны, а `tabular-parser` с неотредактированным `.env.example` не стартует (описано в README, часть OQ-84) | +| 44 | 2026-10-04 | Уточнение к №18: блок `OPENBAO__SMB__*` **не обязателен**. В `arm-core` `OpenBAOSettings.SMB = SMBSettings()` с пустыми значениями по умолчанию (`api/src/core/config.py`), в `scan-gateway` блок необязателен в режиме `SCAN_HOTFOLDER_MODE=local` (по умолчанию) и обязателен целиком при `smb`, в `tabular-parser` блока нет, и `OpenBAOSettings` отвергает неизвестные ключи (`extra_forbidden`; проверено на копии его `config.py` с `.env` из `dev/.env.example`: падает на `OPENBAO.smb`, так же упадёт на `OPENBAO__DATABASE__*`) — `extra="ignore"` относится только к верхнему уровню `Settings`. Заглушки в `dev/.env.example` оставлены; комментарий и README исправлены и говорят: `.env.example` годится для `arm-core` как есть; для `scan-gateway` из него нужно убрать блок `DATABASE` (в его `OpenBAOSettings` поля нет, `extra_forbidden`), для `tabular-parser` — блоки `DATABASE` и `SMB` (уточнено 04.10 по ревью, N-1) | в момент решения №18 `Settings` arm-core требовал блок; затем в arm-core блок сделан необязательным (в Срезе 0 SMB ядру не нужен), а формулировка осталась в `.env.example` и README | ложное утверждение «обязателен» заставляло бы переносить заглушки в окружение, где они не нужны; цена низкая — у `arm-core` заглушки безвредны, а `scan-gateway` и `tabular-parser` с неотредактированным `.env.example` не стартуют (описано в README, часть OQ-84) | -- 2.47.2 From f809cc936bed754ad28ccbda366cb13b47dedcd3 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 13:33:55 +0400 Subject: [PATCH 65/68] =?UTF-8?q?docs:=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D1=8B=20=D1=87=D0=B5=D0=BB=D0=BE=D0=B2=D0=B5=D0=BA=D0=B0=2004.?= =?UTF-8?q?10.2026=20=E2=80=94=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0=20=D0=98=D0=A1=20=D0=BF?= =?UTF-8?q?=D0=BE=20aep=5F3/aep=5F6,=20OQ-13/14/56/76=E2=80=9379,=20JetStr?= =?UTF-8?q?eam=20=D0=BA=D0=B0=D0=BA=20=D0=B2=D0=BE=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=20=D0=BA=20=D0=BF=D0=BB=D0=B0=D1=82=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §11 05-domain-rules.md переписан по реальным ответам ИС: data_ocr — объект, table_structure — список таблиц, успех — error.code == "", в каталоге ответа несколько json (склейка), confidence 0–100. OQ-13/14/77/78/79 закрыты или уточнены, OQ-56 — осознанный техдолг, OQ-76 — частично (образцы добавлены), новый OQ-85 — возможность JetStream на платформе (проверить позже). OQ-73 остаётся открытым. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- context/05-domain-rules.md | 34 ++++++++++++++++++++++++---------- context/07-open-questions.md | 17 +++++++++-------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 589215d..ed8e5c1 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -363,17 +363,31 @@ Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят толь ## §11. Маппинг JSON ИС Сканирование (FNR01/FNR02) → формы -Контракт (по ТЗ и фикстурам `aep_*.json`): `file_uid`; `error {code, message} | null` -(FILE_READ_ERROR, INVALID_FORMAT, INVALID_DOCUMENT, RECOGNITION_FAILED, UNKNOWN_ERROR); `data_ocr[]` по документам: -`label.form_name` («Бухгалтерский баланс» → BALANCE, «Отчет о финансовых результатах» → INCOME), `organization.full_name`, -`inn.inn`, `units.measurement`, `date`, `table_structure.rows[]` (`row_index`, `cells[] {column_index, value, confidence}`). -Строка `rows[0]` — заголовки колонок: cells[0] «Наименование показателя», [1] «Код», [2..] даты колонок. -`confidence` — в ТЗ доли 0..1, в реальных файлах проценты 0..100 **[открыто]** — нормализовать в `numeric(5,2)` 0–100. +Формат ответа — **по реальным файлам** `fixtures/ocr/aep_3.json`, `aep_3_ofr.json`, `aep_6.json` (проверено 04.10.2026; ТЗ +и первая редакция этого параграфа расходятся с ними — см. OQ-13, OQ-14, OQ-77): -Маппинг: `data_ocr[i]` → `document` (BALANCE/INCOME, `locator = pages:…`) + `financial_statement_meta` (ИНН, имя, -units, report_date, okud) + `financial_statement_column` (из `rows[0].cells[2..]`, `period_date` распарсить из -«На 30 сентября 2025 г.», `is_current` = первая) + `financial_statement_line` (`cells[1]` код, `cells[0]` имя; только -строки с кодом) + `financial_statement_value` (число: убрать пробелы/`-`, скобки → минус; `confidence`; `is_recognized`). +- Каталог ответа `/` содержит **несколько json** — по одному на распознанный документ файла (пример: `aep_3.json` — + успех, `aep_3_ofr.json` — ошибка по второму документу того же PDF). Шлюз читает все json каталога и склеивает в один ответ + (`raw/{job_id}.json`: `{job_id, files[{name, response, parse_error}]}`); ошибка одного файла не отменяет остальные. +- Корень файла: `{file_uid, error {code, message}, data_ocr}`. **Успех — `error.code == ""`** (поле `error` не `null`, а + `{"code": "", "message": ""}`); ошибка — непустой `code` (`INVALID_DOCUMENT`, `FILE_READ_ERROR`, `INVALID_FORMAT`, + `RECOGNITION_FAILED`, `UNKNOWN_ERROR`), `data_ocr` с пустыми полями и `table_structure: []`. +- `data_ocr` — **объект** (не список): `label`, `organization`, `inn`, `units`, `date` — каждый `{form_name, confidence}` + (в `form_name` шумный OCR-текст; `date.form_name` ненадёжна, ИНН может быть мусором), `table_structure` — **список таблиц** + `{label {form_name, confidence}, rows[]}`: `label.form_name` «Бухгалтерский баланс» → BALANCE, «Отчет о финансовых + результатах» → INCOME; одна таблица = один `document`, в одном json их может быть несколько. +- `rows[]`: `row_index`, `cells[] {column_index, value, confidence}`; `column_index` не обязательно подряд (бывают 0, 1, 2, 4, 5), + `value` — сырой OCR-текст (мусор в ячейках, переносы строк, скобки для отрицательных, пробелы-разделители тысяч). + В `aep_6.json` `rows[0]` — строка-шапка («Наименование показателя», «Код», «На 30 июня 2025 г», …); в `aep_3.json` шапки + **нет** — первая строка данных. Бывают строки без кода (расшифровки «в том числе…», OQ-36) и код-строка внутри ячейки + наименования («АКТИВ / I. Внеоборотные активы / Гудвил» с кодом 1105). +- `confidence` — **числа 0–100** (в ТЗ — доли 0..1; в реальных файлах проценты) → `numeric(5,2)` как есть. + +Маппинг (Срез 3): таблица `table_structure[i]` → `document` (BALANCE/INCOME, `locator = pages:…`) + `financial_statement_meta` +(ИНН, имя, units, report_date, okud — из `organization/inn/units/date.form_name`, с низкой уверенностью → `NEEDS_CHECK`) + +`financial_statement_column` (из шапки `rows[0]`, если она есть; `period_date` распарсить из «На 30 июня 2025 г», `is_current` = +первая; без шапки — колонки без дат, документ `NEEDS_CHECK`) + `financial_statement_line` (`cells[1]` код, `cells[0]` имя; +только строки с кодом) + `financial_statement_value` (число: убрать пробелы/`-`, скобки → минус; `confidence`; `is_recognized`). Сверки: ИНН/имя vs `legal_entity` → `document.counterparty_check`; дата vs период версии → `period_check`. --- diff --git a/context/07-open-questions.md b/context/07-open-questions.md index 50e6ae4..fa8a627 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -18,8 +18,8 @@ | OQ-10 | Гудвилл: входит ли стр. 1350 (добавочный капитал) в базу участия; УК из ББ (1310) или из ОСВ 80/83 | DBML, DOC-259025, FNR03 | срез 6 | допущение: 1310 из ББ; 1350 — настраиваемый флаг, по умолчанию выключен | | OQ-11 | Строка «ППА» в КФО (правило № 1, когда у А нет ОСВ 03) — что это и куда | FNR03 | срез 6 | не реализовывать | | OQ-12 | Формулы ОФР: знаки 2410/2420/2430 по ФСБУ 4/2023 vs 66н; состав 1100 (1120–1140) | DOC-221281 | срез 3 | допущение: формулы §7, строки без значения = 0 | -| OQ-13 | `confidence` от ИС Сканирование — доли 0..1 или проценты 0..100; имя поля организации (`full_name` vs `form_name`) | FNR02 vs `aep_*.json` | срез 3 (scan-gateway) | допущение: нормализовать в 0–100 по фактическим файлам | -| OQ-14 | `reporting_date` в JSON ИС Сканирование — «ждём обновлённый JSON» | FNR01 | срез 3 | допущение: парсить из заголовка «на 30 сентября 2025 г.» | +| OQ-13 | `confidence` от ИС Сканирование — доли 0..1 или проценты 0..100; имя поля организации (`full_name` vs `form_name`) | FNR02 vs `aep_*.json` | срез 3 (scan-gateway) | **закрыт для scan-gateway (04.10.2026, по `aep_3.json`, `aep_6.json`):** `confidence` — числа 0–100 (не доли); название организации — `data_ocr.organization.form_name` (шумный OCR-текст), ИНН — `inn.form_name`, единицы — `units.form_name`, дата — `date.form_name` (в `aep_6.json` мусор); `full_name`/`inn.inn`/`units.measurement` из ТЗ в реальных файлах нет. Нормализация — Срез 3 | +| OQ-14 | `reporting_date` в JSON ИС Сканирование — «ждём обновлённый JSON» | FNR01 | срез 3 | **уточнено (04.10.2026):** отдельного поля `reporting_date` в ответе нет; `date.form_name` ненадёжен (в `aep_6.json` — «Общество с ограниченной ответственностью», в `aep_3.json` — «2\|0\|2\|4\|»); даты колонок — в строке-шапке `rows[0]` таблицы («На 30 июня 2025 г» в `aep_6.json`), в `aep_3.json` шапки нет. Допущение прежнее: парсить из шапки таблицы, при её отсутствии — `NEEDS_CHECK`; решение — Срез 3 | | OQ-15 | Таймаут OCR 5 мин / 0 повторов (ТЗ 3) vs outbox с повторами (DBML) | DOC-037736 | срез 3 | решено DBML: `max_attempts` OCR = 1, парсер = 3 | | OQ-16 | Матрица прав (ТЗ сноска 29 «будет разработана»); роли макета (Аналитик ДКБ, Наблюдатель) ≠ ТЗ (ANL-034) | ТЗ 8.1, макет | срез 1 | допущение: seed по §10 правил | | OQ-17 | `user_role` M:N или одна роль на пользователя | DBML | срез 1 | допущение: M:N | @@ -61,7 +61,7 @@ | OQ-53 | Контракт v1.0: схемы нигде не задают `additionalProperties: false` (кроме `columns` ОСВ). Считать ли неизвестные поля допустимыми расширениями? | `contracts/*.schema.json`; arm-core: предл. 6 (T20) | контракт v1.x; все три сервиса | допущение принято: модели ядра `extra = "ignore"` (строже схемы быть нельзя — ядро отвергнет совместимого пира); изменение — через MINOR контракта | | OQ-54 | Параметры JWT РАИСА неизвестны: JWKS URL / публичный ключ, `iss`, `aud`, имя claim с email. Что в продуктивном контуре? Делать ли проверку `aud`/`iss` обязательной в проде? | ТЗ 8.1, `03-architecture.md` (SSO РАИСА); `core/auth.py`; arm-core: предл. 7 (T16.2) | срез 1+ (прод-конфигурация) | допущение принято: настраиваются `AUTH_JWKS_URL`/`AUTH_PUBLIC_KEY` (взаимоисключающие), `AUTH_ISSUER`, `AUTH_AUDIENCE`, `AUTH_EMAIL_CLAIM` (по умолчанию `email`); при пустых `AUTH_AUDIENCE`/`AUTH_ISSUER` `aud`/`iss` не проверяются; предложение: в проде сделать `aud` обязательным и предупреждать при старте, если пусто | | OQ-55 | В enum `audit_action` нет действия для смены статуса задания распознавания (`recognition_job`). Добавлять ли (DBML v5.14) и нужен ли аудит обмена вообще? | DBML v5.13 `audit_action`; arm-core: предл. 8 (R-7, design #20 arm-core) | срез 2 (обмен); кандидат DBML v5.14 | допущение принято: Срез 0 не пишет `audit_event` из обмена `recognition_job`; сервис аудита (`services/audit.py`) готов и покрыт тестами; если нужно — действие в v5.14 и вызовы в `recognition_exchange.py` | -| OQ-56 | Append-only аудит обеспечен только на уровне кода (единственная точка записи, статический тест на UPDATE/DELETE по `AuditEvent`). Нужна ли защита на уровне БД (триггер или `REVOKE UPDATE, DELETE` для роли приложения)? Вопрос поднят дважды (в том числе финальным ревью ветки). | инвариант «Append-only аудит» (`../CLAUDE.md`); `services/audit.py`; arm-core: предл. 9 и дополнение к нему (задача 21) | миграции, права БД; платформа | вопрос: допущение принято на Срез 0 — только код и тесты; решение по триггеру/правам — с командой платформы до прода | +| OQ-56 | Append-only аудит обеспечен только на уровне кода (единственная точка записи, статический тест на UPDATE/DELETE по `AuditEvent`). Нужна ли защита на уровне БД (триггер или `REVOKE UPDATE, DELETE` для роли приложения)? Вопрос поднят дважды (в том числе финальным ревью ветки). | инвариант «Append-only аудит» (`../CLAUDE.md`); `services/audit.py`; arm-core: предл. 9 и дополнение к нему (задача 21) | миграции, права БД; платформа | **решено человеком 04.10.2026: осознанный техдолг.** Append-only на уровне кода и тестов; защита на уровне БД (триггер или `REVOKE UPDATE, DELETE`) — позже, вместе с платформой, до прода | | OQ-57 | Счётчик повторных публикаций запроса (`ACK_TIMEOUT_SECONDS` × `OUTBOX_MAX_REPUBLISH`) хранится в `recognition_job.metrics.republish_count` (jsonb), колонки в DBML нет; при каждой повторной публикации `published_at` перезаписывается временем последней, время первой не сохраняется. Нужны ли колонка счётчика и время первой публикации (v5.14)? | `services/recognition_exchange.py` (design #6 arm-core); arm-core: предл. 10 (T23, T23.1); docs: O-19 (задача 4 фазы 2) | срез replay; кандидат DBML v5.14 | допущение принято: счётчик в `metrics.republish_count`, `attempt` при повторе не растёт; `published_at` — время последней публикации (`dlq` через (`OUTBOX_MAX_REPUBLISH`+1)×`ACK_TIMEOUT_SECONDS` ≈ 120 с, описано в `dev/README.md`); при будущем replay счётчик и `reply_deadline` надо сбрасывать | | OQ-58 | Семантика сроков и ошибок обмена в ядре: (а) `reply_deadline` по умолчанию = `now + 5 мин` для обоих pipeline (`tabular`, `image`), если у задания NULL; в DBML срок есть только для OCR («OCR — 5 мин»), для парсера значения нет; (б) при невозможности собрать запрос по контракту задание уходит в `error` / `INVALID_REQUEST`; (в) при `ACK_TIMEOUT × (MAX_REPUBLISH+1) ≥ 5 мин` статус `dlq` недостижим (раньше сработает `timeout`). Верно ли? Какой срок у `tabular-parser`? | DBML `recognition_job.reply_deadline` note; `recognition_exchange.py` (`DEFAULT_REPLY_TIMEOUT`); arm-core: предл. 11 и дополнение (T23.4) | срез 2–3; настройка таймаутов | допущение принято: как описано (дефолты 30 с × 3 укладываются в 5 мин); сроки по pipeline уточнить с командами парсера и шлюза | | OQ-59 | Email пользователя сопоставляется с `app_user.email` по `lower(email)`; при двух записях, различающихся регистром, — 403 (fail-closed). Добавить `CHECK`/уникальный индекс по `lower(email)` в DBML? | `core/auth.py`, `api/dependencies/auth.py`; arm-core: предл. 12 (R-17, design #29 arm-core) | срез 1 (админ-экран); кандидат DBML v5.14 | допущение принято: уникальность `email` регистрозависимая (как в DBML); регистронезависимая — в v5.14 | @@ -78,18 +78,19 @@ | OQ-70 | Перед чтением файла результата маппер должен проверять, что ключ начинается с `results/{file_id}/{job_id}/`, а бакет — бакет роли `results` (иначе сервис может «подсунуть» чужой объект). Закрепить это в контракте и `03-architecture.md`? | контракт `recognition-result-message` (`result_s3`); arm-core `_register_result_object` (`recognition_exchange.py`); arm-core: предл. 21 (финальное ревью) | срез 2 (маппинг); контракт v1.x | допущение принято: в Срезе 0 результат только регистрируется в `s3_object`, не читается; проверку реализует маппер Среза 2 | | OQ-71 | Запрос без читаемых `job_id` / `file_id` / `attempt` остаётся без ответа (конверт собрать нельзя): сервис логирует ошибку и отбрасывает запрос, ядро закрывает задание `dlq` / `NO_ACK` примерно через 120 с. Расходятся и реализации: при нечитаемом `attempt` `tabular-parser` берёт его из заголовка `Nats-Msg-Id` запроса или ничего не отправляет, `scan-gateway` подставляет `attempt = 1` и публикует конверт. Оставить так, выровнять реализации или сделать `file_id` необязательным в конверте `error` (MINOR 1.1)? | `recognition-contract` spec, сценарий «Нечитаемый запрос»; tabular-parser: P-1 (задача 4.3); docs: O-24, design #35 | контракт v1.x; срез 2–3 | допущение принято: молчание сервиса (design #35); реализации выровнять в Срезе 2/3, влияние только на битые запросы | | OQ-72 | Ack при несовместимой версии запроса: ранняя формулировка контракта и `03-architecture.md` требовали «`SCHEMA_MISMATCH` и ack», оба сервиса публикуют `error SCHEMA_MISMATCH` без ack (ядро принимает результат из `published`, ack перед ним ничего не добавляет). Спека и документ приведены к поведению сервисов — подтвердить. | tabular-parser: P-2; scan-gateway: п. 2 таблицы предложений (T12.13, R-16); docs: design #34 | контракт v1.x; срез 2 | допущение принято: без ack, результат `SCHEMA_MISMATCH` закрывает задание (design #34); если ядро начнёт требовать ack перед результатом — правка одной строки в воркерах и спеке | -| OQ-73 | Повторная попытка (`attempt + 1`) того же `job_id` после ошибки или результата: оба сервиса дедуплицируют по `job_id` (LRU `DONE_CACHE_SIZE`), поэтому replay с новым `attempt` получает только ack и не обрабатывается, на единственном инстансе задание повисает до `reply_deadline` → `timeout`. По `03-architecture.md` replay — «сброс в `queued` с `attempt + 1`». Сделать ключ дедупликации `(job_id, attempt)`; повтор во время обработки — только ack? | tabular-parser: P-3 (задача 4.3); scan-gateway: п. 17 таблицы предложений (I-3, R-17); `03-architecture.md` (replay) | срез 3+ (replay); контракт v1.x | вопрос: допущение принято на Срез 0 — дедуп по `job_id`, ядро повторяет только тем же `attempt`; при одобрении — одна строка в воркере (ключ LRU) | +| OQ-73 | Повторная попытка (`attempt + 1`) того же `job_id` после ошибки или результата: оба сервиса дедуплицируют по `job_id` (LRU `DONE_CACHE_SIZE`), поэтому replay с новым `attempt` получает только ack и не обрабатывается, на единственном инстансе задание повисает до `reply_deadline` → `timeout`. По `03-architecture.md` replay — «сброс в `queued` с `attempt + 1`». Сделать ключ дедупликации `(job_id, attempt)`; повтор во время обработки — только ack? | tabular-parser: P-3 (задача 4.3); scan-gateway: п. 17 таблицы предложений (I-3, R-17); `03-architecture.md` (replay) | срез 3+ (replay); контракт v1.x | вопрос: допущение принято на Срез 0 — дедуп по `job_id`, ядро повторяет только тем же `attempt`; при одобрении — одна строка в воркере (ключ LRU); **человеком 04.10.2026 не рассмотрен — вопрос остаётся открытым** | | OQ-74 | `PUBLISH_RETRIES` = одна первая попытка + до 3 повторов (всего до 4) с тем же `Nats-Msg-Id`; у `scan-gateway` тип `PositiveInt` (повторы нельзя отключить настройкой), у `tabular-parser` `NonNegativeInt` (0 допустим); паузы 0,5 с и 2,0 с. Согласовать единую семантику (повторы или всего попыток) и тип? | scan-gateway: п. 1 таблицы предложений (R-13, T11.1); docs: O-25, design #36 | контракт v1.x; настройки сервисов | допущение принято: «1 + 3 повтора» (design #36), расхождение «3 против 4» обмен не ломает (дедуп по `Nats-Msg-Id`); выровнять тип — косметика конфигурации | | OQ-75 | `reply_deadline` проверяет только `scan-gateway` (`error TIMEOUT` до отправки в ИС и потолок ожидания ответа); `tabular-parser` обрабатывает просроченный запрос. Пример `contracts/examples/request-image.json` имеет `reply_deadline` в прошлом (`2026-10-03T12:45:00Z`) и даёт `TIMEOUT` при ручном прогоне. Единая обязанность сервисов и свежая дата примера? | scan-gateway: п. 12 таблицы предложений (O-e); docs: O-26, design #37 | контракт v1.x; срез 2 | допущение принято: срок — дело сервиса, пример — иллюстрация формы (оговорка в spec и `03-architecture.md`, design #37); обновить дату можно одним PR с пересинхронизацией копий в трёх репозиториях | -| OQ-76 | Обе реальные фикстуры `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС (`error.code = INVALID_DOCUMENT`, пустой `table_structure`). Успешный путь `image` (`done` / `needs_check`, файл в `results/…`, `s3_object` с `bucket_role = results`) сквозной проверкой не покрыт; в `scan-gateway` проверяется только синтетическим ответом. Нужны образцы успешных ответов Content AI. | scan-gateway: п. 3 таблицы предложений (O-1, R-10, design #32); docs: O-15 (задача 4 фазы 2), design #31 | срез 3 (OQ-13/14, нормализатор); fixtures | вопрос: допущение принято на Срез 0 — успех проверяется синтетическим ответом, `NullNormalizer` пропускает ошибку ИС; образцы запросить у владельцев ИС Сканирование | -| OQ-77 | §11 `05-domain-rules.md` описывает `data_ocr[]` (список), а в фикстурах `data_ocr` — объект с полями `label/organization/inn/units/date/table_structure`. Какой формат фактический на стенде ИС (связано с OQ-13, OQ-14)? | `05-domain-rules.md` §11; scan-gateway: п. 4 таблицы предложений (O-1, T3.2) | срез 3 (нормализатор) | вопрос: допущение принято на Срез 0 — формат не интерпретируется; решение нужно до нормализатора Среза 3 | -| OQ-78 | Шлюз читает ровно `//.json` (спека); старый код читал все файлы каталога `Output//`. Какие файлы и когда реальная ИС кладёт в `Output`, в каком порядке (момент готовности)? | scan-gateway: п. 5 таблицы предложений (T8.5) | срез 3 (стенд ИС) | вопрос: допущение принято на Срез 0 — имя ответа по спеке; проверить на стенде ИС | -| OQ-79 | Временный файл `.<имя>.part` лежит в `Input` (атомарная запись `.part` + rename): реальная ИС без маски может забрать недописанный файл. Уточнить у владельцев ИС маску и поведение или перенести временный файл в служебный подкаталог рядом с `Input`? | scan-gateway: п. 6 таблицы предложений (R-5, T7/T8) | срез 3 (стенд ИС) | вопрос: допущение принято на Срез 0 — `.part` + rename в `Input`; вариант с подкаталогом `Input/.tmp` или корнем — после ответа владельцев ИС | +| OQ-76 | Обе реальные фикстуры `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС (`error.code = INVALID_DOCUMENT`, пустой `table_structure`). Успешный путь `image` (`done` / `needs_check`, файл в `results/…`, `s3_object` с `bucket_role = results`) сквозной проверкой не покрыт; в `scan-gateway` проверяется только синтетическим ответом. Нужны образцы успешных ответов Content AI. | scan-gateway: п. 3 таблицы предложений (O-1, R-10, design #32); docs: O-15 (задача 4 фазы 2), design #31 | срез 3 (OQ-13/14, нормализатор); fixtures | **частично закрыт человеком 04.10.2026:** добавлены реальные образцы — `aep_3.json` (успех: баланс 42 строки + ОФР 18 строк), `aep_3_ofr.json` (второй ответ ИС по тому же файлу, `INVALID_DOCUMENT`), `aep_6.json` (успех: баланс 122 строки + ОФР 49 строк); PR в `arm-ka-fixtures`. Успешный путь `image` на реальных ответах — Срез 3 (нормализатор); транспорт `scan-gateway` проверяется на них в Срезе 0 (склейка нескольких json, `NOT_IMPLEMENTED`) | +| OQ-77 | §11 `05-domain-rules.md` описывает `data_ocr[]` (список), а в фикстурах `data_ocr` — объект с полями `label/organization/inn/units/date/table_structure`. Какой формат фактический на стенде ИС (связано с OQ-13, OQ-14)? | `05-domain-rules.md` §11; scan-gateway: п. 4 таблицы предложений (O-1, T3.2) | срез 3 (нормализатор) | **закрыт человеком 04.10.2026 (по `aep_3.json`, `aep_6.json`):** `data_ocr` — **объект**, один на json-файл (`label`, `organization`, `inn`, `units`, `date` — `{form_name, confidence}`; `table_structure` — **список таблиц** `{label, rows[]}`, одна таблица = один документ БС/ОФР, в одном json их может быть несколько). Успех — `error = {code: "", message: ""}` (не `null`!), ошибка — непустой `code` и пустой `table_structure`. §11 `05-domain-rules.md` исправлен | +| OQ-78 | Шлюз читает ровно `//.json` (спека); старый код читал все файлы каталога `Output//`. Какие файлы и когда реальная ИС кладёт в `Output`, в каком порядке (момент готовности)? | scan-gateway: п. 5 таблицы предложений (T8.5) | срез 3 (стенд ИС) | **закрыт человеком 04.10.2026:** ИС кладёт **все распознанные документы одновременно** в каталог ``, по одному json на документ (пример: `aep_3.json` + `aep_3_ofr.json`); шлюз читает **все** json каталога и склеивает в один ответ (готовность — по стабилизации набора файлов, `SCAN_SETTLE_SECONDS`); ошибка одного документа не отменяет остальные (`metrics.warnings`). Реализуется в Срезе 0 (доработка PR scan-gateway #3) | +| OQ-79 | Временный файл `.<имя>.part` лежит в `Input` (атомарная запись `.part` + rename): реальная ИС без маски может забрать недописанный файл. Уточнить у владельцев ИС маску и поведение или перенести временный файл в служебный подкаталог рядом с `Input`? | scan-gateway: п. 6 таблицы предложений (R-5, T7/T8) | срез 3 (стенд ИС) | **закрыт человеком 04.10.2026:** временный файл записи уходит из `Input` в служебный каталог рядом (`SCAN_TMP_DIR`, по умолчанию `Tmp`, на той же шаре — `rename` остаётся атомарным). Реализуется в Срезе 0 (доработка PR scan-gateway #3) | | 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 человеку (сервисы не правились) | | OQ-82 | `.dockerignore` в `arm-core` не исключает `api/.env.test`: файл попадает в образ (у `tabular-parser` и `scan-gateway` исключён). Исправлять? | arm-core `.dockerignore`; docs: T3.2 (задача 3 фазы 2) | срез 0 (образы), прод-сборка | допущение принято: безвредно (в файле стандартные dev-учётные данные compose, не секреты), в Срезе 0 не правилось; добавить исключение в arm-core — follow-up | | 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-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 | Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения; `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. -- 2.47.2 From 3c1d1d12618fdc5547bd902aeeea8e220e2c8b10 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 13:38:18 +0400 Subject: [PATCH 66/68] =?UTF-8?q?docs:=20=C2=A711=20=E2=80=94=20=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8B=20=D0=B8=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D1=8B=20=D0=98=D0=A1=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=BE=D0=B4=D0=BD=D0=BE=D0=B7=D0=BD=D0=B0=D1=87=D0=BD=D1=8B=20?= =?UTF-8?q?(=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=20=D1=87?= =?UTF-8?q?=D0=B5=D0=BB=D0=BE=D0=B2=D0=B5=D0=BA=D0=B0=2004.10.2026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Тип документа задаёт label таблицы; несколько таблиц одного типа (баланс по страницам) — части одного документа (уникальный индекс document в DBML: один ББ и один ОФР на файл); в одном json может быть несколько таблиц. Допущение вынесено в OQ-77 для подтверждения на реальном ответе. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- context/05-domain-rules.md | 10 ++++++++-- context/07-open-questions.md | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index ed8e5c1..daa8187 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -375,7 +375,12 @@ Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят толь - `data_ocr` — **объект** (не список): `label`, `organization`, `inn`, `units`, `date` — каждый `{form_name, confidence}` (в `form_name` шумный OCR-текст; `date.form_name` ненадёжна, ИНН может быть мусором), `table_structure` — **список таблиц** `{label {form_name, confidence}, rows[]}`: `label.form_name` «Бухгалтерский баланс» → BALANCE, «Отчет о финансовых - результатах» → INCOME; одна таблица = один `document`, в одном json их может быть несколько. + результатах» → INCOME. **Соответствие «таблица ↔ документ» не взаимно однозначно:** в одном json может быть + несколько таблиц (в `aep_3.json` и `aep_6.json` — баланс и ОФР), а несколько таблиц одного типа (например, баланс, разбитый + по страницам на Актив и Пассив) — части одного документа. Тип документа задаёт `label`, а не число таблиц; документ — это + **тип формы на файл** (в DBML индекс `document (stored_file_id, document_type, osv_account_code)` уникален: один ББ и один ОФР + на файл), поэтому таблицы одного `label` из всех json каталога ответа склеиваются по порядку строк в один документ; неизвестный + `label` → `OTHER`. - `rows[]`: `row_index`, `cells[] {column_index, value, confidence}`; `column_index` не обязательно подряд (бывают 0, 1, 2, 4, 5), `value` — сырой OCR-текст (мусор в ячейках, переносы строк, скобки для отрицательных, пробелы-разделители тысяч). В `aep_6.json` `rows[0]` — строка-шапка («Наименование показателя», «Код», «На 30 июня 2025 г», …); в `aep_3.json` шапки @@ -383,7 +388,8 @@ Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят толь наименования («АКТИВ / I. Внеоборотные активы / Гудвил» с кодом 1105). - `confidence` — **числа 0–100** (в ТЗ — доли 0..1; в реальных файлах проценты) → `numeric(5,2)` как есть. -Маппинг (Срез 3): таблица `table_structure[i]` → `document` (BALANCE/INCOME, `locator = pages:…`) + `financial_statement_meta` +Маппинг (Срез 3): таблицы `table_structure[*]` одного `label` (по всем json каталога ответа) → один `document` +(BALANCE/INCOME, `locator = pages:…`) + `financial_statement_meta` (ИНН, имя, units, report_date, okud — из `organization/inn/units/date.form_name`, с низкой уверенностью → `NEEDS_CHECK`) + `financial_statement_column` (из шапки `rows[0]`, если она есть; `period_date` распарсить из «На 30 июня 2025 г», `is_current` = первая; без шапки — колонки без дат, документ `NEEDS_CHECK`) + `financial_statement_line` (`cells[1]` код, `cells[0]` имя; diff --git a/context/07-open-questions.md b/context/07-open-questions.md index fa8a627..53d855b 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -82,7 +82,7 @@ | OQ-74 | `PUBLISH_RETRIES` = одна первая попытка + до 3 повторов (всего до 4) с тем же `Nats-Msg-Id`; у `scan-gateway` тип `PositiveInt` (повторы нельзя отключить настройкой), у `tabular-parser` `NonNegativeInt` (0 допустим); паузы 0,5 с и 2,0 с. Согласовать единую семантику (повторы или всего попыток) и тип? | scan-gateway: п. 1 таблицы предложений (R-13, T11.1); docs: O-25, design #36 | контракт v1.x; настройки сервисов | допущение принято: «1 + 3 повтора» (design #36), расхождение «3 против 4» обмен не ломает (дедуп по `Nats-Msg-Id`); выровнять тип — косметика конфигурации | | OQ-75 | `reply_deadline` проверяет только `scan-gateway` (`error TIMEOUT` до отправки в ИС и потолок ожидания ответа); `tabular-parser` обрабатывает просроченный запрос. Пример `contracts/examples/request-image.json` имеет `reply_deadline` в прошлом (`2026-10-03T12:45:00Z`) и даёт `TIMEOUT` при ручном прогоне. Единая обязанность сервисов и свежая дата примера? | scan-gateway: п. 12 таблицы предложений (O-e); docs: O-26, design #37 | контракт v1.x; срез 2 | допущение принято: срок — дело сервиса, пример — иллюстрация формы (оговорка в spec и `03-architecture.md`, design #37); обновить дату можно одним PR с пересинхронизацией копий в трёх репозиториях | | OQ-76 | Обе реальные фикстуры `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС (`error.code = INVALID_DOCUMENT`, пустой `table_structure`). Успешный путь `image` (`done` / `needs_check`, файл в `results/…`, `s3_object` с `bucket_role = results`) сквозной проверкой не покрыт; в `scan-gateway` проверяется только синтетическим ответом. Нужны образцы успешных ответов Content AI. | scan-gateway: п. 3 таблицы предложений (O-1, R-10, design #32); docs: O-15 (задача 4 фазы 2), design #31 | срез 3 (OQ-13/14, нормализатор); fixtures | **частично закрыт человеком 04.10.2026:** добавлены реальные образцы — `aep_3.json` (успех: баланс 42 строки + ОФР 18 строк), `aep_3_ofr.json` (второй ответ ИС по тому же файлу, `INVALID_DOCUMENT`), `aep_6.json` (успех: баланс 122 строки + ОФР 49 строк); PR в `arm-ka-fixtures`. Успешный путь `image` на реальных ответах — Срез 3 (нормализатор); транспорт `scan-gateway` проверяется на них в Срезе 0 (склейка нескольких json, `NOT_IMPLEMENTED`) | -| OQ-77 | §11 `05-domain-rules.md` описывает `data_ocr[]` (список), а в фикстурах `data_ocr` — объект с полями `label/organization/inn/units/date/table_structure`. Какой формат фактический на стенде ИС (связано с OQ-13, OQ-14)? | `05-domain-rules.md` §11; scan-gateway: п. 4 таблицы предложений (O-1, T3.2) | срез 3 (нормализатор) | **закрыт человеком 04.10.2026 (по `aep_3.json`, `aep_6.json`):** `data_ocr` — **объект**, один на json-файл (`label`, `organization`, `inn`, `units`, `date` — `{form_name, confidence}`; `table_structure` — **список таблиц** `{label, rows[]}`, одна таблица = один документ БС/ОФР, в одном json их может быть несколько). Успех — `error = {code: "", message: ""}` (не `null`!), ошибка — непустой `code` и пустой `table_structure`. §11 `05-domain-rules.md` исправлен | +| OQ-77 | §11 `05-domain-rules.md` описывает `data_ocr[]` (список), а в фикстурах `data_ocr` — объект с полями `label/organization/inn/units/date/table_structure`. Какой формат фактический на стенде ИС (связано с OQ-13, OQ-14)? | `05-domain-rules.md` §11; scan-gateway: п. 4 таблицы предложений (O-1, T3.2) | срез 3 (нормализатор) | **закрыт человеком 04.10.2026 (по `aep_3.json`, `aep_6.json`):** `data_ocr` — **объект**, один на json-файл (`label`, `organization`, `inn`, `units`, `date` — `{form_name, confidence}`; `table_structure` — **список таблиц** `{label, rows[]}`, одна таблица = один документ БС/ОФР, в одном json их может быть несколько). Успех — `error = {code: "", message: ""}` (не `null`!), ошибка — непустой `code` и пустой `table_structure`. §11 `05-domain-rules.md` исправлен. **Уточнение человека 04.10.2026:** таблица и документ не взаимно однозначны — ИС может отдать несколько таблиц в одном json. Допущение (ASSUMPTION): документ = тип формы на файл (уникальный индекс `document` в DBML), таблицы одного `label` склеиваются в один документ; подтвердить на реальном ответе, где один баланс разбит на несколько таблиц | | OQ-78 | Шлюз читает ровно `//.json` (спека); старый код читал все файлы каталога `Output//`. Какие файлы и когда реальная ИС кладёт в `Output`, в каком порядке (момент готовности)? | scan-gateway: п. 5 таблицы предложений (T8.5) | срез 3 (стенд ИС) | **закрыт человеком 04.10.2026:** ИС кладёт **все распознанные документы одновременно** в каталог ``, по одному json на документ (пример: `aep_3.json` + `aep_3_ofr.json`); шлюз читает **все** json каталога и склеивает в один ответ (готовность — по стабилизации набора файлов, `SCAN_SETTLE_SECONDS`); ошибка одного документа не отменяет остальные (`metrics.warnings`). Реализуется в Срезе 0 (доработка PR scan-gateway #3) | | OQ-79 | Временный файл `.<имя>.part` лежит в `Input` (атомарная запись `.part` + rename): реальная ИС без маски может забрать недописанный файл. Уточнить у владельцев ИС маску и поведение или перенести временный файл в служебный подкаталог рядом с `Input`? | scan-gateway: п. 6 таблицы предложений (R-5, T7/T8) | срез 3 (стенд ИС) | **закрыт человеком 04.10.2026:** временный файл записи уходит из `Input` в служебный каталог рядом (`SCAN_TMP_DIR`, по умолчанию `Tmp`, на той же шаре — `rename` остаётся атомарным). Реализуется в Срезе 0 (доработка PR scan-gateway #3) | | 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) | -- 2.47.2 From e6ba1e0e303bfe0219209c08fc531d9310f3fa81 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 14:08:43 +0400 Subject: [PATCH 67/68] =?UTF-8?q?docs:=20=C2=A711=20=D0=B8=20OQ-77=20?= =?UTF-8?q?=E2=80=94=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D0=BE=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BF=D1=83=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=20=D1=81?= =?UTF-8?q?=D0=B5=D0=B3=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=85=20=D1=82=D0=B0?= =?UTF-8?q?=D0=B1=D0=BB=D0=B8=D1=86=D1=8B=20(=D0=BF=D0=BE=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BA=D0=B0=20=D1=87=D0=B5=D0=BB=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D0=BA=D0=B0=2004.10.2026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Разбиение одной формы на несколько таблиц в образцах не встречалось; наблюдается только несколько таблиц разных форм в одном json (баланс + ОФР). Нештатный случай (повтор label) — NEEDS_CHECK, правило — Срез 3. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- context/05-domain-rules.md | 15 +++++++-------- context/07-open-questions.md | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index daa8187..0b1f22f 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -375,12 +375,11 @@ Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят толь - `data_ocr` — **объект** (не список): `label`, `organization`, `inn`, `units`, `date` — каждый `{form_name, confidence}` (в `form_name` шумный OCR-текст; `date.form_name` ненадёжна, ИНН может быть мусором), `table_structure` — **список таблиц** `{label {form_name, confidence}, rows[]}`: `label.form_name` «Бухгалтерский баланс» → BALANCE, «Отчет о финансовых - результатах» → INCOME. **Соответствие «таблица ↔ документ» не взаимно однозначно:** в одном json может быть - несколько таблиц (в `aep_3.json` и `aep_6.json` — баланс и ОФР), а несколько таблиц одного типа (например, баланс, разбитый - по страницам на Актив и Пассив) — части одного документа. Тип документа задаёт `label`, а не число таблиц; документ — это - **тип формы на файл** (в DBML индекс `document (stored_file_id, document_type, osv_account_code)` уникален: один ББ и один ОФР - на файл), поэтому таблицы одного `label` из всех json каталога ответа склеиваются по порядку строк в один документ; неизвестный - `label` → `OTHER`. + результатах» → INCOME. **Наблюдается:** в одном json может быть **несколько таблиц разных форм** (в `aep_3.json` и + `aep_6.json` — баланс и ОФР), каждая со своим `label`; тип документа задаёт `label`. Разбиение одной формы на несколько + таблиц (сегменты) в образцах **не встречалось** и не предполагается; если одна форма вдруг придёт двумя таблицами или + `label` повторится — это нештатный случай: документ не создаётся молча, а уходит в `NEEDS_CHECK` с причиной (решение — + Срез 3, OQ-77). Неизвестный `label` → `OTHER`. - `rows[]`: `row_index`, `cells[] {column_index, value, confidence}`; `column_index` не обязательно подряд (бывают 0, 1, 2, 4, 5), `value` — сырой OCR-текст (мусор в ячейках, переносы строк, скобки для отрицательных, пробелы-разделители тысяч). В `aep_6.json` `rows[0]` — строка-шапка («Наименование показателя», «Код», «На 30 июня 2025 г», …); в `aep_3.json` шапки @@ -388,8 +387,8 @@ Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят толь наименования («АКТИВ / I. Внеоборотные активы / Гудвил» с кодом 1105). - `confidence` — **числа 0–100** (в ТЗ — доли 0..1; в реальных файлах проценты) → `numeric(5,2)` как есть. -Маппинг (Срез 3): таблицы `table_structure[*]` одного `label` (по всем json каталога ответа) → один `document` -(BALANCE/INCOME, `locator = pages:…`) + `financial_statement_meta` +Маппинг (Срез 3): таблица `table_structure[i]` с `label` «Бухгалтерский баланс» / «Отчет о финансовых результатах» → +`document` (BALANCE/INCOME, `locator = pages:…`) + `financial_statement_meta` (ИНН, имя, units, report_date, okud — из `organization/inn/units/date.form_name`, с низкой уверенностью → `NEEDS_CHECK`) + `financial_statement_column` (из шапки `rows[0]`, если она есть; `period_date` распарсить из «На 30 июня 2025 г», `is_current` = первая; без шапки — колонки без дат, документ `NEEDS_CHECK`) + `financial_statement_line` (`cells[1]` код, `cells[0]` имя; diff --git a/context/07-open-questions.md b/context/07-open-questions.md index 53d855b..9425c2e 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -82,7 +82,7 @@ | OQ-74 | `PUBLISH_RETRIES` = одна первая попытка + до 3 повторов (всего до 4) с тем же `Nats-Msg-Id`; у `scan-gateway` тип `PositiveInt` (повторы нельзя отключить настройкой), у `tabular-parser` `NonNegativeInt` (0 допустим); паузы 0,5 с и 2,0 с. Согласовать единую семантику (повторы или всего попыток) и тип? | scan-gateway: п. 1 таблицы предложений (R-13, T11.1); docs: O-25, design #36 | контракт v1.x; настройки сервисов | допущение принято: «1 + 3 повтора» (design #36), расхождение «3 против 4» обмен не ломает (дедуп по `Nats-Msg-Id`); выровнять тип — косметика конфигурации | | OQ-75 | `reply_deadline` проверяет только `scan-gateway` (`error TIMEOUT` до отправки в ИС и потолок ожидания ответа); `tabular-parser` обрабатывает просроченный запрос. Пример `contracts/examples/request-image.json` имеет `reply_deadline` в прошлом (`2026-10-03T12:45:00Z`) и даёт `TIMEOUT` при ручном прогоне. Единая обязанность сервисов и свежая дата примера? | scan-gateway: п. 12 таблицы предложений (O-e); docs: O-26, design #37 | контракт v1.x; срез 2 | допущение принято: срок — дело сервиса, пример — иллюстрация формы (оговорка в spec и `03-architecture.md`, design #37); обновить дату можно одним PR с пересинхронизацией копий в трёх репозиториях | | OQ-76 | Обе реальные фикстуры `fixtures/ocr` (`aep_5.json`, `aep_8_form4.json`) — ответы-ошибки ИС (`error.code = INVALID_DOCUMENT`, пустой `table_structure`). Успешный путь `image` (`done` / `needs_check`, файл в `results/…`, `s3_object` с `bucket_role = results`) сквозной проверкой не покрыт; в `scan-gateway` проверяется только синтетическим ответом. Нужны образцы успешных ответов Content AI. | scan-gateway: п. 3 таблицы предложений (O-1, R-10, design #32); docs: O-15 (задача 4 фазы 2), design #31 | срез 3 (OQ-13/14, нормализатор); fixtures | **частично закрыт человеком 04.10.2026:** добавлены реальные образцы — `aep_3.json` (успех: баланс 42 строки + ОФР 18 строк), `aep_3_ofr.json` (второй ответ ИС по тому же файлу, `INVALID_DOCUMENT`), `aep_6.json` (успех: баланс 122 строки + ОФР 49 строк); PR в `arm-ka-fixtures`. Успешный путь `image` на реальных ответах — Срез 3 (нормализатор); транспорт `scan-gateway` проверяется на них в Срезе 0 (склейка нескольких json, `NOT_IMPLEMENTED`) | -| OQ-77 | §11 `05-domain-rules.md` описывает `data_ocr[]` (список), а в фикстурах `data_ocr` — объект с полями `label/organization/inn/units/date/table_structure`. Какой формат фактический на стенде ИС (связано с OQ-13, OQ-14)? | `05-domain-rules.md` §11; scan-gateway: п. 4 таблицы предложений (O-1, T3.2) | срез 3 (нормализатор) | **закрыт человеком 04.10.2026 (по `aep_3.json`, `aep_6.json`):** `data_ocr` — **объект**, один на json-файл (`label`, `organization`, `inn`, `units`, `date` — `{form_name, confidence}`; `table_structure` — **список таблиц** `{label, rows[]}`, одна таблица = один документ БС/ОФР, в одном json их может быть несколько). Успех — `error = {code: "", message: ""}` (не `null`!), ошибка — непустой `code` и пустой `table_structure`. §11 `05-domain-rules.md` исправлен. **Уточнение человека 04.10.2026:** таблица и документ не взаимно однозначны — ИС может отдать несколько таблиц в одном json. Допущение (ASSUMPTION): документ = тип формы на файл (уникальный индекс `document` в DBML), таблицы одного `label` склеиваются в один документ; подтвердить на реальном ответе, где один баланс разбит на несколько таблиц | +| OQ-77 | §11 `05-domain-rules.md` описывает `data_ocr[]` (список), а в фикстурах `data_ocr` — объект с полями `label/organization/inn/units/date/table_structure`. Какой формат фактический на стенде ИС (связано с OQ-13, OQ-14)? | `05-domain-rules.md` §11; scan-gateway: п. 4 таблицы предложений (O-1, T3.2) | срез 3 (нормализатор) | **закрыт человеком 04.10.2026 (по `aep_3.json`, `aep_6.json`):** `data_ocr` — **объект**, один на json-файл (`label`, `organization`, `inn`, `units`, `date` — `{form_name, confidence}`; `table_structure` — **список таблиц** `{label, rows[]}`, одна таблица = один документ БС/ОФР, в одном json их может быть несколько). Успех — `error = {code: "", message: ""}` (не `null`!), ошибка — непустой `code` и пустой `table_structure`. §11 `05-domain-rules.md` исправлен. **Уточнение человека 04.10.2026:** разбиение одной формы на несколько таблиц (сегменты) в образцах не встречалось; наблюдается только несколько таблиц **разных** форм в одном json (баланс + ОФР). Нештатный случай (повтор `label`) — `NEEDS_CHECK`, правило уточнить в Срезе 3 по реальным данным | | OQ-78 | Шлюз читает ровно `//.json` (спека); старый код читал все файлы каталога `Output//`. Какие файлы и когда реальная ИС кладёт в `Output`, в каком порядке (момент готовности)? | scan-gateway: п. 5 таблицы предложений (T8.5) | срез 3 (стенд ИС) | **закрыт человеком 04.10.2026:** ИС кладёт **все распознанные документы одновременно** в каталог ``, по одному json на документ (пример: `aep_3.json` + `aep_3_ofr.json`); шлюз читает **все** json каталога и склеивает в один ответ (готовность — по стабилизации набора файлов, `SCAN_SETTLE_SECONDS`); ошибка одного документа не отменяет остальные (`metrics.warnings`). Реализуется в Срезе 0 (доработка PR scan-gateway #3) | | OQ-79 | Временный файл `.<имя>.part` лежит в `Input` (атомарная запись `.part` + rename): реальная ИС без маски может забрать недописанный файл. Уточнить у владельцев ИС маску и поведение или перенести временный файл в служебный подкаталог рядом с `Input`? | scan-gateway: п. 6 таблицы предложений (R-5, T7/T8) | срез 3 (стенд ИС) | **закрыт человеком 04.10.2026:** временный файл записи уходит из `Input` в служебный каталог рядом (`SCAN_TMP_DIR`, по умолчанию `Tmp`, на той же шаре — `rename` остаётся атомарным). Реализуется в Срезе 0 (доработка PR scan-gateway #3) | | 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) | -- 2.47.2 From f83052420f6075e52e74724b9a4a0d9cf0b9d784 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 16:20:26 +0400 Subject: [PATCH 68/68] =?UTF-8?q?docs:=20dev/README=20=E2=80=94=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0=20=D0=BE=D0=BA=D1=80?= =?UTF-8?q?=D1=83=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=B4=20?= =?UTF-8?q?=D1=81=D0=BA=D0=BB=D0=B5=D0=B9=D0=BA=D1=83=20=D0=BE=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=82=D0=BE=D0=B2=20=D0=98=D0=A1=20(aep=5F3,=20aep=5F6,?= =?UTF-8?q?=20aep=5F5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раздел «Проверка окружения» приведён в соответствие с доработанным scan-gateway (склейка всех *.json каталога ответа в raw/{job_id}.json, ошибочные файлы набора в metrics.warnings), по результатам сквозной проверки Task 6 (04.10.2026): - сценарий 2: размер и формат raw/ (`{"job_id", "files": [...]}`), лог эмулятора с каталогом набора, processing ~10 с (задержка эмулятора + SCAN_POLL_SECONDS + SCAN_SETTLE_SECONDS), абзац про фикстуры ИС (успешные ответы есть); - добавлен сценарий 2а: aep_3 / aep_6 -> успешный ответ ИС -> NOT_IMPLEMENTED, metrics.warnings про aep_3_ofr.json; - абзац про прогон рецепта: проверены оба пути вставки legal_entity (чистая БД и on conflict); - «не покрывает»: нормализатор шлюза Среза 0 даёт NOT_IMPLEMENTED, у ошибок scan-gateway в metrics только warnings, оговорка, что `down -v` удаляет общие тома postgres-data/minio-data всех worktree; - dev/docker-compose.yml: комментарий про фикстуры ответов ИС — набор <имя>.json + <имя>_*.json (поведение не менялось). Co-Authored-By: Claude Sonnet 5.5 --- dev/README.md | 47 ++++++++++++++++++++++++++++-------------- dev/docker-compose.yml | 2 +- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/dev/README.md b/dev/README.md index 0b94c19..e46fffb 100644 --- a/dev/README.md +++ b/dev/README.md @@ -182,9 +182,8 @@ poll() { # poll <секунд> — статус задания $JOB раз в } ``` -Рецепт прогнан дословно при уже существовавшей в БД строке `legal_entity` с ИНН `7700000001`, поэтому ветка вставки -(без конфликта по `inn`) на чистой БД не выполнялась; `on conflict (inn) do nothing` рассчитан на оба случая, но первая -вставка на пустой БД отдельно не проверялась. +Рецепт прогнан дословно на чистой БД (вставка `legal_entity` без конфликта по `inn`) и повторно при уже существующей +строке (`on conflict (inn) do nothing`); оба пути проверены. Ключ исходника — `uploads/{file_id}/{job_id}/{имя}` (`../context/03-architecture.md`, «Схема ключей S3»). Сообщения на шине можно смотреть параллельно: `docker run --rm --network arm-ka_default natsio/nats-box:0.17.0 nats sub -s @@ -217,17 +216,32 @@ new_job image PDF ocr/aep_5.PDF && poll 30 # 02:12:29 processing # 02:12:34 error INVALID_DOCUMENT dmc ls local/arm-ka/raw/ -# [2026-10-04 02:12:34 UTC] 588B STANDARD 46b0c90b-5a1b-4ac0-9e8a-0cdad80d857e.json -dmc cat "local/arm-ka/raw/$JOB.json" # {"file_uid": "", "error": {"code": "INVALID_DOCUMENT", "message": "Не удалось определить тип документа."}, "data_ocr": {…}} +# [2026-10-04 11:23:58 UTC] 600B STANDARD 0ea16933-4cc1-4b50-bda9-a97089a918c5.json +dmc cat "local/arm-ka/raw/$JOB.json" # {"job_id": "", "files": [{"name": ".json", "response": {"file_uid": "", "error": {"code": "INVALID_DOCUMENT", "message": "Не удалось определить тип документа."}, "data_ocr": {…}}, "parse_error": null}]} ``` -Код `INVALID_DOCUMENT` здесь — **настоящий ответ ИС из фикстуры**: обе пары в `fixtures/ocr` (`aep_5.json`, -`aep_8_form4.json`) — ответы-ошибки ИС Сканирование («Не удалось определить тип документа.», пустой `table_structure`). -Эмулятор нашёл пару (в логе `scan-emulator`: `_aep_5.PDF -> …/Output//.json`; при -отсутствии пары сообщение было бы «…: нет эталонного ответа в фикстурах»). Поэтому успешный путь `image` -(`done`/`needs_check`, файл `results/…`) сквозной проверкой на текущих фикстурах не проходит — нужен успешный ответ ИС -в `fixtures/ocr`. Имя файла — `aep_5.PDF` (расширение в верхнем регистре; на macOS `aep_5.pdf` тоже находится, на Linux — нет). -`processing` виден ~5 с: задержка эмулятора 2 с плюс опрос горячей папки шлюзом. +Код `INVALID_DOCUMENT` здесь — **настоящий ответ ИС из фикстуры**: `aep_5.json` и `aep_8_form4.json` — ответы-ошибки +ИС Сканирование («Не удалось определить тип документа.», пустой `table_structure`); у `aep_3` и `aep_6` есть успешные +ответы (сценарий 2а). Эмулятор нашёл набор ответов (в логе `scan-emulator`: `_aep_5.PDF -> +/data/hotfolder/Output/`; при отсутствии набора сообщение было бы «…: нет эталонного ответа в фикстурах»). +Успешные ответы ИС есть (`aep_3.json` + `aep_3_ofr.json`, `aep_6.json`); шлюз склеивает все `*.json` каталога ответа и +отвечает `NOT_IMPLEMENTED` (нормализация — Срез 3), ошибочные файлы набора — в `metrics.warnings`. Имя файла — `aep_5.PDF` (расширение в верхнем регистре; на macOS `aep_5.pdf` тоже находится, на +Linux — нет). +`processing` виден ~10 с: задержка эмулятора 2 с, затем до `SCAN_POLL_SECONDS + SCAN_SETTLE_SECONDS` (5 + 2 с) после +первого опроса, увидевшего набор. + +**2а. `image` → успешный ответ ИС → `NOT_IMPLEMENTED`:** + +```bash +new_job image PDF ocr/aep_3.pdf && poll 60 +# 11:22:07 queued +# 11:22:08 processing +# 11:22:18 error NOT_IMPLEMENTED +dpsql -c "select status, error_code, metrics from dkb.recognition_job where id = '$JOB'" +# error | NOT_IMPLEMENTED | {"warnings": ["_ofr.json: INVALID_DOCUMENT: Не удалось определить тип документа."]} +new_job image PDF ocr/aep_6.pdf && poll 60 +# error NOT_IMPLEMENTED, metrics пусто +``` **3. Сервис недоступен → три повторные публикации → `dlq` / `NO_ACK`:** @@ -250,10 +264,13 @@ docker compose --profile services up -d --wait tabular-parser # вернут перезапускается (replay — вне Среза 0). Что сквозная проверка **не** покрывает (Срез 0): успешные результаты (`done`/`needs_check`, регистрация файла -`results/…` в `s3_object`) — заглушка парсера и фикстуры ИС дают только ошибки; `stored_file.status` остаётся +`results/…` в `s3_object`) — заглушка парсера и нормализатор шлюза Среза 0 (`NOT_IMPLEMENTED`) дают только ошибки, +хотя фикстуры ИС (`aep_3`, `aep_6`) успешные ответы содержат; `stored_file.status` остаётся `UPLOADED`, `current_recognition_job_id` не заполняется (маппинг результата — `NoopMapper`, Срез 2); у ошибок -`scan-gateway` `recognition_job.metrics` пустой (`tabular-parser` присылает `duration_ms`). Тестовые строки и объекты -остаются в БД и MinIO; полная очистка — `docker compose --profile services down -v`. +`scan-gateway` в `recognition_job.metrics` только `warnings` — по строке `<имя файла>: <код>: <сообщение>` на ошибочный +файл ответа ИС, если такие есть (`tabular-parser` присылает `duration_ms`). Тестовые строки и объекты остаются в БД и +MinIO; полная очистка — `docker compose --profile services down -v` (удаляет и общие тома `postgres-data`/`minio-data` +всех worktree: проект compose `arm-ka` общий). ## Переменные `.env` diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index a3d72c1..c98bc3b 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -181,7 +181,7 @@ services: # Эмулятор ИС Сканирование: тот же образ, что у шлюза (собирает сервис scan-gateway, см. arm-core-migrate); общий # том hotfolder с шлюзом. Переменные MinIO/NATS не нужны. - # Фикстуры ответов ИС — ../../fixtures/ocr (только чтение): пара <имя>.json для входного файла UUID_<имя>.ext. + # Фикстуры ответов ИС — ../../fixtures/ocr (только чтение): набор <имя>.json + <имя>_*.json для входного файла UUID_<имя>.ext. scan-emulator: profiles: ["services"] image: scan-gateway:dev -- 2.47.2