Screenshot to code — where the product is the loop, not the first guess.
$ python -m render_loop target.rl
::::::::::::::::::::............................................
::::::::::::::::::::............................................
::::::.::::::...................................................
TTT.............................................................
outcome: budget
6 attempt(s), 6 render(s), best score 10.7500
#1 score 12.0000 best 12.0000 1 matched, 3 missing, 0 extra
...
#6 score 10.7500 best 10.7500 1 matched, 3 missing, 0 extra
The default generator got closer every round and never arrived, and the loop said so instead of returning its last attempt with a confident tone.
Generate markup, render it, compare the render to the target, correct, repeat. Every screenshot-to-code demo has that loop. Three things decide whether it is worth running, and none of them are the model.
Part of Agent Lab · Vol 2 · Project 01 — tools that watch, convert and triage. Standard library only. 25 tests, no API key, no network, no browser.
git clone https://github.com/dev48v/render-loop
cd render-loop
python -m venv .venv && . .venv/Scripts/activate # Linux/macOS: . .venv/bin/activate
pip install -e ".[dev]"
python -m render_loop examples/target.rl # render it, then try to reproduce it
pytest -q # 25 testsThe obvious comparison is "how many pixels differ". It is also the one that guarantees the loop never finishes.
Two renders can be structurally identical and differ on thousands of pixels — a font advanced differently, an edge landed on a half-pixel. So the score never reaches zero, and worse, past a point it stops discriminating: every wrong answer looks equally wrong, so the signal stops pointing anywhere.
That is a testable claim, so it is a test:
def test_pixel_diff_saturates_where_structural_diff_still_discriminates():
near = render(TARGET.replace("width=20", "width=34"))
far = render(TARGET.replace("width=20", "width=48"))
assert s_far > s_near # structural still ranks them
assert abs(p_far - p_near) < (s_far - s_near) / 10 # pixel barely separates themThe structural comparison matches boxes to boxes and scores position, size and colour. It reaches exactly zero when the layouts agree, which is the property that lets a loop terminate at all.
assert structural_diff(boxes, boxes).value == 0.0Both comparisons are implemented and both are measured, because "pixel diff is worse" is an opinion and a convergence curve is a finding.
Every correction can make things worse. A loop that returns its final attempt returns whatever its last mistake produced.
report = run_loop(TARGET, scripted([good, awful, awful]), patience=5)
assert report.best_source == good # not the last thing it built
assert report.attempts[-1].score > report.best_scoreOne variable. It is the difference between "here is my best attempt" and "here is where I happened to stop".
This is easier to get wrong by accident than it sounds — one shared object is enough. The proposer receives the previous source, its rendered boxes, and a score. Never the target markup.
def test_the_generator_never_receives_the_target_source():
run_loop(TARGET, spy, max_attempts=2)
assert TARGET not in seen # only the previous source, never the target
assert seen[0] == "" # and the first call starts from nothingThere is a peeking() generator in the repo that does get handed the answer. It exists
purely as a control: it matches on the first attempt, and nothing else in the suite is
allowed to. A test that the honest loop is honest means very little without something that
shows what cheating would have looked like.
| outcome | meaning |
|---|---|
matched |
the render agrees with the target under the chosen comparison |
no_improvement |
two rounds with no better score — it is going in circles |
budget |
attempts exhausted while still improving; the last resort |
gave_up |
the generator had nothing left to try |
unrenderable |
the target does not parse, so nothing can be evaluated |
unrenderable stops before a single candidate is generated. There is no signal to work
against, so any output would be a guess.
And unrenderable output is an attempt, not a crash:
report = run_loop(TARGET, scripted(["blink width=2", "blink width=3"]), patience=2)
assert report.outcome == "no_improvement"
assert "did not render" in report.attempts[0].noteNo browser, no fonts, no network. A block layout engine over four elements:
col gap=2
box width=20 height=4 bg=#ff0000
row gap=1
box width=6 height=3 bg=#00ff00
box width=6 height=3 bg=#0000ff
text size=1 Hello
Layout is a pure function from document to positioned boxes — same input, same output, always — which is exactly what makes a structural comparison possible. Font metrics are two fixed constants, on purpose: real text shaping is the single largest source of pixel-level disagreement, and this project is about the loop.
Numeric attributes are validated at parse time rather than at layout time. A generator
that emits width=wide should be told on the spot, not from a stack frame three calls away.
def model_proposer(previous_source: str, boxes: list[Box], score: Score, n: int) -> str | None:
reply = call_your_model(render_prompt(previous_source, describe(boxes), score))
return extract_markup(reply) # or None to give up
run_loop(target_source, model_proposer, max_attempts=12, patience=2)The comparison, the best-so-far tracking and the five outcomes never learn what a model is.
The shipped default (nudging) can only widen one box, and cannot solve a general target —
on purpose, so the out-of-the-box run is an honest no_improvement rather than a
demonstration that only works on the example.
src/render_loop/
layout.py parse -> tree -> positioned boxes -> raster. A pure function.
compare.py pixel_diff and structural_diff, side by side
loop.py the loop, best-so-far, and the five outcomes
__main__.py python -m render_loop
tests/ 25 tests, including the control for every honesty claim
MIT