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.
This commit is contained in:
2026-07-03 13:05:27 +02:00
parent f883af4dcc
commit ced9b37d4a
5 changed files with 836 additions and 2 deletions
+512
View File
@@ -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()
+152
View File
@@ -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
+145
View File
@@ -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
+15 -2
View File
@@ -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. 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).""" La permission projet est comportementale (le tool accepte tout projet)."""
import json, os 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 = { MAPPING = {
"concept": "Concepts", "concept": "Concepts",
"composantLogiciel": "EnvironnementTechnique", "composantLogiciel": "EnvironnementTechnique",
"service": "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: 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: with open(os.path.expanduser("~/App/Context_continuity/trilium_ids.json")) as f:
ids = json.load(f) ids = json.load(f)
dossier_cle = MAPPING[type] dossier_cle = MAPPING[type]
+12
View File
@@ -191,6 +191,18 @@ RELATIONS_ONTOLOGIE = [
"reference", "illustre", "provient", "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): def add_relation_safe(source_id, nom, cible_id):
"""Cree une relation (object property) entre deux notes. """Cree une relation (object property) entre deux notes.
Garde-fou Option B : nom dans l ontologie + source/cible existent. Garde-fou Option B : nom dans l ontologie + source/cible existent.