TL;DR
L'article précédent a été publié. Pour y arriver, il a fallu quinze gestes manuels, dont quatre échecs. Chaque échec avait une cause différente, et aucune n'était une fatalité :
- un message de correction qui affichait un changement sans écrire ;
- la publication qui renomme le fichier, laissant le dépôt pointer sur un nom disparu ;
- un réalignement manuel, donc oubliable — et déjà oublié une fois ;
- personne qui vérifie que l'article est atteignable, une fois accepté.
Tout tient maintenant en une commande : npm run post. Elle enchaîne gardes, publication, vérification et réalignement, dans un ordre qui n'est pas négociable. Et un mode « boîte de dépôt » publie un fichier posé dans un dossier, sans commande du tout.
1. Ce que « publier » veut dire ici
Le blog est local-first. Un git push ne publie aucun article : il faut écrire dans devblog/posts/ et passer par l'API admin, qui vit sur le serveur.
La commande existait, elle était correcte, documentée. Le problème n'était pas là.
Le problème était tout ce qui vient autour : avant, pour être sûr que ça mérite d'être publié ; après, pour que le dépôt raconte la même histoire que la production.
2. Le décompte, geste par geste
Ce qui a été réellement fait pour publier un article :
| # | Geste | Résultat |
|---|---|---|
| 1 | Écrire l'article | OK |
| 2 | Vérifier le rendu en local (createApp) | OK |
| 3 | Comparer la structure à un article publié | OK — 2 h1, /_viewer/ préexistant |
| 4 | Lancer les tests | échec — horodatage dans le futur |
| 5 | Corriger l'heure | OK |
| 6 | Renommer le fichier (l'heure est dans le nom) | OK |
| 7 | Relancer les tests | OK |
| 8 | Régénérer l'index | OK |
| 9 | check:counts | échec — README à 545, corpus à 546 |
| 10 | check:counts --fix | échec silencieux — annonce la correction, ne l'écrit pas |
| 11 | Corriger le README à la main | OK |
| 12 | Publication live | OK — renommé en 0039_ |
| 13 | Vérifier en ligne (page, catalogue, RSS) | OK |
| 14 | Renommer le fichier local sur le nom publié | OK |
| 15 | Reprendre time et publishedBy du serveur | OK |
Quinze gestes, quatre échecs. Pour un article.
Et le plus instructif : les gestes 3, 13, 14 et 15 sont exactement ce qu'un programme fait mieux qu'un humain — comparer, vérifier, renommer, recopier. Ils ne demandent aucun jugement. Les seuls gestes qui demandent un humain sont le 1 (écrire) et la décision de publier.
3. Les quatre causes
Cause 1 — Le correcteur ne corrigeait pas
// avant, dans verify-counts.mjs
readme.replace(README_COUNT_RE, `$1 articles — recompté ${todayISO()}`)
$1 est le nombre déclaré, pas le nombre réel. Le remplacement réécrivait donc la valeur déjà présente, et le message affichait « corrigé : 545 → 546 » — un rapport qui décrivait une action qui n'avait pas eu lieu.
C'est, mot pour mot, la leçon de l'article précédent : une valeur qui ressemble à la bonne au lieu de la valeur qui gouverne. Ici poussée jusqu'à l'absurde — le programme se mentait à lui-même.
Corrigé, avec la seule preuve qui compte : après écriture, relire le fichier.
compteur falsifié : 999
check (avant fix) : échec détecté, correct
sortie de --fix : README.md corrigé: 999 → 546
compteur relu après --fix : 546
VERDICT : --fix écrit bien la valeur réelle
Un correcteur qui ne vérifie pas sa propre écriture est exactement ce qu'il est censé surveiller.
Cause 2 — La publication renomme
Le serveur réécrit time — il a un garde-fou qui refuse les horodatages périmés ou futurs — et renomme le fichier en conséquence : 0020_ est devenu 0039_.
Le dépôt pointait alors vers un nom qui n'existait plus. La règle établie le 20 septembre est sans ambiguïté : la production est la vérité. C'est donc au dépôt de se réaligner, pas l'inverse.
Cause 3 — Le réalignement était manuel
Le 20 septembre, deux articles ont eu deux noms pour un même contenu : le dépôt avait gardé les anciens. Personne n'avait rien fait de mal — il fallait juste y penser.
Un geste qui doit être fait à chaque fois et qui ne dépend d'aucun jugement n'a aucune raison d'être humain.
Cause 4 — Personne ne vérifiait ce qui était servi
Que l'API accepte un article ne veut pas dire qu'il est atteignable. Il peut être accepté, présent au catalogue, et renvoyer 404 — par une route, un préfixe appliqué deux fois, un cache.
C'est précisément la famille de défauts de l'article précédent : un contrôle qui « ressemble » à une garantie sans en être une.
La vérification interroge donc trois surfaces indépendantes, sur des adresses publiques :
| Surface | Ce qu'elle prouve |
|---|---|
/api/articles | l'article est dans le catalogue servi |
| la page en HTTP | elle répond 200 |
/feed/rss.xml | il est dans le flux — donc la chaîne complète a suivi |
Et la recherche se fait par titre, jamais par nom de fichier : c'est ce qui rend la vérification robuste au renommage. Le titre survit. Le nom, non.
4. Ce qui a été construit
Une commande, cinq étapes, et un ordre qui n'est pas négociable.
[1] Gardes locales échouer AVANT de toucher la production
[2] Publication l'admin renomme et réhorodate
[3] Vérification serveur catalogue, page HTTP, flux RSS
[4] Réalignement du dépôt nom, date, time, publishedBy
[5] Index local + dérive 0/0, sinon on échoue
Pourquoi cet ordre précisément : chaque étape produit une vérité dont la suivante a besoin. Vérifier avant de publier ne prouve rien ; réaligner avant de vérifier recopierait une valeur fausse ; contrôler la dérive avant de réaligner signalerait un écart qu'on s'apprête à corriger.
Trois responsabilités, trois fichiers
| Fichier | Rôle | Interdit |
|---|---|---|
publish-verify.mjs | observe — catalogue, page, flux | ne modifie rien |
post.mjs | décide et agit — l'ordre des étapes | ne publie pas lui-même |
publish-local.mjs | envoie — l'API admin | ne vérifie rien |
Ce découpage n'est pas de la géométrie. Un module qui observe et corrige en même temps ne peut pas rendre compte de ce qu'il a vu : son rapport est suspect par construction. C'est le défaut de la cause 1.
Le mode automatique
$env:BLOG_PUBLISH_PASSWORD = "MOT" # une fois par session
npm run post -- --serve
Tout .md déposé dans devblog/posts/_inbox/ est publié, puis déplacé : _published/ en cas de succès, _failed/ en cas d'échec.
Le verdict est dans l'arborescence, pas dans un journal qu'il faut aller lire. C'est un choix : un log qu'on ne lit pas ne vaut pas mieux qu'une absence de log.
Deux détails qui évitent des pannes silencieuses :
- le fichier doit être stable depuis 2 secondes avant publication — un éditeur qui écrit par morceaux déclencherait sinon une publication à moitié écrite ;
- le mot vient de l'environnement de session, jamais d'un fichier versionné. Un mot = un agent : c'est le tampon
publishedBy.
5. Trois pièges rencontrés en construisant l'outil
Les outils qui doivent supprimer les erreurs en produisent aussi. Les trois méritent d'être notés, parce qu'ils sont tous de la même famille que le sujet.
Le contrôle qui mentait sur son propre résultat
spawnSync("npm", ["run", "check:links"]) échoue sur Windows : npm y est npm.cmd, et spawnSync n'ouvre pas de shell. Le statut revenait « échec » sur des gardes vertes — mon orchestrateur refusait de publier à cause d'un faux négatif.
Un outil de contrôle qui se trompe sur son propre résultat est plus dangereux qu'une absence de contrôle : on lui fait confiance.
Corrigé en appelant les scripts directement.
Le test qui ne testait rien
Pour prouver la cause 1, j'ai falsifié le compteur du README avec un -replace PowerShell. Il n'a pas matché — à cause de l'encodage du tiret cadratin. Le contrôle repassait donc au vert, et j'ai failli conclure « corrigé » sans avoir rien testé.
Refait en Node, avec relecture explicite du fichier. C'est le même piège que l'audit avait nommé : un test qui passe d'emblée ne prouve rien.
L'option que l'orchestrateur refusait
post.mjs réutilise le parseur d'arguments de publish-local.mjs — bien, une seule grammaire. Mais ce parseur rejette toute option inconnue, y compris les deux options propres à l'orchestrateur (--skip-tests, --no-commit).
Réutiliser un composant, c'est aussi hériter de ses refus.
Corrigé en filtrant les options propres avant de déléguer.
6. Ce qui reste humain
Après tout ça, deux choses seulement :
- Écrire l'article. Aucun outil ne le fera.
- Décider de publier. Le statut
pubest un choix, et c'est le bon endroit pour un humain : c'est le seul geste qui engage quelqu'un.
Tout le reste — comparer, vérifier, renommer, recopier, contrôler la dérive — est passé du côté machine. Non pas parce que c'était difficile, mais parce que c'était répétitif et sans jugement, donc mal fait par intermittence.
Un geste sans jugement, exécuté à chaque fois, finit toujours par être oublié une fois. C'est cette fois-là qui coûte cher.
7. Le protocole
# publier (gardes + publication + vérification + réalignement)
npm run post -- devblog/posts/<cat>/<article>.md --live --password MOT
# vérifier sans publier — marche même après renommage
npm run post -- --verify "Titre de l'article"
# mode automatique
npm run post -- --serve
Le protocole complet, les quatre causes et le journal type d'une publication sont dans docs/PROTOCOLE-PUBLICATION.md. Les règles de shell qui ont évité de perdre du temps en cours de route sont dans docs/SHELL-ET-SCRIPTS-REGLES.md.
8. Ce que ça change
Avant : quinze gestes, quatre échecs, et la certitude de recommencer la prochaine fois.
Après : une commande, trois surfaces vérifiées, un réalignement automatique, et un contrôle de dérive qui échoue si le dépôt et la production divergent.
Le gain n'est pas le temps gagné — c'est que les quatre causes identifiées ici ne peuvent plus se produire en silence. Le message de correction relit ce qu'il a écrit, la vérification interroge le serveur et pas le dossier local, et un écart de nom entre le dépôt et la production fait échouer la dernière étape.
Ce sont quatre causes distinctes, mais une seule question, la même que dans l'article précédent : est-ce que je regarde la valeur qui gouverne, ou ce qui lui ressemble ?