CtrlK
BlogDocsLog inGet started
Tessl Logo

adobe/stardust

Redesign an existing website to make it better. Built on top of impeccable.

71

Quality

89%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

source-fidelity-gate.mdskills/replica/reference/

Source-fidelity gate (the measured heart of replica)

The gate proves an archetype matches the LIVE site — three instruments, per breakpoint, with a hard iteration cap. It replaces the redesign pipeline's craft gates entirely: an archetype ships because it measured true, never because it looked right. Every fix in the loop comes off the instruments; eyeballing is not an input.

Validated (UC1-E1, a typographic retail home page): 8.31% → 2.93% → 1.31% pixel diff across exactly 3 iterations, 0 structural 🔴, height Δ 0 — and the two defects the capture phase missed (span font fork, hero scrim) were both found only by these instruments.

The three probes

ProbeScriptCatchesBlind to
Structural content + type../../diff/scripts/content-diff.mjs (project copy)dropped/mis-slotted headings·eyebrows·CTAs, invented/dropped copy, rendered-face font forks (width probe)geometry
Visual heuristics../../diff/scripts/visual-diff.mjs (project copy)stretched images, dropped wraps, blank renders, surface/ground flips"right text, wrong slot"
Pixel (replica-owned)../scripts/stitch-shot.mjs + ../scripts/pixel-compare.mjseverything the other two abstract away: paint effects, scrims, exact geometry, image cropssemantics (a wrong-but-same-colored word)

Run ALL three — they catch disjoint failure classes; any one alone gives a false "looks fine".

Per-breakpoint procedure

Breakpoints: 1440 AND 360 by default (--breakpoints). Mobile is not free: UC1-E1's gate-passing 1440 prototype measured 24.2% / height Δ −1572px at 360. Each breakpoint is its own full gate pass with its own iteration budget. Gate 1440 first (the geometry lifted from desktop CSS), then 360.

# Serve the prototype from its own dir so relative assets resolve. Verify the
# port is YOURS first (lsof -nP -iTCP:8791 -sTCP:LISTEN); prefer a per-project
# port — a stale server from another stardust project on the shared suggested
# port silently serves a foreign site into the gate (recorded twice, 2026-08).
# On shared machines run gate.sh with --marker "<brand string>": the slug
# default can false-pass against another stardust project sharing the slug
# (both serving a home-proposed.html that contains "home").
(cd stardust/prototypes && python3 -m http.server 8791 &)
PROTO="http://localhost:8791/<slug>-proposed.html"
LIVE="https://<site>/<path>"
W=1440   # then 360
GATE="stardust/replica/gates/<slug>-$W"

# 1. structural — --dismiss keeps consent + timed marketing modals out of the
#    inventory on both sides; add extra selectors for non-standard closers
node scripts/diff/content-diff.mjs "$LIVE" "$PROTO" --profile generic --width $W \
  --main "<content-root>" --dismiss | tee "$GATE/content-diff-iter<N>.txt"

# 2. visual heuristics — --main is a real flag here too (live sites often
#    have no <main>; without it both sides false-flag BLANK RENDER)
node scripts/diff/visual-diff.mjs "$LIVE" "$PROTO" --profile generic --width $W \
  --main "<content-root>" --dismiss --out "$GATE/vdiff" | tee "$GATE/visual-diff-iter<N>.txt"

# 3. pixel — stitched captures on BOTH sides (never fullPage:true)
node scripts/replica/stitch-shot.mjs "$LIVE"  "$GATE/live.png"  --width $W --settle
node scripts/replica/stitch-shot.mjs "$PROTO" "$GATE/proto.png" --width $W
node scripts/replica/pixel-compare.mjs "$GATE/live.png" "$GATE/proto.png" \
  --out "$GATE/diff-iter<N>.png" --threshold 10

On a geo-redirecting site add --locale <tag> to all three (a live side that redirects to a different locale per run is a nondeterministic source); on a bot-managed site that exits 3, escalate with --headed (§ Hardening rule 1).

The live capture is taken ONCE per breakpoint per full gate run and reused across iterations — re-take it only if it is genuinely stale (site changed, capture hardening changed). This is a bot-block control, not just a cost note: content-diff + visual-diff each navigate the live URL per run, so a full 3-iter, 2-breakpoint gate is already ≈12–18 live hits, and hard-CDN sites (recorded: an Akamai-defended luggage retailer) escalate to an IP block after a handful. The prototype capture is re-taken every iteration.

Pass bar (all five, per breakpoint)

  1. content-diff: 0 structural 🔴. 🟡 (body/EXTRA) and 🟠 (font fork) confirmed intended — a substituted licensed font is a permanent justified 🟠; record it once in the ledger.

  2. visual-diff: flags none or justified. A live page's own quirks are justified when the prototype mirrors them (e.g. a 1×1 SEO h1 at x0, a carousel tile at a negative offset — both real UC1-E1 justifications).

  3. pixel diff ≤ 10% full-page — AND no band left unexplained (§ Band breakdown). 10% is the ship bar, not the target; the validated run landed at 1.31%.

  4. height delta: |Δ| ≤ 8px — pixel-compare's own warning threshold is the bar (it prints ⚠ above 8px), so a −9px result is unambiguously a residual, not a pass. A large delta invalidates the % — the overlap crop silently discards the tail, so a short prototype can score deceptively well. Fix heights before trusting anything else.

  5. Chrome crop gate: header band AND footer band each ≤ 2% diff (≥98% match, #115). The full-page bar dilutes the chrome — header/footer are a small share of page pixels but carry disproportionate visual weight and repeat on every page of a rollout. Two field runs shipped full-page-green pages whose chrome measured only 93–97% (lookalike icons, wrong micro-weights, off-by-10px nav rows all fit inside a ≤10% full-page bar). Run ../scripts/crop-compare.mjs over the SAME stitched captures the pixel probe used — no extra live hit:

    node scripts/replica/crop-compare.mjs "$GATE/live.png" "$GATE/proto.png" \
      --y 0 --height <nav-height> --out "$GATE/chrome-header-diff.png"
    node scripts/replica/crop-compare.mjs "$GATE/live.png" "$GATE/proto.png" \
      --y <liveDocH - footerH> --y-b <protoDocH - footerH> --height <footerH> \
      --out "$GATE/chrome-footer-diff.png"

    --y-b gives the footer crop a per-side offset so a small doc-height delta doesn't contaminate it with a false full-band diff. Read the band heights off the section-anchor probe (anchor.mjs prints the footer's [y, height] on both sides).

    Styles diagnose, pixels confirm — run the computed-style parity probe BEFORE any pixel iteration on chrome. ../scripts/chrome-parity.mjs probes the same regions on live and build (default header + footer; add sticky strips with --region strip=<liveSel>|<buildSel>), pairs every text-bearing element by its text, and prints only what differs: family / size / weight / style / line-height / letter-spacing / transform / colour / background / padding / radius, the element rect, the clickable box of links and buttons, and the icon inventory (count + size + signature, paired by order). Recorded: one run found what many pixel-band rounds had not — an italic-vs-normal note, a regular-vs-bold link, a wrong nav link colour, 12px row offsets, a 97×40 vs 71×32 button, six missing icons. Fix every delta, re-run until it is quiet (exit 0), THEN crop-compare — a pixel loop on chrome with parity deltas outstanding is wasted iterations. Each run is one live navigation (budget it like any live probe); --json records both sides as the round's evidence.

    node scripts/replica/chrome-parity.mjs "$LIVE" "$PROTO" --width $W \
      --region header=header --region footer=footer   # + --region strip=<sel>|<sel>

    Glyph-dense chrome has a pixel noise floor — the ONE justified way past the 2% bar, and it is evidence-gated three ways. A footer of ~50 links bottomed out at ~5% pixel diff with family, size, line-height, weight, colour, pitch and positions all numerically identical (recorded): per-glyph antialiasing between a hinted licensed face and the self-hosted webfont dominates, and raw pixel bars over-iterate against noise. A chrome band that FAILS crop-compare may be logged as a justified residual — never a pass — only when ALL three hold, and each is an artifact in the residual entry (§ Residual logging format, cause: "glyph-antialiasing"): (1) chrome-parity.mjs exits 0 for that region at tolerance 1px — every paired text's metrics and position match, no MISSING/EXTRA, icons paired; (2) crop-compare.mjs reports the diff texture as thin-edge (≤15% of differing pixels have ≥5 differing neighbours) — glyph antialiasing is thin, misalignment and missing paint are thick; (3) the region is text-dense (link columns, nav rows) — a band with imagery or icons never qualifies (parity's ICONS finding would not be quiet anyway). One or two of the three is not enough: a quiet parity probe with a THICK texture is a paint defect the probe does not model; a thin texture with parity deltas is a real metric error hiding in noise. The 2% bar itself is unchanged, and the residual is re-verified every gate round like any other justified flag.

    Chrome crops are ELEMENT-ANCHORED per side, never fixed-y — and "chrome" means every site-wide repeating band: header, sticky/quick-link strips, footer. Recorded: the header measured 33.9% and a quick-links strip 19.2% while the full page passed at 6.5% — chrome is small-area, highest-salience and repeats on every page. Two traps: (a) a fixed-y crop produces FALSE reads the moment either side's rhythm shifts — a 35px nav fix moved everything below it and the strip crop read 66% while the strip itself, re-anchored to its own band edges, was at 1.6%. Locate each region on EACH side (its element rect via anchor.mjs, or its band edges via row-profile.mjs's column scan) and pass both anchors (--y/--y-b); every gate round re-reads the anchors. (b) Regions whose live content is authored-volatile — campaign heroes, promo creatives that change between capture and gate — are masked out of the fidelity number with pixel-compare.mjs --mask <yA:h[@yB]> (printed on the verdict line, never silent): they are authored content, not conversion fidelity, and chasing them burns iterations on a moving target.

Applied inconsistency-register entries create expected deltas: cross- reference the entry ID (R-<nn>) when justifying a flag over its zone (preserve-direction.md § Gate interaction).

Calibration honesty — two fidelity regimes, one bar. The validated numbers above (1.31%, Δ0) describe the prototype regime: a standalone prototype gated against the live page, on a typographic page. Pages converted to the delivery platform and gated against the published origin (§ The published-origin gate) carry justified block-model deltas — control UI, split anchors, nondeterministic elements — and landed at 6.9–9.9% in the field while visually faithful. The ≤10% bar covers both regimes; what burns iteration caps is chasing prototype-regime numbers on a published-origin gate. Record which regime a number belongs to in the ledger, and judge each against its own regime's precedent.

Reading the band breakdown

The overall % hides WHERE drift starts. pixel-compare.mjs prints per-500px bands (--band to change); read them top-down:

  • The first hot band (◄◄, >15%) is the actionable one. It points at the section whose height or geometry is wrong at that y-range.
  • Every band below the first hot band is contaminated by the vertical offset that section introduced — do not chase them yet.
  • Fix the first hot band's section (usually a margin/padding/height value — re-lift it from the source CSS rather than nudging), re-capture the prototype, re-compare, repeat.
  • A page with height Δ 0 and uniformly warm bands (no single hot band) has a global fault — wrong base font metric, wrong container width, a missing background — not a per-section one.

The section-anchor probe names the section the band table only points at. ../scripts/anchor.mjs prints [y, height] per top-level section (+ footer + doc height), same shape on both sides:

node scripts/replica/anchor.mjs "$LIVE"  --width $W   # once per fix round at most (live hit)
node scripts/replica/anchor.mjs "$PROTO" --width $W   # free — build-side only

Diff the two outputs, fix the FIRST section whose [y, height] disagrees (top-down — everything below it is offset-contaminated, the same rule as the band table), re-run pixels. Field-validated (a financial-services site, 8 pages): this loop roughly halved iterations vs band-reading alone. ../scripts/gate.sh wraps one full pixel round (stitch both sides — live cached — + compare + verdict) in one command.

Section-level compare (crops) is the escalation when a band stays hot and the cause isn't visible in diff-iter<N>.png — in the validated run it was prepared and never needed, because re-authoring hit exact section heights. crop-compare.mjs (the chrome-gate script, pass-bar item 5) does exactly this for any y-band, not just chrome.

Two row-level instruments replace eyeballing crops (../scripts/row-profile.mjs, runs over the same stitched PNGs — no live hit):

  • Column scan for layout boundaries. Before editing CSS to fix a section height, photo height, band start or card overlap, read the per-column class transitions (white / dark / brand / photo at N x positions) on the capture: node scripts/replica/row-profile.mjs live.png proto.png --columns 7. Recorded: a stacked-crop visual read suggested a 415px photo with a white band under it; the scan of the same capture proved the photo full-bleed to 499px with the "white band" being an overlapping card — the wrong read cost two build/measure cycles. Crop eyeballing is hypothesis; the scan is the measurement.
  • Brand-colour landmarks for vertical alignment. Band percentages say WHERE diffs are, not by how many pixels sections are offset. When a saturated brand colour recurs in every section (CTA buttons are ideal), --color <#rrggbb> lists every row run dominated by it on both sides and pairs them in order: the per-pair delta is each landmark's offset, and the CHANGE in delta between consecutive pairs (gapShift) names the one inter-landmark CSS gap that absorbed the shift. Patch that gap, re-measure, top-down — the same contamination rule as the band table. Recorded: three passes driven this way took a page 16.9% → 11.05% and a 1559px height delta → 48px. Do not tune margins by eye against crops. (Crop with pngjs; macOS sips --cropOffset is unreliable for band crops.)

Wide-viewport fluid check (fluid-vs-fixed is invisible at the gate widths, #116)

Both gate breakpoints render a frozen width: 720px and an authored width: 50% byte-identically at 1440 — and 360 collapses both — so a computed-style lift that recorded the resolved px instead of the sizing MODEL passes every gate and diverges only on wider screens (recorded: a live hero card 940px at 1920 vs a frozen 720px; the CTA row wrapped as a side effect). After the 1440 pass, run a cheap box-map spot check at ≥1920: sample the text-bearing elements' x/width on both sides (the anchor-probe technique at --width 1920, or one extra stitched capture) and compare — a box whose width scales on live but not on the prototype is a frozen fluid value. No full pixel gate is needed at 1920; the box map alone catches the mismatch class. Two rules when reading it: compare the same DOM tier (EDS/section wrappers are full-width by design and false-flag against live INNER containers), and fix upstream — re-lift the authored rule per recreation-procedure.md § Lift the sizing MODEL, don't nudge the px. Sample heights as well as widths, and take one extra sample at an intermediate width (1280 or 1680) when the live layout is fluid: a hero that scales with the viewport on live and is fixed-px on the prototype is identical at 1440 and visibly off at 1512 (recorded).

Iteration discipline

Hard cap: 3 iterations per breakpoint. Matching the validated run's discipline — convergence happened within 3 with the recreation procedure followed; more loops mean the inputs were wrong (values eyeballed instead of lifted, capture unhardened), and the fix is upstream, not a fourth loop.

  • Measure first (iteration 1 IS the map — do not pre-polish).
  • Chrome: parity probe first, pixels second. Before a chrome band's first pixel round, run chrome-parity.mjs and clear its deltas (§ Pass bar, item 5); style deltas are named in one pass, pixels only say where.
  • Every fix cites the instrument line that demanded it.
  • Before counting an iteration, verify the fix changed the render. A byte-identical differing-pixel count after a "fix" means the rule was a no-op (recorded: a padding whose value the EDS section wrapper already carried — the round measured nothing and was burned). The check is free — the count is already on the verdict line; if it didn't move at all, find out why the rule never applied (specificity, wrong selector, value already in effect) before spending another round.
  • Verify geometry fixes on the RULE-BEARING element, cache-free (#117). One field "parity verified" claim was wrong three ways at once: the probe matched a heuristic element ("white column wider than 400px") that wasn't the box carrying the lifted rule — always pair the same semantic element on both sides (the element the fixed rule targets on the build; the element whose source rule was lifted on live); the re-check ran through a CACHED stylesheet (DevTools showed the old rule at its old line number while both hosts already served the fix) — verify serving out-of-band (curl --compressed <css-url> | grep '<new-rule>'; the CSS is gzip-encoded, a bare curl | grep scans binary and silently matches nothing) and re-render in a fresh headless context; and a reviewer's screenshot encodes their zoom — back-compute their CSS viewport from any element with a known percentage rule (a card at 851px under width: 50% → viewport 1702px) and reproduce THAT viewport headlessly before letting their numbers overturn a fix.
  • Probe schedule per fix round: pixels every round; content-diff + visual-diff at milestones — iteration 1, after any fix that touched content or markup (not pure CSS values), and once at final. Across ~25 field fix rounds (financial-services site), pixel-only rounds never regressed structure once it passed, and each content/visual re-run costs 2 extra live navigations — against this doc's own hit-minimization rule. A fix that touched markup re-runs all three; a CSS-value fix re-runs pixels only.
  • The section-anchor probe is the fast inner loop (anchor.mjs, § Band breakdown): run it on both sides, fix the first mismatched section top-down, re-run pixels. Build-side anchor/computed-style passes never navigate the live origin and are FREE — the cap governs live-gate cycles, not measurement.
  • After iteration 3: log residuals (§ Residual logging) and move on. A documented residual is a pass with an asterisk; an undocumented fourth loop is scope creep.
  • Instrument-invalidated runs don't consume the cap — once the defect is fixed and named. The 3-iteration cap assumes valid instruments. When a run is later shown to have measured an instrument defect (a challenge page, a font fork forced by the capture itself — rule 14), the honest ledger practice is: count the runs, mark which ones measured the defect state, and exclude those from the cap, with the instrument fix named in the ledger. This legitimizes the exclusion without weakening the cap — an unnamed "the instrument felt wrong" is still a spent iteration.
  • Hit minimization: ONE live navigation per instrument per breakpoint per full gate run. The live stitch PNG is captured once and reused across iterations; only the prototype side re-captures. On hard-CDN sites (Akamai-class), take the live captures with --headed and treat further live hits as spent budget — the recorded failure mode (luggage retailer) was an IP-level block escalating within ~3–4 automated requests, after which iteration 2's numbers measure the block, not the site. A challenged headless run costs exactly 1 hit: gotoLive throws BotChallengeError on the first challenge-classified response (the wait+reload solve window runs only under --headed, where clearance can actually land) — so the block budget is still intact when you escalate.
  • Media-density budget. The ≤3-iteration convergence was validated on a typographic, low-image page (the retail home). Image-dense commerce homes (recorded: a fashion retailer, ~130 imgs) spend iterations on media parity — populating grids, matching crops — before geometry work even starts. Budget accordingly: on a media-heavy page, image/media parity IS iteration 1's job; geometry starts at iteration 2.
  • A pixel pass at the wrong metric is debt — spot-check base typography against live computed styles once the gate passes. An archetype shipped 16px card body text (live: 17.6px) and still passed at 5.24% because the tuned spacing absorbed the size error; siblings with more text amplified it into extra wraps and +80..250px heights (recorded). After the pass, read body font-size/line-height per block on both sides (a computed-style probe — build-side runs are free) and re-fit rhythm at the true metric. Compensating spacing is the tell.

Hardening rules (false-measurement traps)

Each of these was hit live; skipping one silently corrupts the measurement rather than erroring.

  1. Real-Chrome UA + the standard request headers on every capture and probe. The default HeadlessChrome UA can receive a Cloudflare managed challenge, and the probe then measures the challenge page as the source (3 headings, "Performing security verification" — it diffs cleanly, wrongly). And the UA alone is NOT sufficient: field-proven (F-R1, a nonprofit site), a real-Chrome UA with Playwright's minimal default headers still got HTTP 403 from Akamai; adding the standard set every real Chrome sends (Accept, Accept-Language, Upgrade-Insecure-Requests, sec-ch-ua*) produced HTTP 200 — Akamai bot-manager fingerprints on the absence of those headers, not just the UA. All three instruments now send both by default via the shared diff/scripts/live-session.mjs; --ua overrides the UA string only. The header set rides document requests only (F-B2, financial-services site): forcing it on every request makes cross-origin CORS-mode webfont fetches non-simple and kills them with net::ERR_FAILED — the capture then silently renders fallback type (see rule 14); bot managers fingerprint the navigation request, which still carries the full set. Sanity check when numbers shift inexplicably between runs: grep the content-diff inventory for challenge-page strings.
  2. domcontentloaded, never networkidle, on live targets. Live sites with analytics beacons never reach networkidle — hard timeout. Built in: the diff scripts default domcontentloaded for non-localhost http(s) URLs (decided per side; EDS build/preview origins — *.aem.page, *.aem.live, *.hlx.page, *.hlx.live — are the exception and get networkidle, they decorate async) and keep networkidle for local prototypes; --wait-until overrides. stitch-shot is always domcontentloaded.
  3. Symmetric --main scoping — and never body. Live <main> often contains header nav + hidden mega-menu; unscoped, those diff as ~dozens of missing CTAs (UC1-E1 iteration 1: 41 of 50 reds were scoping artifacts). Scope BOTH sides with the same selector — have the prototype adopt the live content-root class so one --main value fits both. Both diff scripts take --main (visual-diff's is the upstreamed flag: on sites without a <main>, both sides otherwise false-flag BLANK RENDER while the main-scoped checks silently no-op). Two guardrails:
    • --main body is NEVER a valid replica scope. A too-broad root self-poisons the instrument regardless of symmetry: reproduced (a furniture retailer), content-diff run live-vs-ITSELF with --main body produced 103 structural 🔴 and asymmetric node counts (461 vs 73) from analytics/inline-script text plus a nondeterministic cookie-settings panel pulled into the inventory. The content root must exclude consent/analytics chrome.
    • Verify the consent banner is actually gone post-dismiss before trusting an inventory — consent UIs render nondeterministically between two sequential captures (one capture caught the expanded cookie panel, the other didn't). If reds cluster on cookie/consent strings, the scope or the dismissal is wrong, not the recreation.
  4. Stitched captures only, never fullPage:true. Chromium's captureBeyondViewport renders lazy-decoded images as gray placeholders even when the DOM says loaded. Stitch on BOTH sides — the instrument must be symmetric.
  5. Freeze animations for capture, injected AFTER lazyload settle. Injection before the settle breaks some lazy loaders' swaps (recorded failure mode). stitch-shot.mjs orders this correctly.
  6. Overlays dismissed — BOTH classes, by clicking, not DOM removal (so layout settles as a real visit does). Two classes, both handled by the shared dismissOverlays (stitch-shot always; diff probes via --dismiss): (a) cookie consent (clicked accept; --consent <sel> / --dismiss <sel,...> for non-standard banners); (b) timed marketing/newsletter interstitials — recorded (fashion retailer): an undismissed "Sign up, stay updated!" modal fired ~5–9s after load and baked a pixel-diff contributor into the LIVE capture, repeated at every chunk seam, that no prototype fidelity could null out. These fire on a timer, so the dismissal polls for late arrivals and stitch-shot sweeps again after the settle pass.
  7. Granularity parity for JOIN/SPLIT false-reds (#87) — mirror live node granularity or confirm-justify per recreation-procedure.md § Granularity parity.
  8. Capture-state policy — CDN-403 placeholders and hydration states are ground truth (recreation-procedure.md § Capture-state); a probe flag over a logged capture-state zone is justified.
  9. Two classes only the gate sees: rendered-face font forks on inner spans (trust the width probe over captured computed styles) and overlay scrims (recover by per-row luminance fitting). Both in recreation-procedure.md.
  10. Pointer parked after any dismissal click. A consent/modal click leaves the virtual cursor at the button's coordinates; a :hover-styled element under the resting cursor is silently captured in HOVER state (recorded: a hero's a.box-hover:hover img{opacity:.4} shipped the live capture dimmed — measured 0.4 in capture, 1.0 in reality). The shared dismissOverlays parks the mouse (bottom-left) after every dismissal pass — all three instruments inherit it; mirror it in any ad-hoc capture that clicks anything.
  11. Fixed/sticky chrome × stitched capture. Fixed elements repeat at every chunk seam, occlude a band of content per seam, and can morph with scroll state — chunks 2+ then capture different chrome than chunk 1. Symmetry requires the prototype to replicate the chrome including its scroll-state trigger; any height delta turns the seam repeats into ghost bands in the diff. Full treatment: recreation-procedure.md § Fixed and sticky chrome.
  12. A challenge/blocked response FAILS LOUD — it is never measured. All three instruments detect bot-management interstitials on every navigation (crawl.mjs semantics: cf-mitigated: challenge, or 403/429/503 with a Cloudflare/Akamai/F5/Imperva edge signature) and exit 3 with a BotChallengeError naming the URL and the marker. Recorded (luggage retailer): Akamai served "Access Denied" to the headless instruments — which, without this rule, would have silently measured the block page as the source and diffed it cleanly, wrongly. The escalation ladder: default (UA + standard headers) → --headed (stealth real Chrome, same tier as crawl.mjs's fallback) → if STILL blocked, the site needs crawl.mjs-class capture and the gate must not silently degrade — record the breakpoint as gate-blocked in the ledger and surface it to the user; a gate that can't read the live source has no pass to report.
  13. Inner-scroller / scroll-jacked pages fail loud — stitched capture cannot measure them. On pages where html/body are overflow:hidden and an inner container scrolls, the document reports the full content height but window.scrollTo is a no-op — every chunk would capture the top viewport and the rows below would stitch as zero-filled black: a silently fictitious pixel diff. stitch-shot now detects the stall and exits 1 with the signature stitch-shot error: scroll stall at chunk target …px: window scroll is a no-op (window.scrollY stuck at …px) while the document reports …px. Capturing the inner scroller is future work; for now record the page as gate-blocked for the pixel probe and rely on content-diff/visual-diff.
  14. Captures assert fonts loaded — a silent font fork is a false measurement. A webfont that fails to fetch renders the ENTIRE live capture in fallback type: wrong wraps, wrong line counts, wrong section heights, wrong doc height — with no error anywhere. It is the same defect class as silently measuring a Cloudflare interstitial, and it poisons every number the gate reports (recorded, F-B2: an instrument-forced header set killed the live side's Typekit fetch; live doc height moved 6669→6518 once fixed, and a whole class of "one-line-off" defects vanished). stitch-shot now checks after document.fonts.ready for declared faces with FontFace status error and warns loudly with the family names; mirror the check in any ad-hoc capture. On the warning, decide before gating: load the face in a real browser — if it loads there, the failure is instrument-induced (a capture defect: fix the instrument, and the poisoned runs don't consume the iteration cap per § Iteration discipline); if it fails there too, fallback type is the truthful capture (capture-state — log it).

Script adaptations (built-in flags first — but fail-loud outranks script immutability)

The four manual adaptations this section used to prescribe are upstreamed into the shipped scripts. All live-target hardening lives in one shared module — diff/scripts/live-session.mjs (UA + standard headers, challenge fail-loud, overlay dismissal, headed-stealth escalation) — and the diff scripts expose it as flags:

# content-diff against a live source: no source edits, flags only
node scripts/diff/content-diff.mjs "$LIVE" "$PROTO" --profile generic \
  --width 1440 --main "<content-root>" --dismiss

# visual-diff: --main is a real flag (rule 3), same live hardening
node scripts/diff/visual-diff.mjs "$LIVE" "$PROTO" --profile generic \
  --width 1440 --main "<content-root>" --dismiss

# non-standard overlay closer / pinned locale / bot-managed site:
#   --dismiss "#custom-close"    --locale en-GB    --headed

Defaults when no flags are passed: real-Chrome UA + standard headers on every context; domcontentloaded for non-localhost http(s) URLs and networkidle for local ones (per side); no overlay dismissal (pass --dismiss for live pairs); exit 3 on a challenge (rule 12).

A project copy re-implementing retired adaptations is a defect, not diligence: the edits were 10 distinct changes across 2 files, and a partial application silently mis-measured (e.g. one main-scoped selector left hardcoded in visual-diff). If you find // replica ADAPTATION: copies from an older run, re-copy the shipped scripts and pass flags instead.

The narrow exception: a documented instrument-bug fix is the correct move when the shipped instrument measures falsely — fail-loud outranks script immutability. The rule above exists to kill stale re-implementations of upstreamed flags, not to force gating on an instrument known to lie (recorded, F-B2: the shipped header delivery silently killed live webfont loads; the session's most important fix was a hand-edit to the project's live-session.mjs). A legitimate instrument fix is (a) commented in the script with the defect it corrects, (b) recorded in the ledger with the runs it invalidates, and (c) flagged for upstreaming into the plugin. An uncommented, unledgered edit is still a defect.

The published-origin gate (EDS pipeline deltas)

The prototype gate above proves the RECREATION; it does not prove the DELIVERED page. Local render harnesses systematically understate deltas because the real delivery pipeline transforms the markup — field rule (financial-services site, 8 pages published): a page gating at X% on the harness lands at X±(large) on the published origin until the transforms below are handled. Only the published-origin number counts as the final gate for a platform-delivered page: re-run the full gate (same instruments, same pass bar, same iteration discipline) with the live site as source and the published page — preview or live origin — as build. Judge the result in the published-origin regime (§ Pass bar, calibration honesty), not against prototype-regime numbers.

Two rules for that final run:

  • Re-probe live chrome metrics at deploy time — crawl captures are the CONTENT source, live-now is the chrome/metrics source. The live site drifts between crawl and deploy (recorded, one day apart: header 141→106px, footer links 13→16px/24px with new 24px column headings, a swapped campaign hero). A deploy gated against crawl-time captures ships yesterday's chrome. Immediately before the published-origin gate, re-run the chrome probes (anchor + crop gate, computed styles of matched header/footer elements) against the live origin, never the crawl snapshot; mask live-content drift (campaign creatives, promo slots) out of the fidelity number — it is authored content, not conversion fidelity.
  • Budget ONE anchors-driven reconcile round at the published origin. The pipeline shifts vertical rhythm (section wrappers, <p><picture>, fragment chrome): a gate-passed 8.4% prototype first published at 11.75%, and two text-anchor rounds (anchor probe live-vs-published, patch section paddings in block CSS, re-measure) brought it to 6.5% with exact anchor parity (recorded). Treat the pre-publish harness number as provisional and the reconcile round as expected work, not a regression.

Recurring EDS pipeline transforms that move the number (each recorded; none visible on a local harness):

  • Images get wrapped in <p><picture>. The pipeline emits every authored image inside a paragraph. If any base rule makes that <p> positioned, absolutely-positioned imgs inside it collapse to 0×0 (backgrounds vanish) and the now-empty paragraph box distorts flex/grid flow. Style p:has(picture) as the media layer, and expect specificity fights with :not()-heavy base selectors — junction/override rules must match or exceed them.
  • Metadata-only sections render as empty .section divs carrying full section padding — a ~96px phantom band, typically at the page tail (the same class as deploy's emptySectionCollapse; the fix there is main .section:empty { display: none }, see ../../deploy/SKILL.md § Runtime-detection probe).
  • Media URLs are rewritten to /media_<hash> renditions with width params — size/ratio assumptions lifted from the authored URL don't survive; read dimensions from the delivered rendition, not the authored asset.
  • Authored inner blocks may be FLATTENED to default content, so a selector written against the authored markup (.section:has(.some-block)) can silently never match the delivered page (recorded). Verify every :has() / block-class selector against the delivered .plain.html and rendered DOM, not the authored file.

Residual logging format

Per archetype per breakpoint, in stardust/replica/progress.json:

{
  "pageType": "landing",
  "archetype": "home",
  "breakpoints": {
    "1440": {
      "iterations": 3,
      "result": { "structuralRed": 0, "visualFlags": "3 justified",
                   "pixelPct": 1.31, "heightDelta": 0, "pass": true },
      "justified": [
        { "probe": "visual", "flag": "1x1 h1 at x0", "why": "mirrors live SEO h1" },
        { "probe": "content", "flag": "🟠 font fork ×2", "why": "licensed kit substituted, R-policy fonts", "permanent": true }
      ],
      "residuals": [
        { "band": "y 4500–5000", "pct": 6.2, "cause": "capture-state: 3 CDN-403 placeholder tiles", "flaggedFor": "delivery" },
        { "region": "footer", "pct": 4.8, "cause": "glyph-antialiasing", "parity": "gates/home-1440/chrome-parity-iter3.json", "texture": { "thickPct": 6.1 }, "flaggedFor": "user" }
      ],
      "captureState": [ { "what": "product tiles 4–6 on placeholder data-URIs", "where": "carousel-2" } ]
    },
    "360": { "...": "..." }
  }
}

Rules: every residual names its band, its %, its cause, and who inherits it (delivery for capture-state items, user for accepted trade-offs). A residual without a cause is not a residual — it's an unfinished iteration; either diagnose it or spend the remaining budget on it. The rollout phase's final report surfaces the residual list per page type so "gate passed" can't hide "passed with 6% unexplained".

skills

README.md

tile.json