arm-core/docs/superpowers/plans/2026-10-03-slice-0-skeleton.md
Andrey Epifancev f7bde3f82f chore: отметка задачи 27
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 01:18:12 +04:00

130 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Срез 0 — скелет ядра arm-core — план исполнения (Срез 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/arm-core`). **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
- Имена таблиц, колонок, enum'ов и их значений — ровно как в `../docs/model/data-model-v5.13.dbml` (43 таблицы, 27 enum, схема `dkb`); поля вне DBML не вводить. Деньги — `Numeric(18,2)` / `Decimal`, никаких float.
- Раскладка шаблона: все команды из `api/`; `api/src/{api,core,db,models,schemas,repositories,services,domain}`, `api/alembic/`, `api/scripts/`, `api/seed/`, `api/tests/`. Шаблон не переделывать; `settings` создаётся при импорте — тесты через `ENV_FILE=.env.test`.
- Платформа: Python 3.11 (`python:3.11-slim`, `uv venv --python 3.11`), PostgreSQL 16, NATS **без JetStream** (queue group `arm-core`, ack `processing`, повторная публикация 30 с × 3 → `dlq`/`NO_ACK`, дедуп по `result_msg_id`), Nexus недоступен (`make venv` без `raisa_*`), CI нет.
- Контракт: копии схем из `../docs/contracts/` в `api/contracts/`, байт в байт; pydantic v2.
- Алгоритмы — только из `../docs/context/05-domain-rules.md` (§4 правила элиминации, §10 матрица прав), номер § в docstring. Допущения: `ASSUMPTION(OQ-16)`, `ASSUMPTION(OQ-46)`, OQ-47.
- Права — данные (`permission` × `role_permission`), код роли в логике не проверять. `audit_event` append-only. Единственный писатель в БД — ядро.
- Ответы ошибок `{"detail": …}` по-русски; `/api/v1` префикс; dev-эмитент только при `AUTH_DEV_ISSUER=true and DEBUG=true`.
- Линтеры: ruff (line-length 120, E,F,I,UP,B,PL) + mypy; тесты pytest-asyncio auto; БД-тесты на compose-PostgreSQL, маркеры `integration`/`fixtures`.
- Язык кода-комментариев/коммитов — русский, идентификаторы — английские; коммит заканчивается строкой `Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>`.
## Задачи
### Task 1: [1.1] `api/pyproject.toml` (ruff
- [x] `api/pyproject.toml` (ruff: line-length 120, правила E,F,I,UP,B,PL; mypy с плагином pydantic), `requirements-dev.txt` = `-r requirements.txt` + ruff, mypy, pytest, pytest-asyncio, pytest-env, pytest-cov, httpx, PyJWT[crypto], pydbml, PyYAML, types-PyYAML; `Makefile`: `venv` (python3.11, `requirements.txt` без строк `raisa_*`, затем dev), `lint`, `test`, `test-integration`, `check`, `check-full`, `up`, `migrate`, `seed`, `seed-dev`; проверка: `make venv && make lint` зелёные на текущем каркасе без доступа к Nexus
### Task 2: [1.2] `src/core/config.py`
- [x] `src/core/config.py`: `Settings.model_config` читает `env_file` из `ENV_FILE` (по умолчанию `.env`); новые поля `AUTH_DEV_ISSUER=false`, `AUTH_DEV_SECRET`, `AUTH_JWKS_URL`, `AUTH_PUBLIC_KEY`, `AUTH_ISSUER`, `AUTH_AUDIENCE`, `AUTH_EMAIL_CLAIM="email"`, `NATS_ENABLED=false`, `OUTBOX_POLL_SECONDS=5`, `ACK_TIMEOUT_SECONDS=30`, `OUTBOX_MAX_REPUBLISH=3`; `.env.example`, `.env.test`; проверка: `ENV_FILE=.env.test python -c "from src.core.config import settings"` читает тестовые значения
### Task 3: [1.3] `Dockerfile` (`python
- [x] `Dockerfile` (`python:3.11-slim`, установка `requirements.txt` без `raisa_*`, `CMD python run_server.py`), убрать строку `Dockerfile` из `.gitignore`; `web/package.json`: `raisa-ui`, `raisa-fastapi-protected-web` → `optionalDependencies`; проверка: `docker build -t arm-core .` успешен без Nexus; `npm install` в `web/` проходит, `npm run build` собирает
### Task 4: [1.4] `src/app.py`
- [x] `src/app.py`: NATS — пустые `NATS_SEED/NATS_JWT` → подключение без nkeys (перенести `nkeys.from_seed` из модуля в функцию), подключение при `NATS_ENABLED=true` независимо от `DEBUG`, `/healthcheck` отдаёт `nats` (`ok`/`down`/`off`); `api/router.py` — `APIRouter(prefix="/api/v1")`; проверка: `python run_server.py` стартует против compose без учётных данных, `/healthcheck` 200 с `nats = ok`
### Task 5: [2.1] `src/models/enums.py`
- [x] `src/models/enums.py`: 27 enum'ов DBML как `enum.Enum` + фабрика `pg_enum`; `DBML_VERSION = "5.13"` в `models/__init__.py`; проверка: тест перечисляет имена и значения против списка из DBML (парсинг `Enum {}` блоков через pydbml)
### Task 6: [2.2] Части 01–03
- [x] Части 01–03: `directories.py` (`regional_office`, `company_group(+_history)`, `legal_entity(+_history)`, `legal_entity_alias`, `related_group`, `related_group_member` с partial unique), `access.py` (`app_user`, `role`, `permission`, `role_permission`, `user_role`), `accounting.py` (`chart_of_account`, `elimination_rule`, `elimination_rule_account`; `required_document_rule` — в `audit_ex.py`, см. 2.4); проверка: `Base.metadata.create_all` на тестовой БД без ошибок
### Task 7: [2.3] Части 04–06
- [x] Части 04–06: `files.py` (`s3_object`, `stored_file`, `document` с `NULLS NOT DISTINCT`, `recognition_job`; FK-цикл через `use_alter`), `osv.py` (`osv_entry`), `statements.py` (`financial_statement_meta/column/line/value`); проверка: как 2.2
### Task 8: [2.4] Части 07–09
- [x] Части 07–09: `consolidation.py` (`consolidation`, `_version`, `_status_history`, `_lock` с partial unique, `_member`, `_document`), `kfo.py` (`correction`, `goodwill`, `kfo_result`, `kfo_result_line`, `kfo_result_line_company`, `client_kfo_line`), `audit_ex.py` (`comment`, `audit_event`, `ex_snapshot`, `ex_snapshot_member`, `ex_snapshot_item`, `required_document_rule`); проверка: как 2.2, всего 43 таблицы в `Base.metadata.tables`
### Task 9: [2.5] `scripts/check_dbml_vs_orm.py` (pydbml
- [x] `scripts/check_dbml_vs_orm.py` (pydbml; сравнение таблиц, колонок, типов, nullability, PK/UNIQUE/FK, индексов с notes-шаблонами, enum'ов, версии) + `tests/test_dbml_parity.py`; проверка: 0 расхождений на 2.1–2.4; намеренно добавленная колонка даёт код 1 и понятное сообщение
### Task 10: [2.6] `alembic/env.py`
- [x] `alembic/env.py`: `CREATE SCHEMA IF NOT EXISTS dkb` перед миграциями; ревизия `0001_dbml_v5_13.py` (черновик autogenerate → ручная правка: enum'ы, partial-индексы, `NULLS NOT DISTINCT`, `use_alter`, полный `downgrade`); проверка: `alembic upgrade head && alembic downgrade base && alembic upgrade head` на compose-PostgreSQL 16; `alembic revision --autogenerate` после — пустая ревизия
### Task 11: [2.7] `tests/conftest.py`
- [x] `tests/conftest.py`: engine из `.env.test`, миграции один раз на сессию, транзакция с откатом на тест, маркеры `integration`/`fixtures` (skip с причиной при отсутствии `ARM_KA_FIXTURES`); проверка: тест чтения `Decimal` из `osv_entry.closing_debit` проходит; `make check` зелёный
### Task 12: [3.1] `seed/core/roles.yaml`, `permissions.yaml` (16 кодов), `role_permissions.yaml` (
- [x] `seed/core/roles.yaml`, `permissions.yaml` (16 кодов), `role_permissions.yaml` (матрица §10, ASSUMPTION(OQ-16)), `regional_office.yaml` (ГО); `scripts/seed.py` — загрузчик yaml/csv с upsert по натуральному ключу, флаг `--dev`; проверка: двойной запуск — одинаковое число строк; тест сравнивает `role_permission` с таблицей §10
### Task 13: [3.2] `seed/core/chart_of_accounts.csv`
- [x] `seed/core/chart_of_accounts.csv`: План счетов 94н (01–99, 001–011, `kind`, `row_type`, `is_off_balance`) + субсчета, упомянутые в §4/§6 правил и чек-листе (60.xx, 62.xx, 58.01/58.03, 66.xx/67.xx, 76.03/.05/.07.x/.08/.09, 90.01/90.03, 91.x, 80, 83, 03); проверка: все `parent_code` существуют, код уникален, нормализован (`60.01`, не `60.1`)
### Task 14: [3.3] `seed/core/elimination_rules_v1.yaml` — 8 правил ББ из §4 со счетами сторон и ис
- [x] `seed/core/elimination_rules_v1.yaml` — 8 правил ББ из §4 со счетами сторон и исключениями; `seed/core/required_document_rules.yaml` — BASIC (10) и EXTENDED (27, ASSUMPTION(OQ-46)); загрузчик падает до записи при неизвестном счёте; проверка: сценарии «Правило № 6» и «Ссылка на несуществующий счёт» из spec
### Task 15: [3.4] `seed/dev/regional_offices.yaml` (2 РФ), `seed/dev/users.yaml` (7 пользователей
- [x] `seed/dev/regional_offices.yaml` (2 РФ), `seed/dev/users.yaml` (7 пользователей `<role>@armka.local` + `user_role`); `make seed` / `make seed-dev`; проверка: без `--dev` `app_user` пуст; с `--dev` — 7 пользователей по одной роли
### Task 16: [4.1] `src/core/auth.py`
- [x] `src/core/auth.py`: `Claims`, протокол `TokenVerifier`, `DevHs256Verifier`, `JwksVerifier` (PyJWT, RS256/ES256, кэш JWKS), фабрика по настройкам; проверка: unit-тесты — валидный/истёкший/чужой ключ, dev-verifier не создаётся при `AUTH_DEV_ISSUER=false`
### Task 17: [4.2] `src/repositories/users.py` (пользователь по email, эффективные права одним запр
- [x] `src/repositories/users.py` (пользователь по email, эффективные права одним запросом), `api/dependencies/auth.py` (`current_user` → 401/403, `require(code)`), `src/schemas/me.py`; проверка: тесты на dev-пользователях seed — сценарии «Право есть», «Права нет», «Две роли»
### Task 18: [4.3] `api/endpoints/me.py` (`GET /api/v1/me`), `api/endpoints/dev_auth.py` (`POST /ap
- [x] `api/endpoints/me.py` (`GET /api/v1/me`), `api/endpoints/dev_auth.py` (`POST /api/v1/dev/token`, подключается только при `AUTH_DEV_ISSUER and DEBUG`); проверка: тесты — ответ `/me` для `ka_rf@armka.local` равен правам §10; при `DEBUG=false` маршрут `dev/token` → 404 и dev-токен → 401
### Task 19: [4.4] `README.md` репозитория
- [x] `README.md` репозитория: замена шаблонного текста — назначение, команды (`make`), env, работа без Nexus, получение dev-токена, запуск против compose; проверка: новый разработчик проходит README до зелёного `/me`
### Task 20: [5.1] `api/contracts/` — копии трёх схем из `../docs/contracts/` + `examples/`
- [x] `api/contracts/` — копии трёх схем из `../docs/contracts/` + `examples/`; `src/contract/` — pydantic-модели запроса, конверта (статусы `processing | done | needs_check | error`), файла результата (Decimal как строка); `tests/test_contract.py` — идентичность с `../docs/contracts` (skip с предупреждением без docs), валидация примеров; проверка: тесты зелёные
### Task 21: [5.2] `src/services/audit.py` — `AuditService.record(...)` в текущей сессии
- [x] `src/services/audit.py` — `AuditService.record(...)` в текущей сессии; тест: системное событие с `user_id = NULL`, откат транзакции не оставляет записи; статический тест — в `src/` нет `update()/delete()` по `AuditEvent`
### Task 22: [5.3] `src/core/nats.py`
- [x] `src/core/nats.py`: подключение (с nkeys/JWT или без; автопереподключение), `publish(subject, payload, msg_id)` с заголовком `Nats-Msg-Id`, подписка `recognition.result` в queue group `arm-core`; проверка: интеграционный тест против compose-NATS (без JetStream) — публикация и получение сообщения с заголовком; два подписчика в одной группе получают сообщение один раз
### Task 23: [5.4] `src/services/recognition_exchange.py`
- [x] `src/services/recognition_exchange.py`: `publish_pending()` (SKIP LOCKED; первая публикация `queued → published`; повторная публикация `published` без ack через `ACK_TIMEOUT_SECONDS` с тем же `Nats-Msg-Id`, `metrics.republish_count`; после `OUTBOX_MAX_REPUBLISH` → `dlq`, `error_code = NO_ACK`), `expire_deadlines()`; фоновые задачи в lifespan при `NATS_ENABLED`; проверка: интеграционный тест — вставка `s3_object` + `stored_file` + `recognition_job(queued)` → сообщение получено тестовым подписчиком и проходит схему → строка `published`; без ack (с укороченными таймаутами) — три повтора с одним `Nats-Msg-Id` и `dlq`; задание с прошедшим `reply_deadline` → `timeout`
### Task 24: [5.5] `handle_message()`
- [x] `handle_message()`: ack `processing` → `processing`/`started_at`/`service_*`; результат → `ResultMapper`/`NoopMapper` + регистрация `s3_object` + обновление задания; дубликат по `result_msg_id`, неизвестный `job_id`, результат для финального статуса — лог без изменений; проверка: интеграционный тест полного цикла queued → published → processing → done с `s3_object`; повтор не меняет строки; `error NOT_IMPLEMENTED` → `error_code`; `done` после `timeout` игнорируется
### Task 25: [6.1] `make check-full`
- [x] `make check-full`: `make check` + `pytest -m integration` + цикл миграций + `check_dbml_vs_orm` + `docker build` + `web`: `npm install`, `npx biome check src`, `npx tsc --noEmit`, `npm run build`; проверка: цель зелёная против compose docs на ветке `feat/1-slice-0-skeleton`
### Task 26: [6.2] Прогон DoD
- [x] Прогон DoD: `make up` (compose docs) → `make migrate` → `make seed-dev` → `make check-full` → `curl /api/v1/me` с dev-токеном возвращает права; `openspec validate slice-0-skeleton --strict`; проверка: всё зелёное, вывод `make check-full` вписан в описание PR
### Task 27: [6.3] Дополнить `CLAUDE.md` (команды `make`), зафиксировать новые допущения в `../docs
- [x] Дополнить `CLAUDE.md` (команды `make`), зафиксировать новые допущения в `../docs/context/07-open-questions.md` через PR в docs (OQ-46 и всё, что всплыло); проверка: ссылки из README/CLAUDE.md ведут на существующие файлы