The signals: sectionPresent (not dropped/merged), bgDeltaE (ΔE2000 of spec.backgroundColor vs rendered bg, > BG_DELTA_E_FLOOR = 10 diverges), columnCountMatch (replica columnCount >= source), mediaPresent, fallbackUnstyled (island on a CSS-layout section). evidence carries the measured source/replica values backing the score — no record ships without it (prove-it-works: no evidence ≠ matches).
For each divergent section, state concretely what a 10 (matches the source) looks like, then gap-to-target: climb the escalation ladder (Step 6) to close it and re-score, showing before→after. deriveSectionParityStatus(signals, acceptance) decides match / divergent / accepted — and the run-report verdict re-derives from these records, so you cannot talk a divergent section into a pass. Pixel sampling is now only a fallback when a section has no spec-captured backgroundColor.
Vision review must catch what the upstream gates structurally cannot:
- Semantic misclassification. The reconstruction's coverage + provenance gates check that captured text is PRESENT and not invented — they do NOT check it's rendered with the right semantics. A paragraph mis-rendered as a giant
<h2> (body-as-heading), or an eyebrow duplicated as a trailing line, passes every gate. Only vision catches it → classify as A (spec/extractor wrong) and flag the extractor heading/body classification.
core/html fallback islands. Sections the renderer fell back to (run-report.htmlFallbackSections > 0) carry verbatim source HTML but NOT the source CSS. Text-heavy sections render fine; a CSS-styled section (cards/grid/columns) that fell back renders UNSTYLED — looks broken. Vision-check every island: an unstyled-island regression is a fidelity gap, not a pass. (Root tuning lives in section-coverage.ts TEXT_FLOOR — see replicate/SKILL.md.)
Step 4 — Accessibility checks (warn, not block)
For each representative page, flag the following in the run-report. Do not hard-fail or auto-fix — faithfulness to the source wins.
- Contrast: text/background pairs below WCAG AA (4.5:1). Check the palette entries from
design-foundation.json against the source rendering. Log each failing pair as a warning.
- Alt text: images with missing or empty
alt attributes. Carry the source's alt verbatim; flag any that are absent for human fill — never generate alt copy.
Add all flags to the run-report under a11yWarnings[].
Step 5 — Classify discrepancies
For each divergent section from Step 3, classify — class drives which escalation rung (Step 6) you climb, NOT whether you ship:
| Class | Meaning | Action |
|---|
| A | Spec wrong — the section spec or captured content is incorrect | Re-extract that section spec, then rebuild (rung R3) |
| B | Template dropped info — the pattern/template didn't carry through content that was in the spec | Fix section-mapping or rebuild the affected block markup (rungs R1–R2) |
| C | WP renders differently — a genuine core-block/WP rendering constraint, not a theme authoring error | Only Class C may be accepted by the agent, and ONLY with sampled-pixel proof attached |
Class C is narrow and never a flatten. Flattening (3 cards → 1 column), wrong background color, dropped grid/columns, and dropped media are fixable, never Class C — deriveSectionParityStatus rejects a class-c acceptance for them outright. A true Class-C constraint does not trip the robust signals at all (it shows as match with a high pixel-delta you annotate). Accepting any divergent section is therefore the operator's call (Step 7), not the agent's.
Produce the SectionParity[] records (Step 3) plus, per section, { urlPath, band, class, description, rung }.
Step 6 — Close the gap (escalation ladder)
Every divergent section must be driven toward match. The ladder is climbed per section — each section gets its own climb, so a page with several bespoke sections is not capped at one section's worth of fixes. Each iteration climbs to a STRONGER rung — never re-run the same rung. This strictly-climbing rule is what makes the 5-rung ceiling safe: it cannot degrade into five attempts at the same tweak. Climb until the section re-scores match:
- R1 — theme/CSS fix (
editing-themes): band background color, spacing, inter-section gap.
- R2 — rebuild block markup (
editing-blocks): restore the columns/grid the structured render flattened, from the source spec.
- R3 — re-extract the spec (Class A): if the section spec itself is wrong, re-extract it, then rebuild via R2.
- R4a — AI canonical-block rebuild (
rebuild-section): when R1–R3 can't reach match, rebuild the section into native core blocks from its source HTML + styledHtml + section screenshots + spec + design tokens (richer inputs than R2's spec-only). Assemble that input bundle, dispatch the subagent, then run the four acceptance gates: ① block-markup oracle (liberate_validate_artifacts), ② canonicalization round-trip survives @wordpress/blocks, ③ re-measured section-parity = match, ④ measureSectionCoverage = no loss. Accept the rebuild only if all four pass — the agent cannot self-accept; acceptance is the measured re-score. If any gate fails, fall to R4b.
- R4b — deterministic styled-island floor (no AI): the section's
styledHtml snapshot already ships through the reconstruction as a styled core/html island (provenance html-fallback-styled#<i>), which renders pixel-faithful and clears the unstyled-island signal. Faithful but not block-editable — the floor for genuinely bespoke sections R4a can't map to core blocks. R4b reaching match is a valid pass; R4a (editable) is always tried first.
Each R4a dispatch is a subagent, counted toward the run's subagent ceiling in budget-guard — that is the per-run cost bound on AI spend.
editing-themes, editing-blocks, and rebuild-section are disable-model-invocation: true — you cannot Skill-launch them from this inline-run loop. Apply each rung by dispatching a subagent whose prompt points it at the skill's SKILL.md (the subagent reads the file and applies the fix), matching the subagent-dispatch convention; do not stall on a refused Skill call.
After applying a rung, reinstall the updated theme files via the orchestrator's install path and return to Step 1 to re-capture and re-score. "Known gap" / "where it falls short" is an escalation trigger, not a conclusion — never write it as the terminal state of a shipped run.
Step 7 — Circuit-breaker checkpoint (escalate, don't surrender)
The checkpoint is "ladder exhausted per section" (all 5 rungs — R1, R2, R3, R4a, R4b — tried without match) OR the per-run cost ceiling, whichever comes first — not a per-page iteration tally. The 5-rung ceiling is derived from the rung count, so it stays in sync if rungs change; the strictly-climbing rule (Step 6) prevents thrashing within it. Total AI spend is bounded by budget-guard's subagent ceiling (checkBudget), which pauses the run when reached. When a section exhausts the ladder, or the budget guard signals pause, you do NOT log-and-ship. Stop and ask the operator, surfacing what you tried per rung and the current SectionParity:
- Raise the budget — keep climbing / accept more R4a subagent spend.
- Accept with sign-off — the operator accepts the divergence; record it as
acceptance: { by: 'human', proof: <operator rationale> } on that section. This is the ONLY way a divergent section ships.
- Abandon this page — recorded explicitly, surfaced as a hard
fail in the run-report.
Log per-rung attempts to theme/notes.md. Do NOT silently accept or stop. A divergent section with no human acceptance keeps the run at fail.
Output contract
Return to the orchestrator:
Pass the pageParity[] ({ page, sections: SectionParity[] } per content page) into buildRunReport — the verdict is computed from it. Return to the orchestrator:
{
"passed": true,
"iterations": 2,
"perUrl": [
{
"urlPath": "/",
"archetype": "homepage",
"responsiveness": { "passed": true },
"sections": [
{ "band": "hero", "score": 10, "status": "match",
"signals": { "sectionPresent": true, "bgDeltaE": 1.2, "columnCountMatch"
passed is NOT a separate assertion — it is the run-report verdict computed by buildRunReport over pageParity[]: it is true only when (1) the responsiveness gate passes for all archetypes, AND (2) every content-page section re-derives to match or accepted (a divergent section, or a reconstructed page with NO sampled sections, forces fail). You cannot move a section to "accepted" yourself except a Class-C constraint with sampled-pixel proof — flattening/wrong-bg/dropped-grid/dropped-media never qualify and require the operator's sign-off (Step 7).
Rules
- The responsiveness gate is a hard pass/fail. Per-section visual parity is also a hard gate: any unaccepted
divergent section fails the run.
- Record the measured
SectionParity[] with evidence — never claim a page "matches" without sampled signals. Vision + eyeballing repeatedly mis-judged color/size/gaps; a page with no sampled sections is fail (unverified), not a pass.
- Never self-accept a structural divergence. Flattening, wrong bg, dropped grid/media → fix via the escalation ladder or escalate to the operator. Only a genuine Class-C WP constraint may be agent-accepted, and only with
proof.
- Never auto-fix accessibility issues — flag and move on.
- Never accept a result that failed the responsiveness gate.
- Never generate net-new theme structure — drive
editing-themes and editing-blocks for all modifications.
- 3 iterations per page is a circuit-breaker checkpoint with the operator, not a license to stop and ship. Escalate; don't surrender.