mcp: consolidate lot 1 metadata and planning

This commit is contained in:
MaksTinyWorkshop
2026-03-31 15:39:20 +02:00
parent 547ffb8e6f
commit ff8eac0dfb
20 changed files with 788 additions and 8 deletions

View File

@@ -1,3 +1,14 @@
---
title: Backend — Patterns : Auth
domain: backend
bucket: patterns
tags: [auth, requestid, api-errors, sessions, tokens]
applies_to: [analysis, implementation, review, debug]
severity: high
validated_on: 2026-03-16
source_projects: [app-alexandrie]
---
# Backend — Patterns : Auth # Backend — Patterns : Auth
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet. > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.

View File

@@ -1,3 +1,14 @@
---
title: Backend — Patterns : Contracts
domain: backend
bucket: patterns
tags: [contracts, zod, api, error-codes, monorepo]
applies_to: [analysis, implementation, review, architecture]
severity: high
validated_on: 2026-03-20
source_projects: [app-alexandrie]
---
# Backend — Patterns : Contracts # Backend — Patterns : Contracts
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet. > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.

View File

@@ -1,3 +1,14 @@
---
title: Backend — Patterns : NestJS
domain: backend
bucket: patterns
tags: [nestjs, guards, auth, redis, quota]
applies_to: [analysis, implementation, review, debug]
severity: medium
validated_on: 2026-03-07
source_projects: [app-alexandrie]
---
# Backend — Patterns : NestJS # Backend — Patterns : NestJS
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet. > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.

View File

@@ -1,3 +1,14 @@
---
title: Backend — Risques & vigilance : Auth
domain: backend
bucket: risques
tags: [auth, guards, request-user, sessions, admin]
applies_to: [implementation, review, debug]
severity: high
validated_on: 2026-03-24
source_projects: [app-alexandrie]
---
# Backend — Risques & vigilance : Auth # Backend — Risques & vigilance : Auth
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet. > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.

View File

@@ -1,3 +1,14 @@
---
title: Backend — Risques & vigilance : Contracts
domain: backend
bucket: risques
tags: [contracts, zod, validation, error-codes, requestid]
applies_to: [analysis, implementation, review, debug]
severity: high
validated_on: 2026-03-24
source_projects: [app-alexandrie]
---
# Backend — Risques & vigilance : Contracts # Backend — Risques & vigilance : Contracts
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet. > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.

View File

@@ -1,3 +1,14 @@
---
title: Backend — Risques & vigilance : NestJS
domain: backend
bucket: risques
tags: [nestjs, controllers, guards, providers, review]
applies_to: [implementation, review, debug]
severity: high
validated_on: 2026-03-30
source_projects: [app-alexandrie]
---
# Backend — Risques & vigilance : NestJS # Backend — Risques & vigilance : NestJS
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet. > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.

View File

@@ -1,3 +1,14 @@
---
title: Backend — Risques & vigilance : Prisma
domain: backend
bucket: risques
tags: [prisma, transactions, tenant, schema, race-condition]
applies_to: [implementation, review, debug, architecture]
severity: high
validated_on: 2026-03-23
source_projects: [app-template-resto, app-alexandrie]
---
# Backend — Risques & vigilance : Prisma # Backend — Risques & vigilance : Prisma
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet. > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.

View File

@@ -1,3 +1,14 @@
---
title: Frontend — Patterns : Navigation
domain: frontend
bucket: patterns
tags: [navigation, react, expo-router, useeffect, async]
applies_to: [analysis, implementation, review]
severity: medium
validated_on: 2026-03-21
source_projects: [app-template-resto, app-alexandrie]
---
# Frontend — Patterns : Navigation # Frontend — Patterns : Navigation
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/patterns/README.md` pour l'index complet. > Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/patterns/README.md` pour l'index complet.
@@ -165,4 +176,3 @@ return (
</> </>
); );
``` ```

View File

@@ -1,3 +1,14 @@
---
title: Frontend — Patterns : Tests
domain: frontend
bucket: patterns
tags: [tests, react-native, jest, styles, ui]
applies_to: [implementation, review]
severity: medium
validated_on: 2026-03-19
source_projects: [app-alexandrie]
---
# Frontend — Patterns : Tests # Frontend — Patterns : Tests
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/patterns/README.md` pour l'index complet. > Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/patterns/README.md` pour l'index complet.

View File

@@ -1,3 +1,14 @@
---
title: Frontend — Risques & vigilance : Navigation
domain: frontend
bucket: risques
tags: [navigation, expo-router, zustand, useeffect, deep-link]
applies_to: [implementation, review, debug]
severity: high
validated_on: 2026-03-25
source_projects: [app-alexandrie]
---
# Frontend — Risques & vigilance : Navigation # Frontend — Risques & vigilance : Navigation
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet. > Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.

View File

@@ -1,3 +1,14 @@
---
title: Workflow — Risques & vigilance : Story tracking
domain: workflow
bucket: risques
tags: [bmad, story, file-list, review, completion]
applies_to: [analysis, implementation, review]
severity: high
validated_on: 2026-03-31
source_projects: [app-alexandrie, app-template-resto]
---
# Workflow — Risques & vigilance : Story tracking # Workflow — Risques & vigilance : Story tracking
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/workflow/risques/README.md` pour l'index complet. > Extrait de la base de connaissance Lead_tech. Voir `knowledge/workflow/risques/README.md` pour l'index complet.

View File

@@ -4,6 +4,12 @@ Serveur MCP **sidecar** pour brancher la base Lead_tech dans un workflow BMAD sa
Etat actuel : **prototype exploitable** pour un rollout advisory. Etat actuel : **prototype exploitable** pour un rollout advisory.
Documents de référence phase 1 :
- `docs/mcp_v1.md` — contrat fonctionnel stable pour les tools/resources
- `docs/knowledge_metadata.md` — format de front matter pour `knowledge/`
- `docs/implementation_plan.md` — plan de chantier et checklist inter-sessions
## Objectif ## Objectif
- BMAD garde l'orchestration (story, roles, statut, handoff). - BMAD garde l'orchestration (story, roles, statut, handoff).

View File

@@ -0,0 +1,144 @@
# leadtech-bmad-mcp — Plan d'implementation
Ce document sert de **plan de chantier vivant** pour transformer le prototype actuel en MCP propre, stable et exploitable dans BMAD.
Mode d'usage :
- cocher les taches terminees
- mettre a jour le statut des lots en debut/fin de session
- ajouter les decisions structurantes dans `40_decisions_et_archi.md` si besoin
- garder ici le suivi operationnel, pas les longues explications
---
## Vue d'ensemble
| Lot | Objectif | Statut |
| --- | --- | --- |
| Lot 1 | Contrat MCP v1 + metadonnees `knowledge` + compatibilite loader | En cours avance |
| Lot 2 | Index compile local + branchement de la recherche MCP dessus | A faire |
| Lot 3 | Gates configurables + packaging + rollout BMAD | A faire |
---
## Lot 1 — Contrat v1 et base structuree
### Objectif
Stabiliser le contrat du MCP et preparer un corpus `knowledge/` assez structure pour servir de base au futur index.
### Livrables
- [x] Documenter le contrat MCP v1
- [x] Documenter le format de front matter `knowledge/`
- [x] Ajouter la lecture du front matter dans le loader
- [x] Exposer un champ structure `matched_docs` dans `get_guidance`
- [x] Ajouter des tests sur le parsing des metadonnees
- [x] Annoter un premier noyau de documents critiques
### Noyau pilote vise
- [x] `knowledge/backend/patterns/auth.md`
- [x] `knowledge/backend/patterns/contracts.md`
- [x] `knowledge/backend/patterns/nestjs.md`
- [x] `knowledge/backend/risques/auth.md`
- [x] `knowledge/backend/risques/contracts.md`
- [x] `knowledge/backend/risques/nestjs.md`
- [x] `knowledge/backend/risques/prisma.md`
- [x] `knowledge/frontend/patterns/navigation.md`
- [x] `knowledge/frontend/patterns/tests.md`
- [x] `knowledge/frontend/risques/navigation.md`
- [x] `knowledge/workflow/risques/story-tracking.md`
### Reste a faire avant cloture complete du lot
- [ ] Verifier si `knowledge/backend/patterns/prisma.md` doit aussi entrer dans le noyau pilote
- [ ] Verifier si `knowledge/frontend/risques/tests.md` doit aussi entrer dans le noyau pilote
- [ ] Faire un commit de cloture explicite du Lot 1
### Critere de fin
- le contrat v1 est stable
- un noyau representatif de docs critiques est structure
- la recherche MCP sait exploiter les metadonnees sans casser le comportement existant
---
## Lot 2 — Index compile local
### Objectif
Remplacer le scan Markdown a la volee par un index local plus rapide, plus fiable et plus facile a faire evoluer.
### Taches
- [ ] Definir le format de l'index (JSON d'abord)
- [ ] Creer un script de build d'index
- [ ] Indexer les docs `knowledge/*`
- [ ] Indexer les docs globaux `10_*`, `40_*`, `90_*`
- [ ] Prevoir un mode fallback si l'index n'existe pas
- [ ] Rebrancher `search_knowledge()` sur l'index
- [ ] Rebrancher `search_global_docs()` sur l'index
- [ ] Ajouter des tests d'integration sur un mini corpus indexe
### Livrables attendus
- `src/leadtech_bmad_mcp/indexer.py`
- un artefact d'index local versionnable ou regenerable
- documentation de rebuild
### Critere de fin
- les tools de recherche utilisent d'abord l'index
- le fallback texte brut reste disponible pour ne pas bloquer le dev
---
## Lot 3 — Gates configurables et industrialisation
### Objectif
Sortir les regles du code dur, rendre l'installation reproductible, puis cabler proprement le rollout BMAD.
### Taches
- [ ] Extraire les gates dans une config versionnee
- [ ] Distinguer advisory et blocking dans la config
- [ ] Ajouter des tests sur les faux positifs/faux negatifs des gates
- [ ] Stabiliser l'installation `pip install -e ".[dev]"`
- [ ] Ajouter une doc machine vierge / quickstart
- [ ] Preparer un rollout BMAD advisory
- [ ] Definir 2-3 blocages stricts seulement apres validation
### Livrables attendus
- `config/gates.yaml`
- quickstart dev local
- procedure de rollout BMAD
### Critere de fin
- modifier une regle ne demande plus un patch Python
- une autre machine peut installer et lancer le serveur sans bricolage
- BMAD sait quand appeler quels tools et comment interpreter leur sortie
---
## Journal de progression
### 2026-03-31
- Lot 1 demarre
- contrat `mcp_v1.md` ajoute
- convention `knowledge_metadata.md` ajoutee
- loader front matter ajoute
- `matched_docs` ajoute a `get_guidance`
- noyau pilote annote sur backend, frontend et workflow
---
## Regles de maintenance de ce plan
- un lot passe a `Termine` uniquement quand son critere de fin est satisfait
- une case ne se coche pas si le code est seulement commence
- si un arbitrage durable apparait, l'inscrire aussi dans `40_decisions_et_archi.md`

View File

@@ -0,0 +1,108 @@
# leadtech-bmad-mcp — Métadonnées `knowledge/`
Ce document définit le **front matter minimal** attendu pour rendre la base `knowledge/` mieux exploitable par le MCP.
## Objectif
- améliorer le ranking de recherche
- permettre des filtres structurés
- préparer un futur index compilé
## Format retenu
Chaque document `knowledge/*/*.md` peut commencer par :
```md
---
title: Backend — Patterns : NestJS
domain: backend
bucket: patterns
tags: [nestjs, guards, auth, redis]
applies_to: [analysis, implementation, review, debug]
severity: medium
validated_on: 2026-03-07
source_projects: [app-alexandrie]
---
```
Le front matter est **optionnel** dans la phase 1.
Le loader doit :
- fonctionner avec ou sans front matter
- ignorer proprement les champs inconnus
- continuer à servir le markdown brut si demandé en resource
## Champs v1
### `title`
- titre logique du document
- type : `string`
### `domain`
- domaine principal
- type : `backend | frontend | ux | n8n | product | workflow`
### `bucket`
- type de document
- type : `patterns | risques`
### `tags`
- mots-clés de recherche
- type : `string[]`
### `applies_to`
- moments du workflow où le document est pertinent
- type : `analysis | implementation | review | debug | architecture`
- type de stockage : `string[]`
### `severity`
- niveau de criticité métier ou technique du document
- type : `low | medium | high`
### `validated_on`
- date de validation terrain
- type : `YYYY-MM-DD`
### `source_projects`
- projets ayant validé le pattern ou révélé le risque
- type : `string[]`
## Règles de gouvernance
- les README d'index n'ont pas besoin de front matter dans la phase 1
- priorité aux documents les plus consultés ou les plus critiques
- ne pas bloquer une capitalisation parce que le front matter n'existe pas encore
## Politique de migration
Phase 1 :
- ajouter le front matter à un petit noyau de documents pilotes
Noyau pilote actuellement couvert :
- `knowledge/backend/patterns/auth.md`
- `knowledge/backend/patterns/contracts.md`
- `knowledge/backend/patterns/nestjs.md`
- `knowledge/backend/risques/auth.md`
- `knowledge/backend/risques/contracts.md`
- `knowledge/backend/risques/nestjs.md`
- `knowledge/backend/risques/prisma.md`
- `knowledge/frontend/patterns/navigation.md`
- `knowledge/frontend/patterns/tests.md`
- `knowledge/frontend/risques/navigation.md`
- `knowledge/workflow/risques/story-tracking.md`
Phase 2 :
- généraliser aux documents backend, frontend et workflow les plus actifs
Phase 3 :
- exiger ces métadonnées pour l'index compilé

View File

@@ -0,0 +1,209 @@
# leadtech-bmad-mcp — Contrat v1
Ce document fige le **contrat fonctionnel v1** du sidecar MCP Lead_tech.
Objectif :
- stabiliser ce que le serveur promet aux agents BMAD
- éviter les changements de payload implicites
- préparer les itérations suivantes (index compilé, gates configurables) sans casser les prompts existants
## Positionnement
- BMAD garde l'orchestration
- Lead_tech garde la doctrine et la validation humaine
- le MCP sidecar fournit une couche outillée de lecture, guidance, gate qualité, et capitalisation
## Version fonctionnelle
- Nom logique : `leadtech-bmad-mcp`
- Version de contrat : `v1`
- Statut : advisory-first
## Outils v1
### `get_guidance(domain, task_type, story_text?, keywords?, max_items?)`
Rôle :
- retrouver les patterns, risques et docs globaux pertinents pour une story
Entrées :
- `domain` : `backend | frontend | ux | n8n | product | workflow`
- `task_type` : `analysis | implementation | review | debug | architecture`
- `story_text` : texte libre
- `keywords` : liste de mots-clés optionnelle
- `max_items` : entier optionnel
Sortie :
- `must_do[]`
- `should_do[]`
- `red_flags[]`
- `blocking_issues[]`
- `references[]`
- `confidence`
- `gates[]`
Règle :
- advisory par défaut
- pas d'écriture
### `validate_plan(domain, plan_text, agent_role?, strict?)`
Rôle :
- vérifier qu'un plan BMAD couvre les gates Lead_tech les plus importantes
Règle :
- `strict=true` peut produire des `blocking_issues`
- `strict=false` reste advisory
### `validate_patch(domain, diff_text, changed_files?, strict?)`
Rôle :
- contrôler un diff par rapport à des gates Lead_tech
Règle :
- détecte les signaux faibles, pas une preuve formelle de conformité
- un résultat vide ne remplace jamais une review humaine
### `emit_checklist(agent_role, domain, story_text?)`
Rôle :
- produire une checklist opérationnelle par rôle BMAD
### `propose_capitalization(project_name, target_file, why, proposal, dry_run?)`
Rôle :
- préparer une entrée `FILE_UPDATE_PROPOSAL` pour `95_a_capitaliser.md`
Règle :
- `dry_run=true` par défaut
- écriture réelle uniquement si `LEADTECH_MCP_ALLOW_WRITE=1`
### `triage_capitalization(project_filter?, max_entries?)`
Rôle :
- analyser les entrées du buffer de capitalisation
Statut :
- heuristique, non décisionnaire
### `route_to_project_memory(project_name, section, content, dry_run?)`
Rôle :
- router un apprentissage de portée projet vers le `CLAUDE.md` du projet
Règle :
- écriture réelle uniquement si `LEADTECH_MCP_ALLOW_WRITE=1`
## Resources v1
- `leadtech://index`
- `leadtech://capitalisation/pending`
- `leadtech://projects/conf`
- `leadtech://knowledge/{domain}/{bucket}/{slug}`
- `leadtech://global/architecture`
- `leadtech://global/debug`
- `leadtech://global/conventions`
## Payload commun
Les tools de guidance et de validation retournent un payload homogène :
```json
{
"must_do": [],
"should_do": [],
"red_flags": [],
"blocking_issues": [],
"confidence": "MEDIUM",
"references": [],
"matched_docs": [],
"gates": []
}
```
## Niveaux de sévérité
- `must_do` : action attendue fortement recommandée
- `should_do` : amélioration ou vérification utile
- `red_flags` : signal de risque ou d'incertitude
- `blocking_issues` : empêche la progression en mode strict
## Confiance
- `HIGH`
- `MEDIUM`
- `LOW`
Règle :
- la confiance exprime la qualité du matching heuristique, pas la vérité terrain
## Références
`references[]` contient :
```json
{
"path": "/abs/path/doc.md",
"reason": "Match patterns score=7"
}
```
Règle :
- toujours renvoyer des chemins absolus si possible
- référencer la source, pas uniquement un résumé
## Matched docs
`matched_docs[]` expose les meilleurs documents remontés par le moteur de guidance.
Structure cible v1 :
```json
{
"path": "/abs/path/doc.md",
"bucket": "patterns",
"title": "Backend — Patterns : NestJS",
"score": "12",
"severity": "medium",
"applies_to": "analysis, implementation, review, debug",
"tags": "nestjs, guards, auth, redis, quota",
"read_uri": "leadtech://knowledge/backend/patterns/nestjs"
}
```
Règle :
- champ informatif, non bloquant
- utile pour les agents qui veulent afficher ou prioriser explicitement les docs remontés
## Erreurs
Conventions v1 :
- tool de lecture : lever une erreur explicite si ressource introuvable
- tool d'écriture : retourner un objet `{ "error": "..." }` si écriture désactivée ou cible introuvable
- ne jamais écrire dans `knowledge/*`
## Sécurité d'écriture
Écritures autorisées uniquement vers :
- `95_a_capitaliser.md`
- `CLAUDE.md` projet
Écritures interdites :
- `knowledge/*`
- `10_*`
- `40_*`
- `90_*`
## Compatibilité BMAD
Ce contrat v1 est pensé pour :
- enrichir une story à l'entrée
- bloquer certains cas en pre-implémentation ou post-patch
- tracer la décision humaine dans la story
Les prompts BMAD doivent considérer ce contrat comme stable tant qu'un document `v2` n'existe pas.

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any
VALID_DOMAINS = {"backend", "frontend", "ux", "n8n", "product", "workflow"} VALID_DOMAINS = {"backend", "frontend", "ux", "n8n", "product", "workflow"}
@@ -17,6 +18,13 @@ class LeadtechPaths:
projects_conf: Path projects_conf: Path
@dataclass(frozen=True)
class KnowledgeDocument:
path: Path
metadata: dict[str, Any]
body: str
def get_paths() -> LeadtechPaths: def get_paths() -> LeadtechPaths:
root = Path(os.getenv("LEADTECH_ROOT", "/srv/helpers/_Assistant_Lead_Tech")).resolve() root = Path(os.getenv("LEADTECH_ROOT", "/srv/helpers/_Assistant_Lead_Tech")).resolve()
return LeadtechPaths( return LeadtechPaths(
@@ -52,6 +60,56 @@ def read_text(path: Path) -> str:
EXCERPT_LENGTH = 400 EXCERPT_LENGTH = 400
def _coerce_metadata_scalar(value: str) -> Any:
raw = value.strip()
if not raw:
return ""
low = raw.lower()
if low in {"true", "false"}:
return low == "true"
if raw.startswith("[") and raw.endswith("]"):
inner = raw[1:-1].strip()
if not inner:
return []
return [item.strip().strip("'\"") for item in inner.split(",") if item.strip()]
return raw.strip("'\"")
def parse_front_matter(content: str) -> tuple[dict[str, Any], str]:
if not content.startswith("---\n"):
return {}, content
lines = content.splitlines()
end_idx = None
for idx in range(1, len(lines)):
if lines[idx].strip() == "---":
end_idx = idx
break
if end_idx is None:
return {}, content
metadata: dict[str, Any] = {}
for line in lines[1:end_idx]:
stripped = line.strip()
if not stripped or stripped.startswith("#") or ":" not in stripped:
continue
key, value = stripped.split(":", 1)
metadata[key.strip()] = _coerce_metadata_scalar(value)
body = "\n".join(lines[end_idx + 1 :]).lstrip("\n")
return metadata, body
def read_knowledge_document(path: Path) -> KnowledgeDocument:
raw = read_text(path)
metadata, body = parse_front_matter(raw)
return KnowledgeDocument(path=path, metadata=metadata, body=body)
def _extract_excerpt(content: str, tokens: list[str]) -> str: def _extract_excerpt(content: str, tokens: list[str]) -> str:
"""Retourne un extrait centré sur la première occurrence d'un token, ou le début du fichier.""" """Retourne un extrait centré sur la première occurrence d'un token, ou le début du fichier."""
low = content.lower() low = content.lower()
@@ -78,17 +136,23 @@ def search_knowledge(domain: str, query: str, bucket: str | None = None, max_ite
for b in buckets: for b in buckets:
for file_path in list_domain_files(domain, b): for file_path in list_domain_files(domain, b):
content = read_text(file_path) doc = read_knowledge_document(file_path)
score = sum(content.lower().count(tok) for tok in tokens) body_low = doc.body.lower()
metadata_text = " ".join(str(value) for value in doc.metadata.values()).lower()
score = sum(body_low.count(tok) for tok in tokens)
score += sum(metadata_text.count(tok) * 3 for tok in tokens)
if score <= 0: if score <= 0:
continue continue
out.append( out.append(
{ {
"path": str(file_path), "path": str(file_path),
"bucket": b, "bucket": b,
"title": file_path.stem, "title": str(doc.metadata.get("title", file_path.stem)),
"score": str(score), "score": str(score),
"excerpt": _extract_excerpt(content, tokens), "excerpt": _extract_excerpt(doc.body, tokens),
"tags": ", ".join(doc.metadata.get("tags", [])),
"severity": str(doc.metadata.get("severity", "")),
"applies_to": ", ".join(doc.metadata.get("applies_to", [])),
} }
) )

View File

@@ -16,6 +16,7 @@ def empty_gate_output() -> dict[str, Any]:
"blocking_issues": [], "blocking_issues": [],
"confidence": CONFIDENCE_MEDIUM, "confidence": CONFIDENCE_MEDIUM,
"references": [], "references": [],
"matched_docs": [],
} }

View File

@@ -45,6 +45,19 @@ def _base_output() -> dict:
return payload return payload
def _metadata_label(item: dict[str, str]) -> str:
parts: list[str] = []
if item.get("severity"):
parts.append(f"severity={item['severity']}")
if item.get("applies_to"):
parts.append(f"applies_to={item['applies_to']}")
if item.get("tags"):
parts.append(f"tags={item['tags']}")
if not parts:
return ""
return " [" + " | ".join(parts) + "]"
@mcp.resource("leadtech://index") @mcp.resource("leadtech://index")
def resource_index() -> str: def resource_index() -> str:
return read_text(get_paths().root / "00_INDEX.md") return read_text(get_paths().root / "00_INDEX.md")
@@ -89,12 +102,25 @@ def get_guidance(domain: Domain, task_type: TaskType, story_text: str = "", keyw
for item in matches: for item in matches:
slug = Path(item["path"]).stem slug = Path(item["path"]).stem
read_uri = f"leadtech://knowledge/{domain}/{item['bucket']}/{slug}" read_uri = f"leadtech://knowledge/{domain}/{item['bucket']}/{slug}"
label = f"{item['title']}{item['excerpt']} [lire complet: {read_uri}]" metadata = _metadata_label(item)
label = f"{item['title']}{metadata}{item['excerpt']} [lire complet: {read_uri}]"
if item["bucket"] == "patterns": if item["bucket"] == "patterns":
out["must_do"].append(f"Appliquer: {label}") out["must_do"].append(f"Appliquer: {label}")
else: else:
out["red_flags"].append(f"Surveiller: {label}") out["red_flags"].append(f"Surveiller: {label}")
add_reference(out, item["path"], f"Match {item['bucket']} score={item['score']}") add_reference(out, item["path"], f"Match {item['bucket']} score={item['score']}")
out["matched_docs"].append(
{
"path": item["path"],
"bucket": item["bucket"],
"title": item["title"],
"score": item["score"],
"severity": item.get("severity", ""),
"applies_to": item.get("applies_to", ""),
"tags": item.get("tags", ""),
"read_uri": read_uri,
}
)
# Fichiers globaux (decisions, postmortems, conventions) # Fichiers globaux (decisions, postmortems, conventions)
global_matches = search_global_docs(query=query, max_items=3) global_matches = search_global_docs(query=query, max_items=3)

View File

@@ -10,9 +10,10 @@ from leadtech_bmad_mcp.knowledge import (
search_knowledge, search_knowledge,
search_global_docs, search_global_docs,
read_knowledge_doc, read_knowledge_doc,
read_knowledge_document,
_extract_excerpt, _extract_excerpt,
parse_front_matter,
LeadtechPaths, LeadtechPaths,
get_paths,
) )
@@ -122,6 +123,30 @@ def test_search_knowledge_single_bucket(tmp_path):
assert all(r["bucket"] == "risques" for r in results) assert all(r["bucket"] == "risques" for r in results)
def test_search_knowledge_uses_front_matter_tags(tmp_path):
knowledge = tmp_path / "knowledge"
(knowledge / "backend" / "patterns").mkdir(parents=True)
(knowledge / "backend" / "patterns" / "nestjs.md").write_text(
"---\n"
"title: Backend — Patterns : NestJS\n"
"tags: [guards, auth]\n"
"---\n\n"
"# NestJS\n\n"
"Texte volontairement sans le mot cle demande.\n",
encoding="utf-8",
)
paths = LeadtechPaths(
root=tmp_path,
knowledge=knowledge,
capitalisation=tmp_path / "95_a_capitaliser.md",
projects_conf=tmp_path / "_projects.conf",
)
with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
results = search_knowledge("backend", "guards")
assert results
assert results[0]["title"] == "Backend — Patterns : NestJS"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# read_knowledge_doc # read_knowledge_doc
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -147,6 +172,23 @@ def test_read_knowledge_doc_traversal_blocked(tmp_path):
read_knowledge_doc("backend", "patterns", "../../etc/passwd") read_knowledge_doc("backend", "patterns", "../../etc/passwd")
def test_read_knowledge_document_splits_metadata_and_body(tmp_path):
file_path = tmp_path / "doc.md"
file_path.write_text(
"---\n"
"title: Test Doc\n"
"tags: [alpha, beta]\n"
"---\n\n"
"# Heading\n\n"
"Contenu principal.\n",
encoding="utf-8",
)
doc = read_knowledge_document(file_path)
assert doc.metadata["title"] == "Test Doc"
assert doc.metadata["tags"] == ["alpha", "beta"]
assert doc.body.startswith("# Heading")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# _extract_excerpt # _extract_excerpt
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -176,6 +218,31 @@ def test_extract_excerpt_length_bounded():
assert len(excerpt) <= 500 assert len(excerpt) <= 500
def test_parse_front_matter_returns_body_unchanged_without_header():
content = "# Heading\n\nPlain body"
metadata, body = parse_front_matter(content)
assert metadata == {}
assert body == content
def test_parse_front_matter_parses_lists_and_scalars():
content = (
"---\n"
"title: Demo\n"
"tags: [alpha, beta]\n"
"severity: high\n"
"enabled: true\n"
"---\n\n"
"Body\n"
)
metadata, body = parse_front_matter(content)
assert metadata["title"] == "Demo"
assert metadata["tags"] == ["alpha", "beta"]
assert metadata["severity"] == "high"
assert metadata["enabled"] is True
assert body == "Body\n"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# search_global_docs # search_global_docs
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -32,7 +32,7 @@ def _mock_mcp_module():
_mock_mcp_module() _mock_mcp_module()
from leadtech_bmad_mcp.server import validate_plan, validate_patch # noqa: E402 from leadtech_bmad_mcp.server import get_guidance, validate_plan, validate_patch # noqa: E402
from leadtech_bmad_mcp.knowledge import LeadtechPaths # noqa: E402 from leadtech_bmad_mcp.knowledge import LeadtechPaths # noqa: E402
@@ -88,6 +88,41 @@ class TestValidatePlanContracts:
assert not contract_blocks assert not contract_blocks
class TestGetGuidanceMetadata:
def test_exposes_matched_docs_with_metadata(self, tmp_path):
paths = _fake_paths(tmp_path)
with patch(
"leadtech_bmad_mcp.server.search_knowledge",
return_value=[
{
"path": str(tmp_path / "knowledge" / "backend" / "patterns" / "nestjs.md"),
"bucket": "patterns",
"title": "Backend — Patterns : NestJS",
"score": "9",
"excerpt": "Pattern sur les guards globaux.",
"severity": "medium",
"applies_to": "analysis, implementation, review",
"tags": "nestjs, guards, auth",
}
],
), patch(
"leadtech_bmad_mcp.server.search_global_docs",
return_value=[],
), patch(
"leadtech_bmad_mcp.server.get_paths",
return_value=paths,
):
result = get_guidance("backend", "implementation", story_text="Ajouter un guard NestJS")
assert result["matched_docs"]
matched = result["matched_docs"][0]
assert matched["bucket"] == "patterns"
assert matched["severity"] == "medium"
assert "implementation" in matched["applies_to"]
assert matched["read_uri"] == "leadtech://knowledge/backend/patterns/nestjs"
assert any("severity=medium" in item for item in result["must_do"])
class TestValidatePlanRequestId: class TestValidatePlanRequestId:
def test_suggests_requestid_when_error_without_requestid(self, tmp_path): def test_suggests_requestid_when_error_without_requestid(self, tmp_path):
paths = _fake_paths(tmp_path) paths = _fake_paths(tmp_path)