stardust:migrate
Apply the target spec authored by direct, the visual canon
written by prototype --prep, and the brand-module catalog
extracted during prepare-migration to every page in the
inventory. Produces a self-contained, deployable static HTML site
under stardust/migrated/. Per-page, incremental, idempotent.
migrate is the final stardust phase. Output is platform-
agnostic HTML — downstream conversion (AEM EDS, a CMS, a
framework) is the job of a separate plugin that consumes
migrated/ plus DESIGN.json plus the per-page _meta.json
sidecars.
Inputs
<slug> — optional positional. Migrate just this page. Without
it, migrate every page whose status is directed,
prototyped, or approved (and not stale).
--all — migrate every page including stale ones.
--force — re-migrate every page even when the idempotent
skip would skip them.
--require-approved — refuse to migrate any non-approved
page. Default behaviour migrates directed pages too (using
Path A′ or Path B per
reference/template-and-module-rendering.md); this flag flips
approval-gating on.
--strict-canon — refuse approvals that conflict with canon.
Default logs the deviation and continues. Useful for projects
where canon discipline matters more than per-template
flexibility.
--clean — delete assets previously bundled but no longer
referenced from stardust/migrated/assets/. Off by default
(migrate is additive). Implies --force: every page is
re-rendered so the run's bundledAssets Set is the complete
union of currently-referenced assets — otherwise --clean
would risk deleting assets still referenced by
idempotent-skipped pages. See
reference/asset-bundling.md § Stale asset cleanup.
--pin-timestamp <ISO8601> — pin the migrate-provenance
timestamp so re-runs without source changes produce byte-
identical HTML. Default re-uses the current wall clock, which
is fine for normal use; CI deployment fingerprinting may want
the pin.
The mobile-adapt audit, content-sourcing scan, and placeholder
refusal are all mandatory gates — there is no --skip-* or
--allow-* flag to bypass them. If a gate refuses a page, the
remediation is to fix the proposed file (re-prototype, edit
inline, or run an impeccable command) and re-invoke migrate.
Setup
Playwright re-probe (mandatory first step). --no-save playwright
installs from earlier phases are pruned by any later real npm i
(extract SKILL.md § Setup → --no-save installs are ephemeral). Before
any rendering step, probe
node -e "import('playwright').then(()=>process.exit(0))" from the
project root and re-install (npm i -D playwright --no-save --legacy-peer-deps) on failure.
Run the master skill's setup
(skills/stardust/SKILL.md § Setup). Flow guard. If
stardust/state.json exists without flow and the ask is a
migration (a URL plus "migrate" / "to EDS" / "re-platform"), do not
run: print the two-flow table from the master skill § Two migration
flows and hand back to its routing — the flow is chosen and stamped
there before any sub-skill runs
(skills/stardust/reference/state-machine.md § Flow keys). Under
hands-off the master's default applies (keep-design phrase →
replica, otherwise redesign), recorded in direction.md.
(Recorded: migrate <url> as the first command of two same-design
migrations led to a hand-built compiler tuned by eye instead of the
replica gate.)
Verify stardust/state.json exists with at least one
directed page.
Verify project-root DESIGN.md and DESIGN.json exist with
DESIGN.json.extensions.canon populated.
Verify stardust/canon/ exists with at least
header.html, footer.html, canon.css.
Canon auto-bootstrap (when steps 3–4 find no canon). The
documented prototype → migrate → deploy happy path does not
run prepare-migration, so a first migrate legitimately arrives
with no canon (observed on 4 of 6 e2e sites, where every run had
to derive canon by hand to proceed — this is the fix). When
canon is absent and at least one approved prototype exists,
do not stop: run the canon write-back inline from the first
approved prototype (the canon-author, default home) per
../prototype/reference/canon-extraction.md § Five-step
procedure — extract header.html / footer.html / canon.css
to stardust/canon/, pin tokens + compositional moves to
DESIGN.json.extensions.canon, and record
canon.source: "auto-bootstrap: <slug>". This is exactly what
prototype --prep does on first approval; migrate performs it
on demand so the core pipeline never dead-ends. Only stop and
recommend $stardust prepare-migration when canon is absent
and no approved prototype exists (there is nothing to derive
canon from). Under state.json.handsOff the bootstrap is
automatic and logged; interactively, surface it as a one-line
notice before proceeding.
Verify stardust/direction.md has an active (not pending)
direction.
Read state.json.pages[] and partition into:
- inScope: status
directed, prototyped, or
approved, stale: false (or --all / explicit
<slug>).
- skipped: everything else, with reason captured.
Validate provenance on every in-scope page. Call
validateProvenance(page) per
skills/stardust/reference/state-machine.md § Provenance
validation for every page in inScope. Abort with the
helper's error when any page lacks live-render evidence —
migrating a synthesized page record produces deployable HTML
that misrepresents the source site, the exact failure mode
that motivated the validator. Surface Provenance OK on N pages in the migrate-plan output before Phase 1.
Mobile-adapt audit on every Path A / Path A′ source. For
every page whose render branch consumes a proposed or
archetype HTML file (Path A, Path A′ per
reference/template-and-module-rendering.md § Render path
selection), run the audit per skills/prototype/SKILL.md
§ Mobile-adapt audit:
<meta name="viewport" content="width=device-width, ...">
present, width not pinned to a fixed pixel value.
- At least one
@media (max-width: ...) rule.
- At least one mobile-targeted breakpoint at ≤ 640px.
Refuse pages that fail — the audit is mandatory; there is no
skip flag. The user fixes the proposed file (re-prototype or
chat-driven impeccable command) and re-invokes migrate.
Record the audit result per page in the migrate report and
in the post-render _meta.json#audit.adapt sidecar. Path B
(unique-renders) skips the audit because adapt hasn't run
on those pages — a Path B page that needs mobile coverage
gets it via $impeccable adapt invoked separately by the
user. Surface this distinction in the report so it's not
read as a silent skip.
Procedure
Phase 1 — Plan
Dynamic-surface precondition (safety net). If
stardust/dynamic-features.md is missing, the hand-run flow
(extract → direct → prototype → migrate) never passed a pre-import
gate: run the stardust dynamics skill Phases 1–3 now (extract --dynamics
for reach if needed, detector on the archetypes, triage draft, curate)
before rendering any page. Never import a site as static without a
decision per dynamic row. Per page, rows of the inventory that touch it
become contentDeviations[] kind: "dynamic-dependency" entries
(reference/content-preservation.md § Dynamic dependencies).
Gated-archetype precondition (flow: replica). Before rendering any
sibling-tier page, read stardust/replica/progress.json: the page
type's archetype must have a gate result at every configured breakpoint
that is pass: true, or over the bar with every residual carrying a
cause (../replica/reference/source-fidelity-gate.md § Residual
logging format — a documented residual is a pass with an asterisk). An
archetype never gated, or over the bar with no residual entries, blocks
its page type: report the archetype slug and $stardust replica <archetype>, and render nothing for that type. The same rule guards
rollout Setup; the published-origin re-gate is unchanged. Thresholds
are the gate's.
Print the plan and wait for confirmation when the scope is large:
migrate plan
============
In scope: 127 pages
Path A (approved) 6 pages: home, news/post-housing-summit, news, ...
Path A' (template-applied) 118 pages: 84 article, 5 listing, 11 program, 2 form, 16 static
Path B (unique) 3 pages: 404, search, faq
Skipped: 0 stale, 0 unscoped
DESIGN.md sha: 1a2b3c4
DESIGN.json sha: 5d6e7f8
Canon shas: header:7g8h9i footer:9i0j1k css:1k2l3m
Output: stardust/migrated/ + per-page _meta.json sidecars
Idempotent skip: enabled (run with --force to override)
Reply "go" to proceed.
For 1-3 pages or <slug> invocation, skip the confirmation.
Phase 2 — Per-page render
For each page in scope, follow
reference/migration-procedure.md and
reference/template-and-module-rendering.md:
- Idempotent skip check first (sha-compare across
designMd, designJson, sourceCurrent, sourceProposed,
canonShas, archetypeSource).
- Placeholder gate (Path A and Path A′ only — pages with a
proposed file or archetype). Refuse when
[data-placeholder]
elements or non-empty _provenance.unsourcedContent[] are
present — the user fills the missing content in the proposed
file before re-invoking migrate. No bypass flag.
- Render branch selection (LLM judgment per T&M §
Render path selection): A / A′ / B. Declare the page's
fidelityTier from the branch — A → archetype (craft-gated),
A′ → sibling (canon-fork, the cheap default for breadth — variance-probed
once per template before cloning, reference/fidelity-tiers.md § Sibling
variance probe; deltas become variant classes, never per-page forks),
B/bodyless → thin — per reference/fidelity-tiers.md. Record
fidelityTier, archetypeSource, and gatesPassed[] in
_meta.json so coverage shows what was craft-gated vs cloned.
- Render per the chosen branch's procedure in T&M.
- Canon application — chrome injection, canon.css
injection, deviation logging.
- Module rendering — render module instances via
stardust/canon/modules/<id>.html; bespoke slots logged
with data-bespoke.
- Apply content-preservation rules per
reference/content-preservation.md. Internal-link rewriting
always emits migrated-tree paths; missing slugs flagged
broken.
- Content-count acceptance per
reference/fidelity-tiers.md § Content-count acceptance:
compare role-classified node counts (headings, body/list
nodes, CTAs, images) between the captured source page JSON
and the rendered result. A count drop in any class not
covered by a logged contentDeviations[] entry fails the
page — dropped-content importer bugs must surface here,
while the importer is still cheap to fix, not at a
downstream fidelity gate. Record the pass in
_meta.json#gatesPassed[] as "content-count".
- Compose
<head> metadata per
reference/metadata-and-jsonld.md (five categories;
page-type-driven JSON-LD).
- Validate per T&M § Validation contracts. Strict contracts
refuse the page; soft contracts log and continue.
- Compute output path per migration-procedure.md
§ Output path mapping.
- Asset bundling. Scan the final HTML for asset references
(six detection shapes per
reference/asset-bundling.md § Detection), copy each unique
referenced subpath from stardust/current/assets/<subpath> to
stardust/migrated/assets/<subpath> (preserving subdir
structure), then rewrite every reference to the root-relative
form /assets/<subpath>. Cross-page dedup uses a
module-level Set seeded from
state.json.migrate.bundledAssets[]. Missing source assets
warn-and-skip per § Edge cases; the bundle stays internally
consistent.
- Media reconciliation. For every image not bundled to
same-origin (reused source-CDN URLs under Mode A image-reuse),
decide optimize/keep/rewrite/omit per
reference/media-reconciliation.md. Cross-origin <img> kept
as source URLs must skip createOptimizedPicture (it drops
the ?v= key and corrupts the rendition); broken URLs are
repaired (missing ?-delimiter, wrong host) or omitted, never
shipped as about:error. rollout re-runs the authoritative
network resolve at delivery (media-reconcile.mjs).
- Cinematic sibling (when
<slug>-cinematic.html exists).
Migrate consumes the STATIC prototype only — the cinematic layer
is never merged. Copy the motion assets (lenis.min.js,
lenis.min.css) from stardust/prototypes/ to
stardust/migrated/assets/motion/ (idempotent) for downstream
consumers (deploy/rollout decide whether to wire them), and
record cinematic-variant-not-consumed in the page's
_meta.json#migrationDecisions[].
- Write the migrated
index.html and the _meta.json
sidecar in the same directory. Provenance block as first
child of <head>. Record assetsBundled (count of unique
asset refs on this page) in _meta.json.
Phase 3 — Sitewide assets and bundle finalisation
Per-page asset bundling already happened in Phase 2 (every
referenced media subpath is on disk under
stardust/migrated/assets/). Phase 3 fills in the sitewide
assets that no individual page references explicitly:
Copy stardust/current/assets/logo.<ext> to
stardust/migrated/assets/logo.<ext> (only if missing or
stale). Record under state.json.migrate.bundledAssets[].
Verify favicon variants and font files were generated by
prepare-migration Phase 4. If absent, log a warning and
continue (the migrated site renders without them, just
missing some platform-specific affordances).
Add stardust/migrated/robots.txt and sitemap.xml
derived from the migrated page inventory per
reference/metadata-and-jsonld.md § Sitemap entry.
If --clean was passed, compute
stale = priorBundle.filter(p => !bundledAssets.has(p))
from state.json.migrate.bundledAssets[] and remove each
stale subpath from stardust/migrated/assets/. Record the
deletions under state.json.migrate.cleanedAssets[]. Per
reference/asset-bundling.md § Stale asset cleanup.
Verify portability. The bundle must work via file://,
at a webserver root, and at any subpath — "one shape, works
everywhere". Run every audit; any non-empty grep output or
non-zero fixture exit fails the run with the cited error
message:
# No source-tree escapes
find stardust/migrated/ -type f -name '*.html' -exec grep -l '\.\./current/' {} +
# Error: "asset still points outside the migrated tree; rewrite via the
# asset-bundling pass per reference/asset-bundling.md § Detection"
# No absolute internal references in attribute values (404 on file:// and subpath)
grep -rE '(href|src)="/[^/]' stardust/migrated/ --include='*.html'
# Error: "absolute href `/beers/` will 404 on file:// and on subpath hosts;
# rewrite via the page map per migration-procedure.md § Reference shape"
# No absolute internal references in url() (inline style, <style> blocks, CSS)
grep -rE 'url\(\s*["''']?\s*/[^/]' stardust/migrated/ --include='*.html' --include='*.css'
# Error: "absolute url(/...) reference will 404 on file:// and on subpath hosts;
# rewrite via the asset-bundling pass per asset-bundling.md § Rewrite"
# No directory-only nav (doesn't resolve on file://). Pattern accepts
# only relative or root-absolute hrefs (./, ../, /, or bare segment)
# so external URLs like https://google.com/ aren't false-flagged.
grep -rE 'href="(\.{0,2}/|[a-zA-Z0-9_-])[^:"#?]*/"' stardust/migrated/ --include='*.html'
# Error: "directory-only href `./beers/` won't resolve on file://;
# append the explicit index.html (or the source URL's .html leaf)
# per § Reference shape"
# pageMap consistency — every internal href appears as an outputPath
node skills/migrate/fixtures/pagemap-audit.mjs stardust/migrated/ stardust/state.json
# Error: "internal href has no pageMap entry; link rewriting bypassed the
# page map per § Page map (build once, use everywhere)"
# Headless file:// round-trip — the test that proves zip-and-deploy works
node skills/migrate/fixtures/file-protocol-audit.mjs stardust/migrated/
# Error: "<offending file> linked <ref> that 404s under file://; see the
# Playwright network log printed above"
The audits are mandatory — there is no skip flag. The contract
is "self-contained, zip-and-deploy" and these audits are the
verifiers that back the claim.
Asset migration is idempotent — files are content-hashed and
copied only when missing; per-page bundling deduplicates across
the run.
Phase 4 — State and report
Update state.json:
- For each successfully migrated page:
status advances to
migrated, append a history entry, clear any stale flag,
set migratedPath.
- For pages skipped via idempotent skip: leave state
unchanged.
- For pages that failed validation: leave state unchanged, log
the failure in
state.json.lastRun.failures[].
- Write the top-level
migrate block per
skills/stardust/reference/migrate-output-format.md
§ State.json contract: selfContained: true, outputDir,
totalAssetsBundled, bundledAssets[], per-page
assetsBundled counts, missingAssets[], cleanedAssets[].
This is the forward-compat signal downstream consumers test
for.
Print the run summary:
migrate complete
================
122 migrated home, about, news/post-housing-summit, ...
3 unchanged about, programs/shelter, news/post-old (idempotent skip)
2 failed contact (validation: required slot missing),
legal/privacy (validation: color-reservation violated)
0 stale skipped
Render branches:
Path A 6 approved-from-prototype
Path A' 116 template-applied (84 article, 5 listing, 11 program, 2 form, 14 static)
Path B 3 unique-render (404, search, faq)
Pages with non-trivial decisions: 12
about canon-deviation: footer carries financials disclaimer
donate template-adapted: amount-pills slot moved above headline
...
Broken internal links: 5
/events referenced by 2 pages; not in inventory
/press referenced by 1 page; not in inventory
...
Bespoke slots crossing promotion threshold: 1
hotline-211: "state" (3 instances) — consider `$stardust prepare-migration --refine-module`
Missing assets: 2
generated/orphan-1.jpg referenced by 1 page (home)
generated/orphan-2.jpg referenced by 2 pages (about, contact)
(Re-extract or accept the gap — bundle is deployable; refs 404 at view time.)
Output: stardust/migrated/ (122 pages, 47 bundled assets, 4.2 MB) — self-contained, zip-and-deploy
Next:
- Review: open stardust/migrated/index.html in a browser
- Audit: $impeccable critique stardust/migrated/
- Deploy: cd stardust/migrated && zip -r ../site.zip .
upload the zip to any static host that serves at the host root
- Refine: edit DESIGN.md or canon files, then re-run $stardust migrate
Outputs
| Path |
Purpose |
stardust/migrated/<source-url-path> |
Migrated page. Output path mirrors the source URL literally (see reference/migration-procedure.md § Output path mapping). The bundle is zip-and-deploy: drop on any static host at any path, or open index.html directly via file://. Every internal reference is relative to the page that emits it; nav targets carry an explicit index.html (or the source URL's literal filename) so file:// resolves without a server. |
| _meta.json sidecar |
Lives next to each migrated page. For <dir>/index.html the sidecar is <dir>/_meta.json; for <dir>/<name>.html the sidecar is <dir>/<name>._meta.json so multiple .html siblings don't collide. Per reference/migration-procedure.md § _meta.json sidecar. |
stardust/migrated/index.html |
The home page (special case). |
stardust/migrated/_meta.json |
Home sidecar. |
stardust/migrated/assets/logo.<ext> |
Brand logo (sitewide). |
stardust/migrated/assets/<subpath> |
Every asset referenced by any migrated page, bundled. Source subdir structure preserved verbatim. |
stardust/migrated/assets/favicon.<ext> + variants |
Favicon and apple-touch-icon, manifest icons. |
stardust/migrated/assets/fonts/... |
Downloaded font files (from canon @font-face URLs). |
stardust/migrated/robots.txt |
Minimal robots.txt. |
stardust/migrated/sitemap.xml |
Sitemap derived from migrated inventory + page types. |
stardust/state.json |
Updated with migrated status, history, and the migrate block (selfContained: true, asset counts). |
Idempotent and incremental
The whole pipeline is built around two properties:
- Idempotent. Re-running
$stardust migrate with no
changes produces zero file writes. Every page is sha-compared
across designMd, designJson, sourceCurrent, sourceProposed
(Path A), canonShas, archetypeSource (Path A′) — and skipped
if all match.
- Incremental. Migrate 5 pages today, 20 pages tomorrow,
fix one page's content next week — the migrated tree is
always the union of every successful migration to date.
These properties hold even when DESIGN.md, canon, or modules are
edited mid-run: the edit changes the relevant sha, so the next
migrate run re-renders every affected page (canon and DESIGN.md
edits typically affect every page).
Stale handling
When direction.md, canon, or the module catalog changes after
some pages have been migrated:
- Affected pages are flagged
stale: true per
skills/stardust/reference/state-machine.md § Stale flagging.
Stale-flagging is content-aware in all three trigger cases.
$stardust migrate (no flags) skips stale pages and reports
the count.
$stardust migrate --all re-migrates each stale page,
clearing the flag on success.
$stardust migrate <slug> always operates on the named page,
stale or not.
The user decides whether stale pages should be refreshed —
direction/canon/module changes don't invalidate prior migrated
work, they just mark it as out-of-step.
Failure modes
- No directed pages. Recommend
$stardust direct (or
$stardust extract if no extracted state).
- No DESIGN.md or DESIGN.json. Recommend
$stardust direct.
- No canon, but an approved prototype exists. Do NOT stop —
auto-bootstrap canon from the canon-author inline (Setup step 4).
- No canon and no approved prototype. Recommend
$stardust prepare-migration (or approve a prototype first).
- Pending direction. Refuse; user must resolve direction
first.
- Validation failure on a single page. Skip that page,
continue, log the failure under
state.json.lastRun.failures[]. Do not abort the whole run.
- Asset copy failure. Continue the run; record the missing
asset in the page's
migrationDecisions[] with
kind: "asset-missing". The migrated <img src> keeps the
original absolute URL as a fallback.
- Output path collision. Two slugs mapping to the same
output path. Refuse to write the second one and surface to
the user — manual slug rename needed.
- Placeholder content in proposed/archetype file. Refuse
to ship a page whose source contains
[data-placeholder]
elements. Surface the unsourced list and recommend sourcing
real content (re-prototype, or edit the proposed file
directly). There is no bypass flag — shipping placeholders to
a public site is the failure mode this gate exists to prevent.
- Color reservation violated. Refuse the page; surface to
user with the offending color and the reserved-for context.
- Brand-faithful inversion conflict. A hard rule declared
inverted in
extensions.divergence.brand_faithful_inversions[] is lifted
from validation per T&M § Brand-faithful inversion handling.
Emit a one-line note in the run summary acknowledging the
lift.
What migrate does NOT do
- Critique or audit the migrated output. Run
$impeccable critique stardust/migrated/ after migration if
you want a quality assessment.
- Deploy. Stardust does not push, upload, or modify origin.
- Generate AEM EDS, a CMS payload, or framework components. The
output is platform-agnostic static HTML; downstream conversion
is a separate plugin's job.
- Re-fetch the live site. Offline after extract Phase 1.
- Run any iteration loop. Iteration belongs to
prototype;
migrate consumes the result.
References
reference/migration-procedure.md — per-page render procedure,
output path mapping, validation, provenance shape, idempotent
skip, sidecar schema.
reference/template-and-module-rendering.md — three render
branches in detail, slot injection, deviation policy,
validation contracts.
reference/metadata-and-jsonld.md — head composition, JSON-LD
per page-type, canonical strategy.
reference/content-preservation.md — what's kept,
transformed, dropped; internal-link rewriting; asset path
rewriting; form handling.
reference/asset-bundling.md — detection / copy / rewrite
contract for the per-page asset-bundling phase.
skills/stardust/reference/migrate-output-format.md — the
self-contained-bundle contract downstream consumers can rely
on (asset reference shape, directory layout,
state.json.migrate block).
skills/stardust/reference/token-contract.md — :root block
refreshed from DESIGN.md on every render.
skills/stardust/reference/data-attributes.md — structural
attributes including data-template, data-module,
data-slot, data-canon, data-deviation, data-bespoke,
data-broken-link.
skills/stardust/reference/state-machine.md — page lifecycle,
page typing, stale-flagging cascade.
skills/stardust/reference/artifact-map.md — provenance shape
for migrated artifacts; canon files; sidecar shape.
skills/prototype/reference/canon-extraction.md — how canon
is built (input to migrate).
skills/prepare-migration/SKILL.md — the cascade that
produces every input migrate consumes.
1---2name: migrate-23description: Apply DESIGN, canon, and modules to every page in the inventory, producing a deployable static HTML site. Use to migrate or render the whole captured site into the redesigned static tree ("migrate the pages", "render the migrated site", "apply the design to all pages", "build the deployable site", "convert the approved prototype into the full site") — the page-rendering step between prototype and deploy/rollout. Three render branches (approved page, template-applied sibling, unique render), with a declared fidelity tier per page. Per-page, incremental, idempotent, content-preserving by default.4license: Apache-2.05---67# stardust:migrate89Apply the target spec authored by `direct`, the visual canon10written by `prototype --prep`, and the brand-module catalog11extracted during `prepare-migration` to every page in the12inventory. Produces a self-contained, deployable static HTML site13under `stardust/migrated/`. Per-page, incremental, idempotent.1415`migrate` is the final stardust phase. Output is platform-16agnostic HTML — downstream conversion (AEM EDS, a CMS, a17framework) is the job of a separate plugin that consumes18`migrated/` plus `DESIGN.json` plus the per-page `_meta.json`19sidecars.2021## Inputs2223- `<slug>` — optional positional. Migrate just this page. Without24 it, migrate every page whose status is `directed`,25 `prototyped`, or `approved` (and not `stale`).26- `--all` — migrate every page including stale ones.27- `--force` — re-migrate every page even when the idempotent28 skip would skip them.29- `--require-approved` — refuse to migrate any non-`approved`30 page. Default behaviour migrates `directed` pages too (using31 Path A′ or Path B per32 `reference/template-and-module-rendering.md`); this flag flips33 approval-gating on.34- `--strict-canon` — refuse approvals that conflict with canon.35 Default logs the deviation and continues. Useful for projects36 where canon discipline matters more than per-template37 flexibility.38- `--clean` — delete assets previously bundled but no longer39 referenced from `stardust/migrated/assets/`. Off by default40 (migrate is additive). **Implies `--force`**: every page is41 re-rendered so the run's `bundledAssets` Set is the complete42 union of currently-referenced assets — otherwise `--clean`43 would risk deleting assets still referenced by44 idempotent-skipped pages. See45 `reference/asset-bundling.md` § Stale asset cleanup.46- `--pin-timestamp <ISO8601>` — pin the migrate-provenance47 timestamp so re-runs without source changes produce byte-48 identical HTML. Default re-uses the current wall clock, which49 is fine for normal use; CI deployment fingerprinting may want50 the pin.5152The mobile-adapt audit, content-sourcing scan, and placeholder53refusal are all mandatory gates — there is no `--skip-*` or54`--allow-*` flag to bypass them. If a gate refuses a page, the55remediation is to fix the proposed file (re-prototype, edit56inline, or run an impeccable command) and re-invoke migrate.5758## Setup59600. **Playwright re-probe (mandatory first step).** `--no-save` playwright61 installs from earlier phases are pruned by any later real `npm i`62 (extract SKILL.md § Setup → `--no-save` installs are ephemeral). Before63 any rendering step, probe64 `node -e "import('playwright').then(()=>process.exit(0))"` from the65 project root and re-install (`npm i -D playwright --no-save66 --legacy-peer-deps`) on failure.671. Run the master skill's setup68 (`skills/stardust/SKILL.md` § Setup). **Flow guard.** If69 `stardust/state.json` exists without `flow` and the ask is a70 migration (a URL plus "migrate" / "to EDS" / "re-platform"), do not71 run: print the two-flow table from the master skill § Two migration72 flows and hand back to its routing — the flow is chosen and stamped73 there before any sub-skill runs74 (`skills/stardust/reference/state-machine.md` § Flow keys). Under75 hands-off the master's default applies (keep-design phrase →76 `replica`, otherwise `redesign`), recorded in `direction.md`.77 (Recorded: `migrate <url>` as the first command of two same-design78 migrations led to a hand-built compiler tuned by eye instead of the79 replica gate.)802. Verify `stardust/state.json` exists with at least one81 `directed` page.823. Verify project-root `DESIGN.md` and `DESIGN.json` exist with83 `DESIGN.json.extensions.canon` populated.844. Verify `stardust/canon/` exists with at least85 `header.html`, `footer.html`, `canon.css`.8687 **Canon auto-bootstrap (when steps 3–4 find no canon).** The88 documented `prototype → migrate → deploy` happy path does not89 run `prepare-migration`, so a first migrate legitimately arrives90 with no canon (observed on 4 of 6 e2e sites, where every run had91 to derive canon by hand to proceed — this is the fix). When92 canon is absent **and** at least one `approved` prototype exists,93 do not stop: run the canon write-back inline from the first94 approved prototype (the canon-author, default `home`) per95 `../prototype/reference/canon-extraction.md` § Five-step96 procedure — extract `header.html` / `footer.html` / `canon.css`97 to `stardust/canon/`, pin tokens + compositional moves to98 `DESIGN.json.extensions.canon`, and record99 `canon.source: "auto-bootstrap: <slug>"`. This is exactly what100 `prototype --prep` does on first approval; migrate performs it101 on demand so the core pipeline never dead-ends. Only stop and102 recommend `$stardust prepare-migration` when canon is absent103 **and** no approved prototype exists (there is nothing to derive104 canon from). Under `state.json.handsOff` the bootstrap is105 automatic and logged; interactively, surface it as a one-line106 notice before proceeding.1075. Verify `stardust/direction.md` has an active (not pending)108 direction.1096. Read `state.json.pages[]` and partition into:110 - **inScope**: status `directed`, `prototyped`, or111 `approved`, `stale: false` (or `--all` / explicit112 `<slug>`).113 - **skipped**: everything else, with reason captured.1147. **Validate provenance on every in-scope page.** Call115 `validateProvenance(page)` per116 `skills/stardust/reference/state-machine.md` § Provenance117 validation for every page in `inScope`. Abort with the118 helper's error when any page lacks live-render evidence —119 migrating a synthesized page record produces deployable HTML120 that misrepresents the source site, the exact failure mode121 that motivated the validator. Surface `Provenance OK on N122 pages` in the migrate-plan output before Phase 1.1238. **Mobile-adapt audit on every Path A / Path A′ source.** For124 every page whose render branch consumes a proposed or125 archetype HTML file (Path A, Path A′ per126 `reference/template-and-module-rendering.md` § Render path127 selection), run the audit per `skills/prototype/SKILL.md`128 § Mobile-adapt audit:129130 - `<meta name="viewport" content="width=device-width, ...">`131 present, width not pinned to a fixed pixel value.132 - At least one `@media (max-width: ...)` rule.133 - At least one mobile-targeted breakpoint at ≤ 640px.134135 Refuse pages that fail — the audit is mandatory; there is no136 skip flag. The user fixes the proposed file (re-prototype or137 chat-driven impeccable command) and re-invokes migrate.138 Record the audit result per page in the migrate report and139 in the post-render `_meta.json#audit.adapt` sidecar. Path B140 (unique-renders) skips the audit because adapt hasn't run141 on those pages — a Path B page that needs mobile coverage142 gets it via `$impeccable adapt` invoked separately by the143 user. Surface this distinction in the report so it's not144 read as a silent skip.145146## Procedure147148### Phase 1 — Plan149150**Dynamic-surface precondition (safety net).** If151`stardust/dynamic-features.md` is missing, the hand-run flow152(`extract → direct → prototype → migrate`) never passed a pre-import153gate: run the stardust `dynamics` skill Phases 1–3 now (`extract --dynamics`154for reach if needed, detector on the archetypes, triage draft, curate)155before rendering any page. Never import a site as static without a156decision per dynamic row. Per page, rows of the inventory that touch it157become `contentDeviations[]` `kind: "dynamic-dependency"` entries158(`reference/content-preservation.md § Dynamic dependencies`).159160**Gated-archetype precondition (`flow: replica`).** Before rendering any161`sibling`-tier page, read `stardust/replica/progress.json`: the page162type's archetype must have a gate result at every configured breakpoint163that is `pass: true`, or over the bar with every residual carrying a164`cause` (`../replica/reference/source-fidelity-gate.md` § Residual165logging format — a documented residual is a pass with an asterisk). An166archetype never gated, or over the bar with no residual entries, blocks167its page type: report the archetype slug and `$stardust replica168<archetype>`, and render nothing for that type. The same rule guards169`rollout` Setup; the published-origin re-gate is unchanged. Thresholds170are the gate's.171172Print the plan and wait for confirmation when the scope is large:173174```175migrate plan176============177178In scope: 127 pages179 Path A (approved) 6 pages: home, news/post-housing-summit, news, ...180 Path A' (template-applied) 118 pages: 84 article, 5 listing, 11 program, 2 form, 16 static181 Path B (unique) 3 pages: 404, search, faq182183Skipped: 0 stale, 0 unscoped184185DESIGN.md sha: 1a2b3c4186DESIGN.json sha: 5d6e7f8187Canon shas: header:7g8h9i footer:9i0j1k css:1k2l3m188189Output: stardust/migrated/ + per-page _meta.json sidecars190Idempotent skip: enabled (run with --force to override)191192Reply "go" to proceed.193```194195For 1-3 pages or `<slug>` invocation, skip the confirmation.196197### Phase 2 — Per-page render198199For each page in scope, follow200`reference/migration-procedure.md` and201`reference/template-and-module-rendering.md`:202203- **Idempotent skip check** first (sha-compare across204 `designMd`, `designJson`, `sourceCurrent`, `sourceProposed`,205 `canonShas`, `archetypeSource`).206- **Placeholder gate** (Path A and Path A′ only — pages with a207 proposed file or archetype). Refuse when `[data-placeholder]`208 elements or non-empty `_provenance.unsourcedContent[]` are209 present — the user fills the missing content in the proposed210 file before re-invoking migrate. No bypass flag.211- **Render branch selection** (LLM judgment per T&M §212 Render path selection): A / A′ / B. **Declare the page's213 `fidelityTier`** from the branch — A → `archetype` (craft-gated),214 A′ → `sibling` (canon-fork, the cheap default for breadth — variance-probed215 once per template before cloning, `reference/fidelity-tiers.md` § Sibling216 variance probe; deltas become variant classes, never per-page forks),217 B/bodyless → `thin` — per `reference/fidelity-tiers.md`. Record218 `fidelityTier`, `archetypeSource`, and `gatesPassed[]` in219 `_meta.json` so coverage shows what was craft-gated vs cloned.220- **Render** per the chosen branch's procedure in T&M.221- **Canon application** — chrome injection, canon.css222 injection, deviation logging.223- **Module rendering** — render module instances via224 `stardust/canon/modules/<id>.html`; bespoke slots logged225 with `data-bespoke`.226- **Apply content-preservation rules** per227 `reference/content-preservation.md`. Internal-link rewriting228 always emits migrated-tree paths; missing slugs flagged229 broken.230- **Content-count acceptance** per231 `reference/fidelity-tiers.md` § Content-count acceptance:232 compare role-classified node counts (headings, body/list233 nodes, CTAs, images) between the captured source page JSON234 and the rendered result. A count drop in any class not235 covered by a logged `contentDeviations[]` entry fails the236 page — dropped-content importer bugs must surface here,237 while the importer is still cheap to fix, not at a238 downstream fidelity gate. Record the pass in239 `_meta.json#gatesPassed[]` as `"content-count"`.240- **Compose `<head>` metadata** per241 `reference/metadata-and-jsonld.md` (five categories;242 page-type-driven JSON-LD).243- **Validate** per T&M § Validation contracts. Strict contracts244 refuse the page; soft contracts log and continue.245- **Compute output path** per migration-procedure.md246 § Output path mapping.247- **Asset bundling.** Scan the final HTML for asset references248 (six detection shapes per249 `reference/asset-bundling.md` § Detection), copy each unique250 referenced subpath from `stardust/current/assets/<subpath>` to251 `stardust/migrated/assets/<subpath>` (preserving subdir252 structure), then rewrite every reference to the root-relative253 form `/assets/<subpath>`. Cross-page dedup uses a254 module-level Set seeded from255 `state.json.migrate.bundledAssets[]`. Missing source assets256 warn-and-skip per § Edge cases; the bundle stays internally257 consistent.258- **Media reconciliation.** For every image **not** bundled to259 same-origin (reused source-CDN URLs under Mode A image-reuse),260 decide optimize/keep/rewrite/omit per261 `reference/media-reconciliation.md`. Cross-origin `<img>` kept262 as source URLs must **skip `createOptimizedPicture`** (it drops263 the `?v=` key and corrupts the rendition); broken URLs are264 repaired (missing `?`-delimiter, wrong host) or omitted, never265 shipped as `about:error`. `rollout` re-runs the authoritative266 network resolve at delivery (`media-reconcile.mjs`).267- **Cinematic sibling (when `<slug>-cinematic.html` exists).**268 Migrate consumes the STATIC prototype only — the cinematic layer269 is never merged. Copy the motion assets (`lenis.min.js`,270 `lenis.min.css`) from `stardust/prototypes/` to271 `stardust/migrated/assets/motion/` (idempotent) for downstream272 consumers (deploy/rollout decide whether to wire them), and273 record `cinematic-variant-not-consumed` in the page's274 `_meta.json#migrationDecisions[]`.275- **Write** the migrated `index.html` and the `_meta.json`276 sidecar in the same directory. Provenance block as first277 child of `<head>`. Record `assetsBundled` (count of unique278 asset refs on this page) in `_meta.json`.279280### Phase 3 — Sitewide assets and bundle finalisation281282Per-page asset bundling already happened in Phase 2 (every283referenced media subpath is on disk under284`stardust/migrated/assets/`). Phase 3 fills in the **sitewide285assets** that no individual page references explicitly:2862871. Copy `stardust/current/assets/logo.<ext>` to288 `stardust/migrated/assets/logo.<ext>` (only if missing or289 stale). Record under `state.json.migrate.bundledAssets[]`.2902. Verify favicon variants and font files were generated by291 `prepare-migration` Phase 4. If absent, log a warning and292 continue (the migrated site renders without them, just293 missing some platform-specific affordances).2943. Add `stardust/migrated/robots.txt` and `sitemap.xml`295 derived from the migrated page inventory per296 `reference/metadata-and-jsonld.md` § Sitemap entry.2974. If `--clean` was passed, compute298 `stale = priorBundle.filter(p => !bundledAssets.has(p))`299 from `state.json.migrate.bundledAssets[]` and remove each300 stale subpath from `stardust/migrated/assets/`. Record the301 deletions under `state.json.migrate.cleanedAssets[]`. Per302 `reference/asset-bundling.md` § Stale asset cleanup.3035. Verify **portability**. The bundle must work via `file://`,304 at a webserver root, and at any subpath — "one shape, works305 everywhere". Run every audit; any non-empty grep output or306 non-zero fixture exit fails the run with the cited error307 message:308309 ```bash310 # No source-tree escapes311 find stardust/migrated/ -type f -name '*.html' -exec grep -l '\.\./current/' {} +312 # Error: "asset still points outside the migrated tree; rewrite via the313 # asset-bundling pass per reference/asset-bundling.md § Detection"314315 # No absolute internal references in attribute values (404 on file:// and subpath)316 grep -rE '(href|src)="/[^/]' stardust/migrated/ --include='*.html'317 # Error: "absolute href `/beers/` will 404 on file:// and on subpath hosts;318 # rewrite via the page map per migration-procedure.md § Reference shape"319320 # No absolute internal references in url() (inline style, <style> blocks, CSS)321 grep -rE 'url\(\s*["''']?\s*/[^/]' stardust/migrated/ --include='*.html' --include='*.css'322 # Error: "absolute url(/...) reference will 404 on file:// and on subpath hosts;323 # rewrite via the asset-bundling pass per asset-bundling.md § Rewrite"324325 # No directory-only nav (doesn't resolve on file://). Pattern accepts326 # only relative or root-absolute hrefs (./, ../, /, or bare segment)327 # so external URLs like https://google.com/ aren't false-flagged.328 grep -rE 'href="(\.{0,2}/|[a-zA-Z0-9_-])[^:"#?]*/"' stardust/migrated/ --include='*.html'329 # Error: "directory-only href `./beers/` won't resolve on file://;330 # append the explicit index.html (or the source URL's .html leaf)331 # per § Reference shape"332333 # pageMap consistency — every internal href appears as an outputPath334 node skills/migrate/fixtures/pagemap-audit.mjs stardust/migrated/ stardust/state.json335 # Error: "internal href has no pageMap entry; link rewriting bypassed the336 # page map per § Page map (build once, use everywhere)"337338 # Headless file:// round-trip — the test that proves zip-and-deploy works339 node skills/migrate/fixtures/file-protocol-audit.mjs stardust/migrated/340 # Error: "<offending file> linked <ref> that 404s under file://; see the341 # Playwright network log printed above"342 ```343344 The audits are mandatory — there is no skip flag. The contract345 is "self-contained, zip-and-deploy" and these audits are the346 verifiers that back the claim.347348Asset migration is idempotent — files are content-hashed and349copied only when missing; per-page bundling deduplicates across350the run.351352### Phase 4 — State and report353354Update `state.json`:355356- For each successfully migrated page: `status` advances to357 `migrated`, append a history entry, clear any `stale` flag,358 set `migratedPath`.359- For pages skipped via idempotent skip: leave state360 unchanged.361- For pages that failed validation: leave state unchanged, log362 the failure in `state.json.lastRun.failures[]`.363- Write the top-level `migrate` block per364 `skills/stardust/reference/migrate-output-format.md`365 § State.json contract: `selfContained: true`, `outputDir`,366 `totalAssetsBundled`, `bundledAssets[]`, per-page367 `assetsBundled` counts, `missingAssets[]`, `cleanedAssets[]`.368 This is the forward-compat signal downstream consumers test369 for.370371Print the run summary:372373```374migrate complete375================376377 122 migrated home, about, news/post-housing-summit, ...378 3 unchanged about, programs/shelter, news/post-old (idempotent skip)379 2 failed contact (validation: required slot missing),380 legal/privacy (validation: color-reservation violated)381 0 stale skipped382383Render branches:384 Path A 6 approved-from-prototype385 Path A' 116 template-applied (84 article, 5 listing, 11 program, 2 form, 14 static)386 Path B 3 unique-render (404, search, faq)387388Pages with non-trivial decisions: 12389 about canon-deviation: footer carries financials disclaimer390 donate template-adapted: amount-pills slot moved above headline391 ...392393Broken internal links: 5394 /events referenced by 2 pages; not in inventory395 /press referenced by 1 page; not in inventory396 ...397398Bespoke slots crossing promotion threshold: 1399 hotline-211: "state" (3 instances) — consider `$stardust prepare-migration --refine-module`400401Missing assets: 2402 generated/orphan-1.jpg referenced by 1 page (home)403 generated/orphan-2.jpg referenced by 2 pages (about, contact)404 (Re-extract or accept the gap — bundle is deployable; refs 404 at view time.)405406Output: stardust/migrated/ (122 pages, 47 bundled assets, 4.2 MB) — self-contained, zip-and-deploy407408Next:409 - Review: open stardust/migrated/index.html in a browser410 - Audit: $impeccable critique stardust/migrated/411 - Deploy: cd stardust/migrated && zip -r ../site.zip .412 upload the zip to any static host that serves at the host root413 - Refine: edit DESIGN.md or canon files, then re-run $stardust migrate414```415416## Outputs417418| Path | Purpose |419|---------------------------------------------------|--------------------------------------------------------|420| `stardust/migrated/<source-url-path>` | Migrated page. Output path mirrors the source URL literally (see `reference/migration-procedure.md` § Output path mapping). The bundle is **zip-and-deploy**: drop on any static host at any path, or open `index.html` directly via `file://`. Every internal reference is relative to the page that emits it; nav targets carry an explicit `index.html` (or the source URL's literal filename) so file:// resolves without a server. |421| _meta.json sidecar | Lives next to each migrated page. For `<dir>/index.html` the sidecar is `<dir>/_meta.json`; for `<dir>/<name>.html` the sidecar is `<dir>/<name>._meta.json` so multiple `.html` siblings don't collide. Per `reference/migration-procedure.md` § `_meta.json` sidecar. |422| `stardust/migrated/index.html` | The home page (special case). |423| `stardust/migrated/_meta.json` | Home sidecar. |424| `stardust/migrated/assets/logo.<ext>` | Brand logo (sitewide). |425| `stardust/migrated/assets/<subpath>` | Every asset referenced by any migrated page, bundled. Source subdir structure preserved verbatim. |426| `stardust/migrated/assets/favicon.<ext>` + variants| Favicon and apple-touch-icon, manifest icons. |427| `stardust/migrated/assets/fonts/...` | Downloaded font files (from canon @font-face URLs). |428| `stardust/migrated/robots.txt` | Minimal robots.txt. |429| `stardust/migrated/sitemap.xml` | Sitemap derived from migrated inventory + page types. |430| `stardust/state.json` | Updated with `migrated` status, history, and the `migrate` block (`selfContained: true`, asset counts). |431432## Idempotent and incremental433434The whole pipeline is built around two properties:435436- **Idempotent.** Re-running `$stardust migrate` with no437 changes produces zero file writes. Every page is sha-compared438 across designMd, designJson, sourceCurrent, sourceProposed439 (Path A), canonShas, archetypeSource (Path A′) — and skipped440 if all match.441- **Incremental.** Migrate 5 pages today, 20 pages tomorrow,442 fix one page's content next week — the migrated tree is443 always the union of every successful migration to date.444445These properties hold even when DESIGN.md, canon, or modules are446edited mid-run: the edit changes the relevant sha, so the next447migrate run re-renders every affected page (canon and DESIGN.md448edits typically affect every page).449450## Stale handling451452When `direction.md`, canon, or the module catalog changes after453some pages have been migrated:454455- Affected pages are flagged `stale: true` per456 `skills/stardust/reference/state-machine.md` § Stale flagging.457 Stale-flagging is content-aware in all three trigger cases.458- `$stardust migrate` (no flags) skips stale pages and reports459 the count.460- `$stardust migrate --all` re-migrates each stale page,461 clearing the flag on success.462- `$stardust migrate <slug>` always operates on the named page,463 stale or not.464465The user decides whether stale pages should be refreshed —466direction/canon/module changes don't invalidate prior migrated467work, they just mark it as out-of-step.468469## Failure modes470471- **No directed pages.** Recommend `$stardust direct` (or472 `$stardust extract` if no extracted state).473- **No DESIGN.md or DESIGN.json.** Recommend `$stardust direct`.474- **No canon, but an approved prototype exists.** Do NOT stop —475 auto-bootstrap canon from the canon-author inline (Setup step 4).476- **No canon and no approved prototype.** Recommend477 `$stardust prepare-migration` (or approve a prototype first).478- **Pending direction.** Refuse; user must resolve direction479 first.480- **Validation failure on a single page.** Skip that page,481 continue, log the failure under482 `state.json.lastRun.failures[]`. Do not abort the whole run.483- **Asset copy failure.** Continue the run; record the missing484 asset in the page's `migrationDecisions[]` with485 `kind: "asset-missing"`. The migrated `<img src>` keeps the486 original absolute URL as a fallback.487- **Output path collision.** Two slugs mapping to the same488 output path. Refuse to write the second one and surface to489 the user — manual slug rename needed.490- **Placeholder content in proposed/archetype file.** Refuse491 to ship a page whose source contains `[data-placeholder]`492 elements. Surface the unsourced list and recommend sourcing493 real content (re-prototype, or edit the proposed file494 directly). There is no bypass flag — shipping placeholders to495 a public site is the failure mode this gate exists to prevent.496- **Color reservation violated.** Refuse the page; surface to497 user with the offending color and the reserved-for context.498- **Brand-faithful inversion conflict.** A hard rule declared499 inverted in500 `extensions.divergence.brand_faithful_inversions[]` is lifted501 from validation per T&M § Brand-faithful inversion handling.502 Emit a one-line note in the run summary acknowledging the503 lift.504505## What migrate does NOT do506507- Critique or audit the migrated output. Run508 `$impeccable critique stardust/migrated/` after migration if509 you want a quality assessment.510- Deploy. Stardust does not push, upload, or modify origin.511- Generate AEM EDS, a CMS payload, or framework components. The512 output is platform-agnostic static HTML; downstream conversion513 is a separate plugin's job.514- Re-fetch the live site. Offline after extract Phase 1.515- Run any iteration loop. Iteration belongs to `prototype`;516 migrate consumes the result.517518## References519520- `reference/migration-procedure.md` — per-page render procedure,521 output path mapping, validation, provenance shape, idempotent522 skip, sidecar schema.523- `reference/template-and-module-rendering.md` — three render524 branches in detail, slot injection, deviation policy,525 validation contracts.526- `reference/metadata-and-jsonld.md` — head composition, JSON-LD527 per page-type, canonical strategy.528- `reference/content-preservation.md` — what's kept,529 transformed, dropped; internal-link rewriting; asset path530 rewriting; form handling.531- `reference/asset-bundling.md` — detection / copy / rewrite532 contract for the per-page asset-bundling phase.533- `skills/stardust/reference/migrate-output-format.md` — the534 self-contained-bundle contract downstream consumers can rely535 on (asset reference shape, directory layout,536 `state.json.migrate` block).537- `skills/stardust/reference/token-contract.md` — `:root` block538 refreshed from DESIGN.md on every render.539- `skills/stardust/reference/data-attributes.md` — structural540 attributes including `data-template`, `data-module`,541 `data-slot`, `data-canon`, `data-deviation`, `data-bespoke`,542 `data-broken-link`.543- `skills/stardust/reference/state-machine.md` — page lifecycle,544 page typing, stale-flagging cascade.545- `skills/stardust/reference/artifact-map.md` — provenance shape546 for migrated artifacts; canon files; sidecar shape.547- `skills/prototype/reference/canon-extraction.md` — how canon548 is built (input to migrate).549- `skills/prepare-migration/SKILL.md` — the cascade that550 produces every input migrate consumes.