From ced9b37d4aa0af54a4851e43afa487ea48c516aa Mon Sep 17 00:00:00 2001 From: Master Date: Fri, 3 Jul 2026 13:05:27 +0200 Subject: [PATCH] feat: lint_audit.py calibre + TYPES_CANONIQUES source unique - lint_audit.py : moteur d'audit groupe par data quality dimensions (completeness, accuracy, consistency, validity, uniqueness, freshness) - charger() : passe #type globale (voit tous les types reels, dont les intrus) - VAL-type : valide le label type contre la liste canonique (ignore container) - dependDe elargi (domaine +service, portee +outil) : faux positif supprime - TYPES_CANONIQUES dans trilium_api.py : source unique partagee Lint + create_entite - create_entite aligne sur TYPES_CANONIQUES, couvre les 10 types connaissance/technique Etage 1 lecture seule : 348 notes, 29 anomalies reelles, 0 faux positif. --- lint_audit.py | 512 ++++++++++++++++++++++++++ lint_reports/lint_20260703_083924.txt | 152 ++++++++ lint_reports/lint_20260703_130356.txt | 145 ++++++++ mcp_server.py | 17 +- trilium_api.py | 12 + 5 files changed, 836 insertions(+), 2 deletions(-) create mode 100644 lint_audit.py create mode 100644 lint_reports/lint_20260703_083924.txt create mode 100644 lint_reports/lint_20260703_130356.txt diff --git a/lint_audit.py b/lint_audit.py new file mode 100644 index 0000000..b2fddbc --- /dev/null +++ b/lint_audit.py @@ -0,0 +1,512 @@ +#!/usr/bin/env python3 +""" +lint_audit.py - Moteur d'audit (Lint) du knowledge graph Context Continuity. + +ETAGE 1 : LECTURE SEULE. Detecte et rapporte des anomalies ; ne corrige RIEN +et n'ecrit RIEN dans Trilium (rapport texte : stdout + fichier horodate dans +lint_reports/). + +ORGANISATION PAR DATA QUALITY DIMENSIONS (referentiel autoporteur, extensible). +Chaque controle declare la dimension qu'il sert ; le rapport est groupe par +dimension. Ajouter un controle = decorer une fonction avec @check(dimension,...). +Ajouter une dimension = l'ajouter a DIMENSIONS. Le rapport suit automatiquement. + +Chaque anomalie porte aussi un bac d'action (prepare l'etage 2) : + AUTO : correction mecanique sure (accent, casse, enum non ambigue) + SIGNALER : deterministe mais sans correction evidente -> decision humaine + JUGEMENT : demande du contexte metier -> revue par l'agent / l'humain +A l'etage 1, AUCUNE correction n'est appliquee, quel que soit le bac. + +Usage (sur GrosseBertha, dans le venv) : + cd ~/App/Context_continuity && source venv/bin/activate + python3 lint_audit.py +""" + +import os +import json +import unicodedata +from datetime import datetime, timezone +from typing import List, Dict, Optional + +from trilium_api import ( + search_by_label, get_note, get_note_content, + RELATIONS_ONTOLOGIE, TYPES_CANONIQUES, +) + +BASE = os.path.expanduser("~/App/Context_continuity") +IDS = json.load(open(os.path.join(BASE, "trilium_ids.json"))) + + +# ========================================================================== +# 1. LA TAXONOMIE : data quality dimensions (autoporteur) +# ========================================================================== + +# Ordre = ordre d'affichage dans le rapport. Definitions = celles de Bastien +# (Data Governance), alignees sur les dimensions DAMA standard. +DIMENSIONS = { + "completeness": "Les champs obligatoires sont remplis ET les entites attendues sont toutes saisies (exhaustivite).", + "accuracy": "Le niveau de la donnee reflete la realite (bon rangement, contenu conforme au type, statut a jour).", + "consistency": "Coherence dans le temps et a travers les entites (vocabulaire courant, informations repetees a jour partout).", + "validity": "Les valeurs et relations respectent les regles en vigueur (enums, conventions de nommage, ontologie des relations).", + "uniqueness": "Absence de doublons.", + "freshness": "Les entites ne portent pas un etat perime (anciennete anormale, non-cloture).", +} + +# Controles PREVUS mais NON deterministes : delegues a l'agent reviseur (etage 2). +# Rendus visibles pour l'honnetete de la couverture. +CHECKS_DELEGUES = [ + {"dimension": "completeness", "code": "COMP-exhaustivite", + "libelle": "Entites manquantes vs realite du projet (ex : backlog incomplet)", + "raison": "Non detectable en deterministe : requiert la comparaison avec la realite (code, conversations). -> agent reviseur."}, + {"dimension": "accuracy", "code": "ACC-contenu-type", + "libelle": "Le contenu redige d'une note reflete-t-il bien son type", + "raison": "Requiert un jugement semantique. -> agent reviseur."}, + {"dimension": "consistency", "code": "CONS-chiffres-croises", + "libelle": "Une information repetee (ex : nombre de tools) est-elle a jour dans toutes les notes", + "raison": "Partiellement couvert par CONS-vocab ; la verification exhaustive des chiffres croises -> agent reviseur."}, +] + + +# ========================================================================== +# 2. REFERENTIEL : ce que le systeme considere comme valide +# ========================================================================== + +ENUMS = { + ("projet", "statut"): {"actif", "en-pause", "archive"}, + ("conversation", "statut"): {"en-cours", "clos"}, + ("backlogItem", "statut"): {"a faire", "en cours", "bloque", "fait", "abandonne"}, + ("backlogItem", "priorite"): {"haute", "moyenne", "basse"}, + ("decision", "statut"): {"active", "revisee", "annulee"}, + ("historiqueItem", "typeHistorique"): { + "Fait etabli", "Test effectue", "Hypothese invalidee", "Contrainte decouverte", + }, + ("historiqueItem", "encoreValide"): {"true", "false"}, +} + +LABELS_OBLIGATOIRES = { + "projet": ["projet", "statut"], + "conversation": ["projet", "llm", "statut", "date"], + "backlogItem": ["projet", "statut", "priorite"], + "decision": ["projet", "statut"], + "historiqueItem": ["projet", "typeHistorique", "encoreValide"], + "concept": ["projet"], + "contexteReprise": ["projet", "llmCible", "version"], + "composantLogiciel": ["projet"], + "service": ["projet"], + "infrastructure": ["projet"], + "outil": ["projet"], + "contrainte": ["projet"], + "principe": ["projet"], + "convention": ["projet"], + "methode": ["projet"], + "version": ["projet"], +} + +DOSSIER_ATTENDU = { + "backlogItem": "Backlog", + "decision": "Decisions", + "historiqueItem": "Historique", + "conversation": "Conversations", + "concept": "Concepts", + "contexteReprise": "ContextesReprise", + "projet": "Projets", +} + +TYPES_METIER = list(LABELS_OBLIGATOIRES.keys()) + ["skill", "documentation"] + +TYPES_DEPRECIES = {"termeGlossaire"} +MOTS_DEPRECIES = ["type systeme", "type système", "termeGlossaire"] + +JOURS_STALE_ENCOURS = 90 +JOURS_STALE_CONVO = 30 + +# Domaine -> portee (sous-ensemble ROBUSTE ; classes mappees aux types reels). +# domaine None = seule la portee est verifiee. Table volontairement partielle : +# uniquement les relations dont le mapping classe-ontologie -> type est certain. +RELATION_DP = { + "impacte": ({"decision", "historiqueItem"}, {"composantLogiciel", "service"}), + "revise": ({"decision"}, {"decision"}), + "dependDe": ({"composantLogiciel", "service"}, {"composantLogiciel", "service", "outil"}), + "aVersion": ({"composantLogiciel", "service"}, {"version"}), + "implementePar": ({"service"}, {"composantLogiciel"}), + "contraintPar": ({"decision", "composantLogiciel"}, {"contrainte", "principe"}), + "illustre": ({"concept", "composantLogiciel", "service"}, {"concept"}), + "documentePar": ({"service", "outil", "methode", "composantLogiciel"}, {"skill", "documentation"}), + "concerneProjet": (None, {"projet"}), +} + + +# ========================================================================== +# 3. REGISTRE DES CONTROLES (decorateur autoporteur) +# ========================================================================== + +CHECKS: List[dict] = [] # {dimension, code, libelle, fn} +_CTX = {"dimension": None} # dimension du check en cours d'execution + + +def check(dimension: str, code: str, libelle: str): + """Enregistre une fonction de controle sous une dimension de qualite.""" + def deco(fn): + CHECKS.append({"dimension": dimension, "code": code, "libelle": libelle, "fn": fn}) + return fn + return deco + + +# ========================================================================== +# 4. UTILITAIRES + CHARGEMENT (une passe, cache) +# ========================================================================== + +def sans_accents(s: str) -> str: + return "".join(c for c in unicodedata.normalize("NFD", s) + if unicodedata.category(c) != "Mn") + + +NOTES: Dict[str, dict] = {} +BY_TYPE: Dict[str, List[str]] = {} + + +def labels_of(note: dict) -> Dict[str, str]: + return {a.get("name"): a.get("value", "") + for a in note.get("attributes", []) if a.get("type") == "label"} + + +def relations_of(note: dict) -> List[Dict[str, str]]: + return [{"name": a.get("name"), "value": a.get("value", "")} + for a in note.get("attributes", []) if a.get("type") == "relation"] + + +def type_of(note: dict) -> Optional[str]: + return labels_of(note).get("type") + + +def age_jours(note: dict) -> Optional[int]: + dm = note.get("utcDateModified") or note.get("dateModified") + if not dm: + return None + for fmt in ("%Y-%m-%d %H:%M:%S.%fZ", "%Y-%m-%d %H:%M:%SZ", + "%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%d %H:%M:%S.%f"): + try: + d = datetime.strptime(dm, fmt).replace(tzinfo=timezone.utc) + return (datetime.now(timezone.utc) - d).days + except Exception: + continue + return None + + +def charger(): + """Charge TOUTES les notes portant un label type (recherche #type globale), + quel que soit le type - y compris les types invalides/inconnus, pour que + VAL-type puisse les detecter. Indexe par la valeur reelle du label type.""" + try: + res = search_by_label("type", "") # #type sans valeur = toutes les notes typees + except Exception as e: + print(" ! echec recherche #type : %s" % e) + res = [] + for r in res: + nid = r.get("noteId") + if not nid or nid in NOTES: + continue + try: + note = get_note(nid) + except Exception: + continue + NOTES[nid] = note + tv = type_of(note) or "(sans type)" + BY_TYPE.setdefault(tv, []).append(nid) + print(" Charge : %d notes sur %d valeurs de type" % (len(NOTES), len(BY_TYPE))) + + +# ========================================================================== +# 5. COLLECTE DES ANOMALIES +# ========================================================================== + +ANOMALIES: List[dict] = [] + + +def signaler(code, bac, note_id, titre, detail, suggestion=""): + """Enregistre une anomalie ; la dimension est heritee du check courant.""" + ANOMALIES.append({ + "dimension": _CTX["dimension"], "code": code, "bac": bac, + "note_id": note_id, "titre": titre, "detail": detail, "suggestion": suggestion, + }) + + +# ========================================================================== +# 6. LES CONTROLES, GROUPES PAR DIMENSION +# ========================================================================== + +# ---- COMPLETENESS -------------------------------------------------------- + +@check("completeness", "COMP-labels", "Labels obligatoires presents selon le type") +def c_labels_obligatoires(): + for typ, obligatoires in LABELS_OBLIGATOIRES.items(): + for nid in BY_TYPE.get(typ, []): + note = NOTES[nid] + labels = labels_of(note) + manquants = [l for l in obligatoires if not labels.get(l)] + if manquants: + signaler("COMP-labels", "SIGNALER", nid, note.get("title", "?"), + "type=%s : labels manquants %s" % (typ, manquants), + "ajouter les labels manquants") + + +# ---- ACCURACY ------------------------------------------------------------ + +@check("accuracy", "ACC-rangement", "Note rangee dans le dossier attendu de son type") +def a_rangement(): + for typ, cle_dossier in DOSSIER_ATTENDU.items(): + dossier_id = IDS.get(cle_dossier) + if not dossier_id: + continue + for nid in BY_TYPE.get(typ, []): + note = NOTES[nid] + parents = note.get("parentNoteIds", []) + if parents and dossier_id not in parents: + signaler("ACC-rangement", "JUGEMENT", nid, note.get("title", "?"), + "type=%s hors du dossier %s (parents=%s)" % (typ, cle_dossier, parents), + "verifier le rangement (clone legitime possible)") + + +@check("accuracy", "ACC-decision", "Statut des decisions actives reflete la realite (v1 vs v2)") +def a_decisions_obsoletes(): + SEUIL = 12 + par_projet = {} + for nid in BY_TYPE.get("decision", []): + labels = labels_of(NOTES[nid]) + if labels.get("statut") == "active": + par_projet.setdefault(labels.get("projet", "?"), []).append(nid) + for proj, ids in par_projet.items(): + if len(ids) >= SEUIL: + signaler("ACC-decision", "JUGEMENT", ids[0], "(projet %s)" % proj, + "%d decisions actives sur %s : possibles v1 non revisees" % (len(ids), proj), + "revue : tisser ~revise, passer les v1 obsoletes en revisee") + + +# ---- CONSISTENCY --------------------------------------------------------- + +@check("consistency", "CONS-vocab", "Vocabulaire courant (types et termes non deprecies)") +def cons_vocabulaire(): + # Types deprecies + for typ in TYPES_DEPRECIES: + for nid in BY_TYPE.get(typ, []): + signaler("CONS-type", "SIGNALER", nid, NOTES[nid].get("title", "?"), + "type deprecie : %s" % typ, "migrer vers concept") + # Mots deprecies dans le contenu des skills / documentation + for typ in ("skill", "documentation"): + for nid in BY_TYPE.get(typ, []): + try: + contenu = get_note_content(nid) + except Exception: + continue + trouves = [m for m in MOTS_DEPRECIES if m in contenu] + if trouves: + signaler("CONS-vocab", "JUGEMENT", nid, NOTES[nid].get("title", "?"), + "mots deprecies dans le contenu : %s" % trouves, + "reformuler (faux positif possible si mention explicative legitime)") + + +# ---- VALIDITY ------------------------------------------------------------ + +@check("validity", "VAL-enum", "Valeurs de labels conformes aux enumerations permises") +def v_enums(): + for (typ, label), valides in ENUMS.items(): + for nid in BY_TYPE.get(typ, []): + note = NOTES[nid] + val = labels_of(note).get(label) + if val is None or val in valides: + continue + titre = note.get("title", "?") + cible = None + for v in valides: + if sans_accents(val.lower()) == sans_accents(v.lower()): + cible = v + break + if cible is not None: + signaler("VAL-enum", "AUTO", nid, titre, + "%s=%r (casse/accent)" % (label, val), "corriger en %r" % cible) + else: + signaler("VAL-enum", "SIGNALER", nid, titre, + "%s=%r hors valeurs permises %s" % (label, val, sorted(valides)), + "corriger manuellement") + + +@check("validity", "VAL-type", "Label type appartient a la liste canonique de l ontologie") +def v_type(): + for nid, note in NOTES.items(): + tv = type_of(note) + if tv is None or tv == "container" or tv in TYPES_CANONIQUES: + continue # container = dossier structurel, hors perimetre + titre = note.get("title", "?") + cible = None + for t in TYPES_CANONIQUES: + if sans_accents(tv.lower()) == sans_accents(t.lower()): + cible = t + break + if cible is not None: + signaler("VAL-type", "AUTO", nid, titre, + "type=%r (casse)" % tv, "corriger en %r" % cible) + else: + signaler("VAL-type", "SIGNALER", nid, titre, + "type=%r hors liste canonique" % tv, "corriger ou ajouter a l ontologie") + + +@check("validity", "VAL-nommage", "Conventions de nommage (projet sans espace)") +def v_nommage(): + for nid, note in NOTES.items(): + val = labels_of(note).get("projet") + if val and " " in val: + signaler("VAL-nommage", "SIGNALER", nid, note.get("title", "?"), + "label projet=%r contient un espace" % val, + "renommer sans espace + repercuter partout") + + +@check("validity", "VAL-relation", "Relations valides (ontologie, cible existante, domaine/portee)") +def v_relations(): + for nid, note in NOTES.items(): + typ_src = type_of(note) + titre = note.get("title", "?") + for rel in relations_of(note): + nom, cible = rel["name"], rel["value"] + if nom not in RELATIONS_ONTOLOGIE: + signaler("VAL-rel-nom", "SIGNALER", nid, titre, + "relation ~%s hors ontologie" % nom, "renommer ou retirer") + continue + cible_note = NOTES.get(cible) + if cible_note is None: + try: + cible_note = get_note(cible) + except Exception: + cible_note = None + if not cible_note: + signaler("VAL-rel-cible", "SIGNALER", nid, titre, + "~%s pointe vers une note absente (%s)" % (nom, cible), + "corriger ou retirer la relation") + continue + dp = RELATION_DP.get(nom) + if dp: + dom, por = dp + typ_cible = type_of(cible_note) + if dom is not None and typ_src not in dom: + signaler("VAL-rel-domaine", "JUGEMENT", nid, titre, + "~%s : source type=%s hors domaine %s" % (nom, typ_src, sorted(dom)), + "verifier la pertinence") + if por is not None and typ_cible not in por: + signaler("VAL-rel-portee", "JUGEMENT", nid, titre, + "~%s : cible type=%s hors portee %s" % (nom, typ_cible, sorted(por)), + "verifier la pertinence") + + +# ---- UNIQUENESS ---------------------------------------------------------- + +@check("uniqueness", "UNIQ-doublon", "Absence de notes identiques (titre + type + projet)") +def u_doublons(): + vus = {} + for nid, note in NOTES.items(): + cle = ((note.get("title") or "").strip().lower(), + type_of(note), labels_of(note).get("projet", "")) + vus.setdefault(cle, []).append(nid) + for (titre, typ, proj), ids in vus.items(): + if len(ids) > 1: + signaler("UNIQ-doublon", "SIGNALER", ids[0], titre or "(sans titre)", + "%d notes identiques (type=%s projet=%s) : %s" % (len(ids), typ, proj, ids), + "fusionner ou supprimer les doublons") + + +# ---- FRESHNESS ----------------------------------------------------------- + +@check("freshness", "FRESH-staleness", "Entites sans etat perime (anciennete, non-cloture)") +def f_staleness(): + for nid in BY_TYPE.get("backlogItem", []): + note = NOTES[nid] + if labels_of(note).get("statut") == "en cours": + age = age_jours(note) + if age is not None and age > JOURS_STALE_ENCOURS: + signaler("FRESH-backlog", "JUGEMENT", nid, note.get("title", "?"), + "backlog 'en cours' inchange depuis %d j" % age, "verifier si toujours actif") + for nid in BY_TYPE.get("conversation", []): + note = NOTES[nid] + if labels_of(note).get("statut") == "en-cours": + age = age_jours(note) + if age is not None and age > JOURS_STALE_CONVO: + signaler("FRESH-conversation", "JUGEMENT", nid, note.get("title", "?"), + "conversation 'en-cours' non cloturee depuis %d j" % age, "cloturer si terminee") + + +# ========================================================================== +# 7. ORCHESTRATION + RAPPORT (groupes par dimension) +# ========================================================================== + +def executer(): + print("Chargement des notes...") + charger() + print("Execution des controles par dimension...") + ordre = {d: i for i, d in enumerate(DIMENSIONS)} + for chk in sorted(CHECKS, key=lambda c: ordre.get(c["dimension"], 99)): + _CTX["dimension"] = chk["dimension"] + try: + chk["fn"]() + except Exception as e: + print(" ! controle %s (%s) a echoue : %s" % (chk["code"], chk["dimension"], e)) + + +def rapport() -> str: + date = datetime.now().strftime("%Y-%m-%d %H:%M") + L = ["# RAPPORT LINT - Context Continuity - %s" % date, + "# ETAGE 1 (lecture seule) - groupe par data quality dimension", ""] + + # Inventaire + L.append("## Inventaire") + L.append("- Notes chargees : %d" % len(NOTES)) + for t in sorted(BY_TYPE): + if BY_TYPE[t]: + L.append(" - %s : %d" % (t, len(BY_TYPE[t]))) + L.append("") + + # Synthese + par_dim = {d: 0 for d in DIMENSIONS} + par_bac = {"AUTO": 0, "SIGNALER": 0, "JUGEMENT": 0} + for a in ANOMALIES: + par_dim[a["dimension"]] = par_dim.get(a["dimension"], 0) + 1 + par_bac[a["bac"]] = par_bac.get(a["bac"], 0) + 1 + L.append("## Synthese : %d anomalies" % len(ANOMALIES)) + L.append(" Par dimension : " + ", ".join("%s=%d" % (d, par_dim[d]) for d in DIMENSIONS)) + L.append(" Par bac d'action : " + ", ".join("%s=%d" % (b, par_bac[b]) for b in ("AUTO", "SIGNALER", "JUGEMENT"))) + L.append("") + + # Detail par dimension + for dim, definition in DIMENSIONS.items(): + anos = [a for a in ANOMALIES if a["dimension"] == dim] + actifs = [c for c in CHECKS if c["dimension"] == dim] + delegues = [c for c in CHECKS_DELEGUES if c["dimension"] == dim] + L.append("## %s (%d anomalie%s)" % (dim.upper(), len(anos), "s" if len(anos) != 1 else "")) + L.append(" %s" % definition) + L.append(" Controles deterministes : " + ", ".join(c["code"] for c in actifs)) + for d in delegues: + L.append(" [delegue agent] %s : %s" % (d["code"], d["libelle"])) + if anos: + L.append("") + for a in anos: + L.append(" - [%s | %s] %s (%s)" % (a["code"], a["bac"], a["titre"], a["note_id"])) + L.append(" %s" % a["detail"]) + if a["suggestion"]: + L.append(" -> %s" % a["suggestion"]) + L.append("") + return "\n".join(L) + + +def main(): + executer() + txt = rapport() + print("\n" + txt) + rep_dir = os.path.join(BASE, "lint_reports") + os.makedirs(rep_dir, exist_ok=True) + chemin = os.path.join(rep_dir, "lint_%s.txt" % datetime.now().strftime("%Y%m%d_%H%M%S")) + with open(chemin, "w", encoding="utf-8") as f: + f.write(txt) + print("\nRapport ecrit : %s" % chemin) + if len(ANOMALIES) > 100: + print("\n!!! ALERTE : %d anomalies (>100). Investiguer avant toute correction." % len(ANOMALIES)) + + +if __name__ == "__main__": + main() diff --git a/lint_reports/lint_20260703_083924.txt b/lint_reports/lint_20260703_083924.txt new file mode 100644 index 0000000..0d9cd4a --- /dev/null +++ b/lint_reports/lint_20260703_083924.txt @@ -0,0 +1,152 @@ +# RAPPORT LINT - Context Continuity - 2026-07-03 08:39 +# ETAGE 1 (lecture seule) - groupe par data quality dimension + +## Inventaire +- Notes chargees : 336 + - backlogItem : 96 + - composantLogiciel : 12 + - concept : 2 + - contexteReprise : 11 + - contrainte : 2 + - convention : 4 + - conversation : 29 + - decision : 43 + - documentation : 25 + - historiqueItem : 90 + - infrastructure : 1 + - outil : 2 + - principe : 3 + - projet : 2 + - service : 4 + - skill : 10 + +## Synthese : 32 anomalies + Par dimension : completeness=0, accuracy=2, consistency=0, validity=25, uniqueness=3, freshness=2 + Par bac d'action : AUTO=4, SIGNALER=7, JUGEMENT=21 + +## COMPLETENESS (0 anomalies) + Les champs obligatoires sont remplis ET les entites attendues sont toutes saisies (exhaustivite). + Controles deterministes : COMP-labels + [delegue agent] COMP-exhaustivite : Entites manquantes vs realite du projet (ex : backlog incomplet) + +## ACCURACY (2 anomalies) + Le niveau de la donnee reflete la realite (bon rangement, contenu conforme au type, statut a jour). + Controles deterministes : ACC-rangement, ACC-decision + [delegue agent] ACC-contenu-type : Le contenu redige d'une note reflete-t-il bien son type + + - [ACC-decision | JUGEMENT] (projet ContextContinuity) (f9qMQOFVyVCy) + 12 decisions actives sur ContextContinuity : possibles v1 non revisees + -> revue : tisser ~revise, passer les v1 obsoletes en revisee + - [ACC-decision | JUGEMENT] (projet SlidingAutomation) (XOtJDSeSE8Ny) + 23 decisions actives sur SlidingAutomation : possibles v1 non revisees + -> revue : tisser ~revise, passer les v1 obsoletes en revisee + +## CONSISTENCY (0 anomalies) + Coherence dans le temps et a travers les entites (vocabulaire courant, informations repetees a jour partout). + Controles deterministes : CONS-vocab + [delegue agent] CONS-chiffres-croises : Une information repetee (ex : nombre de tools) est-elle a jour dans toutes les notes + +## VALIDITY (25 anomalies) + Les valeurs et relations respectent les regles en vigueur (enums, conventions de nommage, ontologie des relations). + Controles deterministes : VAL-enum, VAL-nommage, VAL-relation + + - [VAL-enum | AUTO] Fix Python 3.9 : Union[X|Y] non supporté (LbUxsCkI5FQw) + typeHistorique='Contrainte découverte' (casse/accent) + -> corriger en 'Contrainte decouverte' + - [VAL-enum | AUTO] Fix polices : détection via assets/fonts/ (pas fc-list) (3GmGgfoK4aJI) + typeHistorique='Fait établi' (casse/accent) + -> corriger en 'Fait etabli' + - [VAL-enum | AUTO] Fix semantic key : self.theme['semantic'] (i5M20n936xmD) + typeHistorique='Fait établi' (casse/accent) + -> corriger en 'Fait etabli' + - [VAL-enum | AUTO] POST /etapi/notes inexistant sur TriliumNext 0.95 (SCYdo6cV20js) + typeHistorique='Contrainte découverte' (casse/accent) + -> corriger en 'Contrainte decouverte' + - [VAL-nommage | SIGNALER] Configurer l'accès SSH (port 2222) et tester le clone via SSH (VBTUmbai1IAE) + label projet='Code Versioning' contient un espace + -> renommer sans espace + repercuter partout + - [VAL-nommage | SIGNALER] Configurer le reverse proxy HTTPS pour forgejo.bertha-cloud.fr (oUFzjF3Ef0UU) + label projet='Code Versioning' contient un espace + -> renommer sans espace + repercuter partout + - [VAL-nommage | SIGNALER] Créer le premier dépôt de test et valider le workflow git (e5rBCaJHEP8a) + label projet='Code Versioning' contient un espace + -> renommer sans espace + repercuter partout + - [VAL-nommage | SIGNALER] Finaliser la configuration initiale de Forgejo (base de données, utilisateur admin, etc.) (VN7HUNymHGYn) + label projet='Code Versioning' contient un espace + -> renommer sans espace + repercuter partout + - [VAL-rel-portee | JUGEMENT] patch_pptx_theme (Eh2R3GGFdt1v) + ~dependDe : cible type=outil hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] render_engine.py (PMdUlYZqzgn6) + ~dependDe : cible type=outil hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Architecture pipeline : Narrator → Designer (ou Free Designer) → Encoder → render_engine_v2.py (LxsGi9O4wPKT) + ~impacte : cible type=ComposantLogiciel hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Design system v2 « PR Editorial » : 21 layouts + freeform, charte navy/coral/glacier, Cambria/Calibri (EWq0H6IhfcAG) + ~impacte : cible type=ComposantLogiciel hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Freeform : palier 3 (primitives + helpers charte), charte IMPOSÉE (tokens couleur uniquement, pas de hex) (bwcIY5LNiCI2) + ~impacte : cible type=ComposantLogiciel hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Refonte majeure v1→v2 du pipeline : moteur, design system et facilitator entièrement reconstruits (B6ZL5ZO7PjxG) + ~impacte : cible type=ComposantLogiciel hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] render_engine durci : org_chart et renderers tolèrent les nœuds en string OU dict (anti-crash Encoder) (Rnu3kQ178XeY) + ~impacte : cible type=ComposantLogiciel hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-domaine | JUGEMENT] Deck 35 slides (data-domain-pitch-deck) rendu sans crash après fix org_chart string/dict (32SqGgA5SzCV) + ~illustre : source type=historiqueItem hors domaine ['composantLogiciel', 'concept', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Deck 35 slides (data-domain-pitch-deck) rendu sans crash après fix org_chart string/dict (32SqGgA5SzCV) + ~illustre : cible type=ComposantLogiciel hors portee ['concept'] + -> verifier la pertinence + - [VAL-rel-domaine | JUGEMENT] Encoder laisse passer du Markdown brut (**gras**) dans les valeurs texte (visible slide 19) (sL21Grl1N62Q) + ~contraintPar : source type=historiqueItem hors domaine ['composantLogiciel', 'decision'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Encoder laisse passer du Markdown brut (**gras**) dans les valeurs texte (visible slide 19) (sL21Grl1N62Q) + ~contraintPar : cible type=composantLogiciel hors portee ['contrainte', 'principe'] + -> verifier la pertinence + - [VAL-rel-domaine | JUGEMENT] Mode collage /coller validé : les commandes dans un texte collé ne sont pas interprétées (E3DDmSw5bFsH) + ~illustre : source type=historiqueItem hors domaine ['composantLogiciel', 'concept', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Mode collage /coller validé : les commandes dans un texte collé ne sont pas interprétées (E3DDmSw5bFsH) + ~illustre : cible type=composantLogiciel hors portee ['concept'] + -> verifier la pertinence + - [VAL-rel-domaine | JUGEMENT] Persistance et rechargement exact du plan compact validés (anti-régression entre sessions) (EwfkXXLfVdyN) + ~illustre : source type=historiqueItem hors domaine ['composantLogiciel', 'concept', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Persistance et rechargement exact du plan compact validés (anti-régression entre sessions) (EwfkXXLfVdyN) + ~illustre : cible type=composantLogiciel hors portee ['concept'] + -> verifier la pertinence + - [VAL-rel-domaine | JUGEMENT] Forgejo (ArTsdwSCqXod) + ~dependDe : source type=service hors domaine ['composantLogiciel'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Forgejo (ArTsdwSCqXod) + ~dependDe : cible type=outil hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + +## UNIQUENESS (3 anomalies) + Absence de doublons. + Controles deterministes : UNIQ-doublon + + - [UNIQ-doublon | SIGNALER] installation réussie de forgejo (via gitea) en docker sur synology (nAxyy3POFpgr) + 2 notes identiques (type=historiqueItem projet=CodeVersioning) : ['nAxyy3POFpgr', 'e2TZu6UWdVcO'] + -> fusionner ou supprimer les doublons + - [UNIQ-doublon | SIGNALER] post /etapi/notes inexistant sur triliumnext 0.95 (SCYdo6cV20js) + 2 notes identiques (type=historiqueItem projet=SlidingAutomation) : ['SCYdo6cV20js', 'PkKQcJZUOSFk'] + -> fusionner ou supprimer les doublons + - [UNIQ-doublon | SIGNALER] synoinfo.conf unique doit rester synology_rtd1296_ds220j (B4cW13Ell9El) + 2 notes identiques (type=historiqueItem projet=SlidingAutomation) : ['B4cW13Ell9El', 'KCecvxr2KUNG'] + -> fusionner ou supprimer les doublons + +## FRESHNESS (2 anomalies) + Les entites ne portent pas un etat perime (anciennete anormale, non-cloture). + Controles deterministes : FRESH-staleness + + - [FRESH-conversation | JUGEMENT] [Claude Sonnet 4.6] 2026-06-01 22:35 — Session 2026-06-01 - Slide 1 cover_split + Trilium install (DbgPmpROLCDR) + conversation 'en-cours' non cloturee depuis 31 j + -> cloturer si terminee + - [FRESH-conversation | JUGEMENT] [Le Chat Large] 2026-05-30 22:30 — Reprise post-doc et style (zJZwyQbOMORY) + conversation 'en-cours' non cloturee depuis 33 j + -> cloturer si terminee diff --git a/lint_reports/lint_20260703_130356.txt b/lint_reports/lint_20260703_130356.txt new file mode 100644 index 0000000..2f0890d --- /dev/null +++ b/lint_reports/lint_20260703_130356.txt @@ -0,0 +1,145 @@ +# RAPPORT LINT - Context Continuity - 2026-07-03 13:03 +# ETAGE 1 (lecture seule) - groupe par data quality dimension + +## Inventaire +- Notes chargees : 348 + - ComposantLogiciel : 1 + - backlogItem : 99 + - composantLogiciel : 11 + - concept : 2 + - container : 8 + - contexteReprise : 11 + - contrainte : 2 + - convention : 4 + - conversation : 29 + - decision : 44 + - documentation : 25 + - historiqueItem : 90 + - infrastructure : 1 + - outil : 2 + - principe : 3 + - projet : 2 + - service : 4 + - skill : 10 + +## Synthese : 29 anomalies + Par dimension : completeness=0, accuracy=2, consistency=0, validity=22, uniqueness=3, freshness=2 + Par bac d'action : AUTO=5, SIGNALER=7, JUGEMENT=17 + +## COMPLETENESS (0 anomalies) + Les champs obligatoires sont remplis ET les entites attendues sont toutes saisies (exhaustivite). + Controles deterministes : COMP-labels + [delegue agent] COMP-exhaustivite : Entites manquantes vs realite du projet (ex : backlog incomplet) + +## ACCURACY (2 anomalies) + Le niveau de la donnee reflete la realite (bon rangement, contenu conforme au type, statut a jour). + Controles deterministes : ACC-rangement, ACC-decision + [delegue agent] ACC-contenu-type : Le contenu redige d'une note reflete-t-il bien son type + + - [ACC-decision | JUGEMENT] (projet ContextContinuity) (f9qMQOFVyVCy) + 13 decisions actives sur ContextContinuity : possibles v1 non revisees + -> revue : tisser ~revise, passer les v1 obsoletes en revisee + - [ACC-decision | JUGEMENT] (projet SlidingAutomation) (XOtJDSeSE8Ny) + 23 decisions actives sur SlidingAutomation : possibles v1 non revisees + -> revue : tisser ~revise, passer les v1 obsoletes en revisee + +## CONSISTENCY (0 anomalies) + Coherence dans le temps et a travers les entites (vocabulaire courant, informations repetees a jour partout). + Controles deterministes : CONS-vocab + [delegue agent] CONS-chiffres-croises : Une information repetee (ex : nombre de tools) est-elle a jour dans toutes les notes + +## VALIDITY (22 anomalies) + Les valeurs et relations respectent les regles en vigueur (enums, conventions de nommage, ontologie des relations). + Controles deterministes : VAL-enum, VAL-type, VAL-nommage, VAL-relation + + - [VAL-enum | AUTO] Fix Python 3.9 : Union[X|Y] non supporté (LbUxsCkI5FQw) + typeHistorique='Contrainte découverte' (casse/accent) + -> corriger en 'Contrainte decouverte' + - [VAL-enum | AUTO] Fix polices : détection via assets/fonts/ (pas fc-list) (3GmGgfoK4aJI) + typeHistorique='Fait établi' (casse/accent) + -> corriger en 'Fait etabli' + - [VAL-enum | AUTO] Fix semantic key : self.theme['semantic'] (i5M20n936xmD) + typeHistorique='Fait établi' (casse/accent) + -> corriger en 'Fait etabli' + - [VAL-enum | AUTO] POST /etapi/notes inexistant sur TriliumNext 0.95 (SCYdo6cV20js) + typeHistorique='Contrainte découverte' (casse/accent) + -> corriger en 'Contrainte decouverte' + - [VAL-type | AUTO] render_engine.py (mKmOPjBQawu4) + type='ComposantLogiciel' (casse) + -> corriger en 'composantLogiciel' + - [VAL-nommage | SIGNALER] Configurer l'accès SSH (port 2222) et tester le clone via SSH (VBTUmbai1IAE) + label projet='Code Versioning' contient un espace + -> renommer sans espace + repercuter partout + - [VAL-nommage | SIGNALER] Configurer le reverse proxy HTTPS pour forgejo.bertha-cloud.fr (oUFzjF3Ef0UU) + label projet='Code Versioning' contient un espace + -> renommer sans espace + repercuter partout + - [VAL-nommage | SIGNALER] Créer le premier dépôt de test et valider le workflow git (e5rBCaJHEP8a) + label projet='Code Versioning' contient un espace + -> renommer sans espace + repercuter partout + - [VAL-nommage | SIGNALER] Finaliser la configuration initiale de Forgejo (base de données, utilisateur admin, etc.) (VN7HUNymHGYn) + label projet='Code Versioning' contient un espace + -> renommer sans espace + repercuter partout + - [VAL-rel-portee | JUGEMENT] Architecture pipeline : Narrator → Designer (ou Free Designer) → Encoder → render_engine_v2.py (LxsGi9O4wPKT) + ~impacte : cible type=ComposantLogiciel hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Design system v2 « PR Editorial » : 21 layouts + freeform, charte navy/coral/glacier, Cambria/Calibri (EWq0H6IhfcAG) + ~impacte : cible type=ComposantLogiciel hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Freeform : palier 3 (primitives + helpers charte), charte IMPOSÉE (tokens couleur uniquement, pas de hex) (bwcIY5LNiCI2) + ~impacte : cible type=ComposantLogiciel hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Refonte majeure v1→v2 du pipeline : moteur, design system et facilitator entièrement reconstruits (B6ZL5ZO7PjxG) + ~impacte : cible type=ComposantLogiciel hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] render_engine durci : org_chart et renderers tolèrent les nœuds en string OU dict (anti-crash Encoder) (Rnu3kQ178XeY) + ~impacte : cible type=ComposantLogiciel hors portee ['composantLogiciel', 'service'] + -> verifier la pertinence + - [VAL-rel-domaine | JUGEMENT] Deck 35 slides (data-domain-pitch-deck) rendu sans crash après fix org_chart string/dict (32SqGgA5SzCV) + ~illustre : source type=historiqueItem hors domaine ['composantLogiciel', 'concept', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Deck 35 slides (data-domain-pitch-deck) rendu sans crash après fix org_chart string/dict (32SqGgA5SzCV) + ~illustre : cible type=ComposantLogiciel hors portee ['concept'] + -> verifier la pertinence + - [VAL-rel-domaine | JUGEMENT] Encoder laisse passer du Markdown brut (**gras**) dans les valeurs texte (visible slide 19) (sL21Grl1N62Q) + ~contraintPar : source type=historiqueItem hors domaine ['composantLogiciel', 'decision'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Encoder laisse passer du Markdown brut (**gras**) dans les valeurs texte (visible slide 19) (sL21Grl1N62Q) + ~contraintPar : cible type=composantLogiciel hors portee ['contrainte', 'principe'] + -> verifier la pertinence + - [VAL-rel-domaine | JUGEMENT] Mode collage /coller validé : les commandes dans un texte collé ne sont pas interprétées (E3DDmSw5bFsH) + ~illustre : source type=historiqueItem hors domaine ['composantLogiciel', 'concept', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Mode collage /coller validé : les commandes dans un texte collé ne sont pas interprétées (E3DDmSw5bFsH) + ~illustre : cible type=composantLogiciel hors portee ['concept'] + -> verifier la pertinence + - [VAL-rel-domaine | JUGEMENT] Persistance et rechargement exact du plan compact validés (anti-régression entre sessions) (EwfkXXLfVdyN) + ~illustre : source type=historiqueItem hors domaine ['composantLogiciel', 'concept', 'service'] + -> verifier la pertinence + - [VAL-rel-portee | JUGEMENT] Persistance et rechargement exact du plan compact validés (anti-régression entre sessions) (EwfkXXLfVdyN) + ~illustre : cible type=composantLogiciel hors portee ['concept'] + -> verifier la pertinence + +## UNIQUENESS (3 anomalies) + Absence de doublons. + Controles deterministes : UNIQ-doublon + + - [UNIQ-doublon | SIGNALER] installation réussie de forgejo (via gitea) en docker sur synology (nAxyy3POFpgr) + 2 notes identiques (type=historiqueItem projet=CodeVersioning) : ['nAxyy3POFpgr', 'e2TZu6UWdVcO'] + -> fusionner ou supprimer les doublons + - [UNIQ-doublon | SIGNALER] post /etapi/notes inexistant sur triliumnext 0.95 (SCYdo6cV20js) + 2 notes identiques (type=historiqueItem projet=SlidingAutomation) : ['SCYdo6cV20js', 'PkKQcJZUOSFk'] + -> fusionner ou supprimer les doublons + - [UNIQ-doublon | SIGNALER] synoinfo.conf unique doit rester synology_rtd1296_ds220j (B4cW13Ell9El) + 2 notes identiques (type=historiqueItem projet=SlidingAutomation) : ['B4cW13Ell9El', 'KCecvxr2KUNG'] + -> fusionner ou supprimer les doublons + +## FRESHNESS (2 anomalies) + Les entites ne portent pas un etat perime (anciennete anormale, non-cloture). + Controles deterministes : FRESH-staleness + + - [FRESH-conversation | JUGEMENT] [Claude Sonnet 4.6] 2026-06-01 22:35 — Session 2026-06-01 - Slide 1 cover_split + Trilium install (DbgPmpROLCDR) + conversation 'en-cours' non cloturee depuis 31 j + -> cloturer si terminee + - [FRESH-conversation | JUGEMENT] [Le Chat Large] 2026-05-30 22:30 — Reprise post-doc et style (zJZwyQbOMORY) + conversation 'en-cours' non cloturee depuis 33 j + -> cloturer si terminee diff --git a/mcp_server.py b/mcp_server.py index 1355f7a..526406e 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -504,14 +504,27 @@ def tool_create_entite(type, projet, titre, contenu=None, definition=None): via trilium_ids.json. Garde-fou anti-doublon sur le titre dans le dossier cible. La permission projet est comportementale (le tool accepte tout projet).""" import json, os - from trilium_api import create_note, get_note_id, set_label, find_note_by_title + from trilium_api import create_note, get_note_id, set_label, find_note_by_title, TYPES_CANONIQUES + # Routage dossier : uniquement la couche connaissance/technique. + # Les types de pilotage (backlogItem, decision...) ont leurs tools dedies. MAPPING = { "concept": "Concepts", "composantLogiciel": "EnvironnementTechnique", "service": "EnvironnementTechnique", + "infrastructure": "EnvironnementTechnique", + "outil": "EnvironnementTechnique", + "contrainte": "EnvironnementTechnique", + "principe": "EnvironnementTechnique", + "convention": "EnvironnementTechnique", + "methode": "Methodes", + "version": "EnvironnementTechnique", } + # 1. Validite du type (source unique : ontologie via TYPES_CANONIQUES) + if type not in TYPES_CANONIQUES: + return {"error": "Type inconnu : %s. Types canoniques : %s" % (type, ", ".join(TYPES_CANONIQUES))} + # 2. Ce tool ne cree que la couche connaissance/technique if type not in MAPPING: - return {"error": "Type non supporte : %s. Types valides : %s" % (type, ", ".join(MAPPING.keys()))} + return {"error": "Type '%s' valide mais non creable ici (type de pilotage : utiliser son tool dedie add_*)." % type} with open(os.path.expanduser("~/App/Context_continuity/trilium_ids.json")) as f: ids = json.load(f) dossier_cle = MAPPING[type] diff --git a/trilium_api.py b/trilium_api.py index 69d673a..d73b43e 100644 --- a/trilium_api.py +++ b/trilium_api.py @@ -191,6 +191,18 @@ RELATIONS_ONTOLOGIE = [ "reference", "illustre", "provient", ] +# Types d'entites canoniques de l'ontologie (source unique cote code). +# NE contient PAS 'container' (= dossiers structurels, hors couche connaissance). +# Cible a terme : lire cette liste depuis les notes d'ontologie (dossier +# Modeles & Ontologie) plutot que la maintenir ici. En attendant, source unique +# partagee par le Lint (VAL-type) et create_entite. +TYPES_CANONIQUES = [ + "projet", "conversation", "backlogItem", "decision", "historiqueItem", + "concept", "contexteReprise", "skill", "documentation", + "composantLogiciel", "service", "infrastructure", "outil", + "contrainte", "principe", "convention", "methode", "version", +] + def add_relation_safe(source_id, nom, cible_id): """Cree une relation (object property) entre deux notes. Garde-fou Option B : nom dans l ontologie + source/cible existent.