mirror of
https://github.com/adobe/skills.git
synced 2026-09-19 08:06:09 +08:00
1ab6833f1e
* fix(stardust): make migrate output genuinely zip-and-deploy portable The 0.7.0 contract claimed "self-contained, zip-and-deploy" but emitted root-absolute paths (/assets/..., /beers/) plus directory-only nav links. Those work on a webserver at the host root only — they 404 on file:// and at any subpath. This PR delivers what the claim promised: - Output path is URL-literal: /beers/ -> beers/index.html, /about-us/history.html -> about-us/history.html. No more synthesizing /about/index.html from a slug. - Page map (state.json.migrate.pageMap[]) built once per run; per-page rendering and internal-link rewriting both consume it. - Reference shape is depth-aware relative: prefix is ./ for depth-0 pages, ../ * depth otherwise. Works on file://, at host root, and at any subpath without rewriting. - Phase 3 broadens the self-containment check into a full portability audit: refuses absolute internal refs, refuses directory-only nav, runs the new pageMap + file:// fixtures. - New fixtures: file-protocol-audit.mjs (Playwright file:// round-trip) and pagemap-audit.mjs (static pageMap consistency). - asset-bundling.md and migrate-output-format.md aligned to the new depth-aware-relative shape; state.json contract gains pageMap[]. Plugin bumped 0.7.0 -> 0.7.1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * review: fix four flagged correctness issues - Directory-only-nav regex now catches `./beers/` (the cited example). Old pattern required a-z as first char; `./beers/` starts with `.` and would have slipped through. New pattern accepts ./, ../, /, or a bare segment, and excludes scheme-bearing URLs (no false positive on https://google.com/). - Add a second grep for absolute url() references inside inline style attrs and <style> blocks (and CSS files); the original href/src grep missed these. - SKILL.md Outputs row no longer claims the sidecar lives at <source-url-path>/_meta.json (nonsense for .html-leaf pages). Replaced with a row that points at the canonical sidecar rule. - migration-procedure.md § Page map now explicitly says same-origin absolute URLs are stripped to their path portion before pageMap lookup (per content-preservation.md § Internal link rewriting), closing the loophole that would let <a href="https://origin/foo/"> pass through verbatim. Regex patterns verified against a test fixture before commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * review: update migrate-self-contained-bundle eval to 0.7.1 contract The eval was pinned to 0.7.0's root-relative /assets/<subpath> shape. This PR's contract change to depth-aware relative paths would have made the eval fail against correct 0.7.1 output. - detection_src_href criterion now asserts <prefix>assets/<subpath> (not /assets/<subpath>). - Renamed prefix_resolution_root_relative -> prefix_normalises_authoring; description flipped from "reference is left as-is" to "authoring is normalised to the depth-aware emit form". - Added 4 new criteria: depth_aware_paths (6pts), pagemap_constructed (6pts), internal_links_via_pagemap (5pts), portability_audit_passes (13pts). - Setup uses real source URLs that exercise the URL-literal rule: /, /beers/, /about/history.html (preserve .html leaf), /docs/api/ (depth-2 nested). - Prototype HTML now carries internal nav in three authoring styles (root-absolute, same-origin absolute, directory-only) plus a broken-link case, exercising the page map rewrite. - Run 1 expected behavior rewritten to expect depth-aware emission ('./assets/...'), pageMap built before render, broken-link flagging, and the 6-step portability audit passing. - Run 4 (cross-page dedup) updated so the second page emits its shared asset with its own '../' prefix while the underlying file on disk is a single copy. - Weights re-balanced to sum to 100; portability_audit_passes gets the largest weight (13pts) since it backs the contract claim. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
30 lines
7.3 KiB
JSON
30 lines
7.3 KiB
JSON
{
|
|
"name": "migrate-self-contained-bundle",
|
|
"total": 100,
|
|
"criteria": [
|
|
{ "id": "activated", "weight": 3, "description": "The stardust:migrate skill was invoked." },
|
|
{ "id": "no_current_escapes", "weight": 6, "description": "After run 1, no HTML file under stardust/migrated/ contains the substring '../current/'. Self-containment verified by absence of escape segments. (Acceptance criterion #1.)" },
|
|
{ "id": "zip_deploy_ready", "weight": 5, "description": "Running 'cd stardust/migrated && zip -r out.zip .' produces a zip that renders identically in three contexts: extracted and served at the host root, extracted and served at any subpath, AND opened directly via file://. No 404s on any bundled asset. (Acceptance criteria #2, #3 + the new file:// portability guarantee.)" },
|
|
{ "id": "idempotent_modulo_timestamp", "weight": 5, "description": "Re-running migrate without source changes produces output that differs only in the migrate-provenance timestamp line. With --pin-timestamp, output is byte-identical to the prior run. (Acceptance criterion #4.)" },
|
|
{ "id": "asset_count_in_report", "weight": 3, "description": "Run summary includes 'N pages, M bundled assets, KKK KB HTML total — self-contained, zip-and-deploy' so the user sees the bundle composition at a glance. (Acceptance criterion #5.)" },
|
|
{ "id": "missing_asset_surfaced", "weight": 4, "description": "The 'generated/missing.jpg' reference surfaces in the run report's Missing assets block AND in state.json.migrate.missingAssets[]. Migrate completes successfully (warnings, not errors). (Acceptance criterion #6.)" },
|
|
{ "id": "detection_src_href", "weight": 3, "description": "Detection shape #1 (single-URL src/href): the <link rel=\"icon\"> and <img src> references are bundled and rewritten to depth-aware relative paths of the form <prefix>assets/<subpath>, where prefix is './' for depth-0 pages and '../'.repeat(depth) for nested pages." },
|
|
{ "id": "detection_srcset", "weight": 5, "description": "Detection shape #2 (srcset multi-URL): each URL in the comma-separated list is rewritten independently to depth-aware relative form; descriptors (1x, 2x, 768w) preserved verbatim; whitespace between entries preserved." },
|
|
{ "id": "detection_inline_style", "weight": 3, "description": "Detection shape #3 (inline style=\"...url(...)...\"): background-image url() is rewritten to depth-aware relative form." },
|
|
{ "id": "detection_style_block_url", "weight": 4, "description": "Detection shape #4 (<style>-block url()): both @font-face src: url() and .grid background url() are rewritten to depth-aware relative form." },
|
|
{ "id": "depth_aware_paths", "weight": 6, "description": "Asset and internal-page references are emitted as depth-aware relative paths, NOT root-relative. From home (depth 0): './assets/foo'. From beers/index.html (depth 1): '../assets/foo' and '../about/index.html'. From docs/api/index.html (depth 2): '../../assets/foo'. Per migration-procedure.md § Reference shape." },
|
|
{ "id": "prefix_normalises_authoring", "weight": 4, "description": "Root-relative authoring ('/assets/generated/hero-1x.jpg') and bare-relative authoring ('assets/foo') are BOTH normalised to the depth-aware relative form on the emitted page. Authoring style is not preserved verbatim — the contract is 'one shape, works everywhere'." },
|
|
{ "id": "asset_subpath_preserved", "weight": 3, "description": "Source subdir structure preserved verbatim under stardust/migrated/assets/: generated/hero-1x.jpg, fonts/inter-var.woff2, photos/family portrait.jpg all land at the expected paths." },
|
|
{ "id": "percent_encoded_subpath", "weight": 3, "description": "The reference to '<prefix>assets/photos/family%20portrait.jpg' is preserved encoded in HTML but the file-system access decodes to 'photos/family portrait.jpg' and the file copies correctly." },
|
|
{ "id": "path_traversal_refused", "weight": 3, "description": "The '/assets/../etc/passwd' reference is refused: no copy, the HTML reference is left unchanged (so the issue surfaces visibly on deploy), and the page's _meta.json#migrationDecisions[] records kind: 'asset-path-traversal'." },
|
|
{ "id": "scheme_bearing_url_skipped", "weight": 2, "description": "The 'https://fonts.googleapis.com/...' stylesheet link is left external — not bundled, not rewritten, not flagged as missing. CDN URLs are by-design external." },
|
|
{ "id": "pagemap_constructed", "weight": 6, "description": "state.json.migrate.pageMap[] is built BEFORE per-page rendering and lists every in-scope source URL with its outputPath (URL-literal rule per migration-procedure.md § Output path mapping): /beers-bev/ -> beers/index.html, /about-us/history.html -> about-us/history.html (preserve .html leaf, no extra index.html). Each entry: { sourceUrl, outputPath, slug }." },
|
|
{ "id": "internal_links_via_pagemap", "weight": 5, "description": "Internal <a href> rewrites resolve via pageMap.outputPath lookup, NOT slug synthesis. Same-origin absolute hrefs (https://<origin>/path) are stripped to their path portion and looked up via pageMap, then emitted as depth-aware relative refs. A target URL with no pageMap entry is flagged data-broken-link='true' and logged under provenance.brokenInternalLinks[]." },
|
|
{ "id": "state_json_migrate_block", "weight": 6, "description": "state.json.migrate is written with: selfContained: true, outputDir, pageMap[] (built first), totalAssetsBundled, bundledAssets[] (unique subpath set), per-page assetsBundled counts, missingAssets[], cleanedAssets[]. Field order and shapes match migrate-output-format.md § State.json contract." },
|
|
{ "id": "self_contained_signal", "weight": 2, "description": "state.json.migrate.selfContained === true is the forward-compat signal — downstream consumers can test for it." },
|
|
{ "id": "cross_page_dedup", "weight": 3, "description": "When two pages reference the same asset, the bundler copies it exactly once. The global Set is seeded from state.json.migrate.bundledAssets[] so cross-run dedup works even when only some pages re-render. Each page emits the asset with its own depth-aware prefix; the underlying copy is shared." },
|
|
{ "id": "stale_cleanup_clean_flag", "weight": 3, "description": "Run 3 with --clean removes assets that were bundled in the prior run but are no longer referenced. --clean implies --force (every page re-renders so the bundledAssets set is the complete union; idempotent-skipped pages' references are not missed). state.json.migrate.cleanedAssets[] records what was removed. Without --clean, the run is additive and honors the idempotent skip." },
|
|
{ "id": "portability_audit_passes", "weight": 13, "description": "Phase 3 portability audit (per SKILL.md § Phase 3) passes: (a) no '../current/' escapes; (b) no absolute internal href/src ('grep -rE \"(href|src)=/[^/]\"' empty); (c) no absolute url() ('grep -rE \"url\\(\\s*[\\\"\\']?\\s*/[^/]\"' empty); (d) no directory-only hrefs ('grep -rE \"href=\\\"(\\.{0,2}/|[a-zA-Z0-9_-])[^:\\\"#?]*/\\\"\"' empty); (e) pagemap-audit.mjs exits 0; (f) file-protocol-audit.mjs exits 0. Any non-empty grep output or non-zero fixture exit fails the run." }
|
|
]
|
|
}
|