blog.vfg.lol

Une revue qui n'a rien réparé — et quatre contrats incapables d'échouer

Un audit de treize contrôles qui n'a rien muté : sept constats, quatre contrats de sûreté incapables d'échouer, et 43 divergences volontairement laissées intactes. Ce qu'il a refusé de réparer est plus instructif que ce qu'il a trouvé.

TL;DR

La revue du travail intégré a produit sept constats et zéro modification de code. Le mandat était explicite : lire, mesurer, écrire un plan — pas réparer. Aucun build statique, aucune publication, aucun déploiement, aucune rotation de secret.

Ce que la revue a trouvé tient en une phrase :

Quatre contrats de sûreté existent, sont documentés, et ne peuvent pas échouer.

Un outil de rotation de secrets qui peut effacer sa propre configuration. Un déploiement qui peut se déclarer vert avec le back-office hors service. Une vérification d'alignement qui ne compare rien, alors qu'elle vient de lire le fichier. Et une garde de parité statique que aucune chaîne n'exécute.

Et la partie la plus utile du résultat est un refus : 43 divergences de contenu sont réelles, mesurées, et volontairement laissées intactes — elles attendent une décision humaine, pas un correctif.


1. Un mandat qui interdit de réparer

Une revue qui corrige en même temps qu'elle mesure ne peut pas rendre compte de ce qu'elle a vu. C'est le même principe que le module d'observation du protocole de publication, qui regarde sans toucher.

Le mandat tenait donc en cinq interdits : ne rien réparer, ne rien publier, ne rien déployer, ne toucher à aucun secret, et ne pas réévaluer le fond éditorial des articles. Un sixième ajout : ne pas toucher aux trois artefacts non suivis trouvés dans l'arbre de travail (.tmp-drift-fields.mjs, .tmp-probe.mjs, snapshots/articles-index/2026-09-22T0244-undefined/). Ils restent en place, et le plan interdit explicitement à tout agent de correction de les ranger : un audit qui « nettoie » en passant rend son propre constat irreproductible.

Le périmètre revu : code, configurations, workflows, tests, pages publiques, documents de reprise et leur cohérence entre eux. Pas le contenu des articles.


2. La méthode : treize contrôles, tous en lecture seule

ContrôleRésultat
npm testréussi
npm run test:ciréussi, clone frais simulé
npm run check:linksréussi ; 116 liens hérités en baseline, 335 hors dépôt
npm run check:rsyncréussi ; 18 motifs synchronisés entre workflow et fichier d'exclusion
npm run check:portsréussi ; 4 ports du dépôt, aucun conflit
npm run check:countsréussi ; 559 sources, 559 articles indexés (mesure de la revue)
npm run check:a11yréussi
npm run check:publishréussi
npm run check:staticréussi sur l'export présent ; 38 surfaces, 0 erreur
npm run check:driftinventaire réussi ; 558 servis, 559 au dépôt, un brouillon attendu
… --contentéchec attendu : 43 divergences de contrat
git diff --check8 défauts d'hygiène de diff
Production anonyme14 chemins vérifiés, contrat tenu

La vérification de production est la seule qui regarde ailleurs qu'ici, et elle a été faite sans identifiant. Atelier 401, /.env 404, documents internes 403, hubs publics 200, alias historiques 308 puis 200. Les protections annoncées tiennent là où ça compte : sur le serveur, pas dans les fichiers.

Un mot sur le seul « échec » de la liste : il est attendu. Le balayage de contenu compare le dépôt et ce qui est servi ; il a trouvé 43 écarts réels et il a le droit d'échouer. C'est le contraire d'un faux signal — c'est un contrôle qui fait son travail et qu'on ne contourne pas.


3. Les quatre contrats incapables d'échouer

R1 — L'outil de sécurité peut détruire sa propre configuration

scripts/rotate-secrets.mjs écrit d'abord le fichier d'environnement (ligne 633), puis écrit le fichier demandé par --save-to (ligne 661). Aucune précondition ne compare les deux chemins.

Un opérateur qui vise son .env avec --save-to remplace sa configuration complète par quelques commentaires et les seules clés tournées. La sauvegarde horodatée créée juste avant permet une récupération manuelle, mais le redémarrage suivant peut partir avec une configuration incomplète.

Ce n'est pas un défaut de droits — le mode 600 est bien appliqué. C'est un défaut d'ordre de sûreté : un outil qui tourne les secrets ne doit pas pouvoir les effacer par une faute de frappe. Les tests de rotation ne couvrent aujourd'hui ni le chemin identique, ni les formes relative et absolue du même fichier.

R2 — Le déploiement peut réussir avec le back-office mort

L'échec de redémarrage de blog-admin est absorbé par un opérateur de secours (ligne 252 de code-deploy.yml). Ensuite le workflow vérifie blog-read, des routes publiques et l'Atelier. Jamais l'API admin sur le port 5220.

Conséquence : le lecteur reste disponible, la publication et la vérification d'identité sont hors service, et le job annonce succès. Une livraison à moitié faite qui se déclare verte. Le correctif proposé renverse la règle : échouer si l'admin ne répond pas 401 ou 200 dans un délai borné — et si une livraison partielle est réellement voulue, qu'elle produise un état explicitement partiel, pas un vert.

R3 — La vérification qui ne compare pas (re-vérifié pour cet article)

post.mjs --verify "Titre" est le contrat robuste au renommage : il retrouve l'article par son titre, ce qui survit au renommage du fichier par le serveur. Le chemin local est même calculé.

Puis la fonction retourne 0 avant d'atteindre le comparateur :

// scripts/post.mjs, branche « titre »
md = fs.readFileSync(abs, "utf8");        // lu
relPath = path.relative(ROOT, abs)…       // calculé
    …
return 0;                                 // ← et on sort ici

const result = compare({ md, relPath, catalog });  // jamais atteint par ce chemin

Le contrat affirme donc « servi », ce qui est vrai, mais ne dit rien d'un champ local divergent — alors que le fichier vient d'être lu. Je l'ai relu dans le code pour cet article : le diagnostic de la revue est exact, ligne par ligne.

C'est la même famille que le défaut corrigé la veille dans le même outil, où le verdict « dépôt et production alignés » pouvait être imprimé sans qu'aucune comparaison ait eu lieu. Un verdict de conformité doit exiger qu'une comparaison se soit produite.

R6 — La parité statique existe, mais rien ne l'impose

check:static tourne, et il passe sur l'export présent. Mais la CI ne le lance pas après build:public, et prepublish non plus. La capacité est là ; aucune chaîne ne s'en sert.

Une garde qui n'est branchée nulle part n'est pas une garde, c'est une commande. C'est le même motif que la CI elle-même, restée disabled_manually pendant des mois : une protection non exécutée et une protection absente produisent le même incident. La différence est seulement qu'on croit la première.


4. Le relais qui envoyait un agent sur le périmètre d'un autre (R4)

Le plan de migrations V4 est découpé en fiches de mission. La fiche M01 couvre sections/, centralisation/ et le corpus ondes/. La fiche M03 couvre l'Atlas, le quiz, le devblog historique et la comparaison RJ/CR.

Le registre actif, lui, décrit M01 comme débloqué et demande de commencer par data/atlas ou demo/rapport-rj-vs-cr.html — deux sources classées M03. Le même passage qualifie M01 de débloqué, puis rappelle qu'il reste bloqué par G1. La distinction voulue est probablement « préparation logique » contre « déplacement physique », mais elle n'est pas opérationnelle pour un agent léger : un exécutant lit une instruction, pas une nuance.

C'est le constat qui a le plus de conséquences pratiques, parce qu'il ne s'agit pas d'un bug : il s'agit d'un document qui donne un ordre faux. Un agent obéissant, travailleur et hors périmètre est plus dangereux qu'un agent bloqué. D'où la vague R0 : corriger le geste, séparer les deux niveaux, renvoyer Atlas et RJ/CR à leur fiche — et ne déplacer aucun fichier. Un correctif documentaire n'a pas le droit de migrer quoi que ce soit.


5. Ce que la revue a refusé de réparer

C'est la partie du résultat que je préfère.

Le balayage de contenu donne, chiffre pour chiffre :

558 comparés
425 identiques
 90 formes connues du protocole
 43 divergences de contrat

Les 43 écarts sont réels, ils portent sur des métadonnées et quelques corps, et le plan les laisse intacts. Une vague R5 existe, elle est marquée bloquée, et sa précondition est « instruction explicite de FG, périmètre de documents nommé ».

Pourquoi ne pas les corriger ? Parce qu'aucun outil ne sait laquelle des deux versions est la bonne. Un correctif automatique choisirait une source de vérité par accident et réécrirait du contenu éditorial — exactement le geste que la veille avait refusé de faire quand un contrôle avait essayé d'absorber une forme non prouvée. On n'absorbe pas une famille qu'on ne peut pas prouver, et on ne recopie pas la production vers le dépôt sans avoir décidé lequel des deux fait foi.

Le dépôt est donc déclaré exploitable, la production protégée, et le chantier pas totalement aligné. Trois affirmations différentes, et la troisième coûte : c'est celle que personne n'écrit d'habitude.


6. Un plan exécutable par des agents légers

Un audit sans plan est une opinion. Le pack de correctifs est donc conçu comme un relais délégable, avec une contrainte de forme qui n'est pas cosmétique : chaque mission doit être réalisable sans lire autre chose que ce qu'elle cite.

VagueObjetDépend deÉtat
R0Réparer le contrat documentaire M01/M03aucuneprête
R1Empêcher la collision --save-to / --envaucuneprête
R2Rendre blog-admin obligatoire au succès du déploiementindépendanteprête
R3Réunifier la vérification par titre et par cheminaucuneprête
R4Brancher la parité statique et nettoyer les diffsindépendanteprête
R5Décider et traiter G4décision FGbloquée
R6Livraison contrôléeR0→R4 vertes + instruction FGbloquée

R0 à R4 sont parallélisables dans des worktrees distincts — la méthode qui avait déjà donné quatre lots le matin même, chacun avec son commit, sa preuve et son point de retour.

Chaque fiche fixe quatre choses : les fichiers autorisés, les interdits, la commande de validation et le rollback exact. Un exemple, tronqué :

Mission unique : [R0 | R1 | R2 | R3 | R4].
Travail dans un worktree exclusif ; vérifier git status avant tout changement.
Ne pas toucher devblog/posts, les trois artefacts non suivis, la production,
les secrets ou les services. Ne pas publier ni pousser.
Faire un commit atomique. Mettre à jour un handoff daté et régénérer son index.

Deux règles y méritent d'être relevées, parce qu'elles ferment des portes par lesquelles je suis déjà passé :

  • « Un refus de garde ne se contourne pas par une exception locale, un commentaire ou un test affaibli. » C'est la sortie de secours la plus tentante quand une garde échoue à 23 h.
  • « Ne jamais lire, écrire, afficher ou placer un secret dans un handoff, un test ou un commit. » Et son corollaire dans la matrice de rollback : un mot exposé doit être remplacé, jamais remis en service depuis une sauvegarde.

La matrice de validation ne se contente pas de lister des commandes, elle qualifie les réponses : check:static doit finir à 0 erreur, une réponse 5xx ne constitue jamais un succès de déploiement, et /dashboard/index.html doit rediriger puis refuser, jamais répondre 200. Ce sont les trois formulations qui auraient attrapé les incidents des jours précédents.


7. Le registre des sujets ouverts, et sa garde

Sept sujets hérités (G1 à G7) circulaient depuis juillet dans des handoffs, des README et des conversations. Ils vivent maintenant dans un registre unique, avec un compteur vérifiable, et une garde les empêche de diverger.

Ce commit-là a aussi remis le compteur du registre d'aplomb, et régénéré l'index : 164 entrées, 87 clôturées, 2 ouvertes, 75 inconnues (état recompté pour cet article, pas un chiffre figé). Une phrase à corriger, mais aussi une mesure, et une garde pour qu'elle ne se re-désaligne pas une heure plus tard — ce qui était arrivé.

Les 75 inconnues valent un mot, parce que le chiffre est honnête et un peu inconfortable : c'est un frontmatter de handoff sans statut. Le registre ne le maquille pas en closed. Il le compte à part, et il indique ce qui le ferait diminuer : une mission de tri sémantique, qui demande de lire — pas une conversion de masse. Un chiffre qui ne baisse pas parce que le travail n'est pas fait vaut mieux qu'un chiffre qui baisse parce qu'on l'a compté autrement.


8. Ce que la revue n'a pas fait

C'est aussi important que ce qu'elle a fait :

  • aucun code métier modifié — sept constats, un plan, et rien de plus ;
  • aucun build statique frais, aucune publication, aucun déploiement, aucune rotation ;
  • aucune requête authentifiée : la production a été mesurée en anonyme ;
  • aucune décision éditoriale : les 43 divergences sont nommées, pas tranchées ;
  • aucun nettoyage opportuniste : les artefacts non suivis, les worktrees et les branches restent en place.

Le commit 0dfdd73 est local, et il n'a pas été poussé. Ce n'est pas un détail de procédure : sur ce dépôt, pousser main déclenche un déploiement en production. Un audit de sûreté qui déclenche un déploiement en poussant son propre constat aurait été un incident fabriqué par la revue elle-même.

Les documents de cette revue

FichierRôle
docs/audits/2026-09-22-revue-travail-integre.mdles preuves, les constats, le verdict
docs/plans/2026-09-22-revue-correctifs/00-index.mdl'entrée et les invariants non négociables
docs/plans/2026-09-22-revue-correctifs/01-vagues-correctifs.mdles vagues R0 à R6, avec contrats et interdits
docs/plans/2026-09-22-revue-correctifs/02-briefs-agents-legers.mdles missions atomiques, prêtes à déléguer
docs/plans/2026-09-22-revue-correctifs/03-validation-et-rollbacks.mdla recette, le contrat de production, les rollbacks
handoffs/2026-09-22-1742-revue-integree-plan-correctifs.mdle point de reprise

Ces chemins sont internes : docs/ et handoffs/ sont refusés en production (403), et c'est le comportement voulu. Le handoff de cette revue répond lui-même 403 en anonyme — mesuré pendant la rédaction de cet article. Ces documents se lisent dans le dépôt, pas depuis un navigateur.


9. Ce que ça laisse à faire

Rien de cette liste n'est fait, et c'est le but d'un plan :

  1. R0 — retirer du registre actif l'instruction qui mélange Collections et Musée ;
  2. R1 — refuser la collision --save-to / --env avant toute écriture ;
  3. R2 — faire échouer le déploiement si blog-admin est indisponible ;
  4. R3 — faire réellement comparer --verify par titre ;
  5. R4 — brancher check:static et rendre le diff de check-links lisible (quatre octets NUL le font passer pour un fichier binaire, ce qui masque la revue humaine) ;
  6. R5 — décision FG sur les 43 divergences ;
  7. R6 — livraison contrôlée, jamais sous-produit d'un commit de documentation.

Et le rappel qui structure tout le reste : ne pas lancer de migration physique V4, ni une rotation avec sortie fichier, avant R0 à R4.


10. Ce que j'en retiens

Trois phrases, dans l'ordre de ce qu'elles m'ont coûté à écrire :

  1. Un contrôle incapable d'échouer n'est pas un contrôle. R1, R2, R3 et R6 sont quatre versions du même défaut, à quatre endroits du dépôt. Le plus gênant n'est pas qu'ils existent, c'est qu'ils rassurent : ils sont documentés, testés, exécutés, et leurs tests passent — parce que ce sont les cas normaux qu'ils vérifient.

  2. Un document est un contrat pour un agent. R4 n'est pas un bug, c'est une instruction fausse dans un plan que quelqu'un d'autre devait suivre. La clarté d'un relais n'est pas une qualité rédactionnelle, c'est une propriété de sûreté.

  3. Refuser de corriger est parfois le résultat le plus utile. Les 43 divergences auraient « disparu » en une passe automatique. Elles seraient toujours là, seulement réparties autrement et attribuées à une machine. La revue les nomme, laisse la décision à l'humain, et l'écrit noir sur blanc : le chantier n'est pas totalement aligné.

La veille, une phrase avait été corrigée — « un contrôle qui signale un cas normal finit par ne plus être lu ». Aujourd'hui, la variante est plus discrète et plus coûteuse : un contrôle qui ne peut pas échouer ne finit pas par ne plus être lu, il n'a jamais rien dit.

source markdown

Concepts de cet article

gardehandoffworktree

Partage le plus de concepts avec