Срез 0 (issue #1): раздел «Соглашения шаблона» (что оставлено от шаблона команды, что вырезается, что добавляется); change openspec/changes/slice-0-skeleton — proposal, design с таблицей «Decisions made without discussion», спеки и tasks; в openspec/config.yaml строки с двоеточием взяты в кавычки (YAML разбирал их как словари). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
108 lines
12 KiB
Markdown
108 lines
12 KiB
Markdown
# Tasks: Срез 0 — скелет ядра arm-core
|
||
|
||
Репозиторий — arm-core, если не указано иное; все команды — из `api/`. Порядок — по зависимостям. Каждая задача ≤ половины
|
||
дня, с указанием файлов и способа проверки. Зависимости от других репозиториев: 1.1 ждёт контракт v1.0
|
||
(`docs` задачи 1.1–1.4); 2.x нужна инфраструктура compose (`docs` 2.1).
|
||
|
||
## 1. Инструменты и окружение шаблона
|
||
|
||
- [ ] 1.1 `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; проверка: `pip install -r requirements-dev.txt` без конфликтов,
|
||
`ruff check . && ruff format --check . && mypy src` зелёные на текущем каркасе
|
||
- [ ] 1.2 `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`, `NATS_MANAGE_STREAM=false`, `OUTBOX_POLL_SECONDS=5`;
|
||
`.env.example`, `.env.test`; проверка: `ENV_FILE=.env.test python -c "from src.core.config import settings"` читает тестовые значения
|
||
- [ ] 1.3 `Dockerfile` (python:3.12-slim, `pip install -r api/requirements.txt`, `CMD python run_server.py`), убрать строку
|
||
`Dockerfile` из `.gitignore`, `Makefile` (`up`, `migrate`, `seed`, `test`, `lint`, `dbml-check`); проверка:
|
||
`docker build -t arm-core .` успешен, `make lint` зелёный
|
||
- [ ] 1.4 `src/app.py`: NATS — пустые `NATS_SEED/NATS_JWT` → подключение без nkeys (перенести `nkeys.from_seed` из
|
||
модуля в функцию), подключение при `NATS_ENABLED=true` независимо от `DEBUG`; `api/router.py` — `APIRouter(prefix="/api/v1")`;
|
||
проверка: `python run_server.py` стартует против compose без учётных данных, `/healthcheck` 200
|
||
|
||
## 2. Модель данных (spec `data-model`)
|
||
|
||
- [ ] 2.1 `src/models/enums.py`: 27 enum'ов DBML как `enum.Enum` + фабрика `pg_enum`; `DBML_VERSION = "5.13"` в
|
||
`models/__init__.py`; проверка: тест перечисляет имена и значения против списка из DBML (парсинг `Enum {}` блоков через pydbml)
|
||
- [ ] 2.2 Части 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` на тестовой БД без ошибок
|
||
- [ ] 2.3 Части 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
|
||
- [ ] 2.4 Части 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`
|
||
- [ ] 2.5 `scripts/check_dbml_vs_orm.py` (pydbml; сравнение таблиц, колонок, типов, nullability, PK/UNIQUE/FK, индексов с
|
||
notes-шаблонами, enum'ов, версии) + `tests/test_dbml_parity.py`; проверка: 0 расхождений на 2.1–2.4; намеренно
|
||
добавленная колонка даёт код 1 и понятное сообщение
|
||
- [ ] 2.6 `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;
|
||
`alembic revision --autogenerate` после — пустая ревизия
|
||
- [ ] 2.7 `tests/conftest.py`: engine из `.env.test`, миграции один раз на сессию, транзакция с откатом на тест, маркеры
|
||
`integration`/`fixtures` (skip с причиной при отсутствии `ARM_KA_FIXTURES`); проверка: тест чтения `Decimal` из
|
||
`osv_entry.closing_debit` проходит; `pytest -m "not integration"` зелёный
|
||
|
||
## 3. Seed (spec `reference-data-seed`)
|
||
|
||
- [ ] 3.1 `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
|
||
- [ ] 3.2 `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`)
|
||
- [ ] 3.3 `seed/core/elimination_rules_v1.yaml` — 8 правил ББ из §4 со счетами сторон и исключениями;
|
||
`seed/core/required_document_rules.yaml` — BASIC (10) и EXTENDED (27, ASSUMPTION(OQ-46)); загрузчик падает до записи
|
||
при неизвестном счёте; проверка: сценарии «Правило № 6» и «Ссылка на несуществующий счёт» из spec
|
||
- [ ] 3.4 `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 пользователей по одной роли
|
||
|
||
## 4. Аутентификация и права (spec `authn-authz`)
|
||
|
||
- [ ] 4.1 `src/core/auth.py`: `Claims`, протокол `TokenVerifier`, `DevHs256Verifier`, `JwksVerifier` (PyJWT, RS256/ES256,
|
||
кэш JWKS), фабрика по настройкам; проверка: unit-тесты — валидный/истёкший/чужой ключ, dev-verifier не создаётся при
|
||
`AUTH_DEV_ISSUER=false`
|
||
- [ ] 4.2 `src/repositories/users.py` (пользователь по email, эффективные права одним запросом), `api/dependencies/auth.py`
|
||
(`current_user` → 401/403, `require(code)`), `src/schemas/me.py`; проверка: тесты на dev-пользователях seed —
|
||
сценарии «Право есть», «Права нет», «Две роли»
|
||
- [ ] 4.3 `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
|
||
- [ ] 4.4 `README.md` репозитория: замена шаблонного текста — назначение, команды (`make`), env, получение dev-токена,
|
||
запуск против compose; проверка: новый разработчик проходит README до зелёного `/me` (самопроверка по шагам)
|
||
|
||
## 5. Обмен через `recognition_job` и аудит (spec `recognition-job-exchange`, `audit-trail`)
|
||
|
||
- [ ] 5.1 `api/contracts/` — копии трёх схем из `../docs/contracts/` + `examples/`; `src/contract/` — pydantic-модели
|
||
запроса, конверта результата, файла результата (Decimal как строка); `tests/test_contract.py` — идентичность с
|
||
`../docs/contracts` (skip с предупреждением без docs), валидация примеров; проверка: тесты зелёные
|
||
- [ ] 5.2 `src/services/audit.py` — `AuditService.record(...)` в текущей сессии; тест: системное событие с `user_id = NULL`,
|
||
откат транзакции не оставляет записи; статический тест — в `src/` нет `update()/delete()` по `AuditEvent`
|
||
- [ ] 5.3 `src/core/nats.py`: подключение (с nkeys/JWT или без), `ensure_stream` (`RECOGNITION`, `recognition.>`, дубликаты
|
||
2 мин), durable pull-consumer `arm-core-results` (`max_deliver` 5, `ack_wait` 60 с); проверка: интеграционный тест
|
||
против compose-NATS создаёт stream и consumer, повторный вызов идемпотентен
|
||
- [ ] 5.4 `src/services/recognition_exchange.py`: `publish_pending()` (SKIP LOCKED, сборка запроса, `Nats-Msg-Id`,
|
||
переход `published`), `expire_deadlines()`; фоновые задачи в lifespan при `NATS_ENABLED`; проверка: интеграционный
|
||
тест — вставка `s3_object` + `stored_file` + `recognition_job(queued)` → сообщение получено тестовым подписчиком и
|
||
проходит схему → строка `published`; задание с прошедшим `reply_deadline` → `timeout`
|
||
- [ ] 5.5 `handle_result()` + `ResultMapper`/`NoopMapper` + регистрация `s3_object` результата + аудит
|
||
`CONSOLIDATION_STATUS_CHANGE`? — нет: для заданий аудит не предусмотрен enum'ом, пишем только лог; обработка
|
||
дубликатов, неизвестного `job_id`, `dlq`; проверка: интеграционный тест — `done` обновляет задание и создаёт
|
||
`s3_object`; повтор не меняет строки; `error NOT_IMPLEMENTED` → `error_code`; пятикратное падение → `dlq`
|
||
|
||
## 6. CI и завершение
|
||
|
||
- [ ] 6.1 `.gitea/workflows/ci.yml` из `../docs/dev/ci/python-service.yml` (блок `env`: arm-core, шаги `migrations`,
|
||
`dbml-check` включены) + job `web` (`npm ci`, `biome check src`, `tsc --noEmit`, `rsbuild build`); проверка: зелёный
|
||
run на ветке `feat/1-slice-0-skeleton`
|
||
- [ ] 6.2 Прогон DoD: `make up` (compose docs) → `make migrate` → `make seed-dev` → `make test` (включая `integration`) →
|
||
`curl /api/v1/me` с dev-токеном возвращает права; `openspec validate slice-0-skeleton --strict`; проверка: всё зелёное,
|
||
результаты вписаны в описание PR
|
||
- [ ] 6.3 Обновить `openspec/specs/` не требуется до архивации; дополнить `CLAUDE.md` (команды `make`), зафиксировать
|
||
новые допущения в `../docs/context/07-open-questions.md` через PR в docs (OQ-46 и всё, что всплыло); проверка:
|
||
ссылки из README/CLAUDE.md ведут на существующие файлы
|