mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
leadtech-bmad-mcp: close lot 3 rollout and gates config
This commit is contained in:
@@ -60,6 +60,18 @@ Documents de référence phase 1 :
|
|||||||
- `95_a_capitaliser.md`
|
- `95_a_capitaliser.md`
|
||||||
- `CLAUDE.md` projet
|
- `CLAUDE.md` projet
|
||||||
|
|
||||||
|
## Gates configurables
|
||||||
|
|
||||||
|
Les gates textuelles de `validate_plan()` et `validate_patch()` sont maintenant configurees dans :
|
||||||
|
|
||||||
|
- `config/gates.yaml`
|
||||||
|
|
||||||
|
Le serveur charge ce fichier au demarrage logique et peut etre pointe vers un autre chemin via :
|
||||||
|
|
||||||
|
- `LEADTECH_GATES_CONFIG`
|
||||||
|
|
||||||
|
Les regles purement structurelles qui ne sont pas basees sur du texte libre, comme `changed_files` avec uniquement `_bmad-output/`, restent codees a part.
|
||||||
|
|
||||||
## Integration recommandee dans BMAD
|
## Integration recommandee dans BMAD
|
||||||
|
|
||||||
1. Analyst
|
1. Analyst
|
||||||
@@ -87,6 +99,11 @@ pip install -e ".[dev]"
|
|||||||
pytest tests -q
|
pytest tests -q
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Quickstart complet :
|
||||||
|
|
||||||
|
- `docs/quickstart_dev_local.md`
|
||||||
|
- `docs/rollout_bmad_advisory.md`
|
||||||
|
|
||||||
## Rebuild de l'index local
|
## Rebuild de l'index local
|
||||||
|
|
||||||
Le MCP cherche d'abord un index JSON local a la racine de `LEADTECH_ROOT` :
|
Le MCP cherche d'abord un index JSON local a la racine de `LEADTECH_ROOT` :
|
||||||
|
|||||||
83
mcp/leadtech_bmad_mcp/config/gates.yaml
Normal file
83
mcp/leadtech_bmad_mcp/config/gates.yaml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
labels:
|
||||||
|
- "Contracts-First / Zod-Infer / No-DTO"
|
||||||
|
- "Format erreur API: { error: { code, message, requestId } }"
|
||||||
|
- "Sessions: expiresAt obligatoire et filtre en query"
|
||||||
|
- "Navigation reactive useEffect"
|
||||||
|
- "NestJS guards: AuthGuard en premier"
|
||||||
|
|
||||||
|
validate_plan:
|
||||||
|
- id: backend_contracts
|
||||||
|
domains: [backend]
|
||||||
|
required_any:
|
||||||
|
- "zod|z\\.object|contracts?[-_]first|shared[-_]contract"
|
||||||
|
actions:
|
||||||
|
- target: must_do
|
||||||
|
message: "Ajouter explicitement la strategie contracts-first / Zod."
|
||||||
|
- target: blocking_issues
|
||||||
|
message: "Plan backend sans reference aux contrats partages."
|
||||||
|
strict_only: true
|
||||||
|
|
||||||
|
- id: backend_request_id
|
||||||
|
domains: [backend]
|
||||||
|
if_any:
|
||||||
|
- "\\berrors?\\b|\\berreurs?\\b|\\bexceptions?\\b"
|
||||||
|
required_any:
|
||||||
|
- "request[_-]?id"
|
||||||
|
actions:
|
||||||
|
- target: should_do
|
||||||
|
message: "Ajouter la normalisation d'erreur API avec requestId."
|
||||||
|
|
||||||
|
- id: test_strategy
|
||||||
|
required_any:
|
||||||
|
- "test|spec"
|
||||||
|
actions:
|
||||||
|
- target: must_do
|
||||||
|
message: "Ajouter une strategie de test (unit/integration/e2e)."
|
||||||
|
|
||||||
|
- id: parallel_dependencies
|
||||||
|
if_any:
|
||||||
|
- "parallel"
|
||||||
|
required_any:
|
||||||
|
- "depends[-_]on|can[-_]run[-_]with"
|
||||||
|
actions:
|
||||||
|
- target: red_flags
|
||||||
|
message: "Parallel mentionne sans clarifier Depends-on/Can-run-with."
|
||||||
|
|
||||||
|
validate_patch:
|
||||||
|
- id: backend_session_expires_at
|
||||||
|
domains: [backend]
|
||||||
|
if_any:
|
||||||
|
- "sessions?"
|
||||||
|
required_any:
|
||||||
|
- "expires[_-]?at"
|
||||||
|
actions:
|
||||||
|
- target: blocking_issues
|
||||||
|
message: "Session modifiee sans expiresAt visible dans le diff."
|
||||||
|
|
||||||
|
- id: backend_request_id
|
||||||
|
domains: [backend]
|
||||||
|
if_any:
|
||||||
|
- "\\berrors?\\b|\\berreurs?\\b|\\bexceptions?\\b"
|
||||||
|
required_any:
|
||||||
|
- "request[_-]?id"
|
||||||
|
actions:
|
||||||
|
- target: must_do
|
||||||
|
message: "Verifier le format erreur API standard avec requestId."
|
||||||
|
|
||||||
|
- id: backend_auth_guard
|
||||||
|
domains: [backend]
|
||||||
|
if_any:
|
||||||
|
- "guard"
|
||||||
|
required_any:
|
||||||
|
- "auth[_\\s-]?guard|jwtauthguard"
|
||||||
|
actions:
|
||||||
|
- target: red_flags
|
||||||
|
message: "Usage de guard sans trace explicite d'ordre AuthGuard en premier."
|
||||||
|
|
||||||
|
- id: tests_visible_in_diff
|
||||||
|
required_any:
|
||||||
|
- "test|spec|describe\\s*\\(|it\\s*\\(|expect\\s*\\("
|
||||||
|
actions:
|
||||||
|
- target: should_do
|
||||||
|
message: "Aucun test visible dans le diff: verifier couverture manuelle."
|
||||||
|
strict_only: true
|
||||||
@@ -15,9 +15,9 @@ Mode d'usage :
|
|||||||
|
|
||||||
| Lot | Objectif | Statut |
|
| Lot | Objectif | Statut |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Lot 1 | Contrat MCP v1 + metadonnees `knowledge` + compatibilite loader | En cours avance |
|
| Lot 1 | Contrat MCP v1 + metadonnees `knowledge` + compatibilite loader | Termine |
|
||||||
| Lot 2 | Index compile local + branchement de la recherche MCP dessus | Termine |
|
| Lot 2 | Index compile local + branchement de la recherche MCP dessus | Termine |
|
||||||
| Lot 3 | Gates configurables + packaging + rollout BMAD | A faire |
|
| Lot 3 | Gates configurables + packaging + rollout BMAD | Termine |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ Stabiliser le contrat du MCP et preparer un corpus `knowledge/` assez structure
|
|||||||
|
|
||||||
- [x] Verifier si `knowledge/backend/patterns/prisma.md` doit aussi entrer dans le noyau pilote
|
- [x] Verifier si `knowledge/backend/patterns/prisma.md` doit aussi entrer dans le noyau pilote
|
||||||
- [x] Verifier si `knowledge/frontend/risques/tests.md` doit aussi entrer dans le noyau pilote
|
- [x] Verifier si `knowledge/frontend/risques/tests.md` doit aussi entrer dans le noyau pilote
|
||||||
- [ ] Faire un commit de cloture explicite du Lot 1
|
- [x] Faire un commit de cloture explicite du Lot 1
|
||||||
|
|
||||||
### Critere de fin
|
### Critere de fin
|
||||||
|
|
||||||
@@ -105,19 +105,19 @@ Sortir les regles du code dur, rendre l'installation reproductible, puis cabler
|
|||||||
|
|
||||||
### Taches
|
### Taches
|
||||||
|
|
||||||
- [ ] Extraire les gates dans une config versionnee
|
- [x] Extraire les gates dans une config versionnee
|
||||||
- [ ] Distinguer advisory et blocking dans la config
|
- [x] Distinguer advisory et blocking dans la config
|
||||||
- [ ] Ajouter des tests sur les faux positifs/faux negatifs des gates
|
- [x] Ajouter des tests sur les faux positifs/faux negatifs des gates
|
||||||
- [ ] Stabiliser l'installation `pip install -e ".[dev]"`
|
- [x] Stabiliser l'installation `pip install -e ".[dev]"`
|
||||||
- [ ] Ajouter une doc machine vierge / quickstart
|
- [x] Ajouter une doc machine vierge / quickstart
|
||||||
- [ ] Preparer un rollout BMAD advisory
|
- [x] Preparer un rollout BMAD advisory
|
||||||
- [ ] Definir 2-3 blocages stricts seulement apres validation
|
- [x] Definir 2-3 blocages stricts seulement apres validation
|
||||||
|
|
||||||
### Livrables attendus
|
### Livrables attendus
|
||||||
|
|
||||||
- `config/gates.yaml`
|
- `config/gates.yaml`
|
||||||
- quickstart dev local
|
- `docs/quickstart_dev_local.md`
|
||||||
- procedure de rollout BMAD
|
- `docs/rollout_bmad_advisory.md`
|
||||||
|
|
||||||
### Critere de fin
|
### Critere de fin
|
||||||
|
|
||||||
@@ -142,6 +142,16 @@ Sortir les regles du code dur, rendre l'installation reproductible, puis cabler
|
|||||||
- `search_knowledge()` et `search_global_docs()` relis d'abord sur l'index avec fallback scan
|
- `search_knowledge()` et `search_global_docs()` relis d'abord sur l'index avec fallback scan
|
||||||
- script `leadtech-bmad-build-index` ajoute
|
- script `leadtech-bmad-build-index` ajoute
|
||||||
- tests d'integration indexes ajoutes
|
- tests d'integration indexes ajoutes
|
||||||
|
- commit explicite de cloture Lot 1 + Lot 2: `bafc872`
|
||||||
|
- Lot 3 demarre
|
||||||
|
- gates textuelles extraites dans `config/gates.yaml`
|
||||||
|
- distinction advisory / blocking portee par la config
|
||||||
|
- chargeur `gates.py` ajoute avec override `LEADTECH_GATES_CONFIG`
|
||||||
|
- couverture de tests gates et serveur etendue
|
||||||
|
- install `pip install -e ".[dev]"` reverifiee sur venv propre
|
||||||
|
- quickstart local ajoute
|
||||||
|
- procedure de rollout BMAD advisory ajoutee
|
||||||
|
- shortlist de 3 blocages stricts recommandes documentee
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
53
mcp/leadtech_bmad_mcp/docs/quickstart_dev_local.md
Normal file
53
mcp/leadtech_bmad_mcp/docs/quickstart_dev_local.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# leadtech-bmad-mcp — Quickstart dev local
|
||||||
|
|
||||||
|
Objectif : installer et verifier rapidement le serveur MCP sur une machine vierge ou un nouvel environnement local.
|
||||||
|
|
||||||
|
## Prerequis
|
||||||
|
|
||||||
|
- Python `>= 3.10`
|
||||||
|
- acces au repo Lead_tech
|
||||||
|
- shell POSIX
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /srv/helpers/_Assistant_Lead_Tech/mcp/leadtech_bmad_mcp
|
||||||
|
python3.10 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification minimale
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
pytest tests -q
|
||||||
|
leadtech-bmad-build-index
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendus :
|
||||||
|
|
||||||
|
- tous les tests passent
|
||||||
|
- un fichier `.leadtech_mcp_index.json` est genere a la racine de `LEADTECH_ROOT`
|
||||||
|
|
||||||
|
## Lancement local
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
export LEADTECH_ROOT=/srv/helpers/_Assistant_Lead_Tech
|
||||||
|
leadtech-bmad-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
En dev local hors `/srv/helpers`, `LEADTECH_ROOT` peut pointer vers le checkout courant.
|
||||||
|
|
||||||
|
## Variables utiles
|
||||||
|
|
||||||
|
- `LEADTECH_ROOT` : racine de la base Lead_tech
|
||||||
|
- `LEADTECH_GATES_CONFIG` : chemin alternatif vers `config/gates.yaml`
|
||||||
|
- `LEADTECH_MCP_ALLOW_WRITE=1` : active les writes controles (`95_a_capitaliser.md`, memoire projet)
|
||||||
|
|
||||||
|
## Depannage
|
||||||
|
|
||||||
|
- `ModuleNotFoundError: yaml` : relancer `pip install -e ".[dev]"` pour installer `PyYAML`
|
||||||
|
- `command not found: pytest` : verifier que `.venv` est active
|
||||||
|
- index ecrit au mauvais endroit : verifier `LEADTECH_ROOT`
|
||||||
155
mcp/leadtech_bmad_mcp/docs/rollout_bmad_advisory.md
Normal file
155
mcp/leadtech_bmad_mcp/docs/rollout_bmad_advisory.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# leadtech-bmad-mcp — Rollout BMAD advisory
|
||||||
|
|
||||||
|
Ce document decrit comment brancher `leadtech-bmad-mcp` dans BMAD sans introduire de blocage automatique au demarrage.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
- injecter la guidance Lead_tech au bon moment
|
||||||
|
- rendre les gates visibles dans les artefacts BMAD
|
||||||
|
- observer les faux positifs avant d'activer des blocages stricts
|
||||||
|
|
||||||
|
## Principe
|
||||||
|
|
||||||
|
Phase de depart :
|
||||||
|
|
||||||
|
- **advisory only**
|
||||||
|
- aucune story ne s'arrete automatiquement sur la seule base du MCP
|
||||||
|
- la decision finale reste humaine
|
||||||
|
|
||||||
|
Le MCP doit d'abord servir a :
|
||||||
|
|
||||||
|
- augmenter le niveau de vigilance
|
||||||
|
- homogeniser la lecture des patterns et risques
|
||||||
|
- detecter les cas manifestement fragiles
|
||||||
|
|
||||||
|
## Points d'injection BMAD
|
||||||
|
|
||||||
|
### 1. Analyst — entree de story
|
||||||
|
|
||||||
|
Appel recommande :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
get_guidance(domain, task_type="analysis", story_text=...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Sortie a reporter dans la story :
|
||||||
|
|
||||||
|
- patterns a appliquer
|
||||||
|
- risques a eviter
|
||||||
|
- docs prioritaires a lire
|
||||||
|
|
||||||
|
### 2. Builder — avant implementation
|
||||||
|
|
||||||
|
Appel recommande :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
validate_plan(domain, plan_text, agent_role="builder", strict=false)
|
||||||
|
```
|
||||||
|
|
||||||
|
Regle advisory :
|
||||||
|
|
||||||
|
- les `blocking_issues` sont traces mais ne bloquent pas encore automatiquement
|
||||||
|
- le Builder doit repondre explicitement a chaque point dans son plan ou son Dev Agent Record
|
||||||
|
|
||||||
|
### 3. Builder — apres diff
|
||||||
|
|
||||||
|
Appel recommande :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
validate_patch(domain, diff_text, changed_files, strict=false)
|
||||||
|
```
|
||||||
|
|
||||||
|
Regle advisory :
|
||||||
|
|
||||||
|
- ajouter un resume dans le Dev Agent Record
|
||||||
|
- si `blocking_issues` apparait, le statut cible doit devenir `review`, pas `done`
|
||||||
|
|
||||||
|
### 4. Reviewer — revue finale
|
||||||
|
|
||||||
|
Appels recommandes :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
emit_checklist(agent_role="reviewer", domain, story_text)
|
||||||
|
validate_patch(domain, diff_text, changed_files, strict=false)
|
||||||
|
```
|
||||||
|
|
||||||
|
Regle advisory :
|
||||||
|
|
||||||
|
- un `blocking_issues` n'est pas un veto automatique
|
||||||
|
- mais il impose une justification explicite si la PR/story est acceptee
|
||||||
|
|
||||||
|
### 5. Curator — apprentissage
|
||||||
|
|
||||||
|
Appels recommandes :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
propose_capitalization(..., dry_run=true)
|
||||||
|
triage_capitalization()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trace minimale a imposer
|
||||||
|
|
||||||
|
Chaque story doit contenir une section courte `Leadtech MCP Gates` avec :
|
||||||
|
|
||||||
|
- timestamp
|
||||||
|
- tools appeles
|
||||||
|
- resume de `must_do`, `red_flags`, `blocking_issues`
|
||||||
|
- decision humaine prise
|
||||||
|
|
||||||
|
Format minimal :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Leadtech MCP Gates
|
||||||
|
- 2026-03-31 16:20
|
||||||
|
- get_guidance / validate_plan / validate_patch
|
||||||
|
- blocking_issues: 0
|
||||||
|
- red_flags: AuthGuard non visible dans le diff
|
||||||
|
- decision: review demandee avant done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observabilite a suivre pendant la phase advisory
|
||||||
|
|
||||||
|
Sur 10 a 20 stories, relever :
|
||||||
|
|
||||||
|
- nombre de `blocking_issues`
|
||||||
|
- nombre de faux positifs
|
||||||
|
- nombre de cas ou le MCP a evite une regression reelle
|
||||||
|
- domaines les plus stables (`backend`, `workflow`, etc.)
|
||||||
|
|
||||||
|
Objectif de sortie de phase advisory :
|
||||||
|
|
||||||
|
- faux positifs faibles sur les gates promues
|
||||||
|
- vocabulaire compris par les agents BMAD
|
||||||
|
- section `Leadtech MCP Gates` remplie de facon reguliere
|
||||||
|
|
||||||
|
## Blocages stricts recommandes apres validation
|
||||||
|
|
||||||
|
Ne promouvoir en strict que des regles avec signal fort et faible ambiguite.
|
||||||
|
|
||||||
|
### Blocage strict 1
|
||||||
|
|
||||||
|
- `validate_patch` : `Patch sans fichier source: seulement des artefacts BMAD.`
|
||||||
|
- Pourquoi : signal tres fort, directement aligne avec `story-tracking.md`
|
||||||
|
|
||||||
|
### Blocage strict 2
|
||||||
|
|
||||||
|
- `validate_patch` : `Session modifiee sans expiresAt visible dans le diff.`
|
||||||
|
- Pourquoi : gate backend precise, issue recurrente et couteuse
|
||||||
|
|
||||||
|
### Blocage strict 3
|
||||||
|
|
||||||
|
- `validate_plan` : `Plan backend sans reference aux contrats partages.`
|
||||||
|
- Pourquoi : aligne avec la regle contracts-first, utile surtout sur monorepos TS fullstack
|
||||||
|
|
||||||
|
## Ce qu'il ne faut pas promouvoir trop tot
|
||||||
|
|
||||||
|
- warning `requestId` sur texte libre si la formulation du plan/diff est trop partielle
|
||||||
|
- warning `AuthGuard` sur des extraits de diff incomplets
|
||||||
|
- suggestions de tests si la story est purement documentaire
|
||||||
|
|
||||||
|
## Sequence recommandee
|
||||||
|
|
||||||
|
1. phase advisory sur toutes les stories backend + workflow
|
||||||
|
2. promotion du blocage strict `bmad-only artifacts`
|
||||||
|
3. promotion du blocage strict `sessions sans expiresAt`
|
||||||
|
4. promotion eventuelle du blocage `contracts-first` si le contexte repo le justifie
|
||||||
@@ -9,7 +9,8 @@ description = "Serveur MCP sidecar pour Lead_tech et workflow BMAD"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mcp>=1.2.0"
|
"mcp>=1.2.0",
|
||||||
|
"PyYAML>=6.0"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
74
mcp/leadtech_bmad_mcp/src/leadtech_bmad_mcp/gates.py
Normal file
74
mcp/leadtech_bmad_mcp/src/leadtech_bmad_mcp/gates.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def get_gates_config_path() -> Path:
|
||||||
|
override = os.getenv("LEADTECH_GATES_CONFIG")
|
||||||
|
if override:
|
||||||
|
return Path(override).resolve()
|
||||||
|
return Path(__file__).resolve().parents[2] / "config" / "gates.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=4)
|
||||||
|
def load_gates_config(config_path: str | None = None) -> dict[str, Any]:
|
||||||
|
path = Path(config_path).resolve() if config_path else get_gates_config_path()
|
||||||
|
payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||||
|
return payload if isinstance(payload, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_gate_labels() -> list[str]:
|
||||||
|
config = load_gates_config()
|
||||||
|
labels = config.get("labels", [])
|
||||||
|
return [str(label) for label in labels]
|
||||||
|
|
||||||
|
|
||||||
|
def _matches_any(patterns: list[str], text: str) -> bool:
|
||||||
|
return any(re.search(pattern, text, re.IGNORECASE) for pattern in patterns)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_text_rules(tool_name: str, domain: str, text: str, strict: bool) -> list[dict[str, str]]:
|
||||||
|
config = load_gates_config()
|
||||||
|
rules = config.get(tool_name, [])
|
||||||
|
findings: list[dict[str, str]] = []
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
if not isinstance(rule, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
domains = rule.get("domains", [])
|
||||||
|
if domains and domain not in domains:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if_any = [str(pattern) for pattern in rule.get("if_any", [])]
|
||||||
|
if if_any and not _matches_any(if_any, text):
|
||||||
|
continue
|
||||||
|
|
||||||
|
unless_any = [str(pattern) for pattern in rule.get("unless_any", [])]
|
||||||
|
if unless_any and _matches_any(unless_any, text):
|
||||||
|
continue
|
||||||
|
|
||||||
|
required_any = [str(pattern) for pattern in rule.get("required_any", [])]
|
||||||
|
if required_any and _matches_any(required_any, text):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for action in rule.get("actions", []):
|
||||||
|
if not isinstance(action, dict):
|
||||||
|
continue
|
||||||
|
if action.get("strict_only") and not strict:
|
||||||
|
continue
|
||||||
|
findings.append(
|
||||||
|
{
|
||||||
|
"target": str(action["target"]),
|
||||||
|
"message": str(action["message"]),
|
||||||
|
"rule_id": str(rule.get("id", "")),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return findings
|
||||||
@@ -9,6 +9,7 @@ from typing import Literal
|
|||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
from .gates import apply_text_rules, get_gate_labels
|
||||||
from .knowledge import get_paths, read_knowledge_doc, read_text, search_knowledge, search_global_docs
|
from .knowledge import get_paths, read_knowledge_doc, read_text, search_knowledge, search_global_docs
|
||||||
from .schemas import CONFIDENCE_HIGH, CONFIDENCE_LOW, CONFIDENCE_MEDIUM, add_reference, empty_gate_output
|
from .schemas import CONFIDENCE_HIGH, CONFIDENCE_LOW, CONFIDENCE_MEDIUM, add_reference, empty_gate_output
|
||||||
from .triage import parse_capitalisation_entries, novelty_level, scope_level
|
from .triage import parse_capitalisation_entries, novelty_level, scope_level
|
||||||
@@ -18,30 +19,9 @@ TaskType = Literal["analysis", "implementation", "review", "debug", "architectur
|
|||||||
AgentRole = Literal["analyst", "builder", "reviewer", "curator"]
|
AgentRole = Literal["analyst", "builder", "reviewer", "curator"]
|
||||||
|
|
||||||
mcp = FastMCP(name="leadtech-bmad-mcp")
|
mcp = FastMCP(name="leadtech-bmad-mcp")
|
||||||
|
|
||||||
GATES = [
|
|
||||||
"Contracts-First / Zod-Infer / No-DTO",
|
|
||||||
"Format erreur API: { error: { code, message, requestId } }",
|
|
||||||
"Sessions: expiresAt obligatoire et filtre en query",
|
|
||||||
"Navigation reactive useEffect",
|
|
||||||
"NestJS guards: AuthGuard en premier",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Patterns compilés une fois — couvrent camelCase, snake_case, kebab-case et variantes courantes
|
|
||||||
_RE_CONTRACT = re.compile(r"zod|z\.object|contracts?[-_]first|shared[-_]contract", re.IGNORECASE)
|
|
||||||
_RE_REQUEST_ID = re.compile(r"request[_-]?id", re.IGNORECASE)
|
|
||||||
_RE_EXPIRES_AT = re.compile(r"expires[_-]?at", re.IGNORECASE)
|
|
||||||
_RE_AUTH_GUARD = re.compile(r"auth[_\s-]?guard|jwtauthguard", re.IGNORECASE)
|
|
||||||
_RE_PARALLEL = re.compile(r"parallel", re.IGNORECASE)
|
|
||||||
_RE_DEPENDS_ON = re.compile(r"depends[-_]on|can[-_]run[-_]with", re.IGNORECASE)
|
|
||||||
_RE_SESSION = re.compile(r"sessions?", re.IGNORECASE)
|
|
||||||
_RE_ERROR = re.compile(r"\berrors?\b|\berreurs?\b|\bexceptions?\b", re.IGNORECASE)
|
|
||||||
_RE_GUARD = re.compile(r"guard", re.IGNORECASE)
|
|
||||||
|
|
||||||
|
|
||||||
def _base_output() -> dict:
|
def _base_output() -> dict:
|
||||||
payload = empty_gate_output()
|
payload = empty_gate_output()
|
||||||
payload["gates"] = GATES
|
payload["gates"] = get_gate_labels()
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
@@ -152,19 +132,8 @@ def get_guidance(domain: Domain, task_type: TaskType, story_text: str = "", keyw
|
|||||||
def validate_plan(domain: Domain, plan_text: str, agent_role: AgentRole = "builder", strict: bool = True) -> dict:
|
def validate_plan(domain: Domain, plan_text: str, agent_role: AgentRole = "builder", strict: bool = True) -> dict:
|
||||||
out = _base_output()
|
out = _base_output()
|
||||||
|
|
||||||
if domain == "backend" and not _RE_CONTRACT.search(plan_text):
|
for finding in apply_text_rules("validate_plan", domain, plan_text, strict):
|
||||||
out["must_do"].append("Ajouter explicitement la strategie contracts-first / Zod.")
|
out[finding["target"]].append(finding["message"])
|
||||||
if strict:
|
|
||||||
out["blocking_issues"].append("Plan backend sans reference aux contrats partages.")
|
|
||||||
|
|
||||||
if domain == "backend" and not _RE_REQUEST_ID.search(plan_text) and _RE_ERROR.search(plan_text):
|
|
||||||
out["should_do"].append("Ajouter la normalisation d'erreur API avec requestId.")
|
|
||||||
|
|
||||||
if not re.search(r"test|spec", plan_text, re.IGNORECASE):
|
|
||||||
out["must_do"].append("Ajouter une strategie de test (unit/integration/e2e).")
|
|
||||||
|
|
||||||
if _RE_PARALLEL.search(plan_text) and not _RE_DEPENDS_ON.search(plan_text):
|
|
||||||
out["red_flags"].append("Parallel mentionne sans clarifier Depends-on/Can-run-with.")
|
|
||||||
|
|
||||||
add_reference(out, str(get_paths().root / "80_bmad/process_llm_et_parallelisation.md"), "Regles de synchronisation BMAD")
|
add_reference(out, str(get_paths().root / "80_bmad/process_llm_et_parallelisation.md"), "Regles de synchronisation BMAD")
|
||||||
out["confidence"] = CONFIDENCE_HIGH if not out["blocking_issues"] else CONFIDENCE_MEDIUM
|
out["confidence"] = CONFIDENCE_HIGH if not out["blocking_issues"] else CONFIDENCE_MEDIUM
|
||||||
@@ -175,13 +144,8 @@ def validate_plan(domain: Domain, plan_text: str, agent_role: AgentRole = "build
|
|||||||
def validate_patch(domain: Domain, diff_text: str, changed_files: list[str] | None = None, strict: bool = True) -> dict:
|
def validate_patch(domain: Domain, diff_text: str, changed_files: list[str] | None = None, strict: bool = True) -> dict:
|
||||||
out = _base_output()
|
out = _base_output()
|
||||||
|
|
||||||
if domain == "backend":
|
for finding in apply_text_rules("validate_patch", domain, diff_text, strict):
|
||||||
if not _RE_EXPIRES_AT.search(diff_text) and _RE_SESSION.search(diff_text):
|
out[finding["target"]].append(finding["message"])
|
||||||
out["blocking_issues"].append("Session modifiee sans expiresAt visible dans le diff.")
|
|
||||||
if not _RE_REQUEST_ID.search(diff_text) and _RE_ERROR.search(diff_text):
|
|
||||||
out["must_do"].append("Verifier le format erreur API standard avec requestId.")
|
|
||||||
if _RE_GUARD.search(diff_text) and not _RE_AUTH_GUARD.search(diff_text):
|
|
||||||
out["red_flags"].append("Usage de guard sans trace explicite d'ordre AuthGuard en premier.")
|
|
||||||
|
|
||||||
if changed_files and all("_bmad-output/" in f for f in changed_files):
|
if changed_files and all("_bmad-output/" in f for f in changed_files):
|
||||||
out["blocking_issues"].append("Patch sans fichier source: seulement des artefacts BMAD.")
|
out["blocking_issues"].append("Patch sans fichier source: seulement des artefacts BMAD.")
|
||||||
|
|||||||
36
mcp/leadtech_bmad_mcp/tests/test_gates.py
Normal file
36
mcp/leadtech_bmad_mcp/tests/test_gates.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from leadtech_bmad_mcp.gates import apply_text_rules, get_gate_labels, load_gates_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_gates_config_reads_yaml():
|
||||||
|
config = load_gates_config()
|
||||||
|
assert config["labels"]
|
||||||
|
assert config["validate_plan"]
|
||||||
|
assert config["validate_patch"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_gate_labels_returns_configured_labels():
|
||||||
|
labels = get_gate_labels()
|
||||||
|
assert "Contracts-First / Zod-Infer / No-DTO" in labels
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_text_rules_triggers_backend_contracts_blocking_in_strict_mode():
|
||||||
|
findings = apply_text_rules("validate_plan", "backend", "Plan backend sans mention de schema. Tests inclus.", strict=True)
|
||||||
|
assert any(item["target"] == "blocking_issues" for item in findings)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_text_rules_skips_backend_contracts_blocking_in_non_strict_mode():
|
||||||
|
findings = apply_text_rules("validate_plan", "backend", "Plan backend sans mention de schema. Tests inclus.", strict=False)
|
||||||
|
assert not any(item["target"] == "blocking_issues" for item in findings)
|
||||||
|
assert any(item["target"] == "must_do" for item in findings)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_text_rules_respects_required_pattern():
|
||||||
|
findings = apply_text_rules("validate_plan", "backend", "Approche contracts-first avec Zod et tests.", strict=True)
|
||||||
|
assert not any(item["rule_id"] == "backend_contracts" for item in findings)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_text_rules_skips_test_hint_when_tests_present():
|
||||||
|
findings = apply_text_rules("validate_patch", "backend", "describe('x', () => { it('works', () => {}) })", strict=True)
|
||||||
|
assert not any(item["rule_id"] == "tests_visible_in_diff" for item in findings)
|
||||||
Reference in New Issue
Block a user