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/PLAYBOOK.md ADDED
@@ -0,0 +1,260 @@
1
+ # PLAYBOOK — clone a site pixel-perfect, and prove it with numbers
2
+
3
+ A repeatable method for recreating a website's visual identity and verifying the
4
+ match objectively. Every phase has a **Do**, a **Verify** (an objective check), and
5
+ an **Evaluate** (the pass bar). No step is "done" because it looks right — it's done
6
+ because a command exits 0.
7
+
8
+ **The one principle** (everything below is a corollary):
9
+
10
+ > Measure the **painted mark**, not the wrapper — and measure it **completely**.
11
+ > Text → the glyph box via `Range` + all font metrics. Icons/logos → the drawn
12
+ > pixels (SVG bbox or background element + `background-position`). Colour, underline,
13
+ > shadow are painted marks too. A wrapper box, or a hand-picked subset of properties,
14
+ > will pass while the pixels are wrong.
15
+
16
+ ---
17
+
18
+ ## Phase 0 — Decide the target and the viewport
19
+ **Do:** Pick the exact page/section to clone and a **fixed measurement width** (e.g.
20
+ 1728px). Everything is measured at that width; the diff refuses to trust x-positions
21
+ if the two snapshots differ in width.
22
+ **Verify:** you can state the width and section in one sentence.
23
+ **Evaluate:** ✅ target + width written down.
24
+
25
+ ## Phase 1 — Extract the real assets (never redraw / retype)
26
+ **Do:** On the live site's DevTools console:
27
+ - `extractFonts(/family/i)` → `<host>-fonts.zip` (real `.woff2` + a `fonts.css`).
28
+ - `extractIcons('header')` → `<host>-icons.zip` (clean SVGs + `icons.css` with the
29
+ **exact** original data-URIs + a `preview.html` + `report.json`).
30
+ - Capture any non-icon vectors (logo/wordmark `<svg>`) and rasters by their real
31
+ `src`/`srcset` URLs.
32
+ **Verify:** every `.woff2` begins with magic bytes `wOF2`; each SVG starts with
33
+ `<svg` and matches the live path data; each raster downloads at the same URL/size.
34
+ **Evaluate:** ✅ every shipped asset is a real captured file, none hand-drawn/typed.
35
+
36
+ ## Phase 2 — Measure the design spec (no guessing)
37
+ **Do:** From the live DOM, capture a deduped inventory of every rendered text style
38
+ — `weight / size / line-height / letter-spacing / text-transform / color` — plus the
39
+ logo/icon intrinsic sizes and the section's box geometry. Record in a `spec.md`.
40
+ **Verify:** the token table covers every distinct text role (hero, heading, nav,
41
+ button, label, body, caption) and each icon's intrinsic size.
42
+ **Evaluate:** ✅ the spec is measured, not inferred. **`line-height` and
43
+ `letter-spacing` are mandatory columns** — they are the two everyone skips, and they
44
+ decide where glyphs land.
45
+
46
+ ## Phase 3 — Capture the breakpoints
47
+ **Do:** Sweep viewport widths and read the live stylesheet's `@media` rules to record
48
+ the exact px breakpoints and how each token shifts. Set your clone's `screens` to match.
49
+ **Verify:** every width where a measured token changes is recorded with its exact px.
50
+ **Evaluate:** ✅ breakpoints come from the live `@media`, not from eyeballing the sweep.
51
+
52
+ ## Phase 4 — Build: capture by default, rebuild only to integrate
53
+ **Do (default — build by capture):** don't reconstruct the markup; capture it. The gate's
54
+ chronic blind spot is *technique* mismatches — same numbers, different construction,
55
+ different rasterisation (LEARNINGS #12/#14/#15/#17/#18) — and every one of them is
56
+ self-inflicted by hand-rebuilding. A clone built from the captured post-hydration DOM
57
+ inherits live's doctype, authored line-heights, `font-feature-settings`, and drawing
58
+ primitives **by construction** (LEARNINGS #19). Capture the settled DOM with
59
+ `pxSendDom('http://localhost:7799/dom.html')`, then:
60
+ ```sh
61
+ ppk capture-build <name> # self-hosts CSS+fonts, strips scripts/CSP, PRESERVES the doctype
62
+ ```
63
+ See RUNBOOK "Build by capture". JS-driven behavior and animated/generative content can't
64
+ be captured statically — reproduce those separately.
65
+ **Do (rebuild — only when the deliverable is a component in your own stack):** recreate
66
+ the section driven strictly by the captured spec: self-host the woff2s, use the real icon
67
+ data-URIs, apply the measured type tokens. Reproduce quirks faithfully (e.g. a faux-bold
68
+ button that renders `font-weight:900` over a 700-max face — replicate the synthesis, don't
69
+ substitute a heavier font). Expect the technique-mismatch class to be live — every
70
+ LEARNINGS rule below was paid for on this path.
71
+ **Verify:** the page renders standalone in the self-hosted faces (no external font
72
+ request); capture-build exited 0 (a failed asset download is loud, never silent).
73
+ **Evaluate:** ✅ builds clean; assets resolve locally.
74
+
75
+ ## Phase 5 — Verify with the numeric diff (the core)
76
+ **Do:** Capture both pages at the same width and diff. See `tools/RUNBOOK.md` for the
77
+ exact fast sequence. In short:
78
+ - Clone (localhost, no CSP): `pxSend("http://localhost:7799/clone.json")` — one call.
79
+ - Live (strict CSP): inject `pixel-diff.js` **source directly** (never
80
+ `fetch(...).then(eval)` — that hangs to a 45s timeout), then `pxStash()` +
81
+ batched `pxRead(0..n)` → reassemble as `live.json`.
82
+ ```sh
83
+ node tools/pixel-diff.js live.json clone.json --visual # "does it look identical?" exit 0 = pass
84
+ node tools/pixel-diff.js live.json clone.json # strict: also flags structure
85
+ ```
86
+ Measure **all** of these per element (skipping any one is how a defect hides):
87
+ geometry (`x,y,w,h,top,right,bottom,fromRight`); the **text-glyph box** via `Range`
88
+ (not the element box — padding moves the box but not the glyphs); the **painted
89
+ glyph** for icons/logos (SVG bbox or bg element + `background-position`, not the
90
+ clickable wrapper); the full box-model; **font incl. `line-height`,
91
+ `letter-spacing`, `color`, `-webkit-font-smoothing`, and `underline`**; layout + the
92
+ parent's `gap`.
93
+
94
+ **Every painted mark is a box — and the gate now measures the ones that used to slip a
95
+ green sweep, so trust the diff instead of re-deriving them:** `font.smoothing`
96
+ (`-webkit-font-smoothing` — changes *perceived weight* while `font-weight` matches), the
97
+ **underline as a box** (`underline.{thickness,x,right,w,top,bottom}`, measured on
98
+ whichever element — often an **ancestor** — draws it, not a boolean), and the **painted
99
+ backdrop** (`bg` — the nearest opaque `background-color` behind a mark, a
100
+ transparent chain resolving to the white canvas; catches a wrong announcement-bar /
101
+ button / badge colour that lives on a container the text leaf never measured — #16).
102
+ `--visual` compares all three — plus the **line-box strut** (`font.strut`: the nearest
103
+ line-box *container's* line-height, often an ancestor td/div you never targeted; it
104
+ positions the glyphs vertically, and `normal` vs a number is a technique mismatch that
105
+ drifts across platforms even when the same-machine delta is sub-tolerance — #17).
106
+ Generalise the principle to any mark the tool doesn't special-case (a
107
+ `box-shadow`, an `outline`): measure the drawing element's box with `--inspect`. Set the
108
+ clone root to `antialiased` / `grayscale` by default. What the gate can *not* do —
109
+ reproducing the technique so it also rasterises identically — is Phase 6.
110
+
111
+ **`--visual` vs strict.** `--visual` compares only pixel-determining props (geometry,
112
+ text-glyph box, `font weight/size/line/spacing/transform/color/decoration/underline`,
113
+ glyph center + `background-position`) — use it to answer "does it look identical?".
114
+ Strict (default) additionally compares structure (`display`, `position`, parent
115
+ `gap`, padding, the `font-family` alias); two valid implementations legitimately
116
+ differ there — fix or **document** each structural delta, don't fail the pixel check
117
+ on it.
118
+
119
+ **Verify:** `--visual` exits 0 at every breakpoint.
120
+ **Evaluate:** ✅ only when every pixel-determining property is within tolerance
121
+ (0.5px default) at every breakpoint.
122
+
123
+ ## Phase 5b — Close coverage (don't skip)
124
+ **Do:** After `--visual` is green, **auto-enumerate** every painted leaf in the
125
+ region (each element with own text, a background-image, or an `<svg>`) and require a
126
+ `pxTargets` entry for each. Add the missing ones and re-diff.
127
+ **Verify:** the count of painted elements in the region equals the count of targets;
128
+ off-region/hidden nodes (x<0, display:none, flyouts below the band) are excluded. A
129
+ **solid-colour container** (an announcement bar, a coloured button) is a painted mark
130
+ too: either target it, or rely on its text/icon child — whose `bg` now carries that
131
+ backdrop colour (#16) — but don't leave a coloured bar with no covered mark on it.
132
+ **Evaluate:** ✅ coverage list empty. *A green table only proves the elements you
133
+ measured; an uncovered element is unverified, not matched.*
134
+
135
+ ## Phase 5c — Reproduce JS-driven dynamics (the `behavior` phase)
136
+ Statics are proven by Phases 5/5b before this one runs. A static capture strips `<script>`,
137
+ so anything driven by JS — animations, rotations, reveals, marquees, counters, hover-mounted
138
+ content — is frozen at its captured moment. This phase reproduces it, and the workflow's
139
+ `behavior` gate proves the reproduction by MEASURED number, never by eye. Method ported
140
+ verbatim from `lovable_dupe_html/CLONE_PLAYBOOK.md` §8/§8a — this is a port, not a
141
+ reinvention:
142
+
143
+ 1. **Static pass (candidates, necessary but noisy):** grep the captured CSS for every
144
+ `@keyframes` name; grep the DOM for `opacity-0`, `translate-x/y-*`, `blur-`,
145
+ `will-change`, `data-[starting-style]`, `animate-*`, `data-state` markers.
146
+ `tools/behavior-capture.js`'s `pxBehaviorDiscover()` does this scan for you.
147
+ 2. **Dynamic differential pass on LIVE (authoritative):** attach a `MutationObserver`
148
+ (watching `class`/`style`/`data-*`) across the region, snapshot each candidate's computed
149
+ `opacity`/`transform`/`filter` before, then **scripted-scroll** the page in increments
150
+ (dwelling at each stop) and dispatch synthetic hover events on each trigger. Record what
151
+ changed, from what to what, and by what trigger (load/scroll/hover/mutation). A candidate
152
+ frozen in its start state after the sweep is presentational noise, not a behavior.
153
+ `pxBehaviorDiscover()` runs this pass and writes the result — save it as
154
+ `targets/<name>/behaviors-live.json`.
155
+ 3. **Measure, never eyeball.** A marquee's speed is a real px/sec sampled over a real
156
+ 1-second window (`pxBehaviorDiscover`'s `marqueeSelectors` option), not "looks about
157
+ right." A reveal's end-state is its settled computed style, not a screenshot glance.
158
+ 4. **Reproduce in ONE vanilla `clone/fixes.js`** (no framework, each behavior in its own
159
+ guarded `try`). Common patterns and how to do them right (full detail + the reference
160
+ implementation live in `lovable_dupe_html/CLONE_PLAYBOOK.md` §8 and
161
+ `lovable_dupe_html/snapshot/fixes.js`):
162
+ - **Marquee/logo belt:** find the actual moving wrapper (often holds *multiple* track
163
+ copies inside an `overflow-clip` viewport) and animate that WHOLE wrapper as one unit —
164
+ animating a single inner track gives "static + moving at the same time."
165
+ - **Scroll reveals / gradient sweeps:** the element is usually already in the DOM, stuck
166
+ in its start state. Re-trigger via `IntersectionObserver` on scroll-in.
167
+ - **Hover-mounted content (mega-menus, portals):** not in the static DOM at all — capture
168
+ the live markup on hover (dispatch `pointerover`/`pointerenter`, wait for it to settle,
169
+ POST the panel's `outerHTML`), sanitize it, and toggle it on hover positioned by the
170
+ MEASURED relationship to its anchor, never a guessed offset.
171
+ - **Typewriter / rotating placeholder:** sample the live text over several seconds to
172
+ collect the real phrases before typing/erasing them.
173
+ `harness/capture-build.js --fixes` wires `<script src="fixes.js" defer>` before `</body>`
174
+ and scaffolds a starter file the first time (never overwrites one you've written).
175
+ 5. **Discover on the CLONE the same way**, with `fixes.js` loaded and captured AFTER the
176
+ same settle procedure as live (so end-states genuinely match, not just at t=0) →
177
+ `targets/<name>/behaviors-clone.json`.
178
+ 6. **Gate:** `node harness/workflow.js gate <name> behavior` compares every live behavior to
179
+ the clone's by key and measured value within a documented tolerance (WORKFLOW.md). A
180
+ behavior that's genuinely irreproducible statically (WebGL/canvas generative content) is
181
+ documented in `targets/<name>/behavior-deviations.json` with a reason — never silently
182
+ dropped. An empty/absent live inventory does not pass unless the discovery pass's own
183
+ metadata proves it actually ran (scroll sweep range, observer duration, elements scanned).
184
+
185
+ ## Phase 6 — The human-flagged fix loop (detection → measure → fix → verify)
186
+ Detection stays with a human (or a vision reviewer) — they're the best perceptual
187
+ detector. **The workflow enforces this as the `human` phase**: `node harness/human-qa.js
188
+ file <name> --draft <public-url>` files a scope-pinned PingHumans side-by-side (per-leaf
189
+ steps from coverage.json; JS behavior marked informational), and the gate passes only on
190
+ a fetched approving verdict — a rejection's notes are your flag list for the loop below,
191
+ then refile. But once a human points, **stop guessing the property.** Measure the whole
192
+ element:
193
+
194
+ 1. **Localize the element that *paints* the mark — not the one you labelled.** A
195
+ colour lives on the text, but an underline may be a `border-bottom` on an
196
+ **ancestor group**, a shadow on a wrapper, a rule on a sibling. Resolve *that*
197
+ element on **both** pages:
198
+ ```js
199
+ copy(pxInspect({ text: "sign in" })) // also {aria:/cart/}, {sel:".x"}, {at:[x,y]}
200
+ ```
201
+ Save `el_live.json` / `el_clone.json`. (Live CSP → `pxStashInspect(resolver)` +
202
+ batched `pxRead`.) If the paint bucket comes back empty but the human still sees it,
203
+ you inspected the wrong element — walk up to the ancestor whose box actually draws
204
+ the mark and measure *its* box (top/height/border), because that box positions it.
205
+ 2. **Measure** — diff the element's **entire computed style**, paint-first:
206
+ ```sh
207
+ node tools/pixel-diff.js --inspect el_live.json el_clone.json
208
+ ```
209
+ - **PAINT** bucket = every visible difference with exact values (colour, border,
210
+ decoration/underline, background, transform, opacity, geometry…). *This is the
211
+ fix list.*
212
+ - **STRUCTURAL** = layout-technique differences, demoted (usually fine).
213
+ - Irrelevant props (`transition`, `cursor`, …) hidden.
214
+ 3. **Fix by reproducing the box model + technique, not by offsetting.** If the mark's
215
+ position falls out of a box (a `border-bottom` sits at a group's content-bottom;
216
+ the group's `top`+`height`+`box-sizing` decide where), build **that same box** so
217
+ the mark *emerges* in place. A hand-tuned `top:-3.6px` that merely lands the number
218
+ is fragile — it drifts when neighbours change and **rasterises differently** (a
219
+ positioned `<div>` ≠ a `border`), so it fails a flicker/overlay comparison even at
220
+ 0.01px. Match the drawing element's type too.
221
+ 4. **Fix the *whole* mark in one shot, not the one facet named.** A decoration has
222
+ thickness + width + offset + colour + technique; fixing only "it's too thin" leaves
223
+ width and Y wrong and the human iterates again. Measure and set all of them together.
224
+ 5. **Verify** — re-run step 2. **Done = the PAINT bucket is empty (exit 0)** — a
225
+ measured fact, never a screenshot.
226
+
227
+ Why this beats patching the tool per bug: the measure step compares the **whole**
228
+ computed style, so a novel property (a shadow, an outline) is caught the **first**
229
+ time — you never grow an allowlist one embarrassing miss at a time.
230
+
231
+ ## Phase 7 — Final evaluation
232
+ **Do:** Produce one match table straight from `pixel-diff.js` output (element →
233
+ property → live vs ours → Δ → ✅/❌), noting intentional non-matches and why.
234
+ **Verify:** `--visual` exits 0 at every breakpoint; coverage list empty; every strict
235
+ structural delta explained; every human-flagged element's `--inspect` paint bucket empty.
236
+ **Evaluate:** overall pass = real assets + all pixel-determining properties of all
237
+ covered elements within tolerance + no uncovered element + every structural
238
+ difference documented. A green diff that skipped elements or properties is not a pass.
239
+
240
+ ---
241
+
242
+ ## Ground rules (the non-negotiables)
243
+ 1. **No asset is hand-drawn or hand-typed** — fonts/icons/logos/vectors/images come
244
+ only from the extractors or a direct live-site capture (real files, real URLs).
245
+ 2. **No eyeballing.** Every "match" is a `pixel-diff.js` run. Screenshots decide only
246
+ *what to measure*, never *whether it matches*.
247
+ 3. **No silent gaps.** Every painted element in the region has a target; enumerate
248
+ and close coverage.
249
+ 4. **Match across breakpoints** — run the diff at each captured breakpoint.
250
+ 5. **Document intentional deviations** (faux-bold, structural technique choices,
251
+ accepted sub-pixel deltas) with their cause — never silent.
252
+ 6. **A colour / visibility / underline delta is never "structural."** If strict shows
253
+ `font.color` or `font.underline`, fix it — don't wave it off as noise.
254
+ 7. **Reproduce the technique of a painted mark, not just its number.** A border,
255
+ decoration, or shadow must be drawn the way the live site draws it (right element,
256
+ right property), so it rasterises identically under a flicker/overlay compare. A
257
+ magic offset that lands the coordinate but uses a different primitive is a defect.
258
+ 8. **A mark is a box.** Decorations (underline/strike/border) and glyphs have
259
+ thickness, width, and position — measure and match all of them, and capture
260
+ `-webkit-font-smoothing`. "Present: true" or a matched colour is not a matched mark.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # pixel-perfect-kit
2
+
3
+ A small, framework-agnostic toolkit for **cloning a website's visual identity and
4
+ proving the clone is pixel-perfect with numbers, never by eye.** Point it at any
5
+ live site, lift the real fonts/icons/vectors, then diff your recreation against the
6
+ live DOM property-by-property until every pixel-determining value matches.
7
+
8
+ The guiding rule behind everything here:
9
+
10
+ > **Measure the *painted mark*, not the wrapper — and measure it *completely*.**
11
+ > A green check is a command that exits 0, not a screenshot that "looks close."
12
+
13
+ ## Start here (one line, then just talk to your agent)
14
+
15
+ ```sh
16
+ npx pixel-perfect-kit setup
17
+ ```
18
+
19
+ One interactive pass: installs `ppk`, offers `cloudflared`, runs the PingHumans login
20
+ (skippable — local review mode needs no account), checks for the optional `ditto` fast
21
+ builder, and teaches your AI agent both skills. Idempotent — re-run anytime. (Prefer the
22
+ pieces? `npm i -g pixel-perfect-kit`, then `ppk doctor` + `ppk agent-setup`.)
23
+
24
+ Then, in your AI agent (e.g. Claude Code with browser access):
25
+
26
+ > **"Clone https://example.com pixel-perfect."**
27
+
28
+ The agent drives the whole pipeline — capture, numeric gates, behavior reproduction,
29
+ review rounds — and you do exactly one job:
30
+
31
+ ### You are the reviewer
32
+ Review pings arrive with a **side-by-side compare UI** (original vs clone). Steer by
33
+ **pinning comments** on anything that looks wrong, in your own words — "this text sits a
34
+ bit high", "these cards should animate in". **Always pick a verdict button**: comment-only
35
+ reviews stall the pipeline (approval is never inferred from prose). Your browser's
36
+ rendering of the original is the ground truth — even when it differs from what the
37
+ agent's browser sees (LEARNINGS #20). The clone is *done* only when every machine gate
38
+ is green **and** you've pressed approve.
39
+
40
+ Two review modes, same contract:
41
+ - **Marketplace (default)** — rounds go to PingHumans reviewers over a public tunnel and
42
+ bill small credits (`npx cpyany setup` creates the login). Independent verdicts.
43
+ - **Local (no account, no tunnel, free)** — `ppk human <name> file --local`, then open
44
+ `http://localhost:8080/__review`: the clone with click-to-pin, the same step list, the
45
+ same mandatory verdict buttons. Recorded as `provider:"local"` in every receipt —
46
+ operator-trusted by definition, so it's never mistaken for independent review.
47
+
48
+ Prerequisites doctor checks for you: Node ≥ 18 (required), plus `cloudflared` and the
49
+ PingHumans login (marketplace mode only — local mode needs neither) — and your agent
50
+ needs browser automation.
51
+
52
+ ### Bring your own builder (ditto, Lovable, anything)
53
+
54
+ Generators like [ditto](https://github.com/ion-design/ditto.site) compile a URL into a
55
+ runnable codebase — by their own framing, a **starting point (~90%)**, not a finished
56
+ replica. The kit's human-review loop is the last 9%: it works with ANY builder's output,
57
+ no pixel pipeline required.
58
+
59
+ ```sh
60
+ ppk adopt acme https://acme.com # register the external build for the loop
61
+ npm run dev # your builder's dev server (ditto: next/vite)
62
+ ppk tunnel acme --url http://localhost:3000 # public, reachability-verified
63
+ ppk human acme file # round 1: a human compares clone vs original
64
+ # fix what they pin → refile with --changelog "…" → repeat until the verdict approves
65
+ ```
66
+
67
+ Agents get this flow automatically: `ppk agent-setup` installs the **ditto-finish**
68
+ skill alongside the main one — "clone https://acme.com with ditto and polish it" runs
69
+ build → adopt → tunnel → human rounds until approval, unattended between your reviews.
70
+
71
+ ### Bring your own builder (ditto, Lovable, hand-built…)
72
+ Generators like [ditto](https://github.com/ion-design/ditto.site) compile a URL into a
73
+ runnable codebase and, by their own framing, produce **starting points — roughly 90%**.
74
+ The human-review loop is the last 9: publish the build, put it side-by-side with the
75
+ original in front of a real human, fix what they pin, refile with a changelog, repeat
76
+ until they approve. The loop is builder-agnostic:
77
+
78
+ ```sh
79
+ ppk adopt mysite https://example.com # register the external build (no pixel gates)
80
+ ppk tunnel mysite --url http://localhost:3000 # publish its dev server (reachability-verified)
81
+ ppk human mysite file # round 1 — then fix pins, refile with --changelog
82
+ ```
83
+
84
+ Agents get this as the **ditto-finish** skill (installed by `ppk agent-setup`): "clone
85
+ https://example.com with ditto and polish it" runs the whole ditto → human-loop flow.
86
+
87
+ <details>
88
+ <summary>Driving it by hand / developing the kit (the manual quickstart)</summary>
89
+
90
+ Run `ppk` in any project — your clone `targets/` are created in the current directory;
91
+ the kit's tools/docs come from the installed package (`ppk where` prints their location).
92
+
93
+ ```sh
94
+ ppk new aloyoga https://www.aloyoga.com/ 1728 # scaffold a target + seed the workflow
95
+ ppk sink & # snapshot receiver (:7799)
96
+ # on the live tab: await pxSendDom('http://localhost:7799/dom.html') ← capture the DOM
97
+ ppk capture-build aloyoga # DEFAULT build: clone FROM the capture (LEARNINGS #19)
98
+ ppk serve aloyoga # serve the clone + the kit's /tools (:8080)
99
+ # …measure live, capture the clone… (see PLAYBOOK.md + tools/RUNBOOK.md)
100
+ ppk score aloyoga # is this iteration better? (a number, not a vibe)
101
+ ppk status aloyoga # the enforced workflow: what's done / next / gated
102
+ ```
103
+
104
+ The workflow is **enforced** (see `WORKFLOW.md`): `ppk advance <name> <phase>` refuses to mark a
105
+ phase done unless its gate exits 0, and refuses out of order — so nothing claims "pixel-perfect"
106
+ without the receipts. `ppk help` lists every command. Handing the run to an agent yourself?
107
+ `LAUNCH-PROMPT.md` is the canonical wrapper (the skill uses the same path).
108
+
109
+ </details>
110
+
111
+ ## What's in the box
112
+
113
+ ```
114
+ pixel-perfect-kit/
115
+ ├── README.md ← you are here — what it is, quickstart, tool index
116
+ ├── PLAYBOOK.md ← the method: how to clone + verify, step by step
117
+ ├── WORKFLOW.md ← the ENFORCED pipeline: hard-gated phases (the kit's `gjc`)
118
+ ├── LEARNINGS.md ← the failure catalog — read before trusting any "match"
119
+ ├── DEVELOP.md ← the META-LOOP: improve the kit by cloning real sites
120
+ ├── CLONE-ANY-SITE.md ← the ONE-SHOT template: whole page, capture-built, gated,
121
+ │ human-approved — hand it to an agent and wait
122
+ ├── CLONE-ANY-HEADER.md ← ready-to-run prompt template ({{URL}}/{{WIDTH}}/{{OUTPUT_PATH}})
123
+ ├── CLONE-ALOYOGA-HEADER.md ← the same prompt, pre-filled for aloyoga.com (example)
124
+ ├── tools/
125
+ │ ├── extract-fonts.js paste in DevTools → one zip of the real @font-face woff2s
126
+ │ ├── extract-icons.js paste in DevTools → one zip of the real SVG/data-URI icons
127
+ │ ├── pixel-diff.js the measure + numeric-diff engine (browser capture + Node diff)
128
+ │ ├── browser-capture.js the browser half of pixel-diff.js, split out to inject as
129
+ │ │ PLAIN SOURCE on a strict-CSP live site (no base64/gzip)
130
+ │ ├── behavior-capture.js the browser half of the `behavior` phase: discovers + MEASURES
131
+ │ │ JS-driven dynamics (reveals, marquees, rotations, hover menus)
132
+ │ │ on live and clone, so reproduction is judged by number — and
133
+ │ │ keeps declared-but-unfired candidates as inventory (no-js /
134
+ │ │ bot-gated sites where nothing fires but everything should)
135
+ │ ├── behavior-worksheet.js one row per supposed-to-move behavior with its disposition;
136
+ │ │ unresolved rows print ready-to-send questions for the human
137
+ │ ├── sink.js tiny POST→file receiver for delivering the clone snapshot
138
+ │ ├── merge-snapshot.js fold a PARTIAL re-capture into a full snapshot (fast fix-loop
139
+ │ │ iteration; the done gate demands one final full capture)
140
+ │ ├── selftest.js guards the diff engine — asserts it still compares the
141
+ │ │ underline box + font-smoothing (run: node tools/selftest.js)
142
+ │ └── RUNBOOK.md the fast, verified command sequence for a live-vs-clone diff
143
+ ├── bin/ppk shim for the enforced workflow (→ harness/workflow.js)
144
+ ├── harness/ the dev framework (see DEVELOP.md)
145
+ │ ├── new-target.js scaffold a disposable clone workspace for a URL (+ seed the workflow)
146
+ │ ├── capture-build.js the DEFAULT build step: clone FROM the captured live DOM —
147
+ │ │ self-hosts CSS/fonts, strips scripts/CSP, preserves the doctype
148
+ │ │ byte-for-byte (kills the technique-mismatch class, LEARNINGS #19)
149
+ │ ├── human-qa.js the HUMAN phase: files a scope-pinned PingHumans side-by-side and
150
+ │ │ gates on the fetched verdict — human iteration rounds, unattended
151
+ │ ├── tunnel.js public HTTPS tunnel for the clone, byte-VERIFIED to serve it before
152
+ │ │ it's recorded (a dead tunnel burns a human round — hn round 1)
153
+ │ ├── workflow.js the ENFORCED phase state machine — gates block until a check exits 0
154
+ │ ├── serve.js static server for a target's clone (+ /tools)
155
+ │ ├── score.js score live-vs-clone, compare to the previous run
156
+ │ ├── regression.js selftest + workflow-selftest + every fixture + detection battery — the guardrail
157
+ │ ├── fixtures/ one file per class-of-miss, so it can never recur
158
+ │ └── benchmarks/ detection-power battery — score a gate change old-vs-new
159
+ │ (node harness/benchmarks/detection-power.js --vs HEAD)
160
+ └── targets/ ← DISPOSABLE per-site instances; the kit is the product
161
+ ```
162
+
163
+ **Developing the kit itself** (cloning sites to make the tools + instructions better) —
164
+ see **`DEVELOP.md`**. In short: `node harness/new-target.js <name> <url> <width>`, build
165
+ the clone, `node harness/score.js <name>` to loop, and turn every miss the gate didn't
166
+ catch into a tool check + a `harness/fixtures/` regression + a generalized lesson.
167
+
168
+ Zero dependencies. The extractors and the browser half of `pixel-diff.js` run by
169
+ pasting into a DevTools console (or via a browser-automation `javascript_tool`);
170
+ the diff half runs in Node (`node tools/pixel-diff.js …`), so pass/fail is
171
+ reproducible and CI-gateable.
172
+
173
+ **What the numeric gate measures** (so a green `--visual` is trustworthy): geometry,
174
+ the text-glyph box via `Range`, the painted glyph + `background-position` for
175
+ icons/logos, the full box-model, font metrics **including `line-height`,
176
+ `letter-spacing`, `color`, `-webkit-font-smoothing`, the underline as a *box***
177
+ (thickness / width / offset, measured on whichever element — often an ancestor —
178
+ draws it), **and the painted *backdrop*** (`bg` — the bar/button/badge colour behind
179
+ a mark, so a wrong announcement-bar colour can't pass a green sweep). Perceived-weight and underline-geometry misses that used to slip a green
180
+ sweep now fail on the first run; `node tools/selftest.js` locks that in. What the gate
181
+ can't do — reproduce the *drawing technique* so a mark also rasterises identically —
182
+ stays with you (see `PLAYBOOK.md` Phase 6 and `LEARNINGS.md` "the gate vs your eyes").
183
+
184
+ ## The two verification modes (this is the whole point)
185
+
186
+ 1. **Full-page regression sweep** — a curated `pxTargets` list (logo, each nav item,
187
+ each icon…). Capture both pages, diff, require an all-green `--visual` table, then
188
+ **close coverage**: every painted element in the region must have a target.
189
+ ```sh
190
+ node tools/pixel-diff.js live.json clone.json --visual # exit 0 = looks identical
191
+ node tools/pixel-diff.js live.json clone.json # strict: also flags structure
192
+ ```
193
+
194
+ 2. **Human-flagged drill-down** — someone says *"this looks wrong here."* Resolve
195
+ that one element and diff its **entire computed style** live-vs-clone, so **every**
196
+ real difference surfaces (a border, a shadow, a `text-decoration`, a background)
197
+ without adding a per-property rule. Paint differences first, structure demoted.
198
+ ```sh
199
+ node tools/pixel-diff.js --inspect el_live.json el_clone.json # exit 0 = 0 paint diffs
200
+ ```
201
+ This is the detection→**measure**→fix→verify loop. Detection is the human;
202
+ everything after is measured, and "fixed" means the paint bucket is empty.
203
+
204
+ ## Quickstart
205
+
206
+ 1. **Extract assets.** On the live site's DevTools console:
207
+ `extractFonts(/yourfont/i)` and `extractIcons('header')` → two zips → unpack into
208
+ your project's `assets/`. (See tool sections in `PLAYBOOK.md`.)
209
+ 2. **Build your recreation** in whatever stack you like, driven by measurements — not
210
+ guesses. Load the real woff2s; use the real icon data-URIs; match the measured
211
+ type tokens.
212
+ 3. **Verify.** Start your clone dev server + `node tools/sink.js`. Follow
213
+ `tools/RUNBOOK.md` to capture both pages at the **same viewport width** and diff.
214
+ Loop `--visual` until it exits 0 **and** coverage is empty.
215
+ 4. **When a human spots something off**, run the `--inspect` loop on that element
216
+ until its paint bucket is empty.
217
+
218
+ Start with **PLAYBOOK.md** for the full method, and read **LEARNINGS.md** first —
219
+ every rule in it was paid for by a real miss.