130 lines
16 KiB
Markdown
130 lines
16 KiB
Markdown
# Срез 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 ведут на существующие файлы
|
||
|