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 isdirected,prototyped, orapproved(and notstale).--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-approvedpage. Default behaviour migratesdirectedpages too (using Path A′ or Path B perreference/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 fromstardust/migrated/assets/. Off by default (migrate is additive). Implies--force: every page is re-rendered so the run'sbundledAssetsSet is the complete union of currently-referenced assets — otherwise--cleanwould risk deleting assets still referenced by idempotent-skipped pages. Seereference/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-saveplaywright installs from earlier phases are pruned by any later realnpm i(extract SKILL.md § Setup →--no-saveinstalls are ephemeral). Before any rendering step, probenode -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. Ifstardust/state.jsonexists withoutflowand 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, otherwiseredesign), recorded indirection.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.jsonexists with at least onedirectedpage. -
Verify project-root
DESIGN.mdandDESIGN.jsonexist withDESIGN.json.extensions.canonpopulated. -
Verify
stardust/canon/exists with at leastheader.html,footer.html,canon.css.Canon auto-bootstrap (when steps 3–4 find no canon). The documented
prototype → migrate → deployhappy path does not runprepare-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 oneapprovedprototype exists, do not stop: run the canon write-back inline from the first approved prototype (the canon-author, defaulthome) per../prototype/reference/canon-extraction.md§ Five-step procedure — extractheader.html/footer.html/canon.csstostardust/canon/, pin tokens + compositional moves toDESIGN.json.extensions.canon, and recordcanon.source: "auto-bootstrap: <slug>". This is exactly whatprototype --prepdoes on first approval; migrate performs it on demand so the core pipeline never dead-ends. Only stop and recommend$stardust prepare-migrationwhen canon is absent and no approved prototype exists (there is nothing to derive canon from). Understate.json.handsOffthe bootstrap is automatic and logged; interactively, surface it as a one-line notice before proceeding. -
Verify
stardust/direction.mdhas an active (not pending) direction. -
Read
state.json.pages[]and partition into:- inScope: status
directed,prototyped, orapproved,stale: false(or--all/ explicit<slug>). - skipped: everything else, with reason captured.
- inScope: status
-
Validate provenance on every in-scope page. Call
validateProvenance(page)perskills/stardust/reference/state-machine.md§ Provenance validation for every page ininScope. 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. SurfaceProvenance OK on N pagesin 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 perskills/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.adaptsidecar. 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 adaptinvoked 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
fidelityTierfrom 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— perreference/fidelity-tiers.md. RecordfidelityTier,archetypeSource,template(the archetype's slug; the archetype's own sidecar leaves it null),modules[](the page's block ids — composite sections only) andgatesPassed[]in_meta.jsonso coverage shows what was craft-gated vs cloned and rollout groups and dedups from the sidecars, without a census. - 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 withdata-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 loggedcontentDeviations[]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 perreference/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 fromstardust/current/assets/<subpath>tostardust/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 fromstate.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 skipcreateOptimizedPicture(it drops the?v=key and corrupts the rendition); broken URLs are repaired (missing?-delimiter, wrong host) or omitted, never shipped asabout:error.rolloutre-runs the authoritative network resolve at delivery (media-reconcile.mjs). - Cinematic sibling (when
<slug>-cinematic.htmlexists). Migrate consumes the STATIC prototype only — the cinematic layer is never merged. Copy the motion assets (lenis.min.js,lenis.min.css) fromstardust/prototypes/tostardust/migrated/assets/motion/(idempotent) for downstream consumers (deploy/rollout decide whether to wire them), and recordcinematic-variant-not-consumedin the page's_meta.json#migrationDecisions[]. - Write the migrated
index.htmland the_meta.jsonsidecar in the same directory. Provenance block as first child of<head>. RecordassetsBundled(count of unique asset refs on this page) in_meta.json.
The mechanics of this phase are scripted:
node skills/migrate/scripts/migrate.mjs render <slug…|--all> (the
project copy runs from stardust/scripts/migrate/) builds the page
map, places each page at its URL-literal path, rewrites internal
links, bundles assets, composes <head> (provenance, :root,
canonical and JSON-LD defaults), validates strictly and writes the
sidecar, skipping pages whose input shas are unchanged. The judgments
it cannot make are recorded on the sidecar with gate, deviation,
decision, variant and modules on the same script; it never
advances pages[].status.
Recorded units. A whole-site render is bookkept as units in
stardust/migrate/progress.json, the shape of rollout's C-deliver unit
ledger (../replica/reference/handoff-contract.md § 3 row C):
units.<name>: {status: pending|running|done|failed, kind: plan|render|assets|report, templates[], pages: n, startedAt, endedAt, verdict}. One plan unit (Phase 1), one render unit per template
cluster of at most ~8 siblings (migrate.mjs render <slug…> takes the
cluster's slug list), one assets unit when Phase 3 rehosts media, one
report unit (Phase 4). Per unit, in this order: the progress.json
write → the checkpoint commit → the next unit; a unit end is a safe
resume point, not a stop. A session resumed inside migrate reads the file
and runs only the units not done; a unit left running re-renders its
slugs — the driver is idempotent, unchanged pages are skipped. A recorded
run rendered 31 siblings through five parallel builders as one unit in
one session — its most expensive — with no resume point between the plan
and the final report.
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>tostardust/migrated/assets/logo.<ext>(only if missing or stale). Record understate.json.migrate.bundledAssets[]. -
Verify favicon variants and font files were generated by
prepare-migrationPhase 4. If absent, log a warning and continue (the migrated site renders without them, just missing some platform-specific affordances). -
Add
stardust/migrated/robots.txtandsitemap.xmlderived from the migrated page inventory perreference/metadata-and-jsonld.md§ Sitemap entry. -
If
--cleanwas passed, computestale = priorBundle.filter(p => !bundledAssets.has(p))fromstate.json.migrate.bundledAssets[]and remove each stale subpath fromstardust/migrated/assets/. Record the deletions understate.json.migrate.cleanedAssets[]. Perreference/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:
statusadvances tomigrated, append a history entry, clear anystaleflag, setmigratedPath. - 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
migrateblock perskills/stardust/reference/migrate-output-format.md§ State.json contract:selfContained: true,outputDir,totalAssetsBundled,bundledAssets[], per-pageassetsBundledcounts,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). |
The driver (node stardust/scripts/migrate/migrate.mjs render …) writes
the per-page rows above (index.html / <name>.html, _meta.json,
bundled assets/**) and the migrate block of state.json (pageMap[],
bundledAssets[], pages[], missingAssets[], lastRun); status
changes, robots.txt and sitemap.xml stay with Phases 3–4.
Idempotent and incremental
The whole pipeline is built around two properties:
- Idempotent. Re-running
$stardust migratewith 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: trueperskills/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 --allre-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 extractif 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[]withkind: "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.migrateblock).skills/stardust/reference/token-contract.md—:rootblock refreshed from DESIGN.md on every render.skills/stardust/reference/data-attributes.md— structural attributes includingdata-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.