pixel-perfect-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CLONE-ALOYOGA-HEADER.md +76 -0
  2. package/CLONE-ANY-HEADER.md +106 -0
  3. package/CLONE-ANY-SITE.md +120 -0
  4. package/DEVELOP.md +144 -0
  5. package/LAUNCH-PROMPT.md +66 -0
  6. package/LEARNINGS.md +398 -0
  7. package/LICENSE +21 -0
  8. package/PLAYBOOK.md +260 -0
  9. package/README.md +219 -0
  10. package/WORKFLOW.md +257 -0
  11. package/bin/ppk +4 -0
  12. package/harness/adopt.js +52 -0
  13. package/harness/agent-setup.js +55 -0
  14. package/harness/behavior-selftest.js +243 -0
  15. package/harness/benchmarks/README.md +34 -0
  16. package/harness/benchmarks/battery.js +86 -0
  17. package/harness/benchmarks/detection-power.js +75 -0
  18. package/harness/capture-build-selftest.js +134 -0
  19. package/harness/capture-build.js +323 -0
  20. package/harness/doctor-selftest.js +58 -0
  21. package/harness/doctor.js +85 -0
  22. package/harness/fixtures/01-backdrop-color.js +51 -0
  23. package/harness/fixtures/02-line-strut.js +53 -0
  24. package/harness/fixtures/03-compat-mode.js +44 -0
  25. package/harness/fixtures/README.md +31 -0
  26. package/harness/human-qa-selftest.js +201 -0
  27. package/harness/human-qa.js +406 -0
  28. package/harness/merge-snapshot-selftest.js +56 -0
  29. package/harness/new-target.js +101 -0
  30. package/harness/regression.js +50 -0
  31. package/harness/score.js +58 -0
  32. package/harness/serve.js +149 -0
  33. package/harness/setup-selftest.js +93 -0
  34. package/harness/setup.js +158 -0
  35. package/harness/tunnel-selftest.js +76 -0
  36. package/harness/tunnel.js +241 -0
  37. package/harness/workflow-selftest.js +211 -0
  38. package/harness/workflow.js +739 -0
  39. package/package.json +40 -0
  40. package/skill/ditto-finish/SKILL.md +52 -0
  41. package/skill/pixel-perfect-clone/SKILL.md +49 -0
  42. package/tools/RUNBOOK.md +228 -0
  43. package/tools/behavior-capture.js +307 -0
  44. package/tools/behavior-worksheet.js +82 -0
  45. package/tools/browser-capture.js +240 -0
  46. package/tools/cli-selftest.js +136 -0
  47. package/tools/extract-fonts.js +133 -0
  48. package/tools/extract-icons.js +255 -0
  49. package/tools/merge-snapshot.js +56 -0
  50. package/tools/pixel-diff.js +845 -0
  51. package/tools/reassemble.js +63 -0
  52. package/tools/selftest.js +67 -0
  53. package/tools/sink.js +80 -0
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "pixel-perfect-kit",
3
+ "version": "0.1.0",
4
+ "description": "Clone a website's visual identity and prove the clone is pixel-perfect with numbers, never by eye — driven by an enforced, gated workflow (ppk).",
5
+ "keywords": [
6
+ "pixel-perfect",
7
+ "clone",
8
+ "css",
9
+ "visual-regression",
10
+ "diff",
11
+ "devtools",
12
+ "workflow",
13
+ "agent"
14
+ ],
15
+ "license": "MIT",
16
+ "type": "commonjs",
17
+ "bin": {
18
+ "ppk": "bin/ppk"
19
+ },
20
+ "files": [
21
+ "bin/",
22
+ "harness/",
23
+ "tools/",
24
+ "skill/",
25
+ "PLAYBOOK.md",
26
+ "WORKFLOW.md",
27
+ "LEARNINGS.md",
28
+ "DEVELOP.md",
29
+ "LAUNCH-PROMPT.md",
30
+ "CLONE-ANY-SITE.md",
31
+ "CLONE-ANY-HEADER.md",
32
+ "CLONE-ALOYOGA-HEADER.md"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "scripts": {
38
+ "test": "node harness/regression.js"
39
+ }
40
+ }
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: ditto-finish
3
+ description: Clone a site with ditto (ditto.site) and finish it with human review rounds via PingHumans. Use when the user asks to clone/copy a site using ditto, or to polish/finish/verify a ditto-generated clone until a human approves it. Ditto's capture-to-code output is the starting point (~90%); the human-in-the-loop rounds close the gap.
4
+ ---
5
+
6
+ # Ditto → human review rounds (the last 9%)
7
+
8
+ Ditto compiles a URL into a runnable Next.js/Vite React codebase from the rendered
9
+ page — by its own README, "starting points, not production-ready replicas." This skill
10
+ finishes the job: publish the ditto build, put it in front of a real human side-by-side
11
+ with the original, fix what they pin, and repeat until they press approve. The
12
+ pixel-perfect-kit (`ppk`) provides the loop machinery; ditto provides the build.
13
+
14
+ ## Steps
15
+
16
+ 1. **Preflight.** `ppk doctor` (surface failures to the user). `ppk where` → KIT.
17
+
18
+ 2. **Build with ditto.** Use whichever ditto interface is available, in this order:
19
+ the ditto MCP server (https://api.ditto.site/mcp) if connected; the REST API if the
20
+ user has DITTO_API_KEY; else the local CLI per ditto's README. Input: the user's URL.
21
+ Output: a generated project — install and run it (`npm i && npm run dev`), note the
22
+ local port. Fix any build errors first; the loop needs a rendering page.
23
+
24
+ 3. **Register + publish.**
25
+ ```sh
26
+ ppk adopt <name> <original-url> # registers the build for the human loop
27
+ ppk tunnel <name> --url http://localhost:<port> # public, reachability-verified (background task)
28
+ ```
29
+
30
+ 4. **Run the human loop** (this is the product — do not skip ahead):
31
+ - `ppk human <name> file [--region "…"]` files a side-by-side compare round; the
32
+ user reviews (pins + a MANDATORY verdict button). No PingHumans login → local mode:
33
+ `ppk human <name> file --local` and tell the user to open /__review; never open or
34
+ submit it yourself.
35
+ - On every rejection: root-cause each pin, fix it IN THE DITTO CODEBASE (its
36
+ components/content files — prefer minimal, targeted edits; re-run the dev server),
37
+ then refile with `--changelog "what changed since your last review"`. Never respond
38
+ to a reviewer comment outside the loop; the verdict is the only decision channel.
39
+ - Polls (`ppk human <name> poll "…"`) are for ONE-SIDED questions only ("on the real
40
+ page, what happens when…?"); anything comparing clone to original must be a filed
41
+ round.
42
+ - Repeat until the verdict is the approving option. Expect 1–3 rounds typically;
43
+ pins get smaller each round if your fixes are root-caused rather than patched.
44
+
45
+ 5. **Report**: the round history (ping ids, what was pinned, what you changed), the final
46
+ approving verdict, where the codebase lives, and how to run it. The deliverable is
47
+ ditto's codebase, human-approved — not a claim, a receipt.
48
+
49
+ Useful background from the kit's failure catalog (KIT/LEARNINGS.md): the reviewer's
50
+ browser is the ground truth and may receive a different variant of the original than
51
+ your browser does (#20); comment-only reviews cannot pass (approval is never inferred
52
+ from prose); a refile without a changelog wastes the reviewer's pass.
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: pixel-perfect-clone
3
+ description: Clone, copy, or replicate a website/page pixel-perfect using the pixel-perfect-kit (ppk). Use when the user asks to clone a site, copy a webpage's design, replicate a page, or make a pixel-perfect copy of a URL. Drives the full enforced pipeline - capture, numeric gates, behavior reproduction, and human review rounds - and treats the user as the reviewer.
4
+ ---
5
+
6
+ # Clone a site pixel-perfect (ppk)
7
+
8
+ The pixel-perfect-kit is an enforced, receipt-driven pipeline: a phase is done because
9
+ its gate command exits 0, never because anyone says so. Your job is to drive it end to
10
+ end; the user's job is to answer review pings.
11
+
12
+ ## Steps
13
+
14
+ 1. **Preflight.** Run `ppk doctor`. If anything required fails, show the user the fix
15
+ lines and stop until resolved. Run `ppk where` → KIT (the installed kit's directory;
16
+ all docs below live there).
17
+
18
+ 2. **Get the three values.** URL (ask if not given). WIDTH: default 1512 unless the user
19
+ specifies. NAME: a short slug from the domain (e.g. `stripe` for stripe.com). Targets
20
+ are created under the CURRENT working directory (`targets/<NAME>/`) — cd to the user's
21
+ preferred workspace first.
22
+
23
+ 3. **Follow the kit's own instructions exactly.** Read KIT/LAUNCH-PROMPT.md (the run
24
+ contract + environment notes) and KIT/CLONE-ANY-SITE.md (the method), and execute them
25
+ with the three values. They override any assumption you have. Key expectations:
26
+ - Build by CAPTURE (`ppk capture-build`), never hand-reconstruction.
27
+ - Every phase advances only through its gate: `ppk advance <NAME> <phase>`. Never
28
+ use --force. `ppk status <NAME>` always tells you what's next.
29
+ - Sink + `node <KIT>/harness/tunnel.js --sink` first for one-call capture delivery.
30
+ - All human contact through `ppk human <NAME> …` (file/poll/verify) — never through
31
+ any MCP directly. Refiles carry `--changelog "what changed"`.
32
+ - No PingHumans login (doctor shows it missing)? Use LOCAL review mode:
33
+ `ppk human <NAME> file --local`, then tell the user to open
34
+ `http://localhost:<serve-port>/__review` and review there. You must NEVER open or
35
+ submit the /__review page yourself — the verdict must come from the human.
36
+
37
+ 4. **Tell the user they are the reviewer** (first run especially): review pings arrive
38
+ with a side-by-side compare UI; they steer by PINNING comments on what looks wrong,
39
+ in their own words; a VERDICT BUTTON pick is required — comment-only reviews stall
40
+ the pipeline; their browser's rendering of the original is the ground truth (the
41
+ reference site may serve their browser a different variant than yours — LEARNINGS #20).
42
+
43
+ 5. **Iterate until done.** Act on every verdict immediately: fix from the site's own
44
+ captured artifacts (authored mechanisms, never invented values), re-green the gates,
45
+ refile with a changelog. Done = `ppk gate <NAME> done` exits 0 — all ten phases,
46
+ including a real approving verdict from the user.
47
+
48
+ 6. **Report with receipts**: the final `ppk status <NAME>` table, gate outputs, the round
49
+ history, and the clone's location (`targets/<NAME>/clone/`).
@@ -0,0 +1,228 @@
1
+ # RUNBOOK — the fast live-vs-clone diff sequence
2
+
3
+ The verified fast path. Target: **~2–4 browser round-trips + 1 local `node` call**,
4
+ not ~20. Every browser-automation call is a slow CDP round-trip, and a blocked
5
+ `fetch`/`eval` on a strict-CSP live site doesn't error — it *hangs to a 45s timeout*.
6
+ So the rules below are about avoiding slow/blocked paths, not compute.
7
+
8
+ ## Golden rules (these are what make it fast)
9
+ - **Probe the delivery path FIRST — don't assume it's blocked** (see Step 0). On many
10
+ "strict-CSP" sites a direct `fetch` POST to `http://localhost:7799` actually
11
+ succeeds; `connect-src` is often looser than expected. If it works you skip the
12
+ entire stash/read chunking dance. *Measured cost of assuming-blocked-without-testing
13
+ on the aloyoga run: the whole 9-chunk manual reassembly, ~6 min wasted.*
14
+ - **Inject the capture code as PLAIN SOURCE, never base64/gzip.** Paste
15
+ `tools/browser-capture.js` (the browser half of `pixel-diff.js`, extracted so it
16
+ drops in as-is) directly as the code to evaluate. CDP-injected source isn't gated by
17
+ `script-src`. Base64/gzip transports of the whole file are fragile — they corrupt or
18
+ trip the automation harness's content filter. **Never `fetch(...).then(eval)`** on a
19
+ strict-CSP origin — that path *is* gated and hangs to a 45s timeout.
20
+ - **Don't return URLs / data-URIs / base64 through the JS tool.** The automation
21
+ harness may silently blank the result (`[BLOCKED: …]`). Deliver payloads by POSTing
22
+ to the sink, or stash to a `window` var and read one clean dump — never inline them
23
+ in a return value.
24
+ - **Batch.** Fold independent calls into one `browser_batch` (both page captures
25
+ together; all chunk reads together).
26
+ - **One capture per page, all targets at once.** Not one call per element.
27
+ - **Same viewport width on both**, or the diff won't trust x-positions.
28
+ - **Check `~/Downloads` before re-running `extractFonts`/`extractIcons`.** They dump
29
+ zips to disk; a prior run's `<host>-fonts.zip` / `<host>-icons.zip` may already be
30
+ there (`ls ~/Downloads/*<host>*`).
31
+
32
+ ## Prereqs
33
+ - Your clone dev server running (note its port).
34
+ - The sink running to receive the clone snapshot: `node tools/sink.js` (listens on 7799).
35
+
36
+ ## Build by capture (the DEFAULT build step — do once, before the diff loop)
37
+ Don't hand-rebuild the markup: capture it (LEARNINGS #19 — a reconstruction manufactures
38
+ exactly the technique mismatches the gate is blind to; a capture inherits live's doctype,
39
+ authored line-heights, and drawing primitives by construction).
40
+
41
+ 1. Let the live page **settle** at the target width: load, wait ~2s, scroll to the bottom
42
+ and back (mounts lazy/animated content), wait, confirm `scrollHeight` + node count are
43
+ stable across two reads.
44
+ 2. Inject `tools/browser-capture.js` as plain source on the live tab (same rules as
45
+ measurement — never fetch+eval), then with the sink running in `targets/<name>/`:
46
+ ```js
47
+ await pxSendDom('http://localhost:7799/dom.html') // doctype-exact post-hydration DOM
48
+ ```
49
+ CSP-blocked POST → `pxStash(null, 900, pxDomHtml())` + batched `pxRead` (Step 3 below).
50
+ 3. Build the standalone clone from it:
51
+ ```sh
52
+ ppk capture-build <name> # or: node harness/capture-build.js <name>
53
+ ```
54
+ Downloads + self-hosts every stylesheet and font, absolutizes other asset refs, strips
55
+ scripts/CSP/`<base>`, and **preserves the doctype or its absence byte-for-byte**. A
56
+ `file://` url in `target.json` also works (build from a saved page). Failed downloads
57
+ exit 1 — fix them before advancing the `build` phase.
58
+ 4. The gates run unchanged from here: measure live, capture the clone, `--visual`,
59
+ coverage, strict. What capture can NOT give you — JS-driven behavior, animated or
60
+ generative content — you reproduce separately, and spend human QA rounds only there.
61
+
62
+ ## Behavior discovery (the `behavior` phase — after the pixel gates are green)
63
+ Same injection + delivery rules as everything else here (plain source, probe the POST,
64
+ stash/read fallback). `tools/behavior-capture.js` provides `pxBehaviorDiscover(opts)` /
65
+ `pxBehaviorSend(url, opts)` / `pxBehaviorStash(opts)` + `pxBehaviorRead(i)`.
66
+
67
+ 1. **On the LIVE tab:** inject `tools/behavior-capture.js`, set `pxRegion`, name what you
68
+ can see moving — `opts = { marqueeSelectors: [["logo_belt", ".belt-wrapper"]],
69
+ hoverTriggers: [["nav_product", "nav a[href*=product]"]] }` — then
70
+ `await pxBehaviorSend('http://localhost:7799/behaviors-live.json', opts)`. The pass
71
+ greps `@keyframes` + markers for candidates, then confirms and MEASURES what actually
72
+ fires across a scripted scroll sweep (a candidate that never moves is noise, not a
73
+ behavior). Its own sweep metadata is recorded — that's the gate's evidence discovery ran.
74
+ 2. **Reproduce** each inventoried behavior in one vanilla `clone/fixes.js` (each in its own
75
+ guarded `try`), using the MEASURED values. Rebuild: `ppk capture-build <name> --fixes`.
76
+ 3. **On the CLONE tab:** same discovery, same opts → `behaviors-clone.json`.
77
+ 4. `node harness/workflow.js gate <name> behavior` — misses are named with exact deltas;
78
+ irreproducible content goes in `behavior-deviations.json` with a reason, never silent.
79
+ 5. **The worksheet — the complete "supposed to move" list.** Discovery keeps
80
+ declared-but-unfired candidates (markers, keyframes, transitions-from-hidden, videos)
81
+ as `declared` inventory instead of discarding them — critical on sites that gate their
82
+ choreography behind no-js/bot detection, where NOTHING fires for automation but
83
+ everything is still supposed to. `node tools/behavior-worksheet.js <name>` prints one
84
+ row per behavior (observed + declared) with its disposition; every UNRESOLVED row gets
85
+ a ready-to-send one-sided poll question so the HUMAN describes what live does before
86
+ you engineer anything. Each behavior having an identity up front is what prevents two
87
+ real behaviors merging into one invented hybrid — the gate refuses undisposed declared
88
+ rows for the same reason.
89
+
90
+ ## Step 0 — probe live delivery (do this before anything else)
91
+ Inject `tools/browser-capture.js` source on the live tab, then try a direct POST:
92
+ ```js
93
+ fetch('http://localhost:7799/live.json', {method:'POST', body: pxCapture(null,{compact:true})})
94
+ .then(r=>r.text()).then(t=>window.__post=t).catch(e=>window.__post='ERR:'+e.message)
95
+ ```
96
+ Check whether `./live.json` appeared on disk. **If it did, you're done delivering** —
97
+ capture both pages with one `pxSend` each and skip Steps 3. Diagnose a refusal by its
98
+ timing: a **clean ~4s abort** on a site that sends no CSP header is the *automation
99
+ extension* blocking page→localhost fetch (environment-level); a **45s hang** is the
100
+ site's `connect-src` (CSP-level).
101
+
102
+ **Blocked either way? Tunnel the SINK — this replaces the stash/chunk dance:**
103
+ ```sh
104
+ node tools/sink.js & # the receiver, as usual
105
+ node harness/tunnel.js --sink & # public HTTPS in front of it (verified by the
106
+ # sink's own signature; records ./sink-tunnel.json)
107
+ ```
108
+ then from any page, live or clone: `await pxSend('<sink-tunnel-url>/live.json')` /
109
+ `pxSendDom('<url>/dom.html')` / `pxBehaviorSend('<url>/behaviors-live.json')` — one call,
110
+ full payload, no truncation caps. Stash + chunked `pxRead` (Step 3) is the LAST resort,
111
+ only for when outbound HTTPS itself is blocked (rare — that's a real site CSP with a
112
+ strict `connect-src`, and even those often allow `*.trycloudflare.com`... probe it).
113
+
114
+ ## Step 1 — both windows to the same width
115
+ Resize the clone tab and the live tab to the same width (e.g. 1728). Confirm
116
+ `innerWidth` matches on both (scrollbars/devtools can shrink it).
117
+
118
+ ## Step 2 — capture BOTH pages in ONE browser_batch
119
+ - **Clone (same-origin localhost, no CSP → 1-call send):** load the capture code
120
+ (`eval(await fetch('/browser-capture.js').then(r=>r.text()))` works on localhost — no
121
+ strict CSP there; copy `browser-capture.js` into the clone's served dir), set
122
+ `pxRegion`, define `pxTargets`, then:
123
+ ```js
124
+ await pxSend('http://localhost:7799/clone.json') // capture + POST, returns "ok clone.json"
125
+ ```
126
+ - **Live:** inject `tools/browser-capture.js` **source directly** as plain text (paste
127
+ the whole file — do NOT fetch+eval, do NOT base64/gzip it), set `pxRegion` +
128
+ `pxTargets`. Then **prefer the direct POST from Step 0** (`pxSend('…/live.json')`).
129
+ Only if that's blocked, stash:
130
+ ```js
131
+ pxStash(null, 900) // compact capture into a hidden <textarea>; returns {bytes, chunks}
132
+ ```
133
+ (900-char chunks fit a typical ~1KB automation result cap; a human DevTools console
134
+ has no cap → just `copy(pxCapture())`.)
135
+
136
+ ## Step 3 — read all live chunks in ONE browser_batch *(only if POST was blocked)*
137
+ Issue `pxRead(0) … pxRead(chunks-1)` as separate actions in a single `browser_batch`,
138
+ save each slice to its own file (zero-padded: `chunk-00.txt`, `chunk-01.txt`, …), then
139
+ **reassemble with the validator — never hand-concatenate** (a dropped character at a chunk
140
+ boundary silently corrupts the snapshot and only surfaces rounds later as unexplainable diffs):
141
+ ```sh
142
+ node tools/reassemble.js live.json --bytes <bytes-from-pxStash> chunk-*.txt
143
+ ```
144
+ It verifies the byte count against pxStash's report, JSON-parses, and checks the snapshot
145
+ shape before writing — any mismatch names the broken chunk instead of writing garbage.
146
+
147
+ ## Step 4 — diff locally
148
+ ```sh
149
+ node tools/pixel-diff.js live.json clone.json --visual # "does it look identical?" exit 0 = pass
150
+ node tools/pixel-diff.js live.json clone.json # strict: also flags structural CSS
151
+ ```
152
+
153
+ ## Sanity checks
154
+ - Each target's `rect.y` lands where you expect (catches a finder grabbing the same
155
+ text elsewhere — that's why `pxRegion` exists).
156
+ - The diff warns and refuses x-comparisons if the two snapshots' widths differ.
157
+ - A `present:false` on either side is a finder failure, not a match — fix the finder.
158
+ - A **`text.present` failure** means a finder resolved a non-text *wrapper* (its own
159
+ text node is empty), which nearly skipped `font.color`/size. Fix the finder to land
160
+ on the text element — use `byText(/…/)` (own-text match), not a hand-rolled
161
+ `children.length<=1` filter that can match a full-width container.
162
+ - **Read the strict table for colour/underline rows before declaring a pass.**
163
+ `font.color`, `font.decoration`, `font.underline` are *visible* marks, not
164
+ structure — a strict `font.color … ❌` under a green `--visual` is a real defect.
165
+
166
+ ## Coverage (don't skip)
167
+ After `--visual` is green, enumerate every painted element in the region (own text,
168
+ background-image, or `<svg>`) that has **no** `pxTargets` entry, and add one. A green
169
+ diff only proves the elements you measured.
170
+
171
+ **Coverage is elements *and* the marks on them.** The gate now measures the marks that
172
+ used to slip a green sweep — `font.smoothing`, the **underline box** (`underline.*`,
173
+ taken off whichever element draws it, ancestor included), and the **painted backdrop**
174
+ (`bg` — the bar/button/badge colour behind a mark, transparent chain → white canvas) —
175
+ and `--visual` compares them, so you don't re-derive them by hand. What's still on you: **enumerate
176
+ every painted leaf** (own text / background-image / `<svg>`) so each has a target, and
177
+ for a mark the tool doesn't special-case (a `box-shadow`, an `outline`) measure the
178
+ drawing element's box with `--inspect`. Set the clone root to `antialiased` / `grayscale`.
179
+
180
+ ## The fast fix loop (incremental re-verify + micro-polls)
181
+ Two tools that keep iteration at seconds/minutes instead of full-round scale:
182
+
183
+ - **Partial re-capture + merge.** After a scoped fix, don't re-capture all N targets —
184
+ capture just the affected ones (`pxSend('<sink-url>/partial.json', subsetTargets)`) and
185
+ fold them in: `node tools/merge-snapshot.js targets/<name>/clone.json partial.json`.
186
+ Gates re-run instantly on the merged file. The merge stamps the snapshot and **the done
187
+ gate refuses stamped snapshots** — one final FULL capture is always required (a fix can
188
+ displace things outside your subset: astryx's bento-height fix moved the footer).
189
+ - **Micro-polls before full rounds.** With a responsive reviewer, ask the ~$0.05 question
190
+ mid-round instead of spending a whole test round discovering the answer:
191
+ `node harness/human-qa.js poll <name> "do the 3 template tiles look right now?"
192
+ --choices "Yes,No"` (draft+original urls auto-appended; blocks up to ~5 min; answers
193
+ recorded in human-qa.json). Polls are ADVISORY — the human gate still requires an
194
+ approving verdict on a full scope-pinned round. Use them to decide, not to certify.
195
+
196
+ ## Human flags one element → exhaustive drill-down (`--inspect`)
197
+ When a human says "*this* looks wrong" (not a full sweep), don't guess the property —
198
+ measure the whole element **that paints the mark** (which may be an ancestor):
199
+
200
+ 1. On **both** pages resolve the flagged element and dump its full computed style:
201
+ ```js
202
+ copy(pxInspect({ text: "sign in" })) // {aria}, {sel}, {at:[x,y]} also work
203
+ ```
204
+ (live CSP → `pxStashInspect(resolver)` + batched `pxRead`; clone → `copy()` or POST to sink.)
205
+ **If the paint bucket is empty but the human still sees it, you inspected the wrong
206
+ element** — the mark is drawn by an ancestor/sibling (e.g. an underline that's a
207
+ `border-bottom` on a wrapping group). Resolve that element (`{sel:".loyalty-name-part"}`)
208
+ and measure **its box**: `top`, `height`, `box-sizing`, `border-bottom` — that box
209
+ positions the mark.
210
+ 2. Diff, paint-first:
211
+ ```sh
212
+ node tools/pixel-diff.js --inspect el_live.json el_clone.json
213
+ ```
214
+ **PAINT** = the fix list (colour, border, decoration/underline, background,
215
+ transform, geometry…); **STRUCTURAL** demoted; irrelevant props hidden.
216
+ 3. **Fix by reproducing the box model + technique, and fix the whole mark at once.**
217
+ Rebuild the same drawing element (same `border`, `height`, `box-sizing`) so the
218
+ mark's position/size *emerge* — don't nudge the text with a magic offset (it drifts
219
+ and rasterises unlike a real border, failing a flicker compare even at ~0.01px). A
220
+ decoration has thickness + width + offset + technique; set them together, or the
221
+ human iterates once per facet.
222
+ 4. **Re-run step 2** — done = PAINT bucket empty (exit 0). Then flicker/overlay the two
223
+ at 1:1 as a final check: a technique mismatch survives a green number.
224
+
225
+ ## Human shortcut (no automation)
226
+ At the keyboard: paste `pxCapture()` (or `pxInspect(resolver)`) into the real DevTools
227
+ console on each page (no CSP-on-console, no truncation, `copy()` works), save the two
228
+ files, and run Step 4 / `--inspect`. This skips injection, the sink, and chunking.