@trueclone/cli 0.0.0-stage → 0.2.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.
@@ -0,0 +1,169 @@
1
+ // Record the discrete states of a page's widgets (tabs, menus, accordions) so the clone can replay them. Many
2
+ // component libraries render only the active panel, so the other tabs' content and closed menus' content aren't in
3
+ // the captured page at all; the only faithful source is the original after each click.
4
+ //
5
+ // For every control (role="tab", or a button with aria-expanded) the reference page is clicked, and the region the
6
+ // control changes is snapshotted: the smallest element containing both the control and the panel it points at
7
+ // (aria-controls), falling back to its section. Controls are marked data-tc-trigger and regions data-tc-scope
8
+ // before clicking, so the snapshots carry the marks and `trueclone seed` can wire the clone: a click on a trigger
9
+ // swaps its region to the recorded state. Markup only, never the site's scripts; template mode gets placeholders.
10
+ //
11
+ // Not covered yet: popovers rendered in a portal at the end of <body> (comboboxes, tooltips); they are listed.
12
+ //
13
+ // usage: node scripts/widgets.mjs <name> [--viewport desktop] -> sites/<name>/reference/widgets.<vp>.json
14
+ import fs from 'node:fs/promises';
15
+ import path from 'node:path';
16
+ import { VIEWPORTS, parseArgs, openBrowser, newPage, waitStill } from './lib.mjs';
17
+
18
+ const { pos, flags } = parseArgs(process.argv.slice(2));
19
+ const [name] = pos;
20
+ if (!name) {
21
+ console.error('usage: trueclone widgets <name> [--viewport desktop]');
22
+ process.exit(1);
23
+ }
24
+ const root = path.resolve('sites', name);
25
+ const meta = JSON.parse(await fs.readFile(path.join(root, 'reference', 'meta.json'), 'utf8'));
26
+ const vp = VIEWPORTS.find((v) => v.name === (flags.viewport || 'desktop'));
27
+
28
+ // Marks every control and its region; returns the groups (a tablist, or a single expandable button).
29
+ function markInPage() {
30
+ const SKIP = new Set(['SCRIPT', 'NOSCRIPT', 'TEMPLATE', 'STYLE', 'LINK', 'META']);
31
+ const pathOf = (el) => {
32
+ const parts = [];
33
+ for (let n = el; n && n !== document.body; n = n.parentElement) {
34
+ const sibs = [...n.parentElement.children].filter((c) => !SKIP.has(c.tagName));
35
+ parts.unshift(`${n.tagName.toLowerCase()}:${sibs.indexOf(n) + 1}`);
36
+ }
37
+ return parts.join('>');
38
+ };
39
+ const lca = (a, b) => { for (let n = a; n; n = n.parentElement) if (n.contains(b)) return n; return null; };
40
+ const visible = (el) => { const r = el.getBoundingClientRect(); return r.width > 2 && r.height > 2; };
41
+ const groups = [];
42
+ const portals = [];
43
+ // tabs, grouped by their tablist
44
+ const lists = new Set([...document.querySelectorAll('[role="tab"]')].filter(visible).map((t) => t.closest('[role="tablist"]') || t.parentElement));
45
+ for (const list of lists) {
46
+ const tabs = [...list.querySelectorAll('[role="tab"]')].filter(visible);
47
+ if (tabs.length < 2) continue;
48
+ const selected = tabs.find((t) => t.getAttribute('aria-selected') === 'true') || tabs[0];
49
+ const panel = selected.getAttribute('aria-controls') && document.getElementById(selected.getAttribute('aria-controls'));
50
+ let scope = panel ? lca(list, panel) : list.closest('section, header, footer, main > div') || list.parentElement;
51
+ if (!scope || scope === document.body) scope = list.closest('section, header, footer') || list.parentElement;
52
+ groups.push({ kind: 'tabs', scope, triggers: tabs, initial: tabs.indexOf(selected) });
53
+ }
54
+ // expandable buttons (menus, accordions, disclosures), not inside a tablist
55
+ for (const btn of [...document.querySelectorAll('button[aria-expanded], [role="button"][aria-expanded]')].filter(visible)) {
56
+ if (btn.closest('[role="tablist"]')) continue;
57
+ const popup = btn.getAttribute('aria-haspopup');
58
+ if (btn.getAttribute('role') === 'combobox' || popup === 'listbox' || popup === 'dialog') { portals.push(btn); continue; }
59
+ const scope = btn.closest('header, nav, section, footer, li') || btn.parentElement;
60
+ groups.push({ kind: 'expand', scope, triggers: [btn], initial: btn.getAttribute('aria-expanded') === 'true' ? 0 : -1 });
61
+ }
62
+ // an element can be the region of several widgets (all the menus of one nav): marks are space-separated lists
63
+ const addMark = (el, v) => { const cur = (el.getAttribute('data-tc-scope') || '').split(' ').filter(Boolean); if (!cur.includes(v)) cur.push(v); el.setAttribute('data-tc-scope', cur.join(' ')); };
64
+ groups.forEach((g, i) => {
65
+ addMark(g.scope, String(i));
66
+ g.triggers.forEach((t, j) => t.setAttribute('data-tc-trigger', `${i}:${j}`));
67
+ });
68
+ window.__tcGroups = groups;
69
+ return {
70
+ groups: groups.map((g, i) => ({ i, kind: g.kind, scopePath: pathOf(g.scope), triggers: g.triggers.map((t) => ({ path: pathOf(t), label: (t.getAttribute('aria-label') || t.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40) })), initial: g.initial })),
71
+ portals: portals.map((b) => (b.getAttribute('aria-label') || b.textContent || '').trim().slice(0, 40)),
72
+ };
73
+ }
74
+
75
+ // Frameworks re-render a region when its state changes, which drops the marks on anything inside it (a tab set
76
+ // nested in another tab's panel). Marks are re-applied by path before every click and every snapshot.
77
+ function remarkInPage(groups) {
78
+ const SKIP = new Set(['SCRIPT', 'NOSCRIPT', 'TEMPLATE', 'STYLE', 'LINK', 'META']);
79
+ const resolve = (p) => {
80
+ let el = document.body;
81
+ for (const part of p.split('>')) {
82
+ const [tag, n] = part.split(':');
83
+ el = [...el.children].filter((c) => !SKIP.has(c.tagName))[Number(n) - 1];
84
+ if (!el || el.tagName.toLowerCase() !== tag) return null;
85
+ }
86
+ return el;
87
+ };
88
+ for (const g of groups) {
89
+ const scope = resolve(g.scopePath);
90
+ if (scope) { const cur = (scope.getAttribute('data-tc-scope') || '').split(' ').filter(Boolean); if (!cur.includes(String(g.i))) cur.push(String(g.i)); scope.setAttribute('data-tc-scope', cur.join(' ')); }
91
+ g.triggers.forEach((t, j) => resolve(t.path)?.setAttribute('data-tc-trigger', `${g.i}:${j}`));
92
+ }
93
+ }
94
+
95
+ // Menus often render their panel outside the trigger's own container (a shared viewport after the list): once open,
96
+ // the region becomes the smallest element holding both the trigger and the panel it controls.
97
+ function widenScopeInPage([i, sel]) {
98
+ const SKIP = new Set(['SCRIPT', 'NOSCRIPT', 'TEMPLATE', 'STYLE', 'LINK', 'META']);
99
+ const pathOf = (el) => {
100
+ const parts = [];
101
+ for (let n = el; n && n !== document.body; n = n.parentElement) {
102
+ const sibs = [...n.parentElement.children].filter((c) => !SKIP.has(c.tagName));
103
+ parts.unshift(`${n.tagName.toLowerCase()}:${sibs.indexOf(n) + 1}`);
104
+ }
105
+ return parts.join('>');
106
+ };
107
+ const btn = document.querySelector(sel);
108
+ const panel = btn?.getAttribute('aria-controls') && document.getElementById(btn.getAttribute('aria-controls'));
109
+ if (!btn || !panel) return null;
110
+ let scope = btn;
111
+ while (scope && !scope.contains(panel)) scope = scope.parentElement;
112
+ if (!scope || scope === document.body) return null;
113
+ for (const old of document.querySelectorAll(`[data-tc-scope~="${i}"]`)) {
114
+ const rest = old.getAttribute('data-tc-scope').split(' ').filter((v) => v !== String(i));
115
+ if (rest.length) old.setAttribute('data-tc-scope', rest.join(' ')); else old.removeAttribute('data-tc-scope');
116
+ }
117
+ const cur = (scope.getAttribute('data-tc-scope') || '').split(' ').filter(Boolean);
118
+ cur.push(String(i));
119
+ scope.setAttribute('data-tc-scope', cur.join(' '));
120
+ return pathOf(scope);
121
+ }
122
+
123
+ const browser = await openBrowser(flags.cdp);
124
+ let result;
125
+ try {
126
+ const { page } = await newPage(browser, vp, { reducedMotion: 'reduce' });
127
+ await page.goto(meta.url, { waitUntil: 'load', timeout: 60000 });
128
+ await waitStill(page, { timeout: 15000, interval: 200 });
129
+ const found = await page.evaluate(markInPage);
130
+ console.log(`${found.groups.length} widget group(s): ${found.groups.filter((g) => g.kind === 'tabs').length} tab sets, ${found.groups.filter((g) => g.kind === 'expand').length} expandable${found.portals.length ? `; not recorded (portal popovers): ${found.portals.join(', ')}` : ''}`);
131
+ for (const g of found.groups) {
132
+ g.states = [];
133
+ const snapshot = async () => { await page.evaluate(remarkInPage, found.groups); return page.evaluate((i) => document.querySelector(`[data-tc-scope~="${i}"]`)?.innerHTML ?? null, g.i); };
134
+ for (let j = 0; j < g.triggers.length; j++) {
135
+ await page.evaluate(remarkInPage, found.groups);
136
+ const handle = await page.$(`[data-tc-trigger="${g.i}:${j}"]`);
137
+ if (!handle) { g.states.push(null); continue; }
138
+ await handle.scrollIntoViewIfNeeded().catch(() => {});
139
+ await handle.click({ timeout: 5000 }).catch(() => {});
140
+ await page.waitForTimeout(350);
141
+ await waitStill(page, { timeout: 2500 });
142
+ if (g.kind === 'expand') {
143
+ const wider = await page.evaluate(widenScopeInPage, [g.i, `[data-tc-trigger="${g.i}:${j}"]`]);
144
+ if (wider) g.scopePath = wider;
145
+ }
146
+ g.states.push(await snapshot());
147
+ if (g.kind === 'expand') { // the closed state, so the clone can close it again
148
+ await page.evaluate(remarkInPage, found.groups);
149
+ await page.click(`[data-tc-trigger="${g.i}:${j}"]`, { timeout: 5000 }).catch(() => {});
150
+ await page.waitForTimeout(350);
151
+ g.closed = await snapshot();
152
+ }
153
+ }
154
+ // back to the initial state before the next group (tabs: click the initially selected one)
155
+ if (g.kind === 'tabs' && g.initial >= 0) {
156
+ await page.evaluate(remarkInPage, found.groups);
157
+ await page.click(`[data-tc-trigger="${g.i}:${g.initial}"]`, { timeout: 5000 }).catch(() => {});
158
+ await page.waitForTimeout(350);
159
+ }
160
+ const changed = new Set(g.states.filter(Boolean)).size;
161
+ console.log(` ${g.kind.padEnd(6)} ${g.triggers.map((t) => t.label).slice(0, 6).join(' | ')}${g.triggers.length > 6 ? ' ...' : ''}: ${changed} distinct state(s)`);
162
+ }
163
+ result = found;
164
+ } finally {
165
+ if (!flags.cdp) await browser.close();
166
+ }
167
+ const out = path.join(root, 'reference', `widgets.${vp.name}.json`);
168
+ await fs.writeFile(out, JSON.stringify({ url: meta.url, viewport: vp, at: new Date().toISOString(), ...result }, null, 2));
169
+ console.log(`-> ${path.relative(process.cwd(), out)}`);
package/skill/SKILL.md ADDED
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: trueclone
3
+ description: Build a working twin of a website from its URL - HTML/CSS/JS clone, Next.js + Tailwind + shadcn app, DESIGN.md and shadcn globals.css - and prove the match with layout, scroll-state, motion, interaction, hover and style scores at mobile, tablet and desktop widths. Use when the user gives a URL and asks to clone, replicate, rebuild or migrate a site or page, or to extract its design system.
4
+ ---
5
+
6
+ # Build a true clone of a website
7
+
8
+ Everything for a site lives in `sites/<name>/` (gitignored, stays local):
9
+ - `clone/`: plain HTML/CSS/JS, the source of truth.
10
+ - `next/`: the same site as a Next.js + Tailwind v4 + shadcn app, generated from `clone/`.
11
+ - `design-tokens.json`: the **one canonical source** of the design system, rendered to `DESIGN.md` (Google design.md format, linted), `globals.css` (shadcn theme) and `clone/tokens.css`.
12
+ - `REPORT.md`: every score against its target, what to fix first, what was not copied. This is the deliverable.
13
+ - `notes.md`: your notes: stack, motion inventory, decisions, known gaps.
14
+
15
+ Run every step as `npx trueclone <step> <name>` from the project folder (`npx trueclone help` lists them).
16
+
17
+ ## 0. Pick the mode
18
+
19
+ | Mode | Use it when | Content in the clone |
20
+ |---|---|---|
21
+ | `template` (default) | Anyone else's site: learning, layout study, a starting template | Placeholder brand, text, images and logos; scored with `--structure` |
22
+ | `exact` | The user owns the site or is authorised to copy it (a client migration, their own Webflow/Framer/WordPress site) | The site's own text and media |
23
+
24
+ Exact mode refuses to run without `--permission "<who authorised it>"`, and the value is recorded in `meta.json` and in `REPORT.md`. Never use exact mode on a site the user doesn't own or have permission for. If permission is unclear, ask.
25
+
26
+ ## 1. Capture
27
+
28
+ ```powershell
29
+ npx trueclone capture <url> [name] [--hide "<cookie banner / popup selectors>"] [--mode exact --permission "<owner>"]
30
+ npx trueclone states <name> # live scroll states + motion inventory (animations running)
31
+ npx trueclone motion <name> # animation runtime: ScrollTriggers, timelines, reveals, cursor tweens, loops, Lenis/Swiper, CSS
32
+ npx trueclone spec <name> # measured boxes and styles per viewport
33
+ ```
34
+
35
+ `npx trueclone <url> [name]` is a shortcut for capture + design-system draft.
36
+
37
+ `reference/` then holds:
38
+ - full-page PNGs and their `*.mask.png` structure versions, `sections.json` and `rendered.html`;
39
+ - `tokens.json` (colours, type, radii, CSS variables), `assets/` with `manifest.json`, and `desktop.har` (every request, including the data/API calls);
40
+ - **`states/<vp>/NN.png`**: what a visitor sees at every half-viewport scroll step, after motion settles;
41
+ - **`states/<vp>.summary.json`** (the motion inventory): `reveals`, `animated`, `parallax`, `pinned` and `fixed` elements;
42
+ - **`motion.<vp>.md`**: the exact animation recipe when the site uses GSAP/ScrollTrigger, Lenis, Swiper or CSS animations: trigger, start/end, scrub, pin, properties and ranges, duration, easing, stagger. Rebuild these with the same public libraries and these numbers, in your own code;
43
+ - in exact mode, `mirror/index.html`: a script-free local snapshot with assets rewritten to local copies.
44
+
45
+ Read the motion inventory before building. Each entry is a behaviour the clone must have.
46
+
47
+ If the page is blank or blocked, capture through your own Chrome over CDP (`chrome --remote-debugging-port=9222 --user-data-dir=.chrome-profile`, then `--cdp`). Never try to get past bot checks, logins or paywalls.
48
+
49
+ ## 2. Understand it with REA
50
+
51
+ trueclone bundles REA (Reverse Engineer Anything). `npx @trueclone/cli init` registers it as an MCP server; without MCP, run `npx trueclone rea <command>`. With the page open in your CDP Chrome:
52
+ - `inspect_web_page`: DOM, scripts, resources.
53
+ - `analyze_web_bundle` with source maps: framework, vendors (GSAP, ScrollTrigger, Lenis, Swiper, Webflow IX2, three.js), routes and API endpoints.
54
+ - `inspect_web_event_listeners`: hover, drag, wheel and click logic.
55
+
56
+ Use REA when the capture can't tell you how something works: a behaviour's exact rule, bundled shader or physics code, the API a component calls. Write `notes.md`: stack, breakpoints, and the motion inventory turned into a list of behaviours (trigger, property, range).
57
+
58
+ ## 3. Design system
59
+
60
+ ```powershell
61
+ npx trueclone design <name> # drafts design-tokens.json if missing, renders DESIGN.md, globals.css, clone/tokens.css + lints
62
+ npx trueclone design <name> --redraft # fresh draft in design-tokens.draft.json (never overwrites your edits)
63
+ ```
64
+
65
+ Review the draft: role names, the `notes` prose (overview, do's and don'ts), anything marked `origin: generated`. Re-run after every edit. `DESIGN.md` must lint with **0 errors**.
66
+
67
+ ## 4. Build (`clone/`)
68
+
69
+ **Start every clone from the mirror** (both modes):
70
+
71
+ ```powershell
72
+ npx trueclone reveals <name> # records scroll reveals frame by frame + class changes caused by scrolling (any library)
73
+ npx trueclone widgets <name> # clicks every tab and menu and records each state (they often aren't in the page until clicked)
74
+ npx trueclone seed <name> # clone/ from the script-free mirror; replays what reveals and widgets recorded
75
+ ```
76
+
77
+ - **Exact mode:** the site's own markup, text, stylesheets and media at local paths.
78
+ - **Template mode:** the same markup and stylesheets, but every text becomes lorem words of the same laid-out width (lines break where the reference's break), every image, video, large SVG and iframe a same-size placeholder, CSS backgrounds a transparent one, every link `#`, brand metadata and tracking hints dropped. No text or media of the site ends up in `clone/`; fonts stay for local study (swap licensed fonts before reusing the result).
79
+ - **Recorded behaviour comes with it:** one-shot scroll reveals replay their measured curve (delay, duration, easing, spring overshoot) with the Web Animations API, and scroll class changes (a header that gets its backdrop past 12px) toggle the same classes at the same point. Both live in `clone/trueclone-reveals.js`; don't rewrite them by hand.
80
+ - **Recorded widgets come with it too** (template mode): every tab set and expandable menu, with each state the original showed after a click, placeholdered like the page. `clone/trueclone-widgets.js` swaps the region on click (menus in a header or nav also on hover). Popovers rendered in a portal (comboboxes, tooltips) are listed as not replayed: build those in `script.js`. In interaction scenarios, `selector@n` picks the n-th match in `at`, `click` and `expectChange`; mind hidden duplicates (a mobile menu button is often the first match).
81
+
82
+ Then write `clone/script.js`: every remaining interaction in your own code, using the parameters you read from the reference (timings, physics constants, offsets, breakpoints). Never copy the site's scripts. JS-set initial states, such as a `<details>` closed on phones, are part of the layout, so check every viewport. Data the page fetches live (a star count, a feed) stays as captured; no requests to third parties.
83
+
84
+ A seed is where layout ends up at 99%+ with zero hand-written CSS; your work is behaviour. Build by hand (below) only what the mirror can't carry.
85
+
86
+ **Building parts by hand:**
87
+
88
+ - `styles.css` starts with `@import url("tokens.css");` and uses its variables. Never hard-code a token value.
89
+ - Use the site's breakpoints and root font-size behaviour (in `design-tokens.json`, under `layout`).
90
+ - Match `sections.json` heights. In template mode write your own placeholder words, then run `npx trueclone textshape <name> --write` (desktop, then `--viewport tablet` and `mobile`) so lines break where the reference's break. Use `--list "<ref row>" "<clone row>"` for repeated lists (calendar rows, cards), and give every data row its own placeholder text.
91
+
92
+ **Both modes:**
93
+
94
+ - **Copy the reference's CSS mechanism, not its look.** Before writing CSS for an element, run `npx trueclone rules <name> "<selector>"` and port the matching rules (with their `@media`). Look-alikes drift in flex/grid: a "capsize" `::before/::after` text trim is not `text-box-trim`, `height: 14.5rem` is not `flex: 1`, and `width: 100%; max-width: 40rem` with default flex shrink is not `flex: 0 10 auto` or `width: min-content`.
95
+ - **Positional selectors:** `:nth-of-type` counts siblings of the same tag only. A logo `<a>` before the column `<div>`s shifts every column by one, so use `:nth-child` or a class.
96
+ - **Stacked media:** keep every layer (colour, grayscale or depth-map images, overlays). Never flatten them into one image.
97
+ - **Canvas/WebGL:** a site can draw its hero or a game on a `<canvas>`. Rebuild the drawing in your own code with the reference's parameters (exact mode) or a placeholder scene. Never drop it silently.
98
+ - **Motion:** implement every inventory entry: reveals, parallax speeds, pinned sections, fixed elements.
99
+ - **Interactions:** sliders, tabs, hover states, cursor followers, menus, counters, demos. Scope query selectors to their own component: a hidden duplicate (a mobile-only list) must never feed a desktop counter.
100
+ - **Sliders in flex/grid:** give the slider and every flex/grid ancestor `min-width: 0`, or Swiper and similar grow to fit their slides and run away. `states --clone` warns when the page height drifts more than 20%.
101
+ - `<meta name="robots" content="noindex">`, forms submit nowhere, no tracking scripts.
102
+
103
+ ## 5. Verify
104
+
105
+ ```powershell
106
+ npx trueclone score <name> --quick # layout, desktop states, interactions: the loop while building (about a minute)
107
+ npx trueclone score <name> # everything, in parallel groups, then REPORT.md
108
+ ```
109
+
110
+ The steps, if you need one alone:
111
+
112
+ ```powershell
113
+ npx trueclone diff <name> [--structure] # layout: full page, frozen (structure = text and media masked)
114
+ npx trueclone states <name> --clone [--structure] # scroll states: the reference's scroll positions replayed on the clone
115
+ npx trueclone curves <name> --draft # motion-map.draft.json from the inventory: fill in clone selectors, save as motion-map.json
116
+ npx trueclone curves <name> [--viewport tablet] # motion: property curves (scroll- and time-driven), reference vs clone
117
+ npx trueclone interact <name> --draft # interactions.draft.json: fill in TODOs, save as interactions.json
118
+ npx trueclone interact <name> # interactions: click/hover/drag/key scenarios + expectChange (geometry, text, state attributes, canvas pixels)
119
+ npx trueclone detail <name> --viewport all # styles at matching points: font, size, weight, line height, colour, boxes
120
+ npx trueclone hovers <name> # hover effects present on the reference but missing on the clone
121
+ ```
122
+
123
+ Reference measurements are cached until the next capture, so only the clone is re-measured. Sites without scroll/time motion or without interactions simply skip those steps.
124
+
125
+ Fix in this order: page height drift, the worst layout section, the worst state step, the lowest motion curve, any interaction marked FAIL, then detail and hovers. `REPORT.md` lists them under "Fix first". For a drift, compare the element's ancestor boxes on both sides until one differs, then `rules` that ancestor: the cause is usually one declaration. Fix the cause; never tune numbers to the score.
126
+
127
+ **Done when:** layout ≥ 98% at every viewport, states within 5 points of layout, motion ≥ 95%, detail ≥ 95%, hovers ≥ 90%, and every interaction passes. Otherwise stop after 8 passes and report what's left and why.
128
+
129
+ How detail measures, so you can trust it:
130
+ - motion on (many originals ignore `prefers-reduced-motion`, and a clone that honours it lays out differently);
131
+ - both sides measured the same way: a hit test at the same point and scroll position, so sticky and pinned content is compared in place and an identical page scores 100%;
132
+ - through `pointer-events: none` overlays, and never on content the browser doesn't paint (closed `<details>`);
133
+ - colours written by scroll-scrubbed JS show up as in-between values: that is a motion difference, so check the motion inventory.
134
+
135
+ How layout and states measure:
136
+ - the structure mask hides text everywhere, gradient text (`background-clip: text`) and `::before/::after` photo backgrounds included;
137
+ - content that a component mounts only while in view (marked `data-tc-viewmount` by capture) can't appear in a full-page still on either side: layout paints its box the same on both and reports the count; the scroll-state check, with a real viewport, scores it;
138
+ - endless animations (marquees, rotating borders) are frozen on their first frame on both sides before every state screenshot: their phase depends on load time, not on the clone.
139
+
140
+ The live site can change after capture (a redeploy changes its CSS bundle name). When live-site tools (detail, rules) and screenshot scores (layout, states) disagree about a section, re-capture (`capture`, then `states <name>`); that also resets the cached reference measurements.
141
+
142
+ ## 6. Next.js (after the HTML clone passes)
143
+
144
+ ```powershell
145
+ npx trueclone next <name> --verify # generates sites/<name>/next, installs, builds, serves, and runs every score on it
146
+ ```
147
+
148
+ The HTML clone stays the source of truth: change `clone/`, then regenerate. The app has one component per section, the motion script as a client component (GSAP, Lenis, Swiper and three from npm at the clone's versions), `app/globals.css` = shadcn theme + tokens + the clone's stylesheet + preflight parity, and `components.json` so `npx shadcn add <component>` works. It is done when its scores match the HTML clone's within a point.
149
+
150
+ ## 7. Report
151
+
152
+ `npx trueclone score <name>` writes `REPORT.md` (or run `npx trueclone report <name>`): mode and permission, every score against its target, "Fix first", the motion found on the reference, and what was not copied (server behaviour, tracking, and each data/API call the reference makes). Hand it over with the clone.
153
+
154
+ ## Honest limits
155
+
156
+ - Server-side behaviour is never visible from outside: databases, accounts, payments, search. The clone reproduces the client; data calls are listed in the report.
157
+ - A live site is never pixel-identical twice: rotating content, A/B tests and font hinting vary.
158
+ - Template clones of other people's sites stay local. Exact clones need recorded permission and also stay local unless the owner deploys them.