Compare commits
3
Commits
5afea8238c
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4a135b4e0 | ||
|
|
e9c7120323 | ||
|
|
3cc8e94863 |
+57
-35
@@ -2,55 +2,77 @@
|
|||||||
|
|
||||||
## Session
|
## Session
|
||||||
|
|
||||||
- **Session ID:** `metaagent-001`
|
- **Session ID:** `metaagent-002`
|
||||||
- **Target repo:** `S:\Git\nifodea`
|
- **Target repo:** `S:\Git\nifodea`
|
||||||
- **Date:** 2026-07-12
|
- **Date:** 2026-07-22
|
||||||
- **Project type:** `greenfield`
|
- **Project type:** `existing`
|
||||||
|
|
||||||
## 1. Общая информация
|
## 1. Общая информация
|
||||||
|
|
||||||
- **README:** CashFlow Forecast — open-source проект для построения и анализа личной финансовой модели с использованием Spreadsheet, Python и AI.
|
- **README:** Личная финансовая модель с прогнозом денежных потоков, сценарным анализом и AI-ассистентом. Python + JSON + Excel + AI.
|
||||||
- **Лицензия:** не указана
|
- **Лицензия:** не выбрана
|
||||||
- **CI/CD:** отсутствует
|
- **CI/CD:** отсутствует
|
||||||
- **Точка входа:** не определена
|
- **Точка входа:** `cli/main.py` (команда `cf`)
|
||||||
- **Система сборки:** отсутствует
|
- **Система сборки:** `pyproject.toml` (setuptools)
|
||||||
|
|
||||||
## 7. Требования (из README)
|
## 2. Стек технологий
|
||||||
|
|
||||||
### Функциональные требования
|
| Компонент | Значение |
|
||||||
|
|---|---|
|
||||||
|
| Язык | Python >= 3.11 |
|
||||||
|
| Фреймворк | Typer (CLI) |
|
||||||
|
| База данных | JSON-файлы |
|
||||||
|
| Тестовый раннер | pytest |
|
||||||
|
| Пакетный менеджер | pip (setuptools) |
|
||||||
|
| Линтер/форматтер | ruff |
|
||||||
|
|
||||||
- Прогноз денежных потоков (Cash Flow Forecasting)
|
## 3. Архитектура
|
||||||
- Моделирование бюджета
|
|
||||||
- Сценарный анализ (What-if Analysis)
|
|
||||||
- Учет активов и обязательств
|
|
||||||
- Прогноз ликвидности
|
|
||||||
- Анализ финансовой устойчивости
|
|
||||||
- Моделирование достижения финансовых целей
|
|
||||||
- Импорт/экспорт данных через Spreadsheet (Excel)
|
|
||||||
|
|
||||||
### Нефункциональные требования
|
```
|
||||||
|
cashflow_model/ # Модели данных (dataclass + JSON)
|
||||||
|
engine/ # Вычислительное ядро (forecast + scenarios)
|
||||||
|
sync/ # Excel import/export
|
||||||
|
ai/ # AI-ассистент (заглушка)
|
||||||
|
cli/ # CLI (Typer)
|
||||||
|
tests/ # pytest
|
||||||
|
data/ # JSON-модели
|
||||||
|
exports/ # Экспортированные .xlsx
|
||||||
|
```
|
||||||
|
|
||||||
- **Spreadsheet** — визуальный редактор и UI
|
**Паттерн:** Модульный монолит
|
||||||
- **Python** — вычислительное ядро
|
|
||||||
- **JSON** — внутреннее представление модели данных
|
|
||||||
- **AI** — инструмент анализа и взаимодействия
|
|
||||||
- Все компоненты должны быть взаимозаменяемыми
|
|
||||||
- Open-source
|
|
||||||
|
|
||||||
### Бизнес-контекст
|
**Ключевые модули:**
|
||||||
|
|
||||||
- **Цель:** отвечать на вопрос "Что произойдет дальше?" (forecast), а не "Что произошло?" (accounting)
|
| Модуль | Описание |
|
||||||
- **Аудитория:** частные лица для персонального финансового планирования
|
|---|---|
|
||||||
- **Успех:** работающая прогнозная модель денежных потоков с возможностью сценарного анализа
|
| cashflow_model | Модели данных: Account, Transaction, RecurringCashflow, Asset, Liability, ForecastScenario, FinancialModel |
|
||||||
|
| engine | Вычислительное ядро: ForecastService (прогноз), ScenarioService (сценарии + what-if) |
|
||||||
|
| sync | ExcelSync — импорт/экспорт .xlsx |
|
||||||
|
| ai | AssistantService — генерация промптов (заглушка) |
|
||||||
|
| cli | Typer CLI — 8 команд |
|
||||||
|
|
||||||
### Неясные моменты / Вопросы
|
## 4. Конвенции
|
||||||
|
|
||||||
- Не указана лицензия — требуется уточнить
|
- **Стиль:** snake_case для функций/переменных, PascalCase для классов
|
||||||
- Не указана конкретная версия Python
|
- **Импорты:** стандартные, сгруппированные
|
||||||
- Какой формат Spreadsheet (Excel .xlsx, Google Sheets, оба)?
|
- **Типизация:** используется (dataclass, type hints)
|
||||||
- Какие AI-провайдеры предполагаются (OpenAI, локальные модели)?
|
- **Обработка ошибок:** через исключения
|
||||||
- Требуется ли веб-интерфейс или только CLI + Spreadsheet?
|
- **Логирование:** не используется
|
||||||
|
|
||||||
|
## 5. Тесты
|
||||||
|
|
||||||
|
- **Команда запуска:** `pytest`
|
||||||
|
- **Всего тестов:** 26
|
||||||
|
- **Пройдено:** 26
|
||||||
|
- **Упало:** 0
|
||||||
|
- **Пропущено:** 0
|
||||||
|
|
||||||
|
## 6. Базовая проверка
|
||||||
|
|
||||||
|
- **Сборка:** OK (pip install -e .)
|
||||||
|
- **Линтер:** OK (ruff check . — all checks passed)
|
||||||
|
- **Git status:** есть незакоммиченные изменения (checkpoints.json, metaagent-request.md, AGENTS.md, data/model.json, .agent/rules/)
|
||||||
|
|
||||||
## 8. Примечания
|
## 8. Примечания
|
||||||
|
|
||||||
Проект greenfield. MVP должен включать: модель данных (JSON), ядро forecast engine, базовый sync с Excel, интерфейс для AI-ассистента. Все модули — Python.
|
Проект полностью функционален: 26 тестов проходят, линтер чист. Требуется только обновление MetaAgent-артефактов до v1.1.0.
|
||||||
|
|||||||
+21
-14
@@ -1,24 +1,31 @@
|
|||||||
{
|
{
|
||||||
"session_id": "metaagent-001",
|
"metaagent_version": "1.1.0",
|
||||||
|
"session_id": "metaagent-003",
|
||||||
"target_repo": "S:\\Git\\nifodea",
|
"target_repo": "S:\\Git\\nifodea",
|
||||||
"goal": "Спроектируй и реализуй MVP по README",
|
"goal": "i18n (ru/en) — инфраструктура, обёртка строк, контроль переводов",
|
||||||
"project_type": "greenfield",
|
"project_type": "existing",
|
||||||
|
"config": {
|
||||||
|
"depth": 4,
|
||||||
|
"design": { "adr": false, "alternative_arch": false },
|
||||||
|
"red_team": false,
|
||||||
|
"risk_register": false,
|
||||||
|
"decomposition": { "invariant_tests": false },
|
||||||
|
"handoff": { "layer_structure": false }
|
||||||
|
},
|
||||||
"phases": {
|
"phases": {
|
||||||
"analysis": "completed",
|
"analysis": "completed",
|
||||||
"design": "completed",
|
"design": "skipped",
|
||||||
|
"red_team": "skipped",
|
||||||
"decomposition": "completed",
|
"decomposition": "completed",
|
||||||
"environment": "completed",
|
"environment": "completed",
|
||||||
"handoff": "completed"
|
"handoff": "pending"
|
||||||
},
|
},
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{"id": "T1", "title": "Инициализация проекта и зависимостей", "status": "pending", "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": "pending", "depends_on": ["T1"], "acceptance_criteria": ["Все сущности dataclass", "FinancialModel save/load JSON", "demo-скрипт работает"]},
|
{ "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": "pending", "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": "pending", "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": "pending", "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": "pending", "depends_on": ["T2","T3","T4","T5","T7"], "acceptance_criteria": ["init/forecast/analyze/import/export/scenario команды"]},
|
|
||||||
{"id": "T7", "title": "AI Assistant", "status": "pending", "depends_on": ["T3"], "acceptance_criteria": ["промпты с моделью и прогнозом", "заглушка ответа"]},
|
|
||||||
{"id": "T8", "title": "Тесты", "status": "pending", "depends_on": ["T2","T3","T4","T5","T6","T7"], "acceptance_criteria": ["pytest проходит", "покрытие всех модулей"]}
|
|
||||||
],
|
],
|
||||||
"last_updated": "2026-07-12T19:36:00Z"
|
"last_updated": "2026-07-22T18:00:00Z"
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-33
@@ -2,36 +2,37 @@
|
|||||||
|
|
||||||
## Session Info
|
## Session Info
|
||||||
|
|
||||||
- **Session ID:** `metaagent-001`
|
- **Session ID:** `metaagent-002`
|
||||||
- **Target Repo:** `S:\Git\nifodea`
|
- **Target Repo:** `S:\Git\nifodea`
|
||||||
- **Goal:** Спроектируй и реализуй MVP по README
|
- **Goal:** Обновление metaagent-артефактов до v1.1.0, валидация существующего кода и окружения
|
||||||
- **Date:** 2026-07-12
|
- **Date:** 2026-07-22
|
||||||
- **Duration:** ~1 session
|
- **Depth:** 4 (Light)
|
||||||
|
- **Config:** depth=4, design=skipped (existing), red_team=no, risk_register=no, invariant_tests=no, layer_structure=no
|
||||||
|
|
||||||
## Repo Summary
|
## Repo Summary
|
||||||
|
|
||||||
Проект CashFlow Forecast — личная финансовая модель с прогнозом денежных потоков. Greenfield. Python + JSON + Excel + AI интерфейс.
|
CashFlow Forecast — личная финансовая модель на Python. Модульный монолит: cashflow_model (dataclass), engine (forecast + scenarios), sync (Excel), ai (заглушка), cli (Typer). 26 тестов, ruff lint чист.
|
||||||
|
|
||||||
## Project Type
|
## Project Type
|
||||||
|
|
||||||
- **Type:** greenfield
|
- **Type:** existing
|
||||||
- **Design report:** `.agent/design-report.md`
|
- **Design report:** —
|
||||||
|
|
||||||
## Environment Status
|
## Environment Status
|
||||||
|
|
||||||
- **Build:** OK (pip install -e . — success)
|
- **Build:** OK
|
||||||
- **Tests:** 0/0 passed (greenfield, scaffold ready)
|
- **Tests:** 26/26 passed
|
||||||
- **Baseline log:** `.agent/baseline-test-report.log`
|
- **Linter:** ruff — all checks passed
|
||||||
- **Dependencies:** openpyxl, typer, rich, pytest, ruff
|
- **Dependencies:** установлены (openpyxl, typer, rich)
|
||||||
|
|
||||||
## Task Overview
|
## Task Overview
|
||||||
|
|
||||||
| Status | Count |
|
| Status | Count |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Total | 8 |
|
| Total | 8 |
|
||||||
| Pending | 8 |
|
| Pending | 0 |
|
||||||
| In Progress | 0 |
|
| In Progress | 0 |
|
||||||
| Completed | 0 |
|
| Completed | 8 |
|
||||||
| Failed/Skipped | 0 |
|
| Failed/Skipped | 0 |
|
||||||
|
|
||||||
**Task by type:**
|
**Task by type:**
|
||||||
@@ -45,60 +46,60 @@
|
|||||||
- Type: config
|
- Type: config
|
||||||
- Depends on: —
|
- Depends on: —
|
||||||
- Files: pyproject.toml, cashflow_model/__init__.py, sync/__init__.py, engine/__init__.py, ai/__init__.py, cli/__init__.py, data/.gitkeep, exports/.gitkeep
|
- Files: pyproject.toml, cashflow_model/__init__.py, sync/__init__.py, engine/__init__.py, ai/__init__.py, cli/__init__.py, data/.gitkeep, exports/.gitkeep
|
||||||
- **Status: completed** (done in SETUP phase)
|
- Status: completed
|
||||||
|
|
||||||
### T2: Модель данных (dataclass + JSON serialization)
|
### T2: Модель данных (dataclass + JSON serialization)
|
||||||
- Type: feature
|
- Type: feature
|
||||||
- Depends on: T1
|
- Depends on: T1
|
||||||
- Files: cashflow_model/*.py
|
- Files: cashflow_model/__init__.py, 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
|
||||||
- Status: pending
|
- Status: completed
|
||||||
|
|
||||||
### T3: Forecast Engine (базовый прогноз)
|
### T3: Forecast Engine (базовый прогноз)
|
||||||
- Type: feature
|
- Type: feature
|
||||||
- Depends on: T2
|
- Depends on: T2
|
||||||
- Files: engine/forecast.py
|
- Files: engine/__init__.py, engine/forecast.py
|
||||||
- Status: pending
|
- Status: completed
|
||||||
|
|
||||||
### T4: Scenario Analysis
|
### T4: Scenario Analysis
|
||||||
- Type: feature
|
- Type: feature
|
||||||
- Depends on: T3
|
- Depends on: T3
|
||||||
- Files: engine/scenarios.py
|
- Files: engine/__init__.py, engine/scenarios.py
|
||||||
- Status: pending
|
- Status: completed
|
||||||
|
|
||||||
### T5: Excel Sync (import/export)
|
### T5: Excel Sync (import/export)
|
||||||
- Type: feature
|
- Type: feature
|
||||||
- Depends on: T2
|
- Depends on: T2
|
||||||
- Files: sync/excel_sync.py
|
- Files: sync/__init__.py, sync/excel_sync.py
|
||||||
- Status: pending
|
- Status: completed
|
||||||
|
|
||||||
### T6: CLI (Typer) — все команды
|
### T6: CLI (Typer) — все команды
|
||||||
- Type: feature
|
- Type: feature
|
||||||
- Depends on: T2, T3, T4, T5, T7
|
- Depends on: T2, T3, T4, T5, T7
|
||||||
- Files: cli/main.py
|
- Files: cli/__init__.py, cli/main.py, pyproject.toml
|
||||||
- Status: pending
|
- Status: completed
|
||||||
|
|
||||||
### T7: AI Assistant (промпты + интерфейс)
|
### T7: AI Assistant (промпты + интерфейс)
|
||||||
- Type: feature
|
- Type: feature
|
||||||
- Depends on: T3
|
- Depends on: T3
|
||||||
- Files: ai/prompts.py, ai/assistant.py
|
- Files: ai/__init__.py, ai/prompts.py, ai/assistant.py
|
||||||
- Status: pending
|
- Status: completed
|
||||||
|
|
||||||
### T8: Тесты на все модули
|
### T8: Тесты на все модули
|
||||||
- Type: test
|
- Type: test
|
||||||
- Depends on: T2, T3, T4, T5, T6, T7
|
- Depends on: T2, T3, T4, T5, T6, T7
|
||||||
- Files: tests/*.py
|
- Files: tests/test_model.py, tests/test_forecast.py, tests/test_scenarios.py, tests/test_excel_sync.py, tests/test_cli.py, tests/test_ai.py, tests/conftest.py
|
||||||
- Status: pending
|
- Status: completed
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
Исполнительный агент начинает с задачи **T1** (уже выполнена в SETUP), затем **T2: Модель данных**.
|
Все 8 задач выполнены. Проект готов к использованию.
|
||||||
|
|
||||||
## Caveats
|
## Caveats
|
||||||
|
|
||||||
- AI Assistant — заглушка для MVP; промпты готовы, но не подключены к реальному API
|
- AI-ассистент — заглушка (промпты готовы, API не подключено)
|
||||||
- Лицензия не указана — требуется решить
|
- База данных — JSON-файлы (не подходит для многопользовательской работы)
|
||||||
- Версия Python — 3.11+ (фактически 3.13 в окружении)
|
- Лицензия не выбрана
|
||||||
- Spreadsheet — только .xlsx через openpyxl
|
- CI/CD не настроен
|
||||||
|
|
||||||
## Checkpoints
|
## Checkpoints
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# MetaAgent Request
|
||||||
|
# Auto-generated from existing checkpoints.json on 2026-07-22
|
||||||
|
|
||||||
|
## Параметры сессии
|
||||||
|
|
||||||
|
| Функция | Вкл | Аргументы |
|
||||||
|
|---|---|---|
|
||||||
|
| ANALYSIS | ✓ | — |
|
||||||
|
| DESIGN | ✗ | project_type=existing |
|
||||||
|
| RED_TEAM | ✗ | — |
|
||||||
|
| RISK_REGISTER | ✗ | — |
|
||||||
|
| DECOMPOSITION | ✓ | invariant_tests=false |
|
||||||
|
| SETUP | ✓ | — |
|
||||||
|
| HANDOFF | ✓ | layer_structure=false |
|
||||||
|
|
||||||
|
## Глубина проработки
|
||||||
|
|
||||||
|
**Значение:** 4 (Light)
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Обновление metaagent-артефактов до v1.1.0, валидация существующего кода и окружения
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Project Rules
|
||||||
|
|
||||||
|
Правила, которым агент обязан следовать во всех фазах.
|
||||||
|
Добавляйте сюда условия, которые должны соблюдаться всегда — они будут прочитаны
|
||||||
|
перед началом каждой фазы и учтены при декомпозиции и реализации.
|
||||||
|
|
||||||
|
## Обязательные правила
|
||||||
|
|
||||||
|
- Всегда читать `.agent/rules/project-rules.md` перед каждой фазой
|
||||||
|
- Следовать протоколам MetaAgent строго последовательно
|
||||||
|
|
||||||
|
## Запреты
|
||||||
|
|
||||||
|
- Не писать production-код (это работа исполнительного агента)
|
||||||
|
- Не удалять файлы
|
||||||
|
- Не коммитить в main/master
|
||||||
|
|
||||||
|
## Конвенции проекта
|
||||||
|
|
||||||
|
- Python-проект: snake_case для функций/переменных, PascalCase для классов
|
||||||
|
- Использовать ruff для линтинга
|
||||||
|
- Тесты через pytest
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# BOUNDARIES — Рамки и границы
|
||||||
|
|
||||||
|
Что агенту **разрешено**, **запрещено** и в каких случаях **нужно остановиться**.
|
||||||
|
|
||||||
|
## Разрешено
|
||||||
|
|
||||||
|
| Действие | Примечание |
|
||||||
|
|---|---|
|
||||||
|
| Читать любые файлы в целевом репозитории | Все файлы, включая .git, конфиги, историю |
|
||||||
|
| Создавать/изменять файлы в `.agent/` | Директория метаданных проекта (rules, decisions, tasks, context, archive, requests, roadmap) |
|
||||||
|
| Создавать `.temp/` в корне проекта | Для временных файлов агента (всегда на одном уровне с `.agent/`) |
|
||||||
|
| Писать production-код | В фазе EXECUTION, по задачам из manifest.json |
|
||||||
|
| Рефакторить существующий код | Только если это часть задачи в manifest.json |
|
||||||
|
| Делать коммиты | По завершении задачи, перед созданием request |
|
||||||
|
| Создавать/дополнять `.gitignore` | Только для добавления `.temp/` |
|
||||||
|
| Устанавливать/обновлять зависимости | Только через штатный пакетный менеджер проекта |
|
||||||
|
| Изменять конфигурационные файлы | Только если это необходимо для сборки/тестов (например, добавить requirements.txt) |
|
||||||
|
| Запускать сборку и тесты | Для верификации окружения и проверки request-ов |
|
||||||
|
| Читать документацию, issue, PRs | Для понимания контекста |
|
||||||
|
| Запрашивать уточнения у пользователя | Если не хватает информации для декомпозиции |
|
||||||
|
| Копировать исходники MetaAgent в `.agent/src/` целевого проекта | Только на фазе INIT, без перезаписи существующих файлов |
|
||||||
|
| Создавать/обновлять `AGENTS.md` в корне целевого проекта | Только если файла не существует |
|
||||||
|
| **Обязательно:** читать `.agent/rules/project-rules.md` перед каждой фазой | Исполнение правил пользователя — приоритет выше стандартных протоколов |
|
||||||
|
| Перемещать завершённые артефакты в `.agent/archive/` | На фазах METASTATE и HANDOFF, только для completed/failed артефактов |
|
||||||
|
| **Обязательно:** после выполнения задачи создавать request в `.agent/requests/active/` | Request — единица результата, основа для METASTATE |
|
||||||
|
|
||||||
|
## Запрещено
|
||||||
|
|
||||||
|
| Действие | Почему |
|
||||||
|
|---|---|
|
||||||
|
| Удалять файлы | Если файл мешает — нужно сообщить пользователю |
|
||||||
|
| Менять удалённые настройки CI/CD | Если CI сломан — сообщить пользователю |
|
||||||
|
| Модифицировать код, не связанный с задачей | Только то, что нужно в рамках задачи из manifest.json |
|
||||||
|
|
||||||
|
## Когда остановиться
|
||||||
|
|
||||||
|
1. **Репозиторий не собирается** — сообщить пользователю с логом ошибки, не продолжать
|
||||||
|
2. **Неясна цель** — запросить уточнение, не гадать
|
||||||
|
3. **Обнаружены секреты/токены** — не копировать, сообщить пользователю
|
||||||
|
4. **Цель выходит за рамки одной сессии** — разбить, запросить приоритет
|
||||||
|
5. **Проект не использует известные технолологии** — запросить у пользователя инструкцию по сборке
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
# META_AGENT_GUIDE — Главная инструкция v2.1
|
||||||
|
|
||||||
|
## Жизненный цикл сессии
|
||||||
|
|
||||||
|
```
|
||||||
|
.agent/metaagent-request.md
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ PROJECT LOOP (однократно) │
|
||||||
|
│ │
|
||||||
|
│ INIT → ANALYSE → ROADMAP → DESIGN → DECOMPOSITION │
|
||||||
|
│ │
|
||||||
|
│ Выход: .agent/tasks/manifest.json │
|
||||||
|
└──────────────────────┬──────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ WORK LOOP (циклически) │
|
||||||
|
│ │
|
||||||
|
│ EXECUTION → (request) → METASTATE (по команде) │
|
||||||
|
│ │
|
||||||
|
│ Цикл повторяется: беру задачу → делаю → │
|
||||||
|
│ создаю request → накопилось → METASTATE │
|
||||||
|
└──────────────────────┬──────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ HANDOFF (завершение) │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Фазы выполняются **строго последовательно** внутри PROJECT LOOP.
|
||||||
|
WORK LOOP может повторяться многократно.
|
||||||
|
HANDOFF — легковесное завершение.
|
||||||
|
|
||||||
|
Все артефакты размещаются в `.agent/` целевого репозитория.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Конфигурация сессии (.agent/metaagent-request.md)
|
||||||
|
|
||||||
|
Перед запуском сессии пользователь заполняет `.agent/metaagent-request.md` (см. `TEMPLATES/metaagent-request.md`). Файл должен находиться в директории `.agent/` целевого репозитория.
|
||||||
|
|
||||||
|
Ключевые параметры:
|
||||||
|
|
||||||
|
### Шкала глубины (depth 1-10)
|
||||||
|
|
||||||
|
| Уровень | Название | Что выполняется |
|
||||||
|
|---|---|---|
|
||||||
|
| 1-2 | Scaffold | INIT → ANALYSIS → SETUP (только структура, без реализации) |
|
||||||
|
| 3-4 | Light | + ROADMAP, DESIGN (без ADR/альтернатив), DECOMPOSITION — **(default)** |
|
||||||
|
| 5-6 | Standard | полный цикл с базовым DESIGN и DECOMPOSITION |
|
||||||
|
| 7-8 | Deep | + ADR, Alternative Architecture, Risk Register, Invariant Tests |
|
||||||
|
| 9-10 | Maximum | + Red Team Review, Executable Invariants для всех ADR |
|
||||||
|
|
||||||
|
### Функции
|
||||||
|
|
||||||
|
| Функция | Фаза | Глубина | Описание |
|
||||||
|
|---|---|---|---|
|
||||||
|
| adr | DESIGN | >=7 | Создание ADR для каждого ключевого решения |
|
||||||
|
| alternative_arch | DESIGN | >=7 | Обязательное описание альтернативной архитектуры |
|
||||||
|
| red_team | DESIGN (после) | >=9 | Red Team Review — попытка разрушить архитектуру |
|
||||||
|
| risk_register | DESIGN | >=7 | Явный реестр допущений |
|
||||||
|
| invariant_tests | DECOMPOSITION | >=7 | Задачи-инварианты для каждого ADR |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 0: INIT
|
||||||
|
|
||||||
|
**Протокол:** `PROTOCOLS/00_CONFIG.md`
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
- Прочитать `VERSION` — текущая версия MetaAgent
|
||||||
|
- Склонировать/открыть целевой репозиторий
|
||||||
|
- Создать директорию `.agent/` в корне целевого репозитория (если нет)
|
||||||
|
- Создать `.temp/` в корне целевого репозитория (если нет), добавить в `.gitignore`
|
||||||
|
- Установить исходники MetaAgent в `.agent/src/`
|
||||||
|
- Создать структуру `.agent/`: `rules/`, `decisions/`, `tasks/` (с `backlog/`), `context/`, `requests/` (с `active/`, `archive/`), `roadmap/` (с `archive/`), `archive/` (с `tasks/`, `decisions/`, `checkpoints/`)
|
||||||
|
- Создать/обновить `AGENTS.md` в корне
|
||||||
|
- Прочитать/создать `.agent/metaagent-request.md` (интервью или default)
|
||||||
|
- Проверить версию, инициализировать `checkpoints.json`
|
||||||
|
|
||||||
|
**Выход:** готовая `.agent/` + checkpoints.json.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 1: ANALYSIS
|
||||||
|
|
||||||
|
**Протокол:** `PROTOCOLS/01_ANALYSIS.md`
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
- Прочитать `.agent/rules/project-rules.md`
|
||||||
|
- Прочитать config из checkpoints.json
|
||||||
|
- Выполнить анализ репозитория:
|
||||||
|
- Определить тип проекта (existing / greenfield / scaffold)
|
||||||
|
- Зафиксировать стек, архитектуру, конвенции, тесты
|
||||||
|
- Для greenfield — извлечь требования из README
|
||||||
|
- Создать начальный `.agent/context/project-state.md` — слепок проекта
|
||||||
|
- Записать `.agent/context/analysis-report.md`
|
||||||
|
- Обновить checkpoints.json
|
||||||
|
|
||||||
|
**Ветвление:**
|
||||||
|
- `project_type = "greenfield"` или `"scaffold"` → далее ROADMAP → DESIGN
|
||||||
|
- `project_type = "existing"` → далее ROADMAP (DESIGN пропускается)
|
||||||
|
|
||||||
|
**Выход:** `.agent/context/analysis-report.md`, `.agent/context/project-state.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 2: ROADMAP
|
||||||
|
|
||||||
|
**Протокол:** `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`
|
||||||
|
|
||||||
|
Выполняется только для greenfield/scaffold.
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
- Спроектировать архитектуру, модули, данные, интерфейсы
|
||||||
|
- Если config.design.adr: создать ADR → `.agent/decisions/`
|
||||||
|
- Если config.risk_register: создать `.agent/context/risk-register.md`
|
||||||
|
- Записать `.agent/context/design-report.md`
|
||||||
|
|
||||||
|
**Выход:** `.agent/context/design-report.md`, опционально ADR, risk-register
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 3b: RED_TEAM (опциональная)
|
||||||
|
|
||||||
|
**Протокол:** `PROTOCOLS/02b_REDTEAM.md`
|
||||||
|
|
||||||
|
Только если config.red_team = yes (depth >= 9).
|
||||||
|
|
||||||
|
**Выход:** `.agent/context/red-team-report.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 4: DECOMPOSITION
|
||||||
|
|
||||||
|
**Протокол:** `PROTOCOLS/03_DECOMPOSITION.md`
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
- Прочитать `.agent/rules/project-rules.md`
|
||||||
|
- Разбить цель (и дизайн) на атомарные задачи
|
||||||
|
- Каждой задаче присвоить `origin` (источник: roadmap, ADR, user, agent)
|
||||||
|
- Если есть `.agent/roadmap/sources.md` — сверить приоритеты
|
||||||
|
- Если config.invariant_tests: создать задачи-инварианты для ADR
|
||||||
|
- Записать `.agent/tasks/manifest.json` и `.agent/tasks/manifest.md`
|
||||||
|
|
||||||
|
**Выход:** `.agent/tasks/manifest.json` + `.agent/tasks/manifest.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 5: EXECUTION (циклическая)
|
||||||
|
|
||||||
|
**Протокол:** `PROTOCOLS/04_EXECUTION.md`
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
1. Выбрать следующую задачу из manifest.json (pending, все depends_on выполнены)
|
||||||
|
2. Отметить `in_progress`
|
||||||
|
3. Реализовать (код, тесты, конфиги)
|
||||||
|
4. Верифицировать (тесты, LSP diagnostics)
|
||||||
|
5. Закоммитить
|
||||||
|
6. Создать request в `.agent/requests/active/req-{id}.json`
|
||||||
|
7. Отметить `completed` в manifest.json
|
||||||
|
8. Повторить, пока есть задачи
|
||||||
|
9. Если задач нет — ожидать команду пользователя
|
||||||
|
|
||||||
|
**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/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 6: METASTATE (по команде пользователя)
|
||||||
|
|
||||||
|
**Протокол:** `PROTOCOLS/06_METASTATE.md`
|
||||||
|
|
||||||
|
Запускается по команде: «обнови метасостояние», «update metastate», «подведи итог».
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
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
|
||||||
|
{
|
||||||
|
"metaagent_version": "2.1.0",
|
||||||
|
"session_id": "<uuid>",
|
||||||
|
"target_repo": "<path>",
|
||||||
|
"goal": "<цель>",
|
||||||
|
"project_type": "existing | greenfield | scaffold",
|
||||||
|
"config": {
|
||||||
|
"depth": 6,
|
||||||
|
"design": { "adr": true, "alternative_arch": true },
|
||||||
|
"red_team": false,
|
||||||
|
"risk_register": false,
|
||||||
|
"decomposition": { "invariant_tests": true }
|
||||||
|
},
|
||||||
|
"phases": {
|
||||||
|
"analysis": "completed",
|
||||||
|
"roadmap": "completed",
|
||||||
|
"design": "completed",
|
||||||
|
"red_team": "skipped",
|
||||||
|
"decomposition": "completed",
|
||||||
|
"execution": "completed",
|
||||||
|
"metastate": "completed",
|
||||||
|
"handoff": "completed"
|
||||||
|
},
|
||||||
|
"tasks": [
|
||||||
|
{ "id": "T1", "title": "...", "status": "archived", "origin": "user:direct" },
|
||||||
|
{ "id": "T2", "title": "...", "status": "pending", "origin": "roadmap:010" }
|
||||||
|
],
|
||||||
|
"last_updated": "<timestamp>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Структура .agent/
|
||||||
|
|
||||||
|
```
|
||||||
|
.agent/
|
||||||
|
checkpoints.json # состояние сессии (ядро)
|
||||||
|
session-summary.md # краткая сводка сессии
|
||||||
|
handoff-summary.md # сводка для следующего агента (создаётся METASTATE)
|
||||||
|
|
||||||
|
src/ # исходники MetaAgent (всегда)
|
||||||
|
META_AGENT_GUIDE.md
|
||||||
|
PROTOCOLS/
|
||||||
|
TEMPLATES/
|
||||||
|
BOUNDARIES.md
|
||||||
|
WORKFLOW.md
|
||||||
|
VERSION
|
||||||
|
install.sh / install.ps1
|
||||||
|
|
||||||
|
rules/
|
||||||
|
project-rules.md
|
||||||
|
|
||||||
|
roadmap/ # ИСТОЧНИКИ ЗАДАЧ (новое в v2.1)
|
||||||
|
sources.md # консолидированный список с приоритетами
|
||||||
|
archive/ # устаревшие roadmap-планы
|
||||||
|
|
||||||
|
decisions/ # архитектурные решения (ADR)
|
||||||
|
index.json
|
||||||
|
001-*.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
|
||||||
|
red-team-report.md
|
||||||
|
baseline-test-report.log
|
||||||
|
|
||||||
|
archive/
|
||||||
|
index.json
|
||||||
|
tasks/
|
||||||
|
decisions/
|
||||||
|
requests/
|
||||||
|
checkpoints/
|
||||||
|
```
|
||||||
|
|
||||||
|
`.temp/` в корне проекта:
|
||||||
|
|
||||||
|
```
|
||||||
|
.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 |
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# Протокол 00: Конфигурация сессии (CONFIG)
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Определить параметры сессии MetaAgent: глубину проработки, набор функций, тип проекта. Выполняется на фазе INIT.
|
||||||
|
|
||||||
|
## Вход
|
||||||
|
|
||||||
|
- `VERSION` — текущая версия MetaAgent
|
||||||
|
- Запрос пользователя (цель)
|
||||||
|
- Опционально: `.agent/metaagent-request.md` (в директории `.agent/` целевого репозитория)
|
||||||
|
|
||||||
|
## Шаги
|
||||||
|
|
||||||
|
### 0.1. Проверить наличие .agent/metaagent-request.md
|
||||||
|
|
||||||
|
Если файл существует — распарсить, провалидировать и использовать.
|
||||||
|
Если нет — перейти к интервью (шаг 0.2).
|
||||||
|
|
||||||
|
### 0.2. Интервью с пользователем
|
||||||
|
|
||||||
|
Задать пользователю серию вопросов для сбора конфигурации.
|
||||||
|
|
||||||
|
**Сценарий интервью:**
|
||||||
|
|
||||||
|
```
|
||||||
|
MetaAgent: .agent/metaagent-request.md не найден. Давайте настроим сессию.
|
||||||
|
(или ответьте "default" — я выберу depth=4, light)
|
||||||
|
|
||||||
|
Q1: Это новый проект (greenfield) или работа с существующим кодом (existing)?
|
||||||
|
Варианты: new / existing / scaffold / default
|
||||||
|
|
||||||
|
Q2: Глубина проработки?
|
||||||
|
1-2: Scaffold — только структура, пустые модули
|
||||||
|
3-4: Light — быстрый дизайн + задачи, без расширений (рекомендуется default)
|
||||||
|
5-6: Standard — полный цикл с acceptance criteria
|
||||||
|
7-8: Deep — + ADR, risk register, alternative architecture
|
||||||
|
9-10: Maximum — + Red Team review, executable invariants
|
||||||
|
Варианты: число 1-10 / default
|
||||||
|
|
||||||
|
Q3 (если глубина >= 7): Нужны ADR (Architecture Decision Records)?
|
||||||
|
Варианты: yes / no / default
|
||||||
|
|
||||||
|
Q4 (если глубина >= 7): Нужен Risk Register?
|
||||||
|
Варианты: yes / no / default
|
||||||
|
|
||||||
|
Q5 (если глубина >= 9): Нужен Red Team Review?
|
||||||
|
Варианты: yes / no / default
|
||||||
|
```
|
||||||
|
|
||||||
|
**Правила обработки ответов:**
|
||||||
|
- Если пользователь ответил `default` или не ответил — применить значение по умолчанию для этого поля
|
||||||
|
- Если пользователь ответил `new` — `project_type = greenfield`
|
||||||
|
- Если `existing` — `project_type = existing`
|
||||||
|
|
||||||
|
### 0.3. Default config
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"depth": 4,
|
||||||
|
"design": {
|
||||||
|
"adr": false,
|
||||||
|
"alternative_arch": false
|
||||||
|
},
|
||||||
|
"red_team": false,
|
||||||
|
"risk_register": false,
|
||||||
|
"decomposition": {
|
||||||
|
"invariant_tests": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Depth=4 (Light) означает:
|
||||||
|
- ANALYSIS — полный (определение типа проекта, извлечение требований)
|
||||||
|
- DESIGN — выполняется (если greenfield), но **без** ADR, Alternative Architecture, Risk Register
|
||||||
|
- DECOMPOSITION — задачи с acceptance criteria, **без** invariant-тестов
|
||||||
|
- SETUP — полный
|
||||||
|
- HANDOFF — `.agent/` организован по семантическим группам (decisions, tasks, context, rules)
|
||||||
|
|
||||||
|
### 0.4. Запись .agent/metaagent-request.md
|
||||||
|
|
||||||
|
Если файла не было, создать его по результатам интервью с пометкой `Auto-generated`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# MetaAgent Request
|
||||||
|
# Auto-generated from user interview on {{ date }}
|
||||||
|
|
||||||
|
## Параметры сессии
|
||||||
|
|
||||||
|
| Функция | Вкл | Аргументы |
|
||||||
|
|---|---|---|
|
||||||
|
| ANALYSIS | ✓ | — |
|
||||||
|
| DESIGN | ✓ | adr={{ adr }}, alternative_arch={{ alt_arch }} |
|
||||||
|
| RED_TEAM | {{ red_team }} | — |
|
||||||
|
| RISK_REGISTER | {{ risk_register }} | — |
|
||||||
|
| DECOMPOSITION | ✓ | invariant_tests={{ invariant_tests }} |
|
||||||
|
| SETUP | ✓ | — |
|
||||||
|
| HANDOFF | ✓ | — |
|
||||||
|
|
||||||
|
## Глубина проработки
|
||||||
|
|
||||||
|
**Значение:** {{ depth }}
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
{{ goal }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 0.5. Создание .agent/rules/
|
||||||
|
|
||||||
|
Создать директорию `.agent/rules/` в корне целевого проекта (если не существует).
|
||||||
|
Если `.agent/rules/project-rules.md` не существует — создать из шаблона `.agent/src/TEMPLATES/project-rules.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Project Rules
|
||||||
|
|
||||||
|
Добавляйте сюда правила, которым агент обязан следовать во всех фазах.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 0.6. Валидация config
|
||||||
|
|
||||||
|
Проверить совместимость параметров с depth:
|
||||||
|
|
||||||
|
```
|
||||||
|
depth < 3 → DESIGN пропускается (даже для greenfield)
|
||||||
|
depth < 7 → adr=false, alternative_arch=false, risk_register=false, invariant_tests=false
|
||||||
|
depth < 9 → red_team=false
|
||||||
|
```
|
||||||
|
|
||||||
|
Если depth несовместим с включёнными функциями — понизить функции до максимума, разрешённого depth.
|
||||||
|
|
||||||
|
## Выход
|
||||||
|
|
||||||
|
- `.agent/metaagent-request.md` (создан или подтверждён)
|
||||||
|
- config — словарь параметров для записи в checkpoints.json
|
||||||
|
|
||||||
|
## Критерии завершения
|
||||||
|
|
||||||
|
- [ ] `.agent/metaagent-request.md` существует (создан или найден)
|
||||||
|
- [ ] Config содержит depth, design.*, red_team, risk_register, decomposition.*, handoff.*
|
||||||
|
- [ ] Config совместим с depth (доп. функции отключены для малых depth)
|
||||||
|
- [ ] При отсутствии файла — проведено интервью, файл создан
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
# Протокол 00b: Миграция артефактов (MIGRATE)
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Обеспечить совместимость артефактов `.agent/` при изменении версии MetaAgent.
|
||||||
|
Позволяет обновлять проекты, созданные старой версией, без потери данных.
|
||||||
|
|
||||||
|
## Вход
|
||||||
|
|
||||||
|
- `VERSION` — текущая версия MetaAgent
|
||||||
|
- `.agent/checkpoints.json` — артефакты целевого проекта
|
||||||
|
- `.agent/` — остальные артефакты
|
||||||
|
|
||||||
|
## Шаги
|
||||||
|
|
||||||
|
### M1. Определить версию артефактов
|
||||||
|
|
||||||
|
Прочитать `.agent/checkpoints.json`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
stored_version = checkpoints.get("metaagent_version", None)
|
||||||
|
current_version = read("VERSION").strip()
|
||||||
|
```
|
||||||
|
|
||||||
|
- Если `metaagent_version` отсутствует → артефакт создан **v0.x** (доверсионный)
|
||||||
|
- Если `metaagent_version` == `current_version` → пропустить миграцию
|
||||||
|
- Если `metaagent_version` < `current_version` → требуется миграция
|
||||||
|
|
||||||
|
### M2. Сравнение версий (SemVer)
|
||||||
|
|
||||||
|
Версии сравниваются по семантическому версионированию (`MAJOR.MINOR.PATCH`).
|
||||||
|
|
||||||
|
```python
|
||||||
|
def needs_migration(stored, current):
|
||||||
|
if stored is None:
|
||||||
|
return True
|
||||||
|
return parse_semver(stored) < parse_semver(current)
|
||||||
|
```
|
||||||
|
|
||||||
|
### M3. Матрица миграций
|
||||||
|
|
||||||
|
Каждая строка — набор шагов для перехода с одной версии на следующую.
|
||||||
|
|
||||||
|
| Из версии | В версию | Шаги миграции |
|
||||||
|
|---|---|---|
|
||||||
|
| v0.x (нет поля) | v1.0.0 | M3.1 — M3.4 |
|
||||||
|
| 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
|
||||||
|
|
||||||
|
... (шаги миграции остаются без изменений)
|
||||||
|
|
||||||
|
### M5. Шаги миграции v1.0.0 → v1.1.0
|
||||||
|
|
||||||
|
M3.5: Создать `.agent/rules/` с шаблоном `project-rules.md` (если не существует).
|
||||||
|
M3.6: Создать `.agent/archive/` (если не существует).
|
||||||
|
|
||||||
|
#### M3.1. Добавить metaagent_version
|
||||||
|
|
||||||
|
Записать в checkpoints.json:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"metaagent_version": "1.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### M3.2. Добавить config (default)
|
||||||
|
|
||||||
|
Если поля `config` нет в checkpoints.json — добавить config по умолчанию:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"config": {
|
||||||
|
"depth": 4,
|
||||||
|
"design": { "adr": false, "alternative_arch": false },
|
||||||
|
"red_team": false,
|
||||||
|
"risk_register": false,
|
||||||
|
"decomposition": { "invariant_tests": false },
|
||||||
|
"handoff": { "layer_structure": false }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### M3.3. Добавить фазу red_team
|
||||||
|
|
||||||
|
Если в `phases` нет ключа `red_team`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"red_team": "skipped"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### M3.4. Создать layer-1/ (опционально, только если config.handoff.layer_structure)
|
||||||
|
|
||||||
|
Если включена layer_structure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p .agent/layer-1/adr
|
||||||
|
touch .agent/layer-1/adr/.gitkeep
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `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. После миграции — резюме
|
||||||
|
|
||||||
|
Записать в `.agent/migration-report.log`:
|
||||||
|
|
||||||
|
```
|
||||||
|
[MIGRATE] {{ timestamp }}
|
||||||
|
From: {{ from_version }}
|
||||||
|
To: {{ to_version }}
|
||||||
|
Steps applied: {{ step_list }}
|
||||||
|
Status: OK
|
||||||
|
```
|
||||||
|
|
||||||
|
## Выход
|
||||||
|
|
||||||
|
- Обновлённый `.agent/checkpoints.json` (metaagent_version + config)
|
||||||
|
- Обновлённая структура `.agent/` (decisions/tasks/context вместо layer-0..3)
|
||||||
|
- `.agent/migration-report.log`
|
||||||
|
|
||||||
|
## Критерии завершения
|
||||||
|
|
||||||
|
- [ ] metaagent_version в checkpoints.json == текущей версии из VERSION
|
||||||
|
- [ ] config присутствует в checkpoints.json
|
||||||
|
- [ ] phases.red_team присутствует (skipped, если не нужен)
|
||||||
|
- [ ] migration-report.log создан
|
||||||
|
- [ ] Все старые данные сохранены (ничего не удалено)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Протокол 01: Анализ репозитория (ANALYSIS)
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Составить полную картину целевого репозитория: тип проекта, архитектура, стек, конвенции, состояние тестов, требования.
|
||||||
|
|
||||||
|
## Вход
|
||||||
|
|
||||||
|
- Целевой репозиторий (локальная копия)
|
||||||
|
- `.agent/metaagent-request.md` (конфигурация сессии: глубина, функции) — или auto-generated
|
||||||
|
- `.agent/checkpoints.json` (фаза analysis: pending)
|
||||||
|
|
||||||
|
## Шаги
|
||||||
|
|
||||||
|
### 0.0. Чтение конфигурации сессии
|
||||||
|
|
||||||
|
Прочитать config из checkpoints.json (установлен на фазе INIT через `00_CONFIG.md`).
|
||||||
|
|
||||||
|
Если config отсутствует или неполный — применить default:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"depth": 4,
|
||||||
|
"design": { "adr": false, "alternative_arch": false },
|
||||||
|
"red_team": false,
|
||||||
|
"risk_register": false,
|
||||||
|
"decomposition": { "invariant_tests": false }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Записать (или подтвердить) конфигурацию в checkpoints.json:
|
||||||
|
```json
|
||||||
|
"config": {
|
||||||
|
"depth": 6,
|
||||||
|
"design": { "adr": true, "alternative_arch": true },
|
||||||
|
"red_team": false,
|
||||||
|
"risk_register": false,
|
||||||
|
"decomposition": { "invariant_tests": true }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `.agent/metaagent-request.md` не найден — использовать значения по умолчанию (depth=6, все базовые функции=true, расширенные=false).
|
||||||
|
|
||||||
|
### 1.0. Определение типа проекта
|
||||||
|
|
||||||
|
Просканировать корень репозитория и определить:
|
||||||
|
|
||||||
|
- **`existing`** — есть исходный код, тесты, система сборки (файлы `.py`, `.js`, `.ts`, `.rs`, `.go` и т.д. помимо конфигов и README)
|
||||||
|
- **`greenfield`** — репозиторий пуст или содержит только README, LICENSE, .gitignore
|
||||||
|
- **`scaffold`** — есть базовая структура (pyproject.toml/package.json), но нет значимого кода
|
||||||
|
|
||||||
|
Записать тип в analysis-report.md.
|
||||||
|
|
||||||
|
**Правило:** если проект `existing` — разделы 1.1–1.6 выполняются полностью. Если `greenfield` — разделы 1.2–1.5 заменяются на 1.7 (извлечение требований из README).
|
||||||
|
|
||||||
|
### 1.1. Общая информация
|
||||||
|
|
||||||
|
Прочитать и зафиксировать:
|
||||||
|
- **README** — описание проекта, how to build/test/run
|
||||||
|
- **Лицензия** — какой LICENSE
|
||||||
|
- **CI/CD** — `.github/workflows/`, `.gitlab-ci.yml`, `Jenkinsfile`, `Makefile` и т.д.
|
||||||
|
- **Главные точки входа** — `main.py`, `index.js`, `cmd/` и т.д.
|
||||||
|
- **Система сборки** — `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `CMakeLists.txt`
|
||||||
|
|
||||||
|
### 1.2. Стек технологий (только для existing/scaffold)
|
||||||
|
|
||||||
|
Определить:
|
||||||
|
- **Язык(и)** — Python, TypeScript, Go, Rust и т.д.
|
||||||
|
- **Фреймворк** — FastAPI, Next.js, React, Actix и т.д.
|
||||||
|
- **База данных** — PostgreSQL, SQLite, MongoDB и т.д.
|
||||||
|
- **Тестовый раннер** — pytest, jest, vitest, go test
|
||||||
|
- **Пакетный менеджер** — pip/poetry, npm/yarn/pnpm, cargo, go modules
|
||||||
|
- **Линтер/форматтер** — ruff, eslint, prettier, rustfmt, gofmt
|
||||||
|
|
||||||
|
### 1.3. Архитектура (только для existing/scaffold)
|
||||||
|
|
||||||
|
- **Структура директорий** — записать схему (можно `tree /F`, но не более 3 уровней глубины)
|
||||||
|
- **Архитектурный паттерн** — MVC, Clean Architecture, модульный монолит, микросервисы
|
||||||
|
- **Ключевые модули/пакеты** — перечислить с кратким описанием
|
||||||
|
- **Внешние зависимости** — основные библиотеки
|
||||||
|
|
||||||
|
### 1.4. Конвенции кода (только для existing/scaffold)
|
||||||
|
|
||||||
|
- **Стиль кода** — судя по линтеру и примерам: именование, импорты, типизация
|
||||||
|
- **Паттерны** — как организованы роуты, хендлеры, модели, тесты
|
||||||
|
- **Обработка ошибок** — как принято обрабатывать ошибки в проекте
|
||||||
|
- **Логирование** — используется ли логгер, какой уровень
|
||||||
|
|
||||||
|
### 1.5. Тесты (только для existing/scaffold)
|
||||||
|
|
||||||
|
- **Какие тесты есть** — unit, integration, e2e
|
||||||
|
- **Где лежат** — `tests/`, `__tests__/`, рядом с модулями
|
||||||
|
- **Запуск** — команда для запуска всех тестов
|
||||||
|
- **Текущее состояние** — запустить тесты, записать результат (сколько всего, сколько пройдено/упало)
|
||||||
|
- **Покрытие** — есть ли метрики покрытия
|
||||||
|
|
||||||
|
### 1.6. Базовая проверка (только для existing/scaffold)
|
||||||
|
|
||||||
|
- **Собирается ли проект?** — запустить сборку
|
||||||
|
- **Запускается ли проект?** — если возможно, проверить старт
|
||||||
|
- **Чистый ли git status?** — нет ли незакоммиченных изменений
|
||||||
|
|
||||||
|
### 1.7. Извлечение требований (только для greenfield/scaffold)
|
||||||
|
|
||||||
|
Если README содержит описание будущего проекта — извлечь и структурировать:
|
||||||
|
|
||||||
|
**Функциональные требования:**
|
||||||
|
- Пользовательские истории (user stories)
|
||||||
|
- Основные сценарии использования
|
||||||
|
- Входные/выходные данные системы
|
||||||
|
|
||||||
|
**Нефункциональные требования:**
|
||||||
|
- Технологические предпочтения (язык, фреймворк, БД)
|
||||||
|
- Требования к производительности, безопасности
|
||||||
|
- Ограничения (сроки, платформа, окружение)
|
||||||
|
|
||||||
|
**Бизнес-контекст:**
|
||||||
|
- Цель системы (зачем)
|
||||||
|
- Целевая аудитория
|
||||||
|
- Ключевые метрики успеха
|
||||||
|
|
||||||
|
**Сомнительные/неясные требования:**
|
||||||
|
- Вопросы, которые нужно задать пользователю перед проектированием
|
||||||
|
- Противоречия в 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/context/analysis-report.md` по шаблону `TEMPLATES/analysis-report.md`
|
||||||
|
- `.agent/context/project-state.md` — начальный слепок проекта
|
||||||
|
|
||||||
|
Обновить checkpoints.json: `phases.analysis = "completed"`. Если проект `greenfield`, также установить `project_type = "greenfield"`.
|
||||||
|
|
||||||
|
## Критерии завершения фазы
|
||||||
|
|
||||||
|
- [ ] Тип проекта определён (existing / greenfield / scaffold)
|
||||||
|
- [ ] Все соответствующие разделы (1.1–1.8) выполнены
|
||||||
|
- [ ] `.agent/context/analysis-report.md` создан и заполнен
|
||||||
|
- [ ] `.agent/context/project-state.md` создан с начальным слепком
|
||||||
|
- [ ] checkpoints.json обновлён
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# Протокол 02: Архитектурное проектирование (DESIGN)
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Спроектировать архитектуру, модули, данные и интерфейсы для greenfield/scaffold-проекта на основе требований из analysis-report.
|
||||||
|
|
||||||
|
## Вход
|
||||||
|
|
||||||
|
- `.agent/context/analysis-report.md` (project_type: greenfield или scaffold)
|
||||||
|
- `.agent/metaagent-request.md` (конфигурация сессии: adr, alternative_arch, risk_register)
|
||||||
|
- `.agent/checkpoints.json` (фаза design: pending)
|
||||||
|
|
||||||
|
## Правила
|
||||||
|
|
||||||
|
1. **Реалистичность** — архитектура должна быть реализуема исполнительным агентом за 1 сессию (до 10 задач)
|
||||||
|
2. **Документируемость** — каждый модуль, модель и интерфейс описывается в design-report.md
|
||||||
|
3. **Тестируемость** — каждый компонент проектируется с учётом того, как его тестировать
|
||||||
|
4. **Итеративность** — первая версия должна быть минимально рабочей (MVP), расширения — отдельными задачами
|
||||||
|
|
||||||
|
## Шаги
|
||||||
|
|
||||||
|
### 2.1. Технологический стек
|
||||||
|
|
||||||
|
Если стек не указан в README — предложить обоснованный выбор. Если указан — зафиксировать.
|
||||||
|
|
||||||
|
Для каждого компонента указать:
|
||||||
|
- Язык и версия
|
||||||
|
- Фреймворк / библиотека
|
||||||
|
- База данных (движок, схема)
|
||||||
|
- Инфраструктура (Docker, CI/CD, хостинг)
|
||||||
|
|
||||||
|
### 2.2. High-level архитектура
|
||||||
|
|
||||||
|
Описать общую структуру системы:
|
||||||
|
|
||||||
|
- **Архитектурный паттерн** — монолит, модульный монолит, микросервисы, слоистая, луковая и т.д.
|
||||||
|
- **Компоненты и их ответственность** — что делает каждый модуль/сервис
|
||||||
|
- **Схема взаимодействия** — текстовое описание потоков данных
|
||||||
|
|
||||||
|
Формат (text diagram):
|
||||||
|
|
||||||
|
```
|
||||||
|
[Client] → HTTP → [API Gateway] → [Auth Service]
|
||||||
|
↓
|
||||||
|
[Core Service] → [Database]
|
||||||
|
↓
|
||||||
|
[External API] → [3rd Party]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3. Модули проекта
|
||||||
|
|
||||||
|
Разбить систему на модули/пакеты. Для каждого:
|
||||||
|
|
||||||
|
| Поле | Описание |
|
||||||
|
|---|---|
|
||||||
|
| **Имя модуля** | `app/services/cashflow.py` |
|
||||||
|
| **Ответственность** | Что делает |
|
||||||
|
| **Ключевые классы/функции** | Только сигнатуры (без реализации) |
|
||||||
|
| **Зависимости** | Какие модули нужны этому |
|
||||||
|
| **Контракт** | Что экспортирует/предоставляет |
|
||||||
|
|
||||||
|
### 2.4. Модели данных
|
||||||
|
|
||||||
|
Описать основные сущности, их поля и связи:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"entity": "Transaction",
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "type": "UUID", "pk": true},
|
||||||
|
{"name": "amount", "type": "Decimal"},
|
||||||
|
{"name": "date", "type": "datetime"},
|
||||||
|
{"name": "category_id", "type": "UUID", "fk": "Category"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если используется ORM — указать аннотации/декораторы.
|
||||||
|
Если БД — схему таблиц, индексы, ключи.
|
||||||
|
|
||||||
|
### 2.5. API интерфейсы
|
||||||
|
|
||||||
|
Если проектируется API — описать эндпоинты:
|
||||||
|
|
||||||
|
| Метод | Путь | Описание | Request | Response | Статусы |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| GET | /transactions | Список транзакций | ?page, ?limit | [Transaction] | 200 |
|
||||||
|
| POST | /transactions | Создать транзакцию | CreateTransactionDTO | Transaction | 201, 400 |
|
||||||
|
|
||||||
|
Если GUI — описать ключевые страницы/экраны.
|
||||||
|
Если CLI — описать команды.
|
||||||
|
|
||||||
|
### 2.6. Обработка ошибок
|
||||||
|
|
||||||
|
- Стратегия ошибок: исключения, Result-тип, коды ошибок
|
||||||
|
- Формат ошибок в API: `{ "error": "...", "code": "...", "details": {} }`
|
||||||
|
- Логирование: какой уровень для каких событий
|
||||||
|
|
||||||
|
### 2.7. Стратегия тестирования
|
||||||
|
|
||||||
|
- Какие тесты нужны (unit, integration, e2e)
|
||||||
|
- Как изолировать зависимости (mocks, fakes, testcontainers)
|
||||||
|
- Команда запуска тестов
|
||||||
|
|
||||||
|
### 2.8. Alternative Architecture (если config.alternative_arch = yes)
|
||||||
|
|
||||||
|
Описать **минимум одну принципиально иную архитектуру** и причину отказа:
|
||||||
|
|
||||||
|
| Критерий | Выбранная архитектура | Альтернатива |
|
||||||
|
|---|---|---|
|
||||||
|
| Название | Модульный монолит | Микросервисы / Событийная / и т.д. |
|
||||||
|
| Сложность реализации | Низкая | Высокая (3+ сервиса) |
|
||||||
|
| Масштабирование | Вертикальное | Горизонтальное |
|
||||||
|
| Почему не выбрана | — | Избыточно для MVP |
|
||||||
|
|
||||||
|
Это снижает риск архитектурной инерции: решение становится осознанным, а не единственным возможным.
|
||||||
|
|
||||||
|
### 2.9. ADR (если config.adr = yes)
|
||||||
|
|
||||||
|
Для каждого ключевого архитектурного решения (стек, БД, паттерн, структура модулей) создать отдельный ADR-файл:
|
||||||
|
|
||||||
|
```
|
||||||
|
.agent/decisions/001-технологический-стек.md
|
||||||
|
.agent/decisions/002-модульный-монолит.md
|
||||||
|
.agent/decisions/003-json-хранение.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Формат — по шаблону `TEMPLATES/adr-NNNN.md`.
|
||||||
|
|
||||||
|
### 2.10. Risk Register (если config.risk_register = yes)
|
||||||
|
|
||||||
|
Создать `.agent/context/risk-register.md` по шаблону `TEMPLATES/risk-register.md`:
|
||||||
|
|
||||||
|
| # | Assumption | Impact if wrong | Mitigation | Review trigger |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
|
||||||
|
Задокументировать **неявные допущения**, на которых держится архитектура. Это даёт future-агентам знать, что можно пересматривать в первую очередь.
|
||||||
|
|
||||||
|
### 2.11. Группировка в задачи
|
||||||
|
|
||||||
|
На основе спроектированных модулей и моделей предварительно наметить группировку в задачи (по модулям). Это будет входом для DECOMPOSITION.
|
||||||
|
|
||||||
|
```
|
||||||
|
T1: Инициализация проекта + зависимости
|
||||||
|
T2: Модель данных (сущности, миграции)
|
||||||
|
T3: Cashflow Service (core logic)
|
||||||
|
T4: API endpoints
|
||||||
|
...и т.д.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Выход
|
||||||
|
|
||||||
|
- `.agent/context/design-report.md` по шаблону `TEMPLATES/design-report.md`
|
||||||
|
- `.agent/decisions/*.md` (если adr=yes)
|
||||||
|
- `.agent/context/risk-register.md` (если risk_register=yes)
|
||||||
|
- Предварительная группировка задач (для передачи в DECOMPOSITION)
|
||||||
|
|
||||||
|
Обновить checkpoints.json: `phases.design = "completed"`.
|
||||||
|
|
||||||
|
## Критерии завершения фазы
|
||||||
|
|
||||||
|
- [ ] Технологический стек определён
|
||||||
|
- [ ] High-level архитектура описана
|
||||||
|
- [ ] Модули и их ответственность описаны
|
||||||
|
- [ ] Модели данных спроектированы
|
||||||
|
- [ ] API/интерфейсы описаны (если применимо)
|
||||||
|
- [ ] Стратегия тестирования определена
|
||||||
|
- [ ] Alternative Architecture описана (если config требует)
|
||||||
|
- [ ] ADR созданы (если config требует)
|
||||||
|
- [ ] Risk Register создан (если config требует)
|
||||||
|
- [ ] Задачи предварительно сгруппированы
|
||||||
|
- [ ] `.agent/context/design-report.md` создан
|
||||||
|
- [ ] checkpoints.json обновлён
|
||||||
@@ -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 обновлён
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Протокол 02b: Red Team Review (опционально)
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Преднамеренно попытаться разрушить спроектированную архитектуру, чтобы найти скрытые проблемы до начала реализации.
|
||||||
|
|
||||||
|
## Вход
|
||||||
|
|
||||||
|
- `.agent/context/design-report.md`
|
||||||
|
- `.agent/decisions/*.md` (если созданы)
|
||||||
|
- `.agent/metaagent-request.md` (глубина проработки >= 9)
|
||||||
|
|
||||||
|
## Когда выполняется
|
||||||
|
|
||||||
|
Только если `config.red_team = yes` (глубина 9-10). Выполняется **после** DESIGN, **до** DECOMPOSITION.
|
||||||
|
|
||||||
|
## Шаги
|
||||||
|
|
||||||
|
### RT1. Поиск скрытых зависимостей
|
||||||
|
|
||||||
|
Проверить каждый модуль на наличие неявных связей:
|
||||||
|
|
||||||
|
- Есть ли циклические зависимости между модулями?
|
||||||
|
- Есть ли модуль, который знает слишком много о других?
|
||||||
|
- Есть ли скрытый vendor lock-in (БД, облачный провайдер, внешний API)?
|
||||||
|
|
||||||
|
### RT2. Точки отказа
|
||||||
|
|
||||||
|
Для каждого внешнего интерфейса (API, БД, файловая система):
|
||||||
|
|
||||||
|
- Что произойдёт при отказе компонента?
|
||||||
|
- Есть ли fallback?
|
||||||
|
- Что произойдёт при невалидных входных данных?
|
||||||
|
|
||||||
|
### RT3. Масштабирование
|
||||||
|
|
||||||
|
Оценить поведение системы при:
|
||||||
|
|
||||||
|
- 10x рост данных
|
||||||
|
- 100x рост данных
|
||||||
|
- Добавлении нового пользователя / клиента
|
||||||
|
|
||||||
|
### RT4. Security (если применимо)
|
||||||
|
|
||||||
|
- Какие данные передаются по сети?
|
||||||
|
- Есть ли аутентификация?
|
||||||
|
- Хранятся ли секреты в коде?
|
||||||
|
|
||||||
|
### RT5. Consistency
|
||||||
|
|
||||||
|
Проверить design-report и ADR на противоречия:
|
||||||
|
|
||||||
|
- Одна сущность описана по-разному в двух местах?
|
||||||
|
- API-контракт не соответствует модели данных?
|
||||||
|
- Технологический стек противоречит нефункциональным требованиям?
|
||||||
|
|
||||||
|
## Выход
|
||||||
|
|
||||||
|
`.agent/context/red-team-report.md` с секциями:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Найденные проблемы
|
||||||
|
|
||||||
|
| # | Проблема | Серьёзность | Рекомендация |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
## Отклонённые атаки (что пытались сломать — но не сломалось)
|
||||||
|
|
||||||
|
| # | Гипотеза | Почему не подтвердилась |
|
||||||
|
|---|---|---|
|
||||||
|
```
|
||||||
|
|
||||||
|
Обновить risk-register.md (если существует) новыми рисками.
|
||||||
|
|
||||||
|
## Критерии завершения
|
||||||
|
|
||||||
|
- [ ] Все 5 секций (RT1-RT5) проверены
|
||||||
|
- [ ] Найденные проблемы записаны в red-team-report.md
|
||||||
|
- [ ] Если найдены критические проблемы — design-report должен быть исправлен
|
||||||
|
- [ ] Risk Register дополнен (если существует)
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Протокол 03: Декомпозиция задач (DECOMPOSITION)
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Разбить цель пользователя (и архитектурный план, если есть) на атомарные, независимо выполнимые задачи и записать их в манифест.
|
||||||
|
|
||||||
|
## Вход
|
||||||
|
|
||||||
|
- `.agent/context/analysis-report.md`
|
||||||
|
- `.agent/context/design-report.md` (опционально — для greenfield/scaffold)
|
||||||
|
- `.agent/decisions/*.md` (опционально)
|
||||||
|
- `.agent/context/risk-register.md` (опционально)
|
||||||
|
- `.agent/roadmap/sources.md` (опционально — из фазы ROADMAP)
|
||||||
|
- `.agent/metaagent-request.md` (конфигурация сессии)
|
||||||
|
- Цель пользователя (из checkpoints.json)
|
||||||
|
- `.agent/checkpoints.json` (фаза decomposition: pending)
|
||||||
|
|
||||||
|
## Правила декомпозиции
|
||||||
|
|
||||||
|
### 3.1. Принципы
|
||||||
|
|
||||||
|
1. **Атомарность** — одна задача = одна логическая единица работы, которую можно выполнить и проверить за один подход
|
||||||
|
2. **Независимость (макс.)** — минимизировать зависимости между задачами
|
||||||
|
3. **Тестируемость** — каждая задача имеет измеримые acceptance criteria
|
||||||
|
4. **Границы** — задача не должна выходить за пределы, указанные в `BOUNDARIES.md`
|
||||||
|
5. **Порядок** — задачи с зависимостями выполняются строго последовательно
|
||||||
|
|
||||||
|
### 3.2. Размер задачи
|
||||||
|
|
||||||
|
Задача должна укладываться в **1-2 часа работы исполнительного агента**. Если задача крупнее — разбить на подзадачи.
|
||||||
|
|
||||||
|
Признак слишком крупной задачи:
|
||||||
|
- Нельзя сформулировать acceptance criteria одной строкой
|
||||||
|
- Затрагивает 5+ файлов
|
||||||
|
- Содержит союзы "и", "а также", "после чего"
|
||||||
|
|
||||||
|
### 3.3. Учёт roadmap
|
||||||
|
|
||||||
|
Если существует `.agent/roadmap/sources.md`:
|
||||||
|
- Сверить задачи с roadmap-приоритетами
|
||||||
|
- Задачи из roadmap получают приоритет P0-P3 в соответствии с sources.md
|
||||||
|
- Задачи без явного источника получают `origin: "decomposition"`
|
||||||
|
|
||||||
|
### 3.4. Структура задачи
|
||||||
|
|
||||||
|
Каждая задача содержит:
|
||||||
|
|
||||||
|
| Поле | Описание | Пример |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | Уникальный идентификатор | `T1`, `T2` |
|
||||||
|
| `title` | Заголовок (что сделать) | "Добавить модель User" |
|
||||||
|
| `description` | Описание (как и зачем) | "Создать SQLAlchemy модель..." |
|
||||||
|
| `type` | Тип задачи | `feature`, `refactor`, `test`, `fix`, `config`, `design`, `docs` |
|
||||||
|
| `status` | Статус задачи | `pending`, `in_progress`, `completed`, `failed`, `archived` |
|
||||||
|
| `origin` | Источник задачи | `roadmap:filename`, `adr:NNN`, `user:direct`, `agent:analysis`, `decomposition` |
|
||||||
|
| `files` | Список файлов, которые нужно создать/изменить | `["app/models/user.py"]` |
|
||||||
|
| `depends_on` | ID задач, от которых зависит | `[]` или `["T0"]` |
|
||||||
|
| `acceptance_criteria` | Список критериев приёмки (3-5 пунктов) | `["Модель проходит миграцию"]` |
|
||||||
|
| `context` | Доп. информация (ссылки на доки, примеры, релевантные секции из design-report) | `"Смотри app/models/base.py"` |
|
||||||
|
|
||||||
|
`origin` связывает задачу с источником:
|
||||||
|
- `roadmap:{filename}` — из FUTURE/ или roadmap плана
|
||||||
|
- `adr:{NNN}` — из Architecture Decision Record
|
||||||
|
- `user:direct` — напрямую от пользователя
|
||||||
|
- `agent:analysis` — выявлено агентом при анализе
|
||||||
|
- `decomposition` — создано при декомпозиции без внешнего источника
|
||||||
|
|
||||||
|
### 3.5. Типы задач (нумерация сдвинута)
|
||||||
|
|
||||||
|
| Тип | Описание |
|
||||||
|
|---|---|
|
||||||
|
| `config` | Настройка окружения, зависимостей, CI, инициализация проекта |
|
||||||
|
| `design` | Архитектурное/дизайнерское решение без кода |
|
||||||
|
| `feature` | Новая функциональность |
|
||||||
|
| `refactor` | Изменение структуры без изменения поведения |
|
||||||
|
| `test` | Добавление/исправление тестов |
|
||||||
|
| `fix` | Исправление бага |
|
||||||
|
| `docs` | Документация |
|
||||||
|
| `invariant` | Тест, проверяющий архитектурный инвариант (см. 3.7) |
|
||||||
|
|
||||||
|
### 3.6. Зелёная декомпозиция (для greenfield/scaffold)
|
||||||
|
|
||||||
|
Если есть `.agent/context/design-report.md` — задачи формируются на основе группировки из дизайна:
|
||||||
|
|
||||||
|
1. **T1: init** — инициализация проекта, зависимости, конфиги, scaffold
|
||||||
|
2. **T2..Tn: features** — модули/функциональность по одному
|
||||||
|
3. **Tn+1: tests** — тесты на каждый модуль (можно в составе feature-задачи)
|
||||||
|
4. **Tn+2: polish** — документация, форматирование, финальная проверка
|
||||||
|
|
||||||
|
### 3.7. Сортировка
|
||||||
|
|
||||||
|
Задачи в манифесте располагаются в порядке выполнения:
|
||||||
|
1. Сначала задачи без зависимостей
|
||||||
|
2. Потом те, чьи зависимости уже выполнены
|
||||||
|
3. Последними — задачи с наибольшим числом зависимостей
|
||||||
|
|
||||||
|
### 3.8. Executable Invariants (если config.invariant_tests = yes)
|
||||||
|
|
||||||
|
Для каждого ADR (из `.agent/decisions/`) создать задачу типа `invariant` — тест, проверяющий архитектурное правило.
|
||||||
|
|
||||||
|
**Правила превращения ADR в инварианты:**
|
||||||
|
|
||||||
|
| ADR | Инвариант-тест |
|
||||||
|
|---|---|
|
||||||
|
| "Модуль X не зависит от Y" | `test_x_does_not_import_y.py` — import test |
|
||||||
|
| "Слой Model не знает о CLI" | `test_model_layer_imports.py` — проверка import graph |
|
||||||
|
| "Все исключения кастомные" | `test_custom_exceptions.py` — проверка hierarchy |
|
||||||
|
| "Интерфейс репозитория не泄漏 implementation details" | `test_repository_interface.py` — ABC check |
|
||||||
|
|
||||||
|
**Формат задачи-инварианта:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "I1",
|
||||||
|
"title": "Инвариант: model не импортирует cli",
|
||||||
|
"type": "invariant",
|
||||||
|
"files": ["tests/invariants/test_layer_imports.py"],
|
||||||
|
"depends_on": ["T2"],
|
||||||
|
"acceptance_criteria": [
|
||||||
|
"Тест проверяет, что cashflow_model не импортирует cli, sync, engine",
|
||||||
|
"Тест проходит на пустом проекте (до реализации функциональности)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Инварианты размещаются в `tests/invariants/` и запускаются вместе с основными тестами.
|
||||||
|
|
||||||
|
## Выход
|
||||||
|
|
||||||
|
- `.agent/tasks/manifest.json` — по шаблону `TEMPLATES/task-manifest.json`
|
||||||
|
- `.agent/tasks/manifest.md` — по шаблону `TEMPLATES/task-manifest.md`
|
||||||
|
|
||||||
|
Обновить checkpoints.json:
|
||||||
|
- `phases.decomposition = "completed"`
|
||||||
|
- `tasks` = полный массив задач со статусом `pending`
|
||||||
|
|
||||||
|
> **Примечание:** после HANDOFF завершённые задачи будут архивированы —
|
||||||
|
> полное описание уходит в `.agent/archive/tasks/`, в manifest.json остаётся
|
||||||
|
> one-liner с `"status": "archived"`.
|
||||||
|
|
||||||
|
## Критерии завершения фазы
|
||||||
|
|
||||||
|
- [ ] Цель разбита на атомарные задачи
|
||||||
|
- [ ] Для каждой задачи указаны acceptance criteria
|
||||||
|
- [ ] Для каждой задачи указан origin (источник)
|
||||||
|
- [ ] Для каждой задачи указаны affected files
|
||||||
|
- [ ] Зависимости между задачами корректны (нет циклов)
|
||||||
|
- [ ] Задачи сверены с roadmap приоритетами (если sources.md существует)
|
||||||
|
- [ ] Invariant-задачи созданы для каждого ADR (если config требует)
|
||||||
|
- [ ] `.agent/tasks/manifest.json` и `.agent/tasks/manifest.md` созданы
|
||||||
|
- [ ] checkpoints.json обновлён
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# DEPRECATED — Протокол 04: Настройка окружения (SETUP)
|
||||||
|
|
||||||
|
> **Устарел в MetaAgent v2.1.** Заменён на `PROTOCOLS/04_EXECUTION.md`.
|
||||||
|
> Оставлен для обратной совместимости (проекты, использующие v2.0).
|
||||||
|
> Новые проекты используют фазу EXECUTION, в которой настройка окружения — первый шаг перед выполнением задач.
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Обеспечить рабочее окружение, в котором исполнительный агент может сразу выполнять задачи.
|
||||||
|
|
||||||
|
## Вход
|
||||||
|
|
||||||
|
- `.agent/context/analysis-report.md`
|
||||||
|
- `.agent/context/design-report.md` (опционально, для greenfield)
|
||||||
|
- `.agent/tasks/manifest.json`
|
||||||
|
- `.agent/checkpoints.json` (фаза environment: pending)
|
||||||
|
|
||||||
|
## Поведение в зависимости от типа проекта
|
||||||
|
|
||||||
|
Фаза SETUP работает по-разному для `existing` и `greenfield/scaffold` проектов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ветка A: existing/scaffold проект
|
||||||
|
|
||||||
|
### 4A.1. Зависимости
|
||||||
|
|
||||||
|
- Установить все зависимости согласно документации проекта
|
||||||
|
- Если есть `requirements.txt`, `pyproject.toml`, `package.json`, `Cargo.toml` и т.д. — выполнить установку
|
||||||
|
- Если в проекте используется виртуальное окружение (venv, .venv, conda) — активировать или создать
|
||||||
|
- Если в проекте используется Docker — проверить, что образ собирается
|
||||||
|
|
||||||
|
**Правило:** если установка зависимостей требует нестандартных шагов, описанных в README — строго следовать им. Если шаги не описаны — запросить у пользователя.
|
||||||
|
|
||||||
|
### 4A.2. Конфигурация
|
||||||
|
|
||||||
|
- Проверить наличие конфигурационных файлов (`.env.example`, `.env`, `config.yaml`)
|
||||||
|
- Если есть `.env.example`, скопировать в `.env` с настройками по умолчанию
|
||||||
|
- Если проекту требуется БД — проверить строку подключения, при необходимости создать БД или использовать SQLite для разработки
|
||||||
|
- Настроить pre-commit хуки, если они есть в проекте
|
||||||
|
|
||||||
|
### 4A.3. Линтеры и форматтеры
|
||||||
|
|
||||||
|
- Запустить линтер на всём проекте: записать результат
|
||||||
|
- Если линтер выдаёт ошибки — не исправлять, только зафиксировать в отчёте
|
||||||
|
- Убедиться, что исполнительный агент может запускать линтер (записать команду)
|
||||||
|
|
||||||
|
### 4A.4. Baseline-тесты
|
||||||
|
|
||||||
|
- Запустить все тесты проекта
|
||||||
|
- Записать в `.agent/context/baseline-test-report.log`:
|
||||||
|
- Команда запуска
|
||||||
|
- Общее количество тестов
|
||||||
|
- Пройдено / упало / пропущено
|
||||||
|
- Время выполнения
|
||||||
|
- Список упавших тестов (если есть)
|
||||||
|
- Если тесты не проходят — указать это в отчёте, но **не исправлять**
|
||||||
|
|
||||||
|
### 4A.5. Сборка проекта
|
||||||
|
|
||||||
|
- Выполнить полную сборку/компиляцию проекта
|
||||||
|
- Записать результат (успех/ошибка с логом)
|
||||||
|
- Сборка должна проходить без ошибок. Если не собирается — остановиться, сообщить пользователю.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ветка B: greenfield проект
|
||||||
|
|
||||||
|
### 4B.1. Инициализация проекта
|
||||||
|
|
||||||
|
- Создать базовую структуру директорий согласно design-report.md
|
||||||
|
- Инициализировать пакетный менеджер:
|
||||||
|
- Python: `pyproject.toml` (poetry, pdm, hatch) или `requirements.txt`
|
||||||
|
- Node: `package.json` и `npm init` / `yarn init`
|
||||||
|
- Go: `go mod init`
|
||||||
|
- Rust: `cargo init`
|
||||||
|
- Настроить базовый конфиг: `.env.example`, `config/` и т.д.
|
||||||
|
- Настроить линтер/форматтер: `ruff`, `eslint`, `gofmt` и т.д.
|
||||||
|
|
||||||
|
### 4B.2. Scaffold-код
|
||||||
|
|
||||||
|
Создать пустые заглушки для модулей, описанных в design-report:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# app/services/cashflow.py — заглушка
|
||||||
|
class CashflowService:
|
||||||
|
"""TBD — реализация в задаче T3"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Назначение: фиксировать структуру, чтобы исполнительный агент не думал о ней, а сразу писал реализацию.
|
||||||
|
|
||||||
|
### 4B.3. Установка зависимостей
|
||||||
|
|
||||||
|
- Установить базовые зависимости согласно стеку из design-report
|
||||||
|
- Если проект использует БД — установить драйвер/ORM
|
||||||
|
- Если проект использует API — установить фреймворк (FastAPI, Express и т.д.)
|
||||||
|
- Установить dev-зависимости: линтер, тестовый раннер, type stubs
|
||||||
|
|
||||||
|
### 4B.4. Базовые тесты (scaffold)
|
||||||
|
|
||||||
|
- Создать пустой тестовый файл для каждого модуля
|
||||||
|
- Настроить тестовый раннер (pytest, jest и т.д.)
|
||||||
|
- Записать в `.agent/context/baseline-test-report.log`: "0 tests — greenfield, scaffold готов"
|
||||||
|
|
||||||
|
### 4B.5. Проверка сборки
|
||||||
|
|
||||||
|
- Убедиться, что проект импортируется без ошибок
|
||||||
|
- Убедиться, что линтер проходит (без кода он должен проходить)
|
||||||
|
- Убедиться, что тестовый раннер запускается (0 tests, exit code 0)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Выход
|
||||||
|
|
||||||
|
- Работоспособное окружение / инициализированный проект
|
||||||
|
- `.agent/context/baseline-test-report.log` — результат прогона тестов
|
||||||
|
- `.agent/context/setup-report.log` — лог установки зависимостей и сборки
|
||||||
|
|
||||||
|
Обновить checkpoints.json: `phases.environment = "completed"`.
|
||||||
|
|
||||||
|
## Критерии завершения фазы
|
||||||
|
|
||||||
|
- [ ] Зависимости установлены / проект инициализирован
|
||||||
|
- [ ] Проект собирается / импортируется без ошибок
|
||||||
|
- [ ] Baseline-тесты запущены, результат записан
|
||||||
|
- [ ] `.agent/context/baseline-test-report.log` и `.agent/context/setup-report.log` созданы
|
||||||
|
- [ ] checkpoints.json обновлён
|
||||||
|
|
||||||
|
Если проект не собирается — **фаза считается проваленной**, checkpoints.json отмечает `phases.environment = "failed"`, управление возвращается пользователю.
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Протокол 07: Завершение сессии (HANDOFF)
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Легковесное завершение сессии: валидация структуры `.agent/`, финализация чекпоинтов, формирование сводки. Архивация и обновление project-state выполняются фазой METASTATE.
|
||||||
|
|
||||||
|
> **Важно:** если перед HANDOFF была выполнена фаза METASTATE (06) — архивация, project-state и handoff-summary уже готовы.
|
||||||
|
> HANDOFF в этом случае только валидирует и финализирует.
|
||||||
|
|
||||||
|
## Вход
|
||||||
|
|
||||||
|
- `.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. Проверка: была ли METASTATE?
|
||||||
|
|
||||||
|
Если существует `.agent/handoff-summary.md` и `.agent/context/project-state.md`:
|
||||||
|
- METASTATE уже выполнен
|
||||||
|
- Перейти к шагу 5.3 (Валидация)
|
||||||
|
|
||||||
|
Если нет:
|
||||||
|
- METASTATE не выполнялся (например, сессия завершается до execution)
|
||||||
|
- Перейти к шагу 5.2 (Лёгкая архивация)
|
||||||
|
|
||||||
|
### 5.2. Лёгкая архивация (если METASTATE не было)
|
||||||
|
|
||||||
|
Если есть completed задачи в manifest.json:
|
||||||
|
- Архивировать их в `.agent/archive/tasks/{id}.json`
|
||||||
|
- Заменить в manifest.json на one-liner
|
||||||
|
- Создать `.agent/archive/index.json`
|
||||||
|
|
||||||
|
Если нет completed задач — пропустить.
|
||||||
|
|
||||||
|
### 5.3. Валидация
|
||||||
|
|
||||||
|
Проверить:
|
||||||
|
|
||||||
|
- [ ] Все фазы отмечены как `completed` или `skipped` в checkpoints.json
|
||||||
|
- [ ] `.agent/` содержит обязательные файлы:
|
||||||
|
- `checkpoints.json`
|
||||||
|
- `context/analysis-report.md`
|
||||||
|
- `context/project-state.md`
|
||||||
|
- `tasks/manifest.json` + `tasks/manifest.md`
|
||||||
|
- `src/META_AGENT_GUIDE.md`
|
||||||
|
- `src/BOUNDARIES.md`
|
||||||
|
- `src/VERSION`
|
||||||
|
- `src/PROTOCOLS/`
|
||||||
|
- `src/TEMPLATES/`
|
||||||
|
- `rules/project-rules.md`
|
||||||
|
- [ ] В `.agent/tasks/manifest.json` нет циклических зависимостей
|
||||||
|
- [ ] Все acceptance criteria сформулированы измеримо
|
||||||
|
- [ ] Для каждой задачи указаны affected files и origin
|
||||||
|
- [ ] `.agent/src/` содержит актуальные исходники
|
||||||
|
- [ ] `AGENTS.md` присутствует в корне репозитория
|
||||||
|
|
||||||
|
### 5.4. Создание session-summary.md
|
||||||
|
|
||||||
|
Создать `.agent/session-summary.md` — краткая сводка сессии:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Session Summary
|
||||||
|
|
||||||
|
**Session:** <id>
|
||||||
|
**MetaAgent version:** 2.1.0
|
||||||
|
**Date:** <timestamp>
|
||||||
|
**Goal:** <goal>
|
||||||
|
|
||||||
|
## Phases Executed
|
||||||
|
- [x] INIT
|
||||||
|
- [x] ANALYSIS
|
||||||
|
- [x] ROADMAP
|
||||||
|
- [x] DESIGN
|
||||||
|
- [x] DECOMPOSITION
|
||||||
|
- [x] EXECUTION (N tasks)
|
||||||
|
- [x] METASTATE
|
||||||
|
- [x] HANDOFF
|
||||||
|
|
||||||
|
## Results
|
||||||
|
- Tasks completed: N
|
||||||
|
- Requests approved: N
|
||||||
|
- Files changed: [list]
|
||||||
|
|
||||||
|
## Next
|
||||||
|
Следующий агент: читай .agent/handoff-summary.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5. Финализация checkpoints
|
||||||
|
|
||||||
|
- Отметить `phases.handoff = "completed"`
|
||||||
|
- Записать финальный `last_updated`
|
||||||
|
|
||||||
|
### 5.6. Сигнал
|
||||||
|
|
||||||
|
```
|
||||||
|
HANDOFF COMPLETE
|
||||||
|
|
||||||
|
Session: <session_id>
|
||||||
|
Target: <target_repo>
|
||||||
|
Type: <existing | greenfield | scaffold>
|
||||||
|
Tasks: <N> total, <M> completed, <K> pending
|
||||||
|
|
||||||
|
Следующий агент начинает с .agent/handoff-summary.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Выход
|
||||||
|
|
||||||
|
- `.agent/session-summary.md`
|
||||||
|
- `.agent/checkpoints.json` (финальный)
|
||||||
|
- Если METASTATE не было: `.agent/archive/index.json`
|
||||||
|
|
||||||
|
## Критерии завершения
|
||||||
|
|
||||||
|
- [ ] Все артефакты на месте (согласно структуре .agent/)
|
||||||
|
- [ ] Если METASTATE не было — completed задачи архивированы
|
||||||
|
- [ ] session-summary.md создан
|
||||||
|
- [ ] checkpoints.json финализирован
|
||||||
|
- [ ] Сигнал отправлен пользователю
|
||||||
@@ -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»
|
||||||
|
- «подведи итог»
|
||||||
|
- «заверши сессию»
|
||||||
|
|
||||||
|
Может запускаться многократно в течение жизни проекта — после каждой группы выполненных задач.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# ADR-NNNN: <Заголовок решения>
|
||||||
|
|
||||||
|
**Статус:** proposed | accepted | deprecated | superseded
|
||||||
|
|
||||||
|
**Дата:** {{ date }}
|
||||||
|
|
||||||
|
**Контекст:** почему возникла необходимость в решении, какая проблема решается.
|
||||||
|
|
||||||
|
**Рассматриваемые альтернативы:**
|
||||||
|
1. Вариант A — описание
|
||||||
|
2. Вариант B — описание
|
||||||
|
3. Вариант C — описание
|
||||||
|
|
||||||
|
**Решение:** выбран вариант <A/B/C>.
|
||||||
|
|
||||||
|
**Обоснование:** почему выбран именно этот вариант (критерии: сложность, поддерживаемость, производительность, совместимость).
|
||||||
|
|
||||||
|
**Последствия:**
|
||||||
|
- Позитивные: ...
|
||||||
|
- Негативные: ...
|
||||||
|
- Риски: ...
|
||||||
|
|
||||||
|
**Invariant (если применимо):** ключевое правило, которое не должен нарушать исполнительный агент. Если можно — ссылка на тест, проверяющий invariant.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Analysis Report
|
||||||
|
|
||||||
|
## Session
|
||||||
|
|
||||||
|
- **Session ID:** `{{ session_id }}`
|
||||||
|
- **Target repo:** `{{ target_repo }}`
|
||||||
|
- **Date:** {{ date }}
|
||||||
|
- **Project type:** `{{ project_type }}` (existing / greenfield / scaffold)
|
||||||
|
|
||||||
|
## 1. Общая информация
|
||||||
|
|
||||||
|
- **README:** {{ readme_summary }}
|
||||||
|
- **Лицензия:** {{ license }}
|
||||||
|
- **CI/CD:** {{ ci_cd }}
|
||||||
|
- **Точка входа:** {{ entry_point }}
|
||||||
|
- **Система сборки:** {{ build_system }}
|
||||||
|
|
||||||
|
{% if project_type == "existing" or project_type == "scaffold" %}
|
||||||
|
## 2. Стек технологий
|
||||||
|
|
||||||
|
| Компонент | Значение |
|
||||||
|
|---|---|
|
||||||
|
| Язык | {{ language }} |
|
||||||
|
| Фреймворк | {{ framework }} |
|
||||||
|
| База данных | {{ database }} |
|
||||||
|
| Тестовый раннер | {{ test_runner }} |
|
||||||
|
| Пакетный менеджер | {{ package_manager }} |
|
||||||
|
| Линтер/форматтер | {{ linter }} |
|
||||||
|
|
||||||
|
## 3. Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
{{ directory_tree }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Паттерн:** {{ architecture_pattern }}
|
||||||
|
|
||||||
|
**Ключевые модули:**
|
||||||
|
|
||||||
|
| Модуль | Описание |
|
||||||
|
|---|---|
|
||||||
|
| {{ module }} | {{ description }} |
|
||||||
|
|
||||||
|
## 4. Конвенции
|
||||||
|
|
||||||
|
- **Стиль:** {{ code_style }}
|
||||||
|
- **Импорты:** {{ import_style }}
|
||||||
|
- **Типизация:** {{ typing_usage }}
|
||||||
|
- **Обработка ошибок:** {{ error_handling }}
|
||||||
|
- **Логирование:** {{ logging }}
|
||||||
|
|
||||||
|
## 5. Тесты
|
||||||
|
|
||||||
|
- **Команда запуска:** `{{ test_command }}`
|
||||||
|
- **Всего тестов:** {{ total_tests }}
|
||||||
|
- **Пройдено:** {{ passed }}
|
||||||
|
- **Упало:** {{ failed }}
|
||||||
|
- **Пропущено:** {{ skipped }}
|
||||||
|
- **Упавшие тесты:**
|
||||||
|
{% for test in failed_tests %}
|
||||||
|
- `{{ test }}`
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
## 6. Базовая проверка
|
||||||
|
|
||||||
|
- **Сборка:** {{ build_status }}
|
||||||
|
- **Запуск:** {{ run_status }}
|
||||||
|
- **Git status:** {{ git_status }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if project_type == "greenfield" or project_type == "scaffold" %}
|
||||||
|
## 7. Требования (из README)
|
||||||
|
|
||||||
|
### Функциональные требования
|
||||||
|
|
||||||
|
{% for req in functional_requirements %}
|
||||||
|
- {{ req }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
### Нефункциональные требования
|
||||||
|
|
||||||
|
{% for req in non_functional_requirements %}
|
||||||
|
- {{ req }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
### Бизнес-контекст
|
||||||
|
|
||||||
|
{% for item in business_context %}
|
||||||
|
- {{ item }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
### Неясные моменты / Вопросы
|
||||||
|
|
||||||
|
{% for question in open_questions %}
|
||||||
|
- {{ question }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
## 8. Примечания
|
||||||
|
|
||||||
|
{{ notes }}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# Design Report
|
||||||
|
|
||||||
|
## Session
|
||||||
|
|
||||||
|
- **Session ID:** `{{ session_id }}`
|
||||||
|
- **Target repo:** `{{ target_repo }}`
|
||||||
|
- **Date:** {{ date }}
|
||||||
|
|
||||||
|
## 1. Технологический стек
|
||||||
|
|
||||||
|
| Компонент | Выбор | Обоснование |
|
||||||
|
|---|---|---|
|
||||||
|
| Язык | {{ language }} | {{ language_rationale }} |
|
||||||
|
| Фреймворк | {{ framework }} | {{ framework_rationale }} |
|
||||||
|
| База данных | {{ database }} | {{ database_rationale }} |
|
||||||
|
| Инфраструктура | {{ infrastructure }} | {{ infrastructure_rationale }} |
|
||||||
|
|
||||||
|
## 2. High-Level архитектура
|
||||||
|
|
||||||
|
**Паттерн:** {{ architecture_pattern }}
|
||||||
|
|
||||||
|
```
|
||||||
|
{{ architecture_diagram }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Поток данных:**
|
||||||
|
1. {{ data_flow_step_1 }}
|
||||||
|
2. {{ data_flow_step_2 }}
|
||||||
|
3. {{ data_flow_step_3 }}
|
||||||
|
|
||||||
|
## 3. Модули
|
||||||
|
|
||||||
|
| Модуль | Ответственность | Ключевые компоненты | Зависит от |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `{{ module_path }}` | {{ responsibility }} | {{ components }} | {{ dependencies }} |
|
||||||
|
|
||||||
|
## 4. Модели данных
|
||||||
|
|
||||||
|
### Сущности
|
||||||
|
|
||||||
|
{% for entity in entities %}
|
||||||
|
### {{ entity.name }}
|
||||||
|
|
||||||
|
| Поле | Тип | Ограничения | Описание |
|
||||||
|
|---|---|---|---|
|
||||||
|
{% for field in entity.fields %}
|
||||||
|
| {{ field.name }} | {{ field.type }} | {{ field.constraints }} | {{ field.description }} |
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
**Связи:** {{ entity.relationships }}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
## 5. API / Интерфейсы
|
||||||
|
|
||||||
|
{% if has_api %}
|
||||||
|
| Метод | Путь | Описание | Request | Response |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
{% for endpoint in api_endpoints %}
|
||||||
|
| {{ endpoint.method }} | {{ endpoint.path }} | {{ endpoint.description }} | {{ endpoint.request }} | {{ endpoint.response }} |
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if has_gui %}
|
||||||
|
**Экраны:** {{ gui_screens }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if has_cli %}
|
||||||
|
**Команды:** {{ cli_commands }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
## 6. Обработка ошибок
|
||||||
|
|
||||||
|
- **Стратегия:** {{ error_strategy }}
|
||||||
|
- **Формат ошибок:** {{ error_format }}
|
||||||
|
- **Логирование:** {{ logging_strategy }}
|
||||||
|
|
||||||
|
## 7. Тестирование
|
||||||
|
|
||||||
|
- **Unit-тесты:** {{ unit_test_strategy }}
|
||||||
|
- **Integration-тесты:** {{ integration_test_strategy }}
|
||||||
|
- **Mock-стратегия:** {{ mock_strategy }}
|
||||||
|
- **Команда запуска:** `{{ test_command }}`
|
||||||
|
|
||||||
|
## 8. Alternative Architecture (если применимо)
|
||||||
|
|
||||||
|
| Критерий | Выбранная архитектура | Альтернатива |
|
||||||
|
|---|---|---|
|
||||||
|
| Название | {{ chosen_arch }} | {{ alt_arch }} |
|
||||||
|
| Сложность | {{ chosen_complexity }} | {{ alt_complexity }} |
|
||||||
|
| Почему не выбрана | — | {{ alt_rejection_reason }} |
|
||||||
|
|
||||||
|
## 9. ADR Reference (если применимо)
|
||||||
|
|
||||||
|
| ID | Решение | Файл |
|
||||||
|
|---|---|---|
|
||||||
|
{% for adr in adr_list %}
|
||||||
|
| {{ adr.id }} | {{ adr.title }} | `{{ adr.path }}` |
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
## 10. Risk Register (если применимо)
|
||||||
|
|
||||||
|
| # | Assumption | Impact | Mitigation |
|
||||||
|
|---|---|---|---|
|
||||||
|
{% for risk in risk_list %}
|
||||||
|
| {{ risk.id }} | {{ risk.assumption }} | {{ risk.impact }} | {{ risk.mitigation }} |
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
## 11. Предварительная группировка задач
|
||||||
|
|
||||||
|
| Задача | Описание | Тип |
|
||||||
|
|---|---|---|
|
||||||
|
| T1 | {{ task_1 }} | config |
|
||||||
|
| T2 | {{ task_2 }} | feature |
|
||||||
|
| T3 | {{ task_3 }} | feature |
|
||||||
|
| T4 | {{ task_4 }} | test |
|
||||||
|
|
||||||
|
## 12. Примечания
|
||||||
|
|
||||||
|
{{ notes }}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Handoff Summary
|
||||||
|
|
||||||
|
## Session Info
|
||||||
|
|
||||||
|
- **Session ID:** `{{ session_id }}`
|
||||||
|
- **Target Repo:** `{{ target_repo }}`
|
||||||
|
- **Goal:** {{ goal }}
|
||||||
|
- **Date:** {{ date }}
|
||||||
|
- **Duration:** {{ duration }}
|
||||||
|
- **Depth:** {{ depth }}
|
||||||
|
- **Config:** {{ config_summary }}
|
||||||
|
|
||||||
|
## Repo Summary
|
||||||
|
|
||||||
|
{{ repo_summary }}
|
||||||
|
|
||||||
|
## Project Type
|
||||||
|
|
||||||
|
- **Type:** {{ project_type }}
|
||||||
|
- **Design report:** {% if project_type == "greenfield" or project_type == "scaffold" %}`.agent/context/design-report.md`{% else %}—{% endif %}
|
||||||
|
|
||||||
|
## ADR Summary (если применимо)
|
||||||
|
|
||||||
|
{% if adr_count > 0 %}
|
||||||
|
Создано ADR: {{ adr_count }}
|
||||||
|
{% for adr in adr_list %}
|
||||||
|
- `{{ adr.path }}` — {{ adr.title }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
## Risk Register (если применимо)
|
||||||
|
|
||||||
|
{% if risk_count > 0 %}
|
||||||
|
Задокументировано допущений: {{ risk_count }}
|
||||||
|
Наиболее критичное: {{ top_risk }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
## Environment Status
|
||||||
|
|
||||||
|
- **Build:** {{ build_status }}
|
||||||
|
- **Tests:** {{ tests_passed }}/{{ tests_total }} passed
|
||||||
|
- **Baseline log:** `.agent/context/baseline-test-report.log`
|
||||||
|
- **Dependencies:** {{ deps_status }}
|
||||||
|
|
||||||
|
## Task Overview
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|---|---|
|
||||||
|
| Total | {{ total }} |
|
||||||
|
| Pending | {{ pending }} |
|
||||||
|
| In Progress | {{ in_progress }} |
|
||||||
|
| Completed | {{ completed }} |
|
||||||
|
| Failed/Skipped | {{ failed }} |
|
||||||
|
|
||||||
|
**Task by type:**
|
||||||
|
{% for type, count in tasks_by_type %}
|
||||||
|
- {{ type }}: {{ count }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
## Tasks (ordered)
|
||||||
|
|
||||||
|
{% for task in tasks %}
|
||||||
|
### {{ task.id }}: {{ task.title }}
|
||||||
|
- Type: {{ task.type }}
|
||||||
|
- Depends on: {{ task.depends_on | default("—") }}
|
||||||
|
- Files: {{ task.files | join(", ") }}
|
||||||
|
- Status: {{ task.status }}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Исполнительный агент начинает с задачи **{{ first_task }}**.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
{% for caveat in caveats %}
|
||||||
|
- {{ caveat }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
## Checkpoints
|
||||||
|
|
||||||
|
Файл: `.agent/checkpoints.json`
|
||||||
|
Актуальное состояние чекпоинтов прилагается.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# MetaAgent Request
|
||||||
|
# Для ручного заполнения перед запуском MetaAgent.
|
||||||
|
# Поместите этот файл в .agent/metaagent-request.md целевого репозитория.
|
||||||
|
# Если файл отсутствует — MetaAgent проведёт интервью (PROTOCOLS/00_CONFIG.md).
|
||||||
|
# Ответьте "default" на любой вопрос — будет использовано значение по умолчанию.
|
||||||
|
|
||||||
|
## Параметры сессии
|
||||||
|
|
||||||
|
| Функция | Вкл | Аргументы |
|
||||||
|
|---|---|---|
|
||||||
|
| ANALYSIS | ✓ | — |
|
||||||
|
| DESIGN | ✓ | adr=yes, alternative_arch=yes |
|
||||||
|
| RED_TEAM | ✗ | — |
|
||||||
|
| RISK_REGISTER | ✗ | — |
|
||||||
|
| DECOMPOSITION | ✓ | invariant_tests=yes |
|
||||||
|
| SETUP | ✓ | — |
|
||||||
|
| HANDOFF | ✓ | — |
|
||||||
|
|
||||||
|
## Глубина проработки
|
||||||
|
|
||||||
|
**Значение:** 6 (1-10)
|
||||||
|
|
||||||
|
| Уровень | Название | Описание |
|
||||||
|
|---|---|---|
|
||||||
|
| 1-2 | Scaffold | Только структура проекта + пустые модули |
|
||||||
|
| 3-4 | Light | (default) Быстрый дизайн + задачи без расширений |
|
||||||
|
| 5-6 | Standard | Полный ANALYSIS→DESIGN→DECOMP→SETUP→HANDOFF |
|
||||||
|
| 7-8 | Deep | Standard + ADR, Risk Register, Alternative Architecture |
|
||||||
|
| 9-10 | Maximum | Deep + Red Team Review, Executable Invariants |
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Сформулируйте задачу для MetaAgent.
|
||||||
|
|
||||||
|
## Дополнительно
|
||||||
|
|
||||||
|
- **Boundaries:** (опционально) ограничения, которые нельзя нарушать
|
||||||
|
- **Target:** путь к репозиторию или URL
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Project Rules
|
||||||
|
|
||||||
|
Правила, которым агент обязан следовать во всех фазах.
|
||||||
|
Добавляйте сюда условия, которые должны соблюдаться всегда — они будут прочитаны
|
||||||
|
перед началом каждой фазы и учтены при декомпозиции и реализации.
|
||||||
|
|
||||||
|
## Обязательные правила
|
||||||
|
|
||||||
|
- (укажите правила, например: «Всегда использовать tabs для отступов»)
|
||||||
|
|
||||||
|
## Запреты
|
||||||
|
|
||||||
|
- (укажите запреты, например: «Не трогать CI/CD конфигурацию»)
|
||||||
|
|
||||||
|
## Конвенции проекта
|
||||||
|
|
||||||
|
- (укажите конвенции, например: «Имена классов в PascalCase, функции в snake_case»)
|
||||||
@@ -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 }}
|
||||||
@@ -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 }}"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Risk Register
|
||||||
|
|
||||||
|
| # | Assumption | Impact if wrong | Mitigation | Review trigger |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| R1 | Пользователи имеют Python 3.11+ | Проект не запускается на старых версиях | Указать требование в README, CI-проверка | При жалобе на установку |
|
||||||
|
| R2 | JSON-файлы не превышают 10MB | Деградация производительности | Добавить лимит в model.py | При первом замедлении |
|
||||||
|
| R3 | ... | ... | ... | ... |
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Session Summary
|
||||||
|
|
||||||
|
**Session:** {{ session_id }}
|
||||||
|
**Target:** {{ target_repo }}
|
||||||
|
**Depth:** {{ depth }}
|
||||||
|
**Date:** {{ date }}
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Функция | Статус |
|
||||||
|
|---|---|
|
||||||
|
| ADR | {{ adr_enabled }} |
|
||||||
|
| Alternative Architecture | {{ alt_arch_enabled }} |
|
||||||
|
| Red Team | {{ red_team_enabled }} |
|
||||||
|
| Risk Register | {{ risk_register_enabled }} |
|
||||||
|
| Invariant Tests | {{ invariant_tests_enabled }} |
|
||||||
|
|
||||||
|
## Phase Status
|
||||||
|
|
||||||
|
| Phase | Status |
|
||||||
|
|---|---|
|
||||||
|
| ANALYSIS | {{ analysis_status }} |
|
||||||
|
| DESIGN | {{ design_status }} |
|
||||||
|
| RED_TEAM | {{ red_team_status }} |
|
||||||
|
| DECOMPOSITION | {{ decomposition_status }} |
|
||||||
|
| SETUP | {{ setup_status }} |
|
||||||
|
| HANDOFF | {{ handoff_status }} |
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- Task Manifest: `.agent/tasks/manifest.json`
|
||||||
|
- Handoff Summary: `.agent/handoff-summary.md`
|
||||||
|
- Design Report: `.agent/context/design-report.md`
|
||||||
|
- ADR: `.agent/decisions/` (если есть)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": ".agent/src/TEMPLATES/schemas/task-manifest-schema.json",
|
||||||
|
"version": "1.0",
|
||||||
|
"session_id": "{{ session_id }}",
|
||||||
|
"goal": "{{ goal }}",
|
||||||
|
"created_at": "{{ timestamp }}",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "T1",
|
||||||
|
"title": "{{ task_title }}",
|
||||||
|
"description": "{{ task_description }}",
|
||||||
|
"type": "feature|refactor|test|fix|config|docs",
|
||||||
|
"files": ["path/to/file1.py", "path/to/file2.py"],
|
||||||
|
"depends_on": [],
|
||||||
|
"acceptance_criteria": [
|
||||||
|
"Критерий 1: ...",
|
||||||
|
"Критерий 2: ..."
|
||||||
|
],
|
||||||
|
"context": "Дополнительная информация",
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Task Manifest
|
||||||
|
|
||||||
|
**Session:** {{ session_id }}
|
||||||
|
**Goal:** {{ goal }}
|
||||||
|
**Date:** {{ timestamp }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task Overview
|
||||||
|
|
||||||
|
| ID | Title | Type | Depends On | Status |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| T1 | {{ title }} | {{ type }} | — | pending |
|
||||||
|
| T2 | {{ title }} | {{ type }} | T1 | pending |
|
||||||
|
|
||||||
|
**Total tasks:** {{ count }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task Details
|
||||||
|
|
||||||
|
### T1: {{ title }}
|
||||||
|
|
||||||
|
**Type:** {{ type }}
|
||||||
|
**Description:** {{ description }}
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `{{ file_path }}`
|
||||||
|
|
||||||
|
**Depends on:** —
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] {{ criterion }}
|
||||||
|
- [ ] {{ criterion }}
|
||||||
|
|
||||||
|
**Context:** {{ context }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### T2: {{ title }}
|
||||||
|
|
||||||
|
...
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
2.1.0
|
||||||
@@ -0,0 +1,544 @@
|
|||||||
|
# WORKFLOW — Сквозной пример сессии
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сценарий A: Existing проект
|
||||||
|
|
||||||
|
**Цель:** Добавить в существующий FastAPI-проект ручку GET /health с тестами.
|
||||||
|
|
||||||
|
**Целевой репозиторий:** `github.com/example/fastapi-app`
|
||||||
|
|
||||||
|
**Пользователь:** "Добавь health-check endpoint и тесты к нему"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Фаза INIT
|
||||||
|
|
||||||
|
Мета-агент читает `.agent/metaagent-request.md`, клонирует репозиторий, создаёт `.agent/`, пишет начальный чекпоинт:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"metaagent_version": "1.1.0",
|
||||||
|
"session_id": "ses_abc123",
|
||||||
|
"target_repo": "/tmp/fastapi-app",
|
||||||
|
"goal": "Добавить GET /health с тестами",
|
||||||
|
"project_type": "existing",
|
||||||
|
"config": {
|
||||||
|
"depth": 6,
|
||||||
|
"design": { "adr": false, "alternative_arch": false },
|
||||||
|
"red_team": false,
|
||||||
|
"risk_register": false,
|
||||||
|
"decomposition": { "invariant_tests": false }
|
||||||
|
},
|
||||||
|
"phases": {
|
||||||
|
"analysis": "pending",
|
||||||
|
"design": "pending",
|
||||||
|
"red_team": "pending",
|
||||||
|
"decomposition": "pending",
|
||||||
|
"environment": "pending",
|
||||||
|
"handoff": "pending"
|
||||||
|
},
|
||||||
|
"tasks": [],
|
||||||
|
"last_updated": "2026-07-12T15:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Фаза ANALYSE
|
||||||
|
|
||||||
|
Мета-агент выполняет `PROTOCOLS/01_ANALYSIS.md`. Определяет тип проекта: `existing`.
|
||||||
|
|
||||||
|
Результат `.agent/context/analysis-report.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 2. Стек технологий
|
||||||
|
| Язык | Python 3.12 |
|
||||||
|
| Фреймворк | FastAPI |
|
||||||
|
| Тестовый раннер | pytest + httpx |
|
||||||
|
| Пакетный менеджер | pip + requirements.txt |
|
||||||
|
|
||||||
|
## 3. Архитектура
|
||||||
|
├── app/
|
||||||
|
│ ├── main.py
|
||||||
|
│ ├── routers/
|
||||||
|
│ │ └── users.py
|
||||||
|
│ ├── models/
|
||||||
|
│ │ └── user.py
|
||||||
|
│ └── schemas/
|
||||||
|
│ └── user.py
|
||||||
|
├── tests/
|
||||||
|
│ └── test_users.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Тесты запущены: **12 passed, 0 failed**.
|
||||||
|
|
||||||
|
Чекпоинт обновлён: `analysis = "completed"`, `project_type = "existing"`.
|
||||||
|
Фаза DESIGN пропускается.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Фаза DECOMPOSITION
|
||||||
|
|
||||||
|
Мета-агент выполняет `PROTOCOLS/03_DECOMPOSITION.md`.
|
||||||
|
|
||||||
|
Декомпозиция цели "Добавить GET /health с тестами":
|
||||||
|
|
||||||
|
| ID | Задача | Тип | Зависит от | AC |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| T1 | Создать health-check router | feature | — | Ручка возвращает 200 + {"status":"ok"} |
|
||||||
|
| T2 | Подключить router в main.py | config | T1 | Ручка доступна по /health |
|
||||||
|
| T3 | Написать тесты для /health | test | T2 | Тесты проверяют 200 и структуру ответа |
|
||||||
|
|
||||||
|
Создан `.agent/tasks/manifest.json` и `.agent/tasks/manifest.md`.
|
||||||
|
|
||||||
|
Чекпоинт обновлён: `decomposition = "completed"`. Tasks: T1-T3 со статусом `pending`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Фаза SETUP
|
||||||
|
|
||||||
|
Мета-агент выполняет `PROTOCOLS/04_ENVIRONMENT_SETUP.md` (ветка A: existing).
|
||||||
|
|
||||||
|
- `pip install -r requirements.txt` — OK
|
||||||
|
- Запуск pytest — OK, 12 passed (базовый тест)
|
||||||
|
- Результат в `.agent/context/baseline-test-report.log`
|
||||||
|
|
||||||
|
Чекпоинт обновлён: `environment = "completed"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Фаза HANDOFF
|
||||||
|
|
||||||
|
Мета-агент выполняет `PROTOCOLS/05_HANDOFF.md`.
|
||||||
|
|
||||||
|
Создан `.agent/handoff-summary.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Next Steps
|
||||||
|
Исполнительный агент начинает с задачи T1: "Создать health-check router".
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
- Придерживаться стиля существующего роутера users.py
|
||||||
|
- Не менять существующие тесты
|
||||||
|
- Убедиться, что response model соответствует JSON: {"status": "ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Чекпоинт финализирован:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"metaagent_version": "1.1.0",
|
||||||
|
"session_id": "ses_abc123",
|
||||||
|
"goal": "Добавить GET /health с тестами",
|
||||||
|
"project_type": "existing",
|
||||||
|
"config": {
|
||||||
|
"depth": 6,
|
||||||
|
"design": { "adr": false, "alternative_arch": false },
|
||||||
|
"red_team": false,
|
||||||
|
"risk_register": false,
|
||||||
|
"decomposition": { "invariant_tests": false }
|
||||||
|
},
|
||||||
|
"phases": {
|
||||||
|
"analysis": "completed",
|
||||||
|
"design": "skipped",
|
||||||
|
"red_team": "skipped",
|
||||||
|
"decomposition": "completed",
|
||||||
|
"environment": "completed",
|
||||||
|
"handoff": "completed"
|
||||||
|
},
|
||||||
|
"tasks": [
|
||||||
|
{ "id": "T1", "title": "Создать health-check router", "status": "pending" },
|
||||||
|
{ "id": "T2", "title": "Подключить router в main.py", "status": "pending" },
|
||||||
|
{ "id": "T3", "title": "Написать тесты для /health", "status": "pending" }
|
||||||
|
],
|
||||||
|
"last_updated": "2026-07-12T15:15:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Сигнал пользователю:
|
||||||
|
|
||||||
|
```
|
||||||
|
HANDOFF COMPLETE
|
||||||
|
Session: ses_abc123
|
||||||
|
Target: /tmp/fastapi-app
|
||||||
|
Type: existing
|
||||||
|
Tasks: 3 tasks ready
|
||||||
|
|
||||||
|
Исполнительный агент может начинать с задачи T1.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сценарий B: Greenfield проект (Cashflow Forecasting)
|
||||||
|
|
||||||
|
**Цель:** Спроектировать и реализовать MVP сервиса прогнозирования денежных потоков.
|
||||||
|
|
||||||
|
**Целевой репозиторий:** `github.com/example/cashflow-app`
|
||||||
|
|
||||||
|
**README:** README содержит описание:
|
||||||
|
> Сервис для прогнозирования движения денежных средств (cashflow forecasting).
|
||||||
|
> Пользователь загружает CSV с транзакциями, сервис строит прогноз на N дней вперёд.
|
||||||
|
> Стек: Python, FastAPI, SQLite, matplotlib для графиков.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Фаза INIT
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"metaagent_version": "1.1.0",
|
||||||
|
"session_id": "ses_def456",
|
||||||
|
"target_repo": "/tmp/cashflow-app",
|
||||||
|
"goal": "Спроектировать и реализовать MVP сервиса прогнозирования денежных потоков",
|
||||||
|
"project_type": "greenfield",
|
||||||
|
"config": {
|
||||||
|
"depth": 7,
|
||||||
|
"design": { "adr": true, "alternative_arch": true },
|
||||||
|
"red_team": false,
|
||||||
|
"risk_register": true,
|
||||||
|
"decomposition": { "invariant_tests": true }
|
||||||
|
},
|
||||||
|
"phases": {
|
||||||
|
"analysis": "pending",
|
||||||
|
"design": "pending",
|
||||||
|
"red_team": "pending",
|
||||||
|
"decomposition": "pending",
|
||||||
|
"environment": "pending",
|
||||||
|
"handoff": "pending"
|
||||||
|
},
|
||||||
|
"tasks": [],
|
||||||
|
"last_updated": "2026-07-12T16:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Фаза ANALYSE
|
||||||
|
|
||||||
|
Мета-агент выполняет `PROTOCOLS/01_ANALYSIS.md`. Определяет тип проекта: `greenfield`.
|
||||||
|
|
||||||
|
Сканирование корня: пусто (кроме README.md, LICENSE, .gitignore).
|
||||||
|
|
||||||
|
Извлечение требований из README:
|
||||||
|
|
||||||
|
| Тип | Требование |
|
||||||
|
|---|---|
|
||||||
|
| Функциональное | Загрузка CSV с транзакциями |
|
||||||
|
| Функциональное | Прогноз на N дней вперёд |
|
||||||
|
| Нефункциональное | Python, FastAPI |
|
||||||
|
| Нефункциональное | SQLite |
|
||||||
|
| Нефункциональное | matplotlib для графиков |
|
||||||
|
|
||||||
|
Чекпоинт: `analysis = "completed"`, `project_type = "greenfield"`.
|
||||||
|
|
||||||
|
Так как проект greenfield — мета-агент переходит к фазе DESIGN.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Фаза DESIGN
|
||||||
|
|
||||||
|
Мета-агент выполняет `PROTOCOLS/02_DESIGN.md`.
|
||||||
|
|
||||||
|
Результат `.agent/context/design-report.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 1. Технологический стек
|
||||||
|
| Язык | Python 3.12 |
|
||||||
|
| Фреймворк | FastAPI + Pydantic |
|
||||||
|
| БД | SQLite + SQLAlchemy |
|
||||||
|
| Визуализация | matplotlib |
|
||||||
|
| Тесты | pytest |
|
||||||
|
|
||||||
|
## 2. Архитектура
|
||||||
|
[Client] → HTTP → [FastAPI] → [CashflowService] → [SQLite]
|
||||||
|
↓
|
||||||
|
[ForecastEngine] → [matplotlib]
|
||||||
|
|
||||||
|
## 3. Модули
|
||||||
|
| Модуль | Ответственность |
|
||||||
|
|---|---|
|
||||||
|
| app/main.py | Точка входа, роуты |
|
||||||
|
| app/models/transaction.py | Модель транзакции |
|
||||||
|
| app/services/cashflow.py | Бизнес-логика |
|
||||||
|
| app/services/forecast.py | Алгоритм прогноза |
|
||||||
|
| app/services/upload.py | Парсинг CSV |
|
||||||
|
| app/schemas/ | Pydantic схемы |
|
||||||
|
|
||||||
|
## 4. Модели
|
||||||
|
Transaction: id, date, amount, category, description
|
||||||
|
|
||||||
|
## 5. API
|
||||||
|
POST /upload — загрузить CSV
|
||||||
|
GET /forecast?days=30 — прогноз + график
|
||||||
|
|
||||||
|
## 6. Задачи (pre-grouped)
|
||||||
|
T1: init — проект, зависимости, scaffold
|
||||||
|
T2: models — модели + миграции
|
||||||
|
T3: upload — загрузка CSV
|
||||||
|
T4: forecast — алгоритм прогноза
|
||||||
|
T5: API — endpoints
|
||||||
|
T6: tests — тесты
|
||||||
|
```
|
||||||
|
|
||||||
|
Чекпоинт: `design = "completed"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Фаза DECOMPOSITION
|
||||||
|
|
||||||
|
Мета-агент выполняет `PROTOCOLS/03_DECOMPOSITION.md`, используя design-report.
|
||||||
|
|
||||||
|
Итоговые задачи:
|
||||||
|
|
||||||
|
| ID | Задача | Тип | Зависит от |
|
||||||
|
|---|---|---|---|
|
||||||
|
| T1 | Инициализация проекта + зависимости | config | — |
|
||||||
|
| T2 | Модель Transaction + SQLAlchemy + SQLite | feature | T1 |
|
||||||
|
| T3 | Сервис загрузки и парсинга CSV | feature | T2 |
|
||||||
|
| T4 | ForecastEngine — алгоритм прогноза | feature | T2 |
|
||||||
|
| T5 | API endpoints + документация | feature | T3, T4 |
|
||||||
|
| T6 | Тесты (unit + integration) | test | T5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Фаза SETUP
|
||||||
|
|
||||||
|
Мета-агент выполняет `PROTOCOLS/04_ENVIRONMENT_SETUP.md` (ветка B: greenfield).
|
||||||
|
|
||||||
|
- `poetry init` + создание pyproject.toml
|
||||||
|
- Установка fastapi, uvicorn, sqlalchemy, matplotlib, pytest
|
||||||
|
- Создание scaffold-структуры: `app/models/`, `app/services/`, `app/schemas/`, `tests/`
|
||||||
|
- Пустые заглушки модулей
|
||||||
|
- `.agent/context/baseline-test-report.log`: "0 tests — greenfield, scaffold готов"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Фаза HANDOFF
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
HANDOFF COMPLETE
|
||||||
|
Session: ses_def456
|
||||||
|
Target: /tmp/cashflow-app
|
||||||
|
Type: greenfield
|
||||||
|
Config: depth=7, adr=yes, risk_register=yes, invariant_tests=yes
|
||||||
|
Tasks: 6 tasks ready
|
||||||
|
|
||||||
|
Исполнительный агент может начинать с задачи T1 (init).
|
||||||
|
Архитектурный план: .agent/context/design-report.md
|
||||||
|
ADR: .agent/decisions/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## После HANDOFF: работа исполнительного агента
|
||||||
|
|
||||||
|
Исполнительный агент читает `.agent/handoff-summary.md`, `.agent/tasks/manifest.json`, выполняет задачи по порядку, обновляя checkpoints.json после каждой.
|
||||||
|
|
||||||
|
После завершения всех задач:
|
||||||
|
|
||||||
|
```
|
||||||
|
ALL TASKS COMPLETE
|
||||||
|
Session: ses_def456
|
||||||
|
Tasks: 6/6 completed
|
||||||
|
|
||||||
|
T1: Инициализация проекта ✓
|
||||||
|
T2: Модель Transaction ✓
|
||||||
|
T3: Сервис загрузки CSV ✓
|
||||||
|
T4: ForecastEngine ✓
|
||||||
|
T5: API endpoints ✓
|
||||||
|
T6: Тесты ✓
|
||||||
|
|
||||||
|
Все тесты проходят: 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
|
||||||
|
```
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# MetaAgent — установка исходников в целевой проект
|
||||||
|
# Usage: .\install.ps1 [[-Path] target_path] [-Check] [-Update]
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Path = "",
|
||||||
|
[switch]$Check,
|
||||||
|
[switch]$Update,
|
||||||
|
[switch]$Help
|
||||||
|
)
|
||||||
|
|
||||||
|
$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 {
|
||||||
|
@"
|
||||||
|
Usage: install.ps1 [[-Path] target_path] [-Check] [-Update] [-Help]
|
||||||
|
|
||||||
|
Install MetaAgent sources into <target>/.agent/src/
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-Path Path to target project (default: interactive prompt)
|
||||||
|
-Check Dry-run: only check target readiness, no install
|
||||||
|
-Update Overwrite existing files in .agent/src/
|
||||||
|
-Help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
.\install.ps1
|
||||||
|
.\install.ps1 -Path C:\Projects\MyApp
|
||||||
|
.\install.ps1 -Path C:\Projects\MyApp -Check
|
||||||
|
.\install.ps1 -Path C:\Projects\MyApp -Update
|
||||||
|
"@
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Help) { Show-Usage }
|
||||||
|
|
||||||
|
# --- resolve target ---
|
||||||
|
$TargetPath = $Path
|
||||||
|
if (-not $TargetPath) {
|
||||||
|
$TargetPath = Read-Host "Enter path to target project"
|
||||||
|
}
|
||||||
|
$TargetPath = $TargetPath.Trim()
|
||||||
|
|
||||||
|
# --- pre-flight -----------------------------------------------------------
|
||||||
|
Write-Header "Pre-flight"
|
||||||
|
|
||||||
|
# 1. target exists?
|
||||||
|
if (-not (Test-Path $TargetPath -PathType Container)) {
|
||||||
|
Write-Fail "Target directory '$TargetPath' does not exist."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$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"
|
||||||
|
$SrcDir = Join-Path $AgentDir "src"
|
||||||
|
$VersionFile = Join-Path $MetaAgentSrc "VERSION"
|
||||||
|
|
||||||
|
$Version = if (Test-Path $VersionFile -PathType Leaf) {
|
||||||
|
(Get-Content $VersionFile -Raw -Encoding UTF8).Trim()
|
||||||
|
} else { "?" }
|
||||||
|
|
||||||
|
$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
|
||||||
|
|
||||||
|
function Copy-File {
|
||||||
|
param([string]$Src, [string]$DstDir)
|
||||||
|
$name = Split-Path $Src -Leaf
|
||||||
|
$dst = Join-Path $DstDir $name
|
||||||
|
if (-not (Test-Path $Src -PathType Leaf)) {
|
||||||
|
Write-Skip "$name (source not found)"
|
||||||
|
$script:skipCount++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ($Update -or -not (Test-Path $dst)) {
|
||||||
|
try {
|
||||||
|
Copy-Item $Src $dst -Force -ErrorAction Stop
|
||||||
|
Write-Ok $name
|
||||||
|
$script:copyCount++
|
||||||
|
} catch {
|
||||||
|
Write-Fail $name
|
||||||
|
$script:failCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Skip "$name (exists, use -Update to overwrite)"
|
||||||
|
$script:skipCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Copy-Dir {
|
||||||
|
param([string]$Src, [string]$DstDir)
|
||||||
|
$name = Split-Path $Src -Leaf
|
||||||
|
$dst = Join-Path $DstDir $name
|
||||||
|
if (-not (Test-Path $Src -PathType Container)) {
|
||||||
|
Write-Skip "$name/ (source not found)"
|
||||||
|
$script:skipCount++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$null = New-Item -ItemType Directory -Path $dst -Force
|
||||||
|
try {
|
||||||
|
if ($Update) {
|
||||||
|
Get-ChildItem $Src | ForEach-Object {
|
||||||
|
Copy-Item $_.FullName $dst -Recurse -Force -ErrorAction Stop
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Get-ChildItem $Src | ForEach-Object {
|
||||||
|
$targetPath = Join-Path $dst $_.Name
|
||||||
|
if (-not (Test-Path $targetPath)) {
|
||||||
|
Copy-Item $_.FullName $dst -Recurse -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Ok "$name/"
|
||||||
|
$script:copyCount++
|
||||||
|
} catch {
|
||||||
|
Write-Fail "$name/ (partial copy)"
|
||||||
|
$script:failCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-File (Join-Path $MetaAgentSrc "META_AGENT_GUIDE.md") $SrcDir
|
||||||
|
Copy-File (Join-Path $MetaAgentSrc "BOUNDARIES.md") $SrcDir
|
||||||
|
Copy-File (Join-Path $MetaAgentSrc "WORKFLOW.md") $SrcDir
|
||||||
|
Copy-File (Join-Path $MetaAgentSrc "VERSION") $SrcDir
|
||||||
|
Copy-Dir (Join-Path $MetaAgentSrc "PROTOCOLS") $SrcDir
|
||||||
|
Copy-Dir (Join-Path $MetaAgentSrc "TEMPLATES") $SrcDir
|
||||||
|
Copy-File (Join-Path $MetaAgentSrc "install.sh") $SrcDir
|
||||||
|
Copy-File (Join-Path $MetaAgentSrc "install.ps1") $SrcDir
|
||||||
|
|
||||||
|
# --- phase 3: AGENTS.md --------------------------------------------------
|
||||||
|
Write-Header "AGENTS.md"
|
||||||
|
|
||||||
|
if (-not (Test-Path $AgentsMd -PathType Leaf)) {
|
||||||
|
$content = @"
|
||||||
|
# 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 created"
|
||||||
|
} elseif ($Update) {
|
||||||
|
$content = @"
|
||||||
|
# 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 {
|
||||||
|
Write-Skip "AGENTS.md (exists, use -Update to overwrite)"
|
||||||
|
$script:skipCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- summary -------------------------------------------------------------
|
||||||
|
Write-Header "Summary"
|
||||||
|
Write-Host " MetaAgent v$Version → $SrcDir"
|
||||||
|
Write-Host ""
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# MetaAgent — установка исходников в целевой проект
|
||||||
|
# Usage: ./install.sh [--check|--update] [target_path]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
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() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 [--check|--update] [target_path]
|
||||||
|
|
||||||
|
Install MetaAgent sources into <target>/.agent/src/
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--check, -c Dry-run: only check target readiness, no install
|
||||||
|
--update, -u Overwrite existing files in .agent/src/
|
||||||
|
--help, -h Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$0
|
||||||
|
$0 /path/to/project
|
||||||
|
$0 --check /path/to/project
|
||||||
|
$0 --update /path/to/project
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- arg parsing ---
|
||||||
|
CHECK=false
|
||||||
|
UPDATE=false
|
||||||
|
TARGET_PATH=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--check|-c) CHECK=true; shift ;;
|
||||||
|
--update|-u) UPDATE=true; shift ;;
|
||||||
|
--help|-h) usage ;;
|
||||||
|
--*) echo "${red}Unknown option:${rst} $1"; usage ;;
|
||||||
|
*) TARGET_PATH="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- resolve target ---
|
||||||
|
if [[ -z "$TARGET_PATH" ]]; then
|
||||||
|
read -r -p "Enter path to target project: " TARGET_PATH
|
||||||
|
fi
|
||||||
|
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)" || {
|
||||||
|
fail "Cannot access '$TARGET_PATH'."
|
||||||
|
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"
|
||||||
|
SRC_DIR="$AGENT_DIR/src"
|
||||||
|
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"
|
||||||
|
DECISIONS_DIR="$AGENT_DIR/decisions"
|
||||||
|
TASKS_DIR="$AGENT_DIR/tasks"
|
||||||
|
CONTEXT_DIR="$AGENT_DIR/context"
|
||||||
|
ARCHIVE_DIR="$AGENT_DIR/archive"
|
||||||
|
ARCHIVE_TASKS_DIR="$ARCHIVE_DIR/tasks"
|
||||||
|
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_file() {
|
||||||
|
local src="$1" dst_dir="$2"
|
||||||
|
local name; name="$(basename "$src")"
|
||||||
|
local dst="$dst_dir/$name"
|
||||||
|
if [[ ! -f "$src" ]]; then
|
||||||
|
skip "$name (source not found)"
|
||||||
|
((SKIP_COUNT += 1))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ "$UPDATE" == true ]] || [[ ! -f "$dst" ]]; then
|
||||||
|
if cp "$src" "$dst"; then
|
||||||
|
ok "$name"
|
||||||
|
((COPY_COUNT++))
|
||||||
|
else
|
||||||
|
fail "$name"
|
||||||
|
((FAIL_COUNT++))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
skip "$name (exists, use --update to overwrite)"
|
||||||
|
((SKIP_COUNT += 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_dir() {
|
||||||
|
local src="$1" dst_dir="$2"
|
||||||
|
local name; name="$(basename "$src")"
|
||||||
|
local dst="$dst_dir/$name"
|
||||||
|
if [[ ! -d "$src" ]]; then
|
||||||
|
skip "$name/ (source not found)"
|
||||||
|
((SKIP_COUNT += 1))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
mkdir -p "$dst"
|
||||||
|
if [[ "$UPDATE" == true ]]; then
|
||||||
|
if cp -rf "$src"/* "$dst/" 2>/dev/null; then
|
||||||
|
ok "$name/"
|
||||||
|
((COPY_COUNT++))
|
||||||
|
else
|
||||||
|
fail "$name/ (partial copy)"
|
||||||
|
((FAIL_COUNT++))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
cp -rn "$src"/* "$dst/" 2>/dev/null || true
|
||||||
|
ok "$name/"
|
||||||
|
((COPY_COUNT++))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_file "$METAAGENT_SRC/META_AGENT_GUIDE.md" "$SRC_DIR"
|
||||||
|
copy_file "$METAAGENT_SRC/BOUNDARIES.md" "$SRC_DIR"
|
||||||
|
copy_file "$METAAGENT_SRC/WORKFLOW.md" "$SRC_DIR"
|
||||||
|
copy_file "$METAAGENT_SRC/VERSION" "$SRC_DIR"
|
||||||
|
copy_dir "$METAAGENT_SRC/PROTOCOLS" "$SRC_DIR"
|
||||||
|
copy_dir "$METAAGENT_SRC/TEMPLATES" "$SRC_DIR"
|
||||||
|
copy_file "$METAAGENT_SRC/install.sh" "$SRC_DIR"
|
||||||
|
copy_file "$METAAGENT_SRC/install.ps1" "$SRC_DIR"
|
||||||
|
|
||||||
|
# --- phase 3: AGENTS.md --------------------------------------------------
|
||||||
|
header "AGENTS.md"
|
||||||
|
|
||||||
|
create_agents_md() {
|
||||||
|
cat > "$1" << AGENTS_EOF
|
||||||
|
# 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 до актуальной версии.
|
||||||
|
AGENTS_EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ! -f "$AGENTS_MD" ]]; then
|
||||||
|
create_agents_md "$AGENTS_MD"
|
||||||
|
ok "AGENTS.md created"
|
||||||
|
elif [[ "$UPDATE" == true ]]; then
|
||||||
|
create_agents_md "$AGENTS_MD"
|
||||||
|
ok "AGENTS.md updated"
|
||||||
|
else
|
||||||
|
skip "AGENTS.md (exists, use --update to overwrite)"
|
||||||
|
((SKIP_COUNT += 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- summary -------------------------------------------------------------
|
||||||
|
header "Summary"
|
||||||
|
echo " MetaAgent v$VERSION → $SRC_DIR"
|
||||||
|
echo ""
|
||||||
|
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
|
||||||
+54
-153
@@ -1,177 +1,78 @@
|
|||||||
{
|
{
|
||||||
"$schema": "metaagent-task-manifest",
|
"$schema": "metaagent-task-manifest",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"session_id": "metaagent-001",
|
"session_id": "metaagent-003",
|
||||||
"goal": "Спроектируй и реализуй MVP по README",
|
"goal": "i18n (ru/en) — инфраструктура и обёртка строк, контроль актуальности переводов",
|
||||||
"created_at": "2026-07-12T12: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, установить зависимости (openpyxl, typer, pytest, ruff), настроить ruff",
|
"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 запускается (0 tests, exit code 0)"
|
"setup_i18n() читает CF_LANG из окружения"
|
||||||
],
|
],
|
||||||
"context": "Стек: Python 3.11+, openpyxl, typer, pytest, ruff. Структура из design-report.md раздел 3.",
|
"status": "completed"
|
||||||
"status": "pending"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T2",
|
"id": "T10",
|
||||||
"title": "Модель данных (dataclass + JSON serialization)",
|
"title": "Обёртка CLI-строк в t()",
|
||||||
"description": "Реализовать все сущности: Account, Transaction, RecurringCashflow, Asset, Liability, ForecastScenario, FinancialModel. Каждая — dataclass с методами to_dict/from_dict. FinancialModel — корневой объект с методами save/load JSON.",
|
"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 не использует)",
|
||||||
"demo-скрипт создаёт модель с тестовыми данными"
|
"ruff check проходит"
|
||||||
],
|
],
|
||||||
"context": "Модели определены в design-report.md раздел 4. Использовать uuid4 для id.",
|
"status": "completed"
|
||||||
"status": "pending"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T3",
|
"id": "T11",
|
||||||
"title": "Forecast Engine (базовый прогноз)",
|
"title": "AI-промпты через i18n",
|
||||||
"description": "Реализовать ForecastService с методами: forecast_cashflow (на N месяцев), apply_recurring (генерация 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')"
|
||||||
"Обязательства учитываются с процентами и платежами"
|
|
||||||
],
|
],
|
||||||
"context": "Расчёт: balance_{t+1} = balance_t + income_t - expense_t + asset_growth_t - liability_change_t. См. design-report раздел 2.",
|
"status": "completed"
|
||||||
"status": "pending"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T4",
|
"id": "T12",
|
||||||
"title": "Scenario Analysis",
|
"title": "Тесты i18n",
|
||||||
"description": "Реализовать ScenarioService с методами: сценарии (optimistic, pessimistic, baseline), 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%)",
|
|
||||||
"Сравнение сценариев возвращает сводку различий"
|
|
||||||
],
|
|
||||||
"context": "Сценарии меняют параметры модели перед прогнозом. Использовать copy модели для каждого сценария.",
|
|
||||||
"status": "pending"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T5",
|
|
||||||
"title": "Excel Sync (import/export)",
|
|
||||||
"description": "Реализовать ExcelSync: чтение модели из .xlsx (листы: Accounts, Transactions, Assets, Liabilities, Recurring), запись результатов прогноза в новый .xlsx. Использовать openpyxl.",
|
|
||||||
"type": "feature",
|
|
||||||
"files": [
|
|
||||||
"sync/__init__.py",
|
|
||||||
"sync/excel_sync.py"
|
|
||||||
],
|
|
||||||
"depends_on": ["T2"],
|
|
||||||
"acceptance_criteria": [
|
|
||||||
"Импорт из Excel заполняет FinancialModel",
|
|
||||||
"Экспорт FinancialModel в Excel создаёт корректный .xlsx",
|
|
||||||
"Обработка ошибок при невалидном формате Excel"
|
|
||||||
],
|
|
||||||
"context": "Каждая сущность — отдельный лист. Заголовки колонок = поля dataclass. См. design-report раздел 5.",
|
|
||||||
"status": "pending"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T6",
|
|
||||||
"title": "CLI (Typer) — все команды",
|
|
||||||
"description": "Реализовать CLI через Typer с командами: init, import, export, forecast, scenario, analyze. Главный entry point — консольная команда 'cf'.",
|
|
||||||
"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' применяет и выводит сценарий"
|
|
||||||
],
|
|
||||||
"context": "Typer entry point. Команды описаны в design-report раздел 5. rich для форматирования таблиц.",
|
|
||||||
"status": "pending"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T7",
|
|
||||||
"title": "AI Assistant (промпты + интерфейс)",
|
|
||||||
"description": "Реализовать AssistantService: генерация промптов для AI на основе модели и прогноза, форматирование контекста (JSON-дамп модели + результаты forecast), заглушка для вызова AI API. Промпты на русском языке для анализа фин. состояния.",
|
|
||||||
"type": "feature",
|
|
||||||
"files": [
|
|
||||||
"ai/__init__.py",
|
|
||||||
"ai/prompts.py",
|
|
||||||
"ai/assistant.py"
|
|
||||||
],
|
|
||||||
"depends_on": ["T3"],
|
|
||||||
"acceptance_criteria": [
|
|
||||||
"Промпт 'analyze' включает модель и прогноз в JSON",
|
|
||||||
"Промпт 'advice' формирует запрос на финансовые рекомендации",
|
|
||||||
"AssistantService возвращает структурированный ответ (заглушка)"
|
|
||||||
],
|
|
||||||
"context": "AI — заглушка для MVP. Промпты должны быть готовы для реального API. См. design-report раздел 7.",
|
|
||||||
"status": "pending"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T8",
|
|
||||||
"title": "Тесты на все модули",
|
|
||||||
"description": "Написать pytest-тесты для cashflow_model (сериализация), engine (forecast + scenarios), sync (excel roundtrip), ai (prompts), cli (invocation).",
|
|
||||||
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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()"
|
||||||
],
|
],
|
||||||
"context": "Использовать tmp_path для временных файлов. conftest.py с fixture для тестовой модели.",
|
|
||||||
"status": "pending"
|
"status": "pending"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+12
-199
@@ -1,8 +1,8 @@
|
|||||||
# Task Manifest
|
# Task Manifest
|
||||||
|
|
||||||
**Session:** metaagent-001
|
**Session:** metaagent-002
|
||||||
**Goal:** Спроектируй и реализуй MVP по README
|
**Goal:** Обновление metaagent-артефактов до v1.0.0, валидация существующего кода и окружения
|
||||||
**Date:** 2026-07-12T12:00:00Z
|
**Date:** 2026-07-12T20:00:00Z
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -10,200 +10,13 @@
|
|||||||
|
|
||||||
| ID | Title | Type | Depends On | Status |
|
| ID | Title | Type | Depends On | Status |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| T1 | Инициализация проекта и зависимостей | config | — | pending |
|
| T1 | Инициализация проекта и зависимостей | config | — | completed |
|
||||||
| T2 | Модель данных (dataclass + JSON serialization) | feature | T1 | pending |
|
| T2 | Модель данных (dataclass + JSON serialization) | feature | T1 | completed |
|
||||||
| T3 | Forecast Engine (базовый прогноз) | feature | T2 | pending |
|
| T3 | Forecast Engine (базовый прогноз) | feature | T2 | completed |
|
||||||
| T4 | Scenario Analysis | feature | T3 | pending |
|
| T4 | Scenario Analysis | feature | T3 | completed |
|
||||||
| T5 | Excel Sync (import/export) | feature | T2 | pending |
|
| T5 | Excel Sync (import/export) | feature | T2 | completed |
|
||||||
| T6 | CLI (Typer) — все команды | feature | T2, T3, T4, T5, T7 | pending |
|
| T6 | CLI (Typer) — все команды | feature | T2, T3, T4, T5, T7 | completed |
|
||||||
| T7 | AI Assistant (промпты + интерфейс) | feature | T3 | pending |
|
| T7 | AI Assistant (промпты + интерфейс) | feature | T3 | completed |
|
||||||
| T8 | Тесты на все модули | test | T2, T3, T4, T5, T6, T7 | pending |
|
| T8 | Тесты на все модули | test | T2, T3, T4, T5, T6, T7 | completed |
|
||||||
|
|
||||||
**Total tasks:** 8
|
**Total tasks:** 8 — all completed
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task Details
|
|
||||||
|
|
||||||
### T1: Инициализация проекта и зависимостей
|
|
||||||
|
|
||||||
**Type:** config
|
|
||||||
**Description:** Создать структуру директорий, pyproject.toml, venv, установить зависимости (openpyxl, typer, pytest, ruff), настроить ruff
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `pyproject.toml`
|
|
||||||
- `cashflow_model/__init__.py`
|
|
||||||
- `sync/__init__.py`
|
|
||||||
- `engine/__init__.py`
|
|
||||||
- `ai/__init__.py`
|
|
||||||
- `cli/__init__.py`
|
|
||||||
- `data/.gitkeep`
|
|
||||||
- `exports/.gitkeep`
|
|
||||||
|
|
||||||
**Depends on:** —
|
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
|
||||||
- [ ] pyproject.toml создан с правильными зависимостями
|
|
||||||
- [ ] Все директории модулей созданы с __init__.py
|
|
||||||
- [ ] ruff lint проходит без ошибок на пустых модулях
|
|
||||||
- [ ] pytest запускается (0 tests, exit code 0)
|
|
||||||
|
|
||||||
**Context:** Стек: Python 3.11+, openpyxl, typer, pytest, ruff. Структура из design-report.md раздел 3.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### T2: Модель данных (dataclass + JSON serialization)
|
|
||||||
|
|
||||||
**Type:** feature
|
|
||||||
**Description:** Реализовать все сущности: Account, Transaction, RecurringCashflow, Asset, Liability, ForecastScenario, FinancialModel. Каждая — dataclass с методами to_dict/from_dict. FinancialModel — корневой объект с методами save/load JSON.
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `cashflow_model/__init__.py`
|
|
||||||
- `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:**
|
|
||||||
- [ ] Все сущности — dataclass с правильными полями и типами
|
|
||||||
- [ ] FinancialModel корректно сохраняется и загружается из JSON
|
|
||||||
- [ ] Создание Account, Transaction, Asset, Liability через конструктор работает
|
|
||||||
- [ ] demo-скрипт создаёт модель с тестовыми данными
|
|
||||||
|
|
||||||
**Context:** Модели определены в design-report.md раздел 4. Использовать uuid4 для id.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### T3: Forecast Engine (базовый прогноз)
|
|
||||||
|
|
||||||
**Type:** feature
|
|
||||||
**Description:** Реализовать ForecastService с методами: forecast_cashflow (на N месяцев), apply_recurring (генерация recurring-транзакций), project_balance. Алгоритм: начальный баланс + доходы - расходы + изменения по активам/обязательствам.
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `engine/__init__.py`
|
|
||||||
- `engine/forecast.py`
|
|
||||||
|
|
||||||
**Depends on:** T2
|
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
|
||||||
- [ ] forecast_cashflow(months=12) возвращает список помесячных балансов
|
|
||||||
- [ ] Регулярные платежи корректно проецируются на будущие периоды
|
|
||||||
- [ ] Активы учитываются с ростом (growth_rate)
|
|
||||||
- [ ] Обязательства учитываются с процентами и платежами
|
|
||||||
|
|
||||||
**Context:** Расчёт: balance_{t+1} = balance_t + income_t - expense_t + asset_growth_t - liability_change_t.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### T4: Scenario Analysis
|
|
||||||
|
|
||||||
**Type:** feature
|
|
||||||
**Description:** Реализовать ScenarioService с методами: сценарии (optimistic, pessimistic, baseline), what-if модификация параметров, сравнение результатов сценариев.
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `engine/__init__.py`
|
|
||||||
- `engine/scenarios.py`
|
|
||||||
|
|
||||||
**Depends on:** T3
|
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
|
||||||
- [ ] Три предустановленных сценария (baseline, optimistic, pessimistic)
|
|
||||||
- [ ] What-if: изменение параметров (доход +10%, расход -5%)
|
|
||||||
- [ ] Сравнение сценариев возвращает сводку различий
|
|
||||||
|
|
||||||
**Context:** Сценарии меняют параметры модели перед прогнозом. Использовать copy модели для каждого сценария.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### T5: Excel Sync (import/export)
|
|
||||||
|
|
||||||
**Type:** feature
|
|
||||||
**Description:** Реализовать ExcelSync: чтение модели из .xlsx (листы: Accounts, Transactions, Assets, Liabilities, Recurring), запись результатов прогноза в новый .xlsx. Использовать openpyxl.
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `sync/__init__.py`
|
|
||||||
- `sync/excel_sync.py`
|
|
||||||
|
|
||||||
**Depends on:** T2
|
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
|
||||||
- [ ] Импорт из Excel заполняет FinancialModel
|
|
||||||
- [ ] Экспорт FinancialModel в Excel создаёт корректный .xlsx
|
|
||||||
- [ ] Обработка ошибок при невалидном формате Excel
|
|
||||||
|
|
||||||
**Context:** Каждая сущность — отдельный лист. Заголовки колонок = поля dataclass.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### T6: CLI (Typer) — все команды
|
|
||||||
|
|
||||||
**Type:** feature
|
|
||||||
**Description:** Реализовать CLI через Typer с командами: init, import, export, forecast, scenario, analyze. Главный entry point — консольная команда 'cf'.
|
|
||||||
|
|
||||||
**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' применяет и выводит сценарий
|
|
||||||
|
|
||||||
**Context:** Typer entry point. Команды описаны в design-report раздел 5. rich для форматирования таблиц.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### T7: AI Assistant (промпты + интерфейс)
|
|
||||||
|
|
||||||
**Type:** feature
|
|
||||||
**Description:** Реализовать AssistantService: генерация промптов для AI на основе модели и прогноза, форматирование контекста (JSON-дамп модели + результаты forecast), заглушка для вызова AI API. Промпты на русском языке для анализа фин. состояния.
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `ai/__init__.py`
|
|
||||||
- `ai/prompts.py`
|
|
||||||
- `ai/assistant.py`
|
|
||||||
|
|
||||||
**Depends on:** T3
|
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
|
||||||
- [ ] Промпт 'analyze' включает модель и прогноз в JSON
|
|
||||||
- [ ] Промпт 'advice' формирует запрос на финансовые рекомендации
|
|
||||||
- [ ] AssistantService возвращает структурированный ответ (заглушка)
|
|
||||||
|
|
||||||
**Context:** AI — заглушка для MVP. Промпты должны быть готовы для реального API.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### T8: Тесты на все модули
|
|
||||||
|
|
||||||
**Type:** test
|
|
||||||
**Description:** Написать pytest-тесты для cashflow_model (сериализация), engine (forecast + scenarios), sync (excel roundtrip), ai (prompts), cli (invocation).
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `tests/test_model.py`
|
|
||||||
- `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:**
|
|
||||||
- [ ] pytest запускается и все тесты проходят
|
|
||||||
- [ ] Покрытие базовых сценариев для каждой сущности
|
|
||||||
- [ ] Roundtrip-тест Excel: export → import → compare
|
|
||||||
- [ ] Forecast-тест: известные входные данные → ожидаемый результат
|
|
||||||
|
|
||||||
**Context:** Использовать tmp_path для временных файлов. conftest.py с fixture для тестовой модели.
|
|
||||||
|
|||||||
@@ -1,186 +1,35 @@
|
|||||||
# AGENTS.md — контекст для AI-сессий
|
# MetaAgent
|
||||||
|
|
||||||
## Project Overview
|
Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v2.1.0 —
|
||||||
|
набор инструкций для AI-агента.
|
||||||
|
|
||||||
**CashFlow Forecast** — личная финансовая модель с прогнозом денежных потоков. Python CLI-инструмент.
|
## Контекст MetaAgent
|
||||||
|
|
||||||
**Цель:** отвечать на вопрос "что произойдет дальше?" (forecast), а не "что произошло?" (accounting).
|
| Ресурс | Путь |
|
||||||
|
|--------|------|
|
||||||
|
| Главная инструкция | .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 |
|
||||||
|
|
||||||
**Стек:** Python 3.11+, JSON (хранение), openpyxl (Excel), typer (CLI), rich (вывод), pytest (тесты), ruff (линтер).
|
## Состояние сессии (если инициализировано)
|
||||||
|
|
||||||
**Тип проекта:** greenfield, MVP реализован.
|
| Артефакт | Путь |
|
||||||
|
|----------|------|
|
||||||
|
| Чекпоинты сессии | .agent/checkpoints.json |
|
||||||
|
| Манифест задач | .agent/task-manifest.json |
|
||||||
|
| Сводка для exec-агента | .agent/handoff-summary.md |
|
||||||
|
| Анализ репозитория | .agent/analysis-report.md |
|
||||||
|
|
||||||
---
|
## Для исполнительного агента
|
||||||
|
|
||||||
## Quick Start
|
1. **Прочитай** .agent/src/META_AGENT_GUIDE.md — пойми жизненный цикл MetaAgent.
|
||||||
|
2. **Прочитай** .agent/src/BOUNDARIES.md — соблюдай границы.
|
||||||
```bash
|
3. **Прочитай** .agent/rules/project-rules.md — выполни пользовательские правила.
|
||||||
source .venv/bin/activate
|
4. **Проверь** .agent/checkpoints.json — если существует, используй как состояние сессии.
|
||||||
cf init
|
5. **Проверь** .agent/task-manifest.json — если существует, выполняй задачи по порядку.
|
||||||
cf forecast --months 12
|
6. Если .agent/ не инициализирован или устарел — запусти install.sh --update для
|
||||||
pytest
|
обновления исходников MetaAgent до актуальной версии.
|
||||||
ruff check .
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Архитектура
|
|
||||||
|
|
||||||
Модульный монолит (layered):
|
|
||||||
|
|
||||||
```
|
|
||||||
[CLI / Excel File]
|
|
||||||
|
|
|
||||||
v
|
|
||||||
sync/ --> cashflow_model/ --> engine/ --> ai/
|
|
||||||
(Excel R/W) (Entity Model) (Forecast) (Prompts)
|
|
||||||
| | |
|
|
||||||
v v v
|
|
||||||
data/model.json data/model.json data/model.json
|
|
||||||
```
|
|
||||||
|
|
||||||
**Поток данных:**
|
|
||||||
1. Excel -> sync (импорт) -> JSON
|
|
||||||
2. JSON -> cashflow_model (dataclass)
|
|
||||||
3. engine (forecast) читает модель
|
|
||||||
4. ai (assistant) анализирует результаты
|
|
||||||
5. Результаты -> sync (экспорт) -> Excel
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Модули
|
|
||||||
|
|
||||||
| Модуль | Ответственность | Ключевые файлы |
|
|
||||||
|---|---|---|
|
|
||||||
| `cashflow_model/` | dataclass-сущности + JSON serialization | `model.py`, `account.py`, `transaction.py`, `recurring.py`, `asset.py`, `liability.py`, `scenario.py` |
|
|
||||||
| `engine/` | ForecastService, ScenarioService | `forecast.py`, `scenarios.py` |
|
|
||||||
| `sync/` | Excel <-> JSON | `excel_sync.py` |
|
|
||||||
| `ai/` | Промпты, AssistantService (заглушка) | `prompts.py`, `assistant.py` |
|
|
||||||
| `cli/` | Typer CLI | `main.py` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Model
|
|
||||||
|
|
||||||
### Account
|
|
||||||
| Поле | Тип |
|
|
||||||
|---|---|
|
|
||||||
| id | UUID |
|
|
||||||
| name | str |
|
|
||||||
| currency | str (default USD) |
|
|
||||||
| balance | float |
|
|
||||||
|
|
||||||
### Transaction
|
|
||||||
| Поле | Тип |
|
|
||||||
|---|---|
|
|
||||||
| id | UUID |
|
|
||||||
| date | str (ISO) |
|
|
||||||
| account | str (UUID счёта) |
|
|
||||||
| category | str |
|
|
||||||
| amount | float (positive=income, negative=expense) |
|
|
||||||
| description | str |
|
|
||||||
|
|
||||||
### RecurringCashflow
|
|
||||||
| Поле | Тип |
|
|
||||||
|---|---|
|
|
||||||
| id | UUID |
|
|
||||||
| start_date | str (ISO) |
|
|
||||||
| end_date | str (ISO, optional) |
|
|
||||||
| frequency | str (monthly/weekly/yearly) |
|
|
||||||
| amount | float |
|
|
||||||
| category | str |
|
|
||||||
|
|
||||||
### Asset
|
|
||||||
| Поле | Тип |
|
|
||||||
|---|---|
|
|
||||||
| id | UUID |
|
|
||||||
| name | str |
|
|
||||||
| value | float |
|
|
||||||
| growth_rate | float (% годовых) |
|
|
||||||
|
|
||||||
### Liability
|
|
||||||
| Поле | Тип |
|
|
||||||
|---|---|
|
|
||||||
| id | UUID |
|
|
||||||
| name | str |
|
|
||||||
| balance | float |
|
|
||||||
| interest | float (% годовых) |
|
|
||||||
| payment | float (ежемесячный) |
|
|
||||||
|
|
||||||
### ForecastScenario
|
|
||||||
| Поле | Тип |
|
|
||||||
|---|---|
|
|
||||||
| id | UUID |
|
|
||||||
| name | str (baseline/optimistic/pessimistic) |
|
|
||||||
| income_multiplier | float |
|
|
||||||
| expense_multiplier | float |
|
|
||||||
| growth_multiplier | float |
|
|
||||||
|
|
||||||
**FinancialModel** — корневой объект, содержит списки всех сущностей. Методы: `save(path)`, `load(path)`. JSON-файл в `data/model.json`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CLI Reference
|
|
||||||
|
|
||||||
Команда `cf` (entry point: `cli.main:app`):
|
|
||||||
|
|
||||||
| Команда | Аргументы | Описание |
|
|
||||||
|---|---|---|
|
|
||||||
| `init` | — | Создать пустую модель |
|
|
||||||
| `forecast` | `--months 12` | Прогноз cashflow |
|
|
||||||
| `scenario` | `<name>` | Сценарий baseline/optimistic/pessimistic |
|
|
||||||
| `whatif` | `--income 1.0 --expense 1.0 --growth 1.0` | What-if анализ |
|
|
||||||
| `compare` | `--months 12` | Сравнение сценариев |
|
|
||||||
| `import` | `<path.xlsx>` | Импорт из Excel |
|
|
||||||
| `export` | `<path.xlsx>` | Экспорт в Excel |
|
|
||||||
| `analyze` | `--months 12` | AI-анализ (промпт + заглушка) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Coding Conventions
|
|
||||||
|
|
||||||
- Python 3.11+, dataclass для моделей
|
|
||||||
- from_dict/to_dict для JSON-сериализации
|
|
||||||
- ruff (E, F, I, N, W), line-length=100
|
|
||||||
- pytest для тестов
|
|
||||||
- typer + rich для CLI
|
|
||||||
- f-строки, без лишних комментариев
|
|
||||||
- Имена: snake_case, классы PascalCase
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pytest # запуск тестов (26 tests)
|
|
||||||
ruff check . # линтер
|
|
||||||
ruff format . # автоформат
|
|
||||||
cf <command> # запуск CLI
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Known Issues / TODOs
|
|
||||||
|
|
||||||
- AI-ассистент — заглушка (`ai/assistant.py`). Промпты готовы, нужно подключить API (OpenAI и т.д.)
|
|
||||||
- Нет лицензии — требуется выбрать
|
|
||||||
- JSON-файлы — нет конкурентного доступа
|
|
||||||
- Excel — только .xlsx (openpyxl), нет поддержки Google Sheets
|
|
||||||
- Нет веб-интерфейса, только CLI
|
|
||||||
- `engine/forecast.py` — упрощённый алгоритм (без Monte Carlo)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## .agent/ directory
|
|
||||||
|
|
||||||
Директория `.agent/` содержит артефакты MetaAgent — планирование, дизайн, декомпозицию задач. **Не удалять**. Там же `checkpoints.json` с состоянием задач.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Границы (Boundaries)
|
|
||||||
|
|
||||||
**Что НЕ входит в задачу AI-агента:**
|
|
||||||
- Изменение архитектуры без обсуждения с пользователем
|
|
||||||
- Подключение внешних платных API без согласования
|
|
||||||
- Массовый рефакторинг без acceptance criteria
|
|
||||||
- Удаление `.agent/` или `README.arch.md`
|
|
||||||
@@ -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
@@ -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
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||||
|
|||||||
Binary file not shown.
+95
-6
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
+26
-23
@@ -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]:
|
||||||
@@ -45,35 +61,22 @@ class ForecastService:
|
|||||||
expenses += abs(t.amount)
|
expenses += abs(t.amount)
|
||||||
|
|
||||||
for r in self.model.recurring:
|
for r in self.model.recurring:
|
||||||
if r.category == "income":
|
if r.amount > 0:
|
||||||
income += abs(r.amount)
|
income += r.amount
|
||||||
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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user