feat: budget-tracker — spec, constitution, data-model, API contracts, plan
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
<!--
|
||||
Sync Impact Report
|
||||
- Version change: N/A → 1.0.0 (initial ratification)
|
||||
- Added principles: I. API-First, II. Data Integrity, III. Test Coverage,
|
||||
IV. Simplicity & Pragmatism, V. Docker-First Deployment
|
||||
- Added sections: Technical Stack Constraints, Development Workflow, Governance
|
||||
- Removed sections: none
|
||||
- Templates requiring updates:
|
||||
- .specify/templates/plan-template.md ✅ compatible (Constitution Check present)
|
||||
- .specify/templates/spec-template.md ✅ compatible (priorities, acceptance scenarios)
|
||||
- .specify/templates/tasks-template.md ✅ compatible (phased approach, test-optional)
|
||||
- Follow-up TODOs: none
|
||||
-->
|
||||
|
||||
# Budget Tracker Constitution
|
||||
|
||||
## Core Principles
|
||||
|
||||
### I. API-First Design
|
||||
|
||||
Le backend FastAPI expose une API REST documentee (OpenAPI/Swagger) qui
|
||||
constitue le contrat unique entre frontend et backend.
|
||||
|
||||
- Chaque endpoint DOIT etre defini dans un schema OpenAPI avant implementation.
|
||||
- Le frontend React consomme exclusivement l'API REST ; aucun acces direct
|
||||
a la base de donnees depuis le frontend.
|
||||
- Les reponses DOIVENT suivre un format JSON coherent avec codes HTTP
|
||||
standards (200, 201, 400, 404, 422, 500).
|
||||
- La validation des entrees DOIT utiliser les modeles Pydantic de FastAPI.
|
||||
|
||||
### II. Data Integrity
|
||||
|
||||
Les donnees financieres sont critiques et DOIVENT etre exactes et coherentes.
|
||||
|
||||
- Tous les montants DOIVENT etre stockes en centimes (entiers) pour eviter
|
||||
les erreurs d'arrondi en virgule flottante.
|
||||
- Les operations affectant le solde DOIVENT etre transactionnelles (ACID).
|
||||
- Chaque transaction DOIT avoir une date, un montant, une categorie et un type
|
||||
(revenu ou depense).
|
||||
- Les suppressions DOIVENT etre des soft-deletes (champ deleted_at) pour
|
||||
garantir la tracabilite de l'historique.
|
||||
|
||||
### III. Test Coverage
|
||||
|
||||
Les tests couvrent la logique metier critique sans imposer un TDD strict.
|
||||
|
||||
- Les calculs financiers (soldes, totaux, budgets) DOIVENT avoir des tests
|
||||
unitaires.
|
||||
- Les endpoints API DOIVENT avoir des tests d'integration avec une base de
|
||||
donnees de test.
|
||||
- Les tests DOIVENT etre executables via `pytest` en une seule commande.
|
||||
- Le coverage minimum cible est 80% sur la logique metier (services/).
|
||||
|
||||
### IV. Simplicity & Pragmatism
|
||||
|
||||
Pas de sur-ingenierie. Chaque couche d'abstraction DOIT etre justifiee.
|
||||
|
||||
- YAGNI : ne pas implementer de fonctionnalite "au cas ou".
|
||||
- Pas de pattern Repository si l'acces direct SQLAlchemy suffit.
|
||||
- Pas de microservices : monolithe backend + SPA frontend.
|
||||
- Les librairies tierces sont preferees a du code custom quand elles
|
||||
resolvent exactement le probleme (ex: Chart.js pour les graphiques).
|
||||
- Le code DOIT etre lisible par un developpeur junior sans documentation
|
||||
supplementaire.
|
||||
|
||||
### V. Docker-First Deployment
|
||||
|
||||
L'application DOIT etre deployable via `docker compose up` sans
|
||||
configuration manuelle.
|
||||
|
||||
- Un fichier `docker-compose.yml` a la racine DOIT orchestrer backend,
|
||||
frontend et PostgreSQL.
|
||||
- Les variables d'environnement DOIVENT etre centralisees dans un `.env`
|
||||
(avec `.env.example` versionne).
|
||||
- Les migrations de base de donnees DOIVENT s'executer automatiquement
|
||||
au demarrage du conteneur backend.
|
||||
- Le frontend DOIT etre servi en mode production via un build statique
|
||||
(nginx ou equivalent).
|
||||
|
||||
## Technical Stack Constraints
|
||||
|
||||
- **Backend** : Python 3.12+, FastAPI, SQLAlchemy 2.0 (async), Alembic
|
||||
- **Frontend** : React 18+, Tailwind CSS, Vite, Chart.js (ou Recharts)
|
||||
- **Base de donnees** : PostgreSQL 16+
|
||||
- **Tests** : pytest (backend), Vitest (frontend)
|
||||
- **Conteneurisation** : Docker, Docker Compose v2
|
||||
- **Langue du code** : anglais (variables, fonctions, fichiers)
|
||||
- **Langue de la documentation** : francais
|
||||
- Les dependances DOIVENT etre epinglees (versions exactes dans
|
||||
requirements.txt / package.json)
|
||||
|
||||
## Development Workflow
|
||||
|
||||
- Le code DOIT etre formate automatiquement (Ruff pour Python, Prettier
|
||||
pour JS/TS).
|
||||
- Chaque commit DOIT passer le linting et les tests avant merge.
|
||||
- Les branches suivent le pattern : `feature/xxx`, `fix/xxx`, `chore/xxx`.
|
||||
- Les migrations Alembic DOIVENT etre auto-generees puis relues
|
||||
manuellement avant commit.
|
||||
- Le fichier `docker-compose.yml` DOIT inclure un service de
|
||||
developpement avec hot-reload (backend + frontend).
|
||||
|
||||
## Governance
|
||||
|
||||
Cette constitution est le document de reference pour toutes les decisions
|
||||
techniques et architecturales du projet Budget Tracker.
|
||||
|
||||
- Tout changement architectural DOIT etre valide contre les principes
|
||||
ci-dessus avant implementation.
|
||||
- Les amendements a cette constitution requierent : (1) une justification
|
||||
ecrite, (2) une mise a jour du numero de version, (3) une propagation
|
||||
aux artefacts dependants.
|
||||
- Le versionnement suit le Semantic Versioning : MAJOR pour les
|
||||
changements incompatibles, MINOR pour les ajouts, PATCH pour les
|
||||
clarifications.
|
||||
- En cas de conflit entre pragmatisme et rigueur, le principe IV
|
||||
(Simplicity & Pragmatism) prevaut sauf pour les donnees financieres
|
||||
(principe II).
|
||||
|
||||
**Version**: 1.0.0 | **Ratified**: 2026-03-15 | **Last Amended**: 2026-03-15
|
||||
@@ -0,0 +1,247 @@
|
||||
# Contrats API REST — Budget Tracker
|
||||
|
||||
**Version** : 1.0.0 | **Base URL** : `/api/v1` | **Format** : JSON
|
||||
|
||||
## Conventions
|
||||
|
||||
- Tous les montants sont en **centimes** (integer)
|
||||
- Authentification : `Authorization: Bearer <access_token>`
|
||||
- Dates : ISO 8601 (`YYYY-MM-DD`)
|
||||
- Mois : `YYYY-MM`
|
||||
- Soft-delete : les ressources supprimées retournent 404
|
||||
- Erreurs : `{"detail": "message d'erreur"}`
|
||||
|
||||
---
|
||||
|
||||
## Authentification
|
||||
|
||||
### POST /auth/register
|
||||
Créer un compte utilisateur.
|
||||
|
||||
**Body** :
|
||||
```json
|
||||
{"email": "user@example.com", "password": "...", "full_name": "Jean Dupont"}
|
||||
```
|
||||
**Réponse 201** :
|
||||
```json
|
||||
{"id": "uuid", "email": "user@example.com", "full_name": "Jean Dupont"}
|
||||
```
|
||||
|
||||
### POST /auth/login
|
||||
Obtenir les tokens JWT.
|
||||
|
||||
**Body** : `application/x-www-form-urlencoded` : `username=email&password=...`
|
||||
|
||||
**Réponse 200** :
|
||||
```json
|
||||
{"access_token": "...", "refresh_token": "...", "token_type": "bearer"}
|
||||
```
|
||||
|
||||
### POST /auth/refresh
|
||||
Renouveler l'access token.
|
||||
|
||||
**Body** : `{"refresh_token": "..."}`
|
||||
**Réponse 200** : `{"access_token": "...", "token_type": "bearer"}`
|
||||
|
||||
### POST /auth/logout
|
||||
Invalider le refresh token.
|
||||
**Réponse 204** : no content
|
||||
|
||||
---
|
||||
|
||||
## Transactions
|
||||
|
||||
### GET /transactions
|
||||
Lister les transactions (paginées, filtrables).
|
||||
|
||||
**Query params** : `?month=2026-03&category_id=uuid&type=expense&page=1&per_page=20`
|
||||
|
||||
**Réponse 200** :
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"amount_cents": 4500,
|
||||
"type": "expense",
|
||||
"description": "Courses Leclerc",
|
||||
"category": {"id": "uuid", "name": "Alimentation", "color": "#4CAF50"},
|
||||
"transaction_date": "2026-03-15",
|
||||
"created_at": "2026-03-15T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"total": 42,
|
||||
"page": 1,
|
||||
"per_page": 20
|
||||
}
|
||||
```
|
||||
|
||||
### POST /transactions
|
||||
Créer une transaction.
|
||||
|
||||
**Body** :
|
||||
```json
|
||||
{
|
||||
"amount_cents": 4500,
|
||||
"type": "expense",
|
||||
"category_id": "uuid",
|
||||
"description": "Courses Leclerc",
|
||||
"transaction_date": "2026-03-15"
|
||||
}
|
||||
```
|
||||
**Réponse 201** : transaction complète
|
||||
|
||||
### GET /transactions/{id}
|
||||
Détail d'une transaction. **Réponse 200** : transaction complète
|
||||
|
||||
### PUT /transactions/{id}
|
||||
Modifier une transaction. **Body** : mêmes champs que POST. **Réponse 200** : transaction mise à jour
|
||||
|
||||
### DELETE /transactions/{id}
|
||||
Soft-delete. **Réponse 204** : no content
|
||||
|
||||
---
|
||||
|
||||
## Catégories
|
||||
|
||||
### GET /categories
|
||||
Lister les catégories de l'utilisateur (y compris les défauts système).
|
||||
|
||||
**Query** : `?type=expense`
|
||||
|
||||
**Réponse 200** :
|
||||
```json
|
||||
[
|
||||
{"id": "uuid", "name": "Alimentation", "type": "expense", "color": "#4CAF50", "icon": "shopping-cart", "is_default": true}
|
||||
]
|
||||
```
|
||||
|
||||
### POST /categories
|
||||
Créer une catégorie personnalisée.
|
||||
|
||||
**Body** : `{"name": "Loisirs", "type": "expense", "color": "#9C27B0", "icon": "gamepad"}`
|
||||
**Réponse 201** : catégorie créée
|
||||
|
||||
### PUT /categories/{id}
|
||||
Modifier une catégorie. **Réponse 200** : catégorie mise à jour
|
||||
|
||||
### DELETE /categories/{id}
|
||||
Soft-delete. Erreur 409 si des transactions y sont liées.
|
||||
|
||||
---
|
||||
|
||||
## Budgets (Enveloppes)
|
||||
|
||||
### GET /budgets
|
||||
Lister les budgets du mois courant ou d'un mois donné.
|
||||
|
||||
**Query** : `?month=2026-03`
|
||||
|
||||
**Réponse 200** :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"category": {"id": "uuid", "name": "Alimentation"},
|
||||
"month": "2026-03",
|
||||
"limit_cents": 30000,
|
||||
"spent_cents": 12500,
|
||||
"remaining_cents": 17500,
|
||||
"percentage_used": 41.7
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### POST /budgets
|
||||
Définir un budget pour une catégorie/mois.
|
||||
|
||||
**Body** : `{"category_id": "uuid", "month": "2026-03", "limit_cents": 30000}`
|
||||
**Réponse 201** : budget créé
|
||||
|
||||
### PUT /budgets/{id}
|
||||
Modifier la limite d'un budget. **Réponse 200** : budget mis à jour
|
||||
|
||||
### DELETE /budgets/{id}
|
||||
Supprimer un budget. **Réponse 204**
|
||||
|
||||
---
|
||||
|
||||
## Dashboard
|
||||
|
||||
### GET /dashboard
|
||||
KPIs du mois courant ou d'un mois donné.
|
||||
|
||||
**Query** : `?month=2026-03`
|
||||
|
||||
**Réponse 200** :
|
||||
```json
|
||||
{
|
||||
"month": "2026-03",
|
||||
"balance_cents": 150000,
|
||||
"total_income_cents": 250000,
|
||||
"total_expense_cents": 100000,
|
||||
"by_category": [
|
||||
{"category_id": "uuid", "name": "Alimentation", "total_cents": 45000, "percentage": 45.0}
|
||||
],
|
||||
"monthly_trend": [
|
||||
{"month": "2026-01", "income_cents": 240000, "expense_cents": 110000},
|
||||
{"month": "2026-02", "income_cents": 245000, "expense_cents": 95000},
|
||||
{"month": "2026-03", "income_cents": 250000, "expense_cents": 100000}
|
||||
],
|
||||
"budget_alerts": [
|
||||
{"category_id": "uuid", "name": "Loisirs", "percentage_used": 92.0, "status": "warning"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Historique
|
||||
|
||||
### GET /history
|
||||
Résumé mensuel navigable.
|
||||
|
||||
**Query** : `?year=2026`
|
||||
|
||||
**Réponse 200** :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"month": "2026-03",
|
||||
"income_cents": 250000,
|
||||
"expense_cents": 100000,
|
||||
"balance_cents": 150000,
|
||||
"transaction_count": 42
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Export
|
||||
|
||||
### GET /export/csv
|
||||
Exporter les transactions en CSV.
|
||||
|
||||
**Query** : `?month=2026-03` (optionnel — tout exporter si absent)
|
||||
**Réponse 200** : `Content-Type: text/csv`, fichier `transactions_2026-03.csv`
|
||||
|
||||
### GET /export/pdf
|
||||
Exporter le rapport mensuel en PDF.
|
||||
|
||||
**Query** : `?month=2026-03`
|
||||
**Réponse 200** : `Content-Type: application/pdf`, fichier `rapport_2026-03.pdf`
|
||||
|
||||
---
|
||||
|
||||
## Codes d'erreur
|
||||
|
||||
| Code | Signification |
|
||||
|------|--------------|
|
||||
| 400 | Données invalides (validation Pydantic) |
|
||||
| 401 | Token manquant ou expiré |
|
||||
| 403 | Ressource appartenant à un autre utilisateur |
|
||||
| 404 | Ressource introuvable (ou soft-deleted) |
|
||||
| 409 | Conflit (doublon, contrainte métier) |
|
||||
| 422 | Erreur de validation (détail par champ) |
|
||||
| 500 | Erreur serveur |
|
||||
@@ -0,0 +1,175 @@
|
||||
# Modèle de Données — Budget Tracker
|
||||
|
||||
## Principes directeurs
|
||||
- Tous les montants sont stockés en **centimes** (INTEGER) — principe II de la Constitution
|
||||
- Les suppressions sont des **soft-deletes** via `deleted_at` — principe II (tracabilité)
|
||||
- Les identifiants sont des **UUID v4** — évite l'exposition de séquences prédictibles
|
||||
- Les timestamps sont en **UTC** sans timezone stockée
|
||||
|
||||
---
|
||||
|
||||
## Entités
|
||||
|
||||
### User
|
||||
| Champ | Type | Contraintes |
|
||||
|-------|------|-------------|
|
||||
| id | UUID | PK, auto (gen_random_uuid()) |
|
||||
| email | VARCHAR(255) | UNIQUE, NOT NULL |
|
||||
| hashed_password | VARCHAR(255) | NOT NULL |
|
||||
| full_name | VARCHAR(100) | NOT NULL |
|
||||
| is_active | BOOLEAN | DEFAULT true, NOT NULL |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
|
||||
**Notes** :
|
||||
- `email` indexé (recherche lors de l'authentification)
|
||||
- `hashed_password` : bcrypt, jamais exposé en API
|
||||
- `is_active` : permet de désactiver un compte sans le supprimer
|
||||
|
||||
---
|
||||
|
||||
### Category
|
||||
| Champ | Type | Contraintes |
|
||||
|-------|------|-------------|
|
||||
| id | UUID | PK, auto |
|
||||
| user_id | UUID | FK → User.id, NOT NULL |
|
||||
| name | VARCHAR(50) | NOT NULL |
|
||||
| type | ENUM(income, expense) | NOT NULL |
|
||||
| color | VARCHAR(7) | NULL, format hex (#RRGGBB) |
|
||||
| icon | VARCHAR(50) | NULL, nom d'icône (ex: "shopping-cart") |
|
||||
| is_default | BOOLEAN | DEFAULT false, NOT NULL |
|
||||
| deleted_at | TIMESTAMP | NULL = actif |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
|
||||
**Notes** :
|
||||
- `is_default = true` : catégories système créées au premier login (Alimentation, Transport, Logement, Loisirs, Santé, Revenus)
|
||||
- Les catégories par défaut ont `user_id` de l'utilisateur propriétaire — pas de catégories globales partagées (simplicité)
|
||||
- Index : `(user_id, deleted_at)` pour les requêtes de listing
|
||||
- Contrainte : impossible de supprimer si transactions actives liées (contrôlé en service, pas en DB)
|
||||
|
||||
---
|
||||
|
||||
### Transaction
|
||||
| Champ | Type | Contraintes |
|
||||
|-------|------|-------------|
|
||||
| id | UUID | PK, auto |
|
||||
| user_id | UUID | FK → User.id, NOT NULL |
|
||||
| category_id | UUID | FK → Category.id, NOT NULL |
|
||||
| amount_cents | INTEGER | NOT NULL, CHECK (amount_cents > 0) |
|
||||
| type | ENUM(income, expense) | NOT NULL |
|
||||
| description | VARCHAR(255) | NULL autorisé |
|
||||
| transaction_date | DATE | NOT NULL |
|
||||
| deleted_at | TIMESTAMP | NULL = actif |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
|
||||
**Notes** :
|
||||
- `amount_cents > 0` : le signe est porté par `type`, jamais par le montant
|
||||
- `transaction_date` : date métier saisie par l'utilisateur (≠ `created_at` technique)
|
||||
- Index : `(user_id, transaction_date, deleted_at)` pour les requêtes par période
|
||||
- Index : `(user_id, category_id, deleted_at)` pour les agrégats par catégorie
|
||||
- Le soft-delete (`deleted_at IS NOT NULL`) masque la transaction de l'UI sans la détruire
|
||||
|
||||
---
|
||||
|
||||
### Budget
|
||||
| Champ | Type | Contraintes |
|
||||
|-------|------|-------------|
|
||||
| id | UUID | PK, auto |
|
||||
| user_id | UUID | FK → User.id, NOT NULL |
|
||||
| category_id | UUID | FK → Category.id, NOT NULL |
|
||||
| month | CHAR(7) | NOT NULL, format YYYY-MM |
|
||||
| limit_cents | INTEGER | NOT NULL, CHECK (limit_cents > 0) |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
| UNIQUE | | (user_id, category_id, month) |
|
||||
|
||||
**Notes** :
|
||||
- `month` en CHAR(7) format ISO `YYYY-MM` : simple, triable lexicographiquement, pas de confusion timezone
|
||||
- La contrainte UNIQUE garantit un seul budget par catégorie par mois par utilisateur
|
||||
- Index implicite sur la contrainte UNIQUE
|
||||
- Pas de soft-delete : un budget supprimé est réellement supprimé (pas de données financières dans l'entité elle-même)
|
||||
|
||||
---
|
||||
|
||||
### RefreshToken
|
||||
| Champ | Type | Contraintes |
|
||||
|-------|------|-------------|
|
||||
| id | UUID | PK, auto |
|
||||
| user_id | UUID | FK → User.id, NOT NULL |
|
||||
| token_hash | VARCHAR(255) | UNIQUE, NOT NULL |
|
||||
| expires_at | TIMESTAMP | NOT NULL |
|
||||
| revoked_at | TIMESTAMP | NULL = actif |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
|
||||
**Notes** :
|
||||
- Seul le hash du token est stocké (SHA-256), jamais le token brut
|
||||
- Permet la révocation explicite (logout) et l'invalidation par rotation
|
||||
- Nettoyage périodique des tokens expirés recommandé (tâche cron ou au login)
|
||||
|
||||
---
|
||||
|
||||
## Relations
|
||||
|
||||
```
|
||||
User 1 ──────────────────────────── N Category
|
||||
User 1 ──────────────────────────── N Transaction
|
||||
User 1 ──────────────────────────── N Budget
|
||||
User 1 ──────────────────────────── N RefreshToken
|
||||
Category 1 ──────────────────────── N Transaction
|
||||
Category 1 ──────────────────────── N Budget
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requêtes critiques et leurs index
|
||||
|
||||
### Solde courant d'un utilisateur
|
||||
```sql
|
||||
SELECT
|
||||
SUM(CASE WHEN type = 'income' THEN amount_cents ELSE -amount_cents END)
|
||||
FROM transactions
|
||||
WHERE user_id = :uid AND deleted_at IS NULL;
|
||||
```
|
||||
→ Index : `(user_id, deleted_at)`
|
||||
|
||||
### Résumé mensuel (revenus/dépenses d'un mois)
|
||||
```sql
|
||||
SELECT type, SUM(amount_cents)
|
||||
FROM transactions
|
||||
WHERE user_id = :uid
|
||||
AND transaction_date >= :month_start
|
||||
AND transaction_date < :month_end
|
||||
AND deleted_at IS NULL
|
||||
GROUP BY type;
|
||||
```
|
||||
→ Index : `(user_id, transaction_date, deleted_at)`
|
||||
|
||||
### Consommation d'un budget (dépenses catégorie × mois)
|
||||
```sql
|
||||
SELECT SUM(t.amount_cents)
|
||||
FROM transactions t
|
||||
JOIN budgets b ON b.category_id = t.category_id
|
||||
WHERE t.user_id = :uid
|
||||
AND t.category_id = :cat_id
|
||||
AND t.type = 'expense'
|
||||
AND t.transaction_date >= :month_start
|
||||
AND t.transaction_date < :month_end
|
||||
AND t.deleted_at IS NULL;
|
||||
```
|
||||
→ Index : `(user_id, category_id, deleted_at)`
|
||||
|
||||
---
|
||||
|
||||
## Valeurs par défaut — Catégories système
|
||||
|
||||
| Nom | Type | Couleur | Icône |
|
||||
|-----|------|---------|-------|
|
||||
| Alimentation | expense | #22c55e | utensils |
|
||||
| Transport | expense | #3b82f6 | car |
|
||||
| Logement | expense | #f59e0b | home |
|
||||
| Loisirs | expense | #a855f7 | gamepad-2 |
|
||||
| Santé | expense | #ef4444 | heart-pulse |
|
||||
| Revenus | income | #10b981 | trending-up |
|
||||
|
||||
Créées automatiquement lors du premier login utilisateur (dans la logique de service, pas en seed DB globale).
|
||||
@@ -0,0 +1,265 @@
|
||||
# Plan d'Implémentation — Budget Tracker
|
||||
|
||||
**Feature** : `003-budget-tracker-core`
|
||||
**Date** : 2026-03-15
|
||||
**Durée estimée** : 13 jours de travail
|
||||
**Stack** : FastAPI + SQLAlchemy 2.0 async + Alembic / React 18 + Vite + Tailwind + Recharts / PostgreSQL 16 / Docker Compose
|
||||
|
||||
---
|
||||
|
||||
## Vérification Constitution
|
||||
|
||||
| Principe | Conformité | Notes |
|
||||
|----------|-----------|-------|
|
||||
| I. API-First | ✅ | Endpoints définis dans `contracts/api.md`, Pydantic pour la validation |
|
||||
| II. Data Integrity | ✅ | Montants en centimes (int), soft-delete, transactions ACID |
|
||||
| III. Test Coverage | ✅ | pytest backend, Vitest frontend, ≥80% logique métier |
|
||||
| IV. Simplicity | ✅ | Monolithe, pas de microservices, librairies standard |
|
||||
| V. Docker-First | ✅ | docker-compose.yml, migrations auto au démarrage, build statique nginx |
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Setup & Infrastructure (1 jour)
|
||||
|
||||
### Objectif
|
||||
Disposer d'un environnement de développement fonctionnel avec hot-reload.
|
||||
|
||||
### Tâches
|
||||
|
||||
1. **Init repo Git** : `.gitignore`, `README.md`, structure dossiers (`backend/`, `frontend/`, `docker/`)
|
||||
2. **Docker Compose dev** :
|
||||
- Service `db` : PostgreSQL 16, volume persistant, init script
|
||||
- Service `backend` : Python 3.12, hot-reload uvicorn, montage code source
|
||||
- Service `frontend` : Node 22, Vite dev server, montage code source
|
||||
3. **Backend scaffold** :
|
||||
- FastAPI app factory (`backend/app/main.py`)
|
||||
- Config via Pydantic Settings (`.env` → `backend/app/config.py`)
|
||||
- SQLAlchemy 2.0 async engine + session factory
|
||||
- Alembic init (`backend/alembic/`)
|
||||
- Ruff config (`pyproject.toml`)
|
||||
- `requirements.txt` avec versions pinned
|
||||
4. **Frontend scaffold** :
|
||||
- `npm create vite@latest` avec template React + TypeScript
|
||||
- Tailwind CSS setup
|
||||
- Prettier config
|
||||
- Structure dossiers (`src/components/`, `src/pages/`, `src/api/`, `src/hooks/`)
|
||||
5. **`.env.example`** avec toutes les variables documentées
|
||||
6. **Smoke test** : `docker compose up` → backend répond `GET /health`, frontend affiche une page
|
||||
|
||||
### Livrables
|
||||
- `docker-compose.yml` + `docker-compose.override.yml` (dev)
|
||||
- Backend qui démarre, frontend qui affiche, PostgreSQL qui tourne
|
||||
- Premier commit : `chore: initial project setup`
|
||||
|
||||
### Critères de validation
|
||||
- [ ] `docker compose up` fonctionne sans erreur
|
||||
- [ ] `GET /health` retourne `{"status": "ok"}`
|
||||
- [ ] Frontend accessible sur `localhost:5173`
|
||||
- [ ] Ruff et Prettier passent sans erreur
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Backend Core (3 jours)
|
||||
|
||||
### Objectif
|
||||
API REST fonctionnelle avec auth, transactions et catégories.
|
||||
|
||||
### Jour 1 : Modèles & Auth
|
||||
|
||||
1. **Modèles SQLAlchemy** : `User`, `Category`, `Transaction`, `Budget`, `RefreshToken`
|
||||
- UUIDs, `created_at`/`updated_at` auto, `deleted_at` pour soft-delete
|
||||
- `amount_cents` INTEGER, contrainte CHECK > 0
|
||||
2. **Migrations Alembic** : `alembic revision --autogenerate -m "initial models"`
|
||||
3. **Auth JWT** :
|
||||
- Endpoint `POST /auth/register` (hash bcrypt, validation email unique)
|
||||
- Endpoint `POST /auth/login` (access token 15min + refresh token 7j)
|
||||
- Endpoint `POST /auth/refresh`
|
||||
- Endpoint `POST /auth/logout` (invalidation refresh token)
|
||||
- Middleware `get_current_user` pour les routes protégées
|
||||
4. **Seed catégories par défaut** : Alimentation, Transport, Logement, Santé, Loisirs, Divers (dépenses) + Salaire, Freelance, Remboursement (revenus)
|
||||
|
||||
### Jour 2 : CRUD Transactions & Catégories
|
||||
|
||||
1. **Transactions** :
|
||||
- `GET /transactions` : pagination, filtres (mois, catégorie, type), tri par date desc
|
||||
- `POST /transactions` : validation Pydantic, isolation par user_id
|
||||
- `PUT /transactions/{id}` : vérification ownership
|
||||
- `DELETE /transactions/{id}` : soft-delete (set `deleted_at`)
|
||||
2. **Catégories** :
|
||||
- `GET /categories` : catégories user + catégories système (`is_default`)
|
||||
- `POST /categories` : personnalisées uniquement
|
||||
- `PUT /categories/{id}` : pas de modif des catégories système
|
||||
- `DELETE /categories/{id}` : refus 409 si transactions liées actives
|
||||
3. **Services layer** : séparer la logique métier des routes (pas de requêtes SQL dans les endpoints)
|
||||
|
||||
### Jour 3 : Tests Backend
|
||||
|
||||
1. **Fixtures pytest** : base de test PostgreSQL (ou SQLite async pour la vitesse), factory pour User/Transaction/Category
|
||||
2. **Tests unitaires** :
|
||||
- Calcul de solde (revenu - dépense, centimes)
|
||||
- Soft-delete : la transaction n'apparaît plus mais existe en base
|
||||
- Validation montant > 0, date pas dans le futur (si décidé)
|
||||
3. **Tests d'intégration** :
|
||||
- Auth : register → login → token valide → accès protégé
|
||||
- CRUD transactions : create → list → update → delete → list vérifie absence
|
||||
- Filtrage par mois et catégorie
|
||||
- Isolation multi-user : user A ne voit pas les transactions de user B
|
||||
4. **CI** : `pytest --cov=app/services --cov-fail-under=80`
|
||||
|
||||
### Livrables
|
||||
- API complète (auth + transactions + catégories)
|
||||
- Suite de tests avec coverage ≥80% services
|
||||
- Documentation OpenAPI auto (`/docs`)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Frontend Core (3 jours)
|
||||
|
||||
### Objectif
|
||||
SPA React fonctionnelle avec auth et gestion des transactions.
|
||||
|
||||
### Jour 4 : Auth & Layout
|
||||
|
||||
1. **Client API** : module `src/api/client.ts` avec intercepteur Axios/fetch, gestion auto du refresh token
|
||||
2. **Auth store** : Context React ou Zustand léger (user, tokens, login/logout)
|
||||
3. **Pages auth** : Login, Register avec validation côté client
|
||||
4. **Layout principal** : Sidebar navigation (Dashboard, Transactions, Budgets, Historique), header avec nom user + logout
|
||||
5. **Route guard** : redirect vers login si non authentifié
|
||||
|
||||
### Jour 5 : Liste & Formulaire Transactions
|
||||
|
||||
1. **Page Transactions** :
|
||||
- Tableau avec colonnes : date, description, catégorie (badge couleur), montant, type (icône revenu/dépense)
|
||||
- Pagination
|
||||
- Filtres : mois (date picker), catégorie (select), type (toggle)
|
||||
- Bouton supprimer avec confirmation
|
||||
2. **Formulaire Transaction** (modal ou page) :
|
||||
- Champs : montant (€, converti en centimes), date, type (toggle revenu/dépense), catégorie (select), description
|
||||
- Validation temps réel
|
||||
- Mode création et édition
|
||||
3. **TanStack Query** : cache serveur, invalidation après mutation
|
||||
|
||||
### Jour 6 : Catégories & Polish
|
||||
|
||||
1. **Gestion catégories** : page settings ou modal, CRUD catégories personnalisées avec couleur et icône
|
||||
2. **Feedback UI** : toasts de confirmation (ajout, suppression), skeleton loading, états vides
|
||||
3. **Responsive** : mobile-first, sidebar collapsible sur mobile
|
||||
4. **Tests Vitest** : composants critiques (formulaire transaction, affichage montant centimes→euros)
|
||||
|
||||
### Livrables
|
||||
- SPA complète : auth + transactions + catégories
|
||||
- Responsive mobile/desktop
|
||||
- Tests composants critiques
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Features Avancées (4 jours)
|
||||
|
||||
### Jour 7 : Dashboard & Graphiques
|
||||
|
||||
1. **Endpoints backend** :
|
||||
- `GET /dashboard?month=YYYY-MM` : solde, totaux, répartition par catégorie, tendance 6 mois, alertes budget
|
||||
2. **Page Dashboard** :
|
||||
- Cards KPI : solde courant, revenus du mois, dépenses du mois, solde net
|
||||
- Graphique camembert : répartition dépenses par catégorie (Recharts PieChart)
|
||||
- Graphique barres : tendance revenus/dépenses sur 6 mois (Recharts BarChart)
|
||||
- Alertes budgets dépassés (badges warning/danger)
|
||||
3. **Navigation mensuelle** : boutons mois précédent/suivant sur le dashboard
|
||||
|
||||
### Jour 8 : Budgets (Enveloppes)
|
||||
|
||||
1. **Endpoints backend** :
|
||||
- `GET /budgets?month=YYYY-MM` : budgets avec calcul `spent_cents` et `remaining_cents`
|
||||
- `POST /budgets` : création avec contrainte unicité (user, category, month)
|
||||
- `PUT /budgets/{id}`, `DELETE /budgets/{id}`
|
||||
2. **Page Budgets** :
|
||||
- Liste des enveloppes du mois avec barre de progression (vert/orange/rouge)
|
||||
- Formulaire ajout/édition budget (catégorie, limite en €)
|
||||
- Copie des budgets du mois précédent (bouton "Reconduire")
|
||||
3. **Tests** : calcul consommation budget, reconduction, alertes seuil
|
||||
|
||||
### Jour 9 : Historique Mensuel
|
||||
|
||||
1. **Endpoint backend** : `GET /history?year=YYYY` → résumé par mois
|
||||
2. **Page Historique** :
|
||||
- Tableau annuel : mois, revenus, dépenses, solde, nb transactions
|
||||
- Clic sur un mois → redirige vers Dashboard du mois sélectionné
|
||||
- Sélecteur d'année
|
||||
3. **Graphique annuel** : courbe revenus/dépenses sur 12 mois (Recharts LineChart)
|
||||
|
||||
### Jour 10 : Export CSV & PDF
|
||||
|
||||
1. **Endpoint CSV** : `GET /export/csv?month=YYYY-MM` → streaming CSV avec headers
|
||||
2. **Endpoint PDF** : `GET /export/pdf?month=YYYY-MM` → WeasyPrint, template Jinja2 rapport mensuel
|
||||
- En-tête avec titre + mois + date d'export
|
||||
- Tableau des transactions
|
||||
- Résumé par catégorie
|
||||
- Solde
|
||||
3. **Boutons export** dans la page Transactions et le Dashboard
|
||||
4. **Tests** : validité CSV (parsable), PDF non vide
|
||||
|
||||
### Livrables
|
||||
- Dashboard avec graphiques interactifs
|
||||
- Budgets enveloppes avec alertes
|
||||
- Historique annuel navigable
|
||||
- Export CSV + PDF fonctionnels
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Finalisation & Production (2 jours)
|
||||
|
||||
### Jour 11 : Docker Production
|
||||
|
||||
1. **Dockerfile backend** : multi-stage (build deps → runtime slim)
|
||||
2. **Dockerfile frontend** : multi-stage (build Vite → nginx:alpine)
|
||||
3. **docker-compose.prod.yml** :
|
||||
- Backend : gunicorn + uvicorn workers
|
||||
- Frontend : nginx avec compression gzip, cache headers
|
||||
- PostgreSQL avec backup volume
|
||||
- Health checks sur tous les services
|
||||
4. **Entrypoint backend** : exécute `alembic upgrade head` au démarrage
|
||||
5. **Sécurité** : CORS restrictif, rate limiting (slowapi), HTTPS-ready headers
|
||||
|
||||
### Jour 12 : Documentation & Qualité
|
||||
|
||||
1. **README.md** complet :
|
||||
- Description du projet
|
||||
- Prérequis (Docker, Docker Compose)
|
||||
- Quick start : `cp .env.example .env && docker compose up`
|
||||
- Architecture (schéma simplifié)
|
||||
- Endpoints API (lien vers `/docs`)
|
||||
2. **OpenAPI** : vérifier que la doc auto est complète et lisible
|
||||
3. **Tests e2e légers** : script Bash qui lance le stack, crée un user, ajoute une transaction, vérifie le dashboard
|
||||
4. **Cleanup** : supprimer code mort, vérifier tous les TODO, linting final
|
||||
|
||||
### Livrables
|
||||
- Docker Compose production-ready
|
||||
- README complet
|
||||
- Suite complète de tests (unit + integ + e2e)
|
||||
- Projet prêt à déployer
|
||||
|
||||
---
|
||||
|
||||
## Artefacts générés
|
||||
|
||||
| Fichier | Description |
|
||||
|---------|-------------|
|
||||
| `spec.md` | Spécification fonctionnelle (7 user stories, 20 exigences) |
|
||||
| `research.md` | Décisions techniques avec rationale |
|
||||
| `data-model.md` | 5 entités, index, contraintes |
|
||||
| `contracts/api.md` | Contrats API REST (21 endpoints) |
|
||||
| `plan.md` | Ce plan d'implémentation |
|
||||
|
||||
## Risques identifiés
|
||||
|
||||
| Risque | Impact | Mitigation |
|
||||
|--------|--------|-----------|
|
||||
| WeasyPrint lourd en Docker | Image backend volumineuse | Multi-stage build, layer caching |
|
||||
| Performances dashboard sur gros volume | Requêtes lentes >10k transactions | Index dédiés (voir `data-model.md`), pagination |
|
||||
| Complexité auth JWT refresh | Bugs de déconnexion intempestive | Tests d'intégration auth exhaustifs |
|
||||
| Recharts bundle size | Frontend lourd | Tree-shaking Vite, lazy loading des pages graphiques |
|
||||
|
||||
## Prochaines étapes
|
||||
|
||||
1. **`/speckit.tasks`** — Découper ce plan en tâches atomiques assignables
|
||||
2. **`/speckit.implement`** — Lancer l'implémentation phase par phase
|
||||
@@ -0,0 +1,100 @@
|
||||
# Recherche & Décisions Techniques — Budget Tracker
|
||||
|
||||
## Authentification
|
||||
- **Décision** : JWT (tokens access 15min + refresh 7j) via `python-jose` + `passlib[bcrypt]`
|
||||
- **Rationale** : stateless, compatible SPA React, aucune session serveur à gérer, facile à dockeriser
|
||||
- **Alternatives** :
|
||||
- Session cookies (plus simple mais couplé au serveur, complexe en CORS)
|
||||
- OAuth2 externe / Keycloak (overkill pour usage personnel)
|
||||
- FastAPI-Users (bibliothèque haut niveau — ajoute de la magie, préférence pour la transparence)
|
||||
|
||||
## Base de données
|
||||
- **Décision** : PostgreSQL 16
|
||||
- **Rationale** : ACID natif (critique pour données financières), types UUID, ENUM, support transactionnel, standard de l'industrie
|
||||
- **Alternatives** :
|
||||
- SQLite (suffisant en dev, mais limité en concurrence et types)
|
||||
- MySQL (moins bon support UUID/ENUM natif)
|
||||
|
||||
## ORM & Migrations
|
||||
- **Décision** : SQLAlchemy 2.0 (mode async) + Alembic
|
||||
- **Rationale** : async natif pour FastAPI, migrations versionées et réversibles, mature et bien documenté
|
||||
- **Alternatives** :
|
||||
- Tortoise ORM (async natif mais moins mature, moins d'intégrations)
|
||||
- SQLModel (wrapper SQLAlchemy+Pydantic — moins de contrôle sur les modèles)
|
||||
- Prisma (Node.js uniquement)
|
||||
|
||||
## Framework Backend
|
||||
- **Décision** : FastAPI 0.111+
|
||||
- **Rationale** : OpenAPI/Swagger auto-généré (conforme principe I de la Constitution), validation Pydantic intégrée, async natif, typage strict
|
||||
- **Alternatives** :
|
||||
- Django REST Framework (sync-first, plus lourd)
|
||||
- Flask (pas d'async natif, validation manuelle)
|
||||
|
||||
## Framework Frontend
|
||||
- **Décision** : React 18 + TypeScript + Vite
|
||||
- **Rationale** : écosystème riche, SPA adaptée à une app de gestion, typage fort réduit les erreurs sur les montants financiers
|
||||
- **Alternatives** :
|
||||
- Next.js (SSR inutile pour usage authentifié perso)
|
||||
- Vue 3 (viable mais écosystème plus réduit)
|
||||
|
||||
## Styling Frontend
|
||||
- **Décision** : Tailwind CSS 3 + shadcn/ui
|
||||
- **Rationale** : utilitaires CSS rapides, shadcn/ui fournit composants accessibles (formulaires, modales, tableaux) sans dépendance lourde
|
||||
- **Alternatives** :
|
||||
- MUI / Ant Design (plus lourds, style moins personnalisable)
|
||||
- CSS Modules (trop verbeux pour une SPA)
|
||||
|
||||
## Graphiques Frontend
|
||||
- **Décision** : Recharts
|
||||
- **Rationale** : composants React purs (pas de D3 direct), bon support responsive, Tailwind-compatible, léger (~180 ko gzip)
|
||||
- **Alternatives** :
|
||||
- Chart.js + react-chartjs-2 (moins idiomatic React, state management externe)
|
||||
- Nivo (très complet mais plus lourd, over-engineering pour 2 types de graphiques)
|
||||
- Victory (moins maintenu)
|
||||
|
||||
## Gestion d'état Frontend
|
||||
- **Décision** : TanStack Query (React Query) v5
|
||||
- **Rationale** : cache serveur, invalidation automatique après mutations (soldes recalculés), gestion loading/error intégrée — évite un store global Redux pour des données essentiellement serveur
|
||||
- **Alternatives** :
|
||||
- Redux Toolkit (overkill, état global inutile si les données viennent de l'API)
|
||||
- SWR (moins de fonctionnalités sur les mutations)
|
||||
- Zustand (utile pour état local UI uniquement, complémentaire)
|
||||
|
||||
## Export CSV
|
||||
- **Décision** : module `csv` Python stdlib
|
||||
- **Rationale** : aucune dépendance additionnelle, suffisant pour colonnes tabulaires de transactions, streaming possible via `StreamingResponse` FastAPI
|
||||
- **Alternatives** :
|
||||
- pandas (overkill, dépendance lourde)
|
||||
|
||||
## Export PDF
|
||||
- **Décision** : WeasyPrint
|
||||
- **Rationale** : rendu HTML→PDF côté serveur, templates Jinja2 réutilisables, CSS support correct, rendu fidèle
|
||||
- **Alternatives** :
|
||||
- ReportLab (API bas niveau, verbeux pour des tableaux)
|
||||
- pdfkit (dépendance système wkhtmltopdf, difficile à dockeriser)
|
||||
- Reportlab + xhtml2pdf (moins stable)
|
||||
|
||||
## Stockage des montants
|
||||
- **Décision** : entiers en centimes (`amount_cents INTEGER`)
|
||||
- **Rationale** : conforme au principe II de la Constitution — élimine toute erreur d'arrondi IEEE 754 sur les opérations financières (ex : 0.1 + 0.2 ≠ 0.3 en float)
|
||||
- **Règle** : la conversion euros↔centimes se fait uniquement aux frontières (serialisation Pydantic et affichage React)
|
||||
|
||||
## Soft Delete
|
||||
- **Décision** : champ `deleted_at TIMESTAMP NULL` sur `Transaction` et `Category`
|
||||
- **Rationale** : conforme au principe II (tracabilité historique), permet l'audit et la restauration éventuelle
|
||||
- **Implémentation** : filtre `WHERE deleted_at IS NULL` systématique dans les requêtes ; index partiel recommandé
|
||||
|
||||
## Conteneurisation
|
||||
- **Décision** : Docker Compose (backend + frontend + PostgreSQL)
|
||||
- **Rationale** : conforme au principe V de la Constitution, reproductibilité dev/prod, un seul `docker compose up`
|
||||
- **Structure** :
|
||||
- `backend/` — image Python 3.12-slim
|
||||
- `frontend/` — build Vite servi par Nginx
|
||||
- `db/` — PostgreSQL 16 officiel
|
||||
|
||||
## Tests Backend
|
||||
- **Décision** : pytest + httpx (client async) + pytest-asyncio + base de données de test dédiée
|
||||
- **Rationale** : conforme au principe III (80% coverage services/), tests d'intégration sur vraie BDD (pas de mock SQLAlchemy)
|
||||
- **Alternatives** :
|
||||
- unittest (moins expressif)
|
||||
- mocks SQLAlchemy (rejeté — risque de divergence mock/prod)
|
||||
@@ -0,0 +1,187 @@
|
||||
# Feature Specification: Budget Tracker Core
|
||||
|
||||
**Feature Branch**: `003-budget-tracker-core`
|
||||
**Created**: 2026-03-15
|
||||
**Status**: Draft
|
||||
**Input**: User description: "App web de suivi de budget personnel : suivi des revenus et dépenses, catégorisation, tableaux de bord, gestion de budgets par enveloppe, export CSV/PDF, historique mensuel, authentification utilisateur."
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Saisie et consultation des transactions (Priority: P1)
|
||||
|
||||
En tant qu'utilisateur authentifié, je veux ajouter, modifier et supprimer des transactions (revenus ou dépenses) afin de maintenir un historique fidèle de mes mouvements financiers.
|
||||
|
||||
**Why this priority**: C'est le cœur fonctionnel de l'application. Sans la capacité d'enregistrer des transactions, aucune autre fonctionnalité (budget, graphiques, export) n'a de sens. Livrer uniquement cette story constitue déjà un journal financier minimaliste opérationnel.
|
||||
|
||||
**Independent Test**: Peut être testé indépendamment en ajoutant une transaction via le formulaire, en vérifiant son apparition dans la liste, puis en la modifiant et en la supprimant — sans aucune autre fonctionnalité active.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** un utilisateur authentifié sur la page "Transactions", **When** il soumet le formulaire avec un montant (ex: 45,90 €), une date, un type "dépense" et une description, **Then** la transaction apparaît en tête de liste avec le bon montant, la bonne date et le bon type.
|
||||
2. **Given** une transaction existante, **When** l'utilisateur clique sur "Modifier" et change le montant à 50,00 €, **Then** la liste affiche immédiatement le montant mis à jour et le solde courant est recalculé.
|
||||
3. **Given** une transaction existante, **When** l'utilisateur clique sur "Supprimer" et confirme la suppression, **Then** la transaction disparaît de la liste et le solde est recalculé en conséquence ; la transaction reste présente en base (soft-delete) et n'est plus visible dans l'interface.
|
||||
4. **Given** un formulaire de saisie, **When** l'utilisateur soumet sans montant ou avec un montant négatif, **Then** un message d'erreur explicite est affiché et aucune transaction n'est créée.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Tableau de bord avec solde et résumé mensuel (Priority: P2)
|
||||
|
||||
En tant qu'utilisateur authentifié, je veux voir en un coup d'œil mon solde courant, le total de mes revenus et dépenses du mois, ainsi qu'un graphique de répartition des dépenses par catégorie, afin de comprendre rapidement ma situation financière.
|
||||
|
||||
**Why this priority**: Le tableau de bord transforme les données brutes en information utile. Il constitue la page d'accueil naturelle de l'application et justifie la valeur perçue de l'outil dès la première visite. Dépend de P1 (transactions existantes) mais peut être rendu statique avec des données de seed.
|
||||
|
||||
**Independent Test**: Peut être testé indépendamment en pré-chargeant une base avec des transactions de test et en vérifiant que les KPIs et graphiques affichent des valeurs cohérentes avec les données injectées.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** un utilisateur avec des transactions en mars 2026, **When** il accède au tableau de bord, **Then** il voit le solde courant (revenus − dépenses depuis l'origine), le total des revenus du mois, le total des dépenses du mois et le solde net du mois.
|
||||
2. **Given** un utilisateur avec des dépenses réparties sur 3 catégories, **When** il consulte le tableau de bord, **Then** un graphique en camembert ou en barres affiche la répartition des dépenses par catégorie pour le mois courant, avec les montants et pourcentages visibles au survol.
|
||||
3. **Given** un utilisateur sans transaction ce mois-ci, **When** il consulte le tableau de bord, **Then** les KPIs affichent 0 € et le graphique affiche un état vide avec un message d'invitation à saisir des transactions.
|
||||
4. **Given** un utilisateur sur le tableau de bord, **When** il navigue vers le mois précédent via les contrôles de navigation, **Then** tous les KPIs et graphiques se mettent à jour pour refléter la période sélectionnée.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Catégorisation des transactions (Priority: P3)
|
||||
|
||||
En tant qu'utilisateur authentifié, je veux assigner chaque transaction à une catégorie (alimentation, transport, loisirs, etc.) et gérer mes propres catégories personnalisées, afin d'analyser mes dépenses par poste.
|
||||
|
||||
**Why this priority**: La catégorisation enrichit les transactions et débloque les analyses (graphiques, budgets par enveloppe). Les catégories par défaut permettent un onboarding rapide ; la personnalisation répond aux besoins individuels. Dépend de P1.
|
||||
|
||||
**Independent Test**: Peut être testé indépendamment en créant une catégorie personnalisée, en l'assignant à une transaction, et en vérifiant que la transaction apparaît bien filtrée par cette catégorie.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** un utilisateur sur la page "Catégories", **When** il crée une catégorie "Abonnements" avec une couleur et une icône, **Then** la catégorie est disponible dans le sélecteur lors de la création/modification d'une transaction.
|
||||
2. **Given** une catégorie utilisée par au moins une transaction, **When** l'utilisateur tente de la supprimer, **Then** un avertissement l'informe que X transactions utilisent cette catégorie et lui propose de les réassigner avant suppression.
|
||||
3. **Given** un utilisateur sur la liste des transactions, **When** il filtre par catégorie "Alimentation", **Then** seules les transactions de cette catégorie sont affichées, avec le total filtré visible.
|
||||
4. **Given** un nouveau compte utilisateur, **When** il accède pour la première fois à l'application, **Then** un ensemble de catégories par défaut (Alimentation, Transport, Logement, Loisirs, Santé, Revenus) est déjà disponible.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - Gestion des budgets par enveloppe (Priority: P4)
|
||||
|
||||
En tant qu'utilisateur authentifié, je veux définir un budget maximum mensuel par catégorie (ex : 300 € pour "Alimentation") et visualiser ma consommation en temps réel, afin de respecter mes objectifs de dépenses.
|
||||
|
||||
**Why this priority**: La gestion budgétaire est la fonctionnalité de pilotage financier. Elle différencie l'application d'un simple journal de comptes. Nécessite P1 et P3 pour être utile.
|
||||
|
||||
**Independent Test**: Peut être testé indépendamment en créant un budget pour une catégorie, en ajoutant des transactions dans cette catégorie et en vérifiant que la barre de progression reflète le bon taux de consommation.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** un utilisateur sur la page "Budgets", **When** il crée un budget de 300 € pour la catégorie "Alimentation" pour mars 2026, **Then** une carte budget apparaît avec une barre de progression à 0 % et le plafond affiché.
|
||||
2. **Given** un budget "Alimentation" de 300 € et 180 € de dépenses déjà enregistrées dans cette catégorie ce mois-ci, **When** l'utilisateur consulte la page "Budgets", **Then** la barre de progression indique 60 % (180/300 €), avec le montant restant affiché (120 €).
|
||||
3. **Given** un budget dont les dépenses dépassent le plafond, **When** l'utilisateur consulte la page "Budgets", **Then** la barre de progression passe au rouge, le dépassement est affiché en négatif (ex : −30 €) et une alerte visuelle attire l'attention.
|
||||
4. **Given** un budget défini pour mars 2026, **When** avril 2026 commence, **Then** le système crée automatiquement un nouveau budget pour avril avec le même plafond, ou invite l'utilisateur à reconduire ses budgets.
|
||||
|
||||
---
|
||||
|
||||
### User Story 5 - Historique mensuel navigable (Priority: P5)
|
||||
|
||||
En tant qu'utilisateur authentifié, je veux naviguer mois par mois dans mon historique de transactions, afin de retrouver et analyser mes dépenses passées.
|
||||
|
||||
**Why this priority**: La navigation temporelle est essentielle pour le suivi sur le long terme. Elle complète les fonctionnalités de base sans en être un prérequis. Dépend de P1.
|
||||
|
||||
**Independent Test**: Peut être testé indépendamment en naviguant vers un mois passé contenant des transactions seed et en vérifiant que seules les transactions de ce mois s'affichent avec les bons totaux.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** un utilisateur sur la page "Transactions", **When** il clique sur "< Mois précédent", **Then** la liste affiche uniquement les transactions du mois précédent, avec le sélecteur de période mis à jour.
|
||||
2. **Given** un utilisateur sur un mois passé, **When** il clique sur "Mois suivant" jusqu'au mois courant, **Then** les données du mois courant s'affichent correctement.
|
||||
3. **Given** un utilisateur souhaitant aller à une date précise, **When** il sélectionne "Janvier 2025" dans le sélecteur de mois, **Then** la liste de transactions et les statistiques correspondantes s'affichent instantanément.
|
||||
|
||||
---
|
||||
|
||||
### User Story 6 - Export des données (Priority: P6)
|
||||
|
||||
En tant qu'utilisateur authentifié, je veux exporter mes transactions en CSV ou en PDF pour un mois donné ou pour toute la période, afin de les utiliser dans un tableur ou de les archiver.
|
||||
|
||||
**Why this priority**: L'export est une fonctionnalité de sortie de données sans dépendance critique sur les autres features. Elle répond à un besoin de portabilité et de conformité (déclaration fiscale, remboursements). Dépend de P1.
|
||||
|
||||
**Independent Test**: Peut être testé indépendamment en déclenchant un export CSV pour le mois courant et en vérifiant que le fichier téléchargé contient les colonnes et données attendues.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** un utilisateur avec des transactions en mars 2026, **When** il clique sur "Exporter CSV" pour mars 2026, **Then** un fichier `transactions_2026-03.csv` est téléchargé avec les colonnes : date, description, catégorie, type, montant.
|
||||
2. **Given** un utilisateur souhaitant un export PDF, **When** il clique sur "Exporter PDF" pour mars 2026, **Then** un fichier `rapport_2026-03.pdf` est généré avec le résumé mensuel (solde, totaux), le graphique de répartition et le détail des transactions.
|
||||
3. **Given** un mois sans aucune transaction, **When** l'utilisateur tente un export, **Then** un message lui indique qu'il n'y a pas de données à exporter et aucun fichier n'est généré.
|
||||
4. **Given** un export en cours, **When** le fichier dépasse 10 000 lignes, **Then** l'export est déclenché en tâche de fond et l'utilisateur est notifié (ou redirigé vers un lien de téléchargement) à la fin du traitement.
|
||||
|
||||
---
|
||||
|
||||
### User Story 7 - Authentification utilisateur (Priority: P7)
|
||||
|
||||
En tant qu'utilisateur, je veux créer un compte et me connecter de façon sécurisée, afin que mes données financières soient privées et isolées des autres utilisateurs.
|
||||
|
||||
**Why this priority**: L'authentification est un prérequis à l'isolation des données en mode multi-utilisateurs. En mode instance unique (un seul utilisateur), elle peut être différée. Placée en P7 car l'ensemble des fonctionnalités P1–P6 peut être développé et testé avec un utilisateur fictif en seed.
|
||||
|
||||
**Independent Test**: Peut être testé indépendamment en créant un compte, en se déconnectant, en tentant d'accéder à une route protégée (redirection attendue vers login), puis en se reconnectant.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** un visiteur non authentifié, **When** il accède à n'importe quelle route protégée, **Then** il est redirigé vers la page de connexion.
|
||||
2. **Given** un formulaire d'inscription, **When** l'utilisateur saisit un email valide et un mot de passe d'au moins 8 caractères, **Then** son compte est créé, il est automatiquement connecté et redirigé vers le tableau de bord.
|
||||
3. **Given** un utilisateur avec un compte existant, **When** il se connecte avec ses identifiants corrects, **Then** un token JWT est émis, stocké côté client, et il est redirigé vers le tableau de bord.
|
||||
4. **Given** un utilisateur connecté, **When** son token expire ou qu'il clique sur "Déconnexion", **Then** le token est invalidé, la session est détruite et il est redirigé vers la page de connexion.
|
||||
5. **Given** deux utilisateurs distincts avec leurs propres transactions, **When** chacun consulte son tableau de bord, **Then** aucun ne voit les données de l'autre.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Que se passe-t-il si l'utilisateur saisit un montant avec plus de 2 décimales (ex : 10,999 €) ? → arrondi à 2 décimales côté API avant stockage en centimes.
|
||||
- Comment le système gère-t-il un montant de 0 € ? → rejeté avec un message d'erreur (une transaction à 0 n'a pas de sens métier).
|
||||
- Que se passe-t-il si l'utilisateur change la devise en cours de route ? → la devise est fixée par instance (paramètre de configuration), pas modifiable par transaction.
|
||||
- Que se passe-t-il lors de la suppression d'un compte utilisateur ? → soft-delete du compte ; les données associées sont conservées 30 jours avant purge définitive.
|
||||
- Comment le système gère-t-il une date de transaction dans le futur ? → autorisée (prévisionnel), affichée avec une mention "À venir" dans la liste.
|
||||
- Que se passe-t-il si deux utilisateurs soumettent simultanément une modification sur la même transaction ? → la dernière écriture gagne (last-write-wins) ; la cohérence est garantie par les transactions ACID PostgreSQL.
|
||||
- Que se passe-t-il si le budget d'une catégorie est supprimé alors que des transactions y sont rattachées ? → les transactions subsistent sans budget associé ; elles n'apparaissent plus dans la vue "Budgets" mais restent visibles dans "Transactions".
|
||||
- Comment le système gère-t-il un export PDF avec plus de 500 transactions ? → génération asynchrone côté serveur avec lien de téléchargement différé.
|
||||
- Que se passe-t-il si PostgreSQL est indisponible ? → l'API répond 503 Service Unavailable avec un message générique ; aucune donnée partielle n'est exposée.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: Le système DOIT permettre à un utilisateur authentifié de créer, lire, modifier et supprimer (soft-delete) des transactions financières.
|
||||
- **FR-002**: Chaque transaction DOIT obligatoirement contenir : un montant (> 0), une date, un type (revenu ou dépense), et une catégorie.
|
||||
- **FR-003**: Les montants DOIVENT être stockés en centimes (entiers) pour éviter les erreurs d'arrondi en virgule flottante.
|
||||
- **FR-004**: Le système DOIT calculer et exposer le solde courant de l'utilisateur (somme des revenus − somme des dépenses non supprimées depuis l'origine).
|
||||
- **FR-005**: Le système DOIT permettre de filtrer les transactions par période (mois/année), par catégorie et par type.
|
||||
- **FR-006**: Le système DOIT fournir un tableau de bord mensuel exposant : solde courant, total revenus du mois, total dépenses du mois, solde net du mois, répartition des dépenses par catégorie.
|
||||
- **FR-007**: Le système DOIT permettre à un utilisateur de créer, modifier et supprimer des catégories personnalisées avec un nom, une couleur et une icône facultative.
|
||||
- **FR-008**: Le système DOIT fournir un ensemble de catégories par défaut pré-chargées pour tout nouveau compte (Alimentation, Transport, Logement, Loisirs, Santé, Revenus).
|
||||
- **FR-009**: La suppression d'une catégorie utilisée par des transactions DOIT être bloquée jusqu'à réassignation ou suppression des transactions associées.
|
||||
- **FR-010**: Le système DOIT permettre de définir un budget maximum mensuel par catégorie (couple catégorie × mois).
|
||||
- **FR-011**: Le système DOIT calculer en temps réel le taux de consommation de chaque budget (dépenses de la catégorie sur le mois / plafond défini).
|
||||
- **FR-012**: Le système DOIT alerter visuellement l'utilisateur lorsqu'un budget est dépassé (taux > 100 %).
|
||||
- **FR-013**: Le système DOIT permettre la navigation mensuelle dans l'historique des transactions et des budgets.
|
||||
- **FR-014**: Le système DOIT permettre l'export des transactions d'une période donnée au format CSV avec les colonnes : date, description, catégorie, type, montant (en euros).
|
||||
- **FR-015**: Le système DOIT permettre l'export d'un rapport mensuel au format PDF incluant le résumé financier, le graphique de répartition et le détail des transactions.
|
||||
- **FR-016**: Le système DOIT gérer l'authentification des utilisateurs via email + mot de passe avec émission d'un token JWT.
|
||||
- **FR-017**: Toutes les routes de l'API DOIVENT être protégées par authentification à l'exception des endpoints d'inscription et de connexion.
|
||||
- **FR-018**: Les données de chaque utilisateur DOIVENT être strictement isolées : aucun utilisateur ne peut accéder aux données d'un autre.
|
||||
- **FR-019**: Le système DOIT exposer une API REST documentée (OpenAPI/Swagger) pour l'ensemble des fonctionnalités.
|
||||
- **FR-020**: Le système DOIT être déployable via `docker compose up` sans configuration manuelle au-delà du fichier `.env`.
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **User** : Compte utilisateur. Attributs : `id`, `email`, `hashed_password`, `created_at`, `deleted_at`. Un utilisateur possède des transactions, des catégories et des budgets.
|
||||
- **Transaction** : Mouvement financier unitaire. Attributs : `id`, `user_id`, `amount_cents` (entier), `type` (income | expense), `date`, `description`, `category_id`, `created_at`, `updated_at`, `deleted_at`.
|
||||
- **Category** : Classification d'une transaction. Attributs : `id`, `user_id` (null pour les catégories système), `name`, `color`, `icon`, `created_at`, `deleted_at`. Une catégorie appartient à un utilisateur ou est une catégorie globale partagée.
|
||||
- **Budget** : Enveloppe de dépense mensuelle par catégorie. Attributs : `id`, `user_id`, `category_id`, `month` (YYYY-MM), `limit_cents` (entier), `created_at`, `updated_at`. Relation : un budget est lié à une catégorie et à un mois calendaire.
|
||||
- **ExportJob** *(optionnel — exports asynchrones larges)* : Traçabilité des exports générés. Attributs : `id`, `user_id`, `type` (csv | pdf), `period`, `status` (pending | done | error), `file_url`, `created_at`.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: Un utilisateur peut saisir une transaction complète (montant, date, catégorie, type, description) en moins de 30 secondes depuis la page principale.
|
||||
- **SC-002**: Le tableau de bord se charge et affiche des données à jour en moins de 2 secondes pour un compte comportant jusqu'à 10 000 transactions.
|
||||
- **SC-003**: 100 % des calculs de solde, totaux et taux de consommation de budget sont couverts par des tests unitaires passants.
|
||||
- **SC-004**: La couverture de tests sur le dossier `services/` (logique métier backend) est supérieure ou égale à 80 %.
|
||||
- **SC-005**: L'ensemble de l'application (backend + frontend + base de données) démarre correctement via `docker compose up` en moins de 5 minutes sur une machine vierge disposant de Docker.
|
||||
- **SC-006**: Tous les endpoints API répondent avec les codes HTTP corrects (200/201 pour les succès, 400/422 pour les erreurs de validation, 401 pour les accès non authentifiés, 404 pour les ressources introuvables) — vérifiable via les tests d'intégration.
|
||||
- **SC-007**: Un export CSV pour un mois contenant jusqu'à 1 000 transactions se génère et se télécharge en moins de 5 secondes.
|
||||
- **SC-008**: Zéro donnée d'un utilisateur A n'est accessible depuis le compte d'un utilisateur B, vérifié par des tests d'intégration dédiés à l'isolation.
|
||||
- **SC-009**: Le tableau de bord affiche un graphique de répartition des dépenses par catégorie lisible pour 1 à 15 catégories distinctes.
|
||||
- **SC-010**: L'interface est utilisable sur desktop (≥ 1280 px) et sur mobile (≥ 375 px) sans défilement horizontal ni éléments tronqués sur les vues principales (tableau de bord, transactions, budgets).
|
||||
Executable
+190
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Consolidated prerequisite checking script
|
||||
#
|
||||
# This script provides unified prerequisite checking for Spec-Driven Development workflow.
|
||||
# It replaces the functionality previously spread across multiple scripts.
|
||||
#
|
||||
# Usage: ./check-prerequisites.sh [OPTIONS]
|
||||
#
|
||||
# OPTIONS:
|
||||
# --json Output in JSON format
|
||||
# --require-tasks Require tasks.md to exist (for implementation phase)
|
||||
# --include-tasks Include tasks.md in AVAILABLE_DOCS list
|
||||
# --paths-only Only output path variables (no validation)
|
||||
# --help, -h Show help message
|
||||
#
|
||||
# OUTPUTS:
|
||||
# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]}
|
||||
# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md
|
||||
# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc.
|
||||
|
||||
set -e
|
||||
|
||||
# Parse command line arguments
|
||||
JSON_MODE=false
|
||||
REQUIRE_TASKS=false
|
||||
INCLUDE_TASKS=false
|
||||
PATHS_ONLY=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--require-tasks)
|
||||
REQUIRE_TASKS=true
|
||||
;;
|
||||
--include-tasks)
|
||||
INCLUDE_TASKS=true
|
||||
;;
|
||||
--paths-only)
|
||||
PATHS_ONLY=true
|
||||
;;
|
||||
--help|-h)
|
||||
cat << 'EOF'
|
||||
Usage: check-prerequisites.sh [OPTIONS]
|
||||
|
||||
Consolidated prerequisite checking for Spec-Driven Development workflow.
|
||||
|
||||
OPTIONS:
|
||||
--json Output in JSON format
|
||||
--require-tasks Require tasks.md to exist (for implementation phase)
|
||||
--include-tasks Include tasks.md in AVAILABLE_DOCS list
|
||||
--paths-only Only output path variables (no prerequisite validation)
|
||||
--help, -h Show this help message
|
||||
|
||||
EXAMPLES:
|
||||
# Check task prerequisites (plan.md required)
|
||||
./check-prerequisites.sh --json
|
||||
|
||||
# Check implementation prerequisites (plan.md + tasks.md required)
|
||||
./check-prerequisites.sh --json --require-tasks --include-tasks
|
||||
|
||||
# Get feature paths only (no validation)
|
||||
./check-prerequisites.sh --paths-only
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Source common functions
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get feature paths and validate branch
|
||||
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
||||
eval "$_paths_output"
|
||||
unset _paths_output
|
||||
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
||||
|
||||
# If paths-only mode, output paths and exit (support JSON + paths-only combined)
|
||||
if $PATHS_ONLY; then
|
||||
if $JSON_MODE; then
|
||||
# Minimal JSON paths payload (no validation performed)
|
||||
if has_jq; then
|
||||
jq -cn \
|
||||
--arg repo_root "$REPO_ROOT" \
|
||||
--arg branch "$CURRENT_BRANCH" \
|
||||
--arg feature_dir "$FEATURE_DIR" \
|
||||
--arg feature_spec "$FEATURE_SPEC" \
|
||||
--arg impl_plan "$IMPL_PLAN" \
|
||||
--arg tasks "$TASKS" \
|
||||
'{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}'
|
||||
else
|
||||
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
|
||||
"$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")"
|
||||
fi
|
||||
else
|
||||
echo "REPO_ROOT: $REPO_ROOT"
|
||||
echo "BRANCH: $CURRENT_BRANCH"
|
||||
echo "FEATURE_DIR: $FEATURE_DIR"
|
||||
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
||||
echo "IMPL_PLAN: $IMPL_PLAN"
|
||||
echo "TASKS: $TASKS"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Validate required directories and files
|
||||
if [[ ! -d "$FEATURE_DIR" ]]; then
|
||||
echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
|
||||
echo "Run /speckit.specify first to create the feature structure." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$IMPL_PLAN" ]]; then
|
||||
echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
|
||||
echo "Run /speckit.plan first to create the implementation plan." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for tasks.md if required
|
||||
if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then
|
||||
echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2
|
||||
echo "Run /speckit.tasks first to create the task list." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build list of available documents
|
||||
docs=()
|
||||
|
||||
# Always check these optional docs
|
||||
[[ -f "$RESEARCH" ]] && docs+=("research.md")
|
||||
[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
|
||||
|
||||
# Check contracts directory (only if it exists and has files)
|
||||
if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
|
||||
docs+=("contracts/")
|
||||
fi
|
||||
|
||||
[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
|
||||
|
||||
# Include tasks.md if requested and it exists
|
||||
if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then
|
||||
docs+=("tasks.md")
|
||||
fi
|
||||
|
||||
# Output results
|
||||
if $JSON_MODE; then
|
||||
# Build JSON array of documents
|
||||
if has_jq; then
|
||||
if [[ ${#docs[@]} -eq 0 ]]; then
|
||||
json_docs="[]"
|
||||
else
|
||||
json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
|
||||
fi
|
||||
jq -cn \
|
||||
--arg feature_dir "$FEATURE_DIR" \
|
||||
--argjson docs "$json_docs" \
|
||||
'{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}'
|
||||
else
|
||||
if [[ ${#docs[@]} -eq 0 ]]; then
|
||||
json_docs="[]"
|
||||
else
|
||||
json_docs=$(printf '"%s",' "${docs[@]}")
|
||||
json_docs="[${json_docs%,}]"
|
||||
fi
|
||||
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs"
|
||||
fi
|
||||
else
|
||||
# Text output
|
||||
echo "FEATURE_DIR:$FEATURE_DIR"
|
||||
echo "AVAILABLE_DOCS:"
|
||||
|
||||
# Show status of each potential document
|
||||
check_file "$RESEARCH" "research.md"
|
||||
check_file "$DATA_MODEL" "data-model.md"
|
||||
check_dir "$CONTRACTS_DIR" "contracts/"
|
||||
check_file "$QUICKSTART" "quickstart.md"
|
||||
|
||||
if $INCLUDE_TASKS; then
|
||||
check_file "$TASKS" "tasks.md"
|
||||
fi
|
||||
fi
|
||||
Executable
+253
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env bash
|
||||
# Common functions and variables for all scripts
|
||||
|
||||
# Get repository root, with fallback for non-git repositories
|
||||
get_repo_root() {
|
||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
git rev-parse --show-toplevel
|
||||
else
|
||||
# Fall back to script location for non-git repos
|
||||
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
(cd "$script_dir/../../.." && pwd)
|
||||
fi
|
||||
}
|
||||
|
||||
# Get current branch, with fallback for non-git repositories
|
||||
get_current_branch() {
|
||||
# First check if SPECIFY_FEATURE environment variable is set
|
||||
if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
|
||||
echo "$SPECIFY_FEATURE"
|
||||
return
|
||||
fi
|
||||
|
||||
# Then check git if available
|
||||
if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
return
|
||||
fi
|
||||
|
||||
# For non-git repos, try to find the latest feature directory
|
||||
local repo_root=$(get_repo_root)
|
||||
local specs_dir="$repo_root/specs"
|
||||
|
||||
if [[ -d "$specs_dir" ]]; then
|
||||
local latest_feature=""
|
||||
local highest=0
|
||||
|
||||
for dir in "$specs_dir"/*; do
|
||||
if [[ -d "$dir" ]]; then
|
||||
local dirname=$(basename "$dir")
|
||||
if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
|
||||
local number=${BASH_REMATCH[1]}
|
||||
number=$((10#$number))
|
||||
if [[ "$number" -gt "$highest" ]]; then
|
||||
highest=$number
|
||||
latest_feature=$dirname
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "$latest_feature" ]]; then
|
||||
echo "$latest_feature"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "main" # Final fallback
|
||||
}
|
||||
|
||||
# Check if we have git available
|
||||
has_git() {
|
||||
git rev-parse --show-toplevel >/dev/null 2>&1
|
||||
}
|
||||
|
||||
check_feature_branch() {
|
||||
local branch="$1"
|
||||
local has_git_repo="$2"
|
||||
|
||||
# For non-git repos, we can't enforce branch naming but still provide output
|
||||
if [[ "$has_git_repo" != "true" ]]; then
|
||||
echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
|
||||
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
|
||||
echo "Feature branches should be named like: 001-feature-name" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
get_feature_dir() { echo "$1/specs/$2"; }
|
||||
|
||||
# Find feature directory by numeric prefix instead of exact branch match
|
||||
# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
|
||||
find_feature_dir_by_prefix() {
|
||||
local repo_root="$1"
|
||||
local branch_name="$2"
|
||||
local specs_dir="$repo_root/specs"
|
||||
|
||||
# Extract numeric prefix from branch (e.g., "004" from "004-whatever")
|
||||
if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
|
||||
# If branch doesn't have numeric prefix, fall back to exact match
|
||||
echo "$specs_dir/$branch_name"
|
||||
return
|
||||
fi
|
||||
|
||||
local prefix="${BASH_REMATCH[1]}"
|
||||
|
||||
# Search for directories in specs/ that start with this prefix
|
||||
local matches=()
|
||||
if [[ -d "$specs_dir" ]]; then
|
||||
for dir in "$specs_dir"/"$prefix"-*; do
|
||||
if [[ -d "$dir" ]]; then
|
||||
matches+=("$(basename "$dir")")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Handle results
|
||||
if [[ ${#matches[@]} -eq 0 ]]; then
|
||||
# No match found - return the branch name path (will fail later with clear error)
|
||||
echo "$specs_dir/$branch_name"
|
||||
elif [[ ${#matches[@]} -eq 1 ]]; then
|
||||
# Exactly one match - perfect!
|
||||
echo "$specs_dir/${matches[0]}"
|
||||
else
|
||||
# Multiple matches - this shouldn't happen with proper naming convention
|
||||
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
|
||||
echo "Please ensure only one spec directory exists per numeric prefix." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
get_feature_paths() {
|
||||
local repo_root=$(get_repo_root)
|
||||
local current_branch=$(get_current_branch)
|
||||
local has_git_repo="false"
|
||||
|
||||
if has_git; then
|
||||
has_git_repo="true"
|
||||
fi
|
||||
|
||||
# Use prefix-based lookup to support multiple branches per spec
|
||||
local feature_dir
|
||||
if ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then
|
||||
echo "ERROR: Failed to resolve feature directory" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Use printf '%q' to safely quote values, preventing shell injection
|
||||
# via crafted branch names or paths containing special characters
|
||||
printf 'REPO_ROOT=%q\n' "$repo_root"
|
||||
printf 'CURRENT_BRANCH=%q\n' "$current_branch"
|
||||
printf 'HAS_GIT=%q\n' "$has_git_repo"
|
||||
printf 'FEATURE_DIR=%q\n' "$feature_dir"
|
||||
printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"
|
||||
printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"
|
||||
printf 'TASKS=%q\n' "$feature_dir/tasks.md"
|
||||
printf 'RESEARCH=%q\n' "$feature_dir/research.md"
|
||||
printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md"
|
||||
printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md"
|
||||
printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts"
|
||||
}
|
||||
|
||||
# Check if jq is available for safe JSON construction
|
||||
has_jq() {
|
||||
command -v jq >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
|
||||
# Handles backslash, double-quote, and control characters (newline, tab, carriage return).
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
s="${s//$'\r'/\\r}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||
|
||||
# Resolve a template name to a file path using the priority stack:
|
||||
# 1. .specify/templates/overrides/
|
||||
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
|
||||
# 3. .specify/extensions/<ext-id>/templates/
|
||||
# 4. .specify/templates/ (core)
|
||||
resolve_template() {
|
||||
local template_name="$1"
|
||||
local repo_root="$2"
|
||||
local base="$repo_root/.specify/templates"
|
||||
|
||||
# Priority 1: Project overrides
|
||||
local override="$base/overrides/${template_name}.md"
|
||||
[ -f "$override" ] && echo "$override" && return 0
|
||||
|
||||
# Priority 2: Installed presets (sorted by priority from .registry)
|
||||
local presets_dir="$repo_root/.specify/presets"
|
||||
if [ -d "$presets_dir" ]; then
|
||||
local registry_file="$presets_dir/.registry"
|
||||
if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
|
||||
# Read preset IDs sorted by priority (lower number = higher precedence)
|
||||
local sorted_presets
|
||||
sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c "
|
||||
import json, sys, os
|
||||
try:
|
||||
with open(os.environ['SPECKIT_REGISTRY']) as f:
|
||||
data = json.load(f)
|
||||
presets = data.get('presets', {})
|
||||
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10)):
|
||||
print(pid)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
" 2>/dev/null)
|
||||
if [ $? -eq 0 ] && [ -n "$sorted_presets" ]; then
|
||||
while IFS= read -r preset_id; do
|
||||
local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
|
||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
||||
done <<< "$sorted_presets"
|
||||
else
|
||||
# python3 returned empty list — fall through to directory scan
|
||||
for preset in "$presets_dir"/*/; do
|
||||
[ -d "$preset" ] || continue
|
||||
local candidate="$preset/templates/${template_name}.md"
|
||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
||||
done
|
||||
fi
|
||||
else
|
||||
# Fallback: alphabetical directory order (no python3 available)
|
||||
for preset in "$presets_dir"/*/; do
|
||||
[ -d "$preset" ] || continue
|
||||
local candidate="$preset/templates/${template_name}.md"
|
||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Priority 3: Extension-provided templates
|
||||
local ext_dir="$repo_root/.specify/extensions"
|
||||
if [ -d "$ext_dir" ]; then
|
||||
for ext in "$ext_dir"/*/; do
|
||||
[ -d "$ext" ] || continue
|
||||
# Skip hidden directories (e.g. .backup, .cache)
|
||||
case "$(basename "$ext")" in .*) continue;; esac
|
||||
local candidate="$ext/templates/${template_name}.md"
|
||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
||||
done
|
||||
fi
|
||||
|
||||
# Priority 4: Core templates
|
||||
local core="$base/${template_name}.md"
|
||||
[ -f "$core" ] && echo "$core" && return 0
|
||||
|
||||
# Return success with empty output so callers using set -e don't abort;
|
||||
# callers check [ -n "$TEMPLATE" ] to detect "not found".
|
||||
return 0
|
||||
}
|
||||
|
||||
Executable
+333
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
JSON_MODE=false
|
||||
SHORT_NAME=""
|
||||
BRANCH_NUMBER=""
|
||||
ARGS=()
|
||||
i=1
|
||||
while [ $i -le $# ]; do
|
||||
arg="${!i}"
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--short-name)
|
||||
if [ $((i + 1)) -gt $# ]; then
|
||||
echo 'Error: --short-name requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
i=$((i + 1))
|
||||
next_arg="${!i}"
|
||||
# Check if the next argument is another option (starts with --)
|
||||
if [[ "$next_arg" == --* ]]; then
|
||||
echo 'Error: --short-name requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
SHORT_NAME="$next_arg"
|
||||
;;
|
||||
--number)
|
||||
if [ $((i + 1)) -gt $# ]; then
|
||||
echo 'Error: --number requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
i=$((i + 1))
|
||||
next_arg="${!i}"
|
||||
if [[ "$next_arg" == --* ]]; then
|
||||
echo 'Error: --number requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
BRANCH_NUMBER="$next_arg"
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --json Output in JSON format"
|
||||
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
||||
echo " --number N Specify branch number manually (overrides auto-detection)"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
||||
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
|
||||
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Error: Feature description cannot be empty or contain only whitespace" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to find the repository root by searching for existing project markers
|
||||
find_repo_root() {
|
||||
local dir="$1"
|
||||
while [ "$dir" != "/" ]; do
|
||||
if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to get highest number from specs directory
|
||||
get_highest_from_specs() {
|
||||
local specs_dir="$1"
|
||||
local highest=0
|
||||
|
||||
if [ -d "$specs_dir" ]; then
|
||||
for dir in "$specs_dir"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
dirname=$(basename "$dir")
|
||||
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$highest" ]; then
|
||||
highest=$number
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Function to get highest number from git branches
|
||||
get_highest_from_branches() {
|
||||
local highest=0
|
||||
|
||||
# Get all branches (local and remote)
|
||||
branches=$(git branch -a 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$branches" ]; then
|
||||
while IFS= read -r branch; do
|
||||
# Clean branch name: remove leading markers and remote prefixes
|
||||
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
|
||||
|
||||
# Extract feature number if branch matches pattern ###-*
|
||||
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
|
||||
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$highest" ]; then
|
||||
highest=$number
|
||||
fi
|
||||
fi
|
||||
done <<< "$branches"
|
||||
fi
|
||||
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Function to check existing branches (local and remote) and return next available number
|
||||
check_existing_branches() {
|
||||
local specs_dir="$1"
|
||||
|
||||
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||
git fetch --all --prune 2>/dev/null || true
|
||||
|
||||
# Get highest number from ALL branches (not just matching short name)
|
||||
local highest_branch=$(get_highest_from_branches)
|
||||
|
||||
# Get highest number from ALL specs (not just matching short name)
|
||||
local highest_spec=$(get_highest_from_specs "$specs_dir")
|
||||
|
||||
# Take the maximum of both
|
||||
local max_num=$highest_branch
|
||||
if [ "$highest_spec" -gt "$max_num" ]; then
|
||||
max_num=$highest_spec
|
||||
fi
|
||||
|
||||
# Return next number
|
||||
echo $((max_num + 1))
|
||||
}
|
||||
|
||||
# Function to clean and format a branch name
|
||||
clean_branch_name() {
|
||||
local name="$1"
|
||||
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
||||
}
|
||||
|
||||
# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
s="${s//$'\r'/\\r}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# Resolve repository root. Prefer git information when available, but fall back
|
||||
# to searching for repository markers so the workflow still functions in repositories that
|
||||
# were initialised with --no-git.
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
HAS_GIT=true
|
||||
else
|
||||
REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
|
||||
if [ -z "$REPO_ROOT" ]; then
|
||||
echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
HAS_GIT=false
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
SPECS_DIR="$REPO_ROOT/specs"
|
||||
mkdir -p "$SPECS_DIR"
|
||||
|
||||
# Function to generate branch name with stop word filtering and length filtering
|
||||
generate_branch_name() {
|
||||
local description="$1"
|
||||
|
||||
# Common stop words to filter out
|
||||
local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
|
||||
|
||||
# Convert to lowercase and split into words
|
||||
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
||||
|
||||
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
||||
local meaningful_words=()
|
||||
for word in $clean_name; do
|
||||
# Skip empty words
|
||||
[ -z "$word" ] && continue
|
||||
|
||||
# Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
|
||||
if ! echo "$word" | grep -qiE "$stop_words"; then
|
||||
if [ ${#word} -ge 3 ]; then
|
||||
meaningful_words+=("$word")
|
||||
elif echo "$description" | grep -q "\b${word^^}\b"; then
|
||||
# Keep short words if they appear as uppercase in original (likely acronyms)
|
||||
meaningful_words+=("$word")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# If we have meaningful words, use first 3-4 of them
|
||||
if [ ${#meaningful_words[@]} -gt 0 ]; then
|
||||
local max_words=3
|
||||
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
|
||||
|
||||
local result=""
|
||||
local count=0
|
||||
for word in "${meaningful_words[@]}"; do
|
||||
if [ $count -ge $max_words ]; then break; fi
|
||||
if [ -n "$result" ]; then result="$result-"; fi
|
||||
result="$result$word"
|
||||
count=$((count + 1))
|
||||
done
|
||||
echo "$result"
|
||||
else
|
||||
# Fallback to original logic if no meaningful words found
|
||||
local cleaned=$(clean_branch_name "$description")
|
||||
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate branch name
|
||||
if [ -n "$SHORT_NAME" ]; then
|
||||
# Use provided short name, just clean it up
|
||||
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
|
||||
else
|
||||
# Generate from description with smart filtering
|
||||
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
||||
fi
|
||||
|
||||
# Determine branch number
|
||||
if [ -z "$BRANCH_NUMBER" ]; then
|
||||
if [ "$HAS_GIT" = true ]; then
|
||||
# Check existing branches on remotes
|
||||
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
|
||||
else
|
||||
# Fall back to local directory check
|
||||
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
||||
BRANCH_NUMBER=$((HIGHEST + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
|
||||
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
||||
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
||||
|
||||
# GitHub enforces a 244-byte limit on branch names
|
||||
# Validate and truncate if necessary
|
||||
MAX_BRANCH_LENGTH=244
|
||||
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
|
||||
# Calculate how much we need to trim from suffix
|
||||
# Account for: feature number (3) + hyphen (1) = 4 chars
|
||||
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
|
||||
|
||||
# Truncate suffix at word boundary if possible
|
||||
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
|
||||
# Remove trailing hyphen if truncation created one
|
||||
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
|
||||
|
||||
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
|
||||
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
|
||||
|
||||
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
|
||||
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
|
||||
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
||||
fi
|
||||
|
||||
if [ "$HAS_GIT" = true ]; then
|
||||
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
|
||||
# Check if branch already exists
|
||||
if git branch --list "$BRANCH_NAME" | grep -q .; then
|
||||
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
|
||||
exit 1
|
||||
else
|
||||
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
||||
fi
|
||||
|
||||
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
|
||||
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT")
|
||||
SPEC_FILE="$FEATURE_DIR/spec.md"
|
||||
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
|
||||
|
||||
# Inform the user how to persist the feature variable in their own shell
|
||||
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
|
||||
|
||||
if $JSON_MODE; then
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
jq -cn \
|
||||
--arg branch_name "$BRANCH_NAME" \
|
||||
--arg spec_file "$SPEC_FILE" \
|
||||
--arg feature_num "$FEATURE_NUM" \
|
||||
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
|
||||
else
|
||||
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
|
||||
fi
|
||||
else
|
||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||
echo "SPEC_FILE: $SPEC_FILE"
|
||||
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
|
||||
fi
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Parse command line arguments
|
||||
JSON_MODE=false
|
||||
ARGS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--json]"
|
||||
echo " --json Output results in JSON format"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Get script directory and load common functions
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get all paths and variables from common functions
|
||||
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
||||
eval "$_paths_output"
|
||||
unset _paths_output
|
||||
|
||||
# Check if we're on a proper feature branch (only for git repos)
|
||||
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
||||
|
||||
# Ensure the feature directory exists
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
|
||||
# Copy plan template if it exists
|
||||
TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT")
|
||||
if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then
|
||||
cp "$TEMPLATE" "$IMPL_PLAN"
|
||||
echo "Copied plan template to $IMPL_PLAN"
|
||||
else
|
||||
echo "Warning: Plan template not found"
|
||||
# Create a basic plan file if template doesn't exist
|
||||
touch "$IMPL_PLAN"
|
||||
fi
|
||||
|
||||
# Output results
|
||||
if $JSON_MODE; then
|
||||
if has_jq; then
|
||||
jq -cn \
|
||||
--arg feature_spec "$FEATURE_SPEC" \
|
||||
--arg impl_plan "$IMPL_PLAN" \
|
||||
--arg specs_dir "$FEATURE_DIR" \
|
||||
--arg branch "$CURRENT_BRANCH" \
|
||||
--arg has_git "$HAS_GIT" \
|
||||
'{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}'
|
||||
else
|
||||
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
|
||||
"$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")"
|
||||
fi
|
||||
else
|
||||
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
||||
echo "IMPL_PLAN: $IMPL_PLAN"
|
||||
echo "SPECS_DIR: $FEATURE_DIR"
|
||||
echo "BRANCH: $CURRENT_BRANCH"
|
||||
echo "HAS_GIT: $HAS_GIT"
|
||||
fi
|
||||
|
||||
+808
@@ -0,0 +1,808 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Update agent context files with information from plan.md
|
||||
#
|
||||
# This script maintains AI agent context files by parsing feature specifications
|
||||
# and updating agent-specific configuration files with project information.
|
||||
#
|
||||
# MAIN FUNCTIONS:
|
||||
# 1. Environment Validation
|
||||
# - Verifies git repository structure and branch information
|
||||
# - Checks for required plan.md files and templates
|
||||
# - Validates file permissions and accessibility
|
||||
#
|
||||
# 2. Plan Data Extraction
|
||||
# - Parses plan.md files to extract project metadata
|
||||
# - Identifies language/version, frameworks, databases, and project types
|
||||
# - Handles missing or incomplete specification data gracefully
|
||||
#
|
||||
# 3. Agent File Management
|
||||
# - Creates new agent context files from templates when needed
|
||||
# - Updates existing agent files with new project information
|
||||
# - Preserves manual additions and custom configurations
|
||||
# - Supports multiple AI agent formats and directory structures
|
||||
#
|
||||
# 4. Content Generation
|
||||
# - Generates language-specific build/test commands
|
||||
# - Creates appropriate project directory structures
|
||||
# - Updates technology stacks and recent changes sections
|
||||
# - Maintains consistent formatting and timestamps
|
||||
#
|
||||
# 5. Multi-Agent Support
|
||||
# - Handles agent-specific file paths and naming conventions
|
||||
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Antigravity or Generic
|
||||
# - Can update single agents or all existing agent files
|
||||
# - Creates default Claude file if no agent files exist
|
||||
#
|
||||
# Usage: ./update-agent-context.sh [agent_type]
|
||||
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic
|
||||
# Leave empty to update all existing agent files
|
||||
|
||||
set -e
|
||||
|
||||
# Enable strict error handling
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
#==============================================================================
|
||||
# Configuration and Global Variables
|
||||
#==============================================================================
|
||||
|
||||
# Get script directory and load common functions
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get all paths and variables from common functions
|
||||
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
||||
eval "$_paths_output"
|
||||
unset _paths_output
|
||||
|
||||
NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code
|
||||
AGENT_TYPE="${1:-}"
|
||||
|
||||
# Agent-specific file paths
|
||||
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
|
||||
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
|
||||
COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md"
|
||||
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
|
||||
QWEN_FILE="$REPO_ROOT/QWEN.md"
|
||||
AGENTS_FILE="$REPO_ROOT/AGENTS.md"
|
||||
WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
|
||||
KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md"
|
||||
AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
|
||||
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
|
||||
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
|
||||
QODER_FILE="$REPO_ROOT/QODER.md"
|
||||
# AMP, Kiro CLI, and IBM Bob all share AGENTS.md — use AGENTS_FILE to avoid
|
||||
# updating the same file multiple times.
|
||||
AMP_FILE="$AGENTS_FILE"
|
||||
SHAI_FILE="$REPO_ROOT/SHAI.md"
|
||||
TABNINE_FILE="$REPO_ROOT/TABNINE.md"
|
||||
KIRO_FILE="$AGENTS_FILE"
|
||||
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
|
||||
BOB_FILE="$AGENTS_FILE"
|
||||
VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
|
||||
KIMI_FILE="$REPO_ROOT/KIMI.md"
|
||||
|
||||
# Template file
|
||||
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
|
||||
|
||||
# Global variables for parsed plan data
|
||||
NEW_LANG=""
|
||||
NEW_FRAMEWORK=""
|
||||
NEW_DB=""
|
||||
NEW_PROJECT_TYPE=""
|
||||
|
||||
#==============================================================================
|
||||
# Utility Functions
|
||||
#==============================================================================
|
||||
|
||||
log_info() {
|
||||
echo "INFO: $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo "✓ $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "ERROR: $1" >&2
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo "WARNING: $1" >&2
|
||||
}
|
||||
|
||||
# Cleanup function for temporary files
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
# Disarm traps to prevent re-entrant loop
|
||||
trap - EXIT INT TERM
|
||||
rm -f /tmp/agent_update_*_$$
|
||||
rm -f /tmp/manual_additions_$$
|
||||
exit $exit_code
|
||||
}
|
||||
|
||||
# Set up cleanup trap
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
#==============================================================================
|
||||
# Validation Functions
|
||||
#==============================================================================
|
||||
|
||||
validate_environment() {
|
||||
# Check if we have a current branch/feature (git or non-git)
|
||||
if [[ -z "$CURRENT_BRANCH" ]]; then
|
||||
log_error "Unable to determine current feature"
|
||||
if [[ "$HAS_GIT" == "true" ]]; then
|
||||
log_info "Make sure you're on a feature branch"
|
||||
else
|
||||
log_info "Set SPECIFY_FEATURE environment variable or create a feature first"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if plan.md exists
|
||||
if [[ ! -f "$NEW_PLAN" ]]; then
|
||||
log_error "No plan.md found at $NEW_PLAN"
|
||||
log_info "Make sure you're working on a feature with a corresponding spec directory"
|
||||
if [[ "$HAS_GIT" != "true" ]]; then
|
||||
log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if template exists (needed for new files)
|
||||
if [[ ! -f "$TEMPLATE_FILE" ]]; then
|
||||
log_warning "Template file not found at $TEMPLATE_FILE"
|
||||
log_warning "Creating new agent files will fail"
|
||||
fi
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Plan Parsing Functions
|
||||
#==============================================================================
|
||||
|
||||
extract_plan_field() {
|
||||
local field_pattern="$1"
|
||||
local plan_file="$2"
|
||||
|
||||
grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \
|
||||
head -1 | \
|
||||
sed "s|^\*\*${field_pattern}\*\*: ||" | \
|
||||
sed 's/^[ \t]*//;s/[ \t]*$//' | \
|
||||
grep -v "NEEDS CLARIFICATION" | \
|
||||
grep -v "^N/A$" || echo ""
|
||||
}
|
||||
|
||||
parse_plan_data() {
|
||||
local plan_file="$1"
|
||||
|
||||
if [[ ! -f "$plan_file" ]]; then
|
||||
log_error "Plan file not found: $plan_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -r "$plan_file" ]]; then
|
||||
log_error "Plan file is not readable: $plan_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Parsing plan data from $plan_file"
|
||||
|
||||
NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file")
|
||||
NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file")
|
||||
NEW_DB=$(extract_plan_field "Storage" "$plan_file")
|
||||
NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file")
|
||||
|
||||
# Log what we found
|
||||
if [[ -n "$NEW_LANG" ]]; then
|
||||
log_info "Found language: $NEW_LANG"
|
||||
else
|
||||
log_warning "No language information found in plan"
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_FRAMEWORK" ]]; then
|
||||
log_info "Found framework: $NEW_FRAMEWORK"
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then
|
||||
log_info "Found database: $NEW_DB"
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_PROJECT_TYPE" ]]; then
|
||||
log_info "Found project type: $NEW_PROJECT_TYPE"
|
||||
fi
|
||||
}
|
||||
|
||||
format_technology_stack() {
|
||||
local lang="$1"
|
||||
local framework="$2"
|
||||
local parts=()
|
||||
|
||||
# Add non-empty parts
|
||||
[[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang")
|
||||
[[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework")
|
||||
|
||||
# Join with proper formatting
|
||||
if [[ ${#parts[@]} -eq 0 ]]; then
|
||||
echo ""
|
||||
elif [[ ${#parts[@]} -eq 1 ]]; then
|
||||
echo "${parts[0]}"
|
||||
else
|
||||
# Join multiple parts with " + "
|
||||
local result="${parts[0]}"
|
||||
for ((i=1; i<${#parts[@]}; i++)); do
|
||||
result="$result + ${parts[i]}"
|
||||
done
|
||||
echo "$result"
|
||||
fi
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Template and Content Generation Functions
|
||||
#==============================================================================
|
||||
|
||||
get_project_structure() {
|
||||
local project_type="$1"
|
||||
|
||||
if [[ "$project_type" == *"web"* ]]; then
|
||||
echo "backend/\\nfrontend/\\ntests/"
|
||||
else
|
||||
echo "src/\\ntests/"
|
||||
fi
|
||||
}
|
||||
|
||||
get_commands_for_language() {
|
||||
local lang="$1"
|
||||
|
||||
case "$lang" in
|
||||
*"Python"*)
|
||||
echo "cd src && pytest && ruff check ."
|
||||
;;
|
||||
*"Rust"*)
|
||||
echo "cargo test && cargo clippy"
|
||||
;;
|
||||
*"JavaScript"*|*"TypeScript"*)
|
||||
echo "npm test \\&\\& npm run lint"
|
||||
;;
|
||||
*)
|
||||
echo "# Add commands for $lang"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_language_conventions() {
|
||||
local lang="$1"
|
||||
echo "$lang: Follow standard conventions"
|
||||
}
|
||||
|
||||
create_new_agent_file() {
|
||||
local target_file="$1"
|
||||
local temp_file="$2"
|
||||
local project_name="$3"
|
||||
local current_date="$4"
|
||||
|
||||
if [[ ! -f "$TEMPLATE_FILE" ]]; then
|
||||
log_error "Template not found at $TEMPLATE_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -r "$TEMPLATE_FILE" ]]; then
|
||||
log_error "Template file is not readable: $TEMPLATE_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Creating new agent context file from template..."
|
||||
|
||||
if ! cp "$TEMPLATE_FILE" "$temp_file"; then
|
||||
log_error "Failed to copy template file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Replace template placeholders
|
||||
local project_structure
|
||||
project_structure=$(get_project_structure "$NEW_PROJECT_TYPE")
|
||||
|
||||
local commands
|
||||
commands=$(get_commands_for_language "$NEW_LANG")
|
||||
|
||||
local language_conventions
|
||||
language_conventions=$(get_language_conventions "$NEW_LANG")
|
||||
|
||||
# Perform substitutions with error checking using safer approach
|
||||
# Escape special characters for sed by using a different delimiter or escaping
|
||||
local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g')
|
||||
local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g')
|
||||
local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g')
|
||||
|
||||
# Build technology stack and recent change strings conditionally
|
||||
local tech_stack
|
||||
if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then
|
||||
tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)"
|
||||
elif [[ -n "$escaped_lang" ]]; then
|
||||
tech_stack="- $escaped_lang ($escaped_branch)"
|
||||
elif [[ -n "$escaped_framework" ]]; then
|
||||
tech_stack="- $escaped_framework ($escaped_branch)"
|
||||
else
|
||||
tech_stack="- ($escaped_branch)"
|
||||
fi
|
||||
|
||||
local recent_change
|
||||
if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then
|
||||
recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework"
|
||||
elif [[ -n "$escaped_lang" ]]; then
|
||||
recent_change="- $escaped_branch: Added $escaped_lang"
|
||||
elif [[ -n "$escaped_framework" ]]; then
|
||||
recent_change="- $escaped_branch: Added $escaped_framework"
|
||||
else
|
||||
recent_change="- $escaped_branch: Added"
|
||||
fi
|
||||
|
||||
local substitutions=(
|
||||
"s|\[PROJECT NAME\]|$project_name|"
|
||||
"s|\[DATE\]|$current_date|"
|
||||
"s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|"
|
||||
"s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g"
|
||||
"s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|"
|
||||
"s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|"
|
||||
"s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|"
|
||||
)
|
||||
|
||||
for substitution in "${substitutions[@]}"; do
|
||||
if ! sed -i.bak -e "$substitution" "$temp_file"; then
|
||||
log_error "Failed to perform substitution: $substitution"
|
||||
rm -f "$temp_file" "$temp_file.bak"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Convert \n sequences to actual newlines
|
||||
newline=$(printf '\n')
|
||||
sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"
|
||||
|
||||
# Clean up backup files
|
||||
rm -f "$temp_file.bak" "$temp_file.bak2"
|
||||
|
||||
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
|
||||
if [[ "$target_file" == *.mdc ]]; then
|
||||
local frontmatter_file
|
||||
frontmatter_file=$(mktemp) || return 1
|
||||
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
|
||||
cat "$temp_file" >> "$frontmatter_file"
|
||||
mv "$frontmatter_file" "$temp_file"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
update_existing_agent_file() {
|
||||
local target_file="$1"
|
||||
local current_date="$2"
|
||||
|
||||
log_info "Updating existing agent context file..."
|
||||
|
||||
# Use a single temporary file for atomic update
|
||||
local temp_file
|
||||
temp_file=$(mktemp) || {
|
||||
log_error "Failed to create temporary file"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Process the file in one pass
|
||||
local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK")
|
||||
local new_tech_entries=()
|
||||
local new_change_entry=""
|
||||
|
||||
# Prepare new technology entries
|
||||
if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then
|
||||
new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)")
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then
|
||||
new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)")
|
||||
fi
|
||||
|
||||
# Prepare new change entry
|
||||
if [[ -n "$tech_stack" ]]; then
|
||||
new_change_entry="- $CURRENT_BRANCH: Added $tech_stack"
|
||||
elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then
|
||||
new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB"
|
||||
fi
|
||||
|
||||
# Check if sections exist in the file
|
||||
local has_active_technologies=0
|
||||
local has_recent_changes=0
|
||||
|
||||
if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then
|
||||
has_active_technologies=1
|
||||
fi
|
||||
|
||||
if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then
|
||||
has_recent_changes=1
|
||||
fi
|
||||
|
||||
# Process file line by line
|
||||
local in_tech_section=false
|
||||
local in_changes_section=false
|
||||
local tech_entries_added=false
|
||||
local changes_entries_added=false
|
||||
local existing_changes_count=0
|
||||
local file_ended=false
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
# Handle Active Technologies section
|
||||
if [[ "$line" == "## Active Technologies" ]]; then
|
||||
echo "$line" >> "$temp_file"
|
||||
in_tech_section=true
|
||||
continue
|
||||
elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then
|
||||
# Add new tech entries before closing the section
|
||||
if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
|
||||
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
|
||||
tech_entries_added=true
|
||||
fi
|
||||
echo "$line" >> "$temp_file"
|
||||
in_tech_section=false
|
||||
continue
|
||||
elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then
|
||||
# Add new tech entries before empty line in tech section
|
||||
if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
|
||||
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
|
||||
tech_entries_added=true
|
||||
fi
|
||||
echo "$line" >> "$temp_file"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Handle Recent Changes section
|
||||
if [[ "$line" == "## Recent Changes" ]]; then
|
||||
echo "$line" >> "$temp_file"
|
||||
# Add new change entry right after the heading
|
||||
if [[ -n "$new_change_entry" ]]; then
|
||||
echo "$new_change_entry" >> "$temp_file"
|
||||
fi
|
||||
in_changes_section=true
|
||||
changes_entries_added=true
|
||||
continue
|
||||
elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then
|
||||
echo "$line" >> "$temp_file"
|
||||
in_changes_section=false
|
||||
continue
|
||||
elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then
|
||||
# Keep only first 2 existing changes
|
||||
if [[ $existing_changes_count -lt 2 ]]; then
|
||||
echo "$line" >> "$temp_file"
|
||||
((existing_changes_count++))
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
# Update timestamp
|
||||
if [[ "$line" =~ (\*\*)?Last\ updated(\*\*)?:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then
|
||||
echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file"
|
||||
else
|
||||
echo "$line" >> "$temp_file"
|
||||
fi
|
||||
done < "$target_file"
|
||||
|
||||
# Post-loop check: if we're still in the Active Technologies section and haven't added new entries
|
||||
if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
|
||||
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
|
||||
tech_entries_added=true
|
||||
fi
|
||||
|
||||
# If sections don't exist, add them at the end of the file
|
||||
if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
|
||||
echo "" >> "$temp_file"
|
||||
echo "## Active Technologies" >> "$temp_file"
|
||||
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
|
||||
tech_entries_added=true
|
||||
fi
|
||||
|
||||
if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then
|
||||
echo "" >> "$temp_file"
|
||||
echo "## Recent Changes" >> "$temp_file"
|
||||
echo "$new_change_entry" >> "$temp_file"
|
||||
changes_entries_added=true
|
||||
fi
|
||||
|
||||
# Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
|
||||
if [[ "$target_file" == *.mdc ]]; then
|
||||
if ! head -1 "$temp_file" | grep -q '^---'; then
|
||||
local frontmatter_file
|
||||
frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; }
|
||||
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
|
||||
cat "$temp_file" >> "$frontmatter_file"
|
||||
mv "$frontmatter_file" "$temp_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Move temp file to target atomically
|
||||
if ! mv "$temp_file" "$target_file"; then
|
||||
log_error "Failed to update target file"
|
||||
rm -f "$temp_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
#==============================================================================
|
||||
# Main Agent File Update Function
|
||||
#==============================================================================
|
||||
|
||||
update_agent_file() {
|
||||
local target_file="$1"
|
||||
local agent_name="$2"
|
||||
|
||||
if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then
|
||||
log_error "update_agent_file requires target_file and agent_name parameters"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Updating $agent_name context file: $target_file"
|
||||
|
||||
local project_name
|
||||
project_name=$(basename "$REPO_ROOT")
|
||||
local current_date
|
||||
current_date=$(date +%Y-%m-%d)
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
local target_dir
|
||||
target_dir=$(dirname "$target_file")
|
||||
if [[ ! -d "$target_dir" ]]; then
|
||||
if ! mkdir -p "$target_dir"; then
|
||||
log_error "Failed to create directory: $target_dir"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "$target_file" ]]; then
|
||||
# Create new file from template
|
||||
local temp_file
|
||||
temp_file=$(mktemp) || {
|
||||
log_error "Failed to create temporary file"
|
||||
return 1
|
||||
}
|
||||
|
||||
if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then
|
||||
if mv "$temp_file" "$target_file"; then
|
||||
log_success "Created new $agent_name context file"
|
||||
else
|
||||
log_error "Failed to move temporary file to $target_file"
|
||||
rm -f "$temp_file"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_error "Failed to create new agent file"
|
||||
rm -f "$temp_file"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
# Update existing file
|
||||
if [[ ! -r "$target_file" ]]; then
|
||||
log_error "Cannot read existing file: $target_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -w "$target_file" ]]; then
|
||||
log_error "Cannot write to existing file: $target_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if update_existing_agent_file "$target_file" "$current_date"; then
|
||||
log_success "Updated existing $agent_name context file"
|
||||
else
|
||||
log_error "Failed to update existing agent file"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Agent Selection and Processing
|
||||
#==============================================================================
|
||||
|
||||
update_specific_agent() {
|
||||
local agent_type="$1"
|
||||
|
||||
case "$agent_type" in
|
||||
claude)
|
||||
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
|
||||
;;
|
||||
gemini)
|
||||
update_agent_file "$GEMINI_FILE" "Gemini CLI" || return 1
|
||||
;;
|
||||
copilot)
|
||||
update_agent_file "$COPILOT_FILE" "GitHub Copilot" || return 1
|
||||
;;
|
||||
cursor-agent)
|
||||
update_agent_file "$CURSOR_FILE" "Cursor IDE" || return 1
|
||||
;;
|
||||
qwen)
|
||||
update_agent_file "$QWEN_FILE" "Qwen Code" || return 1
|
||||
;;
|
||||
opencode)
|
||||
update_agent_file "$AGENTS_FILE" "opencode" || return 1
|
||||
;;
|
||||
codex)
|
||||
update_agent_file "$AGENTS_FILE" "Codex CLI" || return 1
|
||||
;;
|
||||
windsurf)
|
||||
update_agent_file "$WINDSURF_FILE" "Windsurf" || return 1
|
||||
;;
|
||||
kilocode)
|
||||
update_agent_file "$KILOCODE_FILE" "Kilo Code" || return 1
|
||||
;;
|
||||
auggie)
|
||||
update_agent_file "$AUGGIE_FILE" "Auggie CLI" || return 1
|
||||
;;
|
||||
roo)
|
||||
update_agent_file "$ROO_FILE" "Roo Code" || return 1
|
||||
;;
|
||||
codebuddy)
|
||||
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" || return 1
|
||||
;;
|
||||
qodercli)
|
||||
update_agent_file "$QODER_FILE" "Qoder CLI" || return 1
|
||||
;;
|
||||
amp)
|
||||
update_agent_file "$AMP_FILE" "Amp" || return 1
|
||||
;;
|
||||
shai)
|
||||
update_agent_file "$SHAI_FILE" "SHAI" || return 1
|
||||
;;
|
||||
tabnine)
|
||||
update_agent_file "$TABNINE_FILE" "Tabnine CLI" || return 1
|
||||
;;
|
||||
kiro-cli)
|
||||
update_agent_file "$KIRO_FILE" "Kiro CLI" || return 1
|
||||
;;
|
||||
agy)
|
||||
update_agent_file "$AGY_FILE" "Antigravity" || return 1
|
||||
;;
|
||||
bob)
|
||||
update_agent_file "$BOB_FILE" "IBM Bob" || return 1
|
||||
;;
|
||||
vibe)
|
||||
update_agent_file "$VIBE_FILE" "Mistral Vibe" || return 1
|
||||
;;
|
||||
kimi)
|
||||
update_agent_file "$KIMI_FILE" "Kimi Code" || return 1
|
||||
;;
|
||||
generic)
|
||||
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown agent type '$agent_type'"
|
||||
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
update_all_existing_agents() {
|
||||
local found_agent=false
|
||||
local _updated_paths=()
|
||||
|
||||
# Helper: skip non-existent files and files already updated (dedup by
|
||||
# realpath so that variables pointing to the same file — e.g. AMP_FILE,
|
||||
# KIRO_FILE, BOB_FILE all resolving to AGENTS_FILE — are only written once).
|
||||
# Uses a linear array instead of associative array for bash 3.2 compatibility.
|
||||
update_if_new() {
|
||||
local file="$1" name="$2"
|
||||
[[ -f "$file" ]] || return 0
|
||||
local real_path
|
||||
real_path=$(realpath "$file" 2>/dev/null || echo "$file")
|
||||
local p
|
||||
if [[ ${#_updated_paths[@]} -gt 0 ]]; then
|
||||
for p in "${_updated_paths[@]}"; do
|
||||
[[ "$p" == "$real_path" ]] && return 0
|
||||
done
|
||||
fi
|
||||
update_agent_file "$file" "$name" || return 1
|
||||
_updated_paths+=("$real_path")
|
||||
found_agent=true
|
||||
}
|
||||
|
||||
update_if_new "$CLAUDE_FILE" "Claude Code"
|
||||
update_if_new "$GEMINI_FILE" "Gemini CLI"
|
||||
update_if_new "$COPILOT_FILE" "GitHub Copilot"
|
||||
update_if_new "$CURSOR_FILE" "Cursor IDE"
|
||||
update_if_new "$QWEN_FILE" "Qwen Code"
|
||||
update_if_new "$AGENTS_FILE" "Codex/opencode"
|
||||
update_if_new "$AMP_FILE" "Amp"
|
||||
update_if_new "$KIRO_FILE" "Kiro CLI"
|
||||
update_if_new "$BOB_FILE" "IBM Bob"
|
||||
update_if_new "$WINDSURF_FILE" "Windsurf"
|
||||
update_if_new "$KILOCODE_FILE" "Kilo Code"
|
||||
update_if_new "$AUGGIE_FILE" "Auggie CLI"
|
||||
update_if_new "$ROO_FILE" "Roo Code"
|
||||
update_if_new "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
||||
update_if_new "$SHAI_FILE" "SHAI"
|
||||
update_if_new "$TABNINE_FILE" "Tabnine CLI"
|
||||
update_if_new "$QODER_FILE" "Qoder CLI"
|
||||
update_if_new "$AGY_FILE" "Antigravity"
|
||||
update_if_new "$VIBE_FILE" "Mistral Vibe"
|
||||
update_if_new "$KIMI_FILE" "Kimi Code"
|
||||
|
||||
# If no agent files exist, create a default Claude file
|
||||
if [[ "$found_agent" == false ]]; then
|
||||
log_info "No existing agent files found, creating default Claude file..."
|
||||
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
|
||||
fi
|
||||
}
|
||||
print_summary() {
|
||||
echo
|
||||
log_info "Summary of changes:"
|
||||
|
||||
if [[ -n "$NEW_LANG" ]]; then
|
||||
echo " - Added language: $NEW_LANG"
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_FRAMEWORK" ]]; then
|
||||
echo " - Added framework: $NEW_FRAMEWORK"
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then
|
||||
echo " - Added database: $NEW_DB"
|
||||
fi
|
||||
|
||||
echo
|
||||
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic]"
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Main Execution
|
||||
#==============================================================================
|
||||
|
||||
main() {
|
||||
# Validate environment before proceeding
|
||||
validate_environment
|
||||
|
||||
log_info "=== Updating agent context files for feature $CURRENT_BRANCH ==="
|
||||
|
||||
# Parse the plan file to extract project information
|
||||
if ! parse_plan_data "$NEW_PLAN"; then
|
||||
log_error "Failed to parse plan data"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Process based on agent type argument
|
||||
local success=true
|
||||
|
||||
if [[ -z "$AGENT_TYPE" ]]; then
|
||||
# No specific agent provided - update all existing agent files
|
||||
log_info "No agent specified, updating all existing agent files..."
|
||||
if ! update_all_existing_agents; then
|
||||
success=false
|
||||
fi
|
||||
else
|
||||
# Specific agent provided - update only that agent
|
||||
log_info "Updating specific agent: $AGENT_TYPE"
|
||||
if ! update_specific_agent "$AGENT_TYPE"; then
|
||||
success=false
|
||||
fi
|
||||
fi
|
||||
|
||||
# Print summary
|
||||
print_summary
|
||||
|
||||
if [[ "$success" == true ]]; then
|
||||
log_success "Agent context update completed successfully"
|
||||
exit 0
|
||||
else
|
||||
log_error "Agent context update completed with errors"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Execute main function if script is run directly
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
@@ -0,0 +1,28 @@
|
||||
# [PROJECT NAME] Development Guidelines
|
||||
|
||||
Auto-generated from all feature plans. Last updated: [DATE]
|
||||
|
||||
## Active Technologies
|
||||
|
||||
[EXTRACTED FROM ALL PLAN.MD FILES]
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
[ACTUAL STRUCTURE FROM PLANS]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]
|
||||
|
||||
## Code Style
|
||||
|
||||
[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]
|
||||
|
||||
## Recent Changes
|
||||
|
||||
[LAST 3 FEATURES AND WHAT THEY ADDED]
|
||||
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
<!-- MANUAL ADDITIONS END -->
|
||||
@@ -0,0 +1,40 @@
|
||||
# [CHECKLIST TYPE] Checklist: [FEATURE NAME]
|
||||
|
||||
**Purpose**: [Brief description of what this checklist covers]
|
||||
**Created**: [DATE]
|
||||
**Feature**: [Link to spec.md or relevant documentation]
|
||||
|
||||
**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements.
|
||||
|
||||
<!--
|
||||
============================================================================
|
||||
IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
|
||||
|
||||
The /speckit.checklist command MUST replace these with actual items based on:
|
||||
- User's specific checklist request
|
||||
- Feature requirements from spec.md
|
||||
- Technical context from plan.md
|
||||
- Implementation details from tasks.md
|
||||
|
||||
DO NOT keep these sample items in the generated checklist file.
|
||||
============================================================================
|
||||
-->
|
||||
|
||||
## [Category 1]
|
||||
|
||||
- [ ] CHK001 First checklist item with clear action
|
||||
- [ ] CHK002 Second checklist item
|
||||
- [ ] CHK003 Third checklist item
|
||||
|
||||
## [Category 2]
|
||||
|
||||
- [ ] CHK004 Another category item
|
||||
- [ ] CHK005 Item with specific criteria
|
||||
- [ ] CHK006 Final item in this category
|
||||
|
||||
## Notes
|
||||
|
||||
- Check items off as completed: `[x]`
|
||||
- Add comments or findings inline
|
||||
- Link to relevant resources or documentation
|
||||
- Items are numbered sequentially for easy reference
|
||||
@@ -0,0 +1,50 @@
|
||||
# [PROJECT_NAME] Constitution
|
||||
<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
|
||||
|
||||
## Core Principles
|
||||
|
||||
### [PRINCIPLE_1_NAME]
|
||||
<!-- Example: I. Library-First -->
|
||||
[PRINCIPLE_1_DESCRIPTION]
|
||||
<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
|
||||
|
||||
### [PRINCIPLE_2_NAME]
|
||||
<!-- Example: II. CLI Interface -->
|
||||
[PRINCIPLE_2_DESCRIPTION]
|
||||
<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
|
||||
|
||||
### [PRINCIPLE_3_NAME]
|
||||
<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
|
||||
[PRINCIPLE_3_DESCRIPTION]
|
||||
<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
|
||||
|
||||
### [PRINCIPLE_4_NAME]
|
||||
<!-- Example: IV. Integration Testing -->
|
||||
[PRINCIPLE_4_DESCRIPTION]
|
||||
<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
|
||||
|
||||
### [PRINCIPLE_5_NAME]
|
||||
<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
|
||||
[PRINCIPLE_5_DESCRIPTION]
|
||||
<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
|
||||
|
||||
## [SECTION_2_NAME]
|
||||
<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
|
||||
|
||||
[SECTION_2_CONTENT]
|
||||
<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
|
||||
|
||||
## [SECTION_3_NAME]
|
||||
<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
|
||||
|
||||
[SECTION_3_CONTENT]
|
||||
<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
|
||||
|
||||
## Governance
|
||||
<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
|
||||
|
||||
[GOVERNANCE_RULES]
|
||||
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
|
||||
|
||||
**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
|
||||
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
|
||||
@@ -0,0 +1,104 @@
|
||||
# Implementation Plan: [FEATURE]
|
||||
|
||||
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
||||
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
|
||||
|
||||
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
|
||||
|
||||
## Summary
|
||||
|
||||
[Extract from feature spec: primary requirement + technical approach from research]
|
||||
|
||||
## Technical Context
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: Replace the content in this section with the technical details
|
||||
for the project. The structure here is presented in advisory capacity to guide
|
||||
the iteration process.
|
||||
-->
|
||||
|
||||
**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
|
||||
**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
|
||||
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
|
||||
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
|
||||
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
|
||||
**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
|
||||
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
|
||||
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
|
||||
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
[Gates determined based on constitution file]
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/[###-feature]/
|
||||
├── plan.md # This file (/speckit.plan command output)
|
||||
├── research.md # Phase 0 output (/speckit.plan command)
|
||||
├── data-model.md # Phase 1 output (/speckit.plan command)
|
||||
├── quickstart.md # Phase 1 output (/speckit.plan command)
|
||||
├── contracts/ # Phase 1 output (/speckit.plan command)
|
||||
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
<!--
|
||||
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
|
||||
for this feature. Delete unused options and expand the chosen structure with
|
||||
real paths (e.g., apps/admin, packages/something). The delivered plan must
|
||||
not include Option labels.
|
||||
-->
|
||||
|
||||
```text
|
||||
# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
|
||||
src/
|
||||
├── models/
|
||||
├── services/
|
||||
├── cli/
|
||||
└── lib/
|
||||
|
||||
tests/
|
||||
├── contract/
|
||||
├── integration/
|
||||
└── unit/
|
||||
|
||||
# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── models/
|
||||
│ ├── services/
|
||||
│ └── api/
|
||||
└── tests/
|
||||
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ ├── pages/
|
||||
│ └── services/
|
||||
└── tests/
|
||||
|
||||
# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
|
||||
api/
|
||||
└── [same as backend above]
|
||||
|
||||
ios/ or android/
|
||||
└── [platform-specific structure: feature modules, UI flows, platform tests]
|
||||
```
|
||||
|
||||
**Structure Decision**: [Document the selected structure and reference the real
|
||||
directories captured above]
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
> **Fill ONLY if Constitution Check has violations that must be justified**
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
|
||||
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
|
||||
@@ -0,0 +1,115 @@
|
||||
# Feature Specification: [FEATURE NAME]
|
||||
|
||||
**Feature Branch**: `[###-feature-name]`
|
||||
**Created**: [DATE]
|
||||
**Status**: Draft
|
||||
**Input**: User description: "$ARGUMENTS"
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
<!--
|
||||
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
|
||||
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
|
||||
you should still have a viable MVP (Minimum Viable Product) that delivers value.
|
||||
|
||||
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
|
||||
Think of each story as a standalone slice of functionality that can be:
|
||||
- Developed independently
|
||||
- Tested independently
|
||||
- Deployed independently
|
||||
- Demonstrated to users independently
|
||||
-->
|
||||
|
||||
### User Story 1 - [Brief Title] (Priority: P1)
|
||||
|
||||
[Describe this user journey in plain language]
|
||||
|
||||
**Why this priority**: [Explain the value and why it has this priority level]
|
||||
|
||||
**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"]
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
2. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - [Brief Title] (Priority: P2)
|
||||
|
||||
[Describe this user journey in plain language]
|
||||
|
||||
**Why this priority**: [Explain the value and why it has this priority level]
|
||||
|
||||
**Independent Test**: [Describe how this can be tested independently]
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - [Brief Title] (Priority: P3)
|
||||
|
||||
[Describe this user journey in plain language]
|
||||
|
||||
**Why this priority**: [Explain the value and why it has this priority level]
|
||||
|
||||
**Independent Test**: [Describe how this can be tested independently]
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
|
||||
---
|
||||
|
||||
[Add more user stories as needed, each with an assigned priority]
|
||||
|
||||
### Edge Cases
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: The content in this section represents placeholders.
|
||||
Fill them out with the right edge cases.
|
||||
-->
|
||||
|
||||
- What happens when [boundary condition]?
|
||||
- How does system handle [error scenario]?
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: The content in this section represents placeholders.
|
||||
Fill them out with the right functional requirements.
|
||||
-->
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
|
||||
- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
|
||||
- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
|
||||
- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
|
||||
- **FR-005**: System MUST [behavior, e.g., "log all security events"]
|
||||
|
||||
*Example of marking unclear requirements:*
|
||||
|
||||
- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
|
||||
- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **[Entity 1]**: [What it represents, key attributes without implementation]
|
||||
- **[Entity 2]**: [What it represents, relationships to other entities]
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: Define measurable success criteria.
|
||||
These must be technology-agnostic and measurable.
|
||||
-->
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"]
|
||||
- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"]
|
||||
- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"]
|
||||
- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"]
|
||||
@@ -0,0 +1,251 @@
|
||||
---
|
||||
|
||||
description: "Task list template for feature implementation"
|
||||
---
|
||||
|
||||
# Tasks: [FEATURE NAME]
|
||||
|
||||
**Input**: Design documents from `/specs/[###-feature-name]/`
|
||||
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
|
||||
|
||||
**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification.
|
||||
|
||||
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
||||
|
||||
## Format: `[ID] [P?] [Story] Description`
|
||||
|
||||
- **[P]**: Can run in parallel (different files, no dependencies)
|
||||
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
|
||||
- Include exact file paths in descriptions
|
||||
|
||||
## Path Conventions
|
||||
|
||||
- **Single project**: `src/`, `tests/` at repository root
|
||||
- **Web app**: `backend/src/`, `frontend/src/`
|
||||
- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
|
||||
- Paths shown below assume single project - adjust based on plan.md structure
|
||||
|
||||
<!--
|
||||
============================================================================
|
||||
IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
|
||||
|
||||
The /speckit.tasks command MUST replace these with actual tasks based on:
|
||||
- User stories from spec.md (with their priorities P1, P2, P3...)
|
||||
- Feature requirements from plan.md
|
||||
- Entities from data-model.md
|
||||
- Endpoints from contracts/
|
||||
|
||||
Tasks MUST be organized by user story so each story can be:
|
||||
- Implemented independently
|
||||
- Tested independently
|
||||
- Delivered as an MVP increment
|
||||
|
||||
DO NOT keep these sample tasks in the generated tasks.md file.
|
||||
============================================================================
|
||||
-->
|
||||
|
||||
## Phase 1: Setup (Shared Infrastructure)
|
||||
|
||||
**Purpose**: Project initialization and basic structure
|
||||
|
||||
- [ ] T001 Create project structure per implementation plan
|
||||
- [ ] T002 Initialize [language] project with [framework] dependencies
|
||||
- [ ] T003 [P] Configure linting and formatting tools
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
|
||||
|
||||
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
|
||||
|
||||
Examples of foundational tasks (adjust based on your project):
|
||||
|
||||
- [ ] T004 Setup database schema and migrations framework
|
||||
- [ ] T005 [P] Implement authentication/authorization framework
|
||||
- [ ] T006 [P] Setup API routing and middleware structure
|
||||
- [ ] T007 Create base models/entities that all stories depend on
|
||||
- [ ] T008 Configure error handling and logging infrastructure
|
||||
- [ ] T009 Setup environment configuration management
|
||||
|
||||
**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: [Brief description of what this story delivers]
|
||||
|
||||
**Independent Test**: [How to verify this story works on its own]
|
||||
|
||||
### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️
|
||||
|
||||
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
|
||||
|
||||
- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py
|
||||
- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py
|
||||
- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py
|
||||
- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)
|
||||
- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py
|
||||
- [ ] T016 [US1] Add validation and error handling
|
||||
- [ ] T017 [US1] Add logging for user story 1 operations
|
||||
|
||||
**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - [Title] (Priority: P2)
|
||||
|
||||
**Goal**: [Brief description of what this story delivers]
|
||||
|
||||
**Independent Test**: [How to verify this story works on its own]
|
||||
|
||||
### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
|
||||
|
||||
- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py
|
||||
- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py
|
||||
- [ ] T021 [US2] Implement [Service] in src/services/[service].py
|
||||
- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
|
||||
- [ ] T023 [US2] Integrate with User Story 1 components (if needed)
|
||||
|
||||
**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - [Title] (Priority: P3)
|
||||
|
||||
**Goal**: [Brief description of what this story delivers]
|
||||
|
||||
**Independent Test**: [How to verify this story works on its own]
|
||||
|
||||
### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
|
||||
|
||||
- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py
|
||||
- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py
|
||||
- [ ] T027 [US3] Implement [Service] in src/services/[service].py
|
||||
- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py
|
||||
|
||||
**Checkpoint**: All user stories should now be independently functional
|
||||
|
||||
---
|
||||
|
||||
[Add more user story phases as needed, following the same pattern]
|
||||
|
||||
---
|
||||
|
||||
## Phase N: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Improvements that affect multiple user stories
|
||||
|
||||
- [ ] TXXX [P] Documentation updates in docs/
|
||||
- [ ] TXXX Code cleanup and refactoring
|
||||
- [ ] TXXX Performance optimization across all stories
|
||||
- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
|
||||
- [ ] TXXX Security hardening
|
||||
- [ ] TXXX Run quickstart.md validation
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Setup (Phase 1)**: No dependencies - can start immediately
|
||||
- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
|
||||
- **User Stories (Phase 3+)**: All depend on Foundational phase completion
|
||||
- User stories can then proceed in parallel (if staffed)
|
||||
- Or sequentially in priority order (P1 → P2 → P3)
|
||||
- **Polish (Final Phase)**: Depends on all desired user stories being complete
|
||||
|
||||
### User Story Dependencies
|
||||
|
||||
- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
|
||||
- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable
|
||||
- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable
|
||||
|
||||
### Within Each User Story
|
||||
|
||||
- Tests (if included) MUST be written and FAIL before implementation
|
||||
- Models before services
|
||||
- Services before endpoints
|
||||
- Core implementation before integration
|
||||
- Story complete before moving to next priority
|
||||
|
||||
### Parallel Opportunities
|
||||
|
||||
- All Setup tasks marked [P] can run in parallel
|
||||
- All Foundational tasks marked [P] can run in parallel (within Phase 2)
|
||||
- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows)
|
||||
- All tests for a user story marked [P] can run in parallel
|
||||
- Models within a story marked [P] can run in parallel
|
||||
- Different user stories can be worked on in parallel by different team members
|
||||
|
||||
---
|
||||
|
||||
## Parallel Example: User Story 1
|
||||
|
||||
```bash
|
||||
# Launch all tests for User Story 1 together (if tests requested):
|
||||
Task: "Contract test for [endpoint] in tests/contract/test_[name].py"
|
||||
Task: "Integration test for [user journey] in tests/integration/test_[name].py"
|
||||
|
||||
# Launch all models for User Story 1 together:
|
||||
Task: "Create [Entity1] model in src/models/[entity1].py"
|
||||
Task: "Create [Entity2] model in src/models/[entity2].py"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP First (User Story 1 Only)
|
||||
|
||||
1. Complete Phase 1: Setup
|
||||
2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
|
||||
3. Complete Phase 3: User Story 1
|
||||
4. **STOP and VALIDATE**: Test User Story 1 independently
|
||||
5. Deploy/demo if ready
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Complete Setup + Foundational → Foundation ready
|
||||
2. Add User Story 1 → Test independently → Deploy/Demo (MVP!)
|
||||
3. Add User Story 2 → Test independently → Deploy/Demo
|
||||
4. Add User Story 3 → Test independently → Deploy/Demo
|
||||
5. Each story adds value without breaking previous stories
|
||||
|
||||
### Parallel Team Strategy
|
||||
|
||||
With multiple developers:
|
||||
|
||||
1. Team completes Setup + Foundational together
|
||||
2. Once Foundational is done:
|
||||
- Developer A: User Story 1
|
||||
- Developer B: User Story 2
|
||||
- Developer C: User Story 3
|
||||
3. Stories complete and integrate independently
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- [P] tasks = different files, no dependencies
|
||||
- [Story] label maps task to specific user story for traceability
|
||||
- Each user story should be independently completable and testable
|
||||
- Verify tests fail before implementing
|
||||
- Commit after each task or logical group
|
||||
- Stop at any checkpoint to validate story independently
|
||||
- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence
|
||||
Reference in New Issue
Block a user