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ôle | Résultat |
|---|---|
npm test | réussi |
npm run test:ci | réussi, clone frais simulé |
npm run check:links | réussi ; 116 liens hérités en baseline, 335 hors dépôt |
npm run check:rsync | réussi ; 18 motifs synchronisés entre workflow et fichier d'exclusion |
npm run check:ports | réussi ; 4 ports du dépôt, aucun conflit |
npm run check:counts | réussi ; 559 sources, 559 articles indexés (mesure de la revue) |
npm run check:a11y | réussi |
npm run check:publish | réussi |
npm run check:static | réussi sur l'export présent ; 38 surfaces, 0 erreur |
npm run check:drift | inventaire réussi ; 558 servis, 559 au dépôt, un brouillon attendu |
… --content | échec attendu : 43 divergences de contrat |
git diff --check | 8 défauts d'hygiène de diff |
| Production anonyme | 14 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.
| Vague | Objet | Dépend de | État |
|---|---|---|---|
| R0 | Réparer le contrat documentaire M01/M03 | aucune | prête |
| R1 | Empêcher la collision --save-to / --env | aucune | prête |
| R2 | Rendre blog-admin obligatoire au succès du déploiement | indépendante | prête |
| R3 | Réunifier la vérification par titre et par chemin | aucune | prête |
| R4 | Brancher la parité statique et nettoyer les diffs | indépendante | prête |
| R5 | Décider et traiter G4 | décision FG | bloquée |
| R6 | Livraison contrôlée | R0→R4 vertes + instruction FG | bloqué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
| Fichier | Rôle |
|---|---|
docs/audits/2026-09-22-revue-travail-integre.md | les preuves, les constats, le verdict |
docs/plans/2026-09-22-revue-correctifs/00-index.md | l'entrée et les invariants non négociables |
docs/plans/2026-09-22-revue-correctifs/01-vagues-correctifs.md | les vagues R0 à R6, avec contrats et interdits |
docs/plans/2026-09-22-revue-correctifs/02-briefs-agents-legers.md | les missions atomiques, prêtes à déléguer |
docs/plans/2026-09-22-revue-correctifs/03-validation-et-rollbacks.md | la recette, le contrat de production, les rollbacks |
handoffs/2026-09-22-1742-revue-integree-plan-correctifs.md | le 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 :
- R0 — retirer du registre actif l'instruction qui mélange Collections et Musée ;
- R1 — refuser la collision
--save-to/--envavant toute écriture ; - R2 — faire échouer le déploiement si
blog-adminest indisponible ; - R3 — faire réellement comparer
--verifypar titre ; - R4 — brancher
check:staticet rendre le diff decheck-linkslisible (quatre octets NUL le font passer pour un fichier binaire, ce qui masque la revue humaine) ; - R5 — décision FG sur les 43 divergences ;
- 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 :
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.
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é.
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.