- name
- reskin
- description
- Rebuild an existing site's pages with byte-faithful content on a separately-defined donor design system — another live site, or local static HTML prototypes (Figma donors are contract-defined, not yet implemented). Content fidelity is gated byte-level (text, ordered images, SEO metadata); design application is flexible (content re-laid-out onto donor modules). Use when the user says "reskin my site with this design", "apply this design system to my content", "restyle my site to look like <other site>", "new design, same content", "rebrand my site using these prototypes", or names a content site plus a design donor. NOT for redesigning from intent (that's the stardust core extract/direct/prototype chain) and NOT for keeping the current design while migrating (that's the replica flow).
- license
- Apache-2.0
- compatibility
- Requires Node 22+, Playwright with Chromium resolvable from the project, playwright-cli on PATH, and the impeccable skill (github.com/pbakaus/impeccable) installed alongside stardust.
# 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 in `reference/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
class `figma-mcp` — is specced in
`skills/reskin/reference/donor-sources.md` and 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; a spec's `archetypes.json` names them; scale to siblings happens in Phase 6 via `migrate`).
- `--scope <selectors>` — optional. Pre-declare the content-root
scope for a single-page run (comma-separated, `!` suffix keeps a
scope whole; see `reference/content-model.md` § Scope declaration).
Without it, run the scope-discovery procedure per page.
## Setup
1. Run the master skill's setup (`../stardust/SKILL.md` § Setup):
impeccable dep check, context loader, state read. **Flow.** Stamp
`state.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.
2. **Playwright import-resolvability probe** — same contract as
`../extract/SKILL.md` § Setup:
`node -e "import('playwright').then(()=>process.exit(0))"` from the
project root; on failure
`npm i -D playwright --no-save --legacy-peer-deps`. Re-run the
probe at the start of every phase that renders — a `--no-save`
install is pruned by any later real `npm i`.
3. **Copy the scripts into the project.** ESM resolves
`import('playwright')` from the *script's* directory and the plugin
tree ships no `node_modules`. Copy `skills/reskin/scripts/*` (all
five files — `capture-content.mjs` and `dom-equality.mjs` import
`source-normalize.mjs` as a sibling) byte-identical to
`stardust/scripts/reskin/`, **and** `skills/diff/scripts/
live-session.mjs` to `stardust/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 `--help` or a local-file `--rendered` target. 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.
4. **Origin collision** — if `stardust/state.json` records a different
`site.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 in `stardust/canon-source/`: the
donor's `pages/`, `assets/` (screenshots included),
`_brand-extraction.json`, `_crawl-log.json`, and a descriptive
`DESIGN.md` + `DESIGN.json`, and stamps
`state.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-source` capture path against the
localhost origin. Record the real provenance (localhost serve of
`<path>`) in `canon-source/_crawl-log.json`. Recipe details —
page listing, index-less directories, port hygiene — in
`reference/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 from
`canon-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-source` run) there is no `DESIGN.json`,
`_brand-extraction.json`, or `designSource` stamp 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:
```bash
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:
1. **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 `#content`
root, and byte equality would have "passed" against the incomplete
reference. Never trust `main` or `#content` blind. 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.
2. **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 module `stardust/reskin/normalize/<slug>.mjs` that 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 in
`reference/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 `mapped`** onto named donor modules. Below
that, the donor vocabulary doesn't cover this content — stop and
surface (see § Stop conditions).
- **Every `new-module` entry 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 `innerText`
reflects `text-transform`, so captured text is *rendered-case*).
The underlying text stays byte-faithful; casing is applied via CSS
`text-transform` only, never by editing the string. Policy details
in `reference/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.json` and 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), `sep` flags marking
zero-separator inline joins. The stream is **innerText-consistent
by construction** (its text nodes are sliced from the parent's
rendered `innerText` — the byte-gate basis), so emitting it
verbatim is safe. Emit nodes in stream order; emit
`sep: ""` neighbours with no whitespace between them (inline);
never reorder, and never reconstruct **order or separators** from
`visibleText` —
the first field run did exactly that (per-type arrays matched
greedily against `visibleText` as an oracle) and burned three
debug rounds on duplicate identical strings, both directions of
CTA/container nesting, and zero-separator inline `li` runs, all
of which the stream now captures structurally;
- renders `formControl` nodes 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's `innerText`, 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 in
`mapping.md`. Use the rendered-page conventions the Phase 5 probe
asserts against: content in `<main>`, `.container` for the measure,
`.btn` for 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:
```bash
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:**
```bash
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,
View on GitHub