Diff sémantique, pas pixel
Le HTML est normalisé puis comparé structurellement. Aucun navigateur headless, aucun screenshot, aucun seuil arbitraire.
HtmlNormalizer · HtmlDifferRenderLoop capture l'HTML rendu d'une page PHP, tu modifies le code, tu re-vérifies : le bruit est masqué et seul le changement réel apparaît. Un diff HTML sémantique, sans navigateur headless, lisible par un humain comme par un agent IA.
# 1. capturer AVANT de toucher au code
$ render-loop -a capture \
-u http://localhost:8000/panier -t before
Snapshot saved: localhost_panier_1792491901.2345
# 2. … tu modifies le code …
# 3. re-vérifier, en JSON
$ render-loop -a recheck \
-u http://localhost:8000/panier -f json
{
"summary": {
"lines_added": 1,
"lines_removed": 1,
"hunks": 1,
"changes": [
{ "kind": "text",
"selector": "article > p.price",
"context": "Chaise Aeron",
"before": "1 295,00 €",
"after": "995,00 €" }
]
},
"diff": "@@ -41,7 +41,7 @@"
}
Un seul changement. Tout le reste est identique.
Sur une page PHP legacy sans test, le diff de source ne dit rien de ce que la page affiche vraiment. Une inclusion partagée modifiée, une fonction commune, une requête qui vide un panier : rien de tout cela n'apparaît dans un diff de code. RenderLoop compare l'état rendu avant et après, et rend visible tout ce qui a bougé — y compris ce que tu n'as pas touché.
public function price(): string - return '1295.00'; + return '995.00';
Ce que tu as écrit. Rien de plus.
article.product-detail > p.price - 1 295,00 € + 995,00 €
+ un include partagé a vidé le panier ailleurs sur la page. RenderLoop le voit.
Le principe tient en une phrase : capturer l'état rendu avant de toucher au code, puis re-vérifier après. Tout écart non voulu est une régression.
Une requête HTTP, un snapshot horodaté. Rien à instrumenter dans l'application.
render-loop -a capture -u "http://localhost:8000/page.php" -t before
À la main, ou par un agent. Vérifie juste que le serveur sert bien le nouveau fichier : opcache peut retarder d'une seconde ou deux.
# ton édition, puis rechargement du serveur si nécessaire
RenderLoop re-rend la page, masque le bruit et diffe. Le résumé passe en premier, le diff complet seulement s'il y a quelque chose.
render-loop -a recheck -u "http://localhost:8000/page.php" -f json
Dis à l'avance quel sélecteur ton édition a le droit de toucher. Le verdict est binaire, et une fuite sort en erreur.
render-loop -a recheck -u "http://localhost:8000/page.php" \
--expect-selector='.price' -f json
Pas de capture d'écran, pas de seuil de tolérance visuelle : du HTML normalisé, comparé, et un bruit déclaré explicitement.
Le HTML est normalisé puis comparé structurellement. Aucun navigateur headless, aucun screenshot, aucun seuil arbitraire.
HtmlNormalizer · HtmlDifferDebugBar, jetons CSRF, horodatages, bannières : masqués par sélecteur CSS, jamais par regex sur le contenu. Un prix à 0,00 reste visible.
normalizer.json · mask-addcalibrate capture la même page plusieurs fois sans rien changer. Ce qui diffère est du bruit, et les sélecteurs sont déduits pour toi.
Épingle la sortie rendue de pages legacy sans tests, refactore, puis prouve qu'elle n'a pas dérivé. Le golden se commit et se relit dans git.
golden-record · golden-verify--expect-selector transforme « je crois que c'est bon » en verdict. Toute modification hors du sélecteur attendu fait échouer la commande.
Sortie JSON à budget de tokens, contrat machine capabilities, mode d'emploi advise et résumé sémantique des changements.
Les actions recheck, compare,
list, calibrate, mask-add,
capabilities et advise acceptent
-f json. Seuls recheck et
compare renvoient ce résumé, conçu pour un lecteur à
budget de tokens limité : on lit d'abord les compteurs, on ne déplie
le diff complet que s'il est non vide. capture,
store, golden-record et
golden-verify écrivent toujours du texte.
lines_added, lines_removed,
hunks, tags_touched et
changes : une entrée par changement localisé, avec son
sélecteur CSS, son contexte lisible et les valeurs avant / après.
changes est un enrichissement au mieux sur du markup
sale. Une insertion structurelle peut faire retomber le tableau à
vide : le diff unifié, lui, est toujours fiable.
{
"summary": {
"lines_added": 1,
"lines_removed": 1,
"hunks": 1,
"truncated": false,
"tags_touched": ["p"],
"changes": [
{
"kind": "text",
"selector": "article.product-detail > p.price",
"context": "Aeron Chair",
"before": "$1295.00",
"after": "$995.00"
}
]
},
"diff": "@@ -41,7 +41,7 @@ ..."
}
{
"assertion": {
"expected_selectors": [".price"],
"satisfied": false,
"inside_scope_changed": true,
"reason": "1 change(s) outside the expected scope",
"out_of_scope": {
"lines_added": 1,
"lines_removed": 1,
"hunks": 1,
"diff": "..."
}
}
}
Tu ne peux pas écrire un test unitaire pour une page legacy dont le rendu est le seul comportement observable. Tu peux figer sa sortie actuelle, refactorer, puis vérifier qu'elle n'a pas bougé.
golden/<suite>.json déclare les cas et le masque.golden/<suite>/<cas>.html et se commit.golden-verify vérifie tous les cas et sort en erreur si au moins un dérive.C'est de la caractérisation, pas une spécification : on ne garantit pas que la sortie est correcte, seulement qu'elle n'a pas changé par accident.
# avant le refactor : épingle la sortie
$ render-loop -a golden-record --suite=product
Golden recorded: 4 case(s), noise mask = 3 selector(s).
# … refactor …
$ render-loop -a golden-verify --suite=product
OK product-card
OK product-list
CHANGED product-detail — lines_added 2, lines_removed 1
exit 1
Un agent autonome a besoin d'un signal court, cher en information et pas cher en tokens. RenderLoop répond à une seule question après une édition : « est-ce que mon changement a fait exactement ce qu'il devait, et rien d'autre ? ».
-a capabilities -f json décrit chaque action, ses options requises, la forme de sa sortie et la sémantique des codes retour. -a advise donne le mode opératoire et les pièges.
render-loop -a capabilities -f json
render-loop -a advise -f json
recheck et compare sortent en 0 même avec un diff. L'agent lit le résumé, ou passe --exit-code pour retrouver le comportement de git diff --exit-code.
render-loop -a recheck -u <url> --exit-code
render-loop -a recheck -u <url> \
--expect-selector='.price' -f json
Une bibliothèque Composer et un binaire. RenderLoop tourne dans ton shell, dans ta CI, ou depuis ton propre contrôleur PHP.
# 1. la bibliothèque
$ composer require linquant/render-loop
# 2. capturer, éditer, re-vérifier
$ vendor/bin/render-loop -a capture -u "http://localhost:8000/ma-page" -t before
$ vendor/bin/render-loop -a recheck -u "http://localhost:8000/ma-page" -f json
# 3. les agents peuvent découvrir le reste
$ vendor/bin/render-loop -a capabilities -f json
Composer exige ext-curl. À l'exécution, seule la capture HTTP l'utilise ; la stratégie « push » n'appelle pas curl.
Non pour les pages rendues côté serveur : une requête HTTP suffit. Pour une page JavaScript ou une SPA, le HTML est rendu ailleurs puis poussé avec -a store, qui le lit sur l'entrée standard.
git diff montre ton édition de source. RenderLoop compare l'état rendu avant et après, et révèle donc aussi les effets indirects : include partagé, fonction commune, requête qui vide une table, édition concurrente sur la même page.
Non. RenderLoop observe le rendu, pas les calculs ni les effets de bord. Pour un calcul critique, extrais la fonction et teste-la unitairement. La boucle est un troisième filet, pas un substitut.
Le bruit se masque par sélecteur CSS. calibrate capture la même page plusieurs fois sans changement : ce qui diffère est du bruit, et les sélecteurs sont déduits. mask-add permet aussi de déclarer un widget connu à la main.
La valeur du cookie de session passe par RENDERLOOP_COOKIE, jamais en argument de commande pour ne pas apparaître dans ps. Le nom du cookie se règle avec --cookie-name.
golden-record épingle la sortie rendue actuelle d'une suite de cas. golden-verify la rejoue et sort en erreur si elle a dérivé. C'est de la caractérisation : on garantit l'absence de changement accidentel, pas la correction.