feat: detect stale index.html and surface a one-click regenerate#30
Merged
feat: detect stale index.html and surface a one-click regenerate#30
Conversation
The assembled index.html drifts in two ways: (a) source files (script, manifests, images, voiceovers) get edited without a re-assemble, and (b) new core features / bug-fixes ship that the rendered HTML doesn't include (this is what caused the my-first-video letter-dropout regression — the HTML was assembled before PR #20). Today the user has no signal that this has happened and silently ships an outdated render. This change makes drift visible and one-click recoverable. Backend - assemble.ts now stamps every produced HTML with two meta tags: hyperframes:assembled-at (ISO timestamp) and hyperframes:core-version (the @hyperframes/core package version that produced the file). - coreVersion.ts walks up to the nearest @hyperframes/core/package.json so the stamp works in source mode (tsx CLI), built mode (tsc → dist), and the studio dev server. Falls back to "0.0.0-dev". - assembleStaleness.ts is the pure decision layer: collects source-files-newer + core-version-changed + no-stamp + no-html signals and returns a typed AssemblyStatus with a human-readable message. No writes, no LLM, ~handful of stat() calls + 16KB head read of the HTML. - New routes: GET /projects/:id/script/assembly-status (poll) and POST /projects/:id/script/assemble (re-assemble without audio re-synth, ~2s on a 25-scene project — mirrors the CLI verb). Frontend - StaleAssemblyBanner mounts in the topbar next to the cost badge. Polls every 30s, hides when up-to-date, shows an amber chip + Regenerate button when stale. Tooltip carries the full reason (which files changed, what core version drifted from). Click → POST → preview iframe reloads via a hf:assembly-regenerated CustomEvent (App.tsx bumps refreshKey). Tests - 18 new tests across pure helpers (extractMetaContent, readStamp, computeAssemblyStatus including missing HTML, no-stamp legacy files, tracked-directory walks, dotfile filtering, multi-reason aggregation, tracked-file cap, custom htmlPath). - Full suite stays green: 765 core (was 745) + 281 studio. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
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.
Summary
Closes both follow-ups noted at the end of #29:
my-first-videoletter-dropout regression — fixed by re-assembling the user's local project (one-shot, no PR change).The assembled
index.htmldrifts in two ways:script.json, generated music, added an image, etc., and forgot to re-assemble.@hyperframes/coreafter assembly (the exact case behind themy-first-videoletter-dropout — assembled before PR feat: reels-grade kinetic templates + premiere-style timeline merge #20).Today the user has no signal either condition has occurred. This change makes drift visible and one-click recoverable.
Backend
assemble.tsstamps every produced HTML with<meta name="hyperframes:assembled-at" …>+<meta name="hyperframes:core-version" …>. Stamp constants are exported so the staleness detector and tests stay in lockstep.coreVersion.tswalks up to the nearest@hyperframes/core/package.jsonso the stamp works in source mode (tsxCLI), built mode (tsc → dist), and the studio dev server. Falls back to0.0.0-dev.assembleStaleness.tsis the pure decision layer: collectssource-files-newer+core-version-changed+no-stamp+no-htmlsignals and returns a typedAssemblyStatuswith a human-readablemessage. No writes, no LLM — ~handful ofstat()calls + a 16KB head read of the HTML.GET /api/projects/:id/script/assembly-status— polled by the studio.POST /api/projects/:id/script/assemble— re-assembles the existingscript.generated.json(no audio re-synth, ~2s on a 25-scene project). Mirrors the existing CLI verb so the studio button reuses the same code path.Frontend
StaleAssemblyBannermounts in the topbar next to the cost badge. Polls every 30s, hides when up-to-date, shows an amber chip + Regenerate button when stale. Tooltip carries the full reason (which files newer, what core version drifted from). Click → POST → preview iframe reloads via ahf:assembly-regeneratedCustomEvent(App.tsxbumps the existingrefreshKey).Tests
assembleStaleness.test.ts(extractMetaContent across quoting + casing, readStamp on missing/stale/legacy HTML, computeAssemblyStatus across all four reasons individually + combined, tracked-directory recursion, dotfile filtering, tracked-file cap, custom htmlPath).coreVersion.Failure modes
stale: true, reasons: ["no-html"]with copy that suggestshyperframes script generate.reasons: ["no-stamp"]. User clicks Regenerate; subsequent renders carry the stamp and the chip stays hidden.script.generated.json(no plan yet) → 400 with a clear "run script generate first" message.Test plan
index.html→ no banner.script.jsonwith an editor → within 30s the chip appears: "1 source file newer than index.html". Click Regenerate → chip clears, preview iframe reloads.@hyperframes/core/package.jsonversion manually and reload the studio → chip says "Core 0.4.27 → X.Y.Z". Click Regenerate → chip clears.🤖 Generated with Claude Code