16 KiB
16 KiB
Срез 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 grouparm-core, ackprocessing, повторная публикация 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_eventappend-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 без учётных данных,/healthcheck200 с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; проверка: без--devapp_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 grouparm-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(): ackprocessing→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 ведут на существующие файлы