Phase 3: assets - #18
Open
patshone-gsl wants to merge 18 commits into
Open
Conversation
Measured five corrections to the brief: orphan deletion is not a step since source/ is git-ignored and regenerable, all 50 extension-less files are orphans, the 28 MB GIF is unreferenced, resizing alone cannot meet the 60 MB gate but palette quantisation can, and the ~/* alias removes the need for per-file relative paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017g54jmeHWdVo2uJ8vScwD5
Eight tasks: cover target extraction, pure naming and treatment planning, copy-set extraction, the asset script copying verbatim, map-aware reference emission, image encoding, video encoding, and the phase gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017g54jmeHWdVo2uJ8vScwD5
A missing or non-numeric byte count previously fell through the size comparison as NaN >= threshold (false), silently copying a large GIF into src/assets instead of throwing. Guard mirrors the existing still-image colours check per the loud-failure constraint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017g54jmeHWdVo2uJ8vScwD5
Removed claims of re-encoding and hash-based skip that this commit does not implement; this task copies byte-for-byte and always re-copies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017g54jmeHWdVo2uJ8vScwD5
convert.mjs now consumes asset-map.json to resolve every .gitbook/assets
reference to its real src/assets/, public/media/ or public/files/ location,
replacing the root-absolute placeholders that made every image 404. Without
a map (source/ has never had assets.mjs run against it) the placeholder is
kept so the build stays green; a reference missing from a map that does
exist throws.
convertFile (the {% file %} block, the corpus's one download rather than
image) is made map-aware the same way, reusing figures.mjs's NEEDS_ANGLE
predicate rather than always bracketing the link. The Step 6 on-disk
assertion is extended to resolve markdown-link destinations
([name](/files/…)) as well as quoted attributes (<video src="/media/…">),
so the download's single reference is checked too.
images drops from 529 to 528: the corpus's one video-kind asset
(Knowledge Base Demo.gif) now renders as a <video> element instead of a
markdown image, and that count was measured before the map distinguished
kinds.
npm run convert now runs assets.mjs before convert.mjs so the map exists
by the time it's needed.
- Reword the EXPECTED.images comment: it counts markdown-image occurrences across the corpus, not distinct assets, and states the one-occurrence delta caused by the sole video-kind asset explicitly rather than in a way that read as 500 - 1 = 499. - Consolidate the .gitbook/assets name-unescaping rule into asset-refs.mjs (exported as ASSET_SRC and unescapeAssetName, with the try/catch and named error) and have figures.mjs import it, rather than keeping a second, silently driftable copy of the same regex and decode chain. - Carry a video's alt text forward as an aria-label, guarded so an empty alt emits no empty attribute — the alt text was being dropped with no fallback for the one asset this task turns into a <video> element.
Resizes stills to a 2000px width ceiling and palette-quantises PNGs below a colour threshold, preserving each source's own format so a .jpg file never ends up holding PNG bytes. A content-hash cache skips re-encoding unchanged assets, and the destination sweep is driven by the previous run's map rather than a directory listing, so files this script never wrote (the site logo) are never touched. Brings src/assets from 121.4 MiB to 32.0 MiB, under the 60 MB gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017g54jmeHWdVo2uJ8vScwD5
A slug is not unique across destinations over time: a GIF crossing GIF_VIDEO_THRESHOLD keeps its filename but moves between src/assets and public/media between runs. Keying the sweep on slug alone let a newly claimed slug in its new destination mask a stale copy left behind in its old one, which then had no run that would ever remove it. Legacy map entries without a destination field still resolve via DESTINATION_FOR_KIND, since kind has always determined destination one-to-one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017g54jmeHWdVo2uJ8vScwD5
Knowledge Base Demo.gif was classified kind: video but copied verbatim, so the page emitted a <video> pointing at a .gif that no browser can play. Adds a deterministic ffmpeg encode (bitexact flags, stripped metadata) so the same source always produces byte-identical output, and a guard in figures.mjs that throws if a video-kind map entry ever again resolves to a non-video container. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017g54jmeHWdVo2uJ8vScwD5
Assert no .gitbook/assets path survives conversion when the asset map is present, route the sweep's claimed set through destinationKeyFor, correct CLAUDE.md's stale gotchas and the assets.mjs command description, and record the deferred findings and the corpus-count adjudication procedure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017g54jmeHWdVo2uJ8vScwD5
Carries the look-and-feel scope with its measured specifics: the 41 repeated sidebar parents, the 13 wrapper divs across 9 pages and 31 images, the 13/10/9 card-cover chain, the 64 dropped width attributes, and the progress-bar-message markup decision still awaiting a human answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017g54jmeHWdVo2uJ8vScwD5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FEATURE
Phase 3 of the GitBook → Astro Starlight migration. Moves the 500 referenced GitBook assets into
src/assets/andpublic/, slugified and re-encoded, and makes the page generator emit their real paths from a generatedasset-map.json. Before this branch every image on the migrated site 404'd;src/assetsnow sits at 32.0 MiB against a 60 MB gate.Docs
No Jira ticket — this is migration phase work, following the same branch and PR pattern as #17 (
phase-2/conversion-script).Design —
docs/superpowers/specs/2026-07-30-phase-3-assets-design.mdPlan —
docs/superpowers/plans/2026-07-30-phase-3-assets.mdFindings — the
2026-07-30 — Phase 3 gate: assets completesection ofMIGRATION-NOTES.mdSolution overview
Current behavior
convert.mjsemitted root-absolute placeholders of the form/.gitbook/assets/<original filename>for every image. Nothing resolved them, so every image on the site 404'd. The 1,589 source assets sat only in the git-ignoredsource/tree, 541 MiB of them, unoptimised and unreferenced by the build.Approach in this PR
scripts/assets.mjsowns the physical files and emitsasset-map.json;scripts/convert.mjsconsumes that map exactly as it already consumesroute-map.json.That direction is the load-bearing decision. The brief described
assets.mjsrewriting references in the generated pages after the fact — butconvert.mjsregenerates all 204 pages from scratch and re-runs against fresh GitBook syncs right up to cutover day, so an after-the-fact rewriter would silently revert all 500 asset paths on every run, leaving a green build and a site with no images. One generator, correct on every run.source/alone, never from generated output — a map derived from converted pages would need itself first. 509 references minus 9 covers that conversion drops = 500.scripts/lib/asset-plan.mjsis pure — no filesystem, no image decoding — so every naming and treatment decision is unit-testable. All I/O lives inassets.mjs.<video autoplay loop muted playsinline>. Video and downloads cannot pass throughastro:assets, so they land inpublic/media/andpublic/files/.assetsinsidenpm run convertdoesn't tempt an operator into runningconvert.mjsalone — which is the ordering trap the map exists to prevent.(destination, slug)pair.src/assets/is not script-owned — it holds the site logo referenced fromastro.config.mjs.assetPath's no-map fallback, so a checkout where this phase never ran still builds.Breaking changes
None to URLs, routes or prose.
npm run convertgains anassetsstep and now runsroutes → assets → convert → sidebar.Two behavioural notes for anyone working on the scripts:
EXPECTED.imagesmoved 529 → 528. This is the only expectation adjusted in the phase, and it is single-cause: the corpus's sole video asset now renders as a<video>element rather than a markdown image. Verified three independent ways — a page-only diff of exactly 516 insertions / 516 deletions with nothing added or removed, reference sites totalling 530 before and after, and a direct on-disk count.MAX_WIDTH,QUANTISE_MAX_COLOURSorGIF_VIDEO_THRESHOLDrequires deletingasset-map.jsonfirst, or every asset cache-hits and the new value silently never takes effect. Documented in the code.Dependencies
src/assets/,public/andasset-map.jsonare committed — they are the deliverable.ffmpegis required only when the source GIF's bytes change; the encoded MP4 is committed and the cache keys on content, so ffmpeg is not on the cutover critical path.source/and are restored by putting them back through this same pipeline; the photograph among them must stay full colour. Continuation prompt atdocs/superpowers/PHASE-4-CONTINUATION.md.CLAUDE.mdcorrected in three places — its command description hadassets.mjs"rewriting refs" (the rejected architecture), and two Gotchas asserted things this phase measured false.Verification
npm run convertgreen through all four scripts;npx astro buildsucceeds with 205 pages and 496 optimised image variants.src/assets33,591,322 B = 32.0 MiB against a 60,000,000 gate. Largest file anywhere 1,179,445 B = 1.12 MiB, against Cloudflare's 25 MiB per-file cap.docs.opendialog.aiis untouched and still points at GitBook.Review notes
Every task was reviewed individually and the branch got a final whole-branch review. Between them they found 14 real defects in the plan's never-executed draft code, four of which would have shipped silently with a green build:
survivingAssetPathsassertion.Two further defects were errors in the dispatch instructions rather than the code, caught by reviewers.