feat(core): slideshow schema, parser, and lint rule#1580
Conversation
Slideshow manifest types, JSON-island parser/resolver (sceneId->time-range), the slideshow lint rule, and a ./slideshow subpath export. Foundation for the slideshow player and studio editor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This stack of pull requests is managed by Graphite. Learn more about stacking. |
| export function parseSlideshowManifest(html: string): SlideshowManifest | null { | ||
| // Match <script type="application/hyperframes-slideshow+json"> ... </script> | ||
| const re = new RegExp( | ||
| `<script[^>]*type=["']${ISLAND_TYPE.replace(/[.+]/g, "\\$&")}["'][^>]*>([\\s\\S]*?)<\\/script>`, |
- player: bundle @hyperframes/core into the IIFE/global build (noExternal) - player: resolve audience mode from ?mode=audience URL query, not just attr - player: event-driven waitForScenes + loud failure when no slides resolve - player: scope window keydown so Space/Backspace don't hijack the host page - player: audience mirrors full position (branch + fragment) via syncTo - player: next() reveals remaining fragments even at slide end; enterBranch ignores empty sequences - core: harden extractScenes against null/non-object scene entries - core: strict manifest validation; error on inverted ranges & empty hotspot targets; dedup fragments - core/lint: accept data-end/timeline-derived scene durations (match runtime) - core+studio: share ISLAND_TYPE + island regex from @hyperframes/core/slideshow - studio: SlideList reflects manifest slide order; branch-slide authoring (notes/fragments/hotspots) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
miga-heygen
left a comment
There was a problem hiding this comment.
Review: feat(core): slideshow schema, parser, and lint rule
Solid foundation PR. The "slides as metadata over existing scenes" design is clean — embedding the manifest as a typed JSON island inside the composition HTML avoids a new file format and keeps slides purely additive. The parser, resolver, and lint rule are well-structured and test coverage is thorough. A few things worth discussing before this merges as the base of a 10-PR stack:
1. Schema — types look good, one design question
The types are well-defined and properly constrained. SlideRef → ResolvedSlide progression is clean: optional fields become required, arrays get defaults. The extends SlideRef on ResolvedSlide is the right call — it means downstream consumers always get the superset.
Question on SlideHotspot.region: The { x, y, w, h } comment says "% of slide" — should this be enforced (0-100 range validation) in the resolver, or is that intentionally deferred to the player PR? If deferred, a // TODO: validate bounds in player comment would help, since this type will be the contract for 4 downstream PRs.
TTS fields: Flagging ttsScript/ttsAudioUrl/ttsDurationMs as "reserved — parsed and carried, never consumed" is the right approach. Consider adding @experimental or @reserved JSDoc tags so IDE tooltips surface this intent — right now it's only documented in the PR description, which evaporates from working memory fast.
2. Parser — solid, one edge case
Regex extraction (parseSlideshowManifest): The regex approach is consistent with how the linter extracts scripts elsewhere. The escaping of . and + in the MIME type is correct.
Edge case — multiple islands: RegExp.exec with no g flag returns the first match. If someone accidentally pastes two <script type="application/hyperframes-slideshow+json"> blocks, the second is silently ignored. Worth either (a) erroring on >1 match, or (b) documenting single-island-only as an intentional constraint. Given this is the foundation PR, establishing this invariant now prevents confusion in downstream PRs.
isManifest validator: This only checks typeof === "object" and Array.isArray(slides). It does not validate that each element of slides is a valid SlideRef (e.g., has a sceneId string). A manifest like {"slides": [42, null, "oops"]} passes isManifest and then blows up inside resolveSlide with an unhelpful error. Consider at minimum checking slides.every(s => typeof s?.sceneId === 'string').
3. Resolver — clean logic, two notes
Overlap detection: The adjacent-pair comparison after sorting by start is correct for detecting overlaps. One nuance: if two slides share the exact same start, the sort is not stable on end, so which one is "prev" vs "curr" is arbitrary. This doesn't affect correctness (they overlap either way), but the error message might name them in a surprising order. Not blocking, just noting.
Fragment validation boundary condition: f < start || f > end — a fragment exactly at end is allowed. Is that intentional? Fragments at the exact boundary of a slide's end time are technically at the transition point. If fragments represent "pause points within a slide," having one at the very last moment seems odd. If it's intentional, a comment clarifying the inclusive-end semantics would help.
4. Lint rule — well-integrated, one concern
Scene extraction uses data-composition-id only (not .clip[id]): This is explicitly tested and correct per the PR description — it mirrors the runtime's scene source. The test at line 29-35 of slideshow.test.ts validating that .clip[id] does NOT resolve is a great inclusion.
Concern — isSceneLikeCompositionId is duplicated: The comment says it "mirrors" packages/core/src/runtime/timeline.ts, but it's a full copy. If the runtime's heuristic changes (e.g., a new exclusion pattern), the lint rule silently diverges. Options: (a) import the canonical function, (b) add a // SYNC: comment pointing to the source of truth, (c) extract to a shared util. For a foundation PR, at minimum (b) would be good.
5. API surface
The ./slideshow subpath export is the right call — keeping the parser importable without pulling in the full core barrel is good for downstream bundle size. The re-export from src/index.ts provides convenience for consumers who already depend on core.
6. Security
No injection risk. The manifest is parsed from HTML the author controls, validated through JSON.parse (which is safe), and the regex only extracts content between matched script tags. No eval, no dynamic code execution, no user-supplied HTML interpretation. Clean.
7. Minor nits
slideshow.tsline 1:// fallow-ignore-file code-duplication— is "fallow" the project's lint-ignore directive? Just making sure this isn't a typo for "eslint-disable" or similar.- The
parseTiminghelper in the lint rule re-implements timing extraction thatresolveTimeRangealso handles. Not a problem now, but as the stack grows, consider whether the lint rule should delegate more to the parser rather than re-deriving scene data.
Verdict
This is a well-designed foundation. The schema is minimal and extensible, the parser handles edge cases thoughtfully (partial overrides, missing scenes), the lint rule integrates cleanly with the existing linter architecture, and test coverage is comprehensive (13 test cases covering happy paths and all error variants).
The isManifest shallow validation and the duplicated isSceneLikeCompositionId are the two items I'd want addressed before this merges — everything else is nits or "consider for a follow-up."
Not stamping — leaving for the author to address the above, then happy to re-review.
Review by Miga
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 778e53500acdeeb51fddfbd9f43f514127fede91.
Verdict: clean foundation design — schema/parser/resolver shape is right, test coverage on resolve paths is good. Layering onto Miga's review (who covered isManifest shallow validation, isSceneLikeCompositionId drift, multi-island silent ignore, fragment boundary, region bounds, TTS doc tags). A few things they didn't surface that matter for this being the base of a 10-PR stack.
🛑 Blockers
None.
⚠️ Concerns
1. Lint findings carry no location info — observability gap for the rule's whole job.
packages/core/src/lint/rules/slideshow.ts:55-78 — every finding the slideshow rule emits has only code + message + fixHint. No elementId, no selector, no snippet. Every other rule in packages/core/src/lint/rules/ populates at least one of these (composition.ts:128-136, core.ts, media.ts, gsap.ts, textures.ts all do). When a hyperframes lint user gets Slideshow manifest error: slide references unresolved sceneId "ghost", they don't know whether the offender is in the main-line slides array or in slideSequences[2].slides[4]. For a manifest with 20 slides across 3 sequences, this turns a one-line fix into a hunt. Suggest: thread a path-like locator (slides[3] / slideSequences["deep"].slides[1]) through resolveSlide errors, and populate the island's <script> tag location as snippet (extract via ctx.scripts.find(s => /type=["']application\/hyperframes-slideshow\+json["']/.test(s.attrs))).
2. Duplicate sequence id silently overwrites — wire-contract bug for hotspots.
parseSlideshow.ts:117-123 — for (const seq of manifest.slideSequences ?? []) { sequences[seq.id] = ... }. If two sequences share an id, the second wins, and any earlier hotspots that targeted that id now resolve to the wrong branch with no error. Since hotspot target → sequence is the only navigation primitive in this schema, a silent collision corrupts the deck. Three-line fix:
if (sequences[seq.id]) {
errors.push(`duplicate slideSequence id "${seq.id}"`);
continue;
}
sequences[seq.id] = { ... };
3. Lint rule re-parses ctx.source instead of using the centralized ctx.scripts extractor.
packages/core/src/lint/rules/slideshow.ts:53 calls parseSlideshowManifest(ctx.source), which builds its own regex against the full HTML source. But ctx.scripts already contains every <script> block extracted by the centralized SCRIPT_BLOCK_PATTERN in packages/core/src/lint/utils.ts:21. Two failure modes from running parallel extractors:
- The two regexes can diverge subtly (the central one uses
<script\b([^>]*)>, the new one uses<script[^>]*type=["']…["'][^>]*>). A script tag whose attribute is unquoted (type=application/hyperframes-slideshow+json) matches the central extractor but not the new one. - When Miga's "multiple islands" concern gets addressed, the right loop body is
for (const block of ctx.scripts.filter(s => …))— that's a free fix if you migrate toctx.scriptsnow.
Suggest: filterctx.scriptsby MIME type in the rule, JSON.parse thecontent, and drop the duplicate regex fromparseSlideshowManifestentirely (or keep it as the non-lint extractor for direct consumers).
4. ISLAND_TYPE MIME constant duplicated across files.
parseSlideshow.ts:10 defines ISLAND_TYPE = "application/hyperframes-slideshow+json". The same string appears verbatim in lint/rules/core.ts:167 (the exemption regex), lint/rules/slideshow.ts:60 (the fix-hint message), all four test files (literal strings in HTML fixtures), and the file-level comment block. With 4 downstream PRs (player, studio, skill, examples) about to import this layer, the chance that someone tweaks the MIME type in one place and not another goes up. Suggest exporting ISLAND_TYPE from slideshow.types.ts (or a new slideshow/constants.ts) and importing it everywhere — including the regex literal in core.ts:167 (use new RegExp against the imported constant).
💡 Nits
5. Empty-slides manifest silently passes.
parseSlideshow.ts:125 — manifest.slides.map(...) = [], no errors. A manifest with {"slides": []} (or one whose only slides got removed) lints clean but is nonsense for a slideshow. Probably fine to leave for the player layer to handle ("0-slide composition = no slideshow surface"), but if resolveSlideshow.errors is meant to be the exhaustive validation gate before player accepts a manifest, worth a slides array is empty finding here.
6. Sequence slides aren't overlap-checked.
parseSlideshow.ts:138-145 overlap check applies only to slides (main-line), not slideSequences[*].slides. Reasonable — sequences are branches that may interleave with main-line time — but worth a one-line comment clarifying intent so the player author doesn't assume sequences are also non-overlapping.
7. // fallow-ignore-file code-duplication at slideshow.ts:1.
Effectively asking the repo's Fallow audit CI (which is passing) to skip flagging the isSceneLikeCompositionId copy. That's the right call short-term, but it means Miga's "SYNC comment" suggestion is doubly important — the fallow-ignore explicitly suppresses the one signal that would catch divergence.
✅ Looks good
- The
./slideshowsubpath export design — agreed with Miga, keeps player/studio bundles thin. resolveTimeRangepartial-override semantics + tests atparseSlideshow.test.ts:80-129are exemplary; the "no scene + only one bound" error path is exactly the kind of nuance authors hit.- TTS reservation as parse-and-carry — defers cleanly without polluting the resolver.
core.ts:167MIME exemption from inline-script-syntax check — necessary and minimal.- 13 tests covering happy path + every error variant. Solid.
Stack-coherence note
This is the foundation 4 downstream PRs import from. Concerns 1 (location info), 2 (sequence id collision), and 4 (MIME constant) all get harder to fix once player/studio start importing ResolvedSlideshow.sequences / parseSlideshowManifest. Cheap to address now, expensive later.
Not stamping — Miga's concerns + the above. Happy to re-review.
Review by Rames D Jusso
Addresses review feedback on the split stack: - CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once per document). No inline event handlers → works under strict CSP. - IDs: studio sequence/hotspot id generation used Date.now() (sub-ms collision on rapid clicks) — now crypto.randomUUID(). - Versioning: stamp version on the persisted manifest island (preserving an existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION to the core schema so future schema changes can migrate older islands. These live on the review-fixes tip (consistent with the stack's fixup-on-tip model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b (#1591/#1592), and ss-core (#1580). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- player: bundle @hyperframes/core into the IIFE/global build (noExternal) - player: resolve audience mode from ?mode=audience URL query, not just attr - player: event-driven waitForScenes + loud failure when no slides resolve - player: scope window keydown so Space/Backspace don't hijack the host page - player: audience mirrors full position (branch + fragment) via syncTo - player: next() reveals remaining fragments even at slide end; enterBranch ignores empty sequences - core: harden extractScenes against null/non-object scene entries - core: strict manifest validation; error on inverted ranges & empty hotspot targets; dedup fragments - core/lint: accept data-end/timeline-derived scene durations (match runtime) - core+studio: share ISLAND_TYPE + island regex from @hyperframes/core/slideshow - studio: SlideList reflects manifest slide order; branch-slide authoring (notes/fragments/hotspots) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on the split stack: - CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once per document). No inline event handlers → works under strict CSP. - IDs: studio sequence/hotspot id generation used Date.now() (sub-ms collision on rapid clicks) — now crypto.randomUUID(). - Versioning: stamp version on the persisted manifest island (preserving an existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION to the core schema so future schema changes can migrate older islands. These live on the review-fixes tip (consistent with the stack's fixup-on-tip model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b (#1591/#1592), and ss-core (#1580). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- player: bundle @hyperframes/core into the IIFE/global build (noExternal) - player: resolve audience mode from ?mode=audience URL query, not just attr - player: event-driven waitForScenes + loud failure when no slides resolve - player: scope window keydown so Space/Backspace don't hijack the host page - player: audience mirrors full position (branch + fragment) via syncTo - player: next() reveals remaining fragments even at slide end; enterBranch ignores empty sequences - core: harden extractScenes against null/non-object scene entries - core: strict manifest validation; error on inverted ranges & empty hotspot targets; dedup fragments - core/lint: accept data-end/timeline-derived scene durations (match runtime) - core+studio: share ISLAND_TYPE + island regex from @hyperframes/core/slideshow - studio: SlideList reflects manifest slide order; branch-slide authoring (notes/fragments/hotspots) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on the split stack: - CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once per document). No inline event handlers → works under strict CSP. - IDs: studio sequence/hotspot id generation used Date.now() (sub-ms collision on rapid clicks) — now crypto.randomUUID(). - Versioning: stamp version on the persisted manifest island (preserving an existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION to the core schema so future schema changes can migrate older islands. These live on the review-fixes tip (consistent with the stack's fixup-on-tip model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b (#1591/#1592), and ss-core (#1580). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(slideshow): address code-review findings #1580-1584 - player: bundle @hyperframes/core into the IIFE/global build (noExternal) - player: resolve audience mode from ?mode=audience URL query, not just attr - player: event-driven waitForScenes + loud failure when no slides resolve - player: scope window keydown so Space/Backspace don't hijack the host page - player: audience mirrors full position (branch + fragment) via syncTo - player: next() reveals remaining fragments even at slide end; enterBranch ignores empty sequences - core: harden extractScenes against null/non-object scene entries - core: strict manifest validation; error on inverted ranges & empty hotspot targets; dedup fragments - core/lint: accept data-end/timeline-derived scene durations (match runtime) - core+studio: share ISLAND_TYPE + island regex from @hyperframes/core/slideshow - studio: SlideList reflects manifest slide order; branch-slide authoring (notes/fragments/hotspots) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(player): slideshow fullscreen + presenter-view rework - fullscreen toggle in the nav chrome (button + 'F' key); standard Fullscreen API on the <hyperframes-slideshow> element, icon reflects state - presenter console: live slide on top, speaker-notes panel below, with the nav controls shown in-view; Present button hides once presenting (harness) - audience (viewer) window: chrome reduced to a fullscreen-only control, no nav - fix: audience / back() / backToMain() mirror stayed frozen on the first frame — a bare paused seek does not repaint some compositions. resumeSlide now plays a brief render-nudge (RENDER_NUDGE) past the target so the composition paints, then onTime pauses at the hold - refactor: extract reusable buildNavCluster() + wireChromeButtons(); rework buildPresenterLayout into the bottom notes panel - example: airbnb-deck presenter-test.html harness (Present button + 'F') Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(player): slideshow no auto-progress + presenter slide fits/pins - navigation jumps to a static frame instead of auto-playing the timeline: playTo() seeks to the hold (+ a brief RENDER_NUDGE to repaint) rather than sustaining playback, so slides hold until the user advances - presenter view: pin the live slide to the top and confine the player to the region above the notes panel, so the player CONTAINS the composition — the full slide stays visible (letterboxed) at any width and re-fits on resize; its bottom is no longer cut off by the notes panel - tests: seek targets updated for the render-nudge offset Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slideshow): presenter nav flash, slide-1 boundary, branch buttons Three presenter-mode fixes from testing the airbnb deck: (1) navigation flash — seek to the exact target then play forward to repaint, instead of seeking backward (t-0.2) which painted the previous scene at boundaries; split hold into holdTarget (logical) and holdAt (target+nudge, clamped to slide.end). (2) slide-1 boundary — no-fragment slides rest at the slide midpoint, not slide.end. (3) presenter branch buttons — surface hotspots as buttons in the presenter console (the on-slide pill is lost in the letterboxed view). Also extract paintChrome() to dedupe the three chrome-render sites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slideshow): stop presenter nav buttons flickering / dropping clicks The presenter elapsed clock called render() every second, which rebuilt the entire chrome (innerHTML) including the nav buttons — they flickered and any click landing mid-rebuild was lost. The 1s tick now updates only the elapsed text node; the nav buttons are rebuilt only on actual navigation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slideshow): CSP-safe nav hover, UUID editor ids, manifest version Addresses review feedback on the split stack: - CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once per document). No inline event handlers → works under strict CSP. - IDs: studio sequence/hotspot id generation used Date.now() (sub-ms collision on rapid clicks) — now crypto.randomUUID(). - Versioning: stamp version on the persisted manifest island (preserving an existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION to the core schema so future schema changes can migrate older islands. These live on the review-fixes tip (consistent with the stack's fixup-on-tip model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b (#1591/#1592), and ss-core (#1580). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(ci): fix format + fallow gates for slideshow stack - .prettierignore: exclude generated demo compositions (registry/examples/**/*.html) from oxfmt — large video-pipeline output (GSAP/Three/WebGL), not hand-authored source. Was failing 'Format' repo-wide (pre-existing on main via #1584). - .fallowrc: exempt SlideshowPanel.tsx (health/complexity — section fan-out) and the slideshowPanelHelpers.ts / SlideshowPanel.test.ts parallel-structure clones (duplicates.ignore). File-level config, not inline comments — inline shifts line numbers and breaks fallow's inherited-finding fingerprint (per existing rc note). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slideshow): address PR review + CodeQL findings - CodeQL #638 (parseSlideshow): complete the regex metachar escape in slideshowIslandRegex (was missing backslash); add JSDoc on the factory + lastIndex caveat (reviewer 5a/16). - CodeQL #639/#640 + review items 13/17: remove registry/examples/airbnb-deck/ presenter-test.html — a generated test harness (postMessage w/o origin check, proto-pollution) that was scope-creep into a fix PR and a 3rd duplicate island. Regenerate locally via the scratchpad script when testing. - Review item 15 (docs drift in skills/slideshow/SKILL.md): lint resolves scenes by data-composition-id only (not .clip[id]); fragments are valid INCLUSIVE of [start,end], not 'strictly inside'. IIFE bundles core confirmed (0 external @hyperframes/core refs in the slideshow global build). format/lint/fallow green. * feat(cli): add 'present' command — serve a deck in presenter mode hyperframes present [dir] starts a lightweight HTTP server, wraps the composition in <hyperframes-slideshow> with its island inlined, and opens the browser. A real HTTP origin is required for presenter mode: present() opens the audience window via window.open(?mode=audience) and the two sync over BroadcastChannel — neither works from file://. - New utils/compositionServer.ts factors the server scaffolding shared with 'play' (resolve runtime/player/slideshow bundles, inject runtime, asset content-types, bind to a free port); play.ts now uses it too. - Errors clearly if the deck has no slideshow island. - .fallowrc: exempt the play/present command entrypoints (validation + server wiring) and the per-command startup/logging block from the complexity / duplication gates. Verified end-to-end against registry/examples/airbnb-deck: server serves the wrapper + assets, the component binds and renders (counter 1 / 11). * fix(cli): present renders the deck (player sizing + self-driving serve) Two bugs caused a black slide area: - The <hyperframes-player> had no positioning, so its iframe collapsed to zero size — the (absolutely-positioned) chrome showed but the composition didn't. Add position:absolute; inset:0 (matches demo.html). - The composition was served with the engine runtime injected, which leaves its timelines engine-paused (blank). Slideshow decks self-drive their own timelines (like demo.html / the standalone harness), so serve them raw. Verified end-to-end on registry/examples/airbnb-deck: cover renders, Next advances 1/11 -> 2/11 and slide 2 paints. * fix(cli): present plays slideshow sound effects The composition (in the player's sandboxed iframe) posts { type: 'hf-sfx', name } to the parent on nav, but the iframe is autoplay-blocked — audio must play in the parent that owns the user gesture. Add the parent-side hf-sfx handler (the 4 standard clips advance/fragment/ branch-enter/back, served from the deck's sfx/ under /composition/sfx/), gesture-unlocked and mute-aware, in both presenter and audience windows. Verified: sfx serve 200 (audio/mpeg) and Next delivers [advance, fragment] to the parent handler. * feat(examples): softer mellow slideshow sfx for airbnb-deck Replace the aggressive percussive pops with gentle sine-tone cues (warm pitches C5/G4/E5/F4, 12ms attack + exponential decay, lowpassed) — advance/ fragment/branch-enter/back. Much lighter; fragment is the most subtle. * feat(examples): whoosh + sparkle slideshow sfx for airbnb-deck Replace the sine-tone cues with airy, designed sounds: - advance: a soft whoosh (band-limited pink noise, bell-shaped swell) - back: that whoosh reversed and darkened - fragment: a light sparkle (staggered high chime blips) - branch-enter: whoosh + a trailing sparkle (magical entry) * feat(examples): directional whoosh + richer branch-enter cue (airbnb-deck) - Going backward a slide now plays the reverse whoosh (back), not advance — the sfx logic detects nav direction by scene order instead of firing advance for every scene change. - branch-enter is now a more interesting magical cue: a faint whoosh + an ascending C5-E5-G5-C6 chime arpeggio + a trailing sparkle. Verified: next then prev fires [advance, fragment, back]; no page errors. * fix(cli): harden present sfx handler + mute-hover affordance (R2 review) Addresses Rames R2 items 19-21: - 20: the present audio handler reintroduced the CodeQL classes removed with presenter-test.html — add an origin check (same-origin composition iframe) and an own-property guard so a 'name' like __proto__ can't resolve to and mutate Object.prototype. - 21: assetContentType used a bare index lookup (ext='__proto__' -> prototype); guard with Object.hasOwn. - 19: the CSP hover rule erased the speaker button's muted color; add a higher-specificity [data-hf-muted] [data-hf-mute]:hover override. Verified: hf-sfx origin matches location.origin (guard passes), advance/fragment still fire, deck renders + advances. Items 14/18/22 deferred (minor, pre-existing). * fix(slideshow): address remaining R2 items (14/18/22) + re-remove harness - 14: resumeSlide now mirrors enterSlide — a no-fragment slide resumes at its midpoint (visible-at-rest), not frame-0; fragmented slides still resume to the saved fragment or slide.start. Added a dedicated test naming the heuristic. - 18: fullscreenchange swaps only the fullscreen glyph + aria (hoisted SVGs to module consts) instead of re-rendering the whole chrome. - 22: .prettierignore lists the specific generated demo compositions instead of blanket registry/examples/**/*.html, so hand-authored example HTML still formats. - presenter-test.html: a stray 54a4460 git add -A had re-added the deleted harness (reviving CodeQL #639/#640); remove it again. 106 slideshow tests pass; tsc/lint/fallow/format clean; deck still renders. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
miga-heygen
left a comment
There was a problem hiding this comment.
Re-review: checking prior findings
Re-reviewing against 778e535 — the same (and only) commit. No follow-up pushes since the original reviews from Miga and Rames.
Status of previously flagged items
1. Shallow isManifest() validation — STILL OPEN
parseSlideshow.ts isManifest() still only checks typeof === "object" and Array.isArray(slides). A manifest like {"slides": [42, null, "oops"]} passes validation and then blows up inside resolveSlide with an unhelpful error (trying to read .sceneId off a number). Minimum fix: slides.every(s => typeof s?.sceneId === 'string').
2. Duplicated isSceneLikeCompositionId — STILL OPEN
lint/rules/slideshow.ts still contains a full copy of isSceneLikeCompositionId from runtime/timeline.ts. The comment says "Mirrors" but there's no sync mechanism — no import, no // SYNC: marker, no shared util. The fallow-ignore-file code-duplication directive suppresses the one CI signal that would catch divergence. At minimum, add a // SYNC: runtime/timeline.ts:isSceneLikeCompositionId comment so grep can find both copies. Extracting to a shared util would be better, especially with 4 downstream PRs about to build on this.
3. Sequence-id collision — STILL OPEN (originally flagged by Rames)
parseSlideshow.ts:117-123 still does sequences[seq.id] = { ... } with no collision check. Duplicate sequence IDs silently overwrite earlier entries, which means hotspots targeting that ID resolve to the wrong branch with no error. Three-line fix:
if (sequences[seq.id]) {
errors.push(`duplicate slideSequence id "${seq.id}"`);
continue;
}Summary
All three items from the prior round remain unaddressed — no new commits have been pushed since the original reviews. Waiting on a follow-up push before re-evaluating.
Re-review by Miga

Slideshow mode — 1/5: core schema, parser & lint
Foundation for slideshow mode: a composition can declare an embedded slideshow manifest that turns its continuous timeline into a discrete, navigable deck. This PR adds the data model, parser, and validation — no runtime/UI yet.
What & why
A slide is just an existing scene (
data-composition-id+data-start/data-duration) plus metadata declared in one embedded<script type="application/hyperframes-slideshow+json">island. Keeping the manifest in the composition means no new file format and no build step — slides are additive metadata over a normal composition.Key changes
slideshow/slideshow.types.ts—SlideshowManifest,SlideRef,SlideHotspot,SlideSequenceand their resolved counterparts. TTS fields (ttsScript/ttsAudioUrl/ttsDurationMs) are present but reserved (playback not built).slideshow/parseSlideshow.ts—parseSlideshowManifest(html)extracts the island;resolveSlideshow(manifest, scenes)resolves eachsceneIdto a{start,end}range (honouring optionalstartTime/endTimeoverrides) and returns validation errors for: unresolved sceneId, fragment outside a slide's range, hotspot targeting an unknown sequence, and overlapping main-line slides.lint/rules/slideshow.ts— surfaces those resolve errors underhyperframes lint; derives scenes fromdata-composition-id(matching the runtime's scene source).lint/rules/core.ts— exempts the slideshow island MIME type from the inline-script-syntax check../slideshowsubpath export (dev + publishConfig) so downstream packages import only the lightweight parser, keeping core's Node-only barrel out of their typecheck graph.Testing
parseSlideshow.test.ts+slideshow.test.ts(vitest) cover parse, resolution, every error path, and the lint rule.Stack
Bottom of a 5-PR stack: core → player (#1581) → studio (#1582) → skill (#1583) → examples (#1584).
🤖 Generated with Claude Code