Skip to content

feat: detect stale index.html and surface a one-click regenerate#30

Merged
cuio merged 1 commit intomainfrom
feat/studio-assembly-staleness-banner
Apr 30, 2026
Merged

feat: detect stale index.html and surface a one-click regenerate#30
cuio merged 1 commit intomainfrom
feat/studio-assembly-staleness-banner

Conversation

@cuio
Copy link
Copy Markdown
Owner

@cuio cuio commented Apr 30, 2026

Summary

Closes both follow-ups noted at the end of #29:

  1. The my-first-video letter-dropout regression — fixed by re-assembling the user's local project (one-shot, no PR change).
  2. The "auto-regenerate stale renders" UX gap — fixed here.

The assembled index.html drifts in two ways:

  • Source files newer than HTML: user edited script.json, generated music, added an image, etc., and forgot to re-assemble.
  • Core version newer than HTML's stamp: a feature / bug-fix shipped in @hyperframes/core after assembly (the exact case behind the my-first-video letter-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.ts stamps 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.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 + a 16KB head read of the HTML.
  • Two new routes:
    • GET /api/projects/:id/script/assembly-status — polled by the studio.
    • POST /api/projects/:id/script/assemble — re-assembles the existing script.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

  • 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 newer, what core version drifted from). Click → POST → preview iframe reloads via a hf:assembly-regenerated CustomEvent (App.tsx bumps the existing refreshKey).

Tests

  • 18 new pure-helper tests in 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).
  • 2 sanity tests for coreVersion.
  • Full suite green: 765 core (was 745) + 281 studio passing. Lint / format / typecheck clean.

Failure modes

  • HTML missing → stale: true, reasons: ["no-html"] with copy that suggests hyperframes script generate.
  • Stamp missing (legacy HTML pre-this-PR) → reasons: ["no-stamp"]. User clicks Regenerate; subsequent renders carry the stamp and the chip stays hidden.
  • Network error on poll → banner silently hides rather than screams.
  • POST /assemble fails → button copy flips to "Regenerate failed: " in the tooltip; chip stays.
  • Stale script.generated.json (no plan yet) → 400 with a clear "run script generate first" message.

Test plan

  • Open the studio on a project with a fresh index.html → no banner.
  • Touch script.json with an editor → within 30s the chip appears: "1 source file newer than index.html". Click Regenerate → chip clears, preview iframe reloads.
  • Bump @hyperframes/core/package.json version manually and reload the studio → chip says "Core 0.4.27 → X.Y.Z". Click Regenerate → chip clears.
  • Manually delete the HTML → chip says "No index.html". Click Regenerate → fresh HTML lands.
  • Tooltip text matches the underlying reason in every case.

🤖 Generated with Claude Code

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]>
@cuio cuio merged commit f2d7fa0 into main Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant