# CHANGELOG — Set-OPS ## 2026-09-20 (12) — Une décision appliquée à moitié se croit tenue **83 preuves, `make test` à 0 échec.** L'exploitant a demandé : « n'avions-nous pas décidé d'un plan d'adressage dérivé pour chaque flotte, qu'elle soit tenant ou site ? » Oui. La décision est écrite dans l'underlay du site, datée du 12 septembre : > *« sites et locataires se partagent la classe A, chacun avec SON index. Le site prend > 37, le locataire garde 17. `make instances` les compte ensemble depuis ce jour. »* ### Ce que l'entrée (11) affirmait, et qui était faux Elle disait qu'un site *« déclare ses adresses, il n'a aucun seed dont les faire descendre »*. C'était la recopie d'un commentaire en tête de `SITE-Chezlepro/plan/serveurs.yml` — **périmé depuis le jour où le site a reçu son index**, et lu au lieu d'être mesuré. Un commentaire périmé se propage : celui-ci s'est retrouvé le même jour dans la docstring de `ConsoleSite` et dans une entrée de CHANGELOG. C'est exactement ce que `CARTE-DU-SITE.md` décrit chez un locataire — **inerte et crédible** : le moteur ne le lit pas, donc rien ne le corrige ; un humain le lit, et le croit. ### La mesure Les sept zones du site suivaient `10...0/24` **à la lettre** — et étaient pourtant **écrites** : `sous_reseau` et `passerelle` stockés pour chacune. C'est précisément ce que P20 interdit à un locataire, sous le titre *« Adressage 100 % dérivé du seed (aucun stocké) »*. Et la portée de P20 était *« l'instance liée + les modèles »* : **elle ne regardait jamais le site**. Les valeurs concordaient parce qu'un humain les avait tapées juste, ce jour-là. Rien ne le garantissait : une zone écrite `10.36.x` serait passée sans un mot. ### Deux natures dans une seule liste `reseaux:` mélangeait ce qui peut dériver et ce qui ne le peut pas. Chaque entrée déclare maintenant sa nature : | `nature: fabric` | grappe, iSCSI, Ceph, transit, VXLAN, management | adressage dicté par les participants d'un lien physique (D-02, D-04) — il reste **écrit** | | `nature: site` | les sept zones du site | elles **descendent de son index**, et ne stockent plus rien | Le loader dérive `sous_reseau` et `passerelle` à la lecture, pour que les consommateurs ne voient aucune différence. **Quatorze valeurs stockées ont disparu du fichier, et ce que les consommateurs lisent est identique — écart mesuré : zéro.** `make underlay`, `make devis-sdn` et `make devis-reseau` passent. P20 juge désormais les deux natures, et refuse une entrée qui ne déclare pas la sienne : on ne peut pas juger ce qu'on ne sait pas lire. Contrôle négatif joué : une zone remise à `10.36.31.0/24` est refusée, en nommant l'index dont elle aurait dû descendre. ### Ce qui reste écrit, et qui est une dette nommée Les **machines** du site gardent leurs `ip`, `vmid` et `noeud` dans son plan. Ce n'est plus une doctrine — « le site est le terrain, il n'a rien dont faire descendre » — puisque le seed existe maintenant pour elles aussi. C'est une dette, et la nommer est le minimum : *un objectif qu'on abandonne sans le dire devient un objectif qu'on croit atteint.* **P02 et P60 restent**, pour les raisons déjà consignées. ## 2026-09-20 (11) — Deux fonctions qui doivent rendre la même forme finissent par ne plus la rendre **83 preuves, `make test` à 0 échec, 8 tests de rendu.** La console avait deux assembleurs de charge — `inventaire_api` pour un locataire, `inventaire_api_du_site` pour un site. Ils ont dérivé deux fois le même jour, et ce n'était pas une malchance : c'est ce qui arrive toujours à deux copies d'une même obligation. ### Ce que la duplication a coûté, mesuré **En forme.** `serveurs` valait `[]` d'un côté et `{}` de l'autre. Les deux « vides », et la page ne les lit pas pareil : `charger()` levait, et la console d'un site mourait avant sa première vue. **En contenu.** Cinq registres étaient servis vides — « pour ne pas fabriquer un faux plan de tenant ». Le site n'a pas de faux plan : il a **le sien**, au même format, que les lecteurs du moteur lisent sans broncher. Mesure : **9 serveurs, 21 applications, 2 bases, 1 domaine**, tous invisibles à l'écran. ### Un tronc, deux branches, et le rôle distinct de la portée `Console` assemble — **un seul endroit** où la charge se compose, et c'est lui qui fixe les clés et leurs formes. Les branches ne décident que de ce qui leur appartient : d'où vient le plan, d'où vient l'inventaire, quels pouvoirs elles portent. | Branche | Ce qu'elle est | Son plan | Son inventaire | |---|---|---|---| | `ConsoleLocataire` | configure ses services | dérivé d'un seed | artefact généré | | `ConsoleSite` | matérialise le terrain | **déclaré** (`ip`, `vmid`, `noeud`) | un script | | `ConsolePoste` | l'atelier — hérite du locataire | idem locataire | idem locataire | | `Console` | ne pilote rien, et le dit | aucun | vide | **Le rôle et la portée ne se confondent pas.** Le rôle est une propriété de la classe : ce pour quoi cette console existe. La portée se **calcule** depuis les pouvoirs : ce qu'elle peut vraiment, ici et maintenant. Un poste privé de la voûte du site reste l'atelier du mainteneur et n'engendre pourtant rien — figer la portée sur la classe lui aurait rendu un pouvoir que la mesure lui refuse. Ce découpage a immédiatement montré un trou : `ConsolePoste` héritait du refus d'un locataire — *« elle ne sait pas sur quelle fabric poser »* — alors qu'il **monte** la carte. Ce qui lui manque, c'est la voûte. Un message qui accuse la mauvaise absence fait chercher au mauvais endroit. ### Le jugement des assistants remonte dans le tronc Il était écrit deux fois : dans la route qui **liste** les assistants, et dans celle qui **exécute** une étape. Deux copies d'une même règle finissent par ne plus dire la même chose — et **celle qui se trompe est toujours celle qui exécute, parce que c'est la moins relue**. `Console.peut_conduire()` répond aux deux. Le gestionnaire ne demande plus « est-ce un site ? » pour choisir un assembleur : il demande sa charge à sa console, et ignore laquelle lui répond. ### Ce qui a été vérifié `make test` à 0 échec, les 8 tests de rendu sous `node` — dont deux neufs : la console de site sert bien son plan, et les deux charges portent les mêmes types. La console a été lancée pour de vrai : contexte `poste`, 13 serveurs, 24 applications, 17 runbooks, 127 étapes conduisibles, 0 écart. **Et `make verifier` a trouvé une faute que j'avais laissée passer** : le playbook `depots_perimes.yml` livré une heure plus tôt violait `risky-shell-pipe`. J'avais passé `--syntax-check` dessus et `ansible-lint` seulement sur le rôle voisin. Corrigé — les tubes retirés, `pipefail` armé sans `-e` pour qu'un `git` qui échoue laisse le script répondre au lieu de faire tomber la tâche. **Deux échecs restent.** P02, antérieur, consigné à l'entrée `(6)`. Et **P60** : le wiki publié est en retard d'un commit depuis que l'entrée `(6)` a documenté les Assistants — `make wiki-publier WIKI_REMOTE=…` le republie, et l'adresse publique n'est celle d'aucun plan, donc elle ne se devine pas. ## 2026-09-20 (10) — Un vide doit avoir la forme de ce qu'il remplace **83 preuves, `make test` à 0 échec, 7 tests de rendu.** La console de `console.genese.internal` — celle du runner de SITE-Chezlepro — ne montrait rien. Elle a douze machines. ### `charger()` levait, et la page mourait avant de dessiner `inventaire_api` sert `serveurs` en **liste** ; `inventaire_api_du_site` le servait en **table**. Les deux sont « vides », et la page ne les lit pas pareil : `(data.serveurs || []).map(...)` trouve `{}` — *truthy*, sans `.map` — et `charger()` lève. Toute la console d'un site s'arrêtait là, avant la première vue, et le message accusait une méthode manquante plutôt qu'une forme qui ment. Un vide doit avoir la **forme** de ce qu'il remplace. Sinon ce n'est pas un vide, c'est un autre objet qui se fait passer pour lui — et le malentendu ne se voit qu'au moment où quelqu'un appelle une méthode dessus. Un test compare désormais les deux charges **type par type** pour les clés qu'elles partagent, avec deux écarts admis et motivés. Il ne demande pas les mêmes valeurs — un site n'a pas de plan de locataire — seulement les mêmes formes. ### Et la vue regardait au mauvais endroit Le registre vide était **voulu** : « on ne fabrique pas un faux plan de tenant pour remplir l'écran ». C'est juste — un site déclare ses machines dans son propre plan, avec `ip`, `vmid` et `noeud` **écrits**, là où un locataire les dérive de son seed ; il est le terrain, il n'a rien dont les faire descendre. Mais la vue d'accueil tirait de ce registre vide le message d'un locataire sans serveurs — « + Serveur », `make serveurs-bootstrap` — et les douze machines, servies dans `hotes`, n'étaient dessinées nulle part. Une console de site montre maintenant **ses** machines : mêmes tuiles que les serveurs d'un locataire, en lecture seule, avec leur adresse, leur VMID, leurs rôles et leur point de vie ; le panneau droit dit ce que cette console peut — matérialiser le terrain, n'entrer chez aucun locataire — et renvoie aux Assistants. **C'est le défaut du 2026-09-16 par une autre porte.** La première fois, l'inventaire était vide et la page dessinait ce vide comme un plan vide. Cette fois l'inventaire est **plein** et c'est la vue qui regarde ailleurs. Le premier correctif a donné à la console de quoi répondre ; il ne lui a pas appris où regarder. ### Ce qui a trouvé le défaut Pas une lecture : **le banc**. `test_rendu_gui.py` dessine les vues sous `node` dans un DOM simulé. En lui donnant pour la première fois la charge d'une console de SITE, il a levé `(data.serveurs || []).map is not a function` — c'est-à-dire la cause réelle, sous le symptôme que je m'apprêtais à corriger. Sans lui, j'aurais livré une vue juste par dessus un `charger()` qui lève, et la console serait restée vide. ### Ce qui a été vérifié `node --check` sur le bloc `