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).
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
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),
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.
- 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: migrate3description: Apply design, canon, and modules to every page in the inventory to produce a deployable static HTML site. Per-page, incremental, and idempotent.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).692. Verify `stardust/state.json` exists with at least one70 `directed` page.713. Verify project-root `DESIGN.md` and `DESIGN.json` exist with72 `DESIGN.json.extensions.canon` populated.734. Verify `stardust/canon/` exists with at least74 `header.html`, `footer.html`, `canon.css`.7576 **Canon auto-bootstrap (when steps 3–4 find no canon).** The77 documented `prototype → migrate → deploy` happy path does not78 run `prepare-migration`, so a first migrate legitimately arrives79 with no canon (observed on 4 of 6 e2e sites, where every run had80 to derive canon by hand to proceed — this is the fix). When81 canon is absent **and** at least one `approved` prototype exists,82 do not stop: run the canon write-back inline from the first83 approved prototype (the canon-author, default `home`) per84 `../prototype/reference/canon-extraction.md` § Five-step85 procedure — extract `header.html` / `footer.html` / `canon.css`86 to `stardust/canon/`, pin tokens + compositional moves to87 `DESIGN.json.extensions.canon`, and record88 `canon.source: "auto-bootstrap: <slug>"`. This is exactly what89 `prototype --prep` does on first approval; migrate performs it90 on demand so the core pipeline never dead-ends. Only stop and91 recommend `$stardust prepare-migration` when canon is absent92 **and** no approved prototype exists (there is nothing to derive93 canon from). Under `state.json.handsOff` the bootstrap is94 automatic and logged; interactively, surface it as a one-line95 notice before proceeding.965. Verify `stardust/direction.md` has an active (not pending)97 direction.986. Read `state.json.pages[]` and partition into:99 - **inScope**: status `directed`, `prototyped`, or100 `approved`, `stale: false` (or `--all` / explicit101 `<slug>`).102 - **skipped**: everything else, with reason captured.1037. **Validate provenance on every in-scope page.** Call104 `validateProvenance(page)` per105 `skills/stardust/reference/state-machine.md` § Provenance106 validation for every page in `inScope`. Abort with the107 helper's error when any page lacks live-render evidence —108 migrating a synthesized page record produces deployable HTML109 that misrepresents the source site, the exact failure mode110 that motivated the validator. Surface `Provenance OK on N111 pages` in the migrate-plan output before Phase 1.1128. **Mobile-adapt audit on every Path A / Path A′ source.** For113 every page whose render branch consumes a proposed or114 archetype HTML file (Path A, Path A′ per115 `reference/template-and-module-rendering.md` § Render path116 selection), run the audit per `skills/prototype/SKILL.md`117 § Mobile-adapt audit:118119 - `<meta name="viewport" content="width=device-width, ...">`120 present, width not pinned to a fixed pixel value.121 - At least one `@media (max-width: ...)` rule.122 - At least one mobile-targeted breakpoint at ≤ 640px.123124 Refuse pages that fail — the audit is mandatory; there is no125 skip flag. The user fixes the proposed file (re-prototype or126 chat-driven impeccable command) and re-invokes migrate.127 Record the audit result per page in the migrate report and128 in the post-render `_meta.json#audit.adapt` sidecar. Path B129 (unique-renders) skips the audit because adapt hasn't run130 on those pages — a Path B page that needs mobile coverage131 gets it via `$impeccable adapt` invoked separately by the132 user. Surface this distinction in the report so it's not133 read as a silent skip.134135## Procedure136137### Phase 1 — Plan138139Print the plan and wait for confirmation when the scope is large:140141```142migrate plan143============144145In scope: 127 pages146 Path A (approved) 6 pages: home, news/post-housing-summit, news, ...147 Path A' (template-applied) 118 pages: 84 article, 5 listing, 11 program, 2 form, 16 static148 Path B (unique) 3 pages: 404, search, faq149150Skipped: 0 stale, 0 unscoped151152DESIGN.md sha: 1a2b3c4153DESIGN.json sha: 5d6e7f8154Canon shas: header:7g8h9i footer:9i0j1k css:1k2l3m155156Output: stardust/migrated/ + per-page _meta.json sidecars157Idempotent skip: enabled (run with --force to override)158159Reply "go" to proceed.160```161162For 1-3 pages or `<slug>` invocation, skip the confirmation.163164### Phase 2 — Per-page render165166For each page in scope, follow167`reference/migration-procedure.md` and168`reference/template-and-module-rendering.md`:169170- **Idempotent skip check** first (sha-compare across171 `designMd`, `designJson`, `sourceCurrent`, `sourceProposed`,172 `canonShas`, `archetypeSource`).173- **Placeholder gate** (Path A and Path A′ only — pages with a174 proposed file or archetype). Refuse when `[data-placeholder]`175 elements or non-empty `_provenance.unsourcedContent[]` are176 present — the user fills the missing content in the proposed177 file before re-invoking migrate. No bypass flag.178- **Render branch selection** (LLM judgment per T&M §179 Render path selection): A / A′ / B. **Declare the page's180 `fidelityTier`** from the branch — A → `archetype` (craft-gated),181 A′ → `sibling` (canon-fork, the cheap default for breadth),182 B/bodyless → `thin` — per `reference/fidelity-tiers.md`. Record183 `fidelityTier`, `archetypeSource`, and `gatesPassed[]` in184 `_meta.json` so coverage shows what was craft-gated vs cloned.185- **Render** per the chosen branch's procedure in T&M.186- **Canon application** — chrome injection, canon.css187 injection, deviation logging.188- **Module rendering** — render module instances via189 `stardust/canon/modules/<id>.html`; bespoke slots logged190 with `data-bespoke`.191- **Apply content-preservation rules** per192 `reference/content-preservation.md`. Internal-link rewriting193 always emits migrated-tree paths; missing slugs flagged194 broken.195- **Compose `<head>` metadata** per196 `reference/metadata-and-jsonld.md` (five categories;197 page-type-driven JSON-LD).198- **Validate** per T&M § Validation contracts. Strict contracts199 refuse the page; soft contracts log and continue.200- **Compute output path** per migration-procedure.md201 § Output path mapping.202- **Asset bundling.** Scan the final HTML for asset references203 (six detection shapes per204 `reference/asset-bundling.md` § Detection), copy each unique205 referenced subpath from `stardust/current/assets/<subpath>` to206 `stardust/migrated/assets/<subpath>` (preserving subdir207 structure), then rewrite every reference to the root-relative208 form `/assets/<subpath>`. Cross-page dedup uses a209 module-level Set seeded from210 `state.json.migrate.bundledAssets[]`. Missing source assets211 warn-and-skip per § Edge cases; the bundle stays internally212 consistent.213- **Media reconciliation.** For every image **not** bundled to214 same-origin (reused source-CDN URLs under Mode A image-reuse),215 decide optimize/keep/rewrite/omit per216 `reference/media-reconciliation.md`. Cross-origin `<img>` kept217 as source URLs must **skip `createOptimizedPicture`** (it drops218 the `?v=` key and corrupts the rendition); broken URLs are219 repaired (missing `?`-delimiter, wrong host) or omitted, never220 shipped as `about:error`. `rollout` re-runs the authoritative221 network resolve at delivery (`media-reconcile.mjs`).222- **Cinematic sibling (when `<slug>-cinematic.html` exists).**223 Migrate consumes the STATIC prototype only — the cinematic layer224 is never merged. Copy the motion assets (`lenis.min.js`,225 `lenis.min.css`) from `stardust/prototypes/` to226 `stardust/migrated/assets/motion/` (idempotent) for downstream227 consumers (deploy/rollout decide whether to wire them), and228 record `cinematic-variant-not-consumed` in the page's229 `_meta.json#migrationDecisions[]`.230- **Write** the migrated `index.html` and the `_meta.json`231 sidecar in the same directory. Provenance block as first232 child of `<head>`. Record `assetsBundled` (count of unique233 asset refs on this page) in `_meta.json`.234235### Phase 3 — Sitewide assets and bundle finalisation236237Per-page asset bundling already happened in Phase 2 (every238referenced media subpath is on disk under239`stardust/migrated/assets/`). Phase 3 fills in the **sitewide240assets** that no individual page references explicitly:2412421. Copy `stardust/current/assets/logo.<ext>` to243 `stardust/migrated/assets/logo.<ext>` (only if missing or244 stale). Record under `state.json.migrate.bundledAssets[]`.2452. Verify favicon variants and font files were generated by246 `prepare-migration` Phase 4. If absent, log a warning and247 continue (the migrated site renders without them, just248 missing some platform-specific affordances).2493. Add `stardust/migrated/robots.txt` and `sitemap.xml`250 derived from the migrated page inventory per251 `reference/metadata-and-jsonld.md` § Sitemap entry.2524. If `--clean` was passed, compute253 `stale = priorBundle.filter(p => !bundledAssets.has(p))`254 from `state.json.migrate.bundledAssets[]` and remove each255 stale subpath from `stardust/migrated/assets/`. Record the256 deletions under `state.json.migrate.cleanedAssets[]`. Per257 `reference/asset-bundling.md` § Stale asset cleanup.2585. Verify **portability**. The bundle must work via `file://`,259 at a webserver root, and at any subpath — "one shape, works260 everywhere". Run every audit; any non-empty grep output or261 non-zero fixture exit fails the run with the cited error262 message:263264 ```bash265 # No source-tree escapes266 find stardust/migrated/ -type f -name '*.html' -exec grep -l '\.\./current/' {} +267 # Error: "asset still points outside the migrated tree; rewrite via the268 # asset-bundling pass per reference/asset-bundling.md § Detection"269270 # No absolute internal references in attribute values (404 on file:// and subpath)271 grep -rE '(href|src)="/[^/]' stardust/migrated/ --include='*.html'272 # Error: "absolute href `/beers/` will 404 on file:// and on subpath hosts;273 # rewrite via the page map per migration-procedure.md § Reference shape"274275 # No absolute internal references in url() (inline style, <style> blocks, CSS)276 grep -rE 'url\(\s*["''']?\s*/[^/]' stardust/migrated/ --include='*.html' --include='*.css'277 # Error: "absolute url(/...) reference will 404 on file:// and on subpath hosts;278 # rewrite via the asset-bundling pass per asset-bundling.md § Rewrite"279280 # No directory-only nav (doesn't resolve on file://). Pattern accepts281 # only relative or root-absolute hrefs (./, ../, /, or bare segment)282 # so external URLs like https://google.com/ aren't false-flagged.283 grep -rE 'href="(\.{0,2}/|[a-zA-Z0-9_-])[^:"#?]*/"' stardust/migrated/ --include='*.html'284 # Error: "directory-only href `./beers/` won't resolve on file://;285 # append the explicit index.html (or the source URL's .html leaf)286 # per § Reference shape"287288 # pageMap consistency — every internal href appears as an outputPath289 node skills/migrate/fixtures/pagemap-audit.mjs stardust/migrated/ stardust/state.json290 # Error: "internal href has no pageMap entry; link rewriting bypassed the291 # page map per § Page map (build once, use everywhere)"292293 # Headless file:// round-trip — the test that proves zip-and-deploy works294 node skills/migrate/fixtures/file-protocol-audit.mjs stardust/migrated/295 # Error: "<offending file> linked <ref> that 404s under file://; see the296 # Playwright network log printed above"297 ```298299 The audits are mandatory — there is no skip flag. The contract300 is "self-contained, zip-and-deploy" and these audits are the301 verifiers that back the claim.302303Asset migration is idempotent — files are content-hashed and304copied only when missing; per-page bundling deduplicates across305the run.306307### Phase 4 — State and report308309Update `state.json`:310311- For each successfully migrated page: `status` advances to312 `migrated`, append a history entry, clear any `stale` flag,313 set `migratedPath`.314- For pages skipped via idempotent skip: leave state315 unchanged.316- For pages that failed validation: leave state unchanged, log317 the failure in `state.json.lastRun.failures[]`.318- Write the top-level `migrate` block per319 `skills/stardust/reference/migrate-output-format.md`320 § State.json contract: `selfContained: true`, `outputDir`,321 `totalAssetsBundled`, `bundledAssets[]`, per-page322 `assetsBundled` counts, `missingAssets[]`, `cleanedAssets[]`.323 This is the forward-compat signal downstream consumers test324 for.325326Print the run summary:327328```329migrate complete330================331332 122 migrated home, about, news/post-housing-summit, ...333 3 unchanged about, programs/shelter, news/post-old (idempotent skip)334 2 failed contact (validation: required slot missing),335 legal/privacy (validation: color-reservation violated)336 0 stale skipped337338Render branches:339 Path A 6 approved-from-prototype340 Path A' 116 template-applied (84 article, 5 listing, 11 program, 2 form, 14 static)341 Path B 3 unique-render (404, search, faq)342343Pages with non-trivial decisions: 12344 about canon-deviation: footer carries financials disclaimer345 donate template-adapted: amount-pills slot moved above headline346 ...347348Broken internal links: 5349 /events referenced by 2 pages; not in inventory350 /press referenced by 1 page; not in inventory351 ...352353Bespoke slots crossing promotion threshold: 1354 hotline-211: "state" (3 instances) — consider `$stardust prepare-migration --refine-module`355356Missing assets: 2357 generated/orphan-1.jpg referenced by 1 page (home)358 generated/orphan-2.jpg referenced by 2 pages (about, contact)359 (Re-extract or accept the gap — bundle is deployable; refs 404 at view time.)360361Output: stardust/migrated/ (122 pages, 47 bundled assets, 4.2 MB) — self-contained, zip-and-deploy362363Next:364 - Review: open stardust/migrated/index.html in a browser365 - Audit: $impeccable critique stardust/migrated/366 - Deploy: cd stardust/migrated && zip -r ../site.zip .367 upload the zip to any static host that serves at the host root368 - Refine: edit DESIGN.md or canon files, then re-run $stardust migrate369```370371## Outputs372373| Path | Purpose |374|---------------------------------------------------|--------------------------------------------------------|375| `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. |376| _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. |377| `stardust/migrated/index.html` | The home page (special case). |378| `stardust/migrated/_meta.json` | Home sidecar. |379| `stardust/migrated/assets/logo.<ext>` | Brand logo (sitewide). |380| `stardust/migrated/assets/<subpath>` | Every asset referenced by any migrated page, bundled. Source subdir structure preserved verbatim. |381| `stardust/migrated/assets/favicon.<ext>` + variants| Favicon and apple-touch-icon, manifest icons. |382| `stardust/migrated/assets/fonts/...` | Downloaded font files (from canon @font-face URLs). |383| `stardust/migrated/robots.txt` | Minimal robots.txt. |384| `stardust/migrated/sitemap.xml` | Sitemap derived from migrated inventory + page types. |385| `stardust/state.json` | Updated with `migrated` status, history, and the `migrate` block (`selfContained: true`, asset counts). |386387## Idempotent and incremental388389The whole pipeline is built around two properties:390391- **Idempotent.** Re-running `$stardust migrate` with no392 changes produces zero file writes. Every page is sha-compared393 across designMd, designJson, sourceCurrent, sourceProposed394 (Path A), canonShas, archetypeSource (Path A′) — and skipped395 if all match.396- **Incremental.** Migrate 5 pages today, 20 pages tomorrow,397 fix one page's content next week — the migrated tree is398 always the union of every successful migration to date.399400These properties hold even when DESIGN.md, canon, or modules are401edited mid-run: the edit changes the relevant sha, so the next402migrate run re-renders every affected page (canon and DESIGN.md403edits typically affect every page).404405## Stale handling406407When `direction.md`, canon, or the module catalog changes after408some pages have been migrated:409410- Affected pages are flagged `stale: true` per411 `skills/stardust/reference/state-machine.md` § Stale flagging.412 Stale-flagging is content-aware in all three trigger cases.413- `$stardust migrate` (no flags) skips stale pages and reports414 the count.415- `$stardust migrate --all` re-migrates each stale page,416 clearing the flag on success.417- `$stardust migrate <slug>` always operates on the named page,418 stale or not.419420The user decides whether stale pages should be refreshed —421direction/canon/module changes don't invalidate prior migrated422work, they just mark it as out-of-step.423424## Failure modes425426- **No directed pages.** Recommend `$stardust direct` (or427 `$stardust extract` if no extracted state).428- **No DESIGN.md or DESIGN.json.** Recommend `$stardust direct`.429- **No canon, but an approved prototype exists.** Do NOT stop —430 auto-bootstrap canon from the canon-author inline (Setup step 4).431- **No canon and no approved prototype.** Recommend432 `$stardust prepare-migration` (or approve a prototype first).433- **Pending direction.** Refuse; user must resolve direction434 first.435- **Validation failure on a single page.** Skip that page,436 continue, log the failure under437 `state.json.lastRun.failures[]`. Do not abort the whole run.438- **Asset copy failure.** Continue the run; record the missing439 asset in the page's `migrationDecisions[]` with440 `kind: "asset-missing"`. The migrated `<img src>` keeps the441 original absolute URL as a fallback.442- **Output path collision.** Two slugs mapping to the same443 output path. Refuse to write the second one and surface to444 the user — manual slug rename needed.445- **Placeholder content in proposed/archetype file.** Refuse446 to ship a page whose source contains `[data-placeholder]`447 elements. Surface the unsourced list and recommend sourcing448 real content (re-prototype, or edit the proposed file449 directly). There is no bypass flag — shipping placeholders to450 a public site is the failure mode this gate exists to prevent.451- **Color reservation violated.** Refuse the page; surface to452 user with the offending color and the reserved-for context.453- **Brand-faithful inversion conflict.** A hard rule declared454 inverted in455 `extensions.divergence.brand_faithful_inversions[]` is lifted456 from validation per T&M § Brand-faithful inversion handling.457 Emit a one-line note in the run summary acknowledging the458 lift.459460## What migrate does NOT do461462- Critique or audit the migrated output. Run463 `$impeccable critique stardust/migrated/` after migration if464 you want a quality assessment.465- Deploy. Stardust does not push, upload, or modify origin.466- Generate AEM EDS, a CMS payload, or framework components. The467 output is platform-agnostic static HTML; downstream conversion468 is a separate plugin's job.469- Re-fetch the live site. Offline after extract Phase 1.470- Run any iteration loop. Iteration belongs to `prototype`;471 migrate consumes the result.472473## References474475- `reference/migration-procedure.md` — per-page render procedure,476 output path mapping, validation, provenance shape, idempotent477 skip, sidecar schema.478- `reference/template-and-module-rendering.md` — three render479 branches in detail, slot injection, deviation policy,480 validation contracts.481- `reference/metadata-and-jsonld.md` — head composition, JSON-LD482 per page-type, canonical strategy.483- `reference/content-preservation.md` — what's kept,484 transformed, dropped; internal-link rewriting; asset path485 rewriting; form handling.486- `reference/asset-bundling.md` — detection / copy / rewrite487 contract for the per-page asset-bundling phase.488- `skills/stardust/reference/migrate-output-format.md` — the489 self-contained-bundle contract downstream consumers can rely490 on (asset reference shape, directory layout,491 `state.json.migrate` block).492- `skills/stardust/reference/token-contract.md` — `:root` block493 refreshed from DESIGN.md on every render.494- `skills/stardust/reference/data-attributes.md` — structural495 attributes including `data-template`, `data-module`,496 `data-slot`, `data-canon`, `data-deviation`, `data-bespoke`,497 `data-broken-link`.498- `skills/stardust/reference/state-machine.md` — page lifecycle,499 page typing, stale-flagging cascade.500- `skills/stardust/reference/artifact-map.md` — provenance shape501 for migrated artifacts; canon files; sidecar shape.502- `skills/prototype/reference/canon-extraction.md` — how canon503 is built (input to migrate).504- `skills/prepare-migration/SKILL.md` — the cascade that505 produces every input migrate consumes.