#!/usr/bin/env python3 """Le GUI sait-il editer tout ce que les plans reels contiennent ? Principe 10 : « un operateur humain exploite Set-OPS sans IA — la doc, `make` et le GUI suffisent ». Ce principe se dement en silence des qu'un registre accepte un champ que l'interface ne sait pas ecrire : l'operateur doit editer le YAML a la main, et personne ne s'en apercoit avant de buter dessus. Ce script rend le trou VISIBLE et mesurable. Il croise : - les champs REELLEMENT presents dans les plans (instance courante + tous les modeles decouverts par scripts/modeles.py) ; - `CHAMPS_ECRITS_PAR_GUI` de scripts/inventory_gui.py, qui declare ce que les fonctions de sauvegarde ecrivent. Deux sens de verification : 1. champ dans un plan, absent du GUI -> TROU d'interface (signale) ; 2. champ declare au GUI, introuvable dans son source -> DERIVE de la declaration (signale) — le tableau ment sur ce que le GUI fait. Usage : python3 scripts/couverture_gui.py lister python3 scripts/couverture_gui.py verifier # rc=2 s'il reste un trou python3 scripts/couverture_gui.py verifier --tolerer nomenclature """ from __future__ import annotations import argparse import os import sys from pathlib import Path import yaml from inventory_gui import CHAMPS_ECRITS_PAR_GUI from modeles import decouvrir as decouvrir_modeles RACINE = Path(__file__).resolve().parents[1] INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance")) SOURCE_GUI = RACINE / "scripts" / "inventory_gui.py" # (fichier du plan, cle racine, nom du registre) — les registres tables-de-tables. REGISTRES = [ ("serveurs.yml", "serveurs", "serveurs"), ("applications.yml", "applications", "applications"), ("bases-donnees.yml", "bases_donnees", "bases_donnees"), ("bases-donnees.yml", "serveurs_bd", "serveurs_bd"), ("domaines.yml", "domaines_publics", "domaines_publics"), ("nomenclature.yml", "fonctions", "nomenclature"), ] def _plans() -> list[Path]: """Dossiers plan/ a inspecter : l'instance courante + tous les modeles.""" trouves = [] if (INSTANCE / "plan").is_dir(): trouves.append(INSTANCE / "plan") trouves += [m / "plan" for m in decouvrir_modeles() if (m / "plan").is_dir()] return trouves def champs_utilises() -> dict[str, dict[str, set[str]]]: """{registre: {champ: {origines}}} — champs vus dans les plans reels.""" vus: dict[str, dict[str, set[str]]] = {r: {} for _, _, r in REGISTRES} for plan in _plans(): etiquette = plan.parent.name for fichier, racine_cle, registre in REGISTRES: p = plan / fichier if not p.is_file(): continue try: data = yaml.safe_load(p.read_text(encoding="utf-8")) or {} except yaml.YAMLError: continue entrees = (data or {}).get(racine_cle) or {} if not isinstance(entrees, dict): continue for entree in entrees.values(): if isinstance(entree, dict): for champ in entree: vus[registre].setdefault(str(champ), set()).add(etiquette) return vus def declaration_derive() -> list[str]: """Champs declares au GUI mais introuvables dans son source (declaration qui ment).""" src = SOURCE_GUI.read_text(encoding="utf-8") # On coupe le tableau lui-meme : sa presence ne prouve rien. debut = src.find("CHAMPS_ECRITS_PAR_GUI") fin = src.find("\n}\n", debut) ailleurs = src[:debut] + src[fin:] if debut != -1 and fin != -1 else src absents = [] for registre, champs in CHAMPS_ECRITS_PAR_GUI.items(): for champ in sorted(champs): if champ not in ailleurs: absents.append(f"{registre}.{champ}") return absents def main() -> int: parser = argparse.ArgumentParser(description="Couverture du schema du plan par le GUI.") sub = parser.add_subparsers(dest="commande", required=True) sub.add_parser("lister", help="Detaille champ par champ.") pv = sub.add_parser("verifier", help="Echoue s'il reste un champ non editable.") pv.add_argument("--tolerer", action="append", default=[], metavar="REGISTRE", help="Registre dont les trous sont connus et assumes (repetable).") args = parser.parse_args() utilises = champs_utilises() if args.commande == "lister": for registre in sorted(utilises): couverts = CHAMPS_ECRITS_PAR_GUI.get(registre, set()) print(f"\n== {registre} ==") for champ in sorted(utilises[registre]): etat = "GUI" if champ in couverts else "-- hors GUI --" print(f" {champ:<18} {etat:<16} vu dans : " f"{', '.join(sorted(utilises[registre][champ]))}") return 0 tolere = set(args.tolerer) trous: list[str] = [] for registre, champs in utilises.items(): if registre in tolere: continue couverts = CHAMPS_ECRITS_PAR_GUI.get(registre, set()) for champ in sorted(set(champs) - couverts): trous.append(f"{registre}.{champ} (vu dans : " f"{', '.join(sorted(champs[champ]))})") derive = declaration_derive() if trous: print(f"erreur: {len(trous)} champ(s) present(s) dans un plan reel que le GUI " f"ne sait pas ecrire :", file=sys.stderr) for t in trous: print(f" - {t}", file=sys.stderr) if derive: print(f"erreur: {len(derive)} champ(s) declare(s) dans CHAMPS_ECRITS_PAR_GUI mais " f"introuvable(s) dans le source du GUI : {', '.join(derive)}", file=sys.stderr) if trous or derive: return 2 total = sum(len(c) for c in utilises.values()) print(f"GUI : les {total} champ(s) des plans reels sont editables " f"({len(_plans())} plan(s) inspecte(s))" + (f", registres toleres : {', '.join(sorted(tolere))}." if tolere else ".")) return 0 if __name__ == "__main__": raise SystemExit(main())