carte : P48 — l'index du mainteneur ne peut plus mentir
Some checks are pending
verifier / verifier (push) Waiting to run

`docs/carte-set-ops.md` est l'index du MAINTENEUR : l'ordre de lecture du corpus,
et surtout le catalogue des MECANISMES TRANSVERSES avec, pour chacun, OU IL VIT
DANS LE CODE. Son but est ecrit en toutes lettres : « ne plus re-deterrer ce qui
existe ».

Elle n'avait aucune garde, alors que `catalogue-services.md` a la sienne depuis
P38. Ses sept chiffres etaient faux — 54 roles annonces contre 60, 34 documents
contre 38, 15 pieces d'audit contre 27, 70 decisions contre 78.

Le defaut couteux n'est pourtant pas la. C'est le POINTEUR MORT : la carte dit ou
vit un mecanisme, quelqu'un ne l'y trouve pas, et le reimplemente a cote —
exactement la panne qu'elle existe pour prevenir. Aucun de ces nombres ne fait
travailler personne ; mais un document dont les faits verifiables sont faux cesse
d'etre consulte, et c'est alors ses pointeurs qu'on perd.

P48 verifie les deux : 84 chemins cites existent, et 7 chiffres correspondent a
la mesure. Controle negatif verifie sur les DEUX moities — un chiffre fausse, un
pointeur casse, la preuve echoue dans les deux cas.

QUATRE DISTINCTIONS ont du etre ecrites pour qu'elle ne soit pas fausse dans
l'autre sens : un gabarit de nom (`preuve-<date>.md`) decrit une forme, pas un
fichier ; un chemin hors depot (`~/.config/setops-vault-pass`) vit sur le poste de
l'exploitant, et c'est tout l'interet de la doctrine des voutes ; un fragment
(`meta/acces.yml`) vaut comme SUFFIXE, parce qu'un index se lit ainsi ; et un
artefact GENERE (`hosts.yml`) n'a pas a exister dans le moteur. Les quatre sont
nommees dans le code plutot que sautees en silence.

Le tableau « Le depot en chiffres » remplace les comptes en prose : ce qu'on
n'entretient pas, on ne l'affirme pas — ou bien on le fait recompter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-26 13:56:58 -04:00
parent 87716cefc7
commit 9e8679215d
4 changed files with 205 additions and 7 deletions

View file

@ -59,6 +59,33 @@ qu'elle porte, et on refuse si ça diffère de ce que porte le runner.
sans que rien ne le signale — dont le correctif qui désarme le pare-feu Proxmox. Un
écosystème qui se reproduit depuis une forge en retard reproduit ses défauts.*
### P48 — la carte d'orientation ne peut plus mentir
`docs/carte-set-ops.md` est l'index du **mainteneur** : l'ordre de lecture du corpus, et
surtout le catalogue des **mécanismes transverses** avec, pour chacun, **où il vit dans le
code**. Son but est écrit en toutes lettres : *« ne plus re-déterrer ce qui existe »*.
Elle n'avait aucune garde, alors que `catalogue-services.md` a la sienne depuis P38. Ses
**sept** chiffres étaient faux — 54 rôles annoncés contre 60, 34 documents contre 38, 15
pièces d'audit contre 27, 70 décisions contre 78.
Le défaut coûteux n'est pourtant pas là. C'est le **pointeur mort** : la carte dit où vit
un mécanisme, quelqu'un ne l'y trouve pas, et le réimplémente à côté — exactement la panne
qu'elle existe pour prévenir. *Aucun de ces nombres ne fait travailler personne ; mais un
document dont les faits vérifiables sont faux cesse d'être consulté, et c'est alors ses
pointeurs qu'on perd.*
P48 vérifie les deux : **84 chemins** cités existent, et **7 chiffres** correspondent à la
mesure. Quatre distinctions ont dû être écrites pour qu'elle ne soit pas fausse dans
l'autre sens — un gabarit de nom (`preuve-<date>.md`) décrit une forme, pas un fichier ; un
chemin hors dépôt (`~/.config/setops-vault-pass`) vit sur le poste de l'exploitant ; un
fragment (`meta/acces.yml`) vaut comme suffixe ; et un artefact **généré** (`hosts.yml`) n'a
pas à exister dans le moteur. Les quatre sont nommées dans le code plutôt que sautées en
silence.
*Le tableau « Le dépôt en chiffres » remplace les comptes en prose : ce qu'on n'entretient
pas, on ne l'affirme pas — ou bien on le fait recompter.*
### D-81 — la forge du site fait autorité, et `make genome-etat` le vérifie
Décision de l'exploitant : **la forge du SITE fait autorité pour le génome.** Toute autre

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/production/hosts.yml`
- **Verdict** : ✅ CONFORME (46 OK · 0 echec · 1 saute)
- **Verdict** : ✅ CONFORME (47 OK · 0 echec · 1 saute)
## Preuves
@ -60,6 +60,7 @@
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 84 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
## Couverture des affirmations ✅ du registre

View file

@ -10,10 +10,26 @@ Point d'entrée vers le corpus documentaire, et **catalogue des mécanismes tran
ceux qui vivent dans le code et qu'on *re-découvre* sinon. Créée le 2026-07-03 après un audit
du dépôt, **revue le 2026-07-29**. But : ne plus re-déterrer ce qui existe.
Le dépôt est **déjà bien documenté** (34 docs + 15 pièces d'audit + 23 unités de wiki, et un
README pour chacun des 54 rôles). Le manque n'était pas la doc du *modèle*,
mais (a) un index « par où commencer » et (b) une carte des *mécanismes* (dispersés dans le
code + les README de rôles). Cette page comble ces deux trous.
Le dépôt est **déjà bien documenté**. Le manque n'était pas la doc du *modèle*, mais (a) un
index « par où commencer » et (b) une carte des *mécanismes* (dispersés dans le code + les
README de rôles). Cette page comble ces deux trous.
### Le dépôt en chiffres
> Ces valeurs sont **mesurées**, pas recopiées : **P48** les recompte et refuse tout écart.
> Elles étaient toutes fausses le 2026-08-26 — de 6 rôles, de 12 pièces d'audit, de 8
> décisions. Aucune ne faisait travailler personne ; mais une carte dont les faits
> vérifiables sont faux cesse d'être consultée, et c'est alors ses **pointeurs** qu'on perd.
| Ce qu'on compte | Combien | Comment on le mesure |
|---|---|---|
| rôles | 60 | `roles/*/` |
| README de rôles | 60 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
| documents | 38 | `docs/*.md` |
| pièces d'audit | 27 | `docs/audit/*` |
| unités de wiki | 27 | `wiki/*.md` |
| décisions en vigueur | 78 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
## 1. À lire d'abord (dans l'ordre)
@ -28,11 +44,11 @@ code + les README de rôles). Cette page comble ces deux trous.
| **Ordre de déploiement** | `docs/couches-deploiement.yml` (couches) + `docs/dependances-groupes.yml` (graphe) → `playbooks/site.yml` (**généré**, `make site`) |
| **Conformité du déployé** | `docs/devis-services.md` — les **cinq devis de service** (`make identite-plan`, `certificats-plan`, `expositions-plan`, `postgresql-plan`, `courriel-plan`). Répondent à ce que `make prouver` ne demande jamais : *ce qui tourne correspond-il à ce qui est déclaré ?* |
| **Preuve / recette** | `docs/audit/affirmations.md` (registre), `make prouver` → `docs/audit/preuve-<date>.md` — **statique** : lit le dépôt, aucun appel réseau ; la conformité du déployé est l'affaire des devis de service (ligne au-dessus), `docs/audit/plan-de-recette.md` (**généré** du wiki), `docs/audit/protocole-operateur-independant.md` |
| **Décisions d'architecture** | `docs/decisions-architecture.md` — **70 décisions en vigueur** (D-01 → D-73, 3 renversées), pourquoi, où lire le détail, et ce qui les garde ; plus les **décisions renversées** et leur cause |
| **Décisions d'architecture** | `docs/decisions-architecture.md` — les décisions en vigueur (comptées ci-dessus), pourquoi, où lire le détail, et ce qui les garde ; plus les **décisions renversées** et leur cause |
| **SDN / routage** | `docs/sdn-evpn.md` — décision du 2026-08-02 : le routage inter-zone passe des commutateurs aux hyperviseurs (zones EVPN = VRF). **Non éprouvé** : spike avant génération |
| **Migration de tenant** | `docs/migration-tenant.md` — recette en 8 étapes, machine à états, gardes ; le receveur se construit **avant** tout gel |
| **Exploitation courante** | `docs/runbooks-exploitation.md`, `docs/intrants-communs.md`, `docs/intrants-base-gui-conception.md`, `docs/theme-forgejo-hors-flotte.md` |
| **Pédagogie (le wiki)** | `wiki/` — 21 unités (+ `_Sidebar`) publiées par `make wiki-publier` ; entrer par `wiki/Home.md` |
| **Pédagogie (le wiki)** | `wiki/` — les unités (comptées ci-dessus) publiées par `make wiki-publier` ; entrer par `wiki/Home.md` |
| **Vision / positionnement** | `docs/ecosysteme-chezlepro.md`, `docs/positionnement.md`, `docs/pouvoirs-set-ops.md` |
## 2. Les mécanismes transverses (et OÙ ils vivent)

View file

@ -1244,6 +1244,158 @@ def preuve_zones_inverses_couvrent_l_occupe() -> tuple[bool, str]:
f"a une).")
# Ce que la carte nomme a juste titre et que ce depot ne contient pas : le produit de
# `make instancier`, qui vit dans le depot d'un ecosysteme.
ARTEFACTS_GENERES = {"hosts.yml"}
def _sans_accents(texte: str) -> str:
return "".join(c for c in unicodedata.normalize("NFD", texte)
if unicodedata.category(c) != "Mn")
_ECARTES = {".git", "__pycache__", ".venv", "node_modules", "collections"}
def _propre(chemin: Path) -> bool:
return not any(part in _ECARTES for part in chemin.relative_to(RACINE).parts)
def _existe_en_suffixe(fragment: str) -> bool:
"""Un fichier du depot se termine-t-il par ce fragment de chemin ?"""
nom = fragment.rsplit("/", 1)[-1]
return any(_propre(p) and str(p).endswith("/" + fragment)
for p in RACINE.rglob(nom))
def _existe_quelque_part(nom: str) -> bool:
"""Un fichier ou dossier de ce nom existe-t-il dans le depot ?
La carte nomme volontiers un fichier sans son dossier — c'est ce qui la rend lisible.
On cherche donc par NOM, en ecartant ce qui n'appartient pas au depot (`.git`, les
caches Python, les dependances installees).
"""
return any(_propre(chemin) for chemin in RACINE.rglob(nom))
def _carte_chiffres_mesures() -> dict[str, int]:
"""Ce que la carte affirme, recompte a la source."""
dec = (RACINE / "docs" / "decisions-architecture.md")
texte = dec.read_text(encoding="utf-8") if dec.is_file() else ""
return {
"roles": len([p for p in (RACINE / "roles").glob("*") if p.is_dir()]),
"README de roles": len(list((RACINE / "roles").glob("*/README.md"))),
"documents": len(list((RACINE / "docs").glob("*.md"))),
"pieces d'audit": len([p for p in (RACINE / "docs" / "audit").glob("*")
if p.is_file()]),
"unites de wiki": len(list((RACINE / "wiki").glob("*.md"))),
"decisions en vigueur": len(re.findall(r"^\| \*\*D-\d+\*\* \|", texte, re.M)),
"decisions renversees": len(re.findall(r"^\| \*\*D-\d+\*\* —", texte, re.M)),
}
def preuve_carte_dit_vrai() -> tuple[bool, str]:
"""La carte d'orientation designe des choses qui existent, et compte juste.
CE QU'EST LA CARTE. `docs/carte-set-ops.md` est l'index du MAINTENEUR : l'ordre de
lecture du corpus, et surtout le catalogue des MECANISMES TRANSVERSES avec, pour
chacun, OU IL VIT DANS LE CODE. Son but est ecrit en toutes lettres : « ne plus
re-deterrer ce qui existe ».
POURQUOI UNE GARDE (mesure du 2026-08-26). Un POINTEUR MORT est le defaut couteux :
la carte dit ou vit un mecanisme, quelqu'un ne l'y trouve pas, et le reimplemente a
cote. C'est exactement la panne que la carte existe pour prevenir.
LES CHIFFRES SONT L'AUTRE MOITIE, et ils etaient TOUS faux : 54 roles annonces contre
60, 34 documents contre 38, 15 pieces d'audit contre 27, 70 decisions contre 78.
Aucun de ces nombres ne fait travailler personne. Mais un document dont les faits
verifiables sont faux cesse d'etre consulte — et c'est alors ses pointeurs qu'on perd.
`catalogue-services.md` avait deja sa garde (P38) ; la carte n'en avait aucune.
"""
carte = RACINE / "docs" / "carte-set-ops.md"
if not carte.is_file():
return False, "docs/carte-set-ops.md est introuvable."
texte = carte.read_text(encoding="utf-8")
manques: list[str] = []
# --- Les pointeurs ---------------------------------------------------------
#
# On ne retient que ce qui RESSEMBLE a un chemin du depot : un segment avec un `/`
# ou une extension connue. Les `make cible`, les noms de variables et les fragments
# de code entre accents graves ne sont pas des chemins, et les exiger rendrait la
# preuve fausse dans l'autre sens.
chemins = set()
for brut in re.findall(r"`([^`\n]+)`", texte):
c = brut.strip()
# Un GABARIT de nom n'est pas un chemin : `docs/audit/preuve-<date>.md` decrit
# une forme de fichier, pas un fichier. L'exiger rendrait la preuve fausse.
# Ni un gabarit de nom (`preuve-<date>.md` decrit une FORME), ni un chemin HORS
# depot (`~/.config/setops-vault-pass` vit sur le poste de l'exploitant, et c'est
# tout l'interet de la doctrine des voutes). Les exiger rendrait la preuve fausse.
if " " in c or c.startswith(("$", "-", "~", "/")) or "<" in c or ">" in c:
continue
if not ("/" in c or c.endswith((".md", ".yml", ".py", ".j2"))):
continue
chemins.add(c.rstrip("/"))
for c in sorted(chemins):
# DEUX SEMANTIQUES, ET C'EST VOULU.
#
# Un chemin AVEC dossier (`roles/serveur_nginx/tasks/main.yml`) affirme un
# emplacement : on l'exige tel quel. Un nom SANS dossier (`devis_reseau.py`,
# `expositions.conf.j2`) affirme seulement qu'une chose de ce nom existe — c'est
# ainsi qu'un index se lit, et exiger le chemin complet le rendrait illisible.
# UN ARTEFACT GENERE N'A PAS A EXISTER ICI. La carte nomme `hosts.yml` en disant
# elle-meme « genere » : c'est le produit de `make instancier`, il vit dans le
# depot d'un ECOSYSTEME, pas dans le moteur. L'exiger reprocherait a la carte
# d'etre juste. On les nomme plutot que de les sauter en silence.
if c in ARTEFACTS_GENERES:
continue
if "/" in c:
# Un fragment (`meta/acces.yml`) designe un emplacement RELATIF a un role ou
# a une instance. Il vaut donc comme SUFFIXE : `roles/serveur_forgejo/meta/
# acces.yml` le satisfait. Exiger la racine rendrait la carte illisible.
if any((RACINE / base / c).exists() for base in ("", "docs")):
continue
if _existe_en_suffixe(c):
continue
elif _existe_quelque_part(c):
continue
# Les globs et les chemins d'une INSTANCE (`plan/*.yml`, `roles/*/meta/...`)
# se verifient par expansion : ils designent une forme, pas un fichier.
# UN GLOB DESIGNE UNE FORME, ou qu'elle se trouve. `plan/*.yml` est le plan d'un
# ECOSYSTEME : il n'existe pas a la racine du moteur, mais bien sous `instance/`
# ou dans un depot voisin. On cherche donc la forme partout.
if "*" in c and (list(RACINE.glob(c)) or list(RACINE.glob("**/" + c))):
continue
manques.append(f"`{c}` : cite par la carte, introuvable dans le depot")
# --- Les chiffres ----------------------------------------------------------
#
# L'appariement se fait SANS ACCENTS : la carte ecrit « rôles » et « pièces d'audit »,
# ce code ecrit sans diacritiques comme tout le reste du depot. Comparer les deux
# tels quels ferait echouer la preuve sur son propre alphabet.
annonces = {}
for etiq, valeur in re.findall(r"^\| *([^|]+?) *\| *(\d+) *\|", texte, re.M):
annonces[_sans_accents(etiq).lower()] = int(valeur)
for etiquette, attendu in _carte_chiffres_mesures().items():
cle = _sans_accents(etiquette).lower()
if cle not in annonces:
manques.append(f"« {etiquette} » : absent du tableau « Le depot en chiffres » "
f"(mesure : {attendu})")
continue
if annonces[cle] != attendu:
manques.append(f"« {etiquette} » : la carte annonce {annonces[cle]}, "
f"le depot en compte {attendu}")
if manques:
return False, ("La carte d'orientation ne dit plus vrai :\n - "
+ "\n - ".join(manques))
return True, (f"La carte designe {len(chemins)} chemin(s) qui existent, et ses "
f"{len(_carte_chiffres_mesures())} chiffres correspondent a la mesure.")
def preuve_glossaire_enseigne() -> tuple[bool, str]:
"""Tout mot que le depot emploie devant l'exploitant est explique au glossaire.
@ -1591,6 +1743,8 @@ PREUVES: list[dict] = [
"refs": [], "func": preuve_un_seul_maitre_du_plancher},
{"id": "P47", "titre": "Zones inverses : couvrir l'occupe, et rien de plus",
"refs": [], "func": preuve_zones_inverses_couvrent_l_occupe},
{"id": "P48", "titre": "La carte d'orientation designe ce qui existe, et compte juste",
"refs": [], "func": preuve_carte_dit_vrai},
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
"func": preuve_devis_frontiere_du_site},
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],