stardust:reskin — same content, donor design
The user has a site (the content source) and a design that already exists somewhere else (the donor: another live site, or a directory of static HTML prototypes; Figma is future scope). Reskin rebuilds the content site's pages so that every visible byte of content survives — text, ordered images, CTAs, SEO metadata — while the surface comes entirely from the donor's tokens and module vocabulary.
The two halves have different contracts:
- Content is non-negotiable. The gate is byte equality of whitespace-normalized visible text, the ordered visible-image set, and full metadata carry-over. Not "close" — equal, after an explicitly declared, executable normalization ledger.
- Design is flexible. Content is re-laid-out onto donor modules; a carousel may become a static card grid, a sidebar may become a band. Structure (element counts, tag sequences) is informational, never gating — a reskin re-structures markup by design.
The decisive rule, validated in the UC2-E1 experiment (a healthcare site × a fintech donor: 2281/2281 text bytes, 7/7 images, 47/47 slots, 13/13 metadata, 17/17 donor-token probe): the page is generated programmatically from the captured content model — content strings are never retyped. Byte fidelity then holds by construction and the content gate becomes a regression check instead of a debugging tool.
Inputs
<content-url>— required. The site whose content is preserved.- Donor, exactly one of:
--donor <url>— a live site whose design system is adopted.--donor-dir <path>— a directory of local static HTML prototypes (claude-design / Mobirise / Relume / Lovable / v0 / hand-coded). Served on localhost and captured through the same path — recipe inreference/donor-sources.md§ Local prototypes.--donor-figma <url>— not yet implemented. The contract is defined (reference/donor-sources.md§ Figma) but the adapter is future scope. Tell the user exactly this: "Figma donors are not implemented yet. Export the frames as static HTML prototypes (or point me at a live staging URL of the design) and re-run with--donor-dir/--donor. The Figma adapter — variables→palette, text styles→type, frame screenshots→vision references, provenance classfigma-mcp— is specced inskills/reskin/reference/donor-sources.mdand will land in a later release." Do not improvise a partial Figma capture.
--pages <slug,slug,...>— optional. Restrict the content capture to specific pages. Default: the archetype set (one page per page family; scale to siblings happens in Phase 6 viamigrate).--scope <selectors>— optional. Pre-declare the content-root scope for a single-page run (comma-separated,!suffix keeps a scope whole; seereference/content-model.md§ Scope declaration). Without it, run the scope-discovery procedure per page.
Setup
- Run the master skill's setup (
../stardust/SKILL.md§ Setup): impeccable dep check, context loader, state read. Flow. Stampstate.json.flow: "reskin"if unset (../stardust/reference/state-machine.md§ Flow keys); if another flow is set, print it and the switch command ($stardust reskin --switch-flow) instead of running. - Playwright import-resolvability probe — same contract as
../extract/SKILL.md§ Setup:node -e "import('playwright').then(()=>process.exit(0))"from the project root; on failurenpm i -D playwright --no-save --legacy-peer-deps. Re-run the probe at the start of every phase that renders — a--no-saveinstall is pruned by any later realnpm i. - Copy the scripts into the project. ESM resolves
import('playwright')from the script's directory and the plugin tree ships nonode_modules. Copyskills/reskin/scripts/*(all five files —capture-content.mjsanddom-equality.mjsimportsource-normalize.mjsas a sibling) byte-identical tostardust/scripts/reskin/, andskills/diff/scripts/ live-session.mjstostardust/scripts/diff/— every reskin gate script (capture-content, dom-equality, donor-probe, and slot-coverage) imports it unconditionally at startup, regardless of target type: without the copy each one exits 2 immediately, even for--helpor a local-file--renderedtarget. It supplies ALL live-target hardening (real-Chrome UA + standard headers, challenge detection, headed-stealth escalation), resolved from../diff/next to../reskin/, so keep the two dirs siblings. Run the copies. - Origin collision — if
stardust/state.jsonrecords a differentsite.originUrl, stop and ask before mixing sites, per../extract/SKILL.md§ Setup.
Procedure
Phase 1 — INGEST DONOR
The donor's design system is captured separately from the content
site, into stardust/canon-source/. Full recipes per donor type in
reference/donor-sources.md; summary:
- Live URL (
--donor <url>): invoke$stardust extract <content-url> --design-source <donor-url>— the existing skill, unchanged. It lands instardust/canon-source/: the donor'spages/,assets/(screenshots included),_brand-extraction.json,_crawl-log.json, and a descriptiveDESIGN.md+DESIGN.json, and stampsstate.json.designSource = { url, capturedAt, path }— see../extract/SKILL.md§ Cross-site brand sources. - Local prototype dir (
--donor-dir <path>): serve the directory on localhost (python3 -m http.server <port> --directory <path>) and run the same--design-sourcecapture path against the localhost origin. Record the real provenance (localhost serve of<path>) incanon-source/_crawl-log.json. Recipe details — page listing, index-less directories, port hygiene — inreference/donor-sources.md§ Local prototypes. - Figma (
--donor-figma): FUTURE. Surface the exact message from § Inputs and stop.
Then author two reskin-owned donor artifacts (contracts in
reference/donor-sources.md):
stardust/reskin/donor-tokens.json— the curated, probe-able token sheet: palette roles, type ramp, layout metrics, button specs, radii, shadows, motifs — every value a computed-style string the Phase 5 probe can assert verbatim. Curated fromcanon-source/DESIGN.json+_brand-extraction.json+ the raw computed styles; cites the donor page each value came from. On a bounded donor capture (a single donor page, no full--design-sourcerun) there is noDESIGN.json,_brand-extraction.json, ordesignSourcestamp to curate from — the token sheet is authored entirely from raw computed-style sampling of the donor page, a first-class parallel path (reference/donor-sources.md§ Two first-class token-sourcing paths).stardust/reskin/donor-modules.md— the enumerated module vocabulary (M1..Mn): one row per donor module with where-seen screenshot evidence and an anatomy description. This is the closed set the Phase 3 mapping brief maps onto.
The pin-one-reference-page rule (hard). Real donors run multiple
design systems concurrently — the experiment's donor served radius-4px
/ 1266px on its homepage and pill-radius / 1080px on older product
pages. Consolidating across them produces a chimera no live page ever
shipped, and the token probe then asserts against nothing. When donor
pages disagree on a token, pin ONE donor reference page per module
family, record it in donor-tokens.json (curatedFrom + a note
naming the reference page), and demote the other pages to
corroboration.
Phase 2 — CONTENT-MODEL CAPTURE
The new capability: a byte-oriented capture of each content page — distinct from extract's design-oriented capture. Run the ported capture script per page:
node stardust/scripts/reskin/capture-content.mjs \
--url <page-url> \
--scope '<sel1,sel2!,...>' \
--normalize stardust/reskin/normalize/<slug>.mjs \
--out stardust/reskin/content-model/<slug>/
It writes stardust/reskin/content-model/<slug>/content-model.json
(full contract in reference/content-model.md): per section-slot —
headings with levels, eyebrows and other leftovers, body paragraphs,
list items, CTAs with absolute hrefs, ordered visible images
(currentSrc + alt), plus the ordered stream (ordered) — the
same content as kind-tagged nodes in document order with nesting and
separator flags, the render surface Phase 4 consumes; page-level —
full SEO metadata (title,
description, canonical, OG, Twitter, JSON-LD, lang, favicon), the
whitespace-normalized visible text of the scope (the content-gate
reference string), and a full-page screenshot.
Two declarations are mandatory per page, because they are the experiment's top two failure modes:
- CONTENT-ROOT SCOPE declaration. Naive scoping silently dropped
30% of the experiment page's content — the hero and a banner
carousel lived inside
<header>, outside the obvious#contentroot, and byte equality would have "passed" against the incomplete reference. Never trustmainor#contentblind. Run the scope-discovery procedure (reference/content-model.md§ Scope discovery): compare the captured scope text against the whole-body text and the page screenshot, verify the h1 is inside the scope, widen with comma-separated multi-scope selectors until everything visible in the screenshot that isn't declared chrome is in the model. Chrome exclusions (nav, footer) are declared deltas (D1-style), not silent omissions. The capture script prints a coverage line (bodyTextLen / scopeTextLen / h1InScope) to make the check mechanical. - EXECUTABLE NORMALIZATION LEDGER. Cookie-consent chrome removal,
carousel de-duplication (hidden slides are absent from
innerText; clone slides duplicate it — rotating carousels break byte determinism), and any page-specific cleanup live in a per-page ledger modulestardust/reskin/normalize/<slug>.mjsthat extends the shared default (scripts/source-normalize.mjs). The same ledger file is passed verbatim to the capture and to every gate (--normalize), so the gate measures exactly the normalization the capture declared. The ledger is code, not prose. Format inreference/content-model.md§ Normalization ledger.
Phase 3 — MAPPING BRIEF
Author stardust/reskin/mapping.md — the cross-origin mapping brief.
Full contract and entry schema in reference/mapping-brief.md. Per
every content slot in the content model:
- assigned donor module id (from
donor-modules.md's closed vocabulary) + a one-line rationale grounded in slot anatomy vs module anatomy; - status ∈
{mapped, new-module, chrome, carried-invisible}— the last for sr-only/visually-hidden content inside the byte scope (carried verbatim into an equivalent hidden element, no donor module;reference/mapping-brief.md§ Status semantics).
Gates before any rendering:
- ≥ 80% of content slots
mappedonto named donor modules. Below that, the donor vocabulary doesn't cover this content — stop and surface (see § Stop conditions). - Every
new-moduleentry is explicitly composed from donor tokens (name the tokens it borrows: input spec + primary button spec, etc.) and listed in the brief's stats block. New modules are never silently improvised mid-render. - Chrome swaps documented — nav/footer replaced by donor chrome carrying the source's links is a declared delta, excluded from the content-gate scope.
- Casing / text-transform policy declared. The donor may render
eyebrows uppercase (or the source may — Chrome's
innerTextreflectstext-transform, so captured text is rendered-case). The underlying text stays byte-faithful; casing is applied via CSStext-transformonly, never by editing the string. Policy details inreference/mapping-brief.md§ Casing policy.
Composite source sections (one wrapper div holding two logical regions) are split into atomic slots in the brief — section ≠ slot.
Phase 4 — PROGRAMMATIC RENDER
The decisive validated rule: the page is GENERATED from
content-model.json — never retyped by hand. Write a renderer
script per archetype (stardust/reskin/renderers/<archetype>.mjs,
modeled on the experiment's render-reskin.mjs) that:
- reads the page's
content-model.jsonand interpolates every visible string, href, and image URL from the model (HTML-escaped) — if a string appears in the output HTML but not in the model, the renderer is wrong; - takes each slot's structure from the ordered stream
(
sections[].ordered,reference/content-model.md§ The ordered stream): kind-tagged nodes in document order, nesting preserved (a CTA wrapping its heading and vice versa),sepflags marking zero-separator inline joins. The stream is innerText-consistent by construction (its text nodes are sliced from the parent's renderedinnerText— the byte-gate basis), so emitting it verbatim is safe. Emit nodes in stream order; emitsep: ""neighbours with no whitespace between them (inline); never reorder, and never reconstruct order or separators fromvisibleText— the first field run did exactly that (per-type arrays matched greedily againstvisibleTextas an oracle) and burned three debug rounds on duplicate identical strings, both directions of CTA/container nesting, and zero-separator inlineliruns, all of which the stream now captures structurally; - renders
formControlnodes as equivalent controls, not text: a<select>carrying the captured option texts verbatim, in order; inputs/textareas carrying value/placeholder — restyled with donor tokens (the new-module composition rules apply), never flattened to prose and never dropped. A select's option text is part of the source'sinnerText, so dropping the control fails the byte gate with nothing structured to render from (reference/content-model.md§ Slot taxonomy, formControls); - carries the metadata block verbatim into
<head>(title, description, canonical, OG, Twitter, JSON-LD — including source garbage like broken JSON-LD URLs: fidelity over repair; flag such items for the human in the run report, never silently fix or silently keep); - takes its surface exclusively from
donor-tokens.json(emitted as CSS custom properties) + the donor module patterns assigned inmapping.md. Use the rendered-page conventions the Phase 5 probe asserts against: content in<main>,.containerfor the measure,.btnfor the donor primary button (reference/gates.md§ Rendered-page conventions); - writes
stardust/reskin/pages/<slug>.html, self-contained.
Renderers may parse a single node's text apart (split a list-item row
into kicker/date/title) but must fail loudly (throw) when a parse
doesn't reproduce the node's text — a silent partial parse is a silent
content drop. A slot whose orderedVerified is false is inspected
before rendering, never trusted blind; when inspection shows stream
text genuinely absent from the slot's visibleText, the
sanctioned fallback is to drop that text before rendering and
record the drop in the model's provenance — filtering ghosts against
visibleText is the documented resolution, distinct from the
forbidden order-reconstruction move
(reference/content-model.md § When orderedVerified is false).
Phase 5 — GATES
Three gate families per page; commands, pass bars, tolerances, and
failure modes in reference/gates.md. Serve or file://-address the
rendered page and run:
(a) CONTENT GATE — all three must pass:
node stardust/scripts/reskin/dom-equality.mjs \
--source <page-url> --source-scope '<declared scope>' \
--normalize stardust/reskin/normalize/<slug>.mjs \
--rendered stardust/reskin/pages/<slug>.html --rendered-scope main \
--report stardust/reskin/reports/<slug>-content-gate.md
node stardust/scripts/reskin/slot-coverage.mjs \
--model stardust/reskin/content-model/<slug>/content-model.json \
--rendered stardust/reskin/pages/<slug>.html \
--report stardust/reskin/reports/<slug>-slot-coverage.md
dom-equality.mjs (vendored + adapted from github.com/aemcoder/skills,
Apache-2.0) gates on whitespace-normalized visible-text byte
equality and the ordered, URL-normalized visible-image set;
element count and tag sequence are reported but informational.
slot-coverage.mjs additionally proves every model slot present
(slot text substring, every CTA as a (text, absolute-href) pair,
every image) and asserts metadata carry-over field by field.
(b) DESIGN-ADOPTION GATE:
node stardust/scripts/reskin/donor-probe.mjs \
--tokens stardust/reskin/donor-tokens.json \
--rendered stardust/reskin/pages/<slug>.html \
--report stardust/reskin/reports/<slug>-donor-probe.md
Computed-style assertions of donor token values on the rendered page —
bg/fg, font-family token string, display weight, button spec,
container width, band palette, section rhythm. Tolerances (exact for
colors/radius/family; ±2px button padding; ±20px container; ±16px
section rhythm) documented in reference/gates.md § Tolerances.
Then the side-by-side judgment: open the rendered page next to the
donor reference-page screenshots (canon-source/assets/screenshots/)
and judge adoption with your eyes — the probe proves tokens, the
eyeball proves the page reads as the donor. Record the verdict.
(c) Sanity — no horizontal overflow at 1440 and 360 (donor-probe
runs this automatically; --widths to override).
One fix iteration per gate family — one for the content gate,
one for design-adoption/sanity. On a failure in a family, fix once,
re-run all gates (a fix for one gate can break another). Residual
failures after a family's iteration is spent are logged in
stardust/reskin/ledger.json under the page's residuals[] — never
silently absorbed, never looped on indefinitely. (Both field runs
support the budget: the validation experiment spent one iteration on
360px chrome; the smoke run spent exactly one per family — a
content-side separator emission fix and a sanity-side 360px chrome
fix. A single shared iteration would have forced a false residual;
per-family keeps the loop bounded without absorbing real failures.)
Phase 6 — HANDOFF
Reskin owns the archetype pages; scale and shipping are the existing pipeline, unchanged:
- Scale via the stardust
migrateskill (../migrate/SKILL.md) with the donor-pinned target (thestate.json.designSourcestamp Phase 1 wrote is exactly whatdirect/migrateread for donor pinning; a bounded donor capture wrote no stamp — record the donor origin in the handoff instead,reference/donor-sources.md§ Two first-class token-sourcing paths). Same-type sibling pages render at the sibling tier against the gated archetype. Content rules are the ones reskin already enforces —../migrate/reference/content-preservation.mdis inherited wholesale. - Ship via the stardust
deployandrolloutskills, unchanged. Blocks written for donor modules obey the Experience Workspace editability contract (deploy SKILL.md § 8, EW1–EW10) and pass the EW gate (block-roundtrip --ew) before they are done.
Reskin writes its own state under stardust/reskin/ — ledger.json
holds per-page status (captured → mapped → rendered → gated), gate
results, and residuals. It makes no changes to the core state
machine (../stardust/reference/state-machine.md); the only
state.json touch is the designSource stamp that extract itself
writes in Phase 1.
What reskin never does
- No content rewriting or summarizing. Not headlines, not body, not CTA labels, not casing-by-editing. Faithful ≠ clean: source garbage is carried verbatim and flagged, not fixed.
- No invented sections. Every rendered slot traces to a
content-model slot; every module traces to
donor-modules.mdor an explicit new-module entry inmapping.md. - No donor-token drift. No colors, fonts, radii, or spacing
outside
donor-tokens.json. Licensed donor fonts are adopted as the family token string with local fallback, never rebundled. - No silent slot drops. Anything excluded from the gate scope is a
declared chrome delta or a ledger normalization — declared in
mapping.md, executable in the normalize module.
Stop conditions
- Donor capture fails (bot-block past the headed fallback, prototypes dir unservable) — surface extract's error verbatim.
- Mapping ratio < 80% — the donor vocabulary can't carry this content. Ask: widen the donor capture (more donor pages → more modules) or accept a larger explicit new-module list.
- Scope undecidable — the content page's visible content cannot be covered by any selector set (heavy shadow-DOM, iframe-embedded content). Surface with the screenshot evidence.
- Figma donor requested — future scope; exact message in § Inputs.
Outputs
| Path | Purpose |
|---|---|
stardust/canon-source/ | Donor capture + descriptive DESIGN.md/json (extract --design-source, unchanged) |
stardust/reskin/donor-tokens.json | Curated probe-able token sheet, pinned to one reference page |
stardust/reskin/donor-modules.md | Enumerated donor module vocabulary (M1..Mn) with screenshot evidence |
stardust/reskin/content-model/<slug>/content-model.json | Byte-oriented per-page content model (+ source screenshot) |
stardust/reskin/normalize/<slug>.mjs | Per-page executable normalization ledger (shared capture ↔ gates) |
stardust/reskin/mapping.md | The mapping brief: every slot → donor module, status, rationale |
stardust/reskin/renderers/<archetype>.mjs | Programmatic renderers (model in, HTML out) |
stardust/reskin/pages/<slug>.html | Rendered reskin pages |
stardust/reskin/reports/ | Per-page gate reports + screenshots |
stardust/reskin/ledger.json | Per-page status, gate results, residuals |
stardust/scripts/reskin/ | Project-local copies of the five skill scripts |
References
reference/donor-sources.md— the three donor types and their capture recipes; pin-one-reference-page;donor-tokens.jsonanddonor-modules.mdcontracts; the Figma future contract.reference/content-model.md— the capture contract: slot taxonomy, the ordered stream (the render surface), scope-discovery procedure, normalization ledger format, provenance.reference/mapping-brief.md— entry schema, the ≥80% gate, new-module composition rules, casing policy.reference/gates.md— both gates' commands, pass bars, tolerances, failure modes, residual logging.../extract/SKILL.md§ Cross-site brand sources — the--design-sourcedonor-capture path Phase 1 delegates to.../migrate/reference/content-preservation.md— the content rules reskin inherits (and tightens to byte level).../diff/SKILL.md— the pixel + structural probes; reuse them as a supplementary build-side check after Phase 6 deploy,--profile eds.