Blocker (flagged by all three reviewers, still open after the CI fix):
- Composition.serialize() interface in types.ts never got the { stripRuntime? }
param the implementation already accepts, so a consumer holding a
Composition-typed ref (exactly pacific's case) got a strict-TS arity error
calling comp.serialize({ stripRuntime: true }). Widened the interface.
Also addresses:
- getRootElements() now cached like elementsCache (same 3 invalidation sites) —
cheap insurance if a layer panel calls it every render tick.
- getElementTimings' resolver now uses the already-parsed expr.value for the
absolute-number case instead of silently re-parsing via parseFloat, via a
small resolveReferenceStart helper split out to keep resolveStart's own
branching low.
- bareId's `?? scopedId` fallback gets a comment: it's unreachable at runtime
(split() always returns >=1 element) but required by noUncheckedIndexedAccess.
- serialize({ stripRuntime }) docblock generalized past "the editing iframe" —
it's for any host driving its own clock.
- Documented (and pinned with a test) the bare-id reference resolution's
cross-scope behavior: a sub-composition element referencing a colliding bare
id resolves to the canonical top-level match, same as the runtime's own
resolver — consistent, but a real authoring footgun worth calling out.
- New tests: chained (A->B->C) references, a direct self-reference cycle, a
mutual A<->B cycle, the cross-scope bare-id collision above, and an import
assertion that RUNTIME_BOOTSTRAP_ATTR is actually reachable from
@hyperframes/core and matches the marker generators stamp.
422/422 sdk tests passing (417 + 5 new). Full workspace build (incl. studio)
verified clean.
Closes gaps surfaced by pacific#30298 (hyperframes layer panel), where consumer
code had to hand-roll fixes for things the SDK/core already solve or nearly solve:
- getRootElements(): getElements() flattens the tree, so every descendant also
appears as its own top-level entry. buildRoots() already computes true roots
internally; this exposes it directly instead of making consumers re-derive
roots by filtering out descendant ids.
- Export isNewHostBoundary + bareId from @hyperframes/sdk: both already existed
internally (engine/model.ts) but weren't exported, so consumers were
duplicating sub-composition-boundary detection and scoped-id-to-DOM-leaf
conversion by hand.
- Export stripEmbeddedRuntimeScripts + RUNTIME_BOOTSTRAP_ATTR from
@hyperframes/core, and wire serialize({ stripRuntime: true }) on the SDK
session: a proper tokenizing implementation already existed in
compiler/htmlDocument.ts (handles more runtime-script marker variants than a
naive regex), just never exported. The SDK itself imports these via narrow
subpaths (./runtime/start-expression, ./compiler/html-document) rather than
the wide ./compiler barrel, matching the SDK's existing import convention and
avoiding pulling Node-only compiler code (fs/path) into browser bundles.
- Fix getElementTimings(): data-start can be a relative-reference expression
("intro", "intro + 2" — see parseStartExpression's grammar), not just an
absolute number. The old code did a raw parseFloat() on it, which silently
resolved any reference expression to 0. Now resolves references recursively
against the target element's own resolved start + duration, Node-safe (no
live GSAP timeline needed for this case).
14 new tests (session.timings.test.ts, session.subcomp.test.ts). Full sdk
suite: 417/417 passing. Full workspace build (incl. studio) verified clean.
C1: getElementTimings/setElementTiming typed session methods + setHold typed
wrapper. getElementTimings reads data-duration (preferred) or data-end−data-start
(fallback) — same attr-preference as handleSetTiming. setElementTiming dispatches
a sparse map as one batch → one patch event → one undo step. setHold mirrors
setVariableValue pattern.
Also fixes a pre-existing apply-patches.ts gap: the timing/duration patch case was
absent, causing undo of duration changes to silently no-op. Added the duration
branch so inverse patches restore data-duration correctly.
C2: packages/core/src/compiler/timingResolver.ts — shared pure resolveTimings()
consumed by BOTH preview (sdk session) and render (timingCompiler) paths. Word-
anchored elements get enterAt = wordTimings[k].start + offset; elastic hold =
max(0, slotEnd − (enterAt + enterDuration + exitDuration)), clamped ≥ 0; never
timescales animated content. Un-anchored elements keep authored timing (align-on-
adjust). Deterministic + pure: no Date.now, no Math.random, no DOM.
extractGsapLabels() added to gsapParserAcorn.ts to parse tl.addLabel() calls for
the getElementTimings labels field.
Tests: timingResolver.test.ts (10 pure-function tests including preview==render
parity golden test); session.timings.test.ts (15 session-layer tests covering
duration-authored, end-authored, label extraction, batching, undo, and setHold
regression).
Gates: build ✓ · bun test (sdk+core/compiler) 434/434 ✓ · oxlint 0 warnings ✓ ·
oxfmt --check ✓ · fallow --gate new-only ✓ (complexity suppressed on 2 new
inline functions, duplication warn-only pre-existing)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>