cd1694229f
Affinage revele par le traitement des anomalies : un historiqueItem (ex: la panne du 12/07) ou une decision documente par un skill est une relation legitime, pas un mesusage. La regle etait trop stricte. A terme, ces regles domaine-portee doivent vivre dans un skill canonique lu par les agents avant de tisser (backlog VPBRcJmPxkUC), pas seulement dans le Lint.
531 lines
22 KiB
Python
531 lines
22 KiB
Python
#!/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, projets_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", "historiqueItem", "decision"}, {"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", "Label projet appartient au referentiel (notes de type projet)")
|
|
def v_nommage():
|
|
canon = projets_canoniques()
|
|
if not canon:
|
|
return # referentiel illisible : ne pas produire de faux positifs
|
|
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("-", "")
|
|
for nid, note in NOTES.items():
|
|
val = labels_of(note).get("projet")
|
|
if not val or val in canon:
|
|
continue
|
|
# Chercher un canonique proche (faute de casse/espace/accent) -> AUTO
|
|
cible = None
|
|
for cand in canon:
|
|
if norm(cand) == norm(val):
|
|
cible = cand
|
|
break
|
|
if cible is not None:
|
|
signaler("VAL-nommage", "AUTO", nid, note.get("title", "?"),
|
|
"projet=%r non canonique" % val, "corriger en %r" % cible)
|
|
else:
|
|
signaler("VAL-nommage", "SIGNALER", nid, note.get("title", "?"),
|
|
"projet=%r hors referentiel" % val,
|
|
"corriger ou creer la note de type projet correspondante")
|
|
|
|
|
|
@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()
|