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

16 KiB
Raw Blame History

Срез 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

  • 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

  • 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

  • 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

  • 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

  • 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

  • Части 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); проверка: Base.metadata.create_all на тестовой БД без ошибок

Task 7: [2.3] Части 04–06

  • Части 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

  • Части 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); проверка: как 2.2, всего 43 таблицы в Base.metadata.tables

Task 9: [2.5] scripts/check_dbml_vs_orm.py (pydbml

  • 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

  • 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

  • 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 (

  • 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

  • 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 со счетами сторон и ис

  • 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 пользователей

  • 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

  • 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, эффективные права одним запр

  • 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

  • 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 репозитория

  • README.md репозитория: замена шаблонного текста — назначение, команды (make), env, работа без Nexus, получение dev-токена, запуск против compose; проверка: новый разработчик проходит README до зелёного /me

Task 20: [5.1] api/contracts/ — копии трёх схем из ../docs/contracts/ + examples/

  • 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(...) в текущей сессии

  • src/services/audit.py — AuditService.record(...) в текущей сессии; тест: системное событие с user_id = NULL, откат транзакции не оставляет записи; статический тест — в src/ нет update()/delete() по AuditEvent

Task 22: [5.3] src/core/nats.py

  • 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

  • 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()

  • 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

  • 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

  • Прогон 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

  • Дополнить CLAUDE.md (команды make), зафиксировать новые допущения в ../docs/context/07-open-questions.md через PR в docs (OQ-46 и всё, что всплыло); проверка: ссылки из README/CLAUDE.md ведут на существующие файлы