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.
- package/CLONE-ALOYOGA-HEADER.md +76 -0
- package/CLONE-ANY-HEADER.md +106 -0
- package/CLONE-ANY-SITE.md +120 -0
- package/DEVELOP.md +144 -0
- package/LAUNCH-PROMPT.md +66 -0
- package/LEARNINGS.md +398 -0
- package/LICENSE +21 -0
- package/PLAYBOOK.md +260 -0
- package/README.md +219 -0
- package/WORKFLOW.md +257 -0
- package/bin/ppk +4 -0
- package/harness/adopt.js +52 -0
- package/harness/agent-setup.js +55 -0
- package/harness/behavior-selftest.js +243 -0
- package/harness/benchmarks/README.md +34 -0
- package/harness/benchmarks/battery.js +86 -0
- package/harness/benchmarks/detection-power.js +75 -0
- package/harness/capture-build-selftest.js +134 -0
- package/harness/capture-build.js +323 -0
- package/harness/doctor-selftest.js +58 -0
- package/harness/doctor.js +85 -0
- package/harness/fixtures/01-backdrop-color.js +51 -0
- package/harness/fixtures/02-line-strut.js +53 -0
- package/harness/fixtures/03-compat-mode.js +44 -0
- package/harness/fixtures/README.md +31 -0
- package/harness/human-qa-selftest.js +201 -0
- package/harness/human-qa.js +406 -0
- package/harness/merge-snapshot-selftest.js +56 -0
- package/harness/new-target.js +101 -0
- package/harness/regression.js +50 -0
- package/harness/score.js +58 -0
- package/harness/serve.js +149 -0
- package/harness/setup-selftest.js +93 -0
- package/harness/setup.js +158 -0
- package/harness/tunnel-selftest.js +76 -0
- package/harness/tunnel.js +241 -0
- package/harness/workflow-selftest.js +211 -0
- package/harness/workflow.js +739 -0
- package/package.json +40 -0
- package/skill/ditto-finish/SKILL.md +52 -0
- package/skill/pixel-perfect-clone/SKILL.md +49 -0
- package/tools/RUNBOOK.md +228 -0
- package/tools/behavior-capture.js +307 -0
- package/tools/behavior-worksheet.js +82 -0
- package/tools/browser-capture.js +240 -0
- package/tools/cli-selftest.js +136 -0
- package/tools/extract-fonts.js +133 -0
- package/tools/extract-icons.js +255 -0
- package/tools/merge-snapshot.js +56 -0
- package/tools/pixel-diff.js +845 -0
- package/tools/reassemble.js +63 -0
- package/tools/selftest.js +67 -0
- 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/`).
|
package/tools/RUNBOOK.md
ADDED
|
@@ -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.
|