Commit Graph
11 Commits
Author SHA1 Message Date
James Russo 839881a98e feat(sdk): variable declaration edit ops (declare/update/remove) (#2047) 2026-07-09 13:29:24 -07:00
Vance Ingalls bcda11aa7c fix(sdk): address PR #2098 review feedback on variable CRUD
- validateOp now handles declareVariable/removeVariable (E_NO_ROOT when no
  composition root), matching setVariableValue's existing case — previously
  comp.can() returned E_UNKNOWN_OP for both.
- removeVariable's undo-inverse now tags its {decl, index} reinsert payload
  with __kind: "reinsert" instead of relying on structural "decl"/"index"
  key presence to disambiguate it from a plain declareVariable patch.
  VariableDecl has an open index signature, so a real variable schema could
  legally declare its own "decl"/"index" fields and be misinterpreted by the
  old structural check; a regression test pins the exact collision.
- getVariableValue's return type tightened from `unknown` to
  `string | number | boolean | FontValue | ImageValue | undefined`, matching
  setVariableValue's parameter type for round-trip symmetry. The underlying
  unknown-typed read is cast once at this SDK boundary.
- Added a redo test for declareVariable/removeVariable (existing tests only
  covered undo).
2026-07-09 11:52:12 -07:00
Vance Ingalls 19756faa5d feat(sdk): variable CRUD (declare/remove/get/list) + export gaps
Closes the remaining Tier 2/3 gaps from the SDK surface audit that motivated
#2092 — real, contained fixes short of the two genuinely architectural items
(a live-DOM apply adapter, structural editing ops) that need their own design
pass, not a quick patch.

Variable CRUD was write-only and creation-blocked: setVariableValue existed,
but there was no getVariableValue, listVariables, declareVariable, or
removeVariable — and writeVariableDefault intentionally refuses to create an
undeclared variable ("keep the schema authoritative"), so a variables panel
(list what exists, read current values, let someone add one) could not be
built against the SDK at all.

- getVariableValue(id) / listVariables(): thin reads over the existing
  readVariableDefault / a new listVariableDecls.
- declareVariable(decl) / removeVariable(id): new EditOps with full
  undo/redo support via a new patch path (/variable-decls/{id}, distinct
  from /variables/{id} which is default-only) — removeVariable's inverse
  bundles the original array index so undo reinserts at the same position
  instead of appending, mirroring handleRemoveElement's siblingIndex.

Export gaps (same shape as #2092's fixes — the logic already existed,
just wasn't reachable): resolveScoped, findById, escapeHfId from
engine/model.ts; readVariableDefault from engine/variableModel.ts.

17 new tests across mutate.test.ts (declareVariable/removeVariable engine
semantics + undo), session.test.ts (Composition-level API), and smoke.test.ts
(export-surface import check). 439/439 sdk tests passing. Full workspace
build (incl. studio) verified clean.
2026-07-09 11:52:12 -07:00
Vance Ingalls c5d725abd0 fix(sdk,studio): resolve animation ids from parsed script, not just DOM-matched elements (#1957)
recordAnimationResolverParity reported a false animation_not_found divergence
for any tween whose selector doesn't currently CSS-match a live DOM element,
because it only checked el.animationIds (DOM-gated). The real server-side op
it shadows resolves purely from the parsed script. Adds
Composition.getAllAnimationIds() as a DOM-independent id set and checks it
too, matching the server's actual resolution behavior.
2026-07-05 20:11:49 -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
Vance IngallsandClaude Opus 4.8 7593aac5ef feat(sdk,studio): populate animationIds; shadow GSAP update/delete + value fidelity (#1474)
* feat(sdk,studio): populate animationIds; shadow GSAP update/delete

Closes the GSAP shadow gaps. The server's animationId was assumed to live in a
separate id-space — it does not: the studio-api read path (T6e) and the SDK
both derive tween ids as targetSelector-method-position from the same acorn
parser, so server ids are dispatchable in the SDK as-is.

SDK: populate ElementSnapshot.animationIds (was a hardcoded stub) from
parseGsapScriptAcornForWrite().located, resolving each tween's targetSelector
to element hf-ids. Makes the snapshot truthful and enables real GSAP parity.

Studio: shadow deleteGsapAnimation (removeGsapTween) and updateGsapMeta
(setGsapTween) using the server animationId directly. GSAP add/remove parity
now verifies via animationIds (present after add, gone after remove). set is
existence-only — the SDK still has no per-tween property reader (value fidelity
would need serialize()-script round-trip diffing).

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

* feat(studio): GSAP value fidelity via serialize round-trip diff

Closes the last shadow gap: GSAP value fidelity. Existence parity confirmed a
tween was created/removed but not that its values (duration/ease/position/
properties) matched the server, since the SDK has no per-tween property reader.

runShadowGsapFidelity opens a fresh SDK doc from the server's pre-op file
(result.before), applies the same typed op, serializes, and structurally diffs
the SDK's GSAP script against the server's resulting script (result.scriptText).
Both are re-parsed via parseGsapScriptAcorn, so formatting/whitespace never
produces false positives — only real value drift does. gsapFidelityMismatches
reports per-field drift and tween presence/absence.

Wired at the commitMutation chokepoint (the only place with the server's
before+after scripts); handlers pass the typed ShadowGsapOp via
CommitMutationOptions.shadowGsapOp. Emits sdk_shadow_dispatch op:gsap_fidelity.
Complements the existing live existence shadow (op:gsap).

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

* fix(studio,sdk): address shadow code-review findings

- gsapFidelityMismatches: canonical comparison (sort property keys, numeric-
  coerce position/duration/values). Server (addAnimationToScript) and SDK
  (gsapWriterAcorn) are different writers; non-canonical compare flagged
  key-order / number-vs-string differences as false value drift.
- document.ts buildAnimationIdMap: memoize the acorn parse by script text
  (single-entry). getElements() invalidates on every dispatch, so shadow's
  frequent dispatches were re-parsing the full GSAP AST each rebuild. Selector
  resolution still runs per-call (depends on live DOM).
- runShadowGsapFidelity: early-bail when serverScript/beforeHtml is empty —
  skip the costly openComposition.
- useSafeGsapCommitMutation: import the shared CommitMutationOptions/
  CommitMutation instead of a stale local duplicate (was missing shadowGsapOp).
- align extractGsapScript marker set across sdkShadow.ts and document.ts
  (gsap || __timelines || ScrollTrigger) so both pick the same script.

Tests: +2 canonical-compare cases (key-order, number-vs-string → no drift).

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

* fix(studio,sdk): fallow gate for #1474 (fidelity diff + test clones)

- suppress moderate CRAP on gsapFidelityMismatches and the runShadowGsapTween
  parity arrow (comparison/parity functions are inherently branchy)
- suppress two pre-existing test clones in session.test.ts surfaced by the
  added animationIds tests (TestPreviewAdapter stub, selectionchange setup)

Rebased onto the updated #1473 (no-persist shadow session); inherits the
persist-race fix and prior fallow suppressions.

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

* refactor(studio): extract GSAP fidelity to its own module (file-size gate)

sdkShadow.ts hit 602 lines (CI File size check: max 600). Move the GSAP
value-fidelity diff (gsapFidelityMismatches, runShadowGsapFidelity, and their
private helpers) into sdkShadowGsapFidelity.ts; re-export from sdkShadow.ts so
the import surface is unchanged. sdkShadow.ts now 430 lines.

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

* fix(studio,sdk): address #1474 review feedback

- CodeQL js/bad-tag-filter: the GSAP <script> extraction + test regexes now
  match </script\s*> (whitespace-before-close variant). 3 alerts resolved.
- Wiring (Miguel): extract resolveGsapFidelityArgs — a pure, narrowing gate for
  the commitMutation chokepoint (no non-null assertions) — and unit-test the
  fire/skip conditions (session, op, before, scriptText). Replaces the inline
  guard so the wiring decision is covered without rendering the hook.
- Property-handler scope (Rames): comment at the chokepoint documenting that
  only meta-level ops (add/update-meta/delete) carry shadowGsapOp today;
  per-property and keyframe handlers are a deliberate follow-up. Also why
  scriptText can be null.
- Test coverage (Rames): multi-tween-per-element and shared-selector
  cross-element animationIds cases.

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

* fix(studio): CodeQL js/bad-tag-filter — match </script[^>]*> close tags

`</script\s*>` still tripped CodeQL on attribute-junk closes like
`</script foo>` (HTML5 ignores junk before `>`). Widen the close-tag match to
`</script[^>]*>` in the GSAP-script extraction and the test regexes.

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-15 22:40:28 -07:00
30ce1f35a2 feat(sdk): stage 7 step 2 — setSelection API (#1442)
* feat(sdk): stage 7 step 2 — setSelection API

Adds setSelection(ids: string[]) to Composition interface and CompositionImpl.
Fires selectionchange; does not touch undo stack or patch stream.
11 contract tests: get/set/clear, event firing, copy semantics, no undo/patch side-effects.

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

* fix(sdk): guard setSelection against same-id no-ops

Skip event dispatch when ids are identical (same length, same order)
to prevent double-firing selectionchange from callers that call
setSelection with the same list. Two new tests (RED→GREEN verified).

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

* fix(sdk): de-duplicate ids in setSelection

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

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

* fix(sdk): document PreviewAdapter.on("selection") as stage 8 prep

Reviewer noted it is dead surface in this stack — no caller uses it.
Add comment explaining it is wired up in stage 8 when the preview host
pushes selection events up to the SDK session.

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

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-15 13:55:53 -07:00
9627e03fa7 feat(sdk): stage 4 — canUndo/canRedo, removeElement GSAP cascade, override-set cleanup (#1431)
* feat(sdk): stage 4 — canUndo/canRedo, removeElement GSAP cascade, override-set cleanup

* docs(sdk): document cascadeRemoveAnimations bare-id v1 limitation for scoped ids

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

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

* chore(sdk): remove sdk-status-report.txt from source tree

Internal planning artifact should not be committed to the repo.

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-15 02:07:39 -07:00
Kiyeon Jeon 84a56986c6 fix(sdk): bridge preview selection into session state (#1362) 2026-06-12 18:25:41 -07: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 7010edac85 feat(sdk): session API, optional history + persist-queue, adapters — Phase 3a complete (#1325)
* 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>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:23:09 -07:00