Compare commits

..
3 Commits
Author SHA1 Message Date
oqyude f4a135b4e0 metaagent update 2026-08-07 22:30:19 +03:00
oqyude e9c7120323 testing new features 2026-07-22 12:40:42 +03:00
oqyude 3cc8e94863 metaagent updated 2026-07-22 01:37:07 +03:00
50 changed files with 3536 additions and 805 deletions
+9 -12
View File
@@ -1,8 +1,8 @@
{ {
"metaagent_version": "1.1.0", "metaagent_version": "1.1.0",
"session_id": "metaagent-002", "session_id": "metaagent-003",
"target_repo": "S:\\Git\\nifodea", "target_repo": "S:\\Git\\nifodea",
"goal": "Обновление metaagent-артефактов до v1.0.0, валидация существующего кода и окружения", "goal": "i18n (ru/en) — инфраструктура, обёртка строк, контроль переводов",
"project_type": "existing", "project_type": "existing",
"config": { "config": {
"depth": 4, "depth": 4,
@@ -18,17 +18,14 @@
"red_team": "skipped", "red_team": "skipped",
"decomposition": "completed", "decomposition": "completed",
"environment": "completed", "environment": "completed",
"handoff": "completed" "handoff": "pending"
}, },
"tasks": [ "tasks": [
{ "id": "T1", "title": "Инициализация проекта и зависимостей", "status": "completed", "depends_on": [], "acceptance_criteria": ["pyproject.toml создан", "Все __init__.py созданы", "ruff проходит", "pytest запускается"] }, { "id": "T9", "title": "i18n инфраструктура (cli/i18n.py)", "status": "completed", "depends_on": [], "acceptance_criteria": ["Translator, t(), setup_i18n(), set_lang()", "ru default, en fallback"] },
{ "id": "T2", "title": "Модель данных (dataclass + JSON)", "status": "completed", "depends_on": ["T1"], "acceptance_criteria": ["Все сущности dataclass", "FinancialModel save/load JSON"] }, { "id": "T10", "title": "Обёртка CLI-строк в t()", "status": "completed", "depends_on": ["T9"], "acceptance_criteria": ["main.py + config.py через t()", "ruff check проходит"] },
{ "id": "T3", "title": "Forecast Engine", "status": "completed", "depends_on": ["T2"], "acceptance_criteria": ["forecast_cashflow работает", "recurring проецируются", "активы/обязательства учтены"] }, { "id": "T11", "title": "AI-промпты через i18n", "status": "completed", "depends_on": ["T9"], "acceptance_criteria": ["prompts.py использует t()"] },
{ "id": "T4", "title": "Scenario Analysis", "status": "completed", "depends_on": ["T3"], "acceptance_criteria": ["3 сценария", "what-if модификация", "сравнение сценариев"] }, { "id": "T12", "title": "Тесты i18n", "status": "completed", "depends_on": ["T9"], "acceptance_criteria": ["pytest проходит"] },
{ "id": "T5", "title": "Excel Sync", "status": "completed", "depends_on": ["T2"], "acceptance_criteria": ["импорт из Excel", "экспорт в Excel", "ошибки невалидного формата"] }, { "id": "T13", "title": "Аудит и контроль актуальности переводов", "status": "pending", "depends_on": ["T9"], "acceptance_criteria": ["en-словарь синхронизирован с ru"] }
{ "id": "T6", "title": "CLI (Typer)", "status": "completed", "depends_on": ["T2","T3","T4","T5","T7"], "acceptance_criteria": ["init/forecast/analyze/import/export/scenario/whatif/compare команды"] },
{ "id": "T7", "title": "AI Assistant", "status": "completed", "depends_on": ["T3"], "acceptance_criteria": ["промпты с моделью и прогнозом", "заглушка ответа"] },
{ "id": "T8", "title": "Тесты", "status": "completed", "depends_on": ["T2","T3","T4","T5","T6","T7"], "acceptance_criteria": ["pytest проходит", "покрытие всех модулей"] }
], ],
"last_updated": "2026-07-22T12:00:00Z" "last_updated": "2026-07-22T18:00:00Z"
} }
+11 -9
View File
@@ -1,34 +1,36 @@
# BOUNDARIES — Рамки и границы # BOUNDARIES — Рамки и границы
Что мета-агенту **разрешено**, **запрещено** и в каких случаях **нужно остановиться**. Что агенту **разрешено**, **запрещено** и в каких случаях **нужно остановиться**.
## Разрешено ## Разрешено
| Действие | Примечание | | Действие | Примечание |
|---|---| |---|---|
| Читать любые файлы в целевом репозитории | Все файлы, включая .git, конфиги, историю | | Читать любые файлы в целевом репозитории | Все файлы, включая .git, конфиги, историю |
| Создавать/изменять файлы в `.agent/` | Единственная директория для артефактов | | Создавать/изменять файлы в `.agent/` | Директория метаданных проекта (rules, decisions, tasks, context, archive, requests, roadmap) |
| Создавать `.temp/` в корне проекта | Для временных файлов агента (всегда на одном уровне с `.agent/`) |
| Писать production-код | В фазе EXECUTION, по задачам из manifest.json |
| Рефакторить существующий код | Только если это часть задачи в manifest.json |
| Делать коммиты | По завершении задачи, перед созданием request |
| Создавать/дополнять `.gitignore` | Только для добавления `.temp/` |
| Устанавливать/обновлять зависимости | Только через штатный пакетный менеджер проекта | | Устанавливать/обновлять зависимости | Только через штатный пакетный менеджер проекта |
| Изменять конфигурационные файлы | Только если это необходимо для сборки/тестов (например, добавить requirements.txt) | | Изменять конфигурационные файлы | Только если это необходимо для сборки/тестов (например, добавить requirements.txt) |
| Запускать сборку и тесты | Для верификации окружения | | Запускать сборку и тесты | Для верификации окружения и проверки request-ов |
| Читать документацию, issue, PRs | Для понимания контекста | | Читать документацию, issue, PRs | Для понимания контекста |
| Запрашивать уточнения у пользователя | Если не хватает информации для декомпозиции | | Запрашивать уточнения у пользователя | Если не хватает информации для декомпозиции |
| Копировать исходники MetaAgent в `.agent/src/` целевого проекта | Только на фазе INIT, без перезаписи существующих файлов | | Копировать исходники MetaAgent в `.agent/src/` целевого проекта | Только на фазе INIT, без перезаписи существующих файлов |
| Создавать/обновлять `AGENTS.md` в корне целевого проекта | Только если файла не существует | | Создавать/обновлять `AGENTS.md` в корне целевого проекта | Только если файла не существует |
| **Обязательно:** читать `.agent/rules/project-rules.md` перед каждой фазой | Исполнение правил пользователя — приоритет выше стандартных протоколов | | **Обязательно:** читать `.agent/rules/project-rules.md` перед каждой фазой | Исполнение правил пользователя — приоритет выше стандартных протоколов |
| Перемещать завершённые артефакты в `.agent/archive/` | Только на фазе HANDOFF, только для completed/failed артефактов | | Перемещать завершённые артефакты в `.agent/archive/` | На фазах METASTATE и HANDOFF, только для completed/failed артефактов |
| **Обязательно:** после выполнения задачи создавать request в `.agent/requests/active/` | Request — единица результата, основа для METASTATE |
## Запрещено ## Запрещено
| Действие | Почему | | Действие | Почему |
|---|---| |---|---|
| Писать production-код | Это работа исполнительного агента |
| Рефакторить существующий код | Мета-агент не меняет логику |
| Удалять файлы | Если файл мешает — нужно сообщить пользователю | | Удалять файлы | Если файл мешает — нужно сообщить пользователю |
| Коммитить в main/master | Коммиты делает исполнительный агент по задачам |
| Менять удалённые настройки CI/CD | Если CI сломан — сообщить пользователю | | Менять удалённые настройки CI/CD | Если CI сломан — сообщить пользователю |
| Пул-реквесты | Исполнительный агент создаёт PR после выполнения задач | | Модифицировать код, не связанный с задачей | Только то, что нужно в рамках задачи из manifest.json |
| Модифицировать код, не связанный с задачей | Только то, что нужно для окружения |
## Когда остановиться ## Когда остановиться
+251 -207
View File
@@ -1,23 +1,40 @@
# META_AGENT_GUIDE — Главная инструкция # META_AGENT_GUIDE — Главная инструкция v2.1
## Жизненный цикл сессии ## Жизненный цикл сессии
``` ```
.agent/metaagent-request.md .agent/metaagent-request.md
INIT → ANALYSE → [DESIGN] → [RED_TEAM] → DECOMPOSITION → SETUP → (CHECKPOINT)* → HANDOFF → EXIT ┌─────────────────────────────────────────────────────┐
PROJECT LOOP (однократно)
▼ ▼ │ │
ADR (опц.) Invariant Tasks (опц.) │ INIT → ANALYSE → ROADMAP → DESIGN → DECOMPOSITION │
Alt.Arch (опц.) │ │
Risk Register (опц.) │ Выход: .agent/tasks/manifest.json │
└──────────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ WORK LOOP (циклически) │
│ │
│ EXECUTION → (request) → METASTATE (по команде) │
│ │
│ Цикл повторяется: беру задачу → делаю → │
│ создаю request → накопилось → METASTATE │
└──────────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ HANDOFF (завершение) │
└─────────────────────────────────────────────────────┘
``` ```
Фазы выполняются **строго последовательно**. Фаза DESIGN — только если project_type = greenfield/scaffold. Фазы выполняются **строго последовательно** внутри PROJECT LOOP.
Фаза RED_TEAM — только если config.red_team = yes. WORK LOOP может повторяться многократно.
HANDOFF — легковесное завершение.
Все артефакты размещаются в `.agent/` целевого репозитория (с layer-структурой или плоские, в зависимости от config). Все артефакты размещаются в `.agent/` целевого репозитория.
--- ---
@@ -32,12 +49,12 @@ INIT → ANALYSE → [DESIGN] → [RED_TEAM] → DECOMPOSITION → SETUP → (CH
| Уровень | Название | Что выполняется | | Уровень | Название | Что выполняется |
|---|---|---| |---|---|---|
| 1-2 | Scaffold | INIT → ANALYSIS → SETUP (только структура, без реализации) | | 1-2 | Scaffold | INIT → ANALYSIS → SETUP (только структура, без реализации) |
| 3-4 | Light | + DESIGN (без ADR/альтернатив), DECOMPOSITION (без инвариантов), HANDOFF **(default)** | | 3-4 | Light | + ROADMAP, DESIGN (без ADR/альтернатив), DECOMPOSITION — **(default)** |
| 5-6 | Standard | полный цикл с базовым DESIGN и DECOMPOSITION | | 5-6 | Standard | полный цикл с базовым DESIGN и DECOMPOSITION |
| 7-8 | Deep | + ADR, Alternative Architecture, Risk Register, Invariant Tests | | 7-8 | Deep | + ADR, Alternative Architecture, Risk Register, Invariant Tests |
| 9-10 | Maximum | + Red Team Review, Executable Invariants для всех ADR | | 9-10 | Maximum | + Red Team Review, Executable Invariants для всех ADR |
### Функции (таблица вкл/выкл) ### Функции
| Функция | Фаза | Глубина | Описание | | Функция | Фаза | Глубина | Описание |
|---|---|---|---| |---|---|---|---|
@@ -46,180 +63,190 @@ INIT → ANALYSE → [DESIGN] → [RED_TEAM] → DECOMPOSITION → SETUP → (CH
| red_team | DESIGN (после) | >=9 | Red Team Review — попытка разрушить архитектуру | | red_team | DESIGN (после) | >=9 | Red Team Review — попытка разрушить архитектуру |
| risk_register | DESIGN | >=7 | Явный реестр допущений | | risk_register | DESIGN | >=7 | Явный реестр допущений |
| invariant_tests | DECOMPOSITION | >=7 | Задачи-инварианты для каждого ADR | | invariant_tests | DECOMPOSITION | >=7 | Задачи-инварианты для каждого ADR |
| layer_structure | HANDOFF | любая | Организация .agent/ по слоям (layer-0..3) |
--- ---
## Фаза 0: INIT ## Фаза 0: INIT
**Вход:** целевой репозиторий + опционально `.agent/metaagent-request.md`.
**Протокол:** `PROTOCOLS/00_CONFIG.md` **Протокол:** `PROTOCOLS/00_CONFIG.md`
**Действия:** **Действия:**
- Прочитать `VERSION` — текущая версия MetaAgent - Прочитать `VERSION` — текущая версия MetaAgent
- Склонировать/открыть целевой репозиторий - Склонировать/открыть целевой репозиторий
- Создать директорию `.agent/` в корне целевого репозитория (если нет) - Создать директорию `.agent/` в корне целевого репозитория (если нет)
- **Установить исходники MetaAgent в `.agent/src/`:** - Создать `.temp/` в корне целевого репозитория (если нет), добавить в `.gitignore`
- Скопировать `META_AGENT_GUIDE.md`, `BOUNDARIES.md`, `WORKFLOW.md`, `VERSION` в `.agent/src/` - Установить исходники MetaAgent в `.agent/src/`
- Скопировать `PROTOCOLS/` и `TEMPLATES/` в `.agent/src/` - Создать структуру `.agent/`: `rules/`, `decisions/`, `tasks/` (с `backlog/`), `context/`, `requests/` (с `active/`, `archive/`), `roadmap/` (с `archive/`), `archive/` (с `tasks/`, `decisions/`, `checkpoints/`)
- Скопировать `install.sh` и `install.ps1` в `.agent/src/` (для возможности обновления) - Создать/обновить `AGENTS.md` в корне
- Если файлы уже существуют — пропустить (не перезаписывать) - Прочитать/создать `.agent/metaagent-request.md` (интервью или default)
- **Создать `.agent/rules/`** — директорию для пользовательских правил - Проверить версию, инициализировать `checkpoints.json`
- Если `.agent/rules/project-rules.md` не существует — создать из шаблона `.agent/src/TEMPLATES/project-rules.md`
- **Создать/обновить `AGENTS.md` в корне целевого репозитория** (если нет — создать, если есть — не трогать)
- Прочитать `PROTOCOLS/00_CONFIG.md`
- Выполнить 00_CONFIG:
- Если `.agent/metaagent-request.md` существует — прочитать config из него
- Если нет — провести интервью с пользователем (или принять `default`)
- Валидировать config относительно depth
- Если не было файла — создать `.agent/metaagent-request.md` с пометкой Auto-generated
- **Проверить версию:** если `.agent/checkpoints.json` существует → выполнить `PROTOCOLS/00_MIGRATE.md` (сравнить metaagent_version, применить миграцию при необходимости)
- Прочитать `PROTOCOLS/01_ANALYSIS.md`
- Инициализировать `.agent/checkpoints.json` с `metaagent_version` (если не существовал)
```json **Выход:** готовая `.agent/` + checkpoints.json.
{
"metaagent_version": "1.1.0",
"session_id": "<uuid>",
"target_repo": "<path>",
"goal": "<цель от пользователя>",
"project_type": "pending",
"config": {
"depth": 4,
"design": { "adr": false, "alternative_arch": false },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": false },
"handoff": { "layer_structure": false }
},
"phases": {
"analysis": "pending",
"design": "pending",
"red_team": "pending",
"decomposition": "pending",
"environment": "pending",
"handoff": "pending"
},
"tasks": [],
"last_updated": "<timestamp>"
}
```
**Выход:** готовая `.agent/` + checkpoints.json с metaagent_version и config.
--- ---
## Фаза 1: ANALYSE ## Фаза 1: ANALYSIS
**Вход:** целевой репозиторий, `.agent/metaagent-request.md` (или auto-generated), checkpoints.json (analysis: pending, config: from INIT).
**Протокол:** `PROTOCOLS/01_ANALYSIS.md` **Протокол:** `PROTOCOLS/01_ANALYSIS.md`
**Действия:** **Действия:**
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила - Прочитать `.agent/rules/project-rules.md`
- Прочитать config из checkpoints.json (уже получен на INIT через 00_CONFIG) - Прочитать config из checkpoints.json
- Если config отсутствует — применить default config (depth=4) как fallback - Выполнить анализ репозитория:
- Выполнить анализ репозитория по протоколу (определяет тип проекта) - Определить тип проекта (existing / greenfield / scaffold)
- Записать результат в `.agent/analysis-report.md` - Зафиксировать стек, архитектуру, конвенции, тесты
- Обновить checkpoints.json: `phases.analysis = "completed"`, `project_type = "existing" | "greenfield" | "scaffold"` - Для greenfield — извлечь требования из README
- Создать начальный `.agent/context/project-state.md` — слепок проекта
**Выход:** `.agent/analysis-report.md` - Записать `.agent/context/analysis-report.md`
- Обновить checkpoints.json
**Ветвление:** **Ветвление:**
- `project_type = "greenfield"` или `"scaffold"` → далее фаза DESIGN - `project_type = "greenfield"` или `"scaffold"` → далее ROADMAP → DESIGN
- `project_type = "existing"` → DESIGN пропускается, сразу DECOMPOSITION - `project_type = "existing"`далее ROADMAP (DESIGN пропускается)
**Выход:** `.agent/context/analysis-report.md`, `.agent/context/project-state.md`
--- ---
## Фаза 2: DESIGN (условная) ## Фаза 2: ROADMAP
**Вход:** analysis-report.md, checkpoints.json (analysis: completed, project_type: greenfield/scaffold). **Протокол:** `PROTOCOLS/02_ROADMAP.md`
**Действия:**
- Прочитать `.agent/rules/project-rules.md`
- Сканировать FUTURE/ — долгосрочные планы
- Сканировать `.agent/decisions/index.json` — ADR, требующие реализации
- Учесть пользовательские запросы и выявленные улучшения
- Приоритизировать все источники (P0-P3)
- Создать `.agent/roadmap/sources.md`
**Выход:** `.agent/roadmap/sources.md`
---
## Фаза 3: DESIGN (условная)
**Протокол:** `PROTOCOLS/02_DESIGN.md` **Протокол:** `PROTOCOLS/02_DESIGN.md`
Выполняется только для greenfield/scaffold.
**Действия:** **Действия:**
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила
- Спроектировать архитектуру, модули, данные, интерфейсы - Спроектировать архитектуру, модули, данные, интерфейсы
- Если config.design.alternative_arch: описать альтернативную архитектуру - Если config.design.adr: создать ADR → `.agent/decisions/`
- Если config.design.adr: создать ADR для каждого ключевого решения → `.agent/layer-1/adr/` - Если config.risk_register: создать `.agent/context/risk-register.md`
- Если config.risk_register: создать `.agent/layer-1/risk-register.md` - Записать `.agent/context/design-report.md`
- Записать результат в `.agent/design-report.md`
- Обновить checkpoints.json: `phases.design = "completed"`
**Ветвление:** **Выход:** `.agent/context/design-report.md`, опционально ADR, risk-register
- Если config.red_team = yes → следующая фаза RED_TEAM
- Иначе → сразу DECOMPOSITION
**Выход:** `.agent/design-report.md`, опционально `.agent/layer-1/adr/*.md`, `.agent/layer-1/risk-register.md`
--- ---
## Фаза 2b: RED_TEAM (опциональная) ## Фаза 3b: RED_TEAM (опциональная)
**Вход:** design-report.md, ADR (опционально), checkpoints.json (design: completed).
**Протокол:** `PROTOCOLS/02b_REDTEAM.md` **Протокол:** `PROTOCOLS/02b_REDTEAM.md`
**Действия:** Только если config.red_team = yes (depth >= 9).
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила
- Выполнить Red Team Review по протоколу
- Записать результат в `.agent/layer-1/red-team-report.md`
- Дополнить risk-register.md (если существует)
- Если найдены критические проблемы — исправить design-report
- Обновить checkpoints.json: `phases.red_team = "completed"`
**Выход:** `.agent/layer-1/red-team-report.md` **Выход:** `.agent/context/red-team-report.md`
--- ---
## Фаза 3: DECOMPOSITION ## Фаза 4: DECOMPOSITION
**Вход:** analysis-report.md + design-report.md (опционально) + ADR (опционально) + checkpoints.json.
**Протокол:** `PROTOCOLS/03_DECOMPOSITION.md` **Протокол:** `PROTOCOLS/03_DECOMPOSITION.md`
**Действия:** **Действия:**
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила - Прочитать `.agent/rules/project-rules.md`
- Разбить цель (и дизайн) на атомарные задачи - Разбить цель (и дизайн) на атомарные задачи
- Если config.decomposition.invariant_tests: создать задачи-инварианты для каждого ADR - Каждой задаче присвоить `origin` (источник: roadmap, ADR, user, agent)
- Записать манифест в `.agent/task-manifest.json` и `.agent/task-manifest.md` - Если есть `.agent/roadmap/sources.md` — сверить приоритеты
- Обновить checkpoints.json: `phases.decomposition = "completed"`, заполнить `tasks` - Если config.invariant_tests: создать задачи-инварианты для ADR
- Записать `.agent/tasks/manifest.json` и `.agent/tasks/manifest.md`
**Выход:** `.agent/task-manifest.json`, `.agent/task-manifest.md` **Выход:** `.agent/tasks/manifest.json` + `.agent/tasks/manifest.md`
--- ---
## Фаза 4: SETUP ## Фаза 5: EXECUTION (циклическая)
**Вход:** analysis-report.md, design-report.md (опционально), task-manifest.json, checkpoints.json (decomposition: completed). **Протокол:** `PROTOCOLS/04_EXECUTION.md`
**Протокол:** `PROTOCOLS/04_ENVIRONMENT_SETUP.md`
**Действия:** **Действия:**
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила 1. Выбрать следующую задачу из manifest.json (pending, все depends_on выполнены)
- Выполнить настройку окружения по протоколу (ветка A для existing, ветка B для greenfield) 2. Отметить `in_progress`
- Записать результат проверки в `.agent/baseline-test-report.log` и `.agent/setup-report.log` 3. Реализовать (код, тесты, конфиги)
- Обновить checkpoints.json: `phases.environment = "completed"` 4. Верифицировать (тесты, LSP diagnostics)
5. Закоммитить
6. Создать request в `.agent/requests/active/req-{id}.json`
7. Отметить `completed` в manifest.json
8. Повторить, пока есть задачи
9. Если задач нет — ожидать команду пользователя
**Выход:** рабочее окружение + `.agent/baseline-test-report.log` **Request — единица результата:**
```json
{
"request_id": "req-T1",
"task_id": "T1",
"title": "Human-readable title",
"status": "ready_for_review",
"changes": {
"summary": "Суть изменений",
"commits": ["abc1234"],
"files_changed": ["path/to/file.py"]
},
"verification": {
"tests_passed": "24/24",
"lsp_clean": true
},
"fulfills_ac": ["AC1"]
}
```
**Выход:** выполненные задачи в manifest + request-ы в `.agent/requests/active/`
--- ---
## Фаза 5: CHECKPOINT (сквозная) ## Фаза 6: METASTATE (по команде пользователя)
**Вход:** любая фаза. **Протокол:** `PROTOCOLS/06_METASTATE.md`
**Протокол:** обновлять checkpoints.json после каждого значимого шага. Запускается по команде: «обнови метасостояние», «update metastate», «подведи итог».
**Архивирование перед сохранением чекпоинта:** **Действия:**
- Если checkpoints.json уже существует — сохранить предыдущую версию в `.agent/archive/checkpoints/<last_updated>.json` 1. **Ревью requests** — проверить каждый `ready_for_review`:
- ✅ approved → в `.agent/requests/archive/`, задача confirmed
- ❌ rejected → задача reopened, комментарий
2. **Архивация** — completed задачи → one-liner в manifest, детали в `.agent/archive/tasks/`
3. **Обновление project-state.md** — актуальный слепок проекта
4. **Обновление roadmap** — отметить выполненное, пересчитать приоритеты
5. **Создание handoff-summary.md** — полная сводка для следующего агента
6. **Индекс архива**`.agent/archive/index.json`
**Формат:** **Выход:** обновлённый `.agent/` — полный слепок проекта. Следующий агент читает только `.agent/`.
---
## Фаза 7: HANDOFF
**Протокол:** `PROTOCOLS/05_HANDOFF.md`
**Действия:**
- Если METASTATE был — просто валидировать и финализировать
- Если METASTATE не было — лёгкая архивация completed задач
- Валидация структуры `.agent/`
- Создание `.agent/session-summary.md`
- Финализация checkpoints.json
**Выход:** `.agent/session-summary.md`, финальный checkpoints.json
---
## Checkpoint (сквозная)
checkpoints.json обновляется после каждой фазы:
```json ```json
{ {
"metaagent_version": "1.1.0", "metaagent_version": "2.1.0",
"session_id": "<uuid>", "session_id": "<uuid>",
"target_repo": "<path>", "target_repo": "<path>",
"goal": "<цель>", "goal": "<цель>",
@@ -229,108 +256,125 @@ INIT → ANALYSE → [DESIGN] → [RED_TEAM] → DECOMPOSITION → SETUP → (CH
"design": { "adr": true, "alternative_arch": true }, "design": { "adr": true, "alternative_arch": true },
"red_team": false, "red_team": false,
"risk_register": false, "risk_register": false,
"decomposition": { "invariant_tests": true }, "decomposition": { "invariant_tests": true }
"handoff": { "layer_structure": true }
}, },
"phases": { "phases": {
"analysis": "completed", "analysis": "completed",
"roadmap": "completed",
"design": "completed", "design": "completed",
"red_team": "skipped", "red_team": "skipped",
"decomposition": "in_progress", "decomposition": "completed",
"environment": "pending", "execution": "completed",
"handoff": "pending" "metastate": "completed",
"handoff": "completed"
}, },
"tasks": [ "tasks": [
{ "id": "T1", "title": "...", "status": "completed", { "id": "T1", "title": "...", "status": "archived", "origin": "user:direct" },
"depends_on": [], "acceptance_criteria": ["..."] }, { "id": "T2", "title": "...", "status": "pending", "origin": "roadmap:010" }
{ "id": "T2", "title": "...", "status": "pending",
"depends_on": ["T1"], "acceptance_criteria": ["..."] }
], ],
"last_updated": "<timestamp>" "last_updated": "<timestamp>"
} }
``` ```
`status` может быть: `pending`, `in_progress`, `completed`, `failed`, `skipped`.
Фаза `red_team` может быть `skipped` если config.red_team = false.
---
## Фаза 6: HANDOFF
**Вход:** все предыдущие фазы completed.
**Протокол:** `PROTOCOLS/05_HANDOFF.md`
**Действия:**
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила
- **Архивировать завершённые задачи:**
- Для каждой задачи со статусом `completed` в `task-manifest.json`:
- Перенести полное описание в `.agent/archive/tasks/<id>.json`
- Заменить в манифесте на one-liner: `{ "id": "<id>", "title": "<title>", "status": "archived" }`
- Создать `.agent/archive/index.json` со списком архивированных задач
- Заархивировать предыдущий `checkpoints.json` в `.agent/archive/checkpoints/`
- Выполнить валидацию всех артефактов
- Если config.handoff.layer_structure: организовать `.agent/` по слоям
- Записать `.agent/handoff-summary.md` (в layer-3 при layer_structure=yes)
- Создать `.agent/session-summary.md` (в layer-0 при layer_structure=yes)
- Обновить checkpoints.json: `phases.handoff = "completed"`
- Сообщить пользователю/оркестратору
**Выход:** `.agent/handoff-summary.md` — итоговый документ для исполнительного агента.
---
## Фаза 7: EXIT
Мета-агент завершает работу. Управление переходит к исполнительному агенту.
--- ---
## Структура .agent/ ## Структура .agent/
.agent/ всегда содержит служебную директорию `src/` с исходниками MetaAgent (см. фазу INIT).
При layer_structure=yes артефакты сессии раскладываются по слоям layer-0..3.
``` ```
.agent/ .agent/
src/ # исходники MetaAgent (всегда) checkpoints.json # состояние сессии (ядро)
META_AGENT_GUIDE.md # главная инструкция session-summary.md # краткая сводка сессии
PROTOCOLS/ # протоколы фаз handoff-summary.md # сводка для следующего агента (создаётся METASTATE)
TEMPLATES/ # шаблоны артефактов
BOUNDARIES.md # границы src/ # исходники MetaAgent (всегда)
WORKFLOW.md # примеры работы META_AGENT_GUIDE.md
VERSION # версия MetaAgent PROTOCOLS/
install.sh # скрипт установки/обновления (Unix) TEMPLATES/
install.ps1 # скрипт установки/обновления (Windows) BOUNDARIES.md
rules/ # пользовательские правила (всегда) WORKFLOW.md
project-rules.md # правила проекта — читать перед каждой фазой VERSION
archive/ # архив завершённых артефактов (создаётся при HANDOFF) install.sh / install.ps1
index.json # мета-индекс архива
tasks/ # детали завершённых задач rules/
checkpoints/ # исторические чекпоинты project-rules.md
adr/ # заменённые ADR
reports/ # устаревшие отчёты roadmap/ # ИСТОЧНИКИ ЗАДАЧ (новое в v2.1)
layer-0/ # ядро сессии (только при layer_structure=yes) sources.md # консолидированный список с приоритетами
checkpoints.json # всегда (ядро) archive/ # устаревшие roadmap-планы
session-summary.md # краткая сводка сессии
layer-1/ # архитектурные решения (справочно) decisions/ # архитектурные решения (ADR)
adr/ index.json
001-технологический-стек.md 001-*.md
002-архитектурный-паттерн.md
... tasks/ # задачи
manifest.json # + поле origin
manifest.md
backlog/
requests/ # ЕДИНИЦЫ РЕЗУЛЬТАТА (новое в v2.1)
active/ # req-T1.json (ready_for_review)
archive/ # req-T1.json (approved/rejected)
context/
analysis-report.md # замороженный анализ на старте
project-state.md # динамический слепок проекта (обновляется METASTATE)
design-report.md
risk-register.md risk-register.md
red-team-report.md red-team-report.md
layer-2/ # дизайн и анализ (справочно)
analysis-report.md
design-report.md
layer-3/ # состояние исполнения
handoff-summary.md
task-manifest.json
task-manifest.md
baseline-test-report.log baseline-test-report.log
setup-report.log
archive/
index.json
tasks/
decisions/
requests/
checkpoints/
``` ```
Исполнительный агент всегда начинает с layer-0 (checkpoints + session-summary), `.temp/` в корне проекта:
затем при необходимости обращается к layer-1 (ADR для понимания "почему"),
layer-2 (детали дизайна), layer-3 (что было сделано). ```
.temp/
downloads/
patches/
cache/
agent-session-xxx/
```
---
## Принципы работы
### Два контура
**Project Loop** (однократно): INIT → ANALYSIS → ROADMAP → DESIGN → DECOMPOSITION.
Настраивает проект, определяет задачи.
**Work Loop** (циклически): EXECUTION → (request) → METASTATE (по команде).
Агент работает, создаёт requests, по команде пользователя подводит итог.
### Request — единица результата
Каждая выполненная задача завершается созданием request. Не просто «сделано», а документированный результат:
- суть изменений (не diff, а именно суть)
- ссылки на коммиты
- верификация (тесты, LSP)
- какие acceptance criteria закрыты
Request проходит ревью в METASTATE.
### .agent/ как слепок проекта
После METASTATE `.agent/` содержит полную картину. Следующий агент читает `.agent/` и не лезет в исходники проекта.
### Depth scale
Определяет глубину проработки:
| Depth | PROJECT LOOP | WORK LOOP |
|-------|-------------|-----------|
| 1-2 | INIT → ANALYSIS → DECOMP | EXECUTION (scaffold only) |
| 3-4 | + ROADMAP, DESIGN (light) | EXECUTION → METASTATE |
| 5-6 | + DESIGN (full), invariants | EXECUTION → METASTATE |
| 7-8 | + ADR, risk_register | EXECUTION → METASTATE |
| 9-10 | + Red Team | EXECUTION → METASTATE |
+2 -5
View File
@@ -66,9 +66,6 @@ Q5 (если глубина >= 9): Нужен Red Team Review?
"risk_register": false, "risk_register": false,
"decomposition": { "decomposition": {
"invariant_tests": false "invariant_tests": false
},
"handoff": {
"layer_structure": false
} }
} }
``` ```
@@ -78,7 +75,7 @@ Depth=4 (Light) означает:
- DESIGN — выполняется (если greenfield), но **без** ADR, Alternative Architecture, Risk Register - DESIGN — выполняется (если greenfield), но **без** ADR, Alternative Architecture, Risk Register
- DECOMPOSITION — задачи с acceptance criteria, **без** invariant-тестов - DECOMPOSITION — задачи с acceptance criteria, **без** invariant-тестов
- SETUP — полный - SETUP — полный
- HANDOFF — плоский `.agent/` (без layer-структуры) - HANDOFF — `.agent/` организован по семантическим группам (decisions, tasks, context, rules)
### 0.4. Запись .agent/metaagent-request.md ### 0.4. Запись .agent/metaagent-request.md
@@ -98,7 +95,7 @@ Depth=4 (Light) означает:
| RISK_REGISTER | {{ risk_register }} | — | | RISK_REGISTER | {{ risk_register }} | — |
| DECOMPOSITION | ✓ | invariant_tests={{ invariant_tests }} | | DECOMPOSITION | ✓ | invariant_tests={{ invariant_tests }} |
| SETUP | ✓ | — | | SETUP | ✓ | — |
| HANDOFF | ✓ | layer_structure={{ layer_structure }} | | HANDOFF | ✓ | |
## Глубина проработки ## Глубина проработки
+154 -1
View File
@@ -45,6 +45,7 @@ def needs_migration(stored, current):
|---|---|---| |---|---|---|
| v0.x (нет поля) | v1.0.0 | M3.1 — M3.4 | | v0.x (нет поля) | v1.0.0 | M3.1 — M3.4 |
| v1.0.0 | v1.1.0 | M3.5 — M3.6 (см. ниже) | | v1.0.0 | v1.1.0 | M3.5 — M3.6 (см. ниже) |
| v1.1.x | v2.0.0 | M6.1 — M6.17 (см. ниже) |
### M4. Шаги миграции v0.x → v1.0.0 ### M4. Шаги миграции v0.x → v1.0.0
@@ -97,6 +98,158 @@ touch .agent/layer-1/adr/.gitkeep
Если `risk-register.md` уже существует на верхнем уровне — переместить в `.agent/layer-1/risk-register.md`. Если `risk-register.md` уже существует на верхнем уровне — переместить в `.agent/layer-1/risk-register.md`.
### M6. Шаги миграции v1.1.x → v2.0.0
Цель: перейти от layer-0..3 структуры к семантической (decisions/tasks/context/rules).
#### M6.1. Удалить `handoff.layer_structure` из config
Если в `checkpoints.json` присутствует `config.handoff.layer_structure` — удалить поле:
```python
checkpoints["config"].pop("handoff", None)
# или если handoff пуст — удалить целиком
if "handoff" in checkpoints["config"] and not checkpoints["config"]["handoff"]:
del checkpoints["config"]["handoff"]
```
#### M6.2. Создать новые директории
```bash
mkdir -p .agent/decisions
mkdir -p .agent/tasks/backlog
mkdir -p .agent/context
mkdir -p .agent/archive/decisions
```
#### M6.3. Перенести ADR (.agent/layer-1/adr/ → .agent/decisions/)
```bash
if [ -d ".agent/layer-1/adr" ]; then
cp -n .agent/layer-1/adr/*.md .agent/decisions/ 2>/dev/null || true
fi
```
#### M6.4. Создать decisions/index.json
Если в `.agent/decisions/` есть .md файлы — создать индекс:
```json
{
"version": "2.0.0",
"decisions": [
{ "id": "001", "title": "<извлечь из первого заголовка>", "file": "001-....md" }
],
"created_at": "<timestamp>"
}
```
#### M6.5. Перенести risk-register.md (.agent/layer-1/ → .agent/context/)
```bash
if [ -f ".agent/layer-1/risk-register.md" ]; then
mv .agent/layer-1/risk-register.md .agent/context/risk-register.md
fi
```
#### M6.6. Перенести red-team-report.md (.agent/layer-1/ → .agent/context/)
```bash
if [ -f ".agent/layer-1/red-team-report.md" ]; then
mv .agent/layer-1/red-team-report.md .agent/context/red-team-report.md
fi
```
#### M6.7. Перенести analysis-report.md (.agent/layer-2/ → .agent/context/)
```bash
if [ -f ".agent/layer-2/analysis-report.md" ]; then
mv .agent/layer-2/analysis-report.md .agent/context/analysis-report.md
fi
```
#### M6.8. Перенести design-report.md (.agent/layer-2/ → .agent/context/)
```bash
if [ -f ".agent/layer-2/design-report.md" ]; then
mv .agent/layer-2/design-report.md .agent/context/design-report.md
fi
```
#### M6.9. Перенести task-manifest (.agent/task-manifest.json → .agent/tasks/manifest.json)
```bash
if [ -f ".agent/task-manifest.json" ]; then
mv .agent/task-manifest.json .agent/tasks/manifest.json
fi
if [ -f ".agent/task-manifest.md" ]; then
mv .agent/task-manifest.md .agent/tasks/manifest.md
fi
```
#### M6.10. Перенести handoff-summary.md (.agent/layer-3/ → .agent/)
```bash
if [ -f ".agent/layer-3/handoff-summary.md" ]; then
mv .agent/layer-3/handoff-summary.md .agent/handoff-summary.md
fi
```
#### M6.11. Перенести baseline-test-report.log (.agent/layer-3/ → .agent/context/)
```bash
if [ -f ".agent/layer-3/baseline-test-report.log" ]; then
mv .agent/layer-3/baseline-test-report.log .agent/context/baseline-test-report.log
fi
```
#### M6.12. Перенести setup-report.log (.agent/layer-3/ → .agent/context/)
```bash
if [ -f ".agent/layer-3/setup-report.log" ]; then
mv .agent/layer-3/setup-report.log .agent/context/setup-report.log
fi
```
#### M6.13. Перенести session-summary.md (.agent/layer-0/ → .agent/)
```bash
if [ -f ".agent/layer-0/session-summary.md" ]; then
mv .agent/layer-0/session-summary.md .agent/session-summary.md
fi
```
#### M6.14. Перенести checkpoints.json (.agent/layer-0/ → .agent/)
```bash
if [ -f ".agent/layer-0/checkpoints.json" ]; then
cp .agent/layer-0/checkpoints.json .agent/checkpoints.json
echo "[backup] layer-0/checkpoints.json сохранён на случай отката"
fi
```
#### M6.15. Перенести archive/adr/ → archive/decisions/
```bash
if [ -d ".agent/archive/adr" ]; then
cp -n .agent/archive/adr/* .agent/archive/decisions/ 2>/dev/null || true
rm -rf .agent/archive/adr
fi
```
#### M6.16. Удалить пустые layer-директории
```bash
rm -rf .agent/layer-0 .agent/layer-1 .agent/layer-2 .agent/layer-3
rm -rf .agent/archive/reports 2>/dev/null || true
```
#### M6.17. Обновить metaagent_version в checkpoints.json
```json
"metaagent_version": "2.0.0"
```
### M4. После миграции — резюме ### M4. После миграции — резюме
Записать в `.agent/migration-report.log`: Записать в `.agent/migration-report.log`:
@@ -112,7 +265,7 @@ touch .agent/layer-1/adr/.gitkeep
## Выход ## Выход
- Обновлённый `.agent/checkpoints.json` (metaagent_version + config) - Обновлённый `.agent/checkpoints.json` (metaagent_version + config)
- Опционально: `.agent/layer-1/` структура - Обновлённая структура `.agent/` (decisions/tasks/context вместо layer-0..3)
- `.agent/migration-report.log` - `.agent/migration-report.log`
## Критерии завершения ## Критерии завершения
+17 -7
View File
@@ -24,8 +24,7 @@
"design": { "adr": false, "alternative_arch": false }, "design": { "adr": false, "alternative_arch": false },
"red_team": false, "red_team": false,
"risk_register": false, "risk_register": false,
"decomposition": { "invariant_tests": false }, "decomposition": { "invariant_tests": false }
"handoff": { "layer_structure": false }
} }
``` ```
@@ -36,8 +35,7 @@
"design": { "adr": true, "alternative_arch": true }, "design": { "adr": true, "alternative_arch": true },
"red_team": false, "red_team": false,
"risk_register": false, "risk_register": false,
"decomposition": { "invariant_tests": true }, "decomposition": { "invariant_tests": true }
"handoff": { "layer_structure": true }
} }
``` ```
@@ -125,15 +123,27 @@
- Вопросы, которые нужно задать пользователю перед проектированием - Вопросы, которые нужно задать пользователю перед проектированием
- Противоречия в README - Противоречия в README
### 1.8. Initial project state snapshot
После завершения анализа создать `.agent/context/project-state.md` — начальный слепок проекта по шаблону `TEMPLATES/project-state.md`:
- Тип проекта
- Текущая архитектура (кратко, из p.1.3)
- Ключевые модули и их статус (existing/stub/nonexistent)
- Tech stack (из p.1.2)
- Статус тестов (из p.1.5)
- Этот файл будет обновляться фазой METASTATE по мере эволюции проекта
## Выход ## Выход
`.agent/analysis-report.md` по шаблону `TEMPLATES/analysis-report.md`. - `.agent/context/analysis-report.md` по шаблону `TEMPLATES/analysis-report.md`
- `.agent/context/project-state.md` — начальный слепок проекта
Обновить checkpoints.json: `phases.analysis = "completed"`. Если проект `greenfield`, также установить `project_type = "greenfield"`. Обновить checkpoints.json: `phases.analysis = "completed"`. Если проект `greenfield`, также установить `project_type = "greenfield"`.
## Критерии завершения фазы ## Критерии завершения фазы
- [ ] Тип проекта определён (existing / greenfield / scaffold) - [ ] Тип проекта определён (existing / greenfield / scaffold)
- [ ] Все соответствующие разделы (1.1–1.7) выполнены - [ ] Все соответствующие разделы (1.1–1.8) выполнены
- [ ] `.agent/analysis-report.md` создан и заполнен - [ ] `.agent/context/analysis-report.md` создан и заполнен
- [ ] `.agent/context/project-state.md` создан с начальным слепком
- [ ] checkpoints.json обновлён - [ ] checkpoints.json обновлён
+9 -9
View File
@@ -6,7 +6,7 @@
## Вход ## Вход
- `.agent/analysis-report.md` (project_type: greenfield или scaffold) - `.agent/context/analysis-report.md` (project_type: greenfield или scaffold)
- `.agent/metaagent-request.md` (конфигурация сессии: adr, alternative_arch, risk_register) - `.agent/metaagent-request.md` (конфигурация сессии: adr, alternative_arch, risk_register)
- `.agent/checkpoints.json` (фаза design: pending) - `.agent/checkpoints.json` (фаза design: pending)
@@ -120,16 +120,16 @@
Для каждого ключевого архитектурного решения (стек, БД, паттерн, структура модулей) создать отдельный ADR-файл: Для каждого ключевого архитектурного решения (стек, БД, паттерн, структура модулей) создать отдельный ADR-файл:
``` ```
.agent/layer-1/adr/001-технологический-стек.md .agent/decisions/001-технологический-стек.md
.agent/layer-1/adr/002-модульный-монолит.md .agent/decisions/002-модульный-монолит.md
.agent/layer-1/adr/003-json-хранение.md .agent/decisions/003-json-хранение.md
``` ```
Формат — по шаблону `TEMPLATES/adr-NNNN.md`. Формат — по шаблону `TEMPLATES/adr-NNNN.md`.
### 2.10. Risk Register (если config.risk_register = yes) ### 2.10. Risk Register (если config.risk_register = yes)
Создать `.agent/layer-1/risk-register.md` по шаблону `TEMPLATES/risk-register.md`: Создать `.agent/context/risk-register.md` по шаблону `TEMPLATES/risk-register.md`:
| # | Assumption | Impact if wrong | Mitigation | Review trigger | | # | Assumption | Impact if wrong | Mitigation | Review trigger |
|---|---|---|---|---| |---|---|---|---|---|
@@ -150,9 +150,9 @@ T4: API endpoints
## Выход ## Выход
- `.agent/design-report.md` по шаблону `TEMPLATES/design-report.md` - `.agent/context/design-report.md` по шаблону `TEMPLATES/design-report.md`
- `.agent/layer-1/adr/*.md` (если adr=yes) - `.agent/decisions/*.md` (если adr=yes)
- `.agent/layer-1/risk-register.md` (если risk_register=yes) - `.agent/context/risk-register.md` (если risk_register=yes)
- Предварительная группировка задач (для передачи в DECOMPOSITION) - Предварительная группировка задач (для передачи в DECOMPOSITION)
Обновить checkpoints.json: `phases.design = "completed"`. Обновить checkpoints.json: `phases.design = "completed"`.
@@ -169,5 +169,5 @@ T4: API endpoints
- [ ] ADR созданы (если config требует) - [ ] ADR созданы (если config требует)
- [ ] Risk Register создан (если config требует) - [ ] Risk Register создан (если config требует)
- [ ] Задачи предварительно сгруппированы - [ ] Задачи предварительно сгруппированы
- [ ] `.agent/design-report.md` создан - [ ] `.agent/context/design-report.md` создан
- [ ] checkpoints.json обновлён - [ ] checkpoints.json обновлён
+99
View File
@@ -0,0 +1,99 @@
# Протокол 02: Дорожная карта (ROADMAP)
## Цель
Определить источники задач для проекта, их приоритеты и взаимосвязи. ROADMAP — мост между видением проекта и конкретными задачами в манифесте.
## Вход
- `.agent/context/analysis-report.md` — анализ репозитория
- `.agent/metaagent-request.md` — цель сессии
- `FUTURE/` — директория долгосрочных планов (если существует)
- `.agent/decisions/index.json` — принятые ADR (опционально)
- `.agent/checkpoints.json` (фаза roadmap: pending)
- Внешние источники: issues, feedback, пользовательские запросы
## Шаги
### 2.1. Сканирование FUTURE/
Если в корне проекта существует `FUTURE/`:
- Прочитать все `.md` файлы
- Каждый план: название, статус (active/archived), приоритет, зависимости
- Зафиксировать, какие планы уже реализованы, какие ожидают
### 2.2. Сканирование ADR
Если существует `.agent/decisions/index.json`:
- Прочитать индекс ADR
- Определить, какие решения требуют реализации (не все ADR — технические, часть может быть организационными)
- Для каждого ADR, требующего реализации: сформулировать задачу
### 2.3. Внешние источники
- Прочитать `.agent/metaagent-request.md` — явные запросы пользователя
- Если есть issues / feedback — включить в анализ
- Если агент обнаружил tech debt или улучшения в ANALYSIS — зафиксировать
### 2.4. Приоритизация
Присвоить каждой задаче приоритет:
| Приоритет | Описание |
|-----------|----------|
| **P0** | Критично, делать следующим |
| **P1** | Важно, сделать скоро |
| **P2** | Желательно |
| **P3** | В долгосрочной перспективе / отложено |
Правила приоритизации:
- Блокирующие зависимости поднимают приоритет задачи
- User-requested задачи получают P0-P1 по умолчанию
- ADR-задачи получают приоритет, соответствующий срочности решения
### 2.5. Консолидация в sources.md
Создать `.agent/roadmap/sources.md` по шаблону `TEMPLATES/roadmap-sources.md`:
```
# Roadmap Sources
## FUTURE Plans
| План | Приоритет | Статус |
|------|-----------|--------|
| 010-omo-integration | P1 | active |
## ADR-Derived Tasks
| ADR | Задача | Приоритет |
|-----|--------|-----------|
## User Requests
| Запрос | Приоритет | Источник |
|--------|-----------|----------|
## Agent-Identified Improvements
| Наблюдение | Задача | Приоритет |
|------------|--------|-----------|
## Consolidated Priority Queue
1. task (origin) — P0
```
### 2.6. Архивация устаревших roadmap
Если в `.agent/roadmap/archive/` есть предыдущие версии — они остаются справочно.
Если какие-то планы из FUTURE/* больше не актуальны — переместить в `FUTURE/archive/`.
## Выход
- `.agent/roadmap/sources.md` — консолидированный список источников задач с приоритетами
- Обновлённый `FUTURE/` (если были перемещения в archive)
- Обновить checkpoints.json: `phases.roadmap = "completed"`
## Критерии завершения
- [ ] Все источники задач просканированы (FUTURE, ADR, пользователь, агент)
- [ ] `.agent/roadmap/sources.md` создан с приоритетами P0-P3
- [ ] Каждая задача имеет origin-ссылку на источник
- [ ] Устаревшие планы перемещены в archive
- [ ] checkpoints.json обновлён
+3 -3
View File
@@ -6,8 +6,8 @@
## Вход ## Вход
- `.agent/design-report.md` - `.agent/context/design-report.md`
- `.agent/layer-1/adr/*.md` (если созданы) - `.agent/decisions/*.md` (если созданы)
- `.agent/metaagent-request.md` (глубина проработки >= 9) - `.agent/metaagent-request.md` (глубина проработки >= 9)
## Когда выполняется ## Когда выполняется
@@ -56,7 +56,7 @@
## Выход ## Выход
`.agent/layer-1/red-team-report.md` с секциями: `.agent/context/red-team-report.md` с секциями:
``` ```
## Найденные проблемы ## Найденные проблемы
+33 -15
View File
@@ -6,10 +6,11 @@
## Вход ## Вход
- `.agent/analysis-report.md` - `.agent/context/analysis-report.md`
- `.agent/design-report.md` (опционально — для greenfield/scaffold) - `.agent/context/design-report.md` (опционально — для greenfield/scaffold)
- `.agent/layer-1/adr/*.md` (опционально) - `.agent/decisions/*.md` (опционально)
- `.agent/layer-1/risk-register.md` (опционально) - `.agent/context/risk-register.md` (опционально)
- `.agent/roadmap/sources.md` (опционально — из фазы ROADMAP)
- `.agent/metaagent-request.md` (конфигурация сессии) - `.agent/metaagent-request.md` (конфигурация сессии)
- Цель пользователя (из checkpoints.json) - Цель пользователя (из checkpoints.json)
- `.agent/checkpoints.json` (фаза decomposition: pending) - `.agent/checkpoints.json` (фаза decomposition: pending)
@@ -33,7 +34,14 @@
- Затрагивает 5+ файлов - Затрагивает 5+ файлов
- Содержит союзы "и", "а также", "после чего" - Содержит союзы "и", "а также", "после чего"
### 3.3. Структура задачи ### 3.3. Учёт roadmap
Если существует `.agent/roadmap/sources.md`:
- Сверить задачи с roadmap-приоритетами
- Задачи из roadmap получают приоритет P0-P3 в соответствии с sources.md
- Задачи без явного источника получают `origin: "decomposition"`
### 3.4. Структура задачи
Каждая задача содержит: Каждая задача содержит:
@@ -44,12 +52,20 @@
| `description` | Описание (как и зачем) | "Создать SQLAlchemy модель..." | | `description` | Описание (как и зачем) | "Создать SQLAlchemy модель..." |
| `type` | Тип задачи | `feature`, `refactor`, `test`, `fix`, `config`, `design`, `docs` | | `type` | Тип задачи | `feature`, `refactor`, `test`, `fix`, `config`, `design`, `docs` |
| `status` | Статус задачи | `pending`, `in_progress`, `completed`, `failed`, `archived` | | `status` | Статус задачи | `pending`, `in_progress`, `completed`, `failed`, `archived` |
| `origin` | Источник задачи | `roadmap:filename`, `adr:NNN`, `user:direct`, `agent:analysis`, `decomposition` |
| `files` | Список файлов, которые нужно создать/изменить | `["app/models/user.py"]` | | `files` | Список файлов, которые нужно создать/изменить | `["app/models/user.py"]` |
| `depends_on` | ID задач, от которых зависит | `[]` или `["T0"]` | | `depends_on` | ID задач, от которых зависит | `[]` или `["T0"]` |
| `acceptance_criteria` | Список критериев приёмки (3-5 пунктов) | `["Модель проходит миграцию"]` | | `acceptance_criteria` | Список критериев приёмки (3-5 пунктов) | `["Модель проходит миграцию"]` |
| `context` | Доп. информация (ссылки на доки, примеры, релевантные секции из design-report) | `"Смотри app/models/base.py"` | | `context` | Доп. информация (ссылки на доки, примеры, релевантные секции из design-report) | `"Смотри app/models/base.py"` |
### 3.4. Типы задач `origin` связывает задачу с источником:
- `roadmap:{filename}` — из FUTURE/ или roadmap плана
- `adr:{NNN}` — из Architecture Decision Record
- `user:direct` — напрямую от пользователя
- `agent:analysis` — выявлено агентом при анализе
- `decomposition` — создано при декомпозиции без внешнего источника
### 3.5. Типы задач (нумерация сдвинута)
| Тип | Описание | | Тип | Описание |
|---|---| |---|---|
@@ -62,25 +78,25 @@
| `docs` | Документация | | `docs` | Документация |
| `invariant` | Тест, проверяющий архитектурный инвариант (см. 3.7) | | `invariant` | Тест, проверяющий архитектурный инвариант (см. 3.7) |
### 3.5. Зелёная декомпозиция (для greenfield/scaffold) ### 3.6. Зелёная декомпозиция (для greenfield/scaffold)
Если есть `.agent/design-report.md` — задачи формируются на основе группировки из дизайна: Если есть `.agent/context/design-report.md` — задачи формируются на основе группировки из дизайна:
1. **T1: init** — инициализация проекта, зависимости, конфиги, scaffold 1. **T1: init** — инициализация проекта, зависимости, конфиги, scaffold
2. **T2..Tn: features** — модули/функциональность по одному 2. **T2..Tn: features** — модули/функциональность по одному
3. **Tn+1: tests** — тесты на каждый модуль (можно в составе feature-задачи) 3. **Tn+1: tests** — тесты на каждый модуль (можно в составе feature-задачи)
4. **Tn+2: polish** — документация, форматирование, финальная проверка 4. **Tn+2: polish** — документация, форматирование, финальная проверка
### 3.6. Сортировка ### 3.7. Сортировка
Задачи в манифесте располагаются в порядке выполнения: Задачи в манифесте располагаются в порядке выполнения:
1. Сначала задачи без зависимостей 1. Сначала задачи без зависимостей
2. Потом те, чьи зависимости уже выполнены 2. Потом те, чьи зависимости уже выполнены
3. Последними — задачи с наибольшим числом зависимостей 3. Последними — задачи с наибольшим числом зависимостей
### 3.7. Executable Invariants (если config.invariant_tests = yes) ### 3.8. Executable Invariants (если config.invariant_tests = yes)
Для каждого ADR (из layer-1/adr/) создать задачу типа `invariant` — тест, проверяющий архитектурное правило. Для каждого ADR (из `.agent/decisions/`) создать задачу типа `invariant` — тест, проверяющий архитектурное правило.
**Правила превращения ADR в инварианты:** **Правила превращения ADR в инварианты:**
@@ -111,23 +127,25 @@
## Выход ## Выход
- `.agent/task-manifest.json` — по схеме `TEMPLATES/task-manifest.json` - `.agent/tasks/manifest.json` — по шаблону `TEMPLATES/task-manifest.json`
- `.agent/task-manifest.md` — по шаблону `TEMPLATES/task-manifest.md` - `.agent/tasks/manifest.md` — по шаблону `TEMPLATES/task-manifest.md`
Обновить checkpoints.json: Обновить checkpoints.json:
- `phases.decomposition = "completed"` - `phases.decomposition = "completed"`
- `tasks` = полный массив задач со статусом `pending` - `tasks` = полный массив задач со статусом `pending`
> **Примечание:** после HANDOFF завершённые задачи будут архивированы — > **Примечание:** после HANDOFF завершённые задачи будут архивированы —
> полное описание уходит в `.agent/archive/tasks/`, в манифесте остаётся > полное описание уходит в `.agent/archive/tasks/`, в manifest.json остаётся
> one-liner с `"status": "archived"`. > one-liner с `"status": "archived"`.
## Критерии завершения фазы ## Критерии завершения фазы
- [ ] Цель разбита на атомарные задачи - [ ] Цель разбита на атомарные задачи
- [ ] Для каждой задачи указаны acceptance criteria - [ ] Для каждой задачи указаны acceptance criteria
- [ ] Для каждой задачи указан origin (источник)
- [ ] Для каждой задачи указаны affected files - [ ] Для каждой задачи указаны affected files
- [ ] Зависимости между задачами корректны (нет циклов) - [ ] Зависимости между задачами корректны (нет циклов)
- [ ] Задачи сверены с roadmap приоритетами (если sources.md существует)
- [ ] Invariant-задачи созданы для каждого ADR (если config требует) - [ ] Invariant-задачи созданы для каждого ADR (если config требует)
- [ ] `.agent/task-manifest.json` и `.agent/task-manifest.md` созданы - [ ] `.agent/tasks/manifest.json` и `.agent/tasks/manifest.md` созданы
- [ ] checkpoints.json обновлён - [ ] checkpoints.json обновлён
+13 -9
View File
@@ -1,4 +1,8 @@
# Протокол 04: Настройка окружения (SETUP) # DEPRECATED — Протокол 04: Настройка окружения (SETUP)
> **Устарел в MetaAgent v2.1.** Заменён на `PROTOCOLS/04_EXECUTION.md`.
> Оставлен для обратной совместимости (проекты, использующие v2.0).
> Новые проекты используют фазу EXECUTION, в которой настройка окружения — первый шаг перед выполнением задач.
## Цель ## Цель
@@ -6,9 +10,9 @@
## Вход ## Вход
- `.agent/analysis-report.md` - `.agent/context/analysis-report.md`
- `.agent/design-report.md` (опционально, для greenfield) - `.agent/context/design-report.md` (опционально, для greenfield)
- `.agent/task-manifest.json` - `.agent/tasks/manifest.json`
- `.agent/checkpoints.json` (фаза environment: pending) - `.agent/checkpoints.json` (фаза environment: pending)
## Поведение в зависимости от типа проекта ## Поведение в зависимости от типа проекта
@@ -44,7 +48,7 @@
### 4A.4. Baseline-тесты ### 4A.4. Baseline-тесты
- Запустить все тесты проекта - Запустить все тесты проекта
- Записать в `.agent/baseline-test-report.log`: - Записать в `.agent/context/baseline-test-report.log`:
- Команда запуска - Команда запуска
- Общее количество тестов - Общее количество тестов
- Пройдено / упало / пропущено - Пройдено / упало / пропущено
@@ -97,7 +101,7 @@ class CashflowService:
- Создать пустой тестовый файл для каждого модуля - Создать пустой тестовый файл для каждого модуля
- Настроить тестовый раннер (pytest, jest и т.д.) - Настроить тестовый раннер (pytest, jest и т.д.)
- Записать в `.agent/baseline-test-report.log`: "0 tests — greenfield, scaffold готов" - Записать в `.agent/context/baseline-test-report.log`: "0 tests — greenfield, scaffold готов"
### 4B.5. Проверка сборки ### 4B.5. Проверка сборки
@@ -110,8 +114,8 @@ class CashflowService:
## Выход ## Выход
- Работоспособное окружение / инициализированный проект - Работоспособное окружение / инициализированный проект
- `.agent/baseline-test-report.log` — результат прогона тестов - `.agent/context/baseline-test-report.log` — результат прогона тестов
- `.agent/setup-report.log` — лог установки зависимостей и сборки - `.agent/context/setup-report.log` — лог установки зависимостей и сборки
Обновить checkpoints.json: `phases.environment = "completed"`. Обновить checkpoints.json: `phases.environment = "completed"`.
@@ -120,7 +124,7 @@ class CashflowService:
- [ ] Зависимости установлены / проект инициализирован - [ ] Зависимости установлены / проект инициализирован
- [ ] Проект собирается / импортируется без ошибок - [ ] Проект собирается / импортируется без ошибок
- [ ] Baseline-тесты запущены, результат записан - [ ] Baseline-тесты запущены, результат записан
- [ ] `.agent/baseline-test-report.log` и `.agent/setup-report.log` созданы - [ ] `.agent/context/baseline-test-report.log` и `.agent/context/setup-report.log` созданы
- [ ] checkpoints.json обновлён - [ ] checkpoints.json обновлён
Если проект не собирается — **фаза считается проваленной**, checkpoints.json отмечает `phases.environment = "failed"`, управление возвращается пользователю. Если проект не собирается — **фаза считается проваленной**, checkpoints.json отмечает `phases.environment = "failed"`, управление возвращается пользователю.
+138
View File
@@ -0,0 +1,138 @@
# Протокол 04: Исполнение задач (EXECUTION)
## Цель
Выполнить задачи из manifest.json: реализовать код, написать тесты, закоммитить, создать request — артефакт результата.
## Вход
- `.agent/tasks/manifest.json` — манифест с задачами
- `.agent/context/analysis-report.md` — контекст проекта
- `.agent/context/design-report.md` — архитектурный план (опционально)
- `.agent/decisions/*.md` — ADR (опционально)
- `.agent/rules/project-rules.md` — правила проекта
- `.agent/checkpoints.json` (фаза execution: pending)
## Шаги
### 4.0. Setup окружения (первый запуск)
Если это первый запуск EXECUTION в сессии:
- Установить зависимости (через штатный пакетный менеджер)
- Запустить сборку/базовые тесты для верификации окружения
- Записать baseline в `.agent/context/baseline-test-report.log`
### 4.1. Выбор задачи
Найти в `.agent/tasks/manifest.json` задачу, удовлетворяющую всем условиям:
- `status: "pending"`
- Все `depends_on` имеют статус `completed` или `archived`
Если таких задач нет — EXECUTION завершён, перейти к ожиданию команды пользователя.
### 4.2. Блокировка задачи
Отметить задачу в манифесте:
```json
{
"id": "T1",
"status": "in_progress"
}
```
### 4.3. Исполнение
Реализовать задачу в соответствии с acceptance criteria:
- Следовать конвенциям проекта (выявленным в ANALYSIS)
- Соблюдать BOUNDARIES.md
- Если задача ссылается на ADR — следовать архитектурному решению
- Писать код + тесты
### 4.4. Верификация
- Запустить тесты (все или релевантные)
- Проверить LSP diagnostics на изменённых файлах
- Убедиться, что acceptance criteria выполнены
### 4.5. Коммит
Сделать git-коммит с результатами задачи. Сообщение коммита должно отражать суть выполненной задачи.
### 4.6. Создание request
Создать `.agent/requests/active/req-{task_id}.json`:
```json
{
"request_id": "req-T1",
"task_id": "T1",
"title": "GET /health endpoint",
"status": "ready_for_review",
"goal": "Добавить ручку GET /health с тестами",
"changes": {
"summary": "Создан health router, подключён в main.py, написаны тесты",
"commits": ["abc1234", "abc1235"],
"files_changed": [
"app/routers/health.py",
"app/main.py",
"tests/test_health.py"
]
},
"verification": {
"tests_passed": "24/24",
"lsp_clean": true
},
"fulfills_ac": [
"Ручка возвращает 200 + {\"status\":\"ok\"}"
]
}
```
Request фиксирует:
- **changes.summary** — краткая суть изменений (не полный diff, а именно суть)
- **changes.commits** — ссылки на коммиты (чтобы можно было проанализировать при ревью)
- **changes.files_changed** — какие файлы затронуты
- **verification** — результаты проверки
- **fulfills_ac** — какие acceptance criteria закрыты
### 4.7. Завершение задачи
В манифесте:
```json
{
"id": "T1",
"status": "completed"
}
```
### 4.8. Цикл
Перейти к шагу 4.1 — выбрать следующую задачу.
Если задач больше нет — сообщить пользователю и ожидать команду (METASTATE или новую задачу).
## Request как единица результата
Request — ключевой артефакт v2.1. Не просто «задача сделана», а документированный результат:
- Что сделано (суть, не diff)
- Как проверить (коммиты, тесты)
- Что закрыто (acceptance criteria)
Request проходит ревью в фазе METASTATE:
- `ready_for_review` → после проверки → `approved` или `rejected`
## Выход
- Выполненные задачи в manifest.json (status: completed)
- `.agent/requests/active/req-{task_id}.json` для каждой выполненной задачи
- Обновлённый checkpoints.json (`phases.execution = "in_progress"` или `"completed"`)
## Критерии завершения
Фаза EXECUTION не имеет единого момента завершения — она циклична. Критерии для одной итерации:
- [ ] Acceptance criteria задачи выполнены
- [ ] Тесты проходят
- [ ] LSP diagnostics чист
- [ ] Коммит создан
- [ ] Request создан в `.agent/requests/active/`
- [ ] Задача в manifest.json отмечена completed
+69 -134
View File
@@ -1,188 +1,123 @@
# Протокол 05: Передача исполнительному агенту (HANDOFF) # Протокол 07: Завершение сессии (HANDOFF)
## Цель ## Цель
Подготовить и передать исполнительному агенту полный контекст для работы: задачи, окружение, правила. Легковесное завершение сессии: валидация структуры `.agent/`, финализация чекпоинтов, формирование сводки. Архивация и обновление project-state выполняются фазой METASTATE.
> **Важно:** если перед HANDOFF была выполнена фаза METASTATE (06) — архивация, project-state и handoff-summary уже готовы.
> HANDOFF в этом случае только валидирует и финализирует.
## Вход ## Вход
- `.agent/analysis-report.md`
- `.agent/design-report.md` (опционально, для greenfield)
- `.agent/layer-1/adr/*.md` (опционально)
- `.agent/layer-1/risk-register.md` (опционально)
- `.agent/layer-1/red-team-report.md` (опционально)
- `.agent/task-manifest.json`
- `.agent/task-manifest.md`
- `.agent/baseline-test-report.log`
- `.agent/checkpoints.json` (все предыдущие фазы: completed) - `.agent/checkpoints.json` (все предыдущие фазы: completed)
- `.agent/context/project-state.md` (опционально, создан в ANALYSIS, обновлён в METASTATE)
- `.agent/handoff-summary.md` (опционально, создан в METASTATE)
- `.agent/tasks/manifest.json`
- `.agent/roadmap/sources.md` (опционально)
- Все артефакты `.agent/`
## Шаги ## Шаги
### 5.1. Архивация завершённых артефактов ### 5.1. Проверка: была ли METASTATE?
Перед валидацией и передачей выполнить архивирование. Если существует `.agent/handoff-summary.md` и `.agent/context/project-state.md`:
- METASTATE уже выполнен
- Перейти к шагу 5.3 (Валидация)
**Архивировать завершённые задачи:** Если нет:
- METASTATE не выполнялся (например, сессия завершается до execution)
- Перейти к шагу 5.2 (Лёгкая архивация)
Для каждой задачи в `task-manifest.json` со статусом `completed`: ### 5.2. Лёгкая архивация (если METASTATE не было)
1. Создать `.agent/archive/tasks/<id>.json` — перенести полное описание задачи (все поля)
2. В `task-manifest.json` заменить задачу на one-liner:
```json
{ "id": "<id>", "title": "<title>", "status": "archived" }
```
**Архивировать чекпоинты:** Если есть completed задачи в manifest.json:
- Архивировать их в `.agent/archive/tasks/{id}.json`
- Заменить в manifest.json на one-liner
- Создать `.agent/archive/index.json`
Если `checkpoints.json` уже существует — сохранить предыдущую версию в `.agent/archive/checkpoints/<last_updated>.json`. Если нет completed задач — пропустить.
**Создать индекс архива:** ### 5.3. Валидация
```json Проверить:
{
"version": "1.1.0",
"archived_at": "<timestamp>",
"tasks": [
{ "id": "T1", "title": "...", "archived_at": "<timestamp>" }
],
"checkpoints": [
{ "file": "checkpoints/2026-07-15T10-00-00.json", "archived_at": "<timestamp>" }
]
}
```
### 5.2. Валидация - [ ] Все фазы отмечены как `completed` или `skipped` в checkpoints.json
- [ ] `.agent/` содержит обязательные файлы:
Перед передачей проверить:
- [ ] Все фазы отмечены как `completed` в checkpoints.json
- [ ] `.agent/` содержит все обязательные файлы:
- `checkpoints.json` - `checkpoints.json`
- `analysis-report.md` - `context/analysis-report.md`
- `task-manifest.json` + `task-manifest.md` - `context/project-state.md`
- `baseline-test-report.log` - `tasks/manifest.json` + `tasks/manifest.md`
- `setup-report.log`
- `src/META_AGENT_GUIDE.md` - `src/META_AGENT_GUIDE.md`
- `src/BOUNDARIES.md` - `src/BOUNDARIES.md`
- `src/VERSION` - `src/VERSION`
- `src/PROTOCOLS/` - `src/PROTOCOLS/`
- `src/TEMPLATES/` - `src/TEMPLATES/`
- `rules/project-rules.md` - `rules/project-rules.md`
- `archive/index.json` - [ ] В `.agent/tasks/manifest.json` нет циклических зависимостей
- [ ] Для greenfield: `design-report.md` присутствует
- [ ] В task-manifest.json нет циклических зависимостей
- [ ] Все acceptance criteria сформулированы измеримо - [ ] Все acceptance criteria сформулированы измеримо
- [ ] Для каждой задачи указаны affected files - [ ] Для каждой задачи указаны affected files и origin
- [ ] В репозитории нет незакоммиченных изменений (кроме `.agent/`) - [ ] `.agent/src/` содержит актуальные исходники
- [ ] `.agent/src/` содержит актуальные исходники MetaAgent (META_AGENT_GUIDE.md, PROTOCOLS/, TEMPLATES/, BOUNDARIES.md, VERSION)
- [ ] `AGENTS.md` присутствует в корне репозитория - [ ] `AGENTS.md` присутствует в корне репозитория
- [ ] `.agent/rules/` содержит `project-rules.md`
**Дополнительные проверки (если config включает):** ### 5.4. Создание session-summary.md
- [ ] ADR присутствуют (если adr=yes)
- [ ] Risk Register заполнен (если risk_register=yes)
- [ ] Red Team Report есть (если red_team=yes)
- [ ] Invariant-задачи в манифесте (если invariant_tests=yes)
### 5.3. Layer-структура .agent/ Создать `.agent/session-summary.md` — краткая сводка сессии:
Если `config.layer_structure = yes`, организовать артефакты по слоям: ```markdown
# Session Summary
``` **Session:** <id>
.agent/ **MetaAgent version:** 2.1.0
layer-0/ **Date:** <timestamp>
checkpoints.json # всегда (ядро) **Goal:** <goal>
session-summary.md # краткая сводка сессии (создаётся здесь)
layer-1/ ## Phases Executed
adr/ # ADR (опционально) - [x] INIT
risk-register.md # (опционально) - [x] ANALYSIS
red-team-report.md # (опционально) - [x] ROADMAP
layer-2/ - [x] DESIGN
analysis-report.md - [x] DECOMPOSITION
design-report.md - [x] EXECUTION (N tasks)
design-report.md - [x] METASTATE
layer-3/ - [x] HANDOFF
handoff-summary.md
task-manifest.json ## Results
task-manifest.md - Tasks completed: N
baseline-test-report.log - Requests approved: N
setup-report.log - Files changed: [list]
## Next
Следующий агент: читай .agent/handoff-summary.md
``` ```
Если `layer_structure = no` — артефакты остаются плоскими в `.agent/`, как раньше. ### 5.5. Финализация checkpoints
### 5.4. Создать handoff-summary.md
Заполнить по шаблону `TEMPLATES/handoff-summary.md`:
- **Session Info** — ID, цель, дата
- **Configuration** — какие функции были включены, глубина
- **Repo Summary** — краткая выжимка из analysis-report
- **Environment Status** — результат сборки и тестов
- **Design Summary** (если есть design-report) — ключевые архитектурные решения
- **ADR Summary** (если adr=yes) — какие решения задокументированы
- **Risk Register** (если risk_register=yes) — основные допущения
- **Task Overview** — количество задач, типы, список
- **Next Steps** — с какой задачи начинать исполнительному агенту
- **Project Rules** — ссылка на `.agent/rules/project-rules.md` (передаётся exec-агенту)
- **Archive** — ссылка на `.agent/archive/index.json` (история завершённых задач)
- **Caveats** — известные проблемы, ограничения, неясные моменты
- **Checkpoints** — актуальное состояние чекпоинтов
### 5.5. Финализировать checkpoints
- Отметить `phases.handoff = "completed"` - Отметить `phases.handoff = "completed"`
- Записать финальный `last_updated` - Записать финальный `last_updated`
### 5.6. Сигнал ### 5.6. Сигнал
Сообщить пользователю/оркестратору:
``` ```
HANDOFF COMPLETE HANDOFF COMPLETE
Session: <session_id> Session: <session_id>
Target: <target_repo> Target: <target_repo>
Type: <existing | greenfield | scaffold> Type: <existing | greenfield | scaffold>
Config: depth=<N>, adr=<yes|no>, red_team=<yes|no>, ... Tasks: <N> total, <M> completed, <K> pending
Tasks: <count> tasks ready
Исполнительный агент может начинать с задачи <T1>. Следующий агент начинает с .agent/handoff-summary.md
Контекст: .agent/handoff-summary.md
Манифест: .agent/task-manifest.json
``` ```
## Что получает исполнительный агент
1. **Целевой репозиторий** — полностью настроенный, с установленными зависимостями
2. **`.agent/`** — директория со всеми артефактами (layer-структура или плоская)
3. **`task-manifest.json`** — машиночитаемый список задач
4. **`task-manifest.md`** — человекочитаемый список задач
5. **`handoff-summary.md`** — итоговая сводка
6. **`checkpoints.json`** — актуальное состояние (исполнительный агент будет его обновлять)
7. **`layer-1/adr/*.md`** (опционально) — ключевые решения
8. **`layer-1/risk-register.md`** (опционально) — допущения
9. **`layer-2/analysis-report.md`** — полный анализ репозитория (справочно)
10. **`layer-2/design-report.md`** (только для greenfield) — архитектурный план
11. **`layer-3/baseline-test-report.log`** — baseline тестов (чтобы не сломать существующее)
12. **`.agent/src/`** — полные исходники MetaAgent (справочно, всегда присутствуют)
13. **`.agent/rules/`** — пользовательские правила проекта
14. **`AGENTS.md`** — инструкция для AI-агента в корне проекта (всегда присутствует)
15. **`.agent/archive/`** — архив завершённых задач, чекпоинтов и устаревших артефактов
## Выход ## Выход
- `.agent/layer-0/session-summary.md` (если layer_structure=yes) - `.agent/session-summary.md`
- `.agent/layer-3/handoff-summary.md` - `.agent/checkpoints.json` (финальный)
- `.agent/layer-0/checkpoints.json` (финальный) - Если METASTATE не было: `.agent/archive/index.json`
- `.agent/archive/index.json` (создаётся при архивации)
## Критерии завершения ## Критерии завершения
- [ ] Все артефакты на месте (с учётом layer-структуры) - [ ] Все артефакты на месте (согласно структуре .agent/)
- [ ] `.agent/src/` содержит актуальные исходники MetaAgent - [ ] Если METASTATE не было — completed задачи архивированы
- [ ] `.agent/rules/` содержит `project-rules.md` - [ ] session-summary.md создан
- [ ] `AGENTS.md` присутствует в корне репозитория
- [ ] `.agent/archive/index.json` создан, завершённые задачи архивированы
- [ ] handoff-summary.md заполнен (включая config, design summary, ADR summary, archive)
- [ ] checkpoints.json финализирован - [ ] checkpoints.json финализирован
- [ ] Сигнал отправлен пользователю/оркестратору - [ ] Сигнал отправлен пользователю
+149
View File
@@ -0,0 +1,149 @@
# Протокол 06: Обновление метасостояния (METASTATE)
## Цель
По команде пользователя «обнови метасостояние проекта» — провести ревью накопленных requests, синхронизировать манифест, обновить слепок проекта и подготовить `.agent/` как полную картину для следующей сессии.
## Вход
- `.agent/requests/active/` — все request-ы со статусом `ready_for_review`
- `.agent/tasks/manifest.json` — текущее состояние задач
- `.agent/context/project-state.md` — текущий слепок проекта (создан в ANALYSIS)
- `.agent/roadmap/sources.md` — дорожная карта (создана в ROADMAP)
- `.agent/checkpoints.json`
## Шаги
### 6.1. Сбор requests
Прочитать все файлы из `.agent/requests/active/` со статусом `ready_for_review`.
Каждый request — это выполненная задача, ожидающая подтверждения.
### 6.2. Ревью каждого request
Для каждого request:
1. **Верифицировать** — проверить, что verification корректен:
- Тесты действительно проходят (перезапустить, если нужно)
- LSP diagnostics чист
- Acceptance criteria выполнены
- При необходимости — проверить коммиты (git show)
2. **Принять или отклонить:**
-**approved** — всё ОК:
- Переместить request: `.agent/requests/active/``.agent/requests/archive/`
- Убедиться, что задача в manifest.json имеет `status: "completed"`
-**rejected** — есть проблемы:
- Оставить request в active/ с комментарием о причинах отказа
- В manifest.json: `status: "reopened"`, снять `claimed_by`
- Добавить `rejection_reason` в request
### 6.3. Архивация завершённых задач
Для каждой задачи в manifest.json со статусом `completed`:
1. Создать `.agent/archive/tasks/{id}.json` — полное описание задачи (все поля)
2. В manifest.json заменить на one-liner:
```json
{ "id": "T1", "title": "GET /health endpoint", "status": "archived", "origin": "user:direct" }
```
### 6.4. Обновление project-state.md
Переписать `.agent/context/project-state.md` с учётом выполненных задач:
- Обновить список модулей (какие добавлены/изменены)
- Обновить архитектурную схему (кратко)
- Обновить статус тестов
- Добавить новые ADR, если появились
- Убрать закрытые concerns
Цель: следующий агент читает project-state.md и понимает проект, не открывая исходники.
### 6.5. Обновление roadmap
В `.agent/roadmap/sources.md`:
- Отметить выполненные пункты
- Пересчитать приоритеты
- Если появились новые источники — добавить
### 6.6. Индекс архива
Создать/обновить `.agent/archive/index.json`:
```json
{
"version": "2.1.0",
"archived_at": "<timestamp>",
"tasks": [
{ "id": "T1", "title": "GET /health", "archived_at": "<timestamp>" }
],
"requests": [
{ "id": "req-T1", "task_id": "T1", "archived_at": "<timestamp>" }
],
"checkpoints": [
{ "file": "checkpoints/<timestamp>.json", "archived_at": "<timestamp>" }
]
}
```
### 6.7. Создание handoff-summary.md
Создать `.agent/handoff-summary.md` — полную сводку для следующего агента:
```markdown
## Session Summary
**Session:** <id>
**Goal:** <goal>
**Completed:** N tasks
**Pending:** M tasks
**Approved requests:** req-T1, req-T2
## Project State
(краткая выжимка из project-state.md)
## Next Steps
(с чего начать следующую сессию)
## Key Artifacts
- Project state: `.agent/context/project-state.md`
- Tasks: `.agent/tasks/manifest.json`
- Roadmap: `.agent/roadmap/sources.md`
- Pending reviews: `.agent/requests/active/`
- Archive: `.agent/archive/index.json`
```
### 6.8. Финализация чекпоинта
Обновить checkpoints.json:
- `phases.metastate = "completed"`
- Актуальный список задач
- `last_updated`
## Выход
- Подтверждённые requests: `.agent/requests/archive/`
- Архив задач: `.agent/archive/tasks/{id}.json`
- Обновлённый project-state.md
- Обновлённый roadmap/sources.md
- `.agent/handoff-summary.md` — полная сводка
- `.agent/archive/index.json`
- Финальный checkpoints.json
## Критерии завершения
- [ ] Все ready_for_review requests проверены (approved/rejected)
- [ ] Approved requests перемещены в archive
- [ ] Completed задачи архивированы (one-liner в manifest)
- [ ] project-state.md отражает актуальное состояние проекта
- [ ] roadmap/sources.md обновлён
- [ ] archive/index.json создан
- [ ] handoff-summary.md готов
- [ ] checkpoints.json финализирован
## Когда запускать
По команде пользователя:
- «обнови метасостояние»
- «update metastate»
- «подведи итог»
- «заверши сессию»
Может запускаться многократно в течение жизни проекта — после каждой группы выполненных задач.
+2 -2
View File
@@ -17,7 +17,7 @@
## Project Type ## Project Type
- **Type:** {{ project_type }} - **Type:** {{ project_type }}
- **Design report:** {% if project_type == "greenfield" or project_type == "scaffold" %}`.agent/design-report.md`{% else %}—{% endif %} - **Design report:** {% if project_type == "greenfield" or project_type == "scaffold" %}`.agent/context/design-report.md`{% else %}—{% endif %}
## ADR Summary (если применимо) ## ADR Summary (если применимо)
@@ -39,7 +39,7 @@
- **Build:** {{ build_status }} - **Build:** {{ build_status }}
- **Tests:** {{ tests_passed }}/{{ tests_total }} passed - **Tests:** {{ tests_passed }}/{{ tests_total }} passed
- **Baseline log:** `.agent/baseline-test-report.log` - **Baseline log:** `.agent/context/baseline-test-report.log`
- **Dependencies:** {{ deps_status }} - **Dependencies:** {{ deps_status }}
## Task Overview ## Task Overview
+1 -1
View File
@@ -14,7 +14,7 @@
| RISK_REGISTER | ✗ | — | | RISK_REGISTER | ✗ | — |
| DECOMPOSITION | ✓ | invariant_tests=yes | | DECOMPOSITION | ✓ | invariant_tests=yes |
| SETUP | ✓ | — | | SETUP | ✓ | — |
| HANDOFF | ✓ | layer_structure=yes | | HANDOFF | ✓ | |
## Глубина проработки ## Глубина проработки
+43
View File
@@ -0,0 +1,43 @@
# Project State
# Auto-generated — updated by ANALYSE (initial) and METASTATE (on updates)
**Last updated:** {{ timestamp }}
**Session:** {{ session_id }}
## Project Type
{{ project_type }}
## Tech Stack
| Category | Technology |
|----------|-----------|
| Language | {{ language }} |
| Framework | {{ framework }} |
| Database | {{ database }} |
| Test runner | {{ test_runner }} |
| Package manager | {{ package_manager }} |
## Current Architecture
{{ architecture_description }}
## Key Modules
| Module | Status | Description |
|--------|--------|-------------|
| {{ module_name }} | {{ existing / stub / new }} | {{ module_description }} |
## Decisions in Effect
| ADR | Decision | Status |
|-----|----------|--------|
| {{ adr_id }} | {{ decision_summary }} | {{ active / superseded }} |
## Testing Status
{{ testing_summary }}
## Open Concerns
- {{ concern_1 }}
+29
View File
@@ -0,0 +1,29 @@
{
"$schema": ".agent/src/TEMPLATES/schemas/request-schema.json",
"template_version": "1.0",
"request_id": "req-{{ task_id }}",
"task_id": "{{ task_id }}",
"title": "{{ task_title }}",
"status": "ready_for_review",
"created_at": "{{ timestamp }}",
"goal": "{{ task_goal }}",
"changes": {
"summary": "{{ changes_summary }}",
"commits": [
"{{ commit_hash }}"
],
"files_changed": [
"path/to/file.py"
]
},
"verification": {
"tests_passed": "{{ test_results }}",
"lsp_clean": true
},
"fulfills_ac": [
"{{ acceptance_criterion }}"
]
}
+42
View File
@@ -0,0 +1,42 @@
# Roadmap Sources
# Auto-generated — created by ROADMAP phase
**Created:** {{ timestamp }}
**Session:** {{ session_id }}
## Priority Legend
- **P0** — Critical, do next
- **P1** — Important, do soon
- **P2** — Nice to have
- **P3** — Future / deferred
## Sources
### FUTURE Plans
| Plan | Priority | Status | Origin File |
|------|----------|--------|-------------|
| {{ plan_title }} | {{ P0-P3 }} | {{ active / archived }} | FUTURE/{{ filename }}.md |
### ADR-Derived Tasks
| Source ADR | Task | Priority |
|------------|------|----------|
| {{ adr_id }} | {{ task_description }} | {{ P0-P3 }} |
### User Requests
| Request | Priority | Source |
|---------|----------|--------|
| {{ request }} | {{ P0-P3 }} | {{ direct / issue / feedback }} |
### Agent-Identified Improvements
| Observation | Suggested Task | Priority |
|-------------|----------------|----------|
| {{ observation }} | {{ task }} | {{ P0-P3 }} |
## Consolidated Priority Queue
1. **{{ task_title }}** ({{ origin }}) — {{ priority }}
@@ -0,0 +1,93 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "metaagent/checkpoints/2.0.0",
"title": "MetaAgent Checkpoints",
"description": "Schema for .agent/checkpoints.json — session state",
"type": "object",
"properties": {
"metaagent_version": {
"type": "string",
"description": "MetaAgent version that created this checkpoint",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
},
"session_id": {
"type": "string",
"description": "Unique session identifier"
},
"target_repo": {
"type": "string",
"description": "Path to the target repository"
},
"goal": {
"type": "string",
"description": "Session goal"
},
"project_type": {
"type": "string",
"enum": ["pending", "existing", "greenfield", "scaffold"],
"description": "Type of the target project"
},
"config": {
"type": "object",
"properties": {
"depth": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"description": "Depth of the session (1-10)"
},
"design": {
"type": "object",
"properties": {
"adr": { "type": "boolean" },
"alternative_arch": { "type": "boolean" }
},
"additionalProperties": false
},
"red_team": { "type": "boolean" },
"risk_register": { "type": "boolean" },
"decomposition": {
"type": "object",
"properties": {
"invariant_tests": { "type": "boolean" }
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"phases": {
"type": "object",
"properties": {
"analysis": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"design": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"red_team": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"decomposition": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"environment": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"handoff": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] }
},
"additionalProperties": false
},
"tasks": {
"type": "array",
"description": "List of tasks (one-liners after archiving)",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"status": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "archived"] }
},
"required": ["id", "title", "status"],
"additionalProperties": false
}
},
"last_updated": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of last update"
}
},
"required": ["metaagent_version", "session_id", "goal", "config", "phases", "last_updated"],
"additionalProperties": false
}
@@ -0,0 +1,60 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "metaagent/decisions-index/2.0.0",
"title": "MetaAgent Decisions Index",
"description": "Schema for .agent/decisions/index.json — machine-readable index of ADRs",
"type": "object",
"properties": {
"version": {
"type": "string",
"description": "Schema version",
"enum": ["2.0"]
},
"decisions": {
"type": "array",
"description": "List of architecture decision records",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[0-9]{3,}$",
"description": "ADR number (001, 002, ...)"
},
"title": {
"type": "string",
"description": "Short title of the decision"
},
"status": {
"type": "string",
"enum": ["proposed", "accepted", "deprecated", "superseded"],
"description": "ADR status"
},
"file": {
"type": "string",
"description": "Filename in .agent/decisions/ (e.g. 001-stack.md)"
},
"date": {
"type": "string",
"format": "date",
"description": "Decision date (ISO 8601)"
}
},
"required": ["id", "title", "file"],
"additionalProperties": false
}
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of index creation"
},
"updated_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of last update"
}
},
"required": ["version", "decisions"],
"additionalProperties": false
}
@@ -0,0 +1,91 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "metaagent/task-manifest/2.0.0",
"title": "MetaAgent Task Manifest",
"description": "Schema for .agent/tasks/manifest.json — the global task manifest",
"type": "object",
"properties": {
"version": {
"type": "string",
"description": "Schema version",
"enum": ["2.0"]
},
"session_id": {
"type": "string",
"description": "Session ID that created this manifest"
},
"goal": {
"type": "string",
"description": "Overall goal of the session"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of creation"
},
"tasks": {
"type": "array",
"description": "List of tasks",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^T[0-9]+$",
"description": "Unique task identifier (T1, T2, ...)"
},
"title": {
"type": "string",
"description": "Task title"
},
"description": {
"type": "string",
"description": "Detailed description"
},
"type": {
"type": "string",
"enum": ["feature", "refactor", "test", "fix", "config", "design", "docs", "invariant"],
"description": "Task type"
},
"files": {
"type": "array",
"items": { "type": "string" },
"description": "Files affected by this task"
},
"depends_on": {
"type": "array",
"items": { "type": "string" },
"description": "Task IDs this task depends on"
},
"acceptance_criteria": {
"type": "array",
"items": { "type": "string" },
"description": "Measurable criteria for completion"
},
"context": {
"type": "string",
"description": "Additional context or references"
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed", "failed", "archived"],
"description": "Current task status"
},
"claimed_by": {
"type": "string",
"description": "Worker ID if claimed"
},
"claimed_at": {
"type": "string",
"format": "date-time",
"description": "When the task was claimed"
}
},
"required": ["id", "title", "type", "status"],
"additionalProperties": false
}
}
},
"required": ["version", "tasks"],
"additionalProperties": false
}
+3 -4
View File
@@ -14,7 +14,6 @@
| Red Team | {{ red_team_enabled }} | | Red Team | {{ red_team_enabled }} |
| Risk Register | {{ risk_register_enabled }} | | Risk Register | {{ risk_register_enabled }} |
| Invariant Tests | {{ invariant_tests_enabled }} | | Invariant Tests | {{ invariant_tests_enabled }} |
| Layer Structure | {{ layer_structure_enabled }} |
## Phase Status ## Phase Status
@@ -29,7 +28,7 @@
## Quick Links ## Quick Links
- Task Manifest: `.agent/task-manifest.json` - Task Manifest: `.agent/tasks/manifest.json`
- Handoff Summary: `.agent/handoff-summary.md` - Handoff Summary: `.agent/handoff-summary.md`
- Design Report: `.agent/analysis-report.md` - Design Report: `.agent/context/design-report.md`
- ADR: `.agent/layer-1/adr/` (если есть) - ADR: `.agent/decisions/` (если есть)
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"$schema": "metaagent-task-manifest", "$schema": ".agent/src/TEMPLATES/schemas/task-manifest-schema.json",
"version": "1.0", "version": "1.0",
"session_id": "{{ session_id }}", "session_id": "{{ session_id }}",
"goal": "{{ goal }}", "goal": "{{ goal }}",
+1 -1
View File
@@ -1 +1 @@
1.1.0 2.1.0
+202 -14
View File
@@ -28,8 +28,7 @@
"design": { "adr": false, "alternative_arch": false }, "design": { "adr": false, "alternative_arch": false },
"red_team": false, "red_team": false,
"risk_register": false, "risk_register": false,
"decomposition": { "invariant_tests": false }, "decomposition": { "invariant_tests": false }
"handoff": { "layer_structure": false }
}, },
"phases": { "phases": {
"analysis": "pending", "analysis": "pending",
@@ -50,7 +49,7 @@
Мета-агент выполняет `PROTOCOLS/01_ANALYSIS.md`. Определяет тип проекта: `existing`. Мета-агент выполняет `PROTOCOLS/01_ANALYSIS.md`. Определяет тип проекта: `existing`.
Результат `.agent/analysis-report.md`: Результат `.agent/context/analysis-report.md`:
```markdown ```markdown
## 2. Стек технологий ## 2. Стек технологий
@@ -91,7 +90,7 @@
| T2 | Подключить router в main.py | config | T1 | Ручка доступна по /health | | T2 | Подключить router в main.py | config | T1 | Ручка доступна по /health |
| T3 | Написать тесты для /health | test | T2 | Тесты проверяют 200 и структуру ответа | | T3 | Написать тесты для /health | test | T2 | Тесты проверяют 200 и структуру ответа |
Создан `.agent/task-manifest.json` и `.agent/task-manifest.md`. Создан `.agent/tasks/manifest.json` и `.agent/tasks/manifest.md`.
Чекпоинт обновлён: `decomposition = "completed"`. Tasks: T1-T3 со статусом `pending`. Чекпоинт обновлён: `decomposition = "completed"`. Tasks: T1-T3 со статусом `pending`.
@@ -103,7 +102,7 @@
- `pip install -r requirements.txt` — OK - `pip install -r requirements.txt` — OK
- Запуск pytest — OK, 12 passed (базовый тест) - Запуск pytest — OK, 12 passed (базовый тест)
- Результат в `.agent/baseline-test-report.log` - Результат в `.agent/context/baseline-test-report.log`
Чекпоинт обновлён: `environment = "completed"`. Чекпоинт обновлён: `environment = "completed"`.
@@ -138,8 +137,7 @@
"design": { "adr": false, "alternative_arch": false }, "design": { "adr": false, "alternative_arch": false },
"red_team": false, "red_team": false,
"risk_register": false, "risk_register": false,
"decomposition": { "invariant_tests": false }, "decomposition": { "invariant_tests": false }
"handoff": { "layer_structure": false }
}, },
"phases": { "phases": {
"analysis": "completed", "analysis": "completed",
@@ -199,8 +197,7 @@ Tasks: 3 tasks ready
"design": { "adr": true, "alternative_arch": true }, "design": { "adr": true, "alternative_arch": true },
"red_team": false, "red_team": false,
"risk_register": true, "risk_register": true,
"decomposition": { "invariant_tests": true }, "decomposition": { "invariant_tests": true }
"handoff": { "layer_structure": true }
}, },
"phases": { "phases": {
"analysis": "pending", "analysis": "pending",
@@ -243,7 +240,7 @@ Tasks: 3 tasks ready
Мета-агент выполняет `PROTOCOLS/02_DESIGN.md`. Мета-агент выполняет `PROTOCOLS/02_DESIGN.md`.
Результат `.agent/design-report.md`: Результат `.agent/context/design-report.md`:
```markdown ```markdown
## 1. Технологический стек ## 1. Технологический стек
@@ -313,7 +310,7 @@ T6: tests — тесты
- Установка fastapi, uvicorn, sqlalchemy, matplotlib, pytest - Установка fastapi, uvicorn, sqlalchemy, matplotlib, pytest
- Создание scaffold-структуры: `app/models/`, `app/services/`, `app/schemas/`, `tests/` - Создание scaffold-структуры: `app/models/`, `app/services/`, `app/schemas/`, `tests/`
- Пустые заглушки модулей - Пустые заглушки модулей
- `.agent/baseline-test-report.log`: "0 tests — greenfield, scaffold готов" - `.agent/context/baseline-test-report.log`: "0 tests — greenfield, scaffold готов"
--- ---
@@ -328,15 +325,15 @@ Config: depth=7, adr=yes, risk_register=yes, invariant_tests=yes
Tasks: 6 tasks ready Tasks: 6 tasks ready
Исполнительный агент может начинать с задачи T1 (init). Исполнительный агент может начинать с задачи T1 (init).
Архитектурный план: .agent/design-report.md Архитектурный план: .agent/context/design-report.md
ADR: .agent/layer-1/adr/ ADR: .agent/decisions/
``` ```
--- ---
## После HANDOFF: работа исполнительного агента ## После HANDOFF: работа исполнительного агента
Исполнительный агент читает `.agent/handoff-summary.md`, `.agent/task-manifest.json`, выполняет задачи по порядку, обновляя checkpoints.json после каждой. Исполнительный агент читает `.agent/handoff-summary.md`, `.agent/tasks/manifest.json`, выполняет задачи по порядку, обновляя checkpoints.json после каждой.
После завершения всех задач: После завершения всех задач:
@@ -354,3 +351,194 @@ T6: Тесты ✓
Все тесты проходят: 24/24 passed. Все тесты проходят: 24/24 passed.
``` ```
---
## Сценарий C: v2.1 — Координирующий агент + requests + METASTATE
**Цель:** Рефакторинг модуля авторизации: вынести логику из монолитного файла в отдельные модули.
**Целевой репозиторий:** `github.com/example/fastapi-app`
**Пользователь:** "Вынеси авторизацию в отдельные модули: auth/router.py, auth/schemas.py, auth/deps.py"
**Версия MetaAgent: 2.1.0**
---
### PROJECT LOOP
#### Фаза INIT
Агент создаёт `.agent/`, инициализирует чекпоинт:
```json
{
"metaagent_version": "2.1.0",
"session_id": "ses_v21_001",
"goal": "Рефакторинг авторизации: вынести в модули auth/",
"project_type": "existing",
"phases": {
"analysis": "pending",
"roadmap": "pending",
"design": "skipped",
"decomposition": "pending",
"execution": "pending",
"metastate": "pending",
"handoff": "pending"
}
}
```
#### Фаза ANALYSIS
Агент сканирует проект:
- Стек: Python, FastAPI, SQLAlchemy
- auth/login.py — 450 строк, монолит (цель рефакторинга)
- Создаёт `.agent/context/analysis-report.md`
- Создаёт `.agent/context/project-state.md` — начальный слепок
#### Фаза ROADMAP
Сканирование FUTURE/ — пусто. ADR — нет. Единственный источник — пользовательский запрос.
Создаётся `.agent/roadmap/sources.md`:
```markdown
## Sources
### User Requests
| Вынести авторизацию | P0 | user:direct |
### Consolidated Priority Queue
1. Вынести auth/ (user:direct) — P0
```
#### Фаза DECOMPOSITION
Задачи с origin:
```json
{
"tasks": [
{
"id": "T1",
"title": "Создать auth/router.py",
"origin": "user:direct",
"status": "pending",
"files": ["app/auth/router.py"]
},
{
"id": "T2",
"title": "Создать auth/schemas.py",
"origin": "user:direct",
"status": "pending",
"files": ["app/auth/schemas.py"],
"depends_on": ["T1"]
},
{
"id": "T3",
"title": "Создать auth/deps.py",
"origin": "user:direct",
"status": "pending",
"files": ["app/auth/deps.py"],
"depends_on": ["T1"]
}
]
}
```
---
### WORK LOOP (первая итерация)
#### Фаза EXECUTION — задача T1
1. Берёт T1 (`pending`, нет зависимостей)
2. Отмечает `in_progress`
3. Создаёт `app/auth/router.py` — переносит логику роутов из `auth/login.py`
4. Пишет тесты
5. Тесты проходят: 48/48
6. Коммит: `abc1234 — feat: extract auth router`
7. Создаёт request:
```json
{
"request_id": "req-T1",
"task_id": "T1",
"title": "Создать auth/router.py",
"status": "ready_for_review",
"changes": {
"summary": "Вынесены роуты авторизации из auth/login.py в auth/router.py. Добавлены тесты.",
"commits": ["abc1234"],
"files_changed": ["app/auth/router.py", "app/auth/__init__.py", "tests/test_auth_router.py"]
},
"verification": { "tests_passed": "48/48", "lsp_clean": true },
"fulfills_ac": ["Роуты авторизации доступны через app/auth/router.py", "Старые тесты проходят"]
}
```
8. T1 → completed
#### Фаза EXECUTION — задача T2 (аналогично)
Создаёт `auth/schemas.py`, request `req-T2`.
#### Фаза EXECUTION — задача T3 (аналогично)
Создаёт `auth/deps.py`, request `req-T3`.
Задачи закончились. Агент ждёт команду.
---
### METASTATE (по команде пользователя)
**Пользователь:** "обнови метасостояние"
1. **Ревью requests:** три request-а, все approved
- req-T1 → `.agent/requests/archive/req-T1.json`
- req-T2 → `.agent/requests/archive/req-T2.json`
- req-T3 → `.agent/requests/archive/req-T3.json`
2. **Архивация задач:**
- T1 в manifest → one-liner, детали в `.agent/archive/tasks/T1.json`
- T2, T3 — аналогично
3. **Обновление project-state.md:**
```markdown
## Key Modules
| Module | Status | Description |
|--------|--------|-------------|
| app/auth/router.py | new | Вынесенные роуты авторизации |
| app/auth/schemas.py | new | Pydantic схемы |
| app/auth/deps.py | new | Dependency injection |
```
4. **Обновление roadmap:** задачи выполнены → moved to done
5. **Создание handoff-summary.md:**
```markdown
## Session Summary
**Goal:** Рефакторинг авторизации
**Completed:** 3/3 tasks
**Approved requests:** req-T1, req-T2, req-T3
## Project State
Модуль auth разбит на router+schema+deps.
Исходный auth/login.py: 450 → 120 строк.
## Next Steps
- Проверить, не осталось ли прямых импортов из старого login.py
- Обновить main.py если нужно
```
---
### HANDOFF
```text
HANDOFF COMPLETE
Session: ses_v21_001
Type: existing
Tasks: 3/3 completed
Следующий агент начинает с .agent/handoff-summary.md
```
+203 -45
View File
@@ -1,29 +1,45 @@
#!/usr/bin/env pwsh #!/usr/bin/env pwsh
# MetaAgent — установка исходников в целевой проект # MetaAgent — установка исходников в целевой проект
# Usage: .\install.ps1 [[-Path] target_path] [-Update] # Usage: .\install.ps1 [[-Path] target_path] [-Check] [-Update]
param( param(
[string]$Path = "", [string]$Path = "",
[switch]$Check,
[switch]$Update, [switch]$Update,
[switch]$Help [switch]$Help
) )
$MetaAgentSrc = Split-Path -Parent $MyInvocation.MyCommand.Path $MetaAgentSrc = Split-Path -Parent $MyInvocation.MyCommand.Path
# --- helpers ---
function Write-Info { Write-Host "" -NoNewline -ForegroundColor Blue; Write-Host " $args" }
function Write-Ok { Write-Host "" -NoNewline -ForegroundColor Green; Write-Host " $args" }
function Write-Skip { Write-Host " " -NoNewline -ForegroundColor Yellow; Write-Host " $args" }
function Write-Warn { Write-Host "" -NoNewline -ForegroundColor Yellow; Write-Host " $args" }
function Write-Fail { Write-Host "" -NoNewline -ForegroundColor Red; Write-Host " $args" }
function Write-Header { param([string]$Label)
Write-Host ""
Write-Host ("" * 40)
Write-Host " $Label"
Write-Host ("" * 40)
}
function Show-Usage { function Show-Usage {
@" @"
Usage: install.ps1 [[-Path] target_path] [-Update] [-Help] Usage: install.ps1 [[-Path] target_path] [-Check] [-Update] [-Help]
Install MetaAgent sources into <target>/.agent/src/ Install MetaAgent sources into <target>/.agent/src/
Options: Options:
-Path Path to target project (default: interactive prompt) -Path Path to target project (default: interactive prompt)
-Check Dry-run: only check target readiness, no install
-Update Overwrite existing files in .agent/src/ -Update Overwrite existing files in .agent/src/
-Help Show this help -Help Show this help
Examples: Examples:
.\install.ps1 .\install.ps1
.\install.ps1 -Path C:\Projects\MyApp .\install.ps1 -Path C:\Projects\MyApp
.\install.ps1 -Path C:\Projects\MyApp -Check
.\install.ps1 -Path C:\Projects\MyApp -Update .\install.ps1 -Path C:\Projects\MyApp -Update
"@ "@
exit 0 exit 0
@@ -31,69 +47,163 @@ Examples:
if ($Help) { Show-Usage } if ($Help) { Show-Usage }
# --- resolve target ---
$TargetPath = $Path $TargetPath = $Path
if (-not $TargetPath) { if (-not $TargetPath) {
$TargetPath = Read-Host "Enter path to target project" $TargetPath = Read-Host "Enter path to target project"
} }
$TargetPath = $TargetPath.Trim() $TargetPath = $TargetPath.Trim()
# --- pre-flight -----------------------------------------------------------
Write-Header "Pre-flight"
# 1. target exists?
if (-not (Test-Path $TargetPath -PathType Container)) { if (-not (Test-Path $TargetPath -PathType Container)) {
Write-Error "Directory '$TargetPath' does not exist." Write-Fail "Target directory '$TargetPath' does not exist."
exit 1 exit 1
} }
$TargetPath = (Resolve-Path $TargetPath).Path $TargetPath = (Resolve-Path $TargetPath).Path
Write-Ok "Target: $TargetPath"
# 2. write permission? (try to create a temp file as probe)
$probe = [System.IO.Path]::Combine($TargetPath, ".metaagent_probe.tmp")
try {
[System.IO.File]::WriteAllBytes($probe, [byte[]]@())
Remove-Item $probe -Force
Write-Ok "Write permission: yes"
} catch {
Write-Fail "No write permission on '$TargetPath'."
exit 1
}
# 3. already installed? compare versions
$AgentDir = Join-Path $TargetPath ".agent" $AgentDir = Join-Path $TargetPath ".agent"
$SrcDir = Join-Path $AgentDir "src" $SrcDir = Join-Path $AgentDir "src"
$RulesDir = Join-Path $AgentDir "rules"
$ArchiveDir = Join-Path $AgentDir "archive"
$VersionFile = Join-Path $MetaAgentSrc "VERSION" $VersionFile = Join-Path $MetaAgentSrc "VERSION"
$Version = if (Test-Path $VersionFile) { Get-Content $VersionFile -Raw | ForEach-Object { $_.Trim() } } else { "?" }
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null $Version = if (Test-Path $VersionFile -PathType Leaf) {
New-Item -ItemType Directory -Path $RulesDir -Force | Out-Null (Get-Content $VersionFile -Raw -Encoding UTF8).Trim()
New-Item -ItemType Directory -Path $ArchiveDir -Force | Out-Null } else { "?" }
Write-Host "Installing MetaAgent v$Version$SrcDir"
$oldVerPath = Join-Path $SrcDir "VERSION"
if (Test-Path $oldVerPath -PathType Leaf) {
$oldVer = (Get-Content $oldVerPath -Raw -Encoding UTF8).Trim()
if ($oldVer -ne $Version) {
Write-Info "Existing MetaAgent v$oldVer found → upgrading to v$Version"
} else {
Write-Skip "MetaAgent v${Version} already installed (use -Update to reinstall)"
if (-not $Check) {
Write-Warn "No changes applied. Run with -Update to overwrite existing files."
}
}
} else {
Write-Info "Fresh install: MetaAgent v$Version"
}
# 4. summary
$AgentsMd = Join-Path $TargetPath "AGENTS.md"
$RulesDir = Join-Path $AgentDir "rules"
$DecisionsDir = Join-Path $AgentDir "decisions"
$TasksDir = Join-Path $AgentDir "tasks"
$ContextDir = Join-Path $AgentDir "context"
$ArchiveDir = Join-Path $AgentDir "archive"
$RequestsDir = Join-Path $AgentDir "requests"
$RoadmapDir = Join-Path $AgentDir "roadmap"
$TempDir = Join-Path $TargetPath ".temp"
$DirList = @(
$SrcDir, $RulesDir, $DecisionsDir, $TasksDir,
(Join-Path $TasksDir "backlog"), $ContextDir,
$ArchiveDir,
(Join-Path $ArchiveDir "tasks"),
(Join-Path $ArchiveDir "decisions"),
(Join-Path $ArchiveDir "checkpoints"),
(Join-Path $RequestsDir "active"),
(Join-Path $RequestsDir "archive"),
$RoadmapDir,
(Join-Path $RoadmapDir "archive"),
$TempDir
)
if ($Check) {
Write-Host ""
Write-Info "--check mode: all checks passed, no changes applied."
exit 0
}
# --- phase 1: directories ------------------------------------------------
Write-Header "Directories"
foreach ($d in $DirList) {
$null = New-Item -ItemType Directory -Path $d -Force
$short = $d.Replace("$TargetPath\", "")
if (Test-Path $d -PathType Container) {
Write-Ok $short
} else {
Write-Fail "$short (creation failed)"
}
}
# --- phase 2: files ------------------------------------------------------
Write-Header "Files"
$copyCount = 0
$skipCount = 0
$failCount = 0
# --- copy files ---
function Copy-File { function Copy-File {
param([string]$Src, [string]$DstDir) param([string]$Src, [string]$DstDir)
$name = Split-Path $Src -Leaf $name = Split-Path $Src -Leaf
$dst = Join-Path $DstDir $name
if (-not (Test-Path $Src -PathType Leaf)) { if (-not (Test-Path $Src -PathType Leaf)) {
Write-Host " [skip] $name (not found)" Write-Skip "$name (source not found)"
$script:skipCount++
return return
} }
$dst = Join-Path $DstDir $name
if ($Update -or -not (Test-Path $dst)) { if ($Update -or -not (Test-Path $dst)) {
Copy-Item $Src $dst -Force try {
Write-Host " [copy] $name" Copy-Item $Src $dst -Force -ErrorAction Stop
Write-Ok $name
$script:copyCount++
} catch {
Write-Fail $name
$script:failCount++
}
} else { } else {
Write-Host " [skip] $name (exists, use -Update to overwrite)" Write-Skip "$name (exists, use -Update to overwrite)"
$script:skipCount++
} }
} }
function Copy-Dir { function Copy-Dir {
param([string]$Src, [string]$DstDir) param([string]$Src, [string]$DstDir)
$name = Split-Path $Src -Leaf $name = Split-Path $Src -Leaf
$dst = Join-Path $DstDir $name
if (-not (Test-Path $Src -PathType Container)) { if (-not (Test-Path $Src -PathType Container)) {
Write-Host " [skip] $name/ (not found)" Write-Skip "$name/ (source not found)"
$script:skipCount++
return return
} }
$dst = Join-Path $DstDir $name $null = New-Item -ItemType Directory -Path $dst -Force
New-Item -ItemType Directory -Path $dst -Force | Out-Null try {
if ($Update) { if ($Update) {
Get-ChildItem $Src | ForEach-Object { Get-ChildItem $Src | ForEach-Object {
Copy-Item $_.FullName $dst -Recurse -Force Copy-Item $_.FullName $dst -Recurse -Force -ErrorAction Stop
} }
} else { } else {
Get-ChildItem $Src | ForEach-Object { Get-ChildItem $Src | ForEach-Object {
$targetPath = Join-Path $dst $_.Name $targetPath = Join-Path $dst $_.Name
if (-not (Test-Path $targetPath)) { if (-not (Test-Path $targetPath)) {
Copy-Item $_.FullName $dst -Recurse Copy-Item $_.FullName $dst -Recurse -ErrorAction Stop
}
} }
} }
Write-Ok "$name/"
$script:copyCount++
} catch {
Write-Fail "$name/ (partial copy)"
$script:failCount++
} }
Write-Host " [copy] $name/"
} }
Copy-File (Join-Path $MetaAgentSrc "META_AGENT_GUIDE.md") $SrcDir Copy-File (Join-Path $MetaAgentSrc "META_AGENT_GUIDE.md") $SrcDir
@@ -105,12 +215,11 @@ Copy-Dir (Join-Path $MetaAgentSrc "TEMPLATES") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "install.sh") $SrcDir Copy-File (Join-Path $MetaAgentSrc "install.sh") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "install.ps1") $SrcDir Copy-File (Join-Path $MetaAgentSrc "install.ps1") $SrcDir
# --- create / update AGENTS.md in root of target --- # --- phase 3: AGENTS.md --------------------------------------------------
$AgentsMd = Join-Path $TargetPath "AGENTS.md" Write-Header "AGENTS.md"
function New-AgentsMd { if (-not (Test-Path $AgentsMd -PathType Leaf)) {
param([string]$Path) $content = @"
@"
# MetaAgent # MetaAgent
Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v$Version Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v$Version
@@ -144,20 +253,69 @@ function New-AgentsMd {
3. **Прочитай** `.agent/rules/project-rules.md` выполни пользовательские правила. 3. **Прочитай** `.agent/rules/project-rules.md` выполни пользовательские правила.
4. **Проверь** `.agent/checkpoints.json` если существует, используй как состояние сессии. 4. **Проверь** `.agent/checkpoints.json` если существует, используй как состояние сессии.
5. **Проверь** `.agent/task-manifest.json` если существует, выполняй задачи по порядку. 5. **Проверь** `.agent/task-manifest.json` если существует, выполняй задачи по порядку.
6. Если `.agent/` не инициализирован или устарел запусти `install.ps1 -Update` для 6. Если `.agent/` не инициализирован или устарел запусти `install.sh --update` для
обновления исходников MetaAgent до актуальной версии. обновления исходников MetaAgent до актуальной версии.
"@ "@
} $utf8 = [System.Text.Encoding]::UTF8
[System.IO.File]::WriteAllBytes($AgentsMd, $utf8.GetBytes($content))
if (-not (Test-Path $AgentsMd -PathType Leaf)) { Write-Ok "AGENTS.md created"
New-AgentsMd $AgentsMd | Out-File -FilePath $AgentsMd -Encoding utf8
Write-Host " [create] AGENTS.md"
} elseif ($Update) { } elseif ($Update) {
New-AgentsMd $AgentsMd | Out-File -FilePath $AgentsMd -Encoding utf8 $content = @"
Write-Host " [update] AGENTS.md" # MetaAgent
Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v$Version
набор инструкций для AI-агента.
## Контекст MetaAgent
| Ресурс | Путь |
|--------|------|
| Главная инструкция | `.agent/src/META_AGENT_GUIDE.md` |
| Протоколы фаз | `.agent/src/PROTOCOLS/` |
| Шаблоны артефактов | `.agent/src/TEMPLATES/` |
| Границы (что разрешено/запрещено) | `.agent/src/BOUNDARIES.md` |
| Правила проекта | `.agent/rules/project-rules.md` |
| Примеры работы | `.agent/src/WORKFLOW.md` |
| Версия | `.agent/src/VERSION` |
## Состояние сессии (если инициализировано)
| Артефакт | Путь |
|----------|------|
| Чекпоинты сессии | `.agent/checkpoints.json` |
| Манифест задач | `.agent/task-manifest.json` |
| Сводка для exec-агента | `.agent/handoff-summary.md` |
| Анализ репозитория | `.agent/analysis-report.md` |
## Для исполнительного агента
1. **Прочитай** `.agent/src/META_AGENT_GUIDE.md` пойми жизненный цикл MetaAgent.
2. **Прочитай** `.agent/src/BOUNDARIES.md` соблюдай границы.
3. **Прочитай** `.agent/rules/project-rules.md` выполни пользовательские правила.
4. **Проверь** `.agent/checkpoints.json` если существует, используй как состояние сессии.
5. **Проверь** `.agent/task-manifest.json` если существует, выполняй задачи по порядку.
6. Если `.agent/` не инициализирован или устарел запусти `install.sh --update` для
обновления исходников MetaAgent до актуальной версии.
"@
$utf8 = [System.Text.Encoding]::UTF8
[System.IO.File]::WriteAllBytes($AgentsMd, $utf8.GetBytes($content))
Write-Ok "AGENTS.md updated"
} else { } else {
Write-Host " [skip] AGENTS.md (exists, use -Update to overwrite)" Write-Skip "AGENTS.md (exists, use -Update to overwrite)"
$script:skipCount++
} }
# --- summary -------------------------------------------------------------
Write-Header "Summary"
Write-Host " MetaAgent v$Version$SrcDir"
Write-Host "" Write-Host ""
Write-Host "Done! MetaAgent v$Version installed at $SrcDir" if ($copyCount -gt 0) { Write-Ok "$copyCount file(s) copied" }
if ($skipCount -gt 0) { Write-Skip "$skipCount file(s) skipped" }
if ($failCount -gt 0) { Write-Fail "$failCount file(s) failed" }
Write-Host ""
if ($failCount -eq 0) {
Write-Ok "Installation completed successfully."
} else {
Write-Fail "Installation completed with $failCount error(s)."
exit 1
}
+163 -26
View File
@@ -1,89 +1,207 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# MetaAgent — установка исходников в целевой проект # MetaAgent — установка исходников в целевой проект
# Usage: ./install.sh [--update] [target_path] # Usage: ./install.sh [--check|--update] [target_path]
set -euo pipefail set -euo pipefail
METAAGENT_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" METAAGENT_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --- helpers ---
red=; grn=; ylw=; blu=; rst=
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
red=$(tput setaf 1); grn=$(tput setaf 2)
ylw=$(tput setaf 3); blu=$(tput setaf 4)
rst=$(tput sgr0)
fi
info() { echo " ${blu}${rst} $*"; }
ok() { echo " ${grn}${rst} $*"; }
skip() { echo " ${ylw}${rst} $*"; }
warn() { echo " ${ylw}${rst} $*"; }
fail() { echo " ${red}${rst} $*"; }
header(){ echo; echo "────────────────────────────────────────"; echo " $*"; echo "────────────────────────────────────────"; }
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $0 [--update] [target_path] Usage: $0 [--check|--update] [target_path]
Install MetaAgent sources into <target>/.agent/src/ Install MetaAgent sources into <target>/.agent/src/
Options: Options:
--check, -c Dry-run: only check target readiness, no install
--update, -u Overwrite existing files in .agent/src/ --update, -u Overwrite existing files in .agent/src/
--help, -h Show this help --help, -h Show this help
Examples: Examples:
$0 $0
$0 /path/to/project $0 /path/to/project
$0 --check /path/to/project
$0 --update /path/to/project $0 --update /path/to/project
EOF EOF
exit 0 exit 0
} }
# --- arg parsing ---
CHECK=false
UPDATE=false UPDATE=false
TARGET_PATH="" TARGET_PATH=""
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--update|-u) UPDATE=true; shift ;; --check|-c) CHECK=true; shift ;;
--help|-h) usage ;; --update|-u) UPDATE=true; shift ;;
--*) echo "Unknown option: $1"; usage ;; --help|-h) usage ;;
--*) echo "${red}Unknown option:${rst} $1"; usage ;;
*) TARGET_PATH="$1"; shift ;; *) TARGET_PATH="$1"; shift ;;
esac esac
done done
# --- resolve target ---
if [[ -z "$TARGET_PATH" ]]; then if [[ -z "$TARGET_PATH" ]]; then
read -r -p "Enter path to target project: " TARGET_PATH read -r -p "Enter path to target project: " TARGET_PATH
fi fi
TARGET_PATH="${TARGET_PATH/#\~/$HOME}" TARGET_PATH="${TARGET_PATH/#\~/$HOME}"
# --- pre-flight -----------------------------------------------------------
header "Pre-flight"
# 1. target exists?
if [[ ! -d "$TARGET_PATH" ]]; then
fail "Target directory '$TARGET_PATH' does not exist."
exit 1
fi
# resolve to absolute path
TARGET_PATH="$(cd "$TARGET_PATH" 2>/dev/null && pwd)" || { TARGET_PATH="$(cd "$TARGET_PATH" 2>/dev/null && pwd)" || {
echo "Error: Directory '$TARGET_PATH' does not exist." fail "Cannot access '$TARGET_PATH'."
exit 1 exit 1
} }
ok "Target: $TARGET_PATH"
# 2. write permission?
if [[ ! -w "$TARGET_PATH" ]]; then
fail "No write permission on '$TARGET_PATH'."
exit 1
fi
ok "Write permission: yes"
# 3. already installed? compare versions
AGENT_DIR="$TARGET_PATH/.agent" AGENT_DIR="$TARGET_PATH/.agent"
SRC_DIR="$AGENT_DIR/src" SRC_DIR="$AGENT_DIR/src"
VERSION="$(cat "$METAAGENT_SRC/VERSION" 2>/dev/null || echo '?')" VERSION="$(cat "$METAAGENT_SRC/VERSION" 2>/dev/null || echo '?')"
if [[ -f "$SRC_DIR/VERSION" ]]; then
OLD_VER="$(cat "$SRC_DIR/VERSION" 2>/dev/null || echo '?')"
if [[ "$OLD_VER" != "$VERSION" ]]; then
info "Existing MetaAgent v${OLD_VER} found → upgrading to v${VERSION}"
else
skip "MetaAgent v${VERSION} already installed (use --update to reinstall)"
if [[ "$CHECK" == false ]]; then
warn "No changes applied. Run with --update to overwrite existing files."
fi
fi
else
info "Fresh install: MetaAgent v$VERSION"
fi
# 4. summary
AGENTS_MD="$TARGET_PATH/AGENTS.md"
RULES_DIR="$AGENT_DIR/rules" RULES_DIR="$AGENT_DIR/rules"
DECISIONS_DIR="$AGENT_DIR/decisions"
TASKS_DIR="$AGENT_DIR/tasks"
CONTEXT_DIR="$AGENT_DIR/context"
ARCHIVE_DIR="$AGENT_DIR/archive" ARCHIVE_DIR="$AGENT_DIR/archive"
mkdir -p "$SRC_DIR" "$RULES_DIR" "$ARCHIVE_DIR" ARCHIVE_TASKS_DIR="$ARCHIVE_DIR/tasks"
echo "Installing MetaAgent v$VERSION$SRC_DIR" ARCHIVE_DECISIONS_DIR="$ARCHIVE_DIR/decisions"
ARCHIVE_CHECKPOINTS_DIR="$ARCHIVE_DIR/checkpoints"
REQUESTS_DIR="$AGENT_DIR/requests"
REQUESTS_ACTIVE_DIR="$REQUESTS_DIR/active"
REQUESTS_ARCHIVE_DIR="$REQUESTS_DIR/archive"
ROADMAP_DIR="$AGENT_DIR/roadmap"
ROADMAP_ARCHIVE_DIR="$ROADMAP_DIR/archive"
TEMP_DIR="$TARGET_PATH/.temp"
if [[ "$CHECK" == true ]]; then
echo ""
info "${ylw}--check mode:${rst} all checks passed, no changes applied."
exit 0
fi
# --- phase 1: directories ------------------------------------------------
header "Directories"
mkdir -p "$SRC_DIR" "$RULES_DIR" "$DECISIONS_DIR" "$TASKS_DIR" "$TASKS_DIR/backlog" \
"$CONTEXT_DIR" "$ARCHIVE_DIR" "$ARCHIVE_TASKS_DIR" "$ARCHIVE_DECISIONS_DIR" \
"$ARCHIVE_CHECKPOINTS_DIR" \
"$REQUESTS_ACTIVE_DIR" "$REQUESTS_ARCHIVE_DIR" \
"$ROADMAP_DIR" "$ROADMAP_ARCHIVE_DIR" \
"$TEMP_DIR"
for d in "$SRC_DIR" "$RULES_DIR" "$DECISIONS_DIR" "$TASKS_DIR" "$TASKS_DIR/backlog" \
"$CONTEXT_DIR" "$ARCHIVE_DIR" "$ARCHIVE_TASKS_DIR" "$ARCHIVE_DECISIONS_DIR" \
"$ARCHIVE_CHECKPOINTS_DIR" \
"$REQUESTS_ACTIVE_DIR" "$REQUESTS_ARCHIVE_DIR" \
"$ROADMAP_DIR" "$ROADMAP_ARCHIVE_DIR" \
"$TEMP_DIR"; do
short="${d#$TARGET_PATH/}"
if [[ -d "$d" ]]; then
ok "$short"
else
fail "$short (creation failed)"
fi
done
# --- phase 2: files ------------------------------------------------------
header "Files"
COPY_COUNT=0
SKIP_COUNT=0
FAIL_COUNT=0
# --- copy files ---
copy_file() { copy_file() {
local src="$1" dst_dir="$2" local src="$1" dst_dir="$2"
local name; name="$(basename "$src")" local name; name="$(basename "$src")"
local dst="$dst_dir/$name"
if [[ ! -f "$src" ]]; then if [[ ! -f "$src" ]]; then
echo " [skip] $name (not found)" skip "$name (source not found)"
((SKIP_COUNT += 1))
return return
fi fi
if [[ "$UPDATE" == true ]] || [[ ! -f "$dst_dir/$name" ]]; then if [[ "$UPDATE" == true ]] || [[ ! -f "$dst" ]]; then
cp "$src" "$dst_dir/$name" if cp "$src" "$dst"; then
echo " [copy] $name" ok "$name"
((COPY_COUNT++))
else
fail "$name"
((FAIL_COUNT++))
fi
else else
echo " [skip] $name (exists, use --update to overwrite)" skip "$name (exists, use --update to overwrite)"
((SKIP_COUNT += 1))
fi fi
} }
copy_dir() { copy_dir() {
local src="$1" dst_dir="$2" local src="$1" dst_dir="$2"
local name; name="$(basename "$src")" local name; name="$(basename "$src")"
local dst="$dst_dir/$name"
if [[ ! -d "$src" ]]; then if [[ ! -d "$src" ]]; then
echo " [skip] $name/ (not found)" skip "$name/ (source not found)"
((SKIP_COUNT += 1))
return return
fi fi
mkdir -p "$dst_dir/$name" mkdir -p "$dst"
if [[ "$UPDATE" == true ]]; then if [[ "$UPDATE" == true ]]; then
cp -rf "$src"/* "$dst_dir/$name/" 2>/dev/null || true if cp -rf "$src"/* "$dst/" 2>/dev/null; then
ok "$name/"
((COPY_COUNT++))
else
fail "$name/ (partial copy)"
((FAIL_COUNT++))
fi
else else
cp -rn "$src"/* "$dst_dir/$name/" 2>/dev/null || true cp -rn "$src"/* "$dst/" 2>/dev/null || true
ok "$name/"
((COPY_COUNT++))
fi fi
echo " [copy] $name/"
} }
copy_file "$METAAGENT_SRC/META_AGENT_GUIDE.md" "$SRC_DIR" copy_file "$METAAGENT_SRC/META_AGENT_GUIDE.md" "$SRC_DIR"
@@ -95,8 +213,8 @@ copy_dir "$METAAGENT_SRC/TEMPLATES" "$SRC_DIR"
copy_file "$METAAGENT_SRC/install.sh" "$SRC_DIR" copy_file "$METAAGENT_SRC/install.sh" "$SRC_DIR"
copy_file "$METAAGENT_SRC/install.ps1" "$SRC_DIR" copy_file "$METAAGENT_SRC/install.ps1" "$SRC_DIR"
# --- create / update AGENTS.md in root of target --- # --- phase 3: AGENTS.md --------------------------------------------------
AGENTS_MD="$TARGET_PATH/AGENTS.md" header "AGENTS.md"
create_agents_md() { create_agents_md() {
cat > "$1" << AGENTS_EOF cat > "$1" << AGENTS_EOF
@@ -140,13 +258,32 @@ AGENTS_EOF
if [[ ! -f "$AGENTS_MD" ]]; then if [[ ! -f "$AGENTS_MD" ]]; then
create_agents_md "$AGENTS_MD" create_agents_md "$AGENTS_MD"
echo " [create] AGENTS.md" ok "AGENTS.md created"
elif [[ "$UPDATE" == true ]]; then elif [[ "$UPDATE" == true ]]; then
create_agents_md "$AGENTS_MD" create_agents_md "$AGENTS_MD"
echo " [update] AGENTS.md" ok "AGENTS.md updated"
else else
echo " [skip] AGENTS.md (exists, use --update to overwrite)" skip "AGENTS.md (exists, use --update to overwrite)"
((SKIP_COUNT += 1))
fi fi
# --- summary -------------------------------------------------------------
header "Summary"
echo " MetaAgent v$VERSION$SRC_DIR"
echo "" echo ""
echo "Done! MetaAgent v$VERSION installed at $SRC_DIR" if (( COPY_COUNT > 0 )); then
ok "${COPY_COUNT} file(s) copied"
fi
if (( SKIP_COUNT > 0 )); then
skip "${SKIP_COUNT} file(s) skipped"
fi
if (( FAIL_COUNT > 0 )); then
fail "${FAIL_COUNT} file(s) failed"
fi
echo ""
if (( FAIL_COUNT == 0 )); then
ok "Installation completed successfully."
else
fail "Installation completed with ${FAIL_COUNT} error(s)."
exit 1
fi
+51 -143
View File
@@ -1,171 +1,79 @@
{ {
"$schema": "metaagent-task-manifest", "$schema": "metaagent-task-manifest",
"version": "1.0", "version": "1.0",
"session_id": "metaagent-002", "session_id": "metaagent-003",
"goal": "Обновление metaagent-артефактов до v1.0.0, валидация существующего кода и окружения", "goal": "i18n (ru/en) — инфраструктура и обёртка строк, контроль актуальности переводов",
"created_at": "2026-07-12T20:00:00Z", "created_at": "2026-07-22T18:00:00Z",
"tasks": [ "tasks": [
{ {
"id": "T1", "id": "T9",
"title": "Инициализация проекта и зависимостей", "title": "i18n инфраструктура (cli/i18n.py)",
"description": "Создать структуру директорий, pyproject.toml, venv, установить зависимости", "description": "Создан модуль cli/i18n.py с Translator, t(), set_lang(), setup_i18n(). Язык: CF_LANG (env), по умолчанию 'ru'. Русские переводы — полные, английский — скелет (fallback на ru).",
"type": "config", "type": "feature",
"files": [ "files": ["cli/i18n.py"],
"pyproject.toml",
"cashflow_model/__init__.py",
"sync/__init__.py",
"engine/__init__.py",
"ai/__init__.py",
"cli/__init__.py",
"data/.gitkeep",
"exports/.gitkeep"
],
"depends_on": [], "depends_on": [],
"acceptance_criteria": [ "acceptance_criteria": [
"pyproject.toml создан с правильными зависимостями", "t() возвращает русский текст при CF_LANG=ru",
"Все директории модулей созданы с __init__.py", "t() возвращает русский текст при CF_LANG=en (fallback)",
"ruff lint проходит без ошибок", "t('nonexistent') возвращает 'nonexistent'",
"pytest запускается" "setup_i18n() читает CF_LANG из окружения"
], ],
"status": "completed" "status": "completed"
}, },
{ {
"id": "T2", "id": "T10",
"title": "Модель данных (dataclass + JSON serialization)", "title": "Обёртка CLI-строк в t()",
"description": "Реализовать все сущности: Account, Transaction, RecurringCashflow, Asset, Liability, ForecastScenario, FinancialModel", "description": "Все user-facing строки в cli/main.py и cli/config.py заменены на вызовы t('key', ...). Русский словарь содержит ~150 ключей.",
"type": "feature", "type": "refactor",
"files": [ "files": ["cli/main.py", "cli/config.py"],
"cashflow_model/__init__.py", "depends_on": ["T9"],
"cashflow_model/account.py",
"cashflow_model/transaction.py",
"cashflow_model/recurring.py",
"cashflow_model/asset.py",
"cashflow_model/liability.py",
"cashflow_model/scenario.py",
"cashflow_model/model.py"
],
"depends_on": ["T1"],
"acceptance_criteria": [ "acceptance_criteria": [
"Все сущности — dataclass с правильными полями и типами", "Все help-строки typer.Option/Argument через t()",
"FinancialModel корректно сохраняется и загружается из JSON", "Все console.print сообщения через t()",
"Создание Account, Transaction, Asset, Liability через конструктор работает" "Все docstrings оставлены как комментарии (typer не использует)",
"ruff check проходит"
], ],
"status": "completed" "status": "completed"
}, },
{ {
"id": "T3", "id": "T11",
"title": "Forecast Engine (базовый прогноз)", "title": "AI-промпты через i18n",
"description": "Реализовать ForecastService с методами forecast_cashflow, apply_recurring, project_balance", "description": "prompt.analyze, prompt.advice, prompt.scenario_comparison добавлены в словарь i18n, prompts.py использует t().",
"type": "feature", "type": "refactor",
"files": [ "files": ["ai/prompts.py"],
"engine/__init__.py", "depends_on": ["T9"],
"engine/forecast.py"
],
"depends_on": ["T2"],
"acceptance_criteria": [ "acceptance_criteria": [
"forecast_cashflow(months=12) возвращает список помесячных балансов", "ANALYZE_PROMPT = t('prompt.analyze')",
"Регулярные платежи корректно проецируются на будущие периоды", "ADVICE_PROMPT = t('prompt.advice')",
"Активы учитываются с ростом (growth_rate)", "SCENARIO_COMPARISON_PROMPT = t('prompt.scenario_comparison')"
"Обязательства учитываются с процентами и платежами"
], ],
"status": "completed" "status": "completed"
}, },
{ {
"id": "T4", "id": "T12",
"title": "Scenario Analysis", "title": "Тесты i18n",
"description": "Реализовать ScenarioService с методами: сценарии, what-if, сравнение", "description": "test_i18n.py: базовые тесты Translator, t(), set_lang, fallback, неизвестный ключ.",
"type": "feature",
"files": [
"engine/__init__.py",
"engine/scenarios.py"
],
"depends_on": ["T3"],
"acceptance_criteria": [
"Три предустановленных сценария (baseline, optimistic, pessimistic)",
"What-if: изменение параметров (доход +10%, расход -5%)",
"Сравнение сценариев возвращает сводку различий"
],
"status": "completed"
},
{
"id": "T5",
"title": "Excel Sync (import/export)",
"description": "Реализовать ExcelSync: чтение модели из .xlsx, запись результатов прогноза в .xlsx",
"type": "feature",
"files": [
"sync/__init__.py",
"sync/excel_sync.py"
],
"depends_on": ["T2"],
"acceptance_criteria": [
"Импорт из Excel заполняет FinancialModel",
"Экспорт FinancialModel в Excel создаёт корректный .xlsx",
"Обработка ошибок при невалидном формате Excel"
],
"status": "completed"
},
{
"id": "T6",
"title": "CLI (Typer) — все команды",
"description": "Реализовать CLI через Typer с командами: init, import, export, forecast, scenario, analyze, whatif, compare",
"type": "feature",
"files": [
"cli/__init__.py",
"cli/main.py",
"pyproject.toml"
],
"depends_on": ["T2", "T3", "T4", "T5", "T7"],
"acceptance_criteria": [
"Команда 'cf init' создаёт пустую модель и JSON",
"Команда 'cf forecast --months 12' выводит таблицу прогноза",
"Команда 'cf analyze' вызывает AI Assistant",
"Команда 'cf import' и 'cf export' работают с Excel",
"Команда 'cf scenario' применяет и выводит сценарий",
"Команда 'cf whatif' выполняет what-if анализ",
"Команда 'cf compare' сравнивает сценарии"
],
"status": "completed"
},
{
"id": "T7",
"title": "AI Assistant (промпты + интерфейс)",
"description": "Реализовать AssistantService: генерация промптов, заглушка для вызова AI API",
"type": "feature",
"files": [
"ai/__init__.py",
"ai/prompts.py",
"ai/assistant.py"
],
"depends_on": ["T3"],
"acceptance_criteria": [
"Промпт 'analyze' включает модель и прогноз в JSON",
"Промпт 'advice' формирует запрос на финансовые рекомендации",
"AssistantService возвращает структурированный ответ (заглушка)"
],
"status": "completed"
},
{
"id": "T8",
"title": "Тесты на все модули",
"description": "Написать pytest-тесты для всех модулей",
"type": "test", "type": "test",
"files": [ "files": ["tests/test_i18n.py"],
"tests/test_model.py", "depends_on": ["T9"],
"tests/test_forecast.py",
"tests/test_scenarios.py",
"tests/test_excel_sync.py",
"tests/test_cli.py",
"tests/test_ai.py",
"tests/conftest.py"
],
"depends_on": ["T2", "T3", "T4", "T5", "T6", "T7"],
"acceptance_criteria": [ "acceptance_criteria": [
"pytest запускается и все тесты проходят", "pytest tests/test_i18n.py проходит",
"Покрытие базовых сценариев для каждой сущности", "Покрытие: ru default, en fallback, неизвестный ключ, format args"
"Roundtrip-тест Excel: export → import → compare",
"Forecast-тест: известные входные данные → ожидаемый результат"
], ],
"status": "completed" "status": "completed"
},
{
"id": "T13",
"title": "Аудит и контроль актуальности переводов",
"description": "Периодическая проверка: все ли ключи из TRANSLATIONS['ru'] имеют соответствующий перевод в TRANSLATIONS['en']. При добавлении новых фич — новые ключи должны добавляться в оба словаря.",
"type": "audit",
"files": ["cli/i18n.py"],
"depends_on": ["T9"],
"acceptance_criteria": [
"Все ru-ключи имеют en-перевод или fallback",
"При добавлении нового t('key') он регистрируется в _r()"
],
"status": "pending"
} }
] ]
} }
+18 -18
View File
@@ -1,35 +1,35 @@
# MetaAgent # MetaAgent
Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v1.1.0 — Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v2.1.0 —
набор инструкций для AI-агента. набор инструкций для AI-агента.
## Контекст MetaAgent ## Контекст MetaAgent
| Ресурс | Путь | | Ресурс | Путь |
|--------|------| |--------|------|
| Главная инструкция | `.agent/src/META_AGENT_GUIDE.md` | | Главная инструкция | .agent/src/META_AGENT_GUIDE.md |
| Протоколы фаз | `.agent/src/PROTOCOLS/` | | Протоколы фаз | .agent/src/PROTOCOLS/ |
| Шаблоны артефактов | `.agent/src/TEMPLATES/` | | Шаблоны артефактов | .agent/src/TEMPLATES/ |
| Границы (что разрешено/запрещено) | `.agent/src/BOUNDARIES.md` | | Границы (что разрешено/запрещено) | .agent/src/BOUNDARIES.md |
| Правила проекта | `.agent/rules/project-rules.md` | | Правила проекта | .agent/rules/project-rules.md |
| Примеры работы | `.agent/src/WORKFLOW.md` | | Примеры работы | .agent/src/WORKFLOW.md |
| Версия | `.agent/src/VERSION` | | Версия | .agent/src/VERSION |
## Состояние сессии (если инициализировано) ## Состояние сессии (если инициализировано)
| Артефакт | Путь | | Артефакт | Путь |
|----------|------| |----------|------|
| Чекпоинты сессии | `.agent/checkpoints.json` | | Чекпоинты сессии | .agent/checkpoints.json |
| Манифест задач | `.agent/task-manifest.json` | | Манифест задач | .agent/task-manifest.json |
| Сводка для exec-агента | `.agent/handoff-summary.md` | | Сводка для exec-агента | .agent/handoff-summary.md |
| Анализ репозитория | `.agent/analysis-report.md` | | Анализ репозитория | .agent/analysis-report.md |
## Для исполнительного агента ## Для исполнительного агента
1. **Прочитай** `.agent/src/META_AGENT_GUIDE.md` — пойми жизненный цикл MetaAgent. 1. **Прочитай** .agent/src/META_AGENT_GUIDE.md — пойми жизненный цикл MetaAgent.
2. **Прочитай** `.agent/src/BOUNDARIES.md` — соблюдай границы. 2. **Прочитай** .agent/src/BOUNDARIES.md — соблюдай границы.
3. **Прочитай** `.agent/rules/project-rules.md` — выполни пользовательские правила. 3. **Прочитай** .agent/rules/project-rules.md — выполни пользовательские правила.
4. **Проверь** `.agent/checkpoints.json` — если существует, используй как состояние сессии. 4. **Проверь** .agent/checkpoints.json — если существует, используй как состояние сессии.
5. **Проверь** `.agent/task-manifest.json` — если существует, выполняй задачи по порядку. 5. **Проверь** .agent/task-manifest.json — если существует, выполняй задачи по порядку.
6. Если `.agent/` не инициализирован или устарел — запусти `install.sh --update` для 6. Если .agent/ не инициализирован или устарел — запусти install.sh --update для
обновления исходников MetaAgent до актуальной версии. обновления исходников MetaAgent до актуальной версии.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Jor Oqyude
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+15 -2
View File
@@ -1,7 +1,7 @@
import json import json
from ai import prompts from ai import prompts
from cashflow_model import FinancialModel from cashflow_model import CurrencyConverter, FinancialModel
from engine.forecast import ForecastService from engine.forecast import ForecastService
@@ -10,8 +10,15 @@ class AssistantError(Exception):
class AssistantService: class AssistantService:
def __init__(self, model: FinancialModel): def __init__(
self,
model: FinancialModel,
converter: CurrencyConverter | None = None,
display_currency: str | None = None,
):
self.model = model self.model = model
self.converter = converter or CurrencyConverter(model.exchange_rates)
self.display_currency = display_currency or model.base_currency
def analyze(self, months: int = 12) -> dict: def analyze(self, months: int = 12) -> dict:
forecast_service = ForecastService(self.model) forecast_service = ForecastService(self.model)
@@ -22,6 +29,8 @@ class AssistantService:
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False), model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False),
forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False), forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False),
months=months, months=months,
base_currency=self.model.base_currency,
display_currency=self.display_currency,
) )
return { return {
@@ -39,6 +48,8 @@ class AssistantService:
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False), model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False),
forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False), forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False),
question=question, question=question,
base_currency=self.model.base_currency,
display_currency=self.display_currency,
) )
return { return {
@@ -49,6 +60,8 @@ class AssistantService:
def compare_scenarios(self, scenarios_json: str) -> dict: def compare_scenarios(self, scenarios_json: str) -> dict:
prompt = prompts.SCENARIO_COMPARISON_PROMPT.format( prompt = prompts.SCENARIO_COMPARISON_PROMPT.format(
scenarios_json=scenarios_json, scenarios_json=scenarios_json,
base_currency=self.model.base_currency,
display_currency=self.display_currency,
) )
return { return {
"prompt": prompt, "prompt": prompt,
+13 -38
View File
@@ -1,46 +1,21 @@
ANALYZE_PROMPT = """ from cli.i18n import t
Ты — финансовый AI-ассистент. Проанализируй финансовую модель пользователя.
### Модель (JSON): ANALYZE_PROMPT = t("prompt.analyze")
{model_json} ADVICE_PROMPT = t("prompt.advice")
SCENARIO_COMPARISON_PROMPT = t("prompt.scenario_comparison")
### Прогноз на {months} месяцев:
{forecast_json}
Дай анализ по пунктам:
1. Общее финансовое состояние
2. Тренд денежного потока (рост/падение)
3. Достаточность ликвидности
4. Рекомендации по улучшению
"""
ADVICE_PROMPT = """
Ты — финансовый AI-ассистент. Дай персональные рекомендации.
### Модель:
{model_json}
### Прогноз:
{forecast_json}
Вопрос пользователя: {question}
Ответь как опытный финансовый консультант.
"""
SCENARIO_COMPARISON_PROMPT = """
Ты — финансовый AI-ассистент. Сравни сценарии прогноза.
### Результаты сценариев:
{scenarios_json}
Дай рекомендацию: какой сценарий наиболее вероятен и почему.
"""
def format_context(model_json: str, forecast_json: str, months: int = 12) -> str: def format_context(
model_json: str,
forecast_json: str,
months: int = 12,
base_currency: str = "RUB",
display_currency: str = "RUB",
) -> str:
return ANALYZE_PROMPT.format( return ANALYZE_PROMPT.format(
model_json=model_json, model_json=model_json,
forecast_json=forecast_json, forecast_json=forecast_json,
months=months, months=months,
base_currency=base_currency,
display_currency=display_currency,
) )
+5
View File
@@ -1,5 +1,6 @@
from cashflow_model.account import Account from cashflow_model.account import Account
from cashflow_model.asset import Asset from cashflow_model.asset import Asset
from cashflow_model.currency import CURRENCY_SYMBOLS, CurrencyConverter, CurrencyError, ExchangeRate
from cashflow_model.liability import Liability from cashflow_model.liability import Liability
from cashflow_model.model import FinancialModel from cashflow_model.model import FinancialModel
from cashflow_model.recurring import RecurringCashflow from cashflow_model.recurring import RecurringCashflow
@@ -14,4 +15,8 @@ __all__ = [
"Liability", "Liability",
"ForecastScenario", "ForecastScenario",
"FinancialModel", "FinancialModel",
"ExchangeRate",
"CurrencyConverter",
"CurrencyError",
"CURRENCY_SYMBOLS",
] ]
+79
View File
@@ -0,0 +1,79 @@
from dataclasses import dataclass
CURRENCY_SYMBOLS = {
"RUB": "",
"USD": "$",
"EUR": "",
"GBP": "£",
"CNY": "¥",
"JPY": "¥",
"KZT": "",
"UAH": "",
}
@dataclass
class ExchangeRate:
from_currency: str = "USD"
to_currency: str = "RUB"
rate: float = 80.0
def to_dict(self) -> dict:
return {
"from_currency": self.from_currency,
"to_currency": self.to_currency,
"rate": self.rate,
}
@classmethod
def from_dict(cls, data: dict) -> "ExchangeRate":
return cls(
from_currency=data.get("from_currency", "USD"),
to_currency=data.get("to_currency", "RUB"),
rate=data.get("rate", 80.0),
)
DEFAULT_RATES: list[ExchangeRate] = [
ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0),
]
class CurrencyError(Exception):
pass
class CurrencyConverter:
def __init__(self, rates: list[ExchangeRate] | None = None):
self._rates: dict[tuple[str, str], float] = {}
if rates:
for r in rates:
self.set_rate(r.from_currency, r.to_currency, r.rate)
def set_rate(self, from_currency: str, to_currency: str, rate: float) -> None:
if rate <= 0:
raise CurrencyError(f"Rate must be positive: {rate}")
self._rates[(from_currency, to_currency)] = rate
inverse = 1.0 / rate
self._rates[(to_currency, from_currency)] = inverse
def get_rate(self, from_currency: str, to_currency: str) -> float:
if from_currency == to_currency:
return 1.0
try:
return self._rates[(from_currency, to_currency)]
except KeyError:
raise CurrencyError(f"No exchange rate: {from_currency}{to_currency}")
def convert(self, amount: float, from_currency: str, to_currency: str) -> float:
if from_currency == to_currency:
return amount
rate = self.get_rate(from_currency, to_currency)
return round(amount * rate, 2)
def get_symbol(self, currency: str) -> str:
return CURRENCY_SYMBOLS.get(currency, currency)
@classmethod
def with_defaults(cls) -> "CurrencyConverter":
return cls(DEFAULT_RATES)
+7
View File
@@ -4,6 +4,7 @@ from pathlib import Path
from cashflow_model.account import Account from cashflow_model.account import Account
from cashflow_model.asset import Asset from cashflow_model.asset import Asset
from cashflow_model.currency import DEFAULT_RATES, ExchangeRate
from cashflow_model.liability import Liability from cashflow_model.liability import Liability
from cashflow_model.recurring import RecurringCashflow from cashflow_model.recurring import RecurringCashflow
from cashflow_model.scenario import ForecastScenario from cashflow_model.scenario import ForecastScenario
@@ -12,32 +13,38 @@ from cashflow_model.transaction import Transaction
@dataclass @dataclass
class FinancialModel: class FinancialModel:
base_currency: str = "RUB"
accounts: list[Account] = field(default_factory=list) accounts: list[Account] = field(default_factory=list)
transactions: list[Transaction] = field(default_factory=list) transactions: list[Transaction] = field(default_factory=list)
recurring: list[RecurringCashflow] = field(default_factory=list) recurring: list[RecurringCashflow] = field(default_factory=list)
assets: list[Asset] = field(default_factory=list) assets: list[Asset] = field(default_factory=list)
liabilities: list[Liability] = field(default_factory=list) liabilities: list[Liability] = field(default_factory=list)
scenarios: list[ForecastScenario] = field(default_factory=list) scenarios: list[ForecastScenario] = field(default_factory=list)
exchange_rates: list[ExchangeRate] = field(default_factory=lambda: DEFAULT_RATES.copy())
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
"base_currency": self.base_currency,
"accounts": [a.to_dict() for a in self.accounts], "accounts": [a.to_dict() for a in self.accounts],
"transactions": [t.to_dict() for t in self.transactions], "transactions": [t.to_dict() for t in self.transactions],
"recurring": [r.to_dict() for r in self.recurring], "recurring": [r.to_dict() for r in self.recurring],
"assets": [a.to_dict() for a in self.assets], "assets": [a.to_dict() for a in self.assets],
"liabilities": [li.to_dict() for li in self.liabilities], "liabilities": [li.to_dict() for li in self.liabilities],
"scenarios": [s.to_dict() for s in self.scenarios], "scenarios": [s.to_dict() for s in self.scenarios],
"exchange_rates": [r.to_dict() for r in self.exchange_rates],
} }
@classmethod @classmethod
def from_dict(cls, data: dict) -> "FinancialModel": def from_dict(cls, data: dict) -> "FinancialModel":
return cls( return cls(
base_currency=data.get("base_currency", "RUB"),
accounts=[Account.from_dict(a) for a in data.get("accounts", [])], accounts=[Account.from_dict(a) for a in data.get("accounts", [])],
transactions=[Transaction.from_dict(t) for t in data.get("transactions", [])], transactions=[Transaction.from_dict(t) for t in data.get("transactions", [])],
recurring=[RecurringCashflow.from_dict(r) for r in data.get("recurring", [])], recurring=[RecurringCashflow.from_dict(r) for r in data.get("recurring", [])],
assets=[Asset.from_dict(a) for a in data.get("assets", [])], assets=[Asset.from_dict(a) for a in data.get("assets", [])],
liabilities=[Liability.from_dict(li) for li in data.get("liabilities", [])], liabilities=[Liability.from_dict(li) for li in data.get("liabilities", [])],
scenarios=[ForecastScenario.from_dict(s) for s in data.get("scenarios", [])], scenarios=[ForecastScenario.from_dict(s) for s in data.get("scenarios", [])],
exchange_rates=[ExchangeRate.from_dict(r) for r in data.get("exchange_rates", [])],
) )
def save(self, path: str | Path) -> None: def save(self, path: str | Path) -> None:
+428
View File
@@ -0,0 +1,428 @@
from pathlib import Path
import typer
from rich.console import Console
from rich.table import Table
from cashflow_model import (
Account,
Asset,
ExchangeRate,
FinancialModel,
Liability,
RecurringCashflow,
Transaction,
)
from cli.i18n import t
app = typer.Typer(name="config", help=t("config.help"))
console = Console()
DATA_DIR = Path("data")
MODEL_PATH = DATA_DIR / "model.json"
def _load_model() -> FinancialModel:
if MODEL_PATH.exists():
return FinancialModel.load(MODEL_PATH)
return FinancialModel()
def _save_model(model: FinancialModel) -> None:
model.save(MODEL_PATH)
# ── base-currency ──────────────────────────────────────────────
@app.command()
def base_currency(
currency: str = typer.Argument(None, help=t("cmd.config.base_currency.arg")),
) -> None:
"""Показать или установить базовую валюту модели"""
model = _load_model()
if currency:
model.base_currency = currency.upper()
_save_model(model)
ok = t("global.ok")
msg = t("cmd.config.base_currency.ok", currency=model.base_currency)
console.print(f"[green]{ok}[/green] {msg}")
else:
console.print(t("cmd.config.base_currency.show", currency=f"[bold]{model.base_currency}[/bold]"))
# ── rate ───────────────────────────────────────────────────────
@app.command()
def rate_set(
from_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.from")),
to_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.to")),
rate: float = typer.Argument(..., help=t("cmd.config.rate_set.arg.rate")),
) -> None:
"""Добавить или обновить курс валюты"""
model = _load_model()
fc, tc = from_currency.upper(), to_currency.upper()
existing = [r for r in model.exchange_rates if r.from_currency == fc and r.to_currency == tc]
if existing:
existing[0].rate = rate
else:
model.exchange_rates.append(ExchangeRate(
from_currency=from_currency.upper(),
to_currency=to_currency.upper(),
rate=rate,
))
_save_model(model)
fc, tc = from_currency.upper(), to_currency.upper()
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.rate_set.ok', fc=fc, tc=tc, rate=rate)}")
@app.command(name="rate-list")
def rate_list() -> None:
"""Список курсов валют"""
model = _load_model()
if not model.exchange_rates:
console.print(f"[yellow]{t('cmd.config.rate_list.empty')}[/yellow]")
return
table = Table(title=t("table.rates.title"))
table.add_column(t("table.rates.col.from"), style="cyan")
table.add_column(t("table.rates.col.to"), style="cyan")
table.add_column(t("table.rates.col.rate"), justify="right")
for r in model.exchange_rates:
table.add_row(r.from_currency, r.to_currency, str(r.rate))
console.print(table)
@app.command(name="rate-remove")
def rate_remove(
from_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.from")),
to_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.to")),
) -> None:
"""Удалить курс валюты"""
model = _load_model()
fc, tc = from_currency.upper(), to_currency.upper()
initial = len(model.exchange_rates)
model.exchange_rates = [
r for r in model.exchange_rates
if not (r.from_currency == fc and r.to_currency == tc)
]
if len(model.exchange_rates) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.rate_remove.ok', fc=fc, tc=tc)}")
else:
console.print(f"[red]{t('cmd.config.rate_remove.err', fc=fc, tc=tc)}[/red]")
raise typer.Exit(1)
# ── account ────────────────────────────────────────────────────
@app.command()
def account_add(
name: str = typer.Option(..., "--name", "-n", help=t("cmd.config.account_add.opt.name")),
balance: float = typer.Option(0.0, "--balance", "-b", help=t("cmd.config.account_add.opt.balance")),
currency: str = typer.Option("", "--currency", "-c", help=t("cmd.config.account_add.opt.currency")),
) -> None:
"""Добавить счёт"""
model = _load_model()
curr = currency.upper() if currency else model.base_currency
account = Account(name=name, balance=balance, currency=curr)
model.accounts.append(account)
_save_model(model)
ok = t("global.ok")
msg = t("cmd.config.account_add.ok", name=name, balance=balance, currency=curr)
console.print(f"[green]{ok}[/green] {msg}")
@app.command(name="account-list")
def account_list() -> None:
"""Список счетов"""
model = _load_model()
if not model.accounts:
console.print(f"[yellow]{t('cmd.config.account_list.empty')}[/yellow]")
return
table = Table(title=t("table.accounts_config.title"))
table.add_column(t("table.accounts_config.col.id"), style="dim")
table.add_column(t("table.accounts_config.col.name"), style="cyan")
table.add_column(t("table.accounts_config.col.currency"))
table.add_column(t("table.accounts_config.col.balance"), justify="right")
for a in model.accounts:
table.add_row(str(a.id)[:8], a.name, a.currency, f"{a.balance:,.2f}")
console.print(table)
@app.command(name="account-remove")
def account_remove(
identifier: str = typer.Argument(..., help=t("cmd.config.account_remove.arg")),
) -> None:
"""Удалить счёт по ID или названию"""
model = _load_model()
initial = len(model.accounts)
model.accounts = [
a for a in model.accounts
if str(a.id) != identifier and a.name != identifier
]
if len(model.accounts) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.account_remove.ok', id=identifier)}")
else:
console.print(f"[red]{t('cmd.config.account_remove.err', id=identifier)}[/red]")
raise typer.Exit(1)
# ── transaction ────────────────────────────────────────────────
@app.command()
def transaction_add(
account: str = typer.Option(..., "--account", "-a", help=t("cmd.config.transaction_add.opt.account")),
amount: float = typer.Option(..., "--amount", "-m", help=t("cmd.config.transaction_add.opt.amount")),
category: str = typer.Option("", "--category", "-c", help=t("cmd.config.transaction_add.opt.category")),
description: str = typer.Option("", "--description", "-d", help=t("cmd.config.transaction_add.opt.description")),
date: str = typer.Option("", "--date", "-D", help=t("cmd.config.transaction_add.opt.date")),
) -> None:
"""Добавить транзакцию"""
model = _load_model()
account_id = _resolve_account_id(model, account)
if not account_id:
console.print(f"[red]{t('cmd.config.transaction_add.err', account=account)}[/red]")
raise typer.Exit(1)
txn = Transaction(
date=date or "",
account=account_id,
category=category,
amount=amount,
description=description,
)
model.transactions.append(txn)
_save_model(model)
kind = t("cmd.config.transaction_add.kind.income") if amount >= 0 else t("cmd.config.transaction_add.kind.expense")
ok = t("global.ok")
msg = t("cmd.config.transaction_add.ok", kind=kind, amount=abs(amount), account=account)
console.print(f"[green]{ok}[/green] {msg}")
@app.command(name="transaction-list")
def transaction_list() -> None:
"""Список транзакций"""
model = _load_model()
if not model.transactions:
console.print(f"[yellow]{t('cmd.config.transaction_list.empty')}[/yellow]")
return
table = Table(title=t("table.transactions.title"))
table.add_column(t("table.accounts_config.col.id"), style="dim")
table.add_column(t("table.transactions.col.date"))
table.add_column(t("table.accounts_config.col.name"))
table.add_column(t("table.transactions.col.category"))
table.add_column(t("table.transactions.col.amount"), justify="right")
table.add_column(t("table.transactions.col.description"))
for tx in model.transactions:
account_name = _account_name(model, tx.account)
table.add_row(
str(tx.id)[:8], tx.date, account_name, tx.category,
f"{tx.amount:,.2f}", tx.description,
)
console.print(table)
@app.command(name="transaction-remove")
def transaction_remove(
identifier: str = typer.Argument(..., help=t("cmd.config.transaction_remove.arg")),
) -> None:
"""Удалить транзакцию по ID"""
model = _load_model()
initial = len(model.transactions)
model.transactions = [t for t in model.transactions if str(t.id) != identifier]
if len(model.transactions) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.transaction_remove.ok', id=identifier)}")
else:
console.print(f"[red]{t('cmd.config.transaction_remove.err', id=identifier)}[/red]")
raise typer.Exit(1)
# ── recurring ──────────────────────────────────────────────────
@app.command()
def recurring_add(
amount: float = typer.Option(..., "--amount", "-m", help=t("cmd.config.recurring_add.opt.amount")),
category: str = typer.Option("", "--category", "-c", help=t("cmd.config.recurring_add.opt.category")),
frequency: str = typer.Option("monthly", "--frequency", "-f", help=t("cmd.config.recurring_add.opt.frequency")),
start_date: str = typer.Option("", "--start", "-s", help=t("cmd.config.recurring_add.opt.start")),
end_date: str = typer.Option("", "--end", "-e", help=t("cmd.config.recurring_add.opt.end")),
) -> None:
"""Добавить регулярный платёж"""
model = _load_model()
rc = RecurringCashflow(
start_date=start_date or "", end_date=end_date or "",
frequency=frequency, amount=amount, category=category,
)
model.recurring.append(rc)
_save_model(model)
ok = t("global.ok")
msg = t("cmd.config.recurring_add.ok", amount=abs(amount), frequency=frequency)
console.print(f"[green]{ok}[/green] {msg}")
@app.command(name="recurring-list")
def recurring_list() -> None:
"""Список регулярных платежей"""
model = _load_model()
if not model.recurring:
console.print(f"[yellow]{t('cmd.config.recurring_list.empty')}[/yellow]")
return
table = Table(title=t("table.recurring.title"))
table.add_column(t("table.recurring.col.id"), style="dim")
table.add_column(t("table.recurring.col.start"))
table.add_column(t("table.recurring.col.end"))
table.add_column(t("table.recurring.col.period"))
table.add_column(t("table.recurring.col.amount"), justify="right")
table.add_column(t("table.recurring.col.category"))
for r in model.recurring:
row = (str(r.id)[:8], r.start_date, r.end_date, r.frequency, f"{r.amount:,.2f}", r.category)
table.add_row(*row)
console.print(table)
@app.command(name="recurring-remove")
def recurring_remove(
identifier: str = typer.Argument(..., help=t("cmd.config.recurring_remove.arg")),
) -> None:
"""Удалить регулярный платёж по ID"""
model = _load_model()
initial = len(model.recurring)
model.recurring = [r for r in model.recurring if str(r.id) != identifier]
if len(model.recurring) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.recurring_remove.ok', id=identifier)}")
else:
console.print(f"[red]{t('cmd.config.recurring_remove.err', id=identifier)}[/red]")
raise typer.Exit(1)
# ── asset ──────────────────────────────────────────────────────
@app.command()
def asset_add(
name: str = typer.Option(..., "--name", "-n", help=t("cmd.config.asset_add.opt.name")),
value: float = typer.Option(0.0, "--value", "-v", help=t("cmd.config.asset_add.opt.value")),
growth_rate: float = typer.Option(0.0, "--growth", "-g", help=t("cmd.config.asset_add.opt.growth")),
) -> None:
"""Добавить актив"""
model = _load_model()
asset = Asset(name=name, value=value, growth_rate=growth_rate)
model.assets.append(asset)
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.asset_add.ok', name=name, value=value)}")
@app.command(name="asset-list")
def asset_list() -> None:
"""Список активов"""
model = _load_model()
if not model.assets:
console.print(f"[yellow]{t('cmd.config.asset_list.empty')}[/yellow]")
return
table = Table(title=t("table.assets.title"))
table.add_column(t("table.assets.col.id"), style="dim")
table.add_column(t("table.assets.col.name"), style="cyan")
table.add_column(t("table.assets.col.value"), justify="right")
table.add_column(t("table.assets.col.growth"), justify="right")
for a in model.assets:
table.add_row(str(a.id)[:8], a.name, f"{a.value:,.2f}", f"{a.growth_rate:.1f}%")
console.print(table)
@app.command(name="asset-remove")
def asset_remove(
identifier: str = typer.Argument(..., help=t("cmd.config.asset_remove.arg")),
) -> None:
"""Удалить актив по ID или названию"""
model = _load_model()
initial = len(model.assets)
model.assets = [
a for a in model.assets
if str(a.id) != identifier and a.name != identifier
]
if len(model.assets) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.asset_remove.ok', id=identifier)}")
else:
console.print(f"[red]{t('cmd.config.asset_remove.err', id=identifier)}[/red]")
raise typer.Exit(1)
# ── liability ──────────────────────────────────────────────────
@app.command()
def liability_add(
name: str = typer.Option(..., "--name", "-n", help=t("cmd.config.liability_add.opt.name")),
balance: float = typer.Option(0.0, "--balance", "-b", help=t("cmd.config.liability_add.opt.balance")),
interest: float = typer.Option(0.0, "--interest", "-i", help=t("cmd.config.liability_add.opt.interest")),
payment: float = typer.Option(0.0, "--payment", "-p", help=t("cmd.config.liability_add.opt.payment")),
) -> None:
"""Добавить обязательство"""
model = _load_model()
liability = Liability(name=name, balance=balance, interest=interest, payment=payment)
model.liabilities.append(liability)
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.liability_add.ok', name=name, balance=balance)}")
@app.command(name="liability-list")
def liability_list() -> None:
"""Список обязательств"""
model = _load_model()
if not model.liabilities:
console.print(f"[yellow]{t('cmd.config.liability_list.empty')}[/yellow]")
return
table = Table(title=t("table.liabilities.title"))
table.add_column(t("table.liabilities.col.id"), style="dim")
table.add_column(t("table.liabilities.col.name"), style="cyan")
table.add_column(t("table.liabilities.col.balance"), justify="right")
table.add_column(t("table.liabilities.col.rate"), justify="right")
table.add_column(t("table.liabilities.col.payment"), justify="right")
for li in model.liabilities:
b, i, p = f"{li.balance:,.2f}", f"{li.interest:.1f}%", f"{li.payment:,.2f}"
row = (str(li.id)[:8], li.name, b, i, p)
table.add_row(*row)
console.print(table)
@app.command(name="liability-remove")
def liability_remove(
identifier: str = typer.Argument(..., help=t("cmd.config.liability_remove.arg")),
) -> None:
"""Удалить обязательство по ID или названию"""
model = _load_model()
initial = len(model.liabilities)
model.liabilities = [
li for li in model.liabilities
if str(li.id) != identifier and li.name != identifier
]
if len(model.liabilities) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.liability_remove.ok', id=identifier)}")
else:
console.print(f"[red]{t('cmd.config.liability_remove.err', id=identifier)}[/red]")
raise typer.Exit(1)
# ── helpers ────────────────────────────────────────────────────
def _resolve_account_id(model: FinancialModel, identifier: str) -> str | None:
for a in model.accounts:
if str(a.id) == identifier or a.name == identifier:
return str(a.id)
return None
def _account_name(model: FinancialModel, account_id: str) -> str:
for a in model.accounts:
if str(a.id) == account_id:
return a.name
return account_id[:8]
+350
View File
@@ -0,0 +1,350 @@
import os
_TRANSLATIONS: dict[str, dict[str, str]] = {}
def _r(key: str, text: str) -> str:
_TRANSLATIONS.setdefault("ru", {})[key] = text
return text
def _e(key: str, text: str) -> str:
_TRANSLATIONS.setdefault("en", {})[key] = text
return text
# ── app ────────────────────────────────────────────────────────
_r("app.help", "CashFlow Forecast — личная финансовая модель")
# ── cmd.init ───────────────────────────────────────────────────
_r("cmd.init.help", "Создать пустую финансовую модель")
_r("cmd.init.ok", "Пустая модель создана в {path}")
# ── cmd.forecast ───────────────────────────────────────────────
_r("cmd.forecast.help", "Запустить прогноз денежных потоков")
_r("cmd.forecast.opt.months", "Количество месяцев прогноза")
_r("cmd.forecast.opt.currency", "Валюта отображения (по умолчанию — базовая валюта модели)")
_r("cmd.forecast.title", "Прогноз на {months} мес. ({currency})")
_r("cmd.forecast.col.account", "Счёт")
_r("cmd.forecast.col.month", "Месяц")
_r("cmd.forecast.col.balance", "Баланс")
_r("cmd.forecast.col.income", "Доход")
_r("cmd.forecast.col.expenses", "Расход")
_r("cmd.forecast.total", "Итог: Баланс: {balance} | Доход: {income} | Расход: {expenses}")
# ── cmd.scenario ──────────────────────────────────────────────
_r("cmd.scenario.help", "Применить сценарий и показать прогноз")
_r("cmd.scenario.arg.name", "Имя сценария: baseline, optimistic, pessimistic")
_r("cmd.scenario.opt.months", "Количество месяцев")
_r("cmd.scenario.err.unknown", "Неизвестный сценарий: {name}")
_r("cmd.scenario.err.available", "Доступны: {scenarios}")
_r("cmd.scenario.label", "Сценарий")
_r("cmd.scenario.label.balance", "Баланс")
_r("cmd.scenario.label.income", "Доход")
_r("cmd.scenario.label.expenses", "Расход")
# ── cmd.whatif ────────────────────────────────────────────────
_r("cmd.whatif.help", "What-if анализ с произвольными множителями")
_r("cmd.whatif.opt.income", "Множитель дохода")
_r("cmd.whatif.opt.expense", "Множитель расхода")
_r("cmd.whatif.opt.growth", "Множитель роста активов")
_r("cmd.whatif.opt.months", "Количество месяцев")
_r("cmd.whatif.title", "What-if анализ")
_r("cmd.whatif.params", "Доход x{income} | Расход x{expense} | Рост x{growth}")
# ── cmd.compare ───────────────────────────────────────────────
_r("cmd.compare.help", "Сравнить все сценарии")
_r("cmd.compare.opt.months", "Количество месяцев")
_r("cmd.compare.title", "Сравнение сценариев ({currency})")
_r("cmd.compare.col.scenario", "Сценарий")
# ── cmd.import ────────────────────────────────────────────────
_r("cmd.import.help", "Импорт данных из Excel")
_r("cmd.import.arg.path", "Путь к .xlsx файлу")
_r("cmd.import.ok",
"Импортировано: {accounts} счетов, {transactions} транзакций, "
"{recurring} регулярных платежей, {assets} активов, {liabilities} обязательств")
_r("cmd.import.base_currency", "Базовая валюта: {currency}")
_r("cmd.import.err", "Ошибка импорта: {error}")
# ── cmd.export ────────────────────────────────────────────────
_r("cmd.export.help", "Экспорт модели в Excel")
_r("cmd.export.arg.path", "Путь для .xlsx файла")
_r("cmd.export.ok", "Модель экспортирована в {path}")
_r("cmd.export.err", "Ошибка экспорта: {error}")
# ── cmd.analyze ───────────────────────────────────────────────
_r("cmd.analyze.help", "AI-анализ финансовой модели")
_r("cmd.analyze.opt.months", "Количество месяцев для анализа")
_r("cmd.analyze.title.prompt", "Промпт для AI")
_r("cmd.analyze.title.summary", "Сводка")
_r("cmd.analyze.base_currency", "Базовая валюта: {currency}")
_r("cmd.analyze.stub", "AI-ответ: заглушка. Подключите реальный API в ai/assistant.py")
# ── cmd.info ──────────────────────────────────────────────────
_r("cmd.info.help", "Сводка всей финансовой модели")
_r("cmd.info.metadata", "Метаданные")
_r("cmd.info.base_currency", "Базовая валюта: {currency} ({symbol})")
_r("cmd.info.accounts", "Счетов: {count}")
_r("cmd.info.transactions", "Транзакций: {count}")
_r("cmd.info.recurring", "Регулярных платежей: {count}")
_r("cmd.info.assets", "Активов: {count}")
_r("cmd.info.liabilities", "Обязательств: {count}")
_r("cmd.info.rates", "Курсов валют: {count}")
_r("cmd.info.more", "... и ещё {count} транзакций")
# ── table headers ─────────────────────────────────────────────
_r("table.accounts.title", "Счета")
_r("table.accounts_config.title", "Счета")
_r("table.accounts.col.name", "Название")
_r("table.accounts.col.currency", "Валюта")
_r("table.accounts.col.balance", "Баланс")
_r("table.transactions.title", "Транзакции")
_r("table.transactions.col.date", "Дата")
_r("table.transactions.col.category", "Категория")
_r("table.transactions.col.amount", "Сумма")
_r("table.transactions.col.description", "Описание")
_r("table.recurring.title", "Регулярные платежи")
_r("table.recurring.col.period", "Период")
_r("table.recurring.col.amount", "Сумма")
_r("table.recurring.col.category", "Категория")
_r("table.recurring.col.id", "ID")
_r("table.recurring.col.start", "Начало")
_r("table.recurring.col.end", "Конец")
_r("table.assets.title", "Активы")
_r("table.assets.col.id", "ID")
_r("table.assets.col.name", "Название")
_r("table.assets.col.value", "Стоимость")
_r("table.assets.col.growth", "Рост (%)")
_r("table.liabilities.title", "Обязательства")
_r("table.liabilities.col.id", "ID")
_r("table.liabilities.col.name", "Название")
_r("table.liabilities.col.balance", "Долг")
_r("table.liabilities.col.rate", "Ставка")
_r("table.liabilities.col.payment", "Платёж")
_r("table.rates.title", "Курсы валют")
_r("table.rates.col.from", "Из")
_r("table.rates.col.to", "В")
_r("table.rates.col.rate", "Курс")
_r("table.accounts_config.col.id", "ID")
_r("table.accounts_config.col.name", "Название")
_r("table.accounts_config.col.currency", "Валюта")
_r("table.accounts_config.col.balance", "Баланс")
# ── config ────────────────────────────────────────────────────
_r("config.help", "Управление моделью (счета, транзакции, валюты...)")
_r("cmd.config.base_currency.help", "Показать или установить базовую валюту модели")
_r("cmd.config.base_currency.arg", "Код валюты (RUB, USD, EUR...)")
_r("cmd.config.base_currency.ok", "Базовая валюта установлена: {currency}")
_r("cmd.config.base_currency.show", "Базовая валюта: {currency}")
_r("cmd.config.rate_set.help", "Добавить или обновить курс валюты")
_r("cmd.config.rate_set.arg.from", "Из валюты")
_r("cmd.config.rate_set.arg.to", "В валюту")
_r("cmd.config.rate_set.arg.rate", "Курс")
_r("cmd.config.rate_set.ok", "Курс {fc}{tc} = {rate}")
_r("cmd.config.rate_list.help", "Список курсов валют")
_r("cmd.config.rate_list.empty", "Курсы не заданы")
_r("cmd.config.rate_remove.help", "Удалить курс валюты")
_r("cmd.config.rate_remove.ok", "Курс {fc}{tc} удалён")
_r("cmd.config.rate_remove.err", "Курс {fc}{tc} не найден")
_r("cmd.config.account_add.help", "Добавить счёт")
_r("cmd.config.account_add.opt.name", "Название счёта")
_r("cmd.config.account_add.opt.balance", "Начальный баланс")
_r("cmd.config.account_add.opt.currency", "Валюта счёта")
_r("cmd.config.account_add.ok", "Счёт '{name}' добавлен (баланс: {balance}, валюта: {currency})")
_r("cmd.config.account_list.help", "Список счетов")
_r("cmd.config.account_list.empty", "Счета не добавлены")
_r("cmd.config.account_remove.help", "Удалить счёт по ID или названию")
_r("cmd.config.account_remove.arg", "ID или название счёта")
_r("cmd.config.account_remove.ok", "Счёт '{id}' удалён")
_r("cmd.config.account_remove.err", "Счёт '{id}' не найден")
_r("cmd.config.transaction_add.help", "Добавить транзакцию")
_r("cmd.config.transaction_add.opt.account", "ID или название счёта")
_r("cmd.config.transaction_add.opt.amount", "Сумма (доход/расход)")
_r("cmd.config.transaction_add.opt.category", "Категория")
_r("cmd.config.transaction_add.opt.description", "Описание")
_r("cmd.config.transaction_add.opt.date", "Дата (YYYY-MM-DD)")
_r("cmd.config.transaction_add.ok", "Транзакция '{kind}' на {amount:.2f} (счёт: {account})")
_r("cmd.config.transaction_add.kind.income", "доход")
_r("cmd.config.transaction_add.kind.expense", "расход")
_r("cmd.config.transaction_add.err", "Счёт '{account}' не найден")
_r("cmd.config.transaction_list.help", "Список транзакций")
_r("cmd.config.transaction_list.empty", "Транзакции не добавлены")
_r("cmd.config.transaction_remove.help", "Удалить транзакцию по ID")
_r("cmd.config.transaction_remove.arg", "ID транзакции")
_r("cmd.config.transaction_remove.ok", "Транзакция '{id}' удалена")
_r("cmd.config.transaction_remove.err", "Транзакция '{id}' не найдена")
_r("cmd.config.recurring_add.help", "Добавить регулярный платёж")
_r("cmd.config.recurring_add.opt.amount", "Сумма")
_r("cmd.config.recurring_add.opt.category", "Категория")
_r("cmd.config.recurring_add.opt.frequency", "Периодичность")
_r("cmd.config.recurring_add.opt.start", "Дата начала (YYYY-MM-DD)")
_r("cmd.config.recurring_add.opt.end", "Дата окончания (YYYY-MM-DD)")
_r("cmd.config.recurring_add.ok", "Регулярный платёж на {amount:.2f} ({frequency})")
_r("cmd.config.recurring_list.help", "Список регулярных платежей")
_r("cmd.config.recurring_list.empty", "Регулярные платежи не добавлены")
_r("cmd.config.recurring_remove.help", "Удалить регулярный платёж по ID")
_r("cmd.config.recurring_remove.arg", "ID регулярного платежа")
_r("cmd.config.recurring_remove.ok", "Регулярный платёж '{id}' удалён")
_r("cmd.config.recurring_remove.err", "Регулярный платёж '{id}' не найден")
_r("cmd.config.asset_add.help", "Добавить актив")
_r("cmd.config.asset_add.opt.name", "Название актива")
_r("cmd.config.asset_add.opt.value", "Стоимость")
_r("cmd.config.asset_add.opt.growth", "Годовой рост (%)")
_r("cmd.config.asset_add.ok", "Актив '{name}' добавлен (стоимость: {value})")
_r("cmd.config.asset_list.help", "Список активов")
_r("cmd.config.asset_list.empty", "Активы не добавлены")
_r("cmd.config.asset_remove.help", "Удалить актив по ID или названию")
_r("cmd.config.asset_remove.arg", "ID или название актива")
_r("cmd.config.asset_remove.ok", "Актив '{id}' удалён")
_r("cmd.config.asset_remove.err", "Актив '{id}' не найден")
_r("cmd.config.liability_add.help", "Добавить обязательство")
_r("cmd.config.liability_add.opt.name", "Название")
_r("cmd.config.liability_add.opt.balance", "Остаток долга")
_r("cmd.config.liability_add.opt.interest", "Процентная ставка (%)")
_r("cmd.config.liability_add.opt.payment", "Ежемесячный платёж")
_r("cmd.config.liability_add.ok", "Обязательство '{name}' добавлено (долг: {balance})")
_r("cmd.config.liability_list.help", "Список обязательств")
_r("cmd.config.liability_list.empty", "Обязательства не добавлены")
_r("cmd.config.liability_remove.help", "Удалить обязательство по ID или названию")
_r("cmd.config.liability_remove.arg", "ID или название обязательства")
_r("cmd.config.liability_remove.ok", "Обязательство '{id}' удалено")
_r("cmd.config.liability_remove.err", "Обязательство '{id}' не найдено")
# ── global ─────────────────────────────────────────────────────
_r("global.ok", "OK")
# ── prompts ───────────────────────────────────────────────────
_r("prompt.analyze", (
"Ты — финансовый AI-ассистент. Проанализируй финансовую модель пользователя.\n"
"\n"
"### Модель (JSON):\n"
"{model_json}\n"
"\n"
"### Прогноз на {months} месяцев:\n"
"{forecast_json}\n"
"\n"
"### Валюта:\n"
"Базовая валюта модели: {base_currency}\n"
"Отображаемая валюта: {display_currency}\n"
"\n"
"Дай анализ по пунктам:\n"
"1. Общее финансовое состояние\n"
"2. Тренд денежного потока (рост/падение)\n"
"3. Достаточность ликвидности\n"
"4. Рекомендации по улучшению"
))
_r("prompt.advice", (
"Ты — финансовый AI-ассистент. Дай персональные рекомендации.\n"
"\n"
"### Модель:\n"
"{model_json}\n"
"\n"
"### Прогноз:\n"
"{forecast_json}\n"
"\n"
"### Валюта:\n"
"Базовая валюта модели: {base_currency}\n"
"Отображаемая валюта: {display_currency}\n"
"\n"
"Вопрос пользователя: {question}\n"
"\n"
"Ответь как опытный финансовый консультант."
))
_r("prompt.scenario_comparison", (
"Ты — финансовый AI-ассистент. Сравни сценарии прогноза.\n"
"\n"
"### Результаты сценариев:\n"
"{scenarios_json}\n"
"\n"
"### Валюта:\n"
"Базовая валюта модели: {base_currency}\n"
"Отображаемая валюта: {display_currency}\n"
"\n"
"Дай рекомендацию: какой сценарий наиболее вероятен и почему."
))
_current_lang = "ru"
class Translator:
def __init__(self, lang: str | None = None):
self.lang = lang or _current_lang
if self.lang not in _TRANSLATIONS:
_TRANSLATIONS[self.lang] = {}
def t(self, key: str, **kwargs) -> str:
src = _TRANSLATIONS.get(self.lang) or _TRANSLATIONS.get("ru") or {}
if key in src:
template = src[key]
elif "ru" in _TRANSLATIONS and key in _TRANSLATIONS["ru"]:
template = _TRANSLATIONS["ru"][key]
else:
return key
return template.format(**kwargs) if kwargs else template
_GLOBAL = Translator()
def setup_i18n() -> None:
lang = os.environ.get("CF_LANG", "ru")
if lang not in _TRANSLATIONS:
_TRANSLATIONS[lang] = {}
_GLOBAL.lang = lang
def set_lang(lang: str) -> None:
if lang not in _TRANSLATIONS:
_TRANSLATIONS[lang] = {}
_GLOBAL.lang = lang
def get_lang() -> str:
return _GLOBAL.lang
def t(key: str, **kwargs) -> str:
return _GLOBAL.t(key, **kwargs)
+195 -66
View File
@@ -6,7 +6,9 @@ from rich.console import Console
from rich.table import Table from rich.table import Table
from ai.assistant import AssistantService from ai.assistant import AssistantService
from cashflow_model import FinancialModel from cashflow_model import CurrencyConverter, FinancialModel
from cli.config import app as config_app
from cli.i18n import setup_i18n, t
from engine.forecast import ForecastService from engine.forecast import ForecastService
from engine.scenarios import DEFAULT_SCENARIOS, ScenarioService from engine.scenarios import DEFAULT_SCENARIOS, ScenarioService
from sync.excel_sync import ExcelSync from sync.excel_sync import ExcelSync
@@ -16,12 +18,16 @@ try:
except (AttributeError, OSError): except (AttributeError, OSError):
pass pass
app = typer.Typer(name="cf", help="CashFlow Forecast - personal finance model") setup_i18n()
app = typer.Typer(name="cf", help=t("app.help"))
console = Console() console = Console()
DATA_DIR = Path("data") DATA_DIR = Path("data")
MODEL_PATH = DATA_DIR / "model.json" MODEL_PATH = DATA_DIR / "model.json"
CURRENCY_OPTION = typer.Option(None, "--currency", "-c", help=t("cmd.forecast.opt.currency"))
def _load_model() -> FinancialModel: def _load_model() -> FinancialModel:
if MODEL_PATH.exists(): if MODEL_PATH.exists():
@@ -33,169 +39,292 @@ def _save_model(model: FinancialModel) -> None:
model.save(MODEL_PATH) model.save(MODEL_PATH)
def _get_converter(model: FinancialModel) -> CurrencyConverter:
return CurrencyConverter(model.exchange_rates)
def _fmt(amount: float, currency: str, symbol: str) -> str:
return f"{symbol}{amount:,.2f}"
def _resolve_currency(model: FinancialModel, currency: str | None) -> tuple[str, str]:
target = currency or model.base_currency
converter = _get_converter(model)
symbol = converter.get_symbol(target)
return target, symbol
def _convert_value(
converter: CurrencyConverter, amount: float, from_curr: str, to_curr: str
) -> float:
if to_curr == from_curr:
return amount
return converter.convert(amount, from_curr, to_curr)
@app.command() @app.command()
def init() -> None: def init() -> None:
"""Создать пустую финансовую модель""" """Создать пустую финансовую модель"""
model = FinancialModel() model = FinancialModel()
_save_model(model) _save_model(model)
console.print("[green]OK[/green] Пустая модель создана в data/model.json") console.print(t("cmd.init.ok", path=str(MODEL_PATH)))
@app.command() @app.command()
def forecast( def forecast(
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев прогноза"), months: int = typer.Option(12, "--months", "-m", help=t("cmd.forecast.opt.months")),
currency: str | None = CURRENCY_OPTION,
) -> None: ) -> None:
"""Запустить прогноз денежных потоков""" """Запустить прогноз денежных потоков"""
model = _load_model() model = _load_model()
target_curr, symbol = _resolve_currency(model, currency)
converter = _get_converter(model)
base_curr = model.base_currency
service = ForecastService(model) service = ForecastService(model)
results = service.forecast_cashflow(months) results = service.forecast_cashflow(months)
summary = service.summary(months) summary = service.summary(months)
def _cv(val):
return _convert_value(converter, val, base_curr, target_curr)
def _f(val):
return _fmt(val, target_curr, symbol)
if results: if results:
table = Table(title=f"Прогноз на {months} мес.") table = Table(title=t("cmd.forecast.title", months=months, currency=target_curr))
table.add_column("Счёт", style="cyan") table.add_column(t("cmd.forecast.col.account"), style="cyan")
table.add_column("Месяц", style="white") table.add_column(t("cmd.forecast.col.month"), style="white")
table.add_column("Баланс", justify="right", style="green") table.add_column(t("cmd.forecast.col.balance"), justify="right", style="green")
table.add_column("Доход", justify="right") table.add_column(t("cmd.forecast.col.income"), justify="right")
table.add_column("Расход", justify="right") table.add_column(t("cmd.forecast.col.expenses"), justify="right")
for r in results: for r in results:
table.add_row( table.add_row(
r["account"], str(r["month"]), r["account"], str(r["month"]),
f"${r['balance']:.2f}", _f(_cv(r["balance"])),
f"${r['income']:.2f}", _f(_cv(r["income"])),
f"${r['expenses']:.2f}", _f(_cv(r["expenses"])),
) )
console.print(table) console.print(table)
console.print(f"\n[bold]Итог:[/bold] Баланс: ${summary['total_balance']:.2f} | " cb = _cv(summary["total_balance"])
f"Доход: ${summary['total_income']:.2f} | " ci = _cv(summary["total_income"])
f"Расход: ${summary['total_expenses']:.2f}") ce = _cv(summary["total_expenses"])
console.print(t("cmd.forecast.total", balance=_f(cb), income=_f(ci), expenses=_f(ce)))
@app.command() @app.command()
def scenario( def scenario(
name: str = typer.Argument("baseline", help="Имя сценария: baseline, optimistic, pessimistic"), name: str = typer.Argument("baseline", help=t("cmd.scenario.arg.name")),
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"), months: int = typer.Option(12, "--months", "-m", help=t("cmd.scenario.opt.months")),
currency: str | None = CURRENCY_OPTION,
) -> None: ) -> None:
"""Применить сценарий и показать прогноз""" """Применить сценарий и показать прогноз"""
model = _load_model() model = _load_model()
target_curr, symbol = _resolve_currency(model, currency)
converter = _get_converter(model)
base_curr = model.base_currency
service = ScenarioService(model) service = ScenarioService(model)
if name in DEFAULT_SCENARIOS: if name in DEFAULT_SCENARIOS:
scenario_obj = DEFAULT_SCENARIOS[name] scenario_obj = DEFAULT_SCENARIOS[name]
else: else:
console.print(f"[red]Неизвестный сценарий: {name}[/red]") console.print(f"[red]{t('cmd.scenario.err.unknown', name=name)}[/red]")
console.print(f"Доступны: {', '.join(DEFAULT_SCENARIOS.keys())}") console.print(t("cmd.scenario.err.available", scenarios=", ".join(DEFAULT_SCENARIOS.keys())))
raise typer.Exit(1) raise typer.Exit(1)
result = service.apply(scenario_obj, months) result = service.apply(scenario_obj, months)
console.print(f"[bold]Сценарий:[/bold] {result['scenario']}") balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
income = _convert_value(converter, result["total_income"], base_curr, target_curr)
expenses = _convert_value(converter, result["total_expenses"], base_curr, target_curr)
console.print(f"[bold]{t('cmd.scenario.label')}:[/bold] {result['scenario']}")
console.print(f"[dim]{result['scenario_description']}[/dim]") console.print(f"[dim]{result['scenario_description']}[/dim]")
console.print(f"Баланс: ${result['total_balance']:.2f}") console.print(f"{t('cmd.scenario.label.balance')}: {_fmt(balance, target_curr, symbol)}")
console.print(f"Доход: ${result['total_income']:.2f}") console.print(f"{t('cmd.scenario.label.income')}: {_fmt(income, target_curr, symbol)}")
console.print(f"Расход: ${result['total_expenses']:.2f}") console.print(f"{t('cmd.scenario.label.expenses')}: {_fmt(expenses, target_curr, symbol)}")
@app.command() @app.command()
def whatif( def whatif(
income_mult: float = typer.Option(1.0, "--income", "-i", help="Множитель дохода"), income_mult: float = typer.Option(1.0, "--income", "-i", help=t("cmd.whatif.opt.income")),
expense_mult: float = typer.Option(1.0, "--expense", "-e", help="Множитель расхода"), expense_mult: float = typer.Option(1.0, "--expense", "-e", help=t("cmd.whatif.opt.expense")),
growth_mult: float = typer.Option(1.0, "--growth", "-g", help="Множитель роста активов"), growth_mult: float = typer.Option(1.0, "--growth", "-g", help=t("cmd.whatif.opt.growth")),
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"), months: int = typer.Option(12, "--months", "-m", help=t("cmd.whatif.opt.months")),
currency: str | None = CURRENCY_OPTION,
) -> None: ) -> None:
"""What-if анализ с произвольными множителями""" """What-if анализ с произвольными множителями"""
model = _load_model() model = _load_model()
target_curr, symbol = _resolve_currency(model, currency)
converter = _get_converter(model)
base_curr = model.base_currency
service = ScenarioService(model) service = ScenarioService(model)
result = service.what_if(income_mult, expense_mult, growth_mult, months) result = service.what_if(income_mult, expense_mult, growth_mult, months)
console.print("[bold]What-if анализ[/bold]") balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
console.print(f"Доход x{income_mult} | Расход x{expense_mult} | Рост x{growth_mult}") income = _convert_value(converter, result["total_income"], base_curr, target_curr)
console.print(f"Баланс: ${result['total_balance']:.2f}") expenses = _convert_value(converter, result["total_expenses"], base_curr, target_curr)
console.print(f"Доход: ${result['total_income']:.2f}") console.print(f"[bold]{t('cmd.whatif.title')}[/bold]")
console.print(f"Расход: ${result['total_expenses']:.2f}") console.print(t("cmd.whatif.params", income=income_mult, expense=expense_mult, growth=growth_mult))
console.print(f"{t('cmd.scenario.label.balance')}: {_fmt(balance, target_curr, symbol)}")
console.print(f"{t('cmd.scenario.label.income')}: {_fmt(income, target_curr, symbol)}")
console.print(f"{t('cmd.scenario.label.expenses')}: {_fmt(expenses, target_curr, symbol)}")
@app.command() @app.command()
def compare( def compare(
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"), months: int = typer.Option(12, "--months", "-m", help=t("cmd.compare.opt.months")),
currency: str | None = CURRENCY_OPTION,
) -> None: ) -> None:
"""Сравнить все сценарии""" """Сравнить все сценарии"""
model = _load_model() model = _load_model()
target_curr, symbol = _resolve_currency(model, currency)
converter = _get_converter(model)
base_curr = model.base_currency
service = ScenarioService(model) service = ScenarioService(model)
results = service.compare(months) results = service.compare(months)
table = Table(title="Сравнение сценариев") table = Table(title=t("cmd.compare.title", currency=target_curr))
table.add_column("Сценарий", style="cyan") table.add_column(t("cmd.compare.col.scenario"), style="cyan")
table.add_column("Баланс", justify="right") table.add_column(t("cmd.forecast.col.balance"), justify="right")
table.add_column("Доход", justify="right") table.add_column(t("cmd.forecast.col.income"), justify="right")
table.add_column("Расход", justify="right") table.add_column(t("cmd.forecast.col.expenses"), justify="right")
for name, r in results.items(): for name, r in results.items():
table.add_row( b = _convert_value(converter, r["total_balance"], base_curr, target_curr)
name, i = _convert_value(converter, r["total_income"], base_curr, target_curr)
f"${r['total_balance']:.2f}", e = _convert_value(converter, r["total_expenses"], base_curr, target_curr)
f"${r['total_income']:.2f}", fb = _fmt(b, target_curr, symbol)
f"${r['total_expenses']:.2f}", fi = _fmt(i, target_curr, symbol)
) fe = _fmt(e, target_curr, symbol)
table.add_row(name, fb, fi, fe)
console.print(table) console.print(table)
@app.command() @app.command()
def import_xlsx( def import_xlsx(
path: str = typer.Argument(..., help="Путь к .xlsx файлу"), path: str = typer.Argument(..., help=t("cmd.import.arg.path")),
) -> None: ) -> None:
"""Импорт данных из Excel""" """Импорт данных из Excel"""
sync = ExcelSync() sync = ExcelSync()
try: try:
model = sync.import_model(path) model = sync.import_model(path)
_save_model(model) _save_model(model)
console.print(f"[green]OK[/green] Импортировано: {len(model.accounts)} счетов, " console.print(t("cmd.import.ok",
f"{len(model.transactions)} транзакций, " accounts=len(model.accounts),
f"{len(model.recurring)} регулярных платежей, " transactions=len(model.transactions),
f"{len(model.assets)} активов, " recurring=len(model.recurring),
f"{len(model.liabilities)} обязательств") assets=len(model.assets),
liabilities=len(model.liabilities)))
console.print(t("cmd.import.base_currency", currency=model.base_currency))
except Exception as e: except Exception as e:
console.print(f"[red]Ошибка импорта: {e}[/red]") console.print(f"[red]{t('cmd.import.err', error=str(e))}[/red]")
raise typer.Exit(1) raise typer.Exit(1)
@app.command() @app.command()
def export_xlsx( def export_xlsx(
path: str = typer.Argument("exports/forecast.xlsx", help="Путь для .xlsx файла"), path: str = typer.Argument("exports/forecast.xlsx", help=t("cmd.export.arg.path")),
) -> None: ) -> None:
"""Экспорт модели в Excel""" """Экспорт модели в Excel"""
model = _load_model() model = _load_model()
sync = ExcelSync() sync = ExcelSync()
try: try:
sync.export_model(model, path) sync.export_model(model, path)
console.print(f"[green]OK[/green] Модель экспортирована в {path}") console.print(t("cmd.export.ok", path=path))
except Exception as e: except Exception as e:
console.print(f"[red]Ошибка экспорта: {e}[/red]") console.print(f"[red]{t('cmd.export.err', error=str(e))}[/red]")
raise typer.Exit(1) raise typer.Exit(1)
@app.command() @app.command()
def analyze( def analyze(
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев для анализа"), months: int = typer.Option(12, "--months", "-m", help=t("cmd.analyze.opt.months")),
currency: str | None = CURRENCY_OPTION,
) -> None: ) -> None:
"""AI-анализ финансовой модели""" """AI-анализ финансовой модели"""
model = _load_model() model = _load_model()
assistant = AssistantService(model) target_curr, _ = _resolve_currency(model, currency)
converter = _get_converter(model)
assistant = AssistantService(model, converter=converter, display_currency=target_curr)
result = assistant.analyze(months) result = assistant.analyze(months)
console.print("[bold]Промпт для AI:[/bold]") console.print(f"[bold]{t('cmd.analyze.title.prompt')}:[/bold]")
console.print(result["prompt"][:500] + "...\n") console.print(result["prompt"][:500] + "...\n")
console.print("[bold]Сводка:[/bold]") console.print(f"[bold]{t('cmd.analyze.title.summary')}:[/bold]")
s = result["summary"] console.print(t("cmd.analyze.base_currency", currency=model.base_currency))
console.print(f"Баланс: ${s['total_balance']:.2f}") console.print(f"\n[yellow]{t('cmd.analyze.stub')}[/yellow]")
console.print(f"Доход: ${s['total_income']:.2f}")
console.print(f"Расход: ${s['total_expenses']:.2f}")
console.print(
"\n[yellow]AI-ответ: заглушка. Подключите реальный API в ai/assistant.py[/yellow]"
)
@app.command()
def info() -> None:
"""Сводка всей финансовой модели"""
model = _load_model()
converter = _get_converter(model)
symbol = converter.get_symbol(model.base_currency)
console.print(f"[bold]{t('cmd.info.metadata')}[/bold]")
console.print(f" {t('cmd.info.base_currency', currency=model.base_currency, symbol=symbol)}")
console.print(f" {t('cmd.info.accounts', count=len(model.accounts))}")
console.print(f" {t('cmd.info.transactions', count=len(model.transactions))}")
console.print(f" {t('cmd.info.recurring', count=len(model.recurring))}")
console.print(f" {t('cmd.info.assets', count=len(model.assets))}")
console.print(f" {t('cmd.info.liabilities', count=len(model.liabilities))}")
console.print(f" {t('cmd.info.rates', count=len(model.exchange_rates))}")
if model.accounts:
tbl = Table(title=t("table.accounts.title"))
tbl.add_column(t("table.accounts.col.name"), style="cyan")
tbl.add_column(t("table.accounts.col.currency"))
tbl.add_column(t("table.accounts.col.balance"), justify="right")
for a in model.accounts:
tbl.add_row(a.name, a.currency, f"{a.balance:,.2f}")
console.print(tbl)
if model.transactions:
tbl = Table(title=t("table.transactions.title"))
tbl.add_column(t("table.transactions.col.date"))
tbl.add_column(t("table.transactions.col.category"))
tbl.add_column(t("table.transactions.col.amount"), justify="right")
tbl.add_column(t("table.transactions.col.description"))
for tx in model.transactions[:10]:
tbl.add_row(tx.date, tx.category, f"{tx.amount:,.2f}", tx.description)
if len(model.transactions) > 10:
console.print(tbl)
console.print(t("cmd.info.more", count=len(model.transactions) - 10))
else:
console.print(tbl)
if model.recurring:
tbl = Table(title=t("table.recurring.title"))
tbl.add_column(t("table.recurring.col.period"))
tbl.add_column(t("table.recurring.col.amount"), justify="right")
tbl.add_column(t("table.recurring.col.category"))
for r in model.recurring:
tbl.add_row(r.frequency, f"{r.amount:,.2f}", r.category)
console.print(tbl)
if model.assets:
tbl = Table(title=t("table.assets.title"))
tbl.add_column(t("table.assets.col.name"), style="cyan")
tbl.add_column(t("table.assets.col.value"), justify="right")
tbl.add_column(t("table.assets.col.growth"), justify="right")
for a in model.assets:
tbl.add_row(a.name, f"{a.value:,.2f}", f"{a.growth_rate:.1f}%")
console.print(tbl)
if model.liabilities:
tbl = Table(title=t("table.liabilities.title"))
tbl.add_column(t("table.liabilities.col.name"), style="cyan")
tbl.add_column(t("table.liabilities.col.balance"), justify="right")
tbl.add_column(t("table.liabilities.col.rate"), justify="right")
tbl.add_column(t("table.liabilities.col.payment"), justify="right")
for li in model.liabilities:
tbl.add_row(li.name, f"{li.balance:,.2f}", f"{li.interest:.1f}%", f"{li.payment:,.2f}")
console.print(tbl)
app.add_typer(config_app)
if __name__ == "__main__": if __name__ == "__main__":
app() app()
BIN
View File
Binary file not shown.
+95 -6
View File
@@ -1,8 +1,97 @@
{ {
"accounts": [], "base_currency": "RUB",
"transactions": [], "accounts": [
"recurring": [], {
"assets": [], "id": "cfff5fa0-d42d-46d8-81ec-f460234e4123",
"liabilities": [], "name": "Основной счёт",
"scenarios": [] "currency": "RUB",
"balance": 2000
}
],
"transactions": [
],
"recurring": [
{
"id": "479a9960-d7ec-476b-9621-888ceaa1b671",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": 30000,
"category": "Зарплата"
},
{
"id": "eb565346-679b-4560-89d1-7b0c80bbcb5a",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -10000,
"category": "Аренда"
},
{
"id": "30c168a3-cbe0-4d8e-a842-d430d98a80b6",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -3000,
"category": "Коммуналка"
},
{
"id": "f37ef9d3-76a8-4eb3-9a12-e20817d87079",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -8000,
"category": "Продукты"
},
{
"id": "f8ef073e-a8ca-4637-bd63-e4faeb1b282f",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -2000,
"category": "Транспорт"
},
{
"id": "ae8f6ad5-2c66-40d9-b102-64076e9be978",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -500,
"category": "Телефон"
},
{
"id": "fb1197de-727e-49db-9fb8-c5b2554b0504",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -500,
"category": "Подписки"
},
{
"id": "d8e3392a-4015-4920-b27c-e72f99e6bbb7",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -3000,
"category": "Досуг"
}
],
"assets": [
{
"id": "bd5080c0-45a9-40b1-9dc4-c1ec41157bd7",
"name": "Вклад в банке",
"value": 35000,
"growth_rate": 12
}
],
"liabilities": [
],
"scenarios": [],
"exchange_rates": [
{
"from_currency": "USD",
"to_currency": "RUB",
"rate": 80
}
]
} }
+24 -21
View File
@@ -17,17 +17,33 @@ class ForecastService:
results = [] results = []
for account in self.model.accounts: for account in self.model.accounts:
balance = account.balance
monthly = self._project_account(account, months) monthly = self._project_account(account, months)
for m in range(months): for m in range(months):
balance = monthly[m]["balance"]
results.append({ results.append({
"account": account.name, "account": account.name,
"month": m + 1, "month": m + 1,
"balance": round(balance, 2), "balance": monthly[m]["balance"],
"income": round(monthly[m]["income"], 2), "income": monthly[m]["income"],
"expenses": round(monthly[m]["expenses"], 2), "expenses": monthly[m]["expenses"],
}) })
# Asset growth — once per month, distributed across accounts proportionally
for m in range(months):
total_growth = sum(
a.value * a.growth_rate / 100 / 12
for a in self.model.assets
)
month_rows = [r for r in results if r["month"] == m + 1]
total_bal = sum(r["balance"] for r in month_rows) or 1
for r in month_rows:
share = r["balance"] / total_bal
r["income"] = round(r["income"] + total_growth * share, 2)
r["balance"] = round(r["balance"] + total_growth * share, 2)
# Compound asset values for next month
for a in self.model.assets:
a.value += a.value * a.growth_rate / 100 / 12
return results return results
def _project_account(self, account: Account, months: int) -> list[dict]: def _project_account(self, account: Account, months: int) -> list[dict]:
@@ -50,30 +66,17 @@ class ForecastService:
else: else:
expenses += abs(r.amount) expenses += abs(r.amount)
income += self._asset_income(account)
expenses += self._liability_cost(account) expenses += self._liability_cost(account)
balance += income - expenses balance += income - expenses
asset_growth = sum(
a.value * a.growth_rate / 12
for a in self.model.assets
)
balance += asset_growth
monthly.append({ monthly.append({
"balance": balance, "balance": round(balance, 2),
"income": income, "income": round(income, 2),
"expenses": expenses, "expenses": round(expenses, 2),
}) })
return monthly return monthly
def _asset_income(self, account: Account) -> float:
return sum(
a.value * a.growth_rate / 12
for a in self.model.assets
)
def _liability_cost(self, account: Account) -> float: def _liability_cost(self, account: Account) -> float:
total = 0.0 total = 0.0
for liability in self.model.liabilities: for liability in self.model.liabilities:
+1 -1
View File
@@ -21,7 +21,7 @@ include = ["cashflow_model*", "sync*", "engine*", "ai*", "cli*"]
[tool.ruff] [tool.ruff]
target-version = "py311" target-version = "py311"
line-length = 100 line-length = 120
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "I", "N", "W"] select = ["E", "F", "I", "N", "W"]
+28 -1
View File
@@ -3,7 +3,15 @@ from uuid import UUID
from openpyxl import Workbook, load_workbook from openpyxl import Workbook, load_workbook
from cashflow_model import Account, Asset, FinancialModel, Liability, RecurringCashflow, Transaction from cashflow_model import (
Account,
Asset,
ExchangeRate,
FinancialModel,
Liability,
RecurringCashflow,
Transaction,
)
class SyncError(Exception): class SyncError(Exception):
@@ -31,6 +39,10 @@ _SHEET_CONFIG = {
"fields": ["id", "name", "balance", "interest", "payment"], "fields": ["id", "name", "balance", "interest", "payment"],
"cls": Liability, "cls": Liability,
}, },
"ExchangeRates": {
"fields": ["from_currency", "to_currency", "rate"],
"cls": ExchangeRate,
},
} }
@@ -42,6 +54,7 @@ class ExcelSync:
wb = load_workbook(path, read_only=True, data_only=True) wb = load_workbook(path, read_only=True, data_only=True)
model = FinancialModel() model = FinancialModel()
model.exchange_rates = []
for sheet_name, config in _SHEET_CONFIG.items(): for sheet_name, config in _SHEET_CONFIG.items():
if sheet_name not in wb.sheetnames: if sheet_name not in wb.sheetnames:
@@ -62,6 +75,13 @@ class ExcelSync:
data[header] = str(val) if not isinstance(val, (int, float)) else val data[header] = str(val) if not isinstance(val, (int, float)) else val
self._add_to_model(model, sheet_name, data) self._add_to_model(model, sheet_name, data)
if "ModelInfo" in wb.sheetnames:
ws = wb["ModelInfo"]
rows = list(ws.iter_rows(values_only=True))
for row in rows:
if row[0] and str(row[0]).strip().lower() == "base_currency" and len(row) > 1:
model.base_currency = str(row[1]).strip()
wb.close() wb.close()
return model return model
@@ -76,6 +96,7 @@ class ExcelSync:
"Recurring": model.recurring, "Recurring": model.recurring,
"Assets": model.assets, "Assets": model.assets,
"Liabilities": model.liabilities, "Liabilities": model.liabilities,
"ExchangeRates": model.exchange_rates,
} }
for sheet_name, items in collections.items(): for sheet_name, items in collections.items():
@@ -90,6 +111,10 @@ class ExcelSync:
] ]
ws.append(row) ws.append(row)
ws_info = wb.create_sheet(title="ModelInfo")
ws_info.append(["Property", "Value"])
ws_info.append(["base_currency", model.base_currency])
wb.save(path) wb.save(path)
def _add_to_model(self, model: FinancialModel, sheet_name: str, data: dict) -> None: def _add_to_model(self, model: FinancialModel, sheet_name: str, data: dict) -> None:
@@ -104,5 +129,7 @@ class ExcelSync:
model.assets.append(Asset.from_dict(data)) model.assets.append(Asset.from_dict(data))
elif sheet_name == "Liabilities": elif sheet_name == "Liabilities":
model.liabilities.append(Liability.from_dict(data)) model.liabilities.append(Liability.from_dict(data))
elif sheet_name == "ExchangeRates":
model.exchange_rates.append(ExchangeRate.from_dict(data))
except Exception as e: except Exception as e:
raise SyncError(f"Failed to parse row in {sheet_name}: {e}") from e raise SyncError(f"Failed to parse row in {sheet_name}: {e}") from e
+13 -2
View File
@@ -5,6 +5,8 @@ import pytest
from cashflow_model import ( from cashflow_model import (
Account, Account,
Asset, Asset,
CurrencyConverter,
ExchangeRate,
FinancialModel, FinancialModel,
Liability, Liability,
RecurringCashflow, RecurringCashflow,
@@ -16,9 +18,10 @@ from cashflow_model import (
def sample_model() -> FinancialModel: def sample_model() -> FinancialModel:
acc_id = uuid4() acc_id = uuid4()
return FinancialModel( return FinancialModel(
base_currency="RUB",
accounts=[ accounts=[
Account(id=acc_id, name="Основной счёт", currency="USD", balance=5000.0), Account(id=acc_id, name="Основной счёт", currency="RUB", balance=5000.0),
Account(name="Сбережения", currency="USD", balance=10000.0), Account(name="Сбережения", currency="RUB", balance=10000.0),
], ],
transactions=[ transactions=[
Transaction( Transaction(
@@ -48,9 +51,17 @@ def sample_model() -> FinancialModel:
liabilities=[ liabilities=[
Liability(name="Кредит", balance=20000.0, interest=5.0, payment=500.0), Liability(name="Кредит", balance=20000.0, interest=5.0, payment=500.0),
], ],
exchange_rates=[
ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0),
],
) )
@pytest.fixture @pytest.fixture
def empty_model() -> FinancialModel: def empty_model() -> FinancialModel:
return FinancialModel() return FinancialModel()
@pytest.fixture
def sample_converter() -> CurrencyConverter:
return CurrencyConverter([ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0)])
+144 -1
View File
@@ -1,21 +1,35 @@
from typer.testing import CliRunner from typer.testing import CliRunner
from cli.main import app from cli.main import MODEL_PATH, app
runner = CliRunner() runner = CliRunner()
def _cleanup():
if MODEL_PATH.exists():
MODEL_PATH.unlink()
class TestCli: class TestCli:
def test_init(self): def test_init(self):
_cleanup()
result = runner.invoke(app, ["init"]) result = runner.invoke(app, ["init"])
assert result.exit_code == 0 assert result.exit_code == 0
assert "Пустая модель" in result.stdout assert "Пустая модель" in result.stdout
def test_forecast_after_init(self): def test_forecast_after_init(self):
_cleanup()
runner.invoke(app, ["init"]) runner.invoke(app, ["init"])
result = runner.invoke(app, ["forecast", "--months", "3"]) result = runner.invoke(app, ["forecast", "--months", "3"])
assert result.exit_code == 0 assert result.exit_code == 0
def test_forecast_with_currency(self):
_cleanup()
runner.invoke(app, ["init"])
result = runner.invoke(app, ["forecast", "--months", "3", "--currency", "USD"])
assert result.exit_code == 0
assert "$" in result.stdout
def test_unknown_scenario(self): def test_unknown_scenario(self):
result = runner.invoke(app, ["scenario", "unknown"]) result = runner.invoke(app, ["scenario", "unknown"])
assert result.exit_code != 0 assert result.exit_code != 0
@@ -23,3 +37,132 @@ class TestCli:
def test_help(self): def test_help(self):
result = runner.invoke(app, ["--help"]) result = runner.invoke(app, ["--help"])
assert result.exit_code == 0 assert result.exit_code == 0
def test_info_empty(self):
_cleanup()
runner.invoke(app, ["init"])
result = runner.invoke(app, ["info"])
assert result.exit_code == 0
assert "Базовая валюта" in result.stdout
def test_config_base_currency_set(self):
_cleanup()
runner.invoke(app, ["init"])
result = runner.invoke(app, ["config", "base-currency", "EUR"])
assert result.exit_code == 0
assert "EUR" in result.stdout
info = runner.invoke(app, ["info"])
assert "EUR" in info.stdout
def test_config_base_currency_show(self):
_cleanup()
runner.invoke(app, ["init"])
result = runner.invoke(app, ["config", "base-currency"])
assert result.exit_code == 0
assert "RUB" in result.stdout
def test_config_account_crud(self):
_cleanup()
runner.invoke(app, ["init"])
args = ["config", "account-add", "--name", "Тестовый", "--balance", "1000"]
add = runner.invoke(app, args)
assert add.exit_code == 0
assert "Тестовый" in add.stdout
lst = runner.invoke(app, ["config", "account-list"])
assert "Тестовый" in lst.stdout
remove = runner.invoke(app, ["config", "account-remove", "Тестовый"])
assert remove.exit_code == 0
lst2 = runner.invoke(app, ["config", "account-list"])
assert "Тестовый" not in lst2.stdout
def test_config_transaction_crud(self):
_cleanup()
runner.invoke(app, ["init"])
runner.invoke(app, ["config", "account-add", "--name", "Счёт", "--balance", "0"])
add = runner.invoke(app, [
"config", "transaction-add",
"--account", "Счёт",
"--amount", "5000",
"--category", "income",
"--description", "Зарплата",
])
assert add.exit_code == 0
assert "доход" in add.stdout
lst = runner.invoke(app, ["config", "transaction-list"])
assert "Зарплата" in lst.stdout
def test_config_asset_crud(self):
_cleanup()
runner.invoke(app, ["init"])
add = runner.invoke(app, [
"config", "asset-add",
"--name", "Квартира",
"--value", "5000000",
"--growth", "5",
])
assert add.exit_code == 0
assert "Квартира" in add.stdout
lst = runner.invoke(app, ["config", "asset-list"])
assert "Квартира" in lst.stdout
def test_config_liability_crud(self):
_cleanup()
runner.invoke(app, ["init"])
add = runner.invoke(app, [
"config", "liability-add",
"--name", "Кредит",
"--balance", "100000",
"--interest", "10",
"--payment", "5000",
])
assert add.exit_code == 0
assert "Кредит" in add.stdout
def test_config_recurring_crud(self):
_cleanup()
runner.invoke(app, ["init"])
add = runner.invoke(app, [
"config", "recurring-add",
"--amount", "-500",
"--category", "аренда",
"--frequency", "monthly",
])
assert add.exit_code == 0
lst = runner.invoke(app, ["config", "recurring-list"])
assert "аренда" in lst.stdout
def test_config_rate_crud(self):
_cleanup()
runner.invoke(app, ["init"])
set_r = runner.invoke(app, ["config", "rate-set", "EUR", "RUB", "90"])
assert set_r.exit_code == 0
lst = runner.invoke(app, ["config", "rate-list"])
assert "EUR" in lst.stdout
assert "90" in lst.stdout
def test_info_with_data(self):
_cleanup()
runner.invoke(app, ["init"])
acc_args = ["config", "account-add", "--name", "Основной", "--balance", "50000"]
runner.invoke(app, acc_args)
asset_args = ["config", "asset-add", "--name", "Акции",
"--value", "100000", "--growth", "8.0"]
runner.invoke(app, asset_args)
result = runner.invoke(app, ["info"])
assert "Основной" in result.stdout
assert "Акции" in result.stdout
+72
View File
@@ -0,0 +1,72 @@
import pytest
from cashflow_model import CurrencyConverter, CurrencyError, ExchangeRate
class TestExchangeRate:
def test_to_dict_roundtrip(self):
rate = ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0)
d = rate.to_dict()
r2 = ExchangeRate.from_dict(d)
assert r2.from_currency == "USD"
assert r2.to_currency == "RUB"
assert r2.rate == 80.0
def test_defaults(self):
r = ExchangeRate()
assert r.from_currency == "USD"
assert r.to_currency == "RUB"
assert r.rate == 80.0
class TestCurrencyConverter:
def test_convert_usd_to_rub(self, sample_converter):
result = sample_converter.convert(100, "USD", "RUB")
assert result == 8000.0
def test_convert_rub_to_usd(self, sample_converter):
result = sample_converter.convert(8000, "RUB", "USD")
assert result == 100.0
def test_same_currency(self, sample_converter):
result = sample_converter.convert(500, "USD", "USD")
assert result == 500.0
def test_unknown_pair(self):
converter = CurrencyConverter()
with pytest.raises(CurrencyError):
converter.convert(100, "USD", "RUB")
def test_negative_rate(self):
converter = CurrencyConverter()
with pytest.raises(CurrencyError):
converter.set_rate("USD", "RUB", -1)
def test_zero_rate(self):
converter = CurrencyConverter()
with pytest.raises(CurrencyError):
converter.set_rate("USD", "RUB", 0)
def test_inverse_auto(self):
rate = ExchangeRate(from_currency="EUR", to_currency="RUB", rate=90.0)
converter = CurrencyConverter([rate])
assert converter.convert(900, "RUB", "EUR") == 10.0
def test_get_symbol(self, sample_converter):
assert sample_converter.get_symbol("USD") == "$"
assert sample_converter.get_symbol("RUB") == ""
assert sample_converter.get_symbol("XYZ") == "XYZ"
def test_with_defaults(self):
converter = CurrencyConverter.with_defaults()
assert converter.convert(10, "USD", "RUB") == 800.0
def test_multiple_rates(self):
rates = [
ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0),
ExchangeRate(from_currency="EUR", to_currency="RUB", rate=90.0),
]
converter = CurrencyConverter(rates)
assert converter.convert(10, "USD", "RUB") == 800.0
assert converter.convert(10, "EUR", "RUB") == 900.0
assert converter.convert(900, "RUB", "EUR") == 10.0
+10
View File
@@ -22,6 +22,16 @@ class TestExcelSync:
assert len(loaded.recurring) == len(sample_model.recurring) assert len(loaded.recurring) == len(sample_model.recurring)
assert len(loaded.assets) == len(sample_model.assets) assert len(loaded.assets) == len(sample_model.assets)
assert len(loaded.liabilities) == len(sample_model.liabilities) assert len(loaded.liabilities) == len(sample_model.liabilities)
assert len(loaded.exchange_rates) == len(sample_model.exchange_rates)
def test_roundtrip_preserves_base_currency(self, tmp_path: Path):
sync = ExcelSync()
model = FinancialModel(base_currency="EUR")
p = tmp_path / "eur_model.xlsx"
sync.export_model(model, p)
loaded = sync.import_model(p)
assert loaded.base_currency == "EUR"
def test_import_missing_file(self): def test_import_missing_file(self):
sync = ExcelSync() sync = ExcelSync()
+44
View File
@@ -0,0 +1,44 @@
from cli.i18n import Translator, get_lang, set_lang, setup_i18n, t
class TestTranslator:
def test_ru_by_default(self):
setup_i18n()
assert get_lang() == "ru"
def test_ru_returns_russian(self):
tr = Translator("ru")
assert "финансовую" in tr.t("cmd.init.help")
def test_en_fallback_to_ru(self):
tr = Translator("en")
result = tr.t("cmd.init.help")
assert "финансовую" in result
def test_unknown_key_returns_key(self):
tr = Translator("ru")
assert tr.t("nonexistent.key") == "nonexistent.key"
def test_set_lang(self):
setup_i18n()
set_lang("en")
assert get_lang() == "en"
def test_t_function(self):
setup_i18n()
result = t("cmd.init.ok", path="data/model.json")
assert "data/model.json" in result
assert "Пустая" in result
def test_global_ok(self):
result = t("global.ok")
assert result == "OK"
def test_prompt_analyze(self):
result = t("prompt.analyze")
assert "AI-ассистент" in result
def test_custom_lang_init(self):
tr = Translator("de")
result = tr.t("cmd.init.help")
assert "финансовую" in result # fallback to ru
+31
View File
@@ -3,6 +3,7 @@ from pathlib import Path
from cashflow_model import ( from cashflow_model import (
Account, Account,
Asset, Asset,
ExchangeRate,
FinancialModel, FinancialModel,
ForecastScenario, ForecastScenario,
Liability, Liability,
@@ -42,6 +43,32 @@ class TestTransaction:
class TestFinancialModel: class TestFinancialModel:
def test_default_base_currency(self):
model = FinancialModel()
assert model.base_currency == "RUB"
def test_base_currency_roundtrip(self, tmp_path: Path):
model = FinancialModel(base_currency="EUR")
p = tmp_path / "model.json"
model.save(p)
loaded = FinancialModel.load(p)
assert loaded.base_currency == "EUR"
def test_base_currency_backward_compat(self, tmp_path: Path):
import json
p = tmp_path / "legacy.json"
with open(p, "w") as f:
json.dump({"accounts": []}, f)
loaded = FinancialModel.load(p)
assert loaded.base_currency == "RUB"
def test_exchange_rates_default(self):
model = FinancialModel()
assert len(model.exchange_rates) == 1
assert model.exchange_rates[0].from_currency == "USD"
assert model.exchange_rates[0].to_currency == "RUB"
assert model.exchange_rates[0].rate == 80.0
def test_save_load(self, tmp_path: Path): def test_save_load(self, tmp_path: Path):
model = FinancialModel() model = FinancialModel()
model.accounts.append(Account(name="Test", balance=100.0)) model.accounts.append(Account(name="Test", balance=100.0))
@@ -61,6 +88,8 @@ class TestFinancialModel:
d = model.to_dict() d = model.to_dict()
assert d["accounts"] == [] assert d["accounts"] == []
assert d["transactions"] == [] assert d["transactions"] == []
assert d["base_currency"] == "RUB"
assert "exchange_rates" in d
def test_all_entities_roundtrip(self, tmp_path: Path): def test_all_entities_roundtrip(self, tmp_path: Path):
model = FinancialModel( model = FinancialModel(
@@ -70,6 +99,7 @@ class TestFinancialModel:
assets=[Asset(name="Stock", value=1000.0)], assets=[Asset(name="Stock", value=1000.0)],
liabilities=[Liability(name="Loan", balance=500.0, interest=5.0, payment=100.0)], liabilities=[Liability(name="Loan", balance=500.0, interest=5.0, payment=100.0)],
scenarios=[ForecastScenario(name="test")], scenarios=[ForecastScenario(name="test")],
exchange_rates=[ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0)],
) )
p = tmp_path / "full.json" p = tmp_path / "full.json"
model.save(p) model.save(p)
@@ -80,3 +110,4 @@ class TestFinancialModel:
assert len(loaded.assets) == 1 assert len(loaded.assets) == 1
assert len(loaded.liabilities) == 1 assert len(loaded.liabilities) == 1
assert len(loaded.scenarios) == 1 assert len(loaded.scenarios) == 1
assert len(loaded.exchange_rates) == 1