Semantic diff, not pixel diff
HTML is normalized then compared structurally. No headless browser, no screenshot, no arbitrary threshold.
HtmlNormalizer · HtmlDifferRenderLoop captures the rendered HTML of a PHP page, you change the code, then re-check: noise is masked and only the real change surfaces. A semantic HTML diff, with no headless browser, readable by humans and AI agents alike.
# 1. capture BEFORE touching the code
$ render-loop -a capture \
-u http://localhost:8000/cart -t before
Snapshot saved: localhost_cart_1792491901.2345
# 2. … you change the code …
# 3. re-check, as JSON
$ render-loop -a recheck \
-u http://localhost:8000/cart -f json
{
"summary": {
"lines_added": 1,
"lines_removed": 1,
"hunks": 1,
"changes": [
{ "kind": "text",
"selector": "article > p.price",
"context": "Aeron Chair",
"before": "€1,295.00",
"after": "€995.00" }
]
},
"diff": "@@ -41,7 +41,7 @@"
}
One change. Everything else is identical.
On a legacy PHP page with no test, a source diff says nothing about what the page actually renders. A changed shared include, a common function, a query that empties a cart: none of it shows in a code diff. RenderLoop compares the rendered state before and after, and makes everything that moved visible — including what you did not touch.
public function price(): string - return '1295.00'; + return '995.00';
What you wrote. Nothing more.
article.product-detail > p.price - €1,295.00 + €995.00
+ a shared include emptied the cart elsewhere on the page. RenderLoop sees it.
The principle fits in one sentence: capture the rendered state before touching the code, then re-check after. Any unintended difference is a regression.
One HTTP request, one timestamped snapshot. Nothing to instrument in the application.
render-loop -a capture -u "http://localhost:8000/page.php" -t before
By hand, or by an agent. Just make sure the server serves the new file: opcache can lag by a second or two.
# your edit, then restart the server if needed
RenderLoop re-renders the page, masks noise and diffs. The summary comes first; unfold the full diff only when there is something.
render-loop -a recheck -u "http://localhost:8000/page.php" -f json
State upfront which selector your edit is allowed to touch. The verdict is binary, and a leak exits non-zero.
render-loop -a recheck -u "http://localhost:8000/page.php" \
--expect-selector='.price' -f json
No screenshots, no visual tolerance threshold: normalized HTML, compared, with noise declared explicitly.
HTML is normalized then compared structurally. No headless browser, no screenshot, no arbitrary threshold.
HtmlNormalizer · HtmlDifferDebugBar, CSRF tokens, timestamps, banners: masked by CSS selector, never by content regex. A price dropping to 0.00 stays visible.
normalizer.json · mask-addcalibrate captures the same page several times without changing anything. Whatever differs is noise, and the selectors are derived for you.
Pin the rendered output of legacy pages with no tests, refactor, then prove it did not drift. The golden file is committed and read back in git.
golden-record · golden-verify--expect-selector turns “I think it is fine” into a verdict. Any change outside the expected selector fails the command.
Token-budgeted JSON output, a machine contract via capabilities, a modus operandi via advise, and a semantic summary of changes.
The actions recheck, compare,
list, calibrate, mask-add,
capabilities and advise accept
-f json. Only recheck and
compare return this summary, designed for a
token-budgeted reader: read the counters first, unfold the full diff
only when it is non-empty. capture, store,
golden-record and golden-verify always
write text.
lines_added, lines_removed,
hunks, tags_touched and
changes: one entry per localized change, with its CSS
selector, readable context and before / after values.
changes is a best-effort enrichment on messy markup. A
structural insertion can make the array fall back to empty: the
unified diff is always reliable.
{
"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": "€1,295.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": "..."
}
}
}
You cannot write a unit test for a legacy page whose only observable behaviour is its render. You can pin its current output, refactor, then verify it did not move.
golden/<suite>.json manifest declares the cases and the mask.golden/<suite>/<case>.html and is committed.golden-verify checks every case and exits non-zero if at least one drifts.This is characterization, not specification: we do not claim the output is correct, only that it did not change by accident.
# before the refactor: pin the output
$ 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
An autonomous agent needs a short, information-dense, token-cheap signal. RenderLoop answers one question after an edit: “did my change do exactly what it had to, and nothing else?”.
-a capabilities -f json describes every action, its required options, output shape and exit-code semantics. -a advise gives the modus operandi and pitfalls.
render-loop -a capabilities -f json
render-loop -a advise -f json
recheck and compare exit 0 even with a diff. The agent reads the summary, or passes --exit-code to get git diff --exit-code behaviour.
render-loop -a recheck -u <url> --exit-code
render-loop -a recheck -u <url> \
--expect-selector='.price' -f json
A Composer library and a binary. RenderLoop runs in your shell, in CI, or from your own PHP controller.
# 1. the library
$ composer require linquant/render-loop
# 2. capture, edit, re-check
$ vendor/bin/render-loop -a capture -u "http://localhost:8000/my-page" -t before
$ vendor/bin/render-loop -a recheck -u "http://localhost:8000/my-page" -f json
# 3. agents can discover the rest
$ vendor/bin/render-loop -a capabilities -f json
Composer requires ext-curl at install time. At runtime, only the HTTP capture uses it; the “push” strategy does not call curl.
No for server-rendered pages: one HTTP request is enough. For a JavaScript page or a SPA, the HTML is rendered elsewhere and pushed with -a store, which reads it from standard input.
git diff shows your source edit. RenderLoop compares the rendered state before and after, so it also reveals indirect effects: a shared include, a common function, a query that empties a table, a concurrent edit on the same page.
No. RenderLoop observes the render, not computations or side effects. For a critical computation, extract the function and unit-test it. The loop is a third safety net, not a substitute.
Noise is masked by CSS selector. calibrate captures the same page several times without changes: whatever differs is noise, and the selectors are derived. mask-add also lets you declare a known widget by hand.
The session cookie value goes through RENDERLOOP_COOKIE, never as a command argument, so it does not show up in ps. The cookie name is set with --cookie-name.
golden-record pins the current rendered output of a suite of cases. golden-verify replays it and exits non-zero if it drifted. This is characterization: it guarantees no accidental change, not correctness.