feat: referentiel projets dynamique + validation nommage by design

Durcit la convention de nommage des projets (dérive constatée : 'Sliding
Automation', 'code_versioning'... au lieu des formes canoniques).

- trilium_api.py : projets_canoniques() lit le référentiel = valeurs du label
  projet sur les notes de type=projet (source unique, pas de constante en dur).
  Note-projet CodeVersioning créée (manquait).
- mcp_server.py : _valider_projet() branché dans les 6 tools de création
  (add_decision/history/backlog, new_conversation, create_entite, add_skill).
  Refuse un projet non canonique (suggestion si faute) ou inconnu (renvoi au
  processus de création de projet). Ne verrouille pas si référentiel illisible.
- lint_audit.py : VAL-nommage aligné sur le référentiel (attrape casse, espace
  ET snake_case ; l'ancien 'contient un espace' ratait code_versioning).
- Données : 79 notes ré-étiquetées vers les 3 formes canoniques.

Quality by design : l'erreur de nommage devient impossible à l'écriture, le
Lint n'est plus que le filet de sécurité.
This commit is contained in:
2026-07-17 14:59:33 +02:00
parent d1e67431b6
commit 943acbc573
2425 changed files with 710525 additions and 6 deletions
+41 -1
View File
@@ -24,7 +24,7 @@ from trilium_api import (
create_note, get_note_id, get_note, get_note_content, get_children,
update_note_content, search_by_label, set_label, get_label_value,
find_note_by_title, delete_note_safe, move_note_safe, add_relation_safe,
remove_relation_safe, move_relation_safe,
remove_relation_safe, move_relation_safe, projets_canoniques,
)
load_dotenv()
@@ -397,7 +397,37 @@ def tool_get_contexte(projet, llm_cible="LLM"):
lines += ["---", "Confirme en 3 lignes : (a) objectif (b) prochaine action (c) incertitude"]
return "\n".join(lines)
def _valider_projet(projet):
"""Valide un nom de projet contre le referentiel (notes de type=projet).
Retourne None si OK, sinon un dict d'erreur explicite.
- projet None/vide : OK (cas skill universel).
- referentiel vide (bug lecture) : OK (ne pas verrouiller le systeme).
- proche d'un canonique (casse/accent/espace) : erreur avec suggestion.
- inconnu : erreur renvoyant au processus de creation de projet."""
if not projet:
return None
canon = projets_canoniques()
if not canon:
return None # garde-fou : ne jamais bloquer si le referentiel est illisible
if projet in canon:
return None
import unicodedata
def norm(s):
s = "".join(ch for ch in unicodedata.normalize("NFD", s) if unicodedata.category(ch) != "Mn")
return s.lower().replace(" ", "").replace("_", "").replace("-", "")
cible = None
for cand in canon:
if norm(cand) == norm(projet):
cible = cand
break
if cible:
return {"error": "Nom de projet non canonique: %r. Utiliser %r (referentiel: notes de type projet)." % (projet, cible)}
return {"error": "Projet inconnu: %r. Projets valides: %s. Pour un nouveau projet, creer d'abord une note de type=projet (processus dedie)." % (projet, sorted(canon))}
def tool_add_decision(projet, enonce, justification=""):
err = _valider_projet(projet)
if err: return err
ids = load_ids()
res = create_note(ids["Decisions"], enonce,
content=f"<p><b>Justification</b> : {justification or '-'}</p>")
@@ -407,6 +437,8 @@ def tool_add_decision(projet, enonce, justification=""):
return {"id": nid, "enonce": enonce, "status": "cree"}
def tool_add_history(projet, enonce, type_historique, detail=""):
err = _valider_projet(projet)
if err: return err
valides = ["Fait etabli", "Test effectue", "Hypothese invalidee", "Contrainte decouverte"]
if type_historique not in valides:
return {"error": f"type_historique invalide. Valeurs : {valides}"}
@@ -419,6 +451,8 @@ def tool_add_history(projet, enonce, type_historique, detail=""):
return {"id": nid, "enonce": enonce, "status": "cree"}
def tool_add_backlog(projet, titre, priorite="moyenne"):
err = _valider_projet(projet)
if err: return err
if priorite not in ("haute", "moyenne", "basse"):
return {"error": "priorite doit etre haute, moyenne ou basse"}
ids = load_ids()
@@ -440,6 +474,8 @@ def tool_update_backlog(note_id, statut):
return {"id": note_id, "statut": statut, "status": "mis a jour"}
def tool_new_conversation(projet, titre, llm):
err = _valider_projet(projet)
if err: return err
ids = load_ids()
date = datetime.now().strftime("%Y-%m-%d %H:%M")
full = f"[{llm}] {date}{titre}"
@@ -527,6 +563,8 @@ def tool_update_note(note_id, contenu, format="markdown"):
return {"id": note_id, "titre": note.get("title", ""), "format": format, "status": "mis a jour"}
def tool_create_entite(type, projet, titre, contenu=None, definition=None):
err = _valider_projet(projet)
if err: return err
"""Cree une entite de la couche connaissance/technique (concept, composantLogiciel,
service). Le type est valide contre une liste blanche ; le dossier cible est resolu
via trilium_ids.json. Garde-fou anti-doublon sur le titre dans le dossier cible.
@@ -589,6 +627,8 @@ def tool_move_note(note_id, ancien_parent_id, nouveau_parent_id):
return {"success": ok, "message": message}
def tool_add_skill(titre, contenu, portee="projet", projet=None):
err = _valider_projet(projet)
if err: return err
if portee != "universel" and not projet:
return {"error": "projet requis sauf si portee='universel'"}
ids = load_ids()