diff --git a/CHANGELOG.md b/CHANGELOG.md index f3dbeac..9a18d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # CHANGELOG — Set-OPS +## 2026-07-23 + +### Changé — l'adressage se dérive du seul seed `index` (RUPTURE, mode compact retiré) +Principe posé par l'utilisateur : les valeurs de configuration doivent se dériver des +intrants, pas se réécrire à la main. La nomenclature dupliquait ce que `index` détermine +déjà (supernet, sous-réseaux, passerelles, VLAN). C'est corrigé, en rupture nette. + +- **`inventory_rules`** : nouvelles fonctions de dérivation, **source unique** — + `supernet_de`, `base3_de`, `sous_reseau_de`, `passerelle_de`, `vlan_de`. Le modèle à + 6 zones est encodé une fois : 2ᵉ octet = `10+index`, 3ᵉ octet de zone = `15+catégorie`, + VLAN trunk = `1000+index×10+zone`. `deriver_nomenclature` ne lit plus **aucun** adressage + stocké ; le mode `compact` est supprimé (tout est ip-miroir dérivé). +- **`devis_reseau`** importe ces helpers (plus de duplication) ; découverte des tenants sur + `index` présent (le filtre `vmid_schema` disparaît). +- **Les 10 nomenclatures** (3 instances + 7 modèles) passent au **format maigre** : `index`, + `cidr_hote`, `reservations`, libellés de zones et `fonctions` seulement. Supprimés : + `supernet`, `vmid_schema`, et par zone `sous_reseau`/`passerelle`/`vlan`. `presence-web`, + encore en `compact`, gagne `index: 1`. +- **GUI** : `index` devient un **intrant** (section « Réseau » du panneau Intrants). Il vit + dans la nomenclature (le plan réseau, uniforme partout — contrairement aux intrants des + modèles, hétérogènes) et le GUI l'écrit **chirurgicalement** (une ligne, sans reformater + le fichier). Le miroir JS dérive le VLAN du seed (fin de la lecture de `c.vlan` stocké). + +### Ajouté +- **Preuve P20** (`preuve_nomenclature_derivee`) : aucune nomenclature ne stocke d'adressage + — garde-fou permanent contre une rechute vers l'écriture manuelle. Testée en négatif + (une rechute simulée est bien rejetée). + +Validé : **DIFF VIDE sur les 3 instances** (la dérivation reproduit exactement l'adressage +qui était stocké), les **7 modèles valident**, `devis_reseau` génère les mêmes VLAN +(1011-1016 dérivés), `make verifier` rc=0 **CONFORME 20/20**, `node --check` du GUI OK, +aller-retour d'écriture de `index` : une seule ligne touchée. + ## 2026-07-22 (suite) ### Ajouté — trois preuves qui ferment les angles morts du harnais diff --git a/docs/audit/README.md b/docs/audit/README.md index d5b7a3a..01bfea2 100644 --- a/docs/audit/README.md +++ b/docs/audit/README.md @@ -56,7 +56,8 @@ make prouver | P16 | Inventaire Ansible (voûte) | `ansible-inventory --list` — sauté sans mot de passe Vault. | | P17 | **Tous** les modèles d'instance | chaque modèle découvert valide (pas seulement `socle`). `SETOPS_MODELES=../Set-OPS-Modeles` inclut les modèles assemblés privés. | | P18 | Gabarit de voûte complet | `vault.yml.example` couvre **exactement** les secrets que le plan exige (rôles actifs + bases + group_vars). | -| P19 | GUI couvre le plan | tout champ présent dans un plan réel est éditable par le GUI (nomenclature tolérée : trou connu). | +| P19 | GUI couvre le plan | tout champ présent dans un plan réel est éditable par le GUI (nomenclature tolérée : le seed `index` est désormais un intrant ; l'adressage est dérivé, donc rien à éditer). | +| P20 | Adressage dérivé du seed | aucune nomenclature ne **stocke** d'adressage (supernet, sous-réseau, passerelle, VLAN) : tout se dérive du seul `index`. Attrape toute rechute vers l'écriture manuelle. | > **P17, P18, P19 ferment les angles morts du harnais** : il ne vérifiait qu'*une* instance > et le seul modèle `socle`. P17 aurait attrapé l'hôte fantôme d'`integral` ; P18, les neuf diff --git a/docs/audit/preuve-2026-07-23.md b/docs/audit/preuve-2026-07-23.md new file mode 100644 index 0000000..76c31da --- /dev/null +++ b/docs/audit/preuve-2026-07-23.md @@ -0,0 +1,56 @@ +# Preuve de conformite — Set-OPS — 2026-07-23 + +> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer +> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ; +> aucune validation n'est reimplementee ici. Voir le mode d'emploi : +> [`docs/audit/README.md`](README.md), et le registre trace : +> [`docs/audit/affirmations.md`](affirmations.md). + +- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml` +- **Verdict** : ✅ CONFORME (20 OK · 0 echec · 0 saute) + +## Preuves + +| # | Preuve | Affirmations | Statut | Detail | +|---|---|---|---|---| +| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | | +| P02 | Tests unitaires (inventory_host) | — | ✅ OK | 4 tests passes. | +| P03 | Diff-vide du plan (inventaire genere) | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible. | +| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | | +| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | | +| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | +| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). | +| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 30 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 29 rôles, 63 flux, schéma + matrice OK. | +| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | +| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | playbook: playbooks/proxmox/cloner_vm_debian.yml | +| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | +| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. | +| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. | +| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. | +| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 31 groupes (inventaire dechiffre et parse). | +| P17 | Tous les modeles d'instance valident | AFF-022, AFF-099 | ✅ OK | Les 7 modele(s) decouvert(s) valident. | +| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. | +| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 27 champ(s) des plans reels sont editables (8 plan(s) inspecte(s)), registres toleres : nomenclature. | +| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 8 nomenclature(s) : adressage 100% derive du seed index. | + +## Couverture des affirmations ✅ du registre + +Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus. +Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005 +`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite +d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ; +elles restent hors du harnais recurrent (rien d'executable a rejouer). + +## Declarations d'intention (⚪ invérifiables localement — assumees) + +Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees** +comme declarations d'intention, non comme preuves : + +- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement + contre une flotte vivante. +- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles. +- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee. +- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume. + +_Rapport genere le 2026-07-23._ diff --git a/exemples/modeles/socle/plan/nomenclature.yml b/exemples/modeles/socle/plan/nomenclature.yml index db4f25e..3ebef10 100644 --- a/exemples/modeles/socle/plan/nomenclature.yml +++ b/exemples/modeles/socle/plan/nomenclature.yml @@ -1,17 +1,22 @@ --- -# MODÈLE « socle » — infrastructure de base (4 VM) : edge, DNS, PKI, magasin mail. -# Zones/VLAN alignés sur integral (un socle peut grandir vers un écosystème complet). -# index : ⚠️ REMPLACER par l'index unique du tenant (préfixe VMID + supernet 10.(10+index)). +# Nomenclature = MODELE de l'ecosysteme (zones + placement des fonctions). +# L'ADRESSAGE (supernet, sous-reseaux, passerelles, VLAN, VMID) N'EST PAS ecrit ici : +# il se DERIVE du seul seed `index` (scripts/inventory_rules : supernet_de, base3_de, +# passerelle_de, vlan_de). Modele a 6 zones : 10.(10+index).(15+zone).0/24, VLAN +# 1000+index*10+zone. Editer `index` via le panneau Intrants du GUI. index: 1 -supernet: 10.11.0.0/16 -vmid_schema: ip-miroir cidr_hote: 24 -reservations: { passerelle: 1, reserve_min: 2, reserve_max: 9 } +reservations: + passerelle: 1 + reserve_min: 2 + reserve_max: 9 +# Zones de securite (un /24 + VLAN chacune) — seuls les libelles sont du modele. categories: - 1: { libelle: Frontiere, vlan: 16, sous_reseau: 10.11.16.0/24, passerelle: 10.11.16.1 } - 4: { libelle: Services-infra, vlan: 19, sous_reseau: 10.11.19.0/24, passerelle: 10.11.19.1 } + 1: { libelle: Frontiere } + 4: { libelle: Services-infra } +# Placement : quelle fonction dans quelle zone (categorie) et son rang (service). fonctions: - infra-edge: { categorie: 1, service: 1 } infra-dns: { categorie: 4, service: 1 } - infra-pki: { categorie: 4, service: 2 } + infra-edge: { categorie: 1, service: 1 } infra-mail: { categorie: 4, service: 3 } + infra-pki: { categorie: 4, service: 2 } diff --git a/scripts/devis_reseau.py b/scripts/devis_reseau.py index 8808d44..079ece2 100644 --- a/scripts/devis_reseau.py +++ b/scripts/devis_reseau.py @@ -20,6 +20,10 @@ import yaml RACINE = Path(__file__).resolve().parent.parent DOSSIER_INSTANCES = RACINE.parent +# Adressage et VLAN : derives du seul index, source unique dans inventory_rules. +sys.path.insert(0, str(RACINE / "scripts")) +from inventory_rules import passerelle_de, supernet_de, vlan_de # noqa: E402 + def masque(cidr: int) -> str: return str(ipaddress.IPv4Network(f"0.0.0.0/{cidr}").netmask) @@ -40,17 +44,14 @@ def decouvrir() -> list[tuple[str, str, dict]]: tenants = [] for chemin in sorted(DOSSIER_INSTANCES.glob("*/plan/nomenclature.yml")): n = yaml.safe_load(chemin.read_text(encoding="utf-8")) or {} - if n.get("vmid_schema") == "ip-miroir" and n.get("index") is not None: + # Un tenant federe = une nomenclature avec un index (l'adressage en decoule). + if n.get("index") is not None and n.get("categories"): nom = chemin.parent.parent.name tenants.append((nom, prefixe(nom), n)) tenants.sort(key=lambda t: t[2]["index"]) return tenants -def vlan_de(index: int, zone: int) -> int: - return 1000 + index * 10 + zone - - def generer(tenants: list[tuple[str, str, dict]]) -> str: out = ["configure terminal", "!"] out += [ @@ -75,19 +76,19 @@ def generer(tenants: list[tuple[str, str, dict]]) -> str: c = n["categories"][zone] out.append(f"interface Vlan{vlan_de(n['index'], zone)}") out.append(f" description {nom}-{c['libelle']}") - out.append(f" ip address {c['passerelle']} {m}") + out.append(f" ip address {passerelle_de(n['index'], zone)} {m}") out.append(f" ip access-group {pfx}-ISOLATION in") out.append(" no shutdown") out += ["!", "! ----- 3. ACL d'isolation tenant (default-deny inter-tenant) -----"] for nom, pfx, n in tenants: - reseau, wild = reseau_wildcard(n["supernet"]) + reseau, wild = reseau_wildcard(supernet_de(n["index"])) out.append(f"ip access-list extended {pfx}-ISOLATION") out.append(f" remark Intra-tenant {nom} : routage local autorise") out.append(f" permit ip {reseau} {wild} {reseau} {wild}") for autre_nom, _, autre in tenants: if autre_nom == nom: continue - a_reseau, a_wild = reseau_wildcard(autre["supernet"]) + a_reseau, a_wild = reseau_wildcard(supernet_de(autre["index"])) out.append(f" remark Bloquer le tenant {autre_nom}") out.append(f" deny ip {reseau} {wild} {a_reseau} {a_wild}") out.append(" remark Reste (Internet / inter-tenant controle) -> passerelle OPNsense") diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 39afa71..3fd62dd 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -82,10 +82,15 @@ FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml" # n'y entrent jamais. INTRANTS_IDENTITE = INVENTAIRE_DEFAUT.parent / "group_vars/all/10-intrants.yml" INTRANTS_PROXMOX = INVENTAIRE_MODELE.parent / "group_vars/proxmox.yml" -FICHIERS_INTRANTS = {"identite": INTRANTS_IDENTITE, "proxmox": INTRANTS_PROXMOX} +# Le seed reseau (`index`) vit dans la nomenclature (le plan reseau) — d'ou tout +# l'adressage se derive. Le panneau Intrants le lit/ecrit la, chirurgicalement, sans +# reformater le fichier (cf. _ecrire_index_nomenclature). +FICHIERS_INTRANTS = {"identite": INTRANTS_IDENTITE, "proxmox": INTRANTS_PROXMOX, + "reseau": FICHIER_NOMENCLATURE} # (variable, fichier, classe, section, libelle, type) INTRANTS_SCHEMA = [ + ("index", "reseau", "constante", "Réseau", "Index d'instance dans la fédération (seed : dérive supernet 10.(10+index), VLAN 1000+index×10+zone, VMID)", "int"), ("domaine_interne", "identite", "constante", "Identité", "Domaine DNS interne", "str"), ("fuseau_horaire", "identite", "defaut", "Identité", "Fuseau horaire", "str"), ("organisation", "identite", "defaut", "Identité", "Organisation (annuaire LDAP, certificats)", "str"), @@ -498,6 +503,22 @@ def ecrire_intrants(payload: dict) -> None: INTRANTS_PROXMOX, fusion, "# Parametres Proxmox non sensibles (les secrets vont dans proxmox.vault.yml).\n" "# Cles d'intrants editees par le panneau « Intrants de base » du GUI.\n---\n") + if "reseau" in par_fichier and "index" in par_fichier["reseau"]: + _ecrire_index_nomenclature(int(par_fichier["reseau"]["index"])) + + +def _ecrire_index_nomenclature(index: int) -> None: + """Met a jour `index:` dans plan/nomenclature.yml SANS reformater le reste. + + Le fichier garde ses commentaires et son formatage compact (l'adressage se derive + de ce seul seed). Remplacement de la ligne existante, ou insertion en tete a defaut. + """ + path = FICHIER_NOMENCLATURE + texte = path.read_text(encoding="utf-8") if path.exists() else "---\n" + nouveau, remplace = re.subn(r"(?m)^index:.*$", f"index: {index}", texte) + if not remplace: + nouveau = re.sub(r"(?m)^(---\n)", rf"\g<1>index: {index}\n", texte, count=1) or texte + path.write_text(nouveau, encoding="utf-8") def ecrire_bases(path: Path, registre: dict) -> None: @@ -1952,7 +1973,10 @@ HTML = r""" if (!d) return null; const c = (nomenclature.categories || {})[d.categorie]; if (!c) return null; - return {categorie: d.categorie, service: d.service, libelle: c.libelle, vlan: c.vlan, sous_reseau: c.sous_reseau, passerelle: c.passerelle}; + // VLAN derive du seed index (miroir de inventory_rules.vlan_de) : 1000 + index*10 + zone. + const idx = (nomenclature.index != null) ? nomenclature.index : 9; + const vlan = 1000 + idx * 10 + Number(d.categorie); + return {categorie: d.categorie, service: d.service, libelle: c.libelle, vlan}; } function echapper(valeur) { return String(valeur).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); diff --git a/scripts/inventory_rules.py b/scripts/inventory_rules.py index ae8f31b..d8b8b3a 100644 --- a/scripts/inventory_rules.py +++ b/scripts/inventory_rules.py @@ -376,10 +376,55 @@ def fonction_seq(nom: str) -> tuple[str, int | None]: return str(nom or ""), None +# --- Derivation de l'ADRESSAGE depuis le seul seed `index` -------------------- +# +# Principe : TOUT l'adressage (supernet, sous-reseaux, passerelles, VLAN, VMID) se +# DERIVE de l'index d'instance + du numero de zone. Rien n'est ecrit a la main dans +# la nomenclature (elle ne garde que le MODELE : libelles de zones + placement des +# fonctions). Source unique, ci-dessous, partagee par la generation d'inventaire et +# le devis reseau (scripts/devis_reseau.py). +# +# Conventions du modele a 6 zones (reference integral) : +# - 2e octet du supernet = OCTET_TENANT + index (index 1 -> 10.11.0.0/16) +# - 3e octet de la zone = OCTET_ZONE + categorie (zone 1 -> .16, dans .16.0/20) +# - VLAN sur le trunk convergE = SOCLE_VLAN + index x 10 + zone (4 chiffres, unique) +# - VMID ip-miroir = VLAN(4) . octet-hote(3) . sequence(2) = 9 chiffres mnemotechniques +OCTET_TENANT = 10 +OCTET_ZONE = 15 +SOCLE_VLAN = 1000 +INDEX_BAC_A_SABLE = 9 # index par defaut d'une nomenclature sans seed (jetable) + + +def supernet_de(index: int) -> str: + """/16 du tenant : 10.(10+index).0.0/16.""" + return f"10.{OCTET_TENANT + int(index)}.0.0/16" + + +def base3_de(index: int, categorie: int) -> str: + """Trois premiers octets d'une zone : 10.(10+index).(15+categorie).""" + return f"10.{OCTET_TENANT + int(index)}.{OCTET_ZONE + int(categorie)}" + + +def sous_reseau_de(index: int, categorie: int, cidr: int = 24) -> str: + """/24 d'une zone : 10.(10+index).(15+categorie).0/24.""" + return f"{base3_de(index, categorie)}.0/{int(cidr)}" + + +def passerelle_de(index: int, categorie: int) -> str: + """Passerelle d'une zone (SVI) : premier hote du /24.""" + return f"{base3_de(index, categorie)}.1" + + +def vlan_de(index: int, zone: int) -> int: + """VLAN 802.1Q sur le trunk convergE : 1000 + index x 10 + zone (unique globalement).""" + return SOCLE_VLAN + int(index) * 10 + int(zone) + + def deriver_nomenclature(fonction: str, seq: int, nomenclature: dict) -> dict | None: """Derive VMID / VLAN / IP / passerelle / CIDR depuis la fonction et le rang. Source unique de la derivation (miroir Python de l'auto-proposition du GUI). + Tout vient de l'index + de la zone : aucune lecture d'adressage stocke. """ fonctions = nomenclature.get("fonctions") or {} categories = nomenclature.get("categories") or {} @@ -391,32 +436,14 @@ def deriver_nomenclature(fonction: str, seq: int, nomenclature: dict) -> dict | c = categories.get(cat) or categories.get(str(cat)) if not c: return None - base3 = ".".join(str(c.get("sous_reseau", "")).split(".")[:3]) - # Prefixe VMID = index d'instance (federation multi-ecosystemes). Defaut 9 : - # retro-compatible (instances sans index, bacs a sable). 1 chiffre -> 9 instances - # prod federables avec un VMID a 5 chiffres ; elargir l'index pour davantage. - index = nomenclature.get("index", 9) + index = nomenclature.get("index", INDEX_BAC_A_SABLE) hote = svc * 10 + int(seq) - # Schema du VMID. 'ip-miroir' (opt-in) : 9 chiffres mnemotechniques encodant - # index + VLAN + octet-hote + sequence -> le VMID contient litteralement l'IP - # (10.(10+index).VLAN.hote) et le tenant. 'compact' (defaut, retro-compatible) : - # index.categorie.service.sequence (largeur variable, VMID des instances existantes). - schema = nomenclature.get("vmid_schema", "compact") - if schema == "ip-miroir": - # VLAN DERIVE : 1000 + index x 10 + zone. Le socle 1000 force le VLAN a - # TOUJOURS 4 chiffres (1011..4094) -> le VMID (VLAN.octet.seq) fait TOUJOURS - # 9 chiffres : LONGUEURS UNIFORMES et mnemotechniques. Unique GLOBALEMENT sur - # un trunk convergE (retirer 1000 redonne index x 10 + zone = tenant + zone). - vlan = 1000 + index * 10 + int(cat) - vmid = f"{vlan}{hote:03d}{int(seq):02d}" - else: - vlan = c.get("vlan") - vmid = f"{index}{cat}{svc}{int(seq):02d}" + vlan = vlan_de(index, int(cat)) return { - "vmid": vmid, + "vmid": f"{vlan}{hote:03d}{int(seq):02d}", "vlan": vlan, - "adresse_ip": f"{base3}.{hote}", - "passerelle": c.get("passerelle"), + "adresse_ip": f"{base3_de(index, int(cat))}.{hote}", + "passerelle": passerelle_de(index, int(cat)), "cidr": nomenclature.get("cidr_hote", 24), } diff --git a/scripts/prouver.py b/scripts/prouver.py index c42565c..e414a39 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -163,6 +163,41 @@ def preuve_modele_socle() -> tuple[bool, str]: return True, "Modele public socle : domaines/serveurs/applications/bases valides." +def preuve_nomenclature_derivee() -> tuple[bool, str]: + """Aucune nomenclature ne STOCKE d'adressage : tout se derive du seed `index`. + + Interdit `supernet` au niveau racine et `sous_reseau`/`passerelle`/`vlan` par zone. + Attrape toute rechute vers l'ecriture manuelle de l'adressage (rupture du 2026-07-23). + Portee : l'instance liee + tous les modeles decouverts (SETOPS_MODELES inclus). + """ + import yaml + sys.path.insert(0, str(RACINE / "scripts")) + import modeles as _mod + + cibles = [] + inst_nom = RACINE / INSTANCE / "plan" / "nomenclature.yml" + if inst_nom.exists(): + cibles.append(inst_nom) + cibles += [m / "plan" / "nomenclature.yml" for m in _mod.decouvrir() + if (m / "plan" / "nomenclature.yml").exists()] + + interdits_zone = {"sous_reseau", "passerelle", "vlan"} + fautes = [] + for p in cibles: + n = yaml.safe_load(p.read_text(encoding="utf-8")) or {} + rel = p.relative_to(RACINE) if p.is_relative_to(RACINE) else p + if "supernet" in n: + fautes.append(f"{rel}: supernet stocke") + if "vmid_schema" in n: + fautes.append(f"{rel}: vmid_schema obsolete (mode compact retire)") + for z, c in (n.get("categories") or {}).items(): + for k in interdits_zone & set(c or {}): + fautes.append(f"{rel}: categorie {z}.{k} stocke") + if fautes: + return False, f"{len(fautes)} champ(s) d'adressage stockes : " + " | ".join(fautes[:4]) + return True, f"{len(cibles)} nomenclature(s) : adressage 100% derive du seed index." + + def preuve_pas_lab_code_en_dur() -> tuple[bool, str]: """Aucun chemin `instance/inventories/lab/group_vars` code en dur (AFF-097). @@ -249,6 +284,8 @@ PREUVES: list[dict] = [ "refs": ["AFF-002", "AFF-095"], "cmds": [[sys.executable, "scripts/couverture_gui.py", "verifier", "--tolerer", "nomenclature"]]}, + {"id": "P20", "titre": "Adressage 100% derive du seed (aucun stocke)", + "refs": ["AFF-001", "AFF-003"], "func": preuve_nomenclature_derivee}, ]