Mock Markup — image in, verified page out
The auto-markup / dynamic-markup loops assume targets captured by this repo's own tooling: @1x, pixel width = CSS viewport width, and a reference page that proves the 0/0 floor. A mock arrives with none of that. This mode closes the three gaps in order: normalize the image, transcribe what it shows, loop on the verdict.
Phase 0 — intake (once per image)
vlmkit scan mock mock.png --out targets/target-desktop.png
- Infers the device-pixel scale (@2x Figma exports, @3x phone shots) by matching against common CSS viewport widths, and writes the normalized @1x PNG. Loop against the normalized file, never the raw export — a 2560px-wide retina image renders the attempt at a 2560px viewport and nothing converges.
- If the report says the scale is ambiguous, resolve it yourself
(
--width <design px>when the design width is known; Figma frames are usually 1440 or 1280). - If the image is a real screenshot or has JPEG history (browser
screenshot, exported-then-recompressed asset, anything not a pristine
design-tool PNG), add
--capture real. This writes a sidecar that makesverify markupuse degraded-capture tolerances — compression smears small text, so sub-fragment composition and pixel-presence need looser floors (a pixel-perfect page fails 0/0 against its own JPEG screenshot without this). Capture quality is declared, not auto-detected — you are the one who knows the file's history. - If the report says noisy (component count above ~24): the mock is
photo-heavy or dense. Build the page skeleton with the page loop, but
drive each busy region separately — crop it and use
build component— instead of expecting page-level 0/0 in one pass. - Multiple images at different widths = responsive variants: normalize each, and treat the widths as the breakpoint spec (dynamic-markup Phase A rules apply).
Phase 1 — transcription (once)
Read the normalized image ONCE with your own vision and take thorough
notes: structure, sizes (measure — sizes are spec), colors, and the
complete copy. Real text from the mock, transcribed exactly; there is
no reference page to fall back on, and check copy's placeholder scan
treats lorem-ipsum-style filler as a suspect. Transcription of small
(~14px) text is a measured error source — proper nouns, years, and
·-style separators get silently mangled and no pixel gate will catch
it. Zoom into a crop before transcribing fine print, and if the
requester can supply a copy manifest, ask for one and verify with
check copy --manifest. Manifest matching sweeps disclosure states
(closed <details>, unselected tabs, aria-expanded=false) — copy
inside a collapsed panel passes with provenance, so keep disclosures
in their spec'd default state; never open them just to satisfy the
gate. Matching runs against the VISIBLY rendered text: copy that a
user cannot actually see — font-size:0 / opacity:0 / transparent
color, off-screen positioning, text-indent:-9999px, transforms,
clip/clip-path:inset, zero-size overflow boxes, same-color
camouflage, or sr-only-style visually-hidden text — is reported as
copy-invisible with a reason class, never as satisfied. Render the
line visibly instead of hiding it, and keep assistive-tech-only
strings out of the manifest (it is the user-visible copy spec). When
an invisibility is genuinely deliberate, the requester can accept
that class with --allow-invisible <class> (e.g. visually-hidden
for sr-only) — accepted lines stay listed with their reason. Manifest markdown
headings (# Section) are organizing comments, not required lines.
Note text-transform counts: if CSS uppercases a heading, the user
reads the uppercase form, and a mixed-case manifest line will not
match it. Either way, once composition converges, run
the pixel-side copy check:
vlmkit check copy attempt.html --target targets/target-desktop.png
It crops every rendered text block's bbox out of the target image into
contact sheets plus a worksheet of expected strings. Flag any
character difference — this is the gate that catches Phase-1
transcription errors. The sheets must be read by a different reader
than the transcriber: pass --vlm (API key), or hand the sheet
paths + worksheet to the driver/verifier in your report. Reviewing
your own transcription with your own eyes is measured to fail — the
S9-fresh run misread Imlil as Imili at transcription, misread the
review crop the same way, and reported the copy gate as PASSED while
the typo survived. If no second reader is available, say so in the
report instead of claiming the gate passed. Photos and illustrations
inside the mock are content, not layout: reproduce their box (size,
position, fill approximation or gradient), never try to redraw them.
Phase 2 — the loop
vlmkit verify markup attempt.html --target targets/target-desktop.png [...]
Same discipline as the fixture flows (one fix per round, obey the trend line, fix the first kickback item first), with two mock-specific caveats:
- No
--reference, so no calibration floor. Kickback items are still real by default. A residual may be declared design-tool rendering (and reported instead of fixed) ONLY when ALL of these hold — the S7 run abused this clause to rationalize plain CSS bugs, so the bar is deliberately high:- it is ≤2px thin OR sits inside an embedded photo/illustration region the intake flagged as noisy;
- you re-read the actual crop and name the pixel evidence (colors, alpha gradient) in your report — "survived my fixes" is not evidence;
- it is NOT a solid-fill component ≥8px tall, NOT a page-height delta, and NOT accompanied by gap/ordering items in the same region — those are always your CSS. A page-height error is never an artifact: it is unclosed spacing, and everything below the first wrong gap is its debris. Fix the gap items before touching anything the height error displaced.
- Pixel diff % is advisory. Font rasterization differs between the design tool and Chromium, so expect a higher floor than capture-vs-capture fixtures. Composition 0/0 + height within tolerance is the gate; the diff number is for trend, not for judging done.
Model selection
Measured on this exact task type (S7-fresh A/B, same prompt and
budget): Sonnet reached DONE autonomously in 9 rounds (6x the cost
per run); Haiku 4.5 stalled NOT DONE at 12 rounds on the 1px-divider
endgame (6x cheaper, ~3x faster). Mock work leans harder on the
endgame skills (hairline placement, text-tone matching against
design-tool rasterization) than fixture work does, so the model gap
matters more here:
- Default to Sonnet for a one-off "turn this mock into a page" request that must ship without supervision.
- Use Haiku + driver handoffs when batching many mocks behind a verifier harness and cost dominates; expect to finish hard pages with escalation legs. Full guidance and the escalation pattern: dynamic-markup SKILL.md § Model selection.
Done condition
verify markup prints DONE (all targets 0/0 + height in tolerance
- gates clean or expected-warn-only), the
check copy --targetsheets have been reviewed with zero unexplained character differences, and your report includes the transcribed-copy note and any declared-irreproducible residuals with their reasoning. If the intake step flagged the mock as noisy, the report must say which regions were driven bybuild componentinstead.
Budget and KPIs: as in dynamic-markup (rounds ledger-audited via
.vlmkit/run-ledger.jsonl).