docs: описать проект в CLAUDE.md и ARCHITECTURE.md

- CLAUDE.md: что это, стек и ограничения, архитектура, команды, тесты
- ARCHITECTURE.md: карта модулей, границы импортов, открытые вопросы
- .claude/code-paths: app/, alembic/, tests/ для хука require-plan
- .gitignore: .claude/worktrees/

Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-21 13:51:22 +04:00
parent f10a507b52
commit b4d49bdab8
4 changed files with 64 additions and 18 deletions

3
.claude/code-paths Normal file
View File

@ -0,0 +1,3 @@
app/
alembic/
tests/

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ docs/excel/
.superpowers/
.bounded
.DS_Store
.claude/worktrees/

View File

@ -1,15 +1,36 @@
# Архитектура
{{Карта модулей на одну страницу: что где лежит, границы, потоки данных. Читается агентом перед architectural brainstorming. Обновляется вместе со спеками.}}
Streamlit-приложение поверх async SQLAlchemy. Слои: UI/джоба → сервисы → репозитории → БД; сервисы ходят во внешние API через интеграции и в разбор файлов через imports. Потоки данных по шагам — `README.md`, раздел «Потоки»; решения D… и вопросы Q… — `docs/project.md`.
## Модули
| Модуль | Ответственность | Зависит от |
|---|---|---|
| | | |
| `main.py` | `load_dotenv` → `set_page_config` → `AuthService().run()` → `st.navigation` по страницам | `auth`, `core`, `ui` |
| `run_local.py` | локальный запуск: `LOCATION=local`, `alembic upgrade head`, `streamlit run` | — |
| `app/core` | `Settings` (pydantic-settings, префикс `OPENBAO__SETTINGS__` приоритетнее `.env`), логирование | — |
| `app/auth` | Keycloak через `raisa-streamlit-oauth`, mock-форма при `LOCATION=local` | — |
| `app/db` | async engine (`NullPool`), `SessionLocal`, `Base`, `run_db()`; типы колонок (BigInteger PK, JSON/JSONB, Numeric, аудит); ORM-модели | `core` |
| `app/integrations` | `gateway.py` — requests-клиент к Gravitee (ключ, таймауты, повторы, классификация ошибок, `RequestRecord`); `moex.py`, `cbr.py` — клиенты и разбор ответов в dataclass'ы | `core` |
| `app/imports` | `quik.py` — разбор выгрузки сделок QUIK (.xlsx), ошибки «строка N, колонка» | — |
| `app/repositories` | async-доступ к таблицам, upsert `ON CONFLICT` (`_upsert.py`) | `db`, `integrations` (только dataclass'ы результатов) |
| `app/services` | сценарии: `securities`, `market_data`, `fx_rates`, `quik_import`, `manual_operations`, `request_log`, `dates` | `db`, `repositories`, `integrations`, `imports` |
| `app/ui` | страницы «Бумаги», «Рыночные данные», «Загрузка сделок», «Ручные операции»; `common.py` | `services`, `imports`, `db` (`run_db`, перечисления моделей) |
| `app/jobs` | `update_market` — котировки + курс CNY за дату от пользователя `system`; коды выхода 0/1/2 | `core`, `db`, `services` |
| `alembic/` | async `env.py` со схемой `DATABASE_SCHEMA`, ревизии, сиды справочников | `db` |
Таблицы: `currency`, `portfolio`, `operation_type` (справочники с сидом), `security`, `security_nominal_history`, `market_data`, `market_data_manual`, `exchange_rate`, `operation`, `import_batch`, `external_request_log`. Уникальные ключи и смысл колонок — `README.md`, «БД и event loop».
## Границы
- Что не должно импортировать что.
- `core`, `auth`, `imports` не импортируют другие пакеты `app`.
- `integrations` и `imports` не знают о БД; `integrations` — единственное место, где есть HTTP.
- `repositories` не вызывают внешние API: из `integrations` берут только dataclass'ы результатов разбора.
- `ui` не импортирует `repositories` и `integrations` напрямую — только через `services`; в БД ходит только через `run_db()`.
- Внешние API вызываются только из сервисов по действию пользователя или из джобы, не при рендере страницы.
- Каждый запрос к шлюзу пишется в `external_request_log` без тела ответа и заголовков (кроме `X-Gravitee-Transaction-Id`).
- Загрузка сделок QUIK пишет `import_batch` и операции одной транзакцией; план перед записью пересобирается по БД.
## Открытые вопросы
-
- Бизнес-вопросы Q2–Q28 — в `docs/project.md`; ключевые для следующей итерации — партии внутри выпуска (Q24), фондирование (Q8, Q19), хранение результатов расчёта (Q20), роль NOM и ADJ в результате (Q6, Q28).
- Где будут жить расчёты финрезультата и фондирования (новый пакет `app/calc/` с чистой логикой или сервисы) — решить на brainstorming первой расчётной итерации.
- Как запускать джобу по расписанию в Раисе — не решено.

View File

@ -1,32 +1,53 @@
# DRRK
## Что это
{{ОДНИМ АБЗАЦЕМ: что делает система, для кого, главный сценарий.}}
Streamlit-сервис «Портфели ДРРК» (BIQ-38115) для ДРРК банка: заменяет два Excel-файла с VBA-макросами, в которых ведутся портфели облигаций **UW** (андеррайтинг) и **SALES** (продажи), плюс сводный отчёт по обоим. Главный сценарий: пользователь ведёт справочник бумаг, подтягивает котировки MOEX и курс CNY ЦБ, загружает выгрузку сделок QUIK (.xlsx) в портфель и вводит вручную то, чего нет в QUIK (купоны, амортизация, комиссия UW, корректировки). Расчёт финрезультата, фондирование и отчёты — следующие итерации (`TODO(BIQ-38115)` в коде).
Подробности: `README.md` (потоки, таблицы, переменные окружения), `docs/project.md` (решения D…, открытые вопросы Q…), `docs/legacy_excel.md` и `docs/vba/` (как устроены текущие Excel-файлы).
## Стек и ограничения
- {{Язык, сборка, тесты — например: Python 3.12, uv, pytest.}}
- {{Жёсткие ограничения среды исполнения — без чего код не должен собираться.}}
- Python 3 (CI-образ `streamlit-app-base:2.1.0`), Streamlit 1.41, async SQLAlchemy 2.0 + asyncpg (прод, PostgreSQL) / aiosqlite (локально и в тестах), Alembic, pydantic-settings, requests, openpyxl. Тесты — pytest + pytest-asyncio.
- Платформа — Раиса (k8s), авторизация Keycloak через `raisa-streamlit-oauth`. Этот пакет есть только во внутреннем зеркале: вне контура ставить `requirements-dev.txt` (без него), при `LOCATION=local` он не нужен.
- Внешние данные (MOEX ISS, ЦБ) — только через шлюз Gravitee, у MOEX и ЦБ **разные** ключи. Внешние API вызываются только по кнопке или джобой; экраны читают БД.
- Streamlit синхронный: любой доступ к БД из UI и джобы — только через `run_db(coro)` (`app/db/base.py`), движок с `NullPool`. Свой event loop/пул не заводить.
- Деньги, цены, курсы — `Decimal` / `Numeric`, никогда `float`.
- Ключи шлюза и заголовки запросов не логируются никогда (кроме `X-Gravitee-Transaction-Id`, D18).
- Файлы заказчика (оригинал выгрузки QUIK, `docs/excel/`) — банковская тайна и ПДн: только локально, в git не добавлять. Тесты работают на обезличенной копии `tests/fixtures/quik_sample_300326_anon.xlsx`.
- Где бизнес-решения нет — `TODO(BIQ-38115)` с номером вопроса Q… из `docs/project.md`; решения из спеки/обсуждения записывать туда как D….
- Соглашения (конфиг, БД, Alembic, auth, линтеры, стиль тестов) повторяют проект `SFM-parser` (`../sfm`).
## Архитектура
- `src/core/` (или {{ваш путь}}) — чистая логика без зависимостей от фреймворка/платформы. Покрывается тестами полностью.
- `src/{{adapter}}/` — тонкая обёртка над платформой. Тестируется руками или интеграционно.
- {{Ключевые интерфейсы/порты между ними, если есть.}}
- `app/integrations/` — HTTP-клиенты к шлюзу (`gateway.py`: ключ, таймауты, повторы, журнал запросов) и разбор ответов MOEX/ЦБ в dataclass'ы. Без БД.
- `app/imports/quik.py` — разбор выгрузки сделок QUIK (.xlsx). Чистая логика, без БД и сети.
- `app/db/` — движок, `run_db()`, типы колонок, ORM-модели (`models/`, реэкспорт в `__init__.py`).
- `app/repositories/` — async-доступ к таблицам, upsert через `ON CONFLICT`.
- `app/services/` — сценарии: бумаги, рыночные данные, курсы, загрузка QUIK, ручные операции, даты.
- `app/ui/pages/` — страницы Streamlit; `main.py` — auth + `st.navigation`.
- `app/jobs/update_market.py` — джоба котировок и курса для расписания.
- `app/auth/`, `app/core/` — Keycloak/mock-авторизация, настройки, логирование.
- Карта модулей и границы импортов — `ARCHITECTURE.md`.
## Команды
- `{{npm test / pytest}}` — весь набор тестов. Зелёным считается только весь набор.
- `{{npm run build / ...}}` — сборка.
- `{{npm run dev / ...}}` — локальный запуск.
- `pytest` — весь набор (unit + smoke UI через `AppTest`), без сети и Postgres. Зелёным считается только весь набор.
- `ruff check . && black --check . && isort --check-only --profile black .` — линтеры (они же в pre-commit).
- `python run_local.py` — локальный запуск: `LOCATION=local`, миграции SQLite, `streamlit run main.py`.
- `alembic upgrade head` / `alembic revision -m "..."` — миграции (стиль — `alembic/versions/`).
- `python -m app.jobs.update_market [--date YYYY-MM-DD]` — джоба котировок и курса CNY.
- `DRRK_SMOKE_GATEWAY=1 pytest tests/smoke/test_gateway_smoke.py -v` — smoke против реального шлюза (нужны ключи и доступ к тестовому контуру).
- Окружение: `python -m venv .venv && .venv/bin/pip install -r requirements-dev.txt`. На macOS arm64 SQLAlchemy не тянет `greenlet` — доставить `pip install greenlet`, иначе тесты с БД падают с «the greenlet library is required».
## Тесты
- Golden-тесты: `tests/fixtures/<name>.*` + эталон `<name>.expected.*`. Внешние сервисы в тестах мокаются.
- {{Где лежит тестовое окружение (vault, БД, песочница).}}
- `tests/unit/` — без сети и без Postgres; БД — временный SQLite со схемой из миграций (фикстура `session_local` в `tests/conftest.py`), внешние клиенты — фейки из `tests/unit/fakes.py`.
- `tests/smoke/` — UI через `streamlit.testing.v1.AppTest`; реальный шлюз — только по флагу `DRRK_SMOKE_GATEWAY=1`.
- `tests/fixtures/` — реальные ответы MOEX ISS / ЦБ байт в байт (см. `.gitattributes`) и обезличенная выгрузка QUIK. Новые ответы внешних API сохранять туда же, а не собирать руками.
- `test_anonymized_fixture_matches_original` пропускается без локального оригинала выгрузки — это нормально.
## Первая подсистема
{{Что делается первым и почему. Тестовая инфраструктура — Task 1 первого плана.}}
## Дальше
Следующие итерации — расчёт финрезультата, фондирование, сводный отчёт (см. `TODO(BIQ-38115)` и открытые вопросы Q… в `docs/project.md`). Многие из них упираются в неотвеченные Q — перед brainstorming сверяться со списком.
## OpenSpec и superpowers
- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.
- Когда brainstorming классифицировал задачу как architectural, структуру change создать через `openspec new change <id>` (или `/opsx:propose <id>`), а содержимое proposal.md, specs/ и design.md написать по итогам brainstorming. tasks.md из OpenSpec не заполнять — задачи живут в плане superpowers.
- `/opsx:apply` не использовать: исполнение идёт через writing-plans и SDD/executing-plans.
- `openspec archive <id>` — после merge PR, как часть чеклиста закрытия.
- Хук require-plan защищает любой каталог `src/` на любой глубине. Если код лежит иначе — перечислить каталоги (префиксы от корня, по одному в строке) в `.claude/code-paths`.
- Хук require-plan защищает каталоги из `.claude/code-paths` (`app/`, `alembic/`, `tests/`). Файлы в корне (`main.py`, `run_local.py`) хук не защищает — правки в них всё равно только по плану или с `.bounded`.