feat(sdk,studio): populate animationIds; shadow GSAP update/delete + value fidelity#1474
feat(sdk,studio): populate animationIds; shadow GSAP update/delete + value fidelity#1474vanceingalls wants to merge 7 commits into
Conversation
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Verified at HEAD 51abc5e89cd492fb683ad1326e277bbddef530b7. Canonical-rubric pass on the stacked PR (on top of #1473).
T6e premise — VERIFIED (load-bearing)
Both id-generators implement assignStableIds byte-identically:
- SDK / browser:
packages/core/src/parsers/gsapParserAcorn.ts:1030-1044—${targetSelector}-${method}-${posKey}${groupSuffix}withposKey = Math.round(position * 1000). - Server / legacy recast path:
packages/core/src/parsers/gsapParser.ts:1068-1082— same shape, same posKey quantization, same collision-counter (-${count}suffix whencount > 1). - studio-api read path is on acorn now:
packages/core/src/studio-api/routes/files.ts:27importsparseGsapScriptAcorn— bothgetCompositionAnimations(524) and bulk-update validators (587, 1171, 1236) read through it. The recastparseGsapScriptis gone from the read path.
So server.animationId === sdk.animationId for any tween with the same (targetSelector, method, position, propertyGroup) and a shared pre-op file. The PR's premise holds; update/delete shadow via server's id is structurally sound.
What I checked structurally
buildAnimationIdMap(packages/sdk/src/document.ts:66-87): multi-match selectors (e.g..button→ many) correctly fan tween ids out to every matched element. Unparseable selector → skipped viatry/catcharoundquerySelectorAll. Unparseable script → empty map →animationIds: [](truthful stub fallback).gsapFidelityMismatches(sdkShadow.ts:629-682): per-field, set-symmetric-difference over ids;canonicalPropssorts keys + coerces numeric strings ("0.5"→0.5);numericEqualcovers position/duration writer differences. Tests atsdkShadow.test.ts:398-435lock both edge cases (key-order, number-vs-string-equivalent). The diff is structurally correct — won't false-positive on writer formatting.removeGsapTween/setGsapTween/addGsapTweenexist onComposition(packages/sdk/src/types.ts:237-239,session.ts:134-145). No hallucinated API.- Telemetry dims:
op: gsap(existence) andop: gsap_fidelity(value) are distinct. No dashboard collision with #1473'sgsapop — same op string, but they fire from different code paths andrunShadowGsapFidelityaddsmismatchespayload that existence doesn't.
Concerns (non-blocking)
-
shadowGsapOpcoverage gap on property-level edits. The chokepoint is wired inuseGsapScriptCommits.ts:70, but onlyuseGsapAnimationOps.ts(createGsap / updateGsapMeta / deleteGsap) actually passesshadowGsapOp.useGsapPropertyDebounce.ts(update-property,add-property,remove-property,update-from-property,add-from-property,remove-from-property) anduseGsapKeyframeOps.tsall pass throughcommitMutationSafelywith NOshadowGsapOp— so the property-scrub path (the most frequent edit + the one most likely to surface writer divergence onproperties/fromProperties) is silently un-shadowed. The PR body's "value fidelity (duration/ease/position/properties)" claim is honored at the meta level but not at per-property. Suggest either (a) a follow-up PR enumerating the property handlers and synthesizingshadowGsapOp: { kind: "set", ... }from{ animationId, property, value }, or (b) a comment at the property handlers documenting the deliberate scope omission so the next reader doesn't read the gap as a bug. -
Module-level cache state in
document.ts:46-47.gsapLocatedCacheKey/gsapLocatedCacheValare module-scoped mutable singletons across the entire SDK realm. Two parallel compositions with different script texts will thrash (eachbuildRootsinvalidates the other's entry) — bounded but no longer "covers the hot path" the comment promises. Bigger latent issue: if the SDK is ever consumed from a Worker pool or repeatedbuildRootson different docs concurrently, the single-entry cache becomes a correctness footgun (parsed structures captured against the wrong script text are unlikely because the key check is by reference-equality on the script string, but it's a sharp edge). Lifting the cache onto theDocument(WeakMap keyed by script string) or onto theSdkDocumentinstance would be both faster (no cross-comp thrash) and structurally safer. (nit-leaning concern.) -
runShadowGsapFidelityopen-cost.openComposition(beforeHtml)per edit + twoparseGsapScriptAcorncalls per diff. Fire-and-forgetvoid, gated bySTUDIO_SDK_SHADOW_ENABLED+shadowGsapOppresence, so the prod-default-on flag in #1473 means this runs on every meta-level edit. Fine for the current handler set (low-rate add/delete + a debounced update-meta), but worth a back-of-envelope before broadening to property scrubs — the budget could double or triple. No action this PR.
Nits
isGsapScriptBodyduplicated logic.sdkShadow.ts:422anddocument.ts:101both need the same marker set (gsap/__timelines/ScrollTrigger). The comment on each says "must match the other" — true today but bit-rot-prone. A sharedcore/gsap-script-detection.tshelper would lock the invariant. (nit.)runShadowGsapFidelityextractGsapScriptis regex-based on rawserialize()HTML, whiledocument.tsuses linkedom + DOMquerySelectorAll("script"). Both work for the test fixtures, but a<script>with attributes containing</script>text or nested CDATA would diverge. Not a real scenario for GSAP scripts, but worth knowing if the script-emission shape ever changes. (nit.)
Test coverage
23 new tests on sdkShadow.test.ts + 3 on SDK session.test.ts covering the load-bearing premise (id dispatchable as removeGsapTween target — session.test.ts:451-456), untargeted-empty (session.test.ts:446-449), and the writer-formatting false-positive guards (sdkShadow.test.ts:415-434). Missing: multi-tween-per-element coverage (one element with two tos and one from) and shared-selector cross-element coverage — both follow from the implementation but aren't asserted. Would be nice to lock down in a follow-up.
Recommendation
Greenlight from me. The T6e architectural unification is real and validated, the value-fidelity diff is canonical (writer-formatting won't false-positive), and the existence-parity logic is tight. Concern 1 (property-handler coverage gap) is the only one with a real product implication — flag for the author to confirm intent (deliberate scope-limit vs follow-up).
Tagging @rames-d-jusso for stamp if the author confirms the property-handler coverage is intentional and there are no other blockers from peer reviewers.
Canonical-rubric pass by Rames D Jusso — SDK shadow GSAP fidelity #1474, stacked on #1473
miguel-heygen
left a comment
There was a problem hiding this comment.
Stacked on #1473 — blocked on that PR landing. CI here is CLEAN; this review covers the diff above the base.
Strengths
gsapFidelityMismatchesis the right abstraction: purely functional, takes two script strings, returns per-field diffs. Completely testable in isolation from any HTTP call or session state.- Canonical compare (
numericEqual, key-order-sortedcanonicalProps) is good defensive engineering — the server writer and SDK writer can disagree on1vs"1"and key insertion order without generating false-positive mismatches. Test cases for both cases are solid.sdkShadow.ts:~440 runShadowGsapFidelityis correctly fire-and-forget (voidat callsite,sdkShadow.tsline cited inuseGsapScriptCommits.ts:68) and doesn't block the user path. Server authoritative boundary is respected.- Memoized
parseLocatedCachedindocument.ts:~50is a worthwhile optimization — acorn parse is the hot path ongetElements()rebuilds and the cache is bounded.
Findings
important — gsapFidelityMismatches / runShadowGsapFidelity are exported but have no existence tests in the live session path
runShadowGsapFidelity opens a fresh composition with openComposition(beforeHtml), applies the op, serializes, and diffs. The two canonical-compare test cases in PR #1474's test additions (sdkShadow.test.ts:279–295) are good. However, there is no test asserting that runShadowGsapFidelity is actually called at the commitMutation chokepoint — i.e. that the wiring from useGsapScriptCommits.ts:68 through to telemetry fires on a real addGsapAnimation call. The runShadowGsapTween tests in the same file give existence coverage; runShadowGsapFidelity has integration tests but not wiring tests. Low stakes for shadow-only code, but worth noting before this path is used to gate anything.
nit — result.before != null && result.scriptText != null guards at useGsapScriptCommits.ts:68
The before and scriptText nullchecks are correct (both are string | null | undefined from CommitMutationResult). A comment explaining why scriptText can be null (no GSAP script in the composition) would help the next reader. One line, optional.
nit — parseLocatedCached single-entry cache key in document.ts:~55
The single-entry LRU is fine for the stated hot path. If a composition alternates between two script strings on rapid undo/redo the cache thrashes, but that's an unlikely hot path for shadow telemetry. Leave as-is.
Notes
buildAnimationIdMapcorrectly handles missing script (returns empty map) and gracefully catchesquerySelectorthrows on malformed selectors. No change needed.- Marker set alignment between
document.ts:extractGsapScriptandsdkShadow.ts:isGsapScriptBody(gsap || __timelines || ScrollTrigger) is explicit with a comment on both sides — good cross-file documentation. useSafeGsapCommitMutation.tstype-alias removal (localCommitMutationOptions/CommitMutation→ shared import fromgsapScriptCommitTypes.ts) is a clean dedup; the previously-shadowedshadowGsapOpfield was the concrete bug this fixes.
Verdict: APPROVE (pending #1473)
Reasoning: Implementation is solid, tests are present for the new primitives, and the commitMutation wiring is correct. The one important finding is a wiring-test gap on runShadowGsapFidelity — low stakes for telemetry-only code but worth a follow-up before the path is used to gate anything.
— AI Reviewer
- 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>
51abc5e to
8fb34c8
Compare
- 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>
8fb34c8 to
14db42a
Compare
The base branch was changed.
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>
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>
- 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>
- 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>
14db42a to
1d7a428
Compare
…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>
- 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>
`</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>
What
PR 2/2 of the shadow-extension stack (on top of #1473). Closes the GSAP shadow gaps.
ElementSnapshot.animationIds(was a hardcoded[]stub) from the acorn parser'slocatedtweens, resolving each tween'stargetSelectorto element hf-ids.deleteGsapAnimation→removeGsapTween,updateGsapMeta→setGsapTween, using the server'sanimationIddirectly.runShadowGsapFidelity: open a fresh SDK doc from the server's pre-op file, apply the same op, serialize, and structurally diff the SDK's GSAP script against the server's resulting script.Why
The premise that server and SDK had separate id-spaces was wrong: T6e moved the studio-api read path to the same acorn parser the SDK uses, and both derive tween ids as
targetSelector-method-position. So the server'sanimationIdis dispatchable in the SDK as-is — unblocking update/delete shadow. PopulatinganimationIdsmakes the snapshot truthful and gives real existence parity. Value fidelity (duration/ease/position/properties) needs comparing the two writers' output, since the SDK has no per-tween property reader.How
document.ts:buildAnimationIdMapparses the GSAP script and mapstargetSelector→ hf-id → tween ids; threaded intobuildElement.sdkShadow.ts:gsapFidelityMismatches(re-parses both scripts, diffs per-field — formatting never causes false positives) +runShadowGsapFidelity.commitMutationchokepoint (only place with the server's before+after scripts); handlers pass the typed op viaCommitMutationOptions.shadowGsapOp. Telemetryop: gsap_fidelity(complementsop: gsapexistence).Test plan
session.test.ts— animationIds populated, untargeted empty, populated id dispatchable viaremoveGsapTween(221 total pass).sdkShadow.test.ts— add/remove via animationIds parity,gsapFidelityMismatches(identical / value drift / tween presence),runShadowGsapFidelity(match / divergence) — 23 tests.🤖 Generated with Claude Code