Commit Graph
71 Commits
Author SHA1 Message Date
Miguel Ángel eee376cb5e chore: release v0.7.6 (#1707) 2026-06-24 20:17:20 -04:00
Miguel Ángel 649c216394 chore: release v0.7.5 2026-06-24 04:41:56 +00:00
Miguel Ángel ba77a0bd72 chore: release v0.7.4 2026-06-24 00:10:04 +00:00
Miguel Ángel 997823b6b5 chore: release v0.7.3 2026-06-22 23:07:48 -04:00
Miguel Ángel b8d6c4aa21 chore: release v0.7.2 2026-06-22 22:44:19 +00:00
Miguel Ángel 3a6742bd13 chore: release v0.7.1 2026-06-22 14:09:55 -04:00
Miguel Ángel a97f80433f chore: release v0.7.0 2026-06-22 11:23:33 -04:00
Miguel Ángel e385d213f3 chore: release v0.6.121 (#1627) 2026-06-21 17:59:01 -04:00
Miguel Ángel 6c71cf0240 chore: release v0.6.120 (#1624) 2026-06-21 11:17:48 -04:00
Vance Ingalls b33a7457a7 chore: release v0.6.119 (#1620) 2026-06-21 01:03:18 -07:00
Miguel Ángel 0d2198e654 chore: release v0.6.118 2026-06-20 18:24:11 -04:00
Miguel Ángel 12d5273812 chore: release v0.6.117 2026-06-20 18:05:59 -04:00
Miguel Ángel 6b279267cd chore: release v0.6.116 2026-06-20 21:25:34 +00:00
Miguel Ángel d6135ca155 chore: release v0.6.115 2026-06-20 00:19:28 +00:00
Miguel Ángel 758eda995c fix(sdk,studio): restore DOM edit cutover parity (#1565)
- Add splitStyleDeclarations with quote/paren-aware CSS parsing
- Fix backslash escape handling inside quoted CSS string values
- Close html-attribute safety gap in SDK cutover (event handlers, dangerous URIs)
- Consolidate HTML attribute safety constants to core/utils/htmlAttrSafety.ts
- Extract NON_HTML_CHILD_TAGS set for foreign-content decline gate
- Add sdkCutoverParity test corpus (shorthand/longhand, mixed batches)
2026-06-19 15:37:10 -04:00
Vance Ingalls 4e32c5e0fe chore: release v0.6.114 2026-06-19 04:38:05 -07:00
Vance Ingalls a38fc4e778 chore: release v0.6.113 2026-06-19 00:35:41 -07:00
Vance IngallsandClaude Opus 4.8 967bf9f9ed refactor(core): gate acorn GSAP writer behind cutover flag; keep recast default (WS-3F) (#1573)
* refactor(core): retire recast/babel, route all GSAP mutations to acorn (WS-E/3.F)

- Delete gsapParser.ts (2595-line recast-based parser/writer)
- Delete gsapParser.test.ts, gsapParser.stress.test.ts, gsapParser.test-helpers.ts
- Add gsapParserExports.ts: re-export umbrella for gsap-parser subpath
- Move SplitAnimationsOptions/SplitAnimationsResult to gsapSerialize.ts
- executeGsapMutation: async->sync, static acorn imports replace loadGsapParser()
- Fix 3 function name mismatches in files.ts switch cases
- generators/hyperframes.ts: imports from gsapSerialize (blocker resolved)
- gsapWriterAcorn.ts: SplitAnimationsOptions from gsapSerialize
- Parity tests: recast oracle removed; acorn-only regression (14 pass)
- Remove recast and @babel/parser from core/package.json

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

* fix(sdk): harden mutation handlers + widen variable API (code-review)

Self-contained review fixes for the SDK-hotspot stack (#1569–#1573). The
dispatch path (_dispatch → applyOp) never runs validateOp, so the new
WS-D/WS-3.C guards were advisory-only; re-enforce them in the handlers.

- addElement: null-guard the resolved parent (no more `as Element` masking a
  null → crash on unknown parent id); reject <script> and multi-root fragments
  via parseInsertableFragment instead of inserting raw markup / silently
  dropping extra roots.
- addWithKeyframes / replaceWithKeyframes: bail on empty keyframes (no
  degenerate `keyframes: {}` tween) and when the animationId resolves to
  nothing (no silent degrade-to-add leaving a duplicate tween).
- isObjectVariableValue: exclude arrays so an array override value can't be
  misclassified as a font/image object and written into the variable model.
- Composition.setVariableValue: widen the public interface signature to
  `… | FontValue | ImageValue` to match the impl + EditOp (B2 object-valued
  variables were unreachable via the typed API).
- mutate.gsap.test.ts: import addKeyframeToScript from gsap-writer-acorn —
  the gsap-parser subpath no longer re-exports write fns after recast retire,
  so the test threw at runtime (red suite).
- Dedup: export EXCLUDED_TAGS from hfIds.ts and drop the verbatim
  HF_EXCLUDED_TAGS copy in mutate.ts.

Adds guard regression tests. SDK 340/340, core hfIds 13/13, build green,
fallow --gate new-only clean.

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

* fix(sdk): variable-model dedup + undo/scoped-parent correctness; test honesty (code-review)

Second batch of review fixes for the SDK-hotspot stack.

- Variable model (#7, #13): extract readVariableDefault/writeVariableDefault into
  a shared engine/variableModel.ts used by both mutate.ts (forward) and
  apply-patches.ts (replay), so the model shape can't diverge. Add
  clearVariableDefault and make a `variable` remove patch DELETE the decl's
  `default` key — the exact inverse of a first-set on a default-less variable.
  Previously undo of such a set no-op'd and stranded the value.
- addElement scoped parent (#8): record the caller's id verbatim
  (scoped "hf-host/hf-leaf" path or composition id) as the patch parentId
  instead of the bare data-hf-id, so redo/replay re-resolves the SAME parent via
  resolveScoped rather than the canonical top-level dup (or document.body).
- resolveTimings honesty (#5): correct the header + test that claimed a live
  "preview == render" parity — neither path consumes the resolver yet (anchor
  inputs are Pacific/backend-deferred). It's a pure-function property, not a
  current guarantee.
- GSAP writer parity (#12): the recast oracle was deleted in WS-3.F, leaving the
  WS-3.C keyframe ops comparing acorn output to itself. Pin them as golden inline
  snapshots and drop the now-dead recast scaffolding (replaceWithKfRecast,
  removeAnimRecast alias). Remaining pre-WS-3.C parity blocks noted as follow-up.

Adds regression tests (undo of default-less variable; scoped-parent redo).
SDK 342/342, core timingResolver+parity green, build + fallow --gate new-only clean.

Not changed (need design / out of scope): #9 pre-#1569 persisted-override CSS
replay (moot for unreleased data; proper fix is render-time CSS derivation),
#11 replaceWithKeyframes stale positional id (mitigated by the missing-id no-op
guard + type doc; full fix needs non-positional ids).

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

* fix(sdk): replay CSS-prop derivation for legacy var overrides; stale-id selector guard (code-review)

Final review-fix batch — the two items deferred from the prior pass.

- #9 legacy variable-override CSS: applyOverrideSet now derives the `--{id}`
  CSS custom prop from any scalar `var.{id}` override on replay (and removes it
  for a null override). Sets written before the model/CSS split carried only
  `var.{id}`; without this, replaying them updated the JSON model but left
  `var(--{id})` bindings rendering the schema default. Replay-path only — the
  undo path (applyOne) is untouched, so #1569's separate-patch undo correctness
  is preserved. Object (font/image) values are never CSS, so they are skipped.
- #11 stale positional id: replaceWithKeyframes now requires the located
  animation to still target the caller's `targetSelector`. Position-derived ids
  re-point after structural edits; a stale id resolving to a DIFFERENT element's
  tween previously got silently replaced. It now bails (no-op) unless the id
  still points at the expected selector.

Adds regression tests (legacy var.{id}-only override restores CSS; object
override writes no CSS; stale-id-wrong-selector replace is a no-op).
SDK 345/345, build + fallow --gate new-only clean.

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

* refactor(core): gate acorn GSAP writer behind cutover flag; keep recast default (WS-3F)

Product decision pivot: acorn no longer replaces recast as the GSAP writer.
Recast remains the default server writer; acorn runs only when
STUDIO_SDK_CUTOVER_ENABLED=true (or =1) is set server-side — the same env
flag name as the client Vite var, so a single switch flips both sides.

Changes:
- Restore gsapParser.ts (recast writer) + test/stress/helper files deleted by 3F
- Restore @babel/parser + recast deps in packages/core/package.json
- Add isAcornGsapWriterEnabled() + loadGsapParser() to files.ts (lines 59-82)
- Split executeGsapMutation into async dispatcher + executeGsapMutationRecast
  (recast, async via loadGsapParser) + executeGsapMutationAcorn (acorn, sync)
- Dispatcher defaults to recast; acorn branch taken only when flag is on
- Restore gsapWriter.parity.test.ts, gsapWriterParity.acorn.test.ts, and
  gsapWriterParity.corpus.test.ts to true recast-vs-acorn differential suites
  (not acorn-vs-itself)
- Exempt gsapParser.ts in .fallowrc.jsonc health.ignore + ignoreExports
  (pre-existing complexity + barrel re-exports consumed outside diff scope)
- Add fallow-ignore-file code-duplication to files.ts (intentional parallel
  switch bodies for two writers)

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 00:22:14 -07:00
Vance IngallsandClaude Opus 4.8 37efbcb955 feat(sdk): image-alpha hit-test phase 1 (WS-G) (#1574)
* feat(sdk): image-alpha hit-test phase 1 (WS-G)

Extends the WS-A1 iframe adapter with image-alpha hit-testing:

- Replace `elementFromPoint` with `elementsFromPoint` (z-stack) so
  a transparent-image hit falls through to the layer behind.
- For `<img>` hits: map client point → natural-pixel coords via a
  pure `mapPointToImagePixel` fn (object-fit cover/contain/fill aware);
  draw to an offscreen canvas once (cached by `currentSrc`); sample
  alpha via pure `alphaIsOpaque`. Transparent pixel → miss, continue
  the stack.
- Cross-origin images that taint the canvas → SecurityError fallback
  → treat pixel as opaque (never drop an unverifiable hit).
- Phase 2 (per-pixel alpha via `drawElement`) NOT built; gated on a
  perf spike per plan.

Tests: alphaIsOpaque thresholds, mapPointToImagePixel (fill/cover/contain
+ out-of-box→null), z-stack fallthrough, taint→opaque fallback, non-image
WS-A1 regression.

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

* fix(sdk): correct image-alpha hit-test edge cases (WS-G review)

- map within the content box, not the border box (object-fit positions
  the image inside border+padding; getBoundingClientRect was off for a
  bordered/padded <img>)
- normalize vertical-first object-position keyword pairs ("bottom left")
- guard natural.width/height===0 in cover/contain (Infinity/NaN scale)
- fall back to elementFromPoint when elementsFromPoint is unavailable
- guard `instanceof win.HTMLImageElement` when the constructor is absent
- bound _imgCanvasCache with a FIFO cap so it can't leak one canvas per src
- drop the redundant taint-probe getImageData; the real pixel read already
  surfaces lazy taint
- one opacity walk per candidate: the hf node is on the already-checked
  ancestor chain, so the resolver no longer re-walks for visibility
- remove dead `fit==="fill"||` clause and unused resolveToken param
- tests: none-fit, object-position keyword/px/reversed-pair, zero-natural,
  no-throw-without-HTMLImageElement + elementsFromPoint fallback

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

* fix(sdk): image-alpha review fixes (WS-G) — transform/taint/cache-key/memory

Addresses the phase-1 gaps flagged in review (was documentation-only):
- CSS rotation/skew on the image or an ancestor now fails safe to opaque instead
  of sampling the wrong pixel (getBoundingClientRect is axis-aligned). Full
  transform-inverse mapping stays phase 2. No-op where DOMMatrix is unavailable.
- Cross-origin canvas taint now warns once per src (was silent) so the
  fall-back-to-opaque path is visible, not "hit-test feels wrong".
- Canvas cache keyed on src + natural dimensions (was src only) so a
  srcset/responsive re-render of the same URL doesn't reuse a stale canvas.
- Pathological-size guard: images above a pixel budget skip alpha-testing
  (opaque) to bound OffscreenCanvas memory.
- Docs: border/padding clicks fall through (intentional) noted on imageAlphaOpaqueAt.

Tests: removed the duplicate a=0 threshold case; added transparent-over-
transparent-over-div fallthrough. iframe.test.ts 59/59. tsc/oxlint/oxfmt green.

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-19 00:12:45 -07:00
Vance IngallsandClaude Opus 4.8 dd6fad6bb2 fix(sdk): code-review follow-ups (WS-B/C/3.C, #1569/#1570/#1572) (#1588)
Addresses the still-outstanding review concerns from merged PRs #1569 / #1570 /
#1572 not already hoisted into #1573.

WS-B (#1569):
- validateVariables requires discriminant fields for object-valued font/image
  ({name,source} / {url}); a {name:42} font or {foo:42} image previously passed
  runtime validation and surfaced as a bogus font-family / missing image.
- Dropped ImageValue's [key:string]:unknown index signature (let any {url}-shaped
  object through, swallowed typos); explicit alt?/fit? instead.
- Documented the OverrideSet widening for SDK consumers.

WS-C (#1570):
- getElementTimings caches parsed GSAP labels by exact script text (avoids a full
  acorn re-parse per read; content-key invalidates on edit).
- Documented end-inclusive label window + best-effort extractGsapLabels catch.

WS-3.C (#1572):
- Added typed Composition.addWithKeyframes / replaceWithKeyframes (was asymmetric
  with addGsapTween; Studio had to use raw dispatch).
- Extracted shared KeyframeSpec type; documented position as seconds/number-only.

Gates: build + core 18/18 + sdk 19/19 + oxlint + oxfmt + fallow + typecheck all green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 00:07:53 -07:00
Vance IngallsandClaude Opus 4.8 418f198b33 feat(sdk): ws-3c — addWithKeyframes + replaceWithKeyframes SDK ops (acorn writer) (#1572)
* feat(sdk): ws-b variables/brand — object-valued font/image + B1 JSON model

B1 — setVariableValue now drives the runtime JSON model
(data-composition-variables) so preview == render. CSS custom prop is
kept as a secondary compat write for compositions that CSS-bind directly
to --{id}.

B2 — object-valued font ({name, source}) and image ({url}) variable
types added core-to-SDK. Object values write to the JSON model only;
scalars write both model + explicit CSS style patches. Explicit style-path
patches in forward/inverse ensure apply-patches.ts handles each path type
purely (model vs CSS), so inverse patches restore exact pre-call state
without ambiguity.

Changed files:
  packages/core/src/core.types.ts         — font/image to CompositionVariableType + interfaces
  packages/core/src/lint/rules/composition.ts — accept font/image in lint message
  packages/core/src/parsers/htmlParser.ts     — validate font/image variable declarations
  packages/core/src/parsers/htmlParser.test.ts — tests for new variable types
  packages/core/src/runtime/validateVariables.ts — checkType for font/image
  packages/sdk/src/types.ts            — FontValue/ImageValue; widen OverrideSet + EditOp
  packages/sdk/src/index.ts            — re-export FontValue/ImageValue
  packages/sdk/src/session.ts          — widen setVariableValue signature
  packages/sdk/src/engine/patches.ts   — valueChange helper for object-valued patches
  packages/sdk/src/engine/mutate.ts    — handleSetVariableValue: B1+B2 with explicit CSS patches
  packages/sdk/src/engine/apply-patches.ts — variable case: model-only (CSS via explicit patch)
  packages/sdk/src/engine/mutate.test.ts   — B1+B2 round-trip + inverse tests

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

* feat(sdk): ws-c elastic timing + word-alignment resolver (WS-C)

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>

* feat(sdk): addElement forward op — mint hf-id, inverse = removeElement (WS-D)

Implements WS-D: the addElement EditOp and session.addElement() typed method.

- types.ts: addElement op (parent/index/html) added to EditOp union;
  addElement(parent, index, html): HfId added to Composition interface
- mutate.ts: handleAddElement inserts a single-root HTML fragment at
  parent+index, minting ids against the LIVE document's existing id set
  (not a fresh fragment set) via collectDocumentHfIds + mintFragmentIds;
  forward = patchAdd, inverse = patchRemove; MutationResult.meta.newId
  carries the minted root id
- mutate.ts: validateOp case rejects missing parent, negative index,
  empty html, zero-element html, and <script> in html
- session.ts: typed addElement(parent, index, html) returns minted id
  via result.meta.newId
- mutate.test.ts: 16 tests covering insert position, append semantics,
  id uniqueness, content-collision rehash, nested fragments, forward/
  inverse symmetry, undo, add/undo/redo stability, parent:null body
  insertion, serialize round-trip, and all five validateOp rejection codes

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

* feat(sdk): ws-3c — addWithKeyframes + replaceWithKeyframes SDK ops (acorn writer)

Port add-with-keyframes / replace-with-keyframes from the server recast path
to the acorn/magic-string writer and expose them as typed SDK EditOps.

- gsapWriterAcorn.ts: extend buildKeyframeObjectCode and
  addAnimationWithKeyframesToScript to accept `auto?: boolean` on keyframes
  (emits `_auto: 1` matching the recast writer)
- types.ts: add `addWithKeyframes` and `replaceWithKeyframes` to EditOp union
- mutate.ts: add handleAddWithKeyframes, handleReplaceWithKeyframes, and
  applyGsapWithKeyframesOp dispatch sub-function; validateOp cases for both
- sdkCutover.ts: add sdkAddWithKeyframesPersist + sdkReplaceWithKeyframesPersist
  (shared via dispatchWithKeyframes to eliminate clone)
- useGsapAnimationOps.ts: wire addWithKeyframes + replaceWithKeyframes
  callbacks with SDK-first / server fallback pattern
- gsapWriter.parity.test.ts: add parity tests for _auto endpoint round-trip and
  replaceWithKeyframes (remove + addWithKeyframes) differential golden harness;
  import removeAnimationFromScript from both writers

Landmine note: tween IDs are position-derived — replaceWithKeyframes removes the
old tween (renumbering survivors) then inserts the replacement at the end; the
MutationResult patch pair restores the whole GSAP script on undo, not a per-ID
inverse, so ID-held references in callers must re-parse after structural edits.

Gate: WS-3.F (retire recast / executeGsapMutation) — NOT started here.
Remaining Studio callers (gsapDragCommit, gsapRuntimeBridge, useGestureCommit,
useEnableKeyframes) remain on the server commitMutation path until WS-3.F.

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-18 23:15:02 -07:00
Vance IngallsandClaude Opus 4.8 0ca01c88cf feat(sdk): addElement forward op — mint hf-id, inverse = removeElement (WS-D) (#1571)
* feat(sdk): ws-c elastic timing + word-alignment resolver (WS-C)

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>

* feat(sdk): addElement forward op — mint hf-id, inverse = removeElement (WS-D)

Implements WS-D: the addElement EditOp and session.addElement() typed method.

- types.ts: addElement op (parent/index/html) added to EditOp union;
  addElement(parent, index, html): HfId added to Composition interface
- mutate.ts: handleAddElement inserts a single-root HTML fragment at
  parent+index, minting ids against the LIVE document's existing id set
  (not a fresh fragment set) via collectDocumentHfIds + mintFragmentIds;
  forward = patchAdd, inverse = patchRemove; MutationResult.meta.newId
  carries the minted root id
- mutate.ts: validateOp case rejects missing parent, negative index,
  empty html, zero-element html, and <script> in html
- session.ts: typed addElement(parent, index, html) returns minted id
  via result.meta.newId
- mutate.test.ts: 16 tests covering insert position, append semantics,
  id uniqueness, content-collision rehash, nested fragments, forward/
  inverse symmetry, undo, add/undo/redo stability, parent:null body
  insertion, serialize round-trip, and all five validateOp rejection codes

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-18 23:05:41 -07:00
Vance IngallsandClaude Opus 4.8 f65e229663 feat(sdk): ws-c elastic timing + word-alignment resolver (WS-C) (#1570)
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>
2026-06-18 23:05:06 -07:00
Vance Ingalls d0e520dbd9 feat(sdk): ws-b variables/brand — object-valued font/image + B1 JSON model (#1569)
## WS-B — variables / brand, object-valued (end-to-end)

Part of the AI Studio (Pacific) SDK integration. **Base of the SDK-hotspot stack** (`main → ws-b → ws-c → ws-d → ws-3c → ws-3f`).

### Problem
The variable system was split-brained: SDK `setVariableValue` wrote a `--{id}` CSS custom prop, while the runtime `getVariables()` read a separate JSON model (`data-composition-variables` / `__hfVariables`). The two never connected, and there was no `--brand-*` convention. Variables were scalar-only.

### What this does
- **B1 — one source of truth.** `setVariableValue` now drives the runtime variable model (`data-composition-variables` / `__hfVariables`), with CSS compatibility emitted as explicit `stylePath`-based patches alongside the model patch. A brand kit is a variables JSON; a batch of `setVariableValue` re-skins in one frame.
- **B2 — object-valued variables.** The `CompositionVariable` union extends from scalar-only to typed objects: `font` (`{name, source}`) and `image` (`{url, …}`), end-to-end (core union → SDK op → runtime merge). Colors stay scalar (per §7 LOCKED decision).

### Implementation notes
CSS compatibility was moved out of `apply-patches.ts` (where it was incorrectly writing CSS props as a side-effect of model patches, breaking inverse/undo) and into explicit patches emitted in `mutate.ts`. Forward emits `[modelPatch, cssPatch]` for scalars; inverse correctly generates `patchRemove` for the CSS prop when there was no prior CSS prop. Font/image variables never become CSS props.

### Files (12 changed, +441 −32)
- `packages/core`: `core.types.ts`, `lint/rules/composition.ts`, `parsers/htmlParser.ts` (+test), `runtime/validateVariables.ts`
- `packages/sdk`: `engine/mutate.ts` (+test), `engine/apply-patches.ts`, `engine/patches.ts`, `index.ts`, `types.ts`

### Gates
- `bun run build` 
- `bun test` SDK 304/0  · `validateVariables.test.ts` 13/0 
- `bunx oxlint` 0/0  · `bunx oxfmt --check` 
- `fallow audit --gate new-only`  (complexity inherited only)

> The +8 new `htmlParser.test.ts` font/image tests fail under the pre-existing `DOMParser is not defined` happy-dom limitation (main already carries 425 such failures) — not a logic bug; the pure runtime logic is covered by `validateVariables.test.ts`.

### Deferred
Brand-kit picker UI and `batch(setVariableValue × N)` wiring are Pacific-side; per-composition variable scoping beyond `__hfVariablesByComp`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-18 23:03:52 -07:00
Vance Ingalls 1bab79ef4c chore: release v0.6.112 2026-06-18 01:14:30 -07:00
Vance IngallsandClaude Opus 4.8 28bfe09f21 feat(sdk): ws-3 — reorderElements op (batch z-index update) (#1502)
* feat(sdk): ws-3 — reorderElements op (batch z-index update)

Adds the reorderElements EditOp: each entry sets inline zIndex on one element.
Last-write-wins per target so a duplicated target collapses to a single zIndex
patch. Positioning is unchanged — z-index only takes effect on non-static
elements, so the caller must ensure the target is positioned.

Also fixes single-dispatch undo to reverse the inverse patch list (parity with
batch()): an op emitting multiple patches whose undo order matters — a duplicated
reorderElements target, an aliased multi-target, or a nested parent+child
removeElement — must undo in reverse application order, or undo lands on an
intermediate value / drops a subtree.

validateOp resolves every entry target (E_TARGET_NOT_FOUND for unknown ids;
empty entries is a clean no-op). Tests cover set/inverse/validate/duplicate-target.

Rebuilt standalone on main (reorderElements only depends on handleSetStyle).

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

* refactor(sdk): remove unused HTTP persist adapter

The HTTP PersistAdapter (createHttpAdapter) was dead weight after the studio
cutover went single-writer (ws-4): Studio's writeProjectFile is the sole writer
and useSdkSession opens with no persist queue, so the adapter's write/flush/
listVersions/loadFrom were never used — only read() was, to fetch the
composition source. Replace those two read() calls with a direct optional fetch
(GET /files/<path>?optional=1) and drop the adapter + its export-map entries.

Saved for later re-introduction (when a non-Studio SDK host needs server-backed
persist) at docs/hyperframes/plans/sdk-http-adapter/ (outside the repo).

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

* feat(studio): resolver shadow for z-index reorder targets

The z-index reorder commit takes the server path (no SDK persist), but the
resolver-shadow tripwire is decoupled from cutover — so it should still record
whether the SDK resolves each reordered element (reorderElements' targets), the
same as timing/delete already do before their cutover gate. This gives wild
resolver-parity telemetry on z-index targets before z-index reorder is cut over.

Threads an onReorderShadow callback (sdkSession-bound, mirrors onTrySdkDelete)
from useDomEditSession → useDomEditCommits → useElementLifecycleOps, called with
the reordered elements' hf-ids in handleDomZIndexReorderCommit. Read-only,
divergence-only, never throws — same contract as recordResolverParity elsewhere.

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

* fix(studio): guard project-file read path against traversal (CodeQL CSRF)

readProjectFileOptional interpolated a user-influenced composition path into the
fetch URL, which CodeQL flagged as client-side request forgery. Reject NUL/`..`
up front (mirrors the existing guard in timelineEditingHelpers) and
encodeURIComponent the projectId too, so both values stay confined to single
segments of the same-origin URL. Unsafe path → undefined (graceful for the
optional read).

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-17 23:51:47 -07:00
Vance Ingalls de87f3932e chore: release v0.6.111 2026-06-17 22:28:29 -07:00
Vance IngallsandClaude Opus 4.8 e57e75b9b4 fix(sdk,studio): R5 cutover review fixes (on top of #1539) (#1545)
* fix(sdk,studio): R5 cutover review fixes — fromTo dest, timing sync, parity

Confirmed correctness findings from the R5 review of the SDK cutover stack,
applied on top of #1539:

- fromTo add via cutover dropped its destination: handleAddGsapTween read only
  `toProperties`; now falls back to `properties` like every other method.
- handleSetTiming GSAP sync: a clip with no data-start skipped the shift (now
  treats start as 0, matching the server path) and a blank/non-numeric
  data-start wrote position: NaN (now sanitized).
- handleSetTiming no longer appends an absolute position to an auto-sequenced
  (implicit-position) tween, which collapsed staggers.
- handleSetTiming keeps data-end in sync when a clip carries BOTH data-duration
  and data-end (a stale data-end inverted the clip).
- string/relative tween positions ("+=0.5", "<") documented as a known ceiling.
- opacity/autoAlpha property seed no longer falsy-zero (`|| 1`): an element at
  opacity 0 seeds 0, not 1.
- optimistic add-keyframe cache tolerance aligned to the writer's PCT_TOLERANCE
  (2%) so a near-neighbour keyframe no longer shows then vanishes on reload.
- DOM-patch finiteness validation runs before the SDK cutover path.
- attribute ops mapping to a reserved data-* name decline the cutover up front
  instead of throwing inside dispatch.

Regression tests added for each SDK-side fix.

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

* fix(studio): close two gaps in the reserved-attr cutover gate

- Lowercase the mapped attribute name before the reserved check, matching the
  SDK's validateSetAttribute (which lowercases), so a case-variant reserved
  name is declined up front instead of throwing inside dispatch.
- Also gate `html-attribute` ops (raw, non-prefixed names), not just bare
  `attribute` ops. Both the emitter and the gate now derive the name via one
  shared `sdkAttrName` helper so they can't drift.

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

* fix(studio): match keyframe remove-path tolerance to the writer (mirror of add)

The optimistic remove-keyframe cache filtered with `> 0.001`, dropping only a
near-exact match, while the writer removes within PCT_TOLERANCE (2). Removing
at e.g. 49% dropped a 50% keyframe on disk but left it in the cache — a phantom
that vanished on reload, the inverted twin of the add-path tolerance fix.
Now filters with `> 2` to match.

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-17 17:15:16 -07:00
Vance IngallsandClaude Opus 4.8 8c981a451a fix(studio): restore timeline move/resize fallback parity (review #1466) (#1539)
* fix(studio): restore timeline move/resize fallback parity (review #1466)

The §3.2 sdkTimingPersist rewrite regressed the non-SDK fallback path vs the
pre-cutover behavior. Restored, on both fallback entry points (no-session and
sdkTimingPersist-returned-unhandled):

- Resize live DOM patch dropped the conditional data-playback-start/media-start
  attr — restored so a start-trim updates the preview's in-point immediately.
- Move/resize fallback dropped the GSAP-position sync (shift/scaleGsapPositions)
  + reloadPreview — restored so server-path edits keep GSAP tweens in sync and
  refresh the preview (the SDK path folds both into setTiming).
- Undo-coalesce drift: fallback enqueueEdit carried no coalesceKey while the SDK
  branch did — plumbed coalesceKey through persistTimelineEdit so undo
  granularity is identical on either path.
- Documented the hasPbsAdjustment second clause + sdkTimingPersist before-capture
  transition limitation.

Flag-off (dark launch) so this lands as one fix PR at the stack tip rather than
restacking the mid-stack §3.2 commit. #1500 review items: parity-harness gap
already closed at the tip (arc/unroll recast-vs-acorn parity added); blockRemoveRange
flagged 'potential' but verified correct (no comma residue on any block position).

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

* fix(sdk): retire duplicate removeGsapKeyframe keyframeIndex variant (review #1498)

EditOp had two removeGsapKeyframe members with the same discriminant but
different shapes (keyframeIndex vs percentage) — TS can't discriminate them and
a handler could get the wrong shape. Per both reviewers (option 2): retire the
keyframeIndex variant. It had no production caller (Studio dispatches percentage
only); removed the dead by-index handleRemoveGsapKeyframe + simplified the
dispatcher. resolveKeyframe stays (setGsapKeyframe still uses keyframeIndex).
Converted the one by-index test to the percentage API.

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

* fix(studio): gate ALL cutover persist paths on the flag — true dark launch (review #1469 finding #6)

Only sdkCutoverPersist (style/text/attr) checked STUDIO_SDK_CUTOVER_ENABLED.
sdkTimingPersist, dispatchGsapOpAndPersist (every GSAP op) and sdkDeletePersist
guarded only on `!sdkSession` — and useSdkSession opens a session by default
for shadow/selection, so timing/GSAP/keyframe/delete cutover was ALWAYS live
regardless of the flag. Flipping the flag OFF could not disable it, so the
data-loss bugs in those paths (single-prop wipe, wrong-keyframe match, tween
collapse, arc strip) ship LIVE on merge instead of being dark-launched.

Added the flag guard at all three chokepoints → flag OFF returns false → callers
fall back to the legacy server path. Makes the stack genuinely dark-launchable:
merge is now a no-op in prod, and the remaining cutover correctness bugs become
flip-prerequisites rather than merge-blockers.

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

* fix(core,sdk): correct 8 GSAP write-path review findings (#1539)

Eight correctness bugs from the SDK-cutover review. Several were cases where
BOTH writers were identically wrong, so the recast-vs-acorn parity suite stayed
green; the new tests assert the real-world-correct result, not agreement.

- #2 findKfPropByPct: match the CLOSEST keyframe within tolerance, not the first
  within 2% — removing/updating 50% on 0/49/50/100 no longer hits 49%.
- #3 handleSetTiming: shift each tween by the start DELTA and scale duration by
  the clip-duration RATIO per-tween, instead of writing absolute newStart/
  newDuration onto every tween (which collapsed staggers and blew durations).
- #4 enableArcPath: insert motionPath via appendRight at the object start so the
  insertion can't collide with the x/y remove-range end (which made MagicString
  discard the append and emit '{}').
- #5 splitAnimationsInScript: compute the inherited baseline in a forward pre-pass
  so the split-spanning midpoint sees earlier tweens (the reverse write loop is
  kept for stable count-suffixed ids).
- #9 unrollDynamicAnimations: preserve non-target loop-body statements (e.g.
  tl.set initial-state) per iteration instead of overwriting the whole loop.
- #10 buildMotionPathObjectCode (both writers): emit the cubic form when segment
  curviness varies so per-segment curviness survives, not just segments[0].
- #11 readLastWaypointXY: handle UnaryExpression so negative destination coords
  are recovered when disabling an arc path.
- #15 no-bang: removed every `!` non-null assertion in the touched files,
  replaced with guards/fallbacks.

Tests: gsapWriter.reviewFixes.test.ts (#2/#4/#5/#9/#10/#11) and
mutate.gsap.test.ts setTiming GSAP-sync block (#3). All fail on the base and
pass after the fix; tsc + full core/sdk suites + parity stay green.

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

* fix(studio): SDK cutover review fixes — merge tween props, stabilize debounce, serialize gsap writes, on-disk undo baseline, self-write identity

Addresses 5 SDK-cutover review findings (studio-only):

- #1 useGsapPropertyDebounce: editing one GSAP tween property no longer drops
  the tween's other animated props. setGsapTween REPLACES the property set, so
  merge the single edit into the tween's CURRENT properties (read from the SDK
  doc) before dispatching, mirroring the legacy server merge.
- #7 useGsapPropertyDebounce: stabilize the flush callback by reading sdk deps
  from a ref instead of an unmemoized literal, so a parent re-render mid-edit
  no longer tears down + flushes the debounce (one commit/undo entry per render).
- #8 sdkCutover/useGsapScriptCommits: route SDK gsap-write persists through the
  same per-file keyed serializer the legacy commitMutation uses, so concurrent
  same-file read-modify-writes can't interleave and lose an edit.
- #12 sdkCutover/useTimelineEditing: capture the exact on-disk bytes as the undo
  'before' for timing/GSAP persists (matching the style/delete paths) instead of
  a normalized SDK serialize() re-emit that reformatted the whole file on undo.
- #14 useSdkSession/sdkSelfWriteRegistry: discriminate a cutover echo from an
  undo write by CONTENT identity (registered self-write hash), not just the 2 s
  timestamp window — an undo write always reloads the SDK session.

Tests: useGsapPropertyDebounce(.test), useGsapPropertyDebounceFlush.test,
sdkSelfWriteRegistry.test, and new sdkCutover.test cases; each reproduces the
review scenario and asserts the corrected behavior (verified red before fix).

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

* refactor(core): extract split/collapse helpers to satisfy no-fallow-ignore rule

The #5 (split) and #15 (no-bang guards) fixes pushed splitAnimationsInScript and
removeAllKeyframesFromScript over fallow's complexity threshold, and a fallow-ignore
had been added to splitAnimationsInScript. Per the hard rule (never ignore — fix),
extracted buildSpanningSplit + applyTweenSplit (split) and buildCollapsedFlatVars
(collapse), and removed the ignore. Both functions now under threshold; fallow new-only
gate reports 0 new findings. Behavior unchanged — core 1811 green.

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

* test(studio): pin dark-launch flag-gate contract (review #1539, Rames/Via)

flag OFF ⇒ sdkTimingPersist / sdkGsapTweenPersist (GSAP-op chokepoint) /
sdkDeletePersist all return false even with a valid session → legacy fallback.
The prod flag-flip rests on this contract; sdkCutover.test.ts only mocks the flag
TRUE, so a future gate refactor could silently re-enable cutover on flag-off
without failing CI. This sibling file mocks it FALSE and locks the three guards.

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

* fix(studio): leading flag-gate on sdkGsapTweenPersist (review #1539 nit, Via)

The add-op getElement existence check ran before the inner gate, so flag-off did
an SDK touch before falling back. Lead with the flag guard to match the other
three chokepoints — flag-off is now a clean no-op at every entry point.

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

* fix(core): unroll-preservation regressions — non-for loops + AST index substitution (review R2)

The #9 unroll-preservation fix had two confirmed regressions:
- Non-for loops (forEach/for-of/for-in/while): loopIndexVarName returns null, so
  substitution no-op'd and preserved siblings kept a now-undefined loop variable
  (e.g. `item`) → ReferenceError at render. Now returns null for those forms →
  caller falls back to the blanket loop overwrite (drops siblings, valid code).
  The #9 fixture only used `for(let i…)` so it never caught this.
- substituteLoopIndex did a \bvar\b regex over raw source including string
  literals, corrupting selectors like ".row-i" → ".row-0". Now AST-based:
  substitutes only real Identifier uses, skipping string literals and non-computed
  member/key positions (extracted isIndexBindingPosition helper to stay under the
  fallow complexity threshold — no ignore added).

Two regression tests added (forEach no-dangling-var; for-loop string-literal intact).

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

* fix(sdk,core): unrollDynamicAnimations rejects empty element list (R1 #1501b)

An empty `elements` array has no unrolled form — the writer would overwrite
the loop/statement with zero tween calls, silently deleting the animation.

- gsapWriterAcorn: unrollDynamicAnimations returns the script verbatim on an
  empty list (no-op instead of a destructive overwrite).
- validateOp: reject unrollDynamicAnimations with empty elements as
  E_INVALID_ARGS so callers get a clean error rather than silent corruption.
- Tests: writer no-op on []; validateOp E_INVALID_ARGS on [].

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

* perf(sdk): cache draft element in applyDraft, drop HTMLElement casts (R1 #1490a)

applyDraft runs at 60fps during a drag but re-ran doc.querySelector on every
call — the _draftEl/_draftId fields were only consumed by commit/cancel, never
to skip the query. Reuse the tracked element when the id matches and the node
is still connected; re-query only on id change or detach (iframe reload).

Retypes _draftEl to HTMLElement | null (only ever set from
querySelector<HTMLElement>), which removes the `as HTMLElement` casts in
commitPreview / _clearDraft. Test asserts a repeated same-id drag queries once.

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

* fix(sdk,core): round-3 correctness — unroll AST safety, single-dispatch undo, empty-arg guards, persist decouple

Addresses the highest-severity round-3 review findings:

- gsapWriterAcorn unroll (R3 #1/#2/#9): the round-2 AST-substitution fix emitted
  invalid GSAP for object shorthand `{ i }` (→ `{ 0 }`) and shadowed inner
  bindings (→ `for(let i=0;0<3;0++)`), and silently dropped sibling statements on
  non-`for` loops (forEach/for-of). The unroll now REFUSES (no-ops, leaving the
  dynamic loop intact) whenever siblings can't be safely reproduced — a non-`for`
  loop, an unmodeled statement, or an unsafe index use — instead of dropping or
  corrupting. Plain `for` loops with safe siblings still unroll.

- session single-dispatch undo (R3 #5/#11): _dispatch now reverses the inverse
  patch list (parity with batch()). A single op emitting order-dependent inverse
  patches — a nested parent+child removeElement, an aliased multi-target — undid
  forward and dropped the child subtree / landed on an intermediate value.

- materializeKeyframes empty-array (R3 #10): the unguarded twin of the just-fixed
  unrollDynamicAnimations. Writer no-ops on an empty keyframe list; validateOp
  rejects it as E_INVALID_ARGS (shared gsapScriptMissing helper).

- history:false persist decouple (R3 #4): persist (auto-save) no longer lives
  inside the history-enable block, so opting out of SDK undo no longer silently
  disables all disk writes (data-loss trap for #1496's flag consumers).

Tests: unroll refuse cases (shorthand/shadow/forEach) + safe-for-loop regression;
nested removeElement undo; materializeKeyframes writer no-op + validateOp reject;
history:false-still-persists.

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

* fix(core): stripGsapForId re-parses per removal so all tweens for a deleted element are stripped (R3 #3)

Animation ids are count-based (positional), so removing one tween renumbers the
survivors. stripGsapForId captured every matching id from a single up-front parse
then removed against the mutating script — after the first removal the later ids
were stale and silently no-op'd, leaving an orphaned tl.to() referencing the
just-deleted element. Now re-parse after each removal and strip the first
still-matching animation until none remain.

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

* fix(core): gsap writer — keyframe ease routing, convert preserves delay, addLabel dedup (R3 #7/#8/#12)

- #7: updateAnimationInScript routes an ease update on a keyframe tween to
  keyframes.easeEach (per-keyframe), not a top-level ease that GSAP ignores —
  the user's keyframe-easing edit was silently a no-op.
- #8: convertToKeyframesFromScript now preserves every non-editable vars key
  (delay/callbacks/stagger/yoyo/…) verbatim via preservedVarsEntries instead of
  rebuilding from the GsapAnimation object, which had no `delay` field and
  dropped it — shifting the tween's start time.
- #12: addLabelToScript moves an existing same-named label (overwrites its
  position) instead of appending a duplicate; duplicates made removeLabel
  over-remove (it deletes every match, including a pre-existing label).

Tests: easeEach routing, delay preservation, addLabel move-not-duplicate +
hand-authored-dup removal. Updated the old "no dedup contract" corpus test.

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

* fix(sdk): handleSetTiming #domId + data-duration sync; validateOp resolves ids + arc/selector (R3 #6/#13, CF2 #15/#16)

CF2 #15: handleSetTiming re-synced GSAP tweens only when the selector matched the
element's hf-id. The common #domId-targeted tween (authored by the Studio panel)
never matched, so moving/resizing a clip via the SDK timing path left its
animations unsynced. Now match the tween selector against the DOM id too.

CF2 #16: handleSetTiming read/wrote only data-end. Clips authored with
data-duration (what the runtime prefers) got a fresh data-end beside a stale
data-duration (no playback change) and oldDuration=null collapsed the GSAP
duration-scale ratio to 1. Now read duration preferring data-duration, and write
back to whichever attribute the clip uses (timingPath gains a "duration" field).

R3 #13b: deleteAllForSelector compared selectors with strict === and missed the
alternate quote style ([data-hf-id='x'] vs "x"); now quote-insensitive.

R3 #6/#13a: validateOp now resolves the animationId for id-bearing GSAP ops
(E_TARGET_NOT_FOUND instead of a misleading ok that no-ops at apply), and
updateArcSegment validates the arc is enabled + the segment index is in range.

Tests: #domId move sync, data-duration resize + scale, quote-insensitive delete,
unresolved-id rejection, arc-segment preconditions. Updated the loose-can() test.

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

* refactor(core,sdk): name the acorn-node type alias; keyToPath round-trips timing.duration (R3 #14)

- gsapWriterAcorn: replace the bare `: any` AST-node annotations with the named
  `type Node = any` alias, matching the established convention in
  gsapParserAcorn.ts / gsapInline.ts ("acorn ESTree nodes are structurally
  untyped"). Documents intent and is greppable; type-identical (zero runtime
  change). A full ESTree typing is a deliberate architecture decision the
  codebase has not taken and is out of scope here.
- patches: keyToPath/timingPath now include the "duration" timing field added
  for the data-duration resize fix, so a timing.duration override round-trips on
  T3 replay instead of being dropped.

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

* fix(sdk): cascadeRemoveAnimations re-parses per removal (R4 — SDK twin of #3)

cascadeRemoveAnimations captured every matching animation id from a single
up-front parse, then removed against the mutating script — the SDK-side twin of
the stripGsapForId bug (R3 #3). Animation ids are positional, so removing the
first tween for an element renumbered the survivors and the stale later ids
no-op'd, orphaning those tweens on the just-removed element. Now re-parse after
each removal and strip the first still-matching animation until none remain.

Also adds the reviewer's defense-in-depth test: an aliased multi-target setStyle
(same id twice) undoes to the original, not the intermediate (exercises the
single-dispatch inverse reversal from R3 #5/#11).

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-17 16:55:34 -07:00
09cefc1bb7 feat(sdk,core): ws-3 — unrollDynamicAnimations acorn port + SDK op (#1501)
* feat(sdk,core): ws-3 — unrollDynamicAnimations acorn port + SDK op

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* test(core): recast-vs-acorn parity + acorn fixes for arc/unroll/keyframe-add/%-removeKeyframe/add-with-keyframes (WS-3.F gate)

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

* feat(core): port shiftPositions/scalePositions to acorn writer (WS-3.F)

shiftPositionsInScript + scalePositionsInScript were recast-only GSAP-script
writers reachable from executeGsapMutation (shift-positions/scale-positions),
called by Studio timeline clip move/resize — the last write ops blocking recast
retirement. Ported to gsapWriterAcorn.ts mirroring recast's arithmetic
(shift: max(0,pos+delta); scale: remap pos by duration ratio + scale duration),
reusing a shared overwritePosition helper (also adopted by updateAnimationInScript).
Adds 10 recast-vs-acorn parity tests. Closes the WS-3.F op-coverage gate.

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

---------

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:54:04 -07:00
Vance IngallsandMiguel Ángel 1612d18fdf feat(sdk): stage 6 — arc path ops (setArcPath, updateArcSegment, removeArcPath) (#1500)
Port arc path trio from recast to browser-safe acorn+MagicString writer.
Add SDK op types and mutate.ts handlers for setArcPath / updateArcSegment /
removeArcPath. Decompose buildMotionPathObjectCode into small sub-functions
in gsapSerialize.ts to stay within fallow complexity thresholds. Tests verify
acorn output re-parses to correct arcPath shape.

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
2026-06-17 16:53:02 -07:00
a746db6017 feat(sdk,core): ws-3 prerequisites — acorn keyframe-collapse foundation + removeAllKeyframes (#1499)
* feat(sdk,core): ws-3 prerequisites — acorn keyframe-collapse foundation + removeAllKeyframes

P1: gsapWriter.parity.test.ts — recast-vs-acorn parity harness (reparse-equivalence).
P2: move pure keyframe-conversion transforms (resolveConversionProps, cssIdentityValue)
    to recast-free gsapSerialize.ts so the acorn/SDK path can share them.
P3: MagicString splice primitives in gsapWriterAcorn.ts (buildVarsObjectCode, overwriteVarsArg).
P4: reference vertical slice — removeAllKeyframesFromScript ported to acorn writer +
    removeAllKeyframes SDK op (types/mutate/can) + Studio cutover (useGsapKeyframeOps),
    replacing the server-authoritative ponytail stub.

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

* feat(sdk,core): ws-3 — convertToKeyframes acorn port + SDK op + Studio cutover

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(sdk,core): ws-3 — materializeKeyframes + splitIntoPropertyGroups acorn ports + SDK ops

- acorn: buildKeyframeObjectCode, materializeKeyframesFromScript, addAnimationWithKeyframesToScript
- acorn: splitIntoPropertyGroupsFromScript with filterGroupKeyframes/filterGroupProperties helpers
- parity tests: materialize (2 positive + 1 no-op) and split (2 positive + 2 no-op) suites
- SDK types: materializeKeyframes + splitIntoPropertyGroups EditOp variants
- mutate.ts: handlers + can() gates for both new ops
- mutate.gsap.test.ts: 6 new tests (53 total passing)

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(sdk,core): ws-3 — splitAnimationsInScript acorn port + SDK op

- acorn: updateAnimationSelectorInScript, insertInheritedStateSetInScript helpers
- acorn: splitAnimationsInScript exported (parity with recast version)
- parity: 4 new fixtures (3 cases + no-op) — 23 total parity tests
- SDK types: splitAnimations EditOp variant
- mutate.ts: handleSplitAnimations + can() gate
- mutate.gsap.test.ts: 3 new tests (56 total passing)

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
2026-06-17 16:51:28 -07:00
ceb815c318 feat(sdk,studio): ws-1.2 — percentage-based removeGsapKeyframe (#1498)
* feat(sdk,studio): ws-1.2 — percentage-based removeGsapKeyframe

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(sdk,studio): ws-1.3 — removeGsapProperty SDK op + Studio hook cutover

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(sdk,studio): ws-1.4 — deleteAllForSelector SDK op + Studio hook cutover

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* fix(core): cascade-remove GSAP tweens in removeElementFromHtml (WS-2)

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

---------

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
2026-06-17 16:49:47 -07:00
Vance IngallsandMiguel Ángel a5016ed416 feat(sdk,studio): ws-1.1 — add set method to GsapTweenSpec; route addGsapAnimation(set) through sdk (#1497)
Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
2026-06-17 16:48:20 -07:00
Vance IngallsandMiguel Ángel e35846176e feat(sdk,studio): ws-4 — add history:false option; disable unused sdk undo in studio (#1496)
Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
2026-06-17 16:47:12 -07:00
Vance IngallsandMiguel Ángel 53717a77f4 feat(sdk): ws-a2 — applyDraft/commitPreview/cancelPreview → moveElement op (#1490)
Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
2026-06-17 16:46:04 -07:00
b96e8a3072 feat(sdk): ws-a1 — iframe preview adapter (hit-test + selection) (#1489)
* feat(studio): stage 7 step 3c — sdk cutover for inline-style ops

Introduces sdkCutoverPersist(): when STUDIO_SDK_CUTOVER_ENABLED is set,
inline-style PatchOps are routed through the SDK session's in-memory document
model instead of the server patch-element API. The SDK serialize() result is
written back through the same writeProjectFile + editHistory.recordEdit path,
so the on-disk output is identical to the legacy route.

- packages/studio/src/utils/sdkCutover.ts (new): sdkCutoverPersist() +
  shouldUseSdkCutover() guard; domEditSaveTimestampRef.current is stamped on
  each write to suppress the echo file-change reload.
- packages/studio/src/components/editor/manualEditingAvailability.ts: adds
  STUDIO_SDK_CUTOVER_ENABLED flag (default false); changes
  STUDIO_SDK_SHADOW_ENABLED default to false now that cutover is available.
- packages/studio/src/hooks/useSdkSession.ts: adds optional
  domEditSaveTimestampRef param; self-write suppress window (SELF_WRITE_SUPPRESS_MS)
  gates file-change reloads so SDK writes don't echo back as external edits.
- packages/studio/src/App.tsx: passes domEditSaveTimestampRef to useSdkSession
  so the suppress window can gate reloads triggered by SDK cutover writes.
- Test coverage: sdkCutover.test.ts (new, 141 lines) + useDomEditSession.test.ts
  (new, 50 lines) — guard function + happy-path assertions.

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

* fix(studio): force-reload sdk session after undo/redo bypasses suppress window

writeHistoryFile arms the 2 s self-write suppress window, so the
file-change event for an undo/redo write is swallowed and the SDK
in-memory doc stays on pre-undo content. Expose forceReload() from
useSdkSession (s7.4) and call it in useAppHotkeys after a successful
undo/redo that touched the active composition path.

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(studio): s7.5 — delete shadow scaffolding; keep cutover flag (dark launch)

Removes the SDK shadow telemetry: STUDIO_SDK_SHADOW_ENABLED, sdkShadow.ts +
sdkShadowGsapFidelity/GsapKeyframe/Numeric and their tests, the runShadow*
call-sites across the GSAP/timeline hooks, and the onDomEditPersisted shadow
callback in useDomEditSession. Moves patchOpsToSdkEditOps into sdkCutover.ts.

KEEPS STUDIO_SDK_CUTOVER_ENABLED as a dark-launch kill-switch — default false,
enable per-environment via VITE_STUDIO_SDK_CUTOVER_ENABLED=true. shouldUseSdkCutover
stays flag-gated. The stack can merge with zero behavior change; cutover is
validated by flipping the flag, not by removing it.

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

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* fix(studio): wire onTrySdkPersist to sdkCutoverPersist (cutover was unwired)

Stage 7 s7.5 removed the feature flag and declared cutover 'always-on',
but onTrySdkPersist was never actually passed to useDomEditCommits — the
sdkCutoverPersist function was dead code in production.

Thread sdkSession through useDomEditSession params, build the
onTrySdkPersist closure there (all CutoverDeps are already in scope),
and pass sdkSession from App.tsx. Style/text/attribute/html-attribute
commits now route through SDK dispatch instead of the server patch path.

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(studio): route element delete through SDK removeElement (§3.1)

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(studio): route timeline trim/move through SDK setTiming (§3.2)

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* chore(studio): document CSS-path position cut-over, GSAP-path intentionally deferred (§3.3)

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(studio): route GSAP tween add/update/delete through SDK (§3.5 PR1)

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(studio): route GSAP keyframe add through SDK (§3.5 PR2)

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* fix(studio,core): resolve SDK-cutover review findings

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

* feat(sdk): ws-a1 — iframe preview adapter (hit-test + selection)

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
2026-06-17 16:44:56 -07:00
Miguel Ángel fcc7b314f0 chore: release v0.6.110 2026-06-17 10:23:49 -04:00
Vance IngallsandClaude Opus 4.8 6e32142334 fix(sdk): resolve composition-id targets + emit canonical data-hf-id for GSAP tweens (#1526)
A sub-composition ROOT is addressed by its data-composition-id, but the SDK's
whole element<->tween attribution is data-hf-id based, so the prior fix's
[data-composition-id] selector was invisible to three readers (validateOp/can,
selectorMatchesId -> setTiming + removeElement cascade, buildAnimationIdMap ->
getElement.animationIds), diverging can from apply and orphaning tweens.

Root fix: make composition ids first-class resolvable addresses and emit the
canonical selector everywhere.

- resolveScoped (model.ts): for a bare id with no data-hf-id match, fall back to
  [data-composition-id]. data-hf-id keeps precedence; scoped-path and canonical
  behavior intact. Fixes validateOp gating, findById/getElement, and every op
  handler for comp-root targets in one place.
- gsapTargetSelector (mutate.ts): resolve the target and emit
  [data-hf-id="<resolved host hf-id>"] (canonical). Normal targets unchanged;
  comp-root targets resolve via comp-id -> host -> host hf-id. Defensive
  [data-composition-id] only when the resolved element has no hf-id.
- setTiming syncs the GSAP tween via the resolved element's data-hf-id so a
  comp-root target matches its host tween; removeElement cascade already covers
  the host hf-id via collectSubtreeHfIds.
- export escapeHfId; escape both the querySelector probe and the emitted
  selector string.

Tests: comp-id resolveScoped fallback + precedence (session.subcomp), canonical
selector, validateOp accept, setTiming sync, removeElement cascade, and
getElement.animationIds for comp-root tweens (mutate.gsap). The prior test only
called applyOp, masking all of this.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 23:08:08 -07:00
Vance Ingalls 1028f52aa2 chore: release v0.6.109 2026-06-16 21:16:45 -07:00
Vance Ingalls cc7c206e9c chore: release v0.6.108 2026-06-16 17:56:34 -07:00
Vance IngallsandClaude Opus 4.8 b5ad518957 fix(core,sdk): rebuild acorn GSAP keyframe writer for recast parity (#1520)
The acorn addKeyframeToScript mixed ms.overwrite + ms.appendLeft on the
same _auto endpoint node, crashing MagicString ("Cannot split a chunk
that has already been edited") whenever an interior keyframe adjacent to
an _auto 0/100 endpoint introduced a new backfilled prop — the common SDK
path. It also replaced (not merged) existing keyframes, dropped ease and
_auto markers, corrupted commas on multi-prop backfill into empty {},
used a <0.001 percentage tolerance instead of recast's PCT_TOLERANCE=2,
and silently no-op'd on flat (non-keyframe) tweens.

Rebuild the node model to mirror recast: compute the FINAL property record
for every changed keyframe value node (target merge, _auto endpoint sync,
backfilled siblings) against the original AST, then emit exactly one
ms.overwrite per changed node (one insert for a brand-new key). No node is
ever both overwritten and appended into, so splices can never overlap.

- Merge: re-touching an existing keyframe merges new props over the
  existing record, preserving untouched props, existing ease, and _auto.
- Convert-flat: first keyframe-add on a flat to()/from()/fromTo() tween
  rebuilds its vars object to percentage keyframes (ease->easeEach,
  ease:"none", from/fromTo->to) matching recast, then re-locates via the
  -from-/-fromTo- -> -to- id fallback.
- Tolerance: PCT_TOLERANCE=2 for existing-keyframe detection.
- Shared serializeValue/safeJsKey for keyframe values (recast parity); the
  tween-statement path keeps its local serializer for object/boolean extras.
- keyframeBackfill: only backfill props with a real numeric default; skip
  unknown/string props so color:0 / filter:0 are never emitted.
- setGsapKeyframe move-path threads the same backfill defaults as the add
  path so both entry points behave identically.

Differential tests (acorn vs recast parsed keyframe arrays) cover the
crash (2-endpoint + 0/25/100), empty-{} multi-prop backfill, merge with
extra props + ease, flat to()/fromTo() convert, "50.0%" non-byte-equal
key, near-% tolerance, and _auto-marker preservation onto an endpoint.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:54:46 -07:00
Vance Ingalls 6778ad13ef chore: release v0.6.107 2026-06-16 12:49:26 -07:00
Vance IngallsandClaude Opus 4.8 cc055f318d fix(sdk): agree removeElement/getElement on duplicate bare ids (#1511)
* fix(sdk): setStyle removes hyphenated properties (was kebab/camel key mismatch)

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

* fix(sdk): agree removeElement/getElement on duplicate bare ids

A bare hf-id duplicated across a sub-composition element and a top-level
element resolved to different instances: removeElement → resolveScoped →
querySelector (document-order-first, the inner sub-comp dup) while getElement
preferred the canonical match (scopedId === id, the top-level dup). So
removeElement(bareId) removed the inner instance and getElement(bareId) still
found the surviving top-level one — they disagreed.

resolveScoped now resolves an ambiguous BARE id to the canonical (top-level)
instance via isCanonicalScope (walks ancestors for isNewHostBoundary), falling
back to document order when no canonical match exists — matching getElement.
Fully-scoped paths (hf-host/hf-dup) and non-duplicated bare ids are unchanged.

Surfaced by SDK shadow parity (op:delete expected removed, actual present).

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-16 12:30:54 -07:00
Vance IngallsandClaude Opus 4.8 066ea798b4 fix(sdk): setStyle removes hyphenated properties (was kebab/camel key mismatch) (#1510)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 12:30:35 -07:00
Miguel Ángel c0ac03cab2 chore: release v0.6.106 2026-06-16 13:16:09 -04:00
Miguel Ángel 2798b97ef1 chore: release v0.6.105 2026-06-16 12:30:23 -04:00
Vance Ingalls 479184ecf0 chore: release v0.6.104 2026-06-16 02:58:48 -07:00
Miguel Ángel d9dc88ff60 chore: release v0.6.103 2026-06-16 02:57:54 -04:00
Miguel Ángel d6a846300e chore: release v0.6.102 2026-06-16 01:44:54 -04:00