blog.vfg.lol

Quatre contrats qui peuvent maintenant échouer

La revue du 22 septembre laissait quatre gardes qui réussissaient trop facilement. R0, R1, R2 et R4 les rendent refusantes. R3 et un trou de R1 suivent dans un correctif ; R5 et R6 attendent.

TL;DR

L'audit du 22 septembre (docs/audits/2026-09-22-revue-travail-integre.md) mesurait des contrats qui n'échouaient pas quand ils auraient dû. Quatre lots sont sur main depuis la pull request 81 (commit 1aace85) : le registre M01 n'envoie plus vers le Musée, --save-to refuse d'écraser le fichier de secrets, le déploiement échoue si blog-admin ne répond pas, et la CI lance la parité statique. Le merge a déclenché le déploiement, vert, et la nouvelle garde y a vu blog-admin vivant. Une relecture a ensuite trouvé un trou dans R1 ; il est bouché, avec R3, dans un correctif de suivi. R5 attend encore FG.

Une garde qui réussit quand le service est mort ne garde rien.

D'où viennent ces quatre lots

Le pack docs/plans/2026-09-22-revue-correctifs/ transforme cet audit en vagues. Il ne remplace pas le plan d'architecture. Il interdit de publier, de déployer, ou de déplacer le corpus V4 sans une instruction distincte.

Ce tour demandait R0, R1, R2 et R4. R3 (la vérification par titre) n'était pas au mandat. R5 (43 divergences de contenu) et R6 (livraison) restent bloquées : la première attend une source de vérité nommée par FG, la seconde une instruction de déploiement. Les finitions d'usage déjà sur main (commit ae42f6f) n'ont pas été refaites. Les huit articles Configuration IA n'ont pas été republiés.

R0 — M01 ne migre plus le Musée

Le registre disait M01 « débloqué » et demandait de commencer par data/atlas/ ou demo/rapport-rj-vs-cr.html. La fiche M01 ne porte que trois familles : sections/, centralisation/, et la partie corpus de ondes/. L'Atlas et la comparaison RJ/CR sont la fiche Musée.

Le geste a été réécrit dans docs/SUJETS-OUVERTS.md. Les surfaces logiques sont livrées : collections-index et les trois URLs /collections/e-chaines/, /collections/centralisation/, /collections/ondes/. Le déplacement physique reste interdit tant que G1 n'est pas satisfait. --delete est toujours refusé : un ancien chemin survivrait sur le serveur. Aucun fichier de ces dossiers n'a bougé.

R1 — --save-to ne vise plus la source

Le script de rotation écrivait d'abord l'environnement, puis le fichier --save-to, sans comparer les deux chemins. Viser le .env par erreur remplaçait la configuration par la seule liste des mots tournés. La sauvegarde permettait de revenir, mais le redémarrage suivant pouvait partir incomplet.

Avant toute écriture et avant tout redémarrage, la cible est refusée si elle est :

  • le fichier --env, y compris en relatif, en absolu, ou via un lien ;
  • un dossier ;
  • une sauvegarde <env>.bak-… ;
  • un fichier qui existe déjà.

Une cible neuve est écrite en mode 600. Le test de ligne de commande utilise une fixture temporaire : le fichier d'environnement reste identique, aucune sauvegarde .bak- n'apparaît, et la sortie ne contient ni systemctl ni « redémarrage des services ». Aucun secret de production, aucun VPS.

R2 — un admin mort n'est plus un succès

Le workflow code-deploy.yml faisait reload_unit blog-admin || echo …. Le lecteur pouvait répondre pendant que la publication et le panneau étaient morts, et le job restait vert. Il ne regardait pas le port 5220.

Le redémarrage de blog-admin fait maintenant échouer le script. Ensuite, pendant au plus vingt secondes, GET http://127.0.0.1:5220/admin/api/whoami sans identifiant doit répondre 401 ou 200. Tout autre code, ou le silence, sort en erreur avant la ligne « blog-read + blog-admin OK ». Aucune variante « lecteur seulement » n'a été ajoutée : elle demanderait une décision écrite et un statut partiel, pas un vert.

La preuve n'est plus seulement statique. Le merge du 24 septembre a lancé code-deploy (exécution 74). Le journal montre le signal envoyé à blog-admin à 00:40:52, puis blog-admin /admin/api/whoami · 401 à 00:40:58 : six secondes, le délai de redémarrage de l'unité. C'est donc le nouveau process qui a répondu. Pour que ce ne soit plus une question de chance, le correctif de suivi ajoute deux secondes d'attente avant le premier essai : par la voie « signal », l'ancien process pourrait sinon répondre à la place du nouveau.

R4 — la parité statique entre dans la CI

npm run check:static existait. La CI construisait dist/ et ne le contrôlait pas. Le job lance maintenant cette recette juste après npm run build:public.

Décision assumée : prepublish ne gagne pas cette étape. La recette de livraison R6 la liste déjà à part, et c'est le job d'intégration qui doit faire échouer un export fuyard. Le script check:static lui-même n'a pas été assoupli.

scripts/check-links.mjs n'a pas été réécrit. Il n'a plus d'octet NUL littéral : le séparateur est déjà écrit \u0000, ce qui produit le même caractère à l'exécution et laisse le diff lisible. Le test d'hygiène le verrouille. Sept espaces de fin de ligne ont été retirés de la page Baie, et la ligne vide finale du test de dérive aussi.

La recette lancée après prepublish sur cette branche : 1040 fichiers dans dist/, 41 surfaces publiques, 31 avertissements (liens vers /_viewer/, absent de l'export), 0 erreur.

Ce que la garde a vu

npm run prepublish, une fois les quatre lots et leurs handoffs fusionnés : 0 test en échec, verify-publish OK. L'index des handoffs citait alors 182 entrées (101 closes, 6 ouvertes, 75 sans statut). Le registre a été aligné sur ce total. Cet article, ajouté ensuite, fait passer le corpus markdown de 562 à 563 fichiers : le README est recompté dans la foulée.

Ce que la relecture a trouvé après le merge

--save-to refusait le fichier source, un dossier, une sauvegarde et un fichier existant. Il ne regardait pas le dossier de la cible. Reproduit sur une fixture : avec --save-to dossier-absent/mots.txt, le .env était réécrit et sauvegardé, puis l'écriture de la cible échouait. Le mot neuf n'était ni affiché ni enregistré. Les services n'avaient pas redémarré, la sauvegarde permettait de revenir, mais c'est exactement la cible dangereuse que R1 devait refuser. Le refus couvre maintenant un dossier absent ou non inscriptible, avant toute écriture.

R3, laissé hors du premier tour, est fait dans le même correctif : --verify "Titre" passe par le comparateur commun, status compris. Un écart s'affiche champ par champ, et « alignés » n'est imprimé qu'après une comparaison réelle.

Ce qui reste

  • R5 : 43 divergences de contrat entre le dépôt et le servi. FG désigne la source de vérité. Pas de réécriture.
  • R6 : la livraison contrôlée n'a pas été déroulée. Le déploiement du 24 septembre est celui que déclenche un merge sur main, pas la matrice de recette.
  • M01 : préparation documentaire seulement. Pas de git mv.
  • B04 : le mode --serve n'a pas été exercé. Il faut un article approuvé et une instruction de publication.

Les leçons

Un registre qui dit « commence par » choisit le périmètre à la place de la fiche. Le premier mouvement doit nommer les familles autorisées, et l'interdit à côté.

Un outil de secrets qui écrit avant de comparer ses chemins transforme une faute de frappe en indisponibilité. Le refus utile est celui qui n'a encore rien touché, y compris le redémarrage.

Un || echo dans un workflow est une décision de produit : on accepte de réussir sans le service. Si ce n'est pas la décision, la ligne ne doit pas exister.

Une recette qui n'est lancée que par celui qui s'en souvient n'est pas une garde. Elle le devient le jour où la CI l'exige.

source markdown

Concepts de cet article

gardehandoffdériveVPS

Partage le plus de concepts avec