RenderLoop — complete documentation (llms-full.txt) ==================================================== Source: https://renderloop.jnk.ovh/en/ — pages: https://renderloop.jnk.ovh/en/docs/, https://renderloop.jnk.ovh/en/agents/ and https://renderloop.jnk.ovh/en/demo/ French full text: https://renderloop.jnk.ovh/llms-full.txt Repository: https://github.com/linquant/render-loop Version: 0.4.0 — last updated: 2026-10-20 License: MIT. Author: Johann Nowacki. Composer package: linquant/render-loop. This document is standalone: it contains enough to answer usage questions without reading the site's HTML. 1. In one sentence ------------------ RenderLoop is a render feedback loop for PHP applications: capture the rendered HTML of a page before a change, then compare it afterwards, so you only see the real change — noise (DebugBar, scripts, CSRF tokens, timestamps, site chrome) being masked automatically. 2. The problem -------------- After a code change, the useful question is not "what did I write?" but "what did the rendered page change?". A `git diff` shows the source edit; it shows neither the indirect effects (a shared include, a common function, a query that empties a table), nor a change made in parallel on the same page. On a large legacy codebase where a page has no tests, those effects go unnoticed. RenderLoop compares the rendered state before and after. Anything that changes outside the intended edit is a regression. git diff RenderLoop shows YOUR edit -> shows the RENDERED change - $price = 1295; p.price : "1295.00" -> "995.00" + $price = 995; + a shared include emptied the cart 3. The loop ----------- 1. Capture the rendered state before touching the code: `-a capture -u -t before`. 2. Change the code (by hand or through an agent), and make sure the server actually serves the new file (opcache, build steps). 3. Run `-a recheck -u ` again. The diff must contain only the intended change. Everything else is a regression. Without a "before" capture there is nothing to compare: that is pitfall number one. 4. Installation and requirements -------------------------------- composer require linquant/render-loop The binary is then called `vendor/bin/render-loop` (in a repository checkout: `bin/render-loop`). Requirements: PHP 8.2 or higher, with `ext-dom`, `ext-libxml`, `ext-json` and `ext-curl`. Composer requires `ext-curl` at install time; at runtime, only capturing over an HTTP request uses it — the strategy that pushes already rendered HTML does not call curl. Composer installs symfony/console (6.4, 7.x or 8.x), symfony/css-selector and sebastian/diff. For development: PHPUnit 11 or 12 and PHPStan 2 (level 10 / max on `src` and `tests`). In the examples, `render-loop` refers to `vendor/bin/render-loop` when `vendor/bin` is in the `PATH`. 5. Quick start -------------- # 1. capture the state BEFORE the change vendor/bin/render-loop -a capture -u "http://localhost:8000/my_page.php" -t before # 2. change the page (by hand or through an agent) # 3. recheck: the diff must contain ONLY the intended change vendor/bin/render-loop -a recheck -u "http://localhost:8000/my_page.php" 6. CLI reference ---------------- General form: render-loop -a [options] The default action is `recheck`. Two output formats: `table` (default, human-readable) and `json` (`-f json`, meant for machines and agents) for the actions that support it — see the list in §8. 6.1 capture ----------- Fetches a page over HTTP (curl) and saves its rendered HTML as a snapshot. render-loop -a capture -u "http://localhost:8000/page.php" -t before Options: `-u/--url` (required), `-t/--tag` (label, e.g. `before`), `-d/--dir`, `-c/--config`, `--allow-host`, `--cookie-name`, `--method`, `--header`, `--data`, `--ajax`. Text output: `Snapshot saved: `, where the identifier has the form `_` (for example `page.php_1792491901.2345`). Exit code 0 on success, non-zero on error (URL rejected by the guard, HTTP status greater than or equal to 400, curl failure). Capturing an internal AJAX endpoint (fragment) without a browser: render-loop -a capture -u "http://localhost/ajax/price.php?id=42" --ajax -t before `--ajax` adds the `X-Requested-With: XMLHttpRequest` header (no duplicate if it is already provided through `--header`). For a POST endpoint: render-loop -a capture -u "http://localhost/search" --method POST --data "q=bag&page=2" --ajax -t before `--data` implies POST if `--method` is not provided. `--header` is repeatable, in the `"Name: value"` format. 6.2 store --------- Saves already rendered HTML, read from standard input. Used for JS/SPA pages whose HTML is produced by a headless browser or another tool. cat rendered.html | render-loop -a store -u "http://localhost/app" -t before Options: `-u/--url` (required), `-t/--tag` (default `store`), `-d/--dir`, `-c/--config`. An empty STDIN causes an error and a non-zero exit code. 6.3 recheck ----------- Recaptures the page and compares it to its latest reference: the most recent capture that is not itself a recheck (or the explicitly designated baseline). This is the default action: several successive rechecks therefore do not make the comparison base drift. render-loop -a recheck -u "http://localhost/page.php" render-loop -a recheck -u "http://localhost/page.php" -f json Options: `-u/--url` (required), `-d/--dir`, `-c/--config`, `-f/--format`, `--max-hunks`, `--exit-code`, `--expect-selector`, `--allow-host`, `--cookie-name`, `--method`, `--header`, `--data`, `--ajax`. Exit code: 0 even if the diff is not empty, except with `--exit-code` (behaviour of `git diff --exit-code`). With `--expect-selector`, the confinement verdict drives the exit code. 6.4 compare ----------- Compares two specific snapshots, designated by their identifier. An identifier has the form `_`, for example `page.php_1792491901.2345`. render-loop -a list -f json render-loop -a compare --from=page.php_1792491901.2345 --to=page.php_1792491902.6789 Options: `--from` and `--to` (required), `-d/--dir`, `-c/--config`, `-f/--format`, `--max-hunks`, `--exit-code`, `--expect-selector`. Same output shape and same exit codes as `recheck`. 6.5 list -------- Lists the saved snapshots, optionally filtered by URL. render-loop -a list render-loop -a list -u "http://localhost/page.php" -f json Options: `-u/--url`, `-d/--dir`, `-f/--format`. JSON output: array of objects `{"id": string, "url": string, "tag": string|null, "datetime": string}`. Exit code 0. [{"id": "page.php_1792491901.2345", "url": "http://localhost/page.php", "tag": "before", "datetime": "2026-10-20 10:15:00"}] 6.6 calibrate ------------- Captures the same page several times without any change: anything that differs between identical captures is noise (timestamps, counters, tokens). The action suggests the selectors to mask, or writes them to the configuration with `--apply`. render-loop -a calibrate -u "http://localhost/page.php" render-loop -a calibrate -u "http://localhost/page.php" --samples=4 --apply -c normalizer.json Options: `-u/--url` (required), `--samples` (number of captures, minimum 2, default 2), `--apply`, `-c/--config`, `-d/--dir`, `-f/--format`, `--allow-host`, `--cookie-name`, `--method`, `--header`, `--data`, `--ajax`. Two captures are about 1.1 seconds apart to cross a second boundary and make timestamps vary. More samples catch slower noise. JSON output: `{"candidates": [string], "applied": [string]}`. Exit code 0, including when no noise is detected. 6.7 mask-add ------------ Adds noise selectors to the configuration without going through calibration. Useful when you already know that a chat widget or a cookie banner is noise. render-loop -a mask-add --selector='.chat-widget' --selector='.cookie-banner' -c normalizer.json Options: `--selector` (required, repeatable), `-c/--config`, `-f/--format`. The operation is idempotent: no duplicate is added. JSON output: `{"applied": [string]}` with only the selectors actually added. Non-zero exit code if no `--selector` is provided. 6.8 capabilities ---------------- Returns the machine contract of every action: name, summary, required and optional options, shape of the JSON output, semantics of the exit codes. render-loop -a capabilities -f json Options: `-f/--format`. Without `-f json`, it prints a readable summary that points to the JSON version. Exit code 0. This is the recommended entry point for an agent that does not want to parse the help text. 6.9 advise ---------- Returns the operating procedure meant for an AI agent: the loop to follow, how to read the output, and the pitfalls. Where `capabilities` describes the "what", `advise` conveys the "how". render-loop -a advise render-loop -a advise -f json Options: `-f/--format`. JSON output: `{"audience": string, "one_liner": string, "workflow": [string], "reading_the_output": [string], "pitfalls": [string]}`. Exit code 0. The content is authoritative at runtime; the repository's AGENTS.md file mirrors it for agents that read the repository first, but in case of divergence, the CLI wins. 6.10 golden-record ------------------ Pins the current rendered output of a suite of representative cases, before a refactor (approval testing / characterization testing). render-loop -a golden-record --suite=product Options: `--suite` (required), `--golden-dir` (default `golden`), `-c/--config`, `-d/--dir`, `--allow-host`, `--cookie-name`, `--method`, `--header`, `--data`, `--ajax`. The `golden/.json` manifest declares the cases and the mask: { "name": "shop", "cases": [ { "id": "catalog", "url": "http://localhost:8123/index.php" }, { "id": "product-aeron", "url": "http://localhost:8123/product.php?sku=aeron" } ], "mask": [".phpdebugbar", ".server-time", "input[name=csrf_token]"] } The normalized output is written to `golden//.html`. This file must be committed: the diff is then reviewed in git like an `.approved` file. Text output: number of pinned cases and size of the mask. The manifest's mask is enriched automatically on record, then written back to the file. 6.11 golden-verify ------------------ Recaptures each case of a suite and compares it to its golden. Fails on any rendering drift. render-loop -a golden-verify --suite=product Options: `--suite` (required), `--golden-dir`, `-c/--config`, `-d/--dir`, `--allow-host`, `--cookie-name`, `--method`, `--header`, `--data`, `--ajax`. Output is always text: `-f json` is not supported by this action. Output per case: `OK`, `CHANGED` (with summary and unified diff) or missing golden. Non-zero exit code if at least one case changed or is missing; 0 if everything matches. Important point: capture against frozen data, so that the diff reflects the code and not the data. 7. Cross-cutting options ------------------------ - `-a/--action`: capture, store, recheck, compare, list, calibrate, mask-add, golden-record, golden-verify, capabilities, advise. Default: recheck. - `-u/--url`: URL of the page. - `-t/--tag`: snapshot label (e.g. `before`). - `--from`, `--to`: snapshot identifiers for `compare`. - `--suite`: name of the golden suite (manifest `/.json`). - `--golden-dir`: directory of the golden suites. Default: `golden`. - `-d/--dir`: snapshot storage directory. Default: `.render-loop/snapshots`. - `-c/--config`: normalizer configuration file (noise selectors). Default: the `config/normalizer.json` shipped with the package. - `-f/--format`: `table` or `json`. Default: `table`. - `--max-hunks`: maximum number of hunks displayed. Default: 40, minimum 1. - `--allow-host`: host allowed in addition to `localhost` and `127.0.0.1`. Repeatable. - `--cookie-name`: name of the session cookie. Default: `PHPSESSID`. - `--samples`: number of calibration captures (minimum 2). Default: 2. - `--apply`: `calibrate` writes the selectors it found to the configuration. - `--selector`: noise selector for `mask-add`. Repeatable. - `--expect-selector`: for `recheck` and `compare`, allowed scope of the change. Repeatable. - `--exit-code`: non-zero exit code when the diff is not empty (`recheck`, `compare`), like `git diff --exit-code`. - `--method`: HTTP method of the capture (`POST`, etc.). Default: GET, or POST if `--data` is provided. `--header` adds a `"Name: value"` header (repeatable), `--data` provides the body, `--ajax` adds `X-Requested-With: XMLHttpRequest`. 8. JSON format -------------- The actions `recheck`, `compare`, `list`, `calibrate`, `mask-add`, `capabilities` and `advise` accept `-f json`. Only `recheck` and `compare` return the diff summary; the others have their own shape, described below. `capture`, `store`, `golden-record` and `golden-verify` always write text: the flag is ignored there. JSON outputs are readable (indentation, Unicode and slashes not escaped). 8.1 Diff (`recheck`, `compare`) { "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": "--- ...\n+++ ...\n@@ ..." } - `lines_added`, `lines_removed`, `hunks`: counters of the unified diff. - `truncated`: true if the displayed diff was capped by `--max-hunks`. - `tags_touched`: names of the tags present in the modified lines. - `changes`: semantic layer, one entry per localized change. - `kind`: `text` (modified content), `added` (node appeared) or `removed` (node disappeared); `before` and `after` carry the values depending on the type. - `selector`: best-effort CSS path, degrading from `#id` to `tag.class` then to `tag:nth-of-type(n)`. - `context`: text of the nearest heading or label, a readable anchor on legacy HTML without id or class (the `

`, a ``, the previous `` of a key/value row). `null` if nothing close fits. - `diff`: unified diff of the normalized HTML, always present. It is the reliable foundation; `changes` only enriches it. For a reader on a token budget: read `summary` first, only unroll `diff` if the summary is not empty, and decide from the JSON summary rather than from the exit code (`recheck` and `compare` return 0 even with a diff, except with `--exit-code`). With `--expect-selector`, the response gains an `assertion` block: "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": "..." } } `out_of_scope` is a diff summary (same fields as `summary`, plus `diff`). 8.2 Other JSON outputs - `list`: `[{"id": string, "url": string, "tag": string|null, "datetime": string}]`. - `calibrate`: `{"candidates": [string], "applied": [string]}`. - `mask-add`: `{"applied": [string]}`. - `capabilities`: manifest `{"tool": "render-loop", "formats": ["table", "json"], "exit_codes": string, "actions": [...]}`; each action carries `name`, `summary`, `required`, `optional`, `output`, `exit_code`. - `advise`: `{"audience", "one_liner", "workflow", "reading_the_output", "pitfalls"}`. 9. Noise masking and calibration -------------------------------- Before comparison, the HTML is normalized: `