Commit Graph
701 Commits
Author SHA1 Message Date
Miguel Ángel 3bcab3dc29 fix(studio): reject unsafe keyframe values (#1389) 2026-06-12 22:48:19 -04:00
James RussoandClaude Opus 4.8 ab7f69c1f5 test(core): align file-tree test with backup-only hiding (#1366) (#1400)
main went red again at e2cc134c: my #1399 fix branched off the pre-#1366
state (where `.hyperframes` was wholesale-hidden via IGNORE_DIRS) and, when
it merged on top of #1366, overwrote #1366's corrected test with an
assertion that `.hyperframes/examples` is hidden.

#1366 is the authoritative behavior: walkDir now hides only
`.hyperframes/backup` (shouldIgnoreDir), so `.hyperframes/examples` — like
any other vendored dot-dir — stays visible in the file tree and is gated out
of composition discovery by isInHiddenOrVendorDir. That is the original #1384
intent.

Correct the file-tree test to match:
- `.cache/examples/preset.html` and `.hyperframes/examples/preset.html` are
  both visible in `files` (kept the `.cache` case from #1399 — it exercises
  isInHiddenOrVendorDir gating for a non-special dot-dir).
- `.hyperframes/backup/snapshot.html` is the only thing hidden from the tree.
- Compositions still exclude every dot-dir example.

Full non-producer suite green; walkDir "hides backups" test untouched.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 19:44:26 -07:00
James RussoandClaude Opus 4.8 e2cc134c77 test(core): fix contradictory composition-discovery file-tree test (#1385) (#1399)
#1385 ("exclude dot-directories from composition discovery", b952dc9c)
merged with a failing test, leaving main red. Its commit message assumed
"walkDir only skipped three exact names (.thumbnails, node_modules, .git)",
but `.hyperframes` had already been added to walkDir's IGNORE_DIRS by the
backup feature (with its own passing "hides internal backup files" test).

So the new test "keeps dot-directory files visible in the file tree" used
`.hyperframes/examples/preset.html` — the one dot-dir that walkDir hides —
and asserted it appears in `files`, which can never hold: `files = walkDir(...)`
filters `.hyperframes`. The implementation is coherent; the test picked the
wrong fixture and never exercised the isInHiddenOrVendorDir gating it meant to.

Fix the fixtures (test-only, no production change):
- Add a genuinely-vendored dot-dir `.cache/examples/preset.html` — walkDir does
  not special-case it, so it stays in the file tree but must be gated out of
  composition discovery by isInHiddenOrVendorDir. This is what #1385 actually
  targets, now properly exercised.
- Keep `.hyperframes/examples/preset.html` and assert it is hidden from the file
  tree (IGNORE_DIRS) — documenting the deliberate divergence so the two features
  (Studio-internal backups vs. browsable vendored dot-dirs) don't collide again.

Full non-producer suite green; the walkDir "hides backups" test is untouched.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 19:13:18 -07:00
Miguel Ángel 5f12e692d5 fix(studio): save retries, mutation queue circuit breaker, save_failure diagnostics (#1366)
* fix(studio): save retries, mutation queue circuit breaker, save_failure diagnostics

Save failures could silently drop user work: code-editor saves fired a single
PUT with no retry, DOM-edit failures drained the whole queue against a failing
server, and several failure paths only logged to the console.

- Retry code-editor saves with exponential backoff instead of dropping the
  edit on the first failed PUT.
- Circuit breaker on the DOM-edit save queue: a failing server pauses the
  queue with a user-visible error state instead of burning every queued
  mutation against it.
- save_failure events now carry error_message, status_code, and source on
  every emission path; style/attribute DOM-edit failures that previously only
  logged to the console now emit telemetry too.
- Route unawaited commitMutation call sites (GSAP drag, property scrubbing,
  undo/redo, text fields) through a safe wrapper that reports failures via
  telemetry instead of unhandledrejection.

Follow-ups (deferred): version/ETag conflict guard on file PUTs, offline
save queue.

* fix(studio): narrow save retry changes for fallow
2026-06-12 22:02:17 -04:00
Leonel Rivas b952dc9ce0 fix(core): exclude dot-directories and node_modules from studio composition discovery and lint (#1385)
Projects that vendor tooling assets under dot-directories ended up with
every example/preset HTML inside them listed and preview-rendered in the
comps sidebar, and the studio Lint badge inflated with findings from
files that are not part of the video. walkDir only skipped three exact
names (.thumbnails, node_modules, .git), so any other dot-directory
(.hyperframes/, .cache/, ...) was walked.

Add an isInHiddenOrVendorDir helper that rejects paths with a
dot-directory or node_modules segment and apply it to composition
discovery and the studio lint route. The file tree is deliberately left
unfiltered - this only gates discovery.

Fixes #1384
2026-06-12 17:51:03 -07:00
Miguel Ángel aec3c3b58c fix(studio): journal source writebacks (#1388) 2026-06-12 20:40:12 -04:00
Manu PareekandCursor 7fa3696101 fix(runtime): respect hidden ancestor clips in Studio preview (#1387) (#1395)
* fix(runtime): respect hidden ancestor clips in Studio preview (#1387)

Studio-stamped GSAP tween targets inside timed clips were getting
visibility:visible for the full composition, overriding hidden parent
panels. Skip stamping descendants of authored clips and suppress
visibility on children when an ancestor timed clip is hidden.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(runtime): scope ancestor visibility walk to Studio iframe only

Address review feedback: the hierarchical visibility guard now runs only
when window.parent !== window, matching the Studio-only stamping fix.
Render mode keeps prior per-element visibility semantics. Adds a render-mode
regression test and documents the null rootComp case in findTimedClipAncestor.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 20:28:31 -04:00
Matt Van HornandMatt Van Horn 28e2ab9d5b fix: address review feedback from #1333 and #1335 (#1343)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-12 16:18:18 -07:00
Vance IngallsandClaude Fable 5 7a99ccec6d fix(core): honor root data-duration when GSAP timeline ends short (#1378)
* fix(core): honor root data-duration when GSAP timeline ends short

The authored-duration floor only counted child composition clips, never
the root element's own data-duration. A composition whose GSAP timeline
ended even 0.1s short of its declared data-duration reported the shorter
timeline length from player.getDuration() — and the studio's adapter
selection (docDuration <= adapterDur) then silently rejected the
audio-capable runtime player, downgrading preview playback to the
seek-scrubbing adapter, which never starts media elements or WebAudio.
Result: total audio silence with zero errors anywhere.

- include the root's declared data-duration in
  resolveAuthoredCompositionDurationFloorSeconds, making data-duration
  the source of truth for playable length (per the documented contract)
- console.warn in the studio when playback falls back to the
  seek-driven adapter, since the downgrade loses audio invisibly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(studio): release static-seek adapter on native win, warn once on downgrade

Review findings on the previous commit, all in the static-seek fallback
path of useTimelinePlayer.getAdapter:

- A cached static-seek adapter was never paused when adapter selection
  later resolved a native adapter (the early returns bypass the fallback
  branch entirely), leaving its private rAF loop seeking the player while
  the native transport also drives it. The core data-duration fix makes
  this switch path much more common. releaseStaticSeekCache() now runs
  at every native-adapter return and at unmount.
- The downgrade warning fired on every cache miss — and the cache key can
  never hold for __timelines compositions because wrapTimeline() returns
  a fresh object per call, so it fired every rAF tick. It now warns once
  per downgrade streak (re-armed when a native adapter takes over).
- The warning interpolated adapterDur (the native __player duration,
  0 when absent) instead of the selected adapter's duration, and used a
  one-off "[hyperframes-studio]" prefix instead of the file's
  "[useTimelinePlayer]" convention.

The fallback cache logic moved to playbackAdapter.ts (with unit tests for
warn-once, cache identity, and pause-on-replace/release), which also
keeps useTimelinePlayer.ts inside the studio 600-line limit. Also
corrected a stale "no DOM reads" comment on the runtime transport tick —
the duration floor has always queried the DOM per call, and now also
reads the root's declared data-duration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:01:39 -07:00
Miguel Ángel 8642b1d785 chore: bump version to 0.6.95 2026-06-12 12:44:40 -04:00
Miguel Ángel a8090ca895 chore: bump version to 0.6.94 2026-06-12 11:34:36 -04:00
Miguel Ángel cee6fd02d6 fix(cli): verify browser/ffmpeg binaries exist before render starts (#1365)
## Problem

Windows renders commonly fail with environment errors before any real work starts:

- `Browser was not found at the configured executablePath (...chrome-headless-shell.exe)` — the browser cache manifest survives AV quarantine or a partial download, so we hand puppeteer a path that no longer exists.
- `[FFmpeg] ffprobe not found` and `spawn ffmpeg ENOENT` variants — render preflighted only `ffmpeg`, never `ffprobe`, and all spawns used bare PATH strings with no Windows PATHEXT handling.

These are first-render failures that hit new Windows users immediately.

## Fix

- Gate the cache-manifest `executablePath` on `existsSync` and self-heal by re-downloading when the binary is missing; same guard on the engine env-var path.
- New shared environment preflight (`packages/cli/src/browser/preflight.ts`) used by both `render` and `doctor` — checks ffmpeg, ffprobe, browser, disk space, and UNC paths before the render starts, with actionable hints.
- Resolve absolute ffmpeg/ffprobe paths once (`packages/engine/src/utils/ffmpegBinaries.ts`) and pass them to every engine spawn instead of relying on PATH.
- Map opaque Windows ffmpeg exit codes to actionable messages.

## Testing

- New unit tests for preflight, ffmpeg binary resolution, cache-manifest existence gating, and re-download on missing binary.
- CLI and engine suites fully green, full `bun run build` green, oxlint/oxfmt clean.
- Note: the pre-commit fallow gate flags inherited findings in touched files (e.g. `audioExtractor.ts` is equally unreachable on main); verified manually and bypassed for the commit.
2026-06-12 01:36:28 -04:00
Miguel Ángel c3554dcffe fix(studio): disable keyframes feature flag by default, release v0.6.93 2026-06-12 00:59:17 -04:00
Miguel Ángel bbb36b4e4d chore: bump version to 0.6.92 2026-06-12 00:25:13 -04:00
Miguel Ángel d49ee416a3 fix(studio): gesture recording replaces existing position keyframes (#1360)
Gesture recording uses replace-with-keyframes mutation to replace existing
position-group tween. Fix N1 sign inversion and N9 wheel startPointer
with pointerElementOffset subtraction.
2026-06-12 00:20:08 -04:00
Miguel Ángel b6bf1b1190 fix(core): split-into-property-groups and replace-with-keyframes mutations (#1355)
* fix(core): per-property-group keyframe foundations

Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.

* fix(core): add split-into-property-groups and replace-with-keyframes mutations

Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
2026-06-12 00:12:26 -04:00
Miguel Ángel 889e9f09ad fix(core): per-property-group keyframe foundations (#1354)
Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.
2026-06-12 00:02:27 -04:00
Miguel Ángel 8802c3fdf8 fix(core): actionable error for empty sub-composition HTML in compile (#1364)
## Problem

The most common render failure in recent reports is:

```
Cannot destructure property 'firstElementChild' of 'documentElement' as it is null.
```

It appears when a `data-composition-src` file resolves to empty or unparsable HTML, and started showing up after the render pipeline change in 0.6.73.

## Root cause

When a sub-composition file is empty or unparsable, linkedom's `parseHTML` returns a document with a null `documentElement`, and the shared inliner (`packages/core/src/compiler/inlineSubCompositions.ts`) dereferences `.body`/`.head` on it, crashing inside linkedom internals with the cryptic destructure error instead of telling the user what's wrong.

## Fix

Guard the resolved sub-composition HTML and the extracted content HTML in the shared inliner: empty or unparsable input now fails with an actionable error naming the offending file.

## Testing

- New tests in core and producer reproducing the empty sub-composition case (previously crashed with the destructure error, now throws the actionable message).
- `bun run build` green, all tests pass in the changed test files.
2026-06-11 23:40:11 -04:00
Vance IngallsandClaude Sonnet 4.6 a0ee97210b fix(sdk,core): css tokenizer, override-set replay, setattribute safety, persist errors (#1350)
* fix(sdk,core): css tokenizer, override-set replay, setattribute safety, persist errors

* test(sdk,ci): smoke test + explicit sdk-tests CI gate

Smoke test covers the full public surface:
  openComposition → setStyle/setText/dispatch(moveElement) → serialize
  applyPatches + ORIGIN_APPLY_PATCHES tagging
  batch() coalescing + transactional rollback on throw
  undo/redo round-trip
  persist adapter write + persist:error surfacing
  T3 embedded mode: override-set apply on open + getOverrides round-trip

Adds sdk-tests CI job so SDK coverage is explicitly named and required —
prevents a repeat of the demo-next vitest-never-ran incident.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sdk): export adapter types, awaitable flush(), never-coalesce mode

- Export PersistAdapter, PreviewAdapter, PersistVersionEntry from package
  root — callers can now write typed fakes without reaching into internals
- Add flush(): Promise<void> to Composition interface + CompositionImpl —
  app-close handlers can await a clean drain of the persist queue
- coalesceMs <= 0 disables coalescing entirely in createHistory — enables
  deterministic test scenarios without per-entry timestamp manipulation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(sdk): p2 edge cases — setText no-text-node, override-remove non-existent, flush in smoke

- setText on element with no prior text node (firstTextIdx=-1 path)
- applyOverrideSet null removal on non-existent prop is a no-op (no throw)
- smoke persist test uses comp.flush() instead of setTimeout
- can() JSDoc clarifies Phase 3b false-return is intentional feature-detection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: trigger regression suite

* fix(ci): add packages/sdk/package.json to Dockerfile.test workspace copy

bun install --frozen-lockfile fails in the regression Docker build because
the lockfile references the sdk workspace member but its package.json was
not copied into the image before the install step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 14:07:49 -07:00
Vance IngallsandClaude Fable 5 fc3ab76ce8 fix(studio,core): persist manual position edits for GSAP-owned elements (#1346)
* feat(sdk): scaffold @hyperframes/sdk — engine layer (model, RFC 6902 patches, mutate, apply-patches)

* fix(sdk): make engine-layer PR self-contained — trim index.ts, guard indexed access

- index.ts no longer exports document/session/history/persist-queue (those
  modules land in the next stacked PR); branch now typechecks standalone
- setOwnText: optional-chain children[i] access (TS2532 under
  noUncheckedIndexedAccess)
- fallow suppressions for buildPatchEvent + adapters/types.ts — consumers
  arrive in #1325

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): fail loudly on Phase 3b ops; add sdk to root build pipeline

- applyOp throws UnsupportedOpError (code E_UNSUPPORTED_OP) for the 9
  parser-backed ops instead of silently no-opping — callers must never
  believe an animation edit succeeded when nothing was mutated
- validateOp returns false for Phase 3b ops so can() feature-detects
- root package.json build filter now includes @hyperframes/sdk (package is
  dist-only; top-level build previously produced no SDK artifacts).
  publish.yml intentionally NOT updated — sdk stays unpublished until
  Phase 3 completes.

Adversarial-review findings F3 + F4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): cross-realm origin sentinel, dual width/height channel, contract docs

Round-2 review (Rames/Miguel) on the engine layer:

- ORIGIN_APPLY_PATCHES: unique symbol → namespaced string
  ('@hyperframes/sdk:applyPatches'). Symbols are realm-local — they don't
  survive postMessage/structured-clone, which T3 embedded hosts may forward
  patch events across. Namespaced string keeps collision risk negligible.
- setCompositionMetadata width/height: runtime treats data-width/data-height
  as a forced override of inline style (init.ts applyCompositionSizing).
  Style is always written; the data-* attr is updated when already present
  so the edit isn't clobbered on load. Absent attrs stay absent — inverses
  stay exact. Mirrored in the patch applier; 3 new tests.
- JsonPatchOp documented as the emit-only RFC 6902 subset
  (add/remove/replace); applier header notes move/copy/test are ignored.
- SdkDocument.html documented as a build-time snapshot (serialize() is the
  live state).
- patches.ts path-grammar comment fixed: timing/{start|end|trackIndex}.

NOT changed (with reasons, see PR reply): moveElement left/top matches
Studio's own inline-style commit convention (sourcePatcher); package version
follows the repo-wide single-version policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): moveElement writes data-x/data-y, not left/top CSS

HF elements use data-x/data-y for positioning (read by htmlParser.ts,
emitted by hyperframes generator). CSS left/top is not the runtime convention.

Adds inverse round-trip test for prior position restore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update bun.lock after sdk package registration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(sdk): session API, optional history + persist-queue, adapters — Phase 3a complete

* fix(sdk): address review — live-DOM query cache, single parse, style parse dedup

- getElements/getElement/find now walk the live linkedom DOM via buildRoots
  with a lazily-built cache invalidated on dispatch/applyPatches — no
  serialize→ensureHfIds→parseHTML round trip per query
- openComposition parses once (parseMutable); dropped discarded _doc
  constructor param and the redundant buildDocument call
- document.ts buildElement reuses model.ts getElementStyles — removes
  duplicated parseInlineStyles (also fixes custom-prop camelCase mangling)
- JSDoc note: empty batch() still fires change handlers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): restore full public exports now session/document modules exist

index.ts re-exports document/session/history/persist-queue (trimmed in the
engine-layer PR to keep it self-contained); drops the temporary fallow
suppressions whose consumers now exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): coalesce history by patch paths; replay override-set on open

Adversarial-review findings F1 + F2:

- history: coalescing now requires identical patch paths in addition to
  op types + origin + window. Previously two rapid setStyle calls on
  DIFFERENT elements merged into one entry carrying the second forward +
  first inverse — undo then reverted the wrong element and stranded the
  latest edit. Slider drags on one property still coalesce.
- T3 init: openComposition({ overrides }) now replays the stored
  override-set onto the freshly-parsed base before exposing the session
  (new keyToPath inverse mapping + applyOverrideSet). Previously the
  overrides were copied into the map but never applied — reopening an
  embedded composition showed and serialized the base template.
- examples: GSAP calls now feature-detect with can() (Phase 3b ops throw
  UnsupportedOpError as of the engine-layer fix); UnsupportedOpError
  re-exported from the package entry.
- 8 new session tests: coalesce same-path / cross-element / cross-prop,
  override round-trip (style/text/attr/timing/removal/restore-base).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): transactional batch rollback, sorted coalesce key, root-priority unify

Round-2 review (Rames/Miguel) on the session layer:

- batch() is now transactional: on throw, accumulated inverse patches are
  replayed in reverse and the override-set snapshot restored — the model is
  exactly as it was at batch entry. Previously a throwing batch left the DOM
  partially mutated with no patch trail, no history entry, no recovery path.
  2 new tests (model unchanged + undo is no-op after throwing batch).
- history coalesce key sorts opTypes — same op-type set coalesces regardless
  of dispatch order within a batch.
- applyPatches comment documents that emitted PatchEvents carry an empty
  inversePatches array (hosts keep their own inverse log).
- document.ts extractDimensions/extractDuration now use the engine's
  findRoot — dimension extraction and mutations agree on the root element
  ([data-hf-root] > #stage > first child). Dimensions prefer the runtime's
  data-width/data-height forced-override attrs, falling back to inline style.
- ownText documented: snapshot .text is trimmed display text; setText writes
  verbatim.

Deferred to follow-up (acknowledged, not ship-blocking): persist-queue flush
error surfacing, debounce window, path default, history ring-buffer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(lint): add gsap_studio_edit_blocked rule for manual timeline + GSAP element targeting

* fix(studio,core): persist manual position edits for GSAP-owned elements

- sourceMutation: linkedom CSSStyleDeclaration silently drops CSS custom
  properties and transform longhands via setProperty; patch the style
  attribute string directly so --hf-studio-offset-* and translate survive
  the server round-trip (positions never reached disk before this)
- gsapAnimatesTransform(): GSAP owns the full transform stack when it tweens
  ANY transform prop (scale, rotation, ...), not just x/y — it folds CSS
  translate into its cache once at init, zeroes the longhand once, and never
  re-reads it
- applyStudioPathOffset: for GSAP-owned elements keep translate:none live and
  sync the offset into GSAP's cache via gsap.set; writing the longhand
  double-applied the offset (disappearing elements, scrub snap-back)
- buildPathOffsetPatches: emit the var() translate expression explicitly so
  the persisted file re-folds on reload (live inline is none)
- StudioPathOffsetSnapshot: capture/restore GSAP x/y — the drag-response
  probe mutates GSAP's cache, which inline-style restore cannot undo (click
  made elements jump by the probe distance)
- reapplyPathOffsets: skip GSAP-owned elements (was x/y-only) to stop
  seek-time double-apply
- STUDIO_GSAP_DRAG_INTERCEPT flag (default off): keyframe drag intercept is
  opt-in until its recording path is hardened; commits take the CSS persist
  path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(studio): remove duplicate flag declaration, trim useDomEditCommits to 600 lines

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:30:14 -07:00
Vance IngallsandClaude Fable 5 511665b93a feat(lint): add gsap_studio_edit_blocked rule for manual timeline + GSAP element targeting (#1345)
* feat(sdk): scaffold @hyperframes/sdk — engine layer (model, RFC 6902 patches, mutate, apply-patches)

* fix(sdk): make engine-layer PR self-contained — trim index.ts, guard indexed access

- index.ts no longer exports document/session/history/persist-queue (those
  modules land in the next stacked PR); branch now typechecks standalone
- setOwnText: optional-chain children[i] access (TS2532 under
  noUncheckedIndexedAccess)
- fallow suppressions for buildPatchEvent + adapters/types.ts — consumers
  arrive in #1325

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): fail loudly on Phase 3b ops; add sdk to root build pipeline

- applyOp throws UnsupportedOpError (code E_UNSUPPORTED_OP) for the 9
  parser-backed ops instead of silently no-opping — callers must never
  believe an animation edit succeeded when nothing was mutated
- validateOp returns false for Phase 3b ops so can() feature-detects
- root package.json build filter now includes @hyperframes/sdk (package is
  dist-only; top-level build previously produced no SDK artifacts).
  publish.yml intentionally NOT updated — sdk stays unpublished until
  Phase 3 completes.

Adversarial-review findings F3 + F4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): cross-realm origin sentinel, dual width/height channel, contract docs

Round-2 review (Rames/Miguel) on the engine layer:

- ORIGIN_APPLY_PATCHES: unique symbol → namespaced string
  ('@hyperframes/sdk:applyPatches'). Symbols are realm-local — they don't
  survive postMessage/structured-clone, which T3 embedded hosts may forward
  patch events across. Namespaced string keeps collision risk negligible.
- setCompositionMetadata width/height: runtime treats data-width/data-height
  as a forced override of inline style (init.ts applyCompositionSizing).
  Style is always written; the data-* attr is updated when already present
  so the edit isn't clobbered on load. Absent attrs stay absent — inverses
  stay exact. Mirrored in the patch applier; 3 new tests.
- JsonPatchOp documented as the emit-only RFC 6902 subset
  (add/remove/replace); applier header notes move/copy/test are ignored.
- SdkDocument.html documented as a build-time snapshot (serialize() is the
  live state).
- patches.ts path-grammar comment fixed: timing/{start|end|trackIndex}.

NOT changed (with reasons, see PR reply): moveElement left/top matches
Studio's own inline-style commit convention (sourcePatcher); package version
follows the repo-wide single-version policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): moveElement writes data-x/data-y, not left/top CSS

HF elements use data-x/data-y for positioning (read by htmlParser.ts,
emitted by hyperframes generator). CSS left/top is not the runtime convention.

Adds inverse round-trip test for prior position restore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update bun.lock after sdk package registration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(sdk): session API, optional history + persist-queue, adapters — Phase 3a complete

* fix(sdk): address review — live-DOM query cache, single parse, style parse dedup

- getElements/getElement/find now walk the live linkedom DOM via buildRoots
  with a lazily-built cache invalidated on dispatch/applyPatches — no
  serialize→ensureHfIds→parseHTML round trip per query
- openComposition parses once (parseMutable); dropped discarded _doc
  constructor param and the redundant buildDocument call
- document.ts buildElement reuses model.ts getElementStyles — removes
  duplicated parseInlineStyles (also fixes custom-prop camelCase mangling)
- JSDoc note: empty batch() still fires change handlers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): restore full public exports now session/document modules exist

index.ts re-exports document/session/history/persist-queue (trimmed in the
engine-layer PR to keep it self-contained); drops the temporary fallow
suppressions whose consumers now exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): coalesce history by patch paths; replay override-set on open

Adversarial-review findings F1 + F2:

- history: coalescing now requires identical patch paths in addition to
  op types + origin + window. Previously two rapid setStyle calls on
  DIFFERENT elements merged into one entry carrying the second forward +
  first inverse — undo then reverted the wrong element and stranded the
  latest edit. Slider drags on one property still coalesce.
- T3 init: openComposition({ overrides }) now replays the stored
  override-set onto the freshly-parsed base before exposing the session
  (new keyToPath inverse mapping + applyOverrideSet). Previously the
  overrides were copied into the map but never applied — reopening an
  embedded composition showed and serialized the base template.
- examples: GSAP calls now feature-detect with can() (Phase 3b ops throw
  UnsupportedOpError as of the engine-layer fix); UnsupportedOpError
  re-exported from the package entry.
- 8 new session tests: coalesce same-path / cross-element / cross-prop,
  override round-trip (style/text/attr/timing/removal/restore-base).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): transactional batch rollback, sorted coalesce key, root-priority unify

Round-2 review (Rames/Miguel) on the session layer:

- batch() is now transactional: on throw, accumulated inverse patches are
  replayed in reverse and the override-set snapshot restored — the model is
  exactly as it was at batch entry. Previously a throwing batch left the DOM
  partially mutated with no patch trail, no history entry, no recovery path.
  2 new tests (model unchanged + undo is no-op after throwing batch).
- history coalesce key sorts opTypes — same op-type set coalesces regardless
  of dispatch order within a batch.
- applyPatches comment documents that emitted PatchEvents carry an empty
  inversePatches array (hosts keep their own inverse log).
- document.ts extractDimensions/extractDuration now use the engine's
  findRoot — dimension extraction and mutations agree on the root element
  ([data-hf-root] > #stage > first child). Dimensions prefer the runtime's
  data-width/data-height forced-override attrs, falling back to inline style.
- ownText documented: snapshot .text is trimmed display text; setText writes
  verbatim.

Deferred to follow-up (acknowledged, not ship-blocking): persist-queue flush
error surfacing, debounce window, path default, history ring-buffer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(lint): add gsap_studio_edit_blocked rule for manual timeline + GSAP element targeting

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:23:36 -07:00
Vance Ingalls 2c64f99694 feat(core): expose hf-ids as subpath export for @hyperframes/sdk (#1323)
## Summary

Exposes `hf-ids` as a dedicated subpath export from `@hyperframes/core` so `@hyperframes/sdk` can import ID-stamping logic without pulling in the full core bundle.

- Adds `"exports"` entry for `./hf-ids` in `packages/core/package.json`
- No change to the existing top-level export — no breaking change for existing consumers

## Why

`@hyperframes/sdk` needs `parseMutable`/`stampHfIds` from core. A subpath export isolates that boundary and keeps the SDK bundle lean.

## Test plan
- [ ] `bun run build` — both packages build without errors
- [ ] `bun test packages/sdk` — import resolves correctly

🤖 Generated with [Claude Code](https://claude.ai/claude-code)
2026-06-11 11:58:43 -07:00
Miguel Ángel 83662c11a8 chore: release v0.6.91 2026-06-11 06:09:31 +00:00
Miguel Ángel 45d4a71ed0 feat(core): GSAP-aware split engine for timeline clip splitting (#1330)
* refactor(studio): extract shared timeline components and deduplicate code

Extract shared utilities to reduce duplication across timeline components:

- PlayheadIndicator: shared playhead rendering (was duplicated in
  TimelineCanvas and TimelineEditorNotice)
- useContextMenuDismiss: outside-click/Escape dismiss pattern (was
  duplicated in ClipContextMenu and KeyframeDiamondContextMenu)
- TimelineCallbacks: shared callback interfaces for drop and edit
  operations (was duplicated in NLELayout and Timeline props)
- useTimelineZoom: consolidated zoom store selectors
- timelineElementSplit: shared canSplitElement, buildPatchTarget, and
  readFileContent utilities
- gsapParser.test-helpers: shared test utilities for parser specs

* feat(core): GSAP-aware split engine for timeline clip splitting

Add splitAnimationsInScript to the GSAP parser — correctly re-times
animations when a timeline clip is split at an arbitrary position:

- Animations before split: kept on original, properties inherited via
  tl.set inserted before other tweens for correct GSAP state recording
- Animations after split: retargeted via AST selector update
- Spanning animations: trimmed on original, continuation added for
  new element with correct position and duration
- Keyframes: classified by total per-keyframe duration
- Reverse iteration prevents stale animation ID collisions

Enhance splitElementInHtml:
- CSS rule duplication via PostCSS for ID-based styles
- Server-side ID deduplication for repeated splits
- Media playback-start adjustment for video/audio

Add split-animations route to gsap-mutations endpoint.
2026-06-10 23:48:16 -04:00
Miguel Ángel ab08260201 refactor(studio): extract shared timeline components and deduplicate code (#1329)
Extract shared utilities to reduce duplication across timeline components:

- PlayheadIndicator: shared playhead rendering (was duplicated in
  TimelineCanvas and TimelineEditorNotice)
- useContextMenuDismiss: outside-click/Escape dismiss pattern (was
  duplicated in ClipContextMenu and KeyframeDiamondContextMenu)
- TimelineCallbacks: shared callback interfaces for drop and edit
  operations (was duplicated in NLELayout and Timeline props)
- useTimelineZoom: consolidated zoom store selectors
- timelineElementSplit: shared canSplitElement, buildPatchTarget, and
  readFileContent utilities
- gsapParser.test-helpers: shared test utilities for parser specs
2026-06-10 23:45:03 -04:00
Miguel Ángel 06426b5014 chore: release v0.6.90 2026-06-11 02:40:36 +00:00
Matt Van HornandMatt Van Horn edd85473e7 feat(producer,core): play animated GIF inputs frame-synced via prep-time VP9 transcode (#1335)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-10 22:39:19 -04:00
Miguel Ángel 868c56fdbb chore: release v0.6.89 2026-06-10 23:26:13 +00:00
Miguel Ángel 036b991cbb fix(runtime): preserve authored muted attr; clean up WebAudio on end (#1319)
onSetMuted/onSetMediaOutputMuted set el.muted = effective on every
<video> and <audio> element. When the bridge sent onSetMuted(false),
it unmuted avatar <video muted> elements whose baked-in lip-sync audio
should never play — causing double audio alongside the separate TTS.
Fix: el.muted = effective || el.defaultMuted.

AudioBufferSourceNode fires 'ended' when playback completes naturally,
but _activeSources was never cleaned up. This kept isActive() true
permanently, which force-muted all HTML audio elements via the
outputMuted flag in syncRuntimeMedia — causing audio to disappear
after the WebAudio buffer finished (~5s for short TTS clips).

Add onended listener that removes the source from _activeSources and
restores el.muted to its pre-WebAudio value. All side-effects are
guarded by idx !== -1 so a stale ended event after stopAll() is a
no-op and cannot clobber bridge state set between stop and the async
event delivery.
2026-06-10 19:09:13 -04:00
Miguel Ángel f0b499b582 fix(studio): keyframe bug fixes — gate delete hooks, fix value corruption, gesture recording (#1314)
- Gate stripStudioEditsFromTarget/bakeVisibilityOnDelete behind a
  stripStudioEdits flag on the delete mutation type so they only fire on
  user-initiated deletes, not on internal delete-then-recreate drags.

- Add bakeVisibilityOnDelete to the remove-all-keyframes handler so
  elements with CSS opacity:0 stay visible after collapsing keyframes.

- Fix integer rounding in readAllAnimatedProperties: use 3-decimal
  precision for visual properties (opacity, scale, rotation) instead of
  Math.round which corrupted mid-fade values to 0.

- Guard VISUAL_BASELINE against cross-tween contamination by querying
  __timelines for properties animated by other tweens on the same element.

- Harden bakeVisibilityOnDelete: reverse-scan keyframes for the last one
  containing opacity, guard against relative values (+=/-=/*=), and add
  Number.isFinite check.

- Fix falsy-zero doubling in drag commit: replace || fallback with
  Number.isFinite so a base GSAP position of 0 is correctly preserved.

- Fix gesture recording sign inversion: remove pointerElementOffset
  subtraction from dx/dy formula and instead apply it once to basePosition
  so the element center tracks the pointer.

- Fix TypeScript build errors in gsapSoftReload.ts (6 double-casts).

- Strip all diagnostic logs from production code.
2026-06-10 18:53:06 -04:00
Miguel Ángel d13ae13670 chore: release v0.6.88 2026-06-09 23:34:42 +00:00
Miguel Ángel 192417b7cd fix(core): guard element stamping to iframe-only (Studio preview) 2026-06-09 23:34:08 +00:00
Miguel Ángel 02475ce9f7 chore: release v0.6.87 2026-06-09 22:36:11 +00:00
Miguel Ángel 869fc411a3 chore: release v0.6.85 2026-06-09 22:32:34 +00:00
Miguel Ángel a468550f82 feat(studio): keyframe system — parser, runtime, timeline UI, design panel, gesture recording (#1311)
* feat(studio): runtime hooks — global time compiler + keyframe runtime

Add the runtime bridge layer: global time compilation (tween % → clip %),
soft reload after mutations, runtime keyframe preview, and keyframe
commit helper.

* feat(studio): runtime hooks — global time compiler + keyframe runtime

Add the runtime bridge layer: global time compilation (tween % → clip %),
soft reload after mutations, runtime keyframe preview, and keyframe
commit helper.

* feat(studio): keyframe cache + commit hooks

Add hooks for keyframe cache population (tween → clip-relative %),
mutation dispatch, keyframe snapping, and audio beat detection.

* feat(studio): timeline UI — dopesheet diamonds + keyboard nav

Add dopesheet strip with diamond keyframe indicators, timeline property
rows, keyboard navigation (J/Shift+J/Delete/K), and feature gate
(STUDIO_KEYFRAMES_ENABLED defaults to false).

* feat(studio): design panel — arc controls + ease curve + stagger

Add arc path controls (curviness slider, auto-rotate), motion path SVG
overlay, ease curve visualization, stagger controls, and expanded
animation card. Includes border-radius editor dependency from #1217.

* feat(studio): gesture recording core

Add gesture recording engine with RAF sampling, modifier key property
mapping (Shift→rotationXY, Alt→rotation, Cmd→opacity),
Ramer-Douglas-Peucker simplification, and ghost trail SVG overlay.

* fix(studio): keyframe drag + recording bug bash

21 fixes: capture GSAP base at drag start, translate:none before
gsap.set, skip reapplyPathOffsets for GSAP elements, clamp recording
seek, _auto flag for 100% keyframes, overlay flash fix, block edits
during recording.

* feat(studio): keyframe integration wiring + docs

Wire App.tsx recording orchestration, TimelineToolbar K/R buttons,
PropertyPanel per-property diamonds, shortcuts panel, toast
notifications, and keyframes guide documentation. All gated on
STUDIO_KEYFRAMES_ENABLED (default false).
2026-06-09 18:30:23 -04:00
Miguel Ángel 96b8d617d8 feat(studio): GSAP parser — arc path mutations + keyframe CRUD (#1301)
Add parser-level mutations for arc paths, keyframe add/remove/update,
convert-to-keyframes, and _auto flag for 100% keyframes. Wire route
handlers for new mutation types.
2026-06-09 18:28:44 -04:00
Vance IngallsandClaude Sonnet 4.6 0923bc0787 feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b) (#1299)
* feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b)

* refactor(studio): extract readHfId helper, fix empty-string normalization, add comments (R7 review)

- Extract readHfId(el) to domEditingLayers.ts — centralizes `?.trim() || undefined`
  normalisation; guards against empty-string data-hf-id reaching findTagByTarget
- Wire readHfId into domEditingLayers.ts and useDomEditCommits.ts (the one site
  that still used `?? undefined` instead of `|| undefined`)
- Re-export readHfId through domEditing.ts public API
- Add readHfId unit tests: present, absent, empty-string, whitespace-only
- Add comment on PatchTarget: runtime validation lives in findTagByTarget, type is docs-only
- Suppress pre-existing unused re-exports in timelineDOM.ts (backward-compat re-exports
  brought into fallow scope by the T5b hfId changes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): clear data-hf-id on split clone to prevent dual-match (R7 review)

cloneNode(true) copies all attributes including data-hf-id. Without clearing it,
both halves of a split share the same hf-id; the server's findByHfId picks the first
match and silently patches the wrong clip. Remove the attribute from the clone so
write-back re-mints a fresh id on the next preview load.

Adds a test: splitElementInHtml — hfId clone isolation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(studio): add hfId to DomEditLayerItem + getDomLayerPatchTarget return type (R7 review)

- Add hfId to DomEditLayerItem interface (domEditingTypes.ts) so layer item
  construction in collectDomEditLayerItems compiles
- Widen getDomLayerPatchTarget return type to include hfId + populate it from
  data-hf-id attribute (domEditingElement.ts)
- Widen findDomEditSelectionTarget to check hfId-first when no id/selector
- Widen Pick types in domEditOverlayGeometry.ts and useGsapScriptCommits.ts
- Add hfId to buildMissingCompositionElements element construction
- Add hfId-targeted test coverage in domEditing.test.ts,
  domEditOverlayGeometry.test.ts, timelineIframeHelpers.test.ts
- Update hfIds.test.ts KNOWN LIMITATION labels — write-back landed in R7 T1-2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:15:27 -07:00
Vance Ingalls 2071c85c9a feat(studio,core): populate hfId in DomEditSelection + widen MutationTarget (R7, T5a) (#1296)
## Summary

- Adds `hfId` field to `resolveDomEditSelection` — reads `data-hf-id` off the live element and stores it in `DomEditSelection.hfId`
- `DomEditSelection extends PatchTarget` which already declares `hfId?: string`, so this is a single new line at the return site
- Widens `MutationTarget` in `files.ts` to include `hfId?: string` (type hygiene — the value already survives through `parseMutationBody`'s by-reference pass, so this is documentation not a behaviour change)

## Why

R7 / Task 5a. The full hf-id write-back and patch-engine infrastructure (R1 + R7 Tasks 0–4, PRs #1269–#1292) is server-complete. The only missing piece was: the Studio client never read `data-hf-id` off a hit-tested element, so `target.hfId` was always `undefined` and the `hfId`-first lookup branches in both patch engines were unreachable in production. This PR fixes the selection side — the commit wire (#1297) completes the path.

## Test plan

- [ ] `packages/studio/src/components/editor/domEditingLayers.test.ts` — two new tests with jsdom environment:
  - `resolveDomEditSelection` on an element with `data-hf-id` → `selection.hfId` is populated
  - element without `data-hf-id` → `selection.hfId` is `undefined`
- [ ] All 65 studio test files pass, all 72 core test files pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-09 14:18:14 -07:00
Miguel Ángel 04c77fa7aa chore: release v0.6.86 2026-06-09 19:50:45 +00:00
xiayeandClaude Opus 4.7 5a0966dfeb fix(runtime,player): replay bridge state on iframe ready to repair race
The audio-locked attribute was correctly setting `muted = true` and posting
`set-muted` to the iframe runtime, but on warm-cache reloads of claude.ai
and inside the Claude desktop Electron client, the iframe finishes loading
*after* the parent has already sent control messages — the iframe runtime's
postMessage listener isn't installed yet, so the messages are silently
dropped. Audio plays unmuted with no UI to recover.

Confirmed via:
- "First open" on claude.ai: cold cache, iframe slow → listener up before
  `set-muted` lands → audio muted 
- "Hard refresh" on claude.ai: warm cache, iframe fast → listener up after
  message arrives → message lost → audio plays 
- Claude desktop: Electron renderer consistently fast → race always loses
  → audio plays 

Fix: add a `{source: "hf-preview", type: "ready"}` event the runtime emits
once `installRuntimeControlBridge` has registered the listener. The player
listens for it and replays current bridge state (`set-muted`, `set-volume`,
`set-playback-rate`). Pre-ready messages are now safe to send — they'll be
replayed once the runtime can receive them.

The replay is idempotent — re-asserting defaults is a no-op — so it's also
safe across iframe reloads (new runtime instance emits ready again).

Tests: 6 new (1 bridge: ready posted on install; 5 player: replays muted /
volume / playback-rate / audio-locked-forced-mute / handles second ready /
ignores ready from wrong source). Suites green: core 1387, player 137.

Refs:
- Investigation: heygen-com/hyperframes#1300 (UA-fallback attempt — unrelated
  to actual root cause)
- claude.ai-web.log analysis revealed cross-origin iframe + race condition,
  not attribute stripping as originally hypothesized

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 12:22:27 -07:00
Miguel Ángel acd8e11789 chore: release v0.6.85 2026-06-09 17:14:52 +00:00
Vance IngallsandClaude Sonnet 4.6 3d7d7c0291 refactor(core,studio): extract draft-marker constants + R7 code-review fixes (Task 4) (#1292)
* refactor(core,studio): extract draft-marker constants to core (R7, Task 4)

Create draftMarkers.ts in core with 5 shared CSS custom property names and the
gesture DOM attribute. PreviewAdapter imports from draftMarkers.ts instead of
hardcoding strings. Adds @hyperframes/core/studio-api/draft-markers export
subpath. Studio's manualEditsTypes.ts re-exports the shared constants from core
so all existing call sites are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): address R7 code-review findings (C1–C14, P6–P7)

- previewAdapter: auto-revert previous gesture in applyDraft (C3); clearDraftProps
  on commitPreview not just revertDraft (C4); isVisible NaN→visible for JSDOM (P7);
  remove redundant GestureState.hfId field (C12); remove Array.from (C14);
  extract clearDraftProps/revertGesture helpers (C5/C6)
- hfIdPersist: replace string-equality change detection with attribute count to
  avoid false-positive writes on single-quoted HTML (C1); re-read disk before
  write for TOCTOU guard (C7); remove normalizeHfIds wrapper (C11)
- preview.ts: remove dead null-check on normalizedDisk after diskMain guard (C9);
  catch path re-reads disk fresh instead of using stale pre-request snapshot (C8)
- hfIds.test.ts: replace tautological second stability test with cross-document
  content-keyed id stability test (P6)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): follow-up R7 review fixes — CSS.escape fallback, invariant docs, new edge-case tests

- hfIdPersist: remove ensureHfIds re-export (P2); add JSDoc invariant note;
  improve TOCTOU comment; pass err to console.warn
- preview.ts: split import — ensureHfIds from parsers/hfIds.js (not re-export)
- previewAdapter: CSS.escape + inline fallback for non-browser environments;
  add JSDoc for atTime caller-seek contract; add 0.01 opacity-threshold comment
- previewAdapter.test: rename atTime test to clarify adapter-does-not-seek;
  add nested-hf-root-without-id test; add resize→move prop-leak test;
  add revertDraft-after-commit no-op test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): bundle-vs-disk id-stability test; comment double ensureHfIds (P3)

- preview.test: add "bundle returning untagged HTML gets same ids as disk" test —
  guards against id divergence when bundler reads a pre-write cache snapshot;
  content-keyed FNV1a minting ensures served ids == disk ids for same source HTML
- preview.ts: comment the second ensureHfIds call explaining it's intentional for
  adapter-injected elements and idempotent on the no-bundle path (P3 from miguel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(core): wire-contract comment on mintHfId + fallow suppressions (R7)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 01:13:54 -07:00
Vance IngallsandClaude Sonnet 4.6 1a23938bef feat(core): implement createPreviewAdapter (R7, Task 3) (#1291)
* test(core): data-hf-id survives id/selector patch (R1, T7)

Locks the preservation guarantee the write-back design depends on: a
Studio edit targeting by id or selector (it never sends hfId) must not strip
an existing data-hf-id, or the stable handle is destroyed by the next edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review)

Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated
target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value
injection guard) and warn when a hfId matches more than one element instead of
silently patching an arbitrary one. Adds an injection-guard test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(core): implement createPreviewAdapter — greens 20 T10 tests (R7, Task 3)

elementAtPoint: resolvePoint callback → walk ancestors for data-hf-id,
skip data-hf-root without data-hf-id (stage root), skip opacity-0 elements.

applyDraft: find element by hfId, record originalTranslate, set
--hf-studio-offset-x/y (move) or --hf-studio-width/height (resize),
mark data-hf-studio-manual-edit-gesture.

revertDraft: remove draft CSS props, clear gesture marker, restore
originalTranslate if one was recorded.

commitPreview: extract patch (move→moveElement, resize→resize with w/h
renamed to width/height), clear gesture marker, return patch or null.

getElementTimings: scan [data-hf-id] elements, parse data-start/data-end
as floats, return map with undefined fields for absent attributes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): remove explicit data-hf-id from htmlParser tests so ensureHfIds mints hf- ids

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:39:53 -07:00
Vance IngallsandClaude Sonnet 4.6 740a83abb3 feat(core): hf-id write-back to disk + serve-time surfacing (R7, Tasks 1-2) (#1289)
* feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1)

* docs(core): document legacy-id round-trip in clip-model readback (R1 review)

Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with
id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable,
exact-match) by design — targeting uses exact [data-hf-id="…"] match and does
not require the hf- shape; legacy values re-mint only at the R7 write-back. Not
a bug. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): fix misleading legacy-id migration comment in htmlParser.ts

The original comment said legacy data-hf-id values "are re-minted only
once the R7 write-back persists freshly-minted ids to source" — which is
incorrect. ensureHfIds skips elements that already carry data-hf-id, so
legacy values (e.g. data-hf-id="my-title") persist indefinitely and are
NOT automatically re-minted. Exact-match targeting still works correctly.
Update comment to reflect actual behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(studio): sourcePatcher data-hf-id targeting (R1, T3)

* fix(studio): warn on duplicate match in execDataAttrPattern (R1, T3 review)

Addresses Rames' review on #1271: execDataAttrPattern returned the first regex
match without checking for a second. A duplicate id/data-hf-id in source (id
drift) would silently patch one element and leave the other stale. Now warns
when more than one element matches. By the mint contract it should never fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(studio): pin hfId-is-authoritative-over-selector contract (R1, T3 review)

Adds test: "hfId match is authoritative — selector is not used as a
narrowing filter". When hfId matches element A and selector points at
element B, findTagByTarget returns A without consulting selector as a
narrowing filter. Pins the intended behaviour so a future refactor
cannot silently start narrowing by selector.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(core): sourceMutation data-hf-id targeting (R1, T7)

* test(core): update htmlParser baselines for R1 hf- id format

Elements now get data-hf-id minted by ensureHfIds; parser reads
data-hf-id as model id, so HTML id attrs are no longer the model id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): data-hf-id survives id/selector patch (R1, T7)

Locks the preservation guarantee the write-back design depends on: a
Studio edit targeting by id or selector (it never sends hfId) must not strip
an existing data-hf-id, or the stable handle is destroyed by the next edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review)

Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated
target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value
injection guard) and warn when a hfId matches more than one element instead of
silently patching an arbitrary one. Adds an injection-guard test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(core): previewAdapter contract failing tests (T10 spec for R7)

* feat(core): hf-id write-back to disk + serve-time surfacing (R7, Task 1-2)

* test(core): replace tautological stability tests with real disk tests for persistHfIdsIfNeeded

Prior tests only exercised normalizeHfIds (pure function) and the existing
pin guard in ensureHfIds — both pass on the parent commit without any Task 1
code. Replace with three tests that exercise the actual disk write-back:

- writes data-hf-id to disk when source is untagged
- does not rewrite disk when source is already tagged (idempotent)
- returned id matches id written to disk (serve-time == persist-time invariant)

These fail on the parent commit (persistHfIdsIfNeeded doesn't exist) and
green after Task 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): route-level tests for data-hf-id surfacing and disk write-back (R7, Task 1-2)

Two integration tests against the preview route (via Hono test harness):

- served HTML carries data-hf-id on body elements (>= 2 matches for div+p)
- disk file contains data-hf-id after first GET (write-back verified via readFileSync)

These fail on the parent commit (no hfIdPersist wiring in preview.ts) and
green after Task 1. Closes the verification gap flagged in review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:28:52 -07:00
Vance IngallsandClaude Opus 4.8 72e4f1a0f6 test(core): previewAdapter contract failing tests (T10 spec for R7) (#1286)
* feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1)

* docs(core): document legacy-id round-trip in clip-model readback (R1 review)

Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with
id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable,
exact-match) by design — targeting uses exact [data-hf-id="…"] match and does
not require the hf- shape; legacy values re-mint only at the R7 write-back. Not
a bug. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): fix misleading legacy-id migration comment in htmlParser.ts

The original comment said legacy data-hf-id values "are re-minted only
once the R7 write-back persists freshly-minted ids to source" — which is
incorrect. ensureHfIds skips elements that already carry data-hf-id, so
legacy values (e.g. data-hf-id="my-title") persist indefinitely and are
NOT automatically re-minted. Exact-match targeting still works correctly.
Update comment to reflect actual behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(studio): sourcePatcher data-hf-id targeting (R1, T3)

* fix(studio): warn on duplicate match in execDataAttrPattern (R1, T3 review)

Addresses Rames' review on #1271: execDataAttrPattern returned the first regex
match without checking for a second. A duplicate id/data-hf-id in source (id
drift) would silently patch one element and leave the other stale. Now warns
when more than one element matches. By the mint contract it should never fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(studio): pin hfId-is-authoritative-over-selector contract (R1, T3 review)

Adds test: "hfId match is authoritative — selector is not used as a
narrowing filter". When hfId matches element A and selector points at
element B, findTagByTarget returns A without consulting selector as a
narrowing filter. Pins the intended behaviour so a future refactor
cannot silently start narrowing by selector.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(core): sourceMutation data-hf-id targeting (R1, T7)

* test(core): update htmlParser baselines for R1 hf- id format

Elements now get data-hf-id minted by ensureHfIds; parser reads
data-hf-id as model id, so HTML id attrs are no longer the model id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): data-hf-id survives id/selector patch (R1, T7)

Locks the preservation guarantee the write-back design depends on: a
Studio edit targeting by id or selector (it never sends hfId) must not strip
an existing data-hf-id, or the stable handle is destroyed by the next edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review)

Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated
target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value
injection guard) and warn when a hfId matches more than one element instead of
silently patching an arbitrary one. Adds an injection-guard test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(core): previewAdapter contract failing tests (T10 spec for R7)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:28:21 -07:00
Vance IngallsandClaude Opus 4.8 1cd4ad6ab8 feat(core): sourceMutation data-hf-id targeting (R1, T7) (#1272)
* feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1)

* docs(core): document legacy-id round-trip in clip-model readback (R1 review)

Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with
id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable,
exact-match) by design — targeting uses exact [data-hf-id="…"] match and does
not require the hf- shape; legacy values re-mint only at the R7 write-back. Not
a bug. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): fix misleading legacy-id migration comment in htmlParser.ts

The original comment said legacy data-hf-id values "are re-minted only
once the R7 write-back persists freshly-minted ids to source" — which is
incorrect. ensureHfIds skips elements that already carry data-hf-id, so
legacy values (e.g. data-hf-id="my-title") persist indefinitely and are
NOT automatically re-minted. Exact-match targeting still works correctly.
Update comment to reflect actual behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(studio): sourcePatcher data-hf-id targeting (R1, T3)

* fix(studio): warn on duplicate match in execDataAttrPattern (R1, T3 review)

Addresses Rames' review on #1271: execDataAttrPattern returned the first regex
match without checking for a second. A duplicate id/data-hf-id in source (id
drift) would silently patch one element and leave the other stale. Now warns
when more than one element matches. By the mint contract it should never fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(studio): pin hfId-is-authoritative-over-selector contract (R1, T3 review)

Adds test: "hfId match is authoritative — selector is not used as a
narrowing filter". When hfId matches element A and selector points at
element B, findTagByTarget returns A without consulting selector as a
narrowing filter. Pins the intended behaviour so a future refactor
cannot silently start narrowing by selector.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(core): sourceMutation data-hf-id targeting (R1, T7)

* test(core): update htmlParser baselines for R1 hf- id format

Elements now get data-hf-id minted by ensureHfIds; parser reads
data-hf-id as model id, so HTML id attrs are no longer the model id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): data-hf-id survives id/selector patch (R1, T7)

Locks the preservation guarantee the write-back design depends on: a
Studio edit targeting by id or selector (it never sends hfId) must not strip
an existing data-hf-id, or the stable handle is destroyed by the next edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review)

Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated
target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value
injection guard) and warn when a hfId matches more than one element instead of
silently patching an arbitrary one. Adds an injection-guard test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:27:52 -07:00
Vance IngallsandClaude Sonnet 4.6 8d6b7cf515 feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1) (#1270)
* feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1)

* docs(core): document legacy-id round-trip in clip-model readback (R1 review)

Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with
id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable,
exact-match) by design — targeting uses exact [data-hf-id="…"] match and does
not require the hf- shape; legacy values re-mint only at the R7 write-back. Not
a bug. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): fix misleading legacy-id migration comment in htmlParser.ts

The original comment said legacy data-hf-id values "are re-minted only
once the R7 write-back persists freshly-minted ids to source" — which is
incorrect. ensureHfIds skips elements that already carry data-hf-id, so
legacy values (e.g. data-hf-id="my-title") persist indefinitely and are
NOT automatically re-minted. Exact-match targeting still works correctly.
Update comment to reflect actual behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): update htmlParser tests for R1 hf- id minting

Pre-R1 tests expected clip ids to reflect legacy `id=` attributes.
After R1, ensureHfIds runs first and mints data-hf-id — so clip.id
reflects the minted hf- value unless the element already has data-hf-id.

Fix: add explicit data-hf-id to test HTML elements where tests assert
specific id values. Update no-id test to expect hf- format (/^hf-[a-z0-9]{4}$/)
instead of the pre-R1 generated-id fallback (/^element-\d+$/).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:19:31 -07:00
Vance Ingalls c12854b47c feat(core): ensureHfIds node-id pass + shared hf- mint helper (R1) (#1269)
## What

Adds `ensureHfIds(html: string): string` in `packages/core/src/parsers/hfIds.ts`. Single DOM pass (via linkedom) that mints a `data-hf-id` attribute on every eligible element before the caller sees the markup.

**Id derivation:** FNV-1a 32-bit hash of `tagName | sorted-attrs(\x00/\x01 separated) | ownText`, last 4 chars of base-36, `hf-` prefix. Collision resolution appends a sibling counter and re-hashes. Preserves existing ids (elements with `data-hf-id` already set are skipped). Excludes non-visual tags: `script`, `style`, `template`, `meta`, `link`, `noscript`, `base`.

**Fragment handling:** detects bare HTML fragments (no `<!doctype` / `<html`) and wraps in a full document shell before parsing, then returns `body.innerHTML` — matching the pattern used by `parseSourceDocument` in `sourceMutation`.

## Why

Counter-based ids (`element-0`, `element-1`, …) are positional. Inserting a new layer at position 0 shifts every id below it. The R1 milestone requires content-based, stable ids so that targeting operations (split, patch, probe) stay valid across re-parses and element insertions. T2 spec (`stableIds.test.ts`) defines the contract: same content → same id, adding a sibling doesn't change other ids, format matches `/^hf-[a-z0-9]{4}$/`.

## How

- `toHfId(hash)` — `slice(-4)` of `hash.toString(36)` for better distribution across the suffix space
- `data-hf-id` is excluded from the hash input (prevents circular dependency on the attribute being set)
- Already-assigned ids tracked in a `Set`; duplicates get a counter suffix before re-hashing
- Fragment detection: `/<!doctype|<html[\s>]/i.test(html)` — if bare, wrap→parse→`body.innerHTML`

## Test plan

- [x] T2 spec: `packages/core/src/parsers/stableIds.test.ts` — 7 tests pass (3 were `.fails` stubs targeting R1; 4 were pre-existing baselines that must not regress)
- [x] No changes to existing htmlParser tests needed at this layer (wiring is PR #1270)
2026-06-08 23:28:25 -07:00
Miguel Ángel 4adc9b1108 chore: release v0.6.84 2026-06-09 01:30:37 +00:00
Miguel Ángel 908b455e5e fix(core): apply playbackRate to all media duration resolution sites (#1288)
Extract readElementPlaybackRate() to eliminate clamping duplication across
media.ts, init.ts, startResolver.ts, and timeline.ts. Apply the rate
division to the two remaining sites that were missed:

- startResolver.ts: visibility loop used raw source duration, hiding
  slowed-down videos mid-playback when no data-duration was set
- timeline.ts: resolveMediaElementDurationSeconds underreported the end
  window sent to the renderer, affecting preview parity

Also adds direct tests for readElementPlaybackRate().
2026-06-08 21:29:04 -04:00