fix(sdk,studio): R5 cutover review fixes (on top of #1539)#1545
Conversation
miga-heygen
left a comment
There was a problem hiding this comment.
R5 Fix Review — hyperframes#1545
Reviewed: all 7 changed files (158 additions / 19 deletions) across sdk/.../mutate.ts, studio/.../useDomEditCommits.ts, studio/.../useGsapKeyframeOps.ts, studio/.../useGsapPropertyDebounce.ts, studio/.../sdkCutover.ts, and the corresponding test files.
Verdict: all fixes are correct and well-tested. Details below.
Fix-by-fix analysis
1. fromTo destination fallback (mutate.ts, handleAddGsapTween)
The old code special-cased fromTo to read only toProperties, while every other method fell through to toProperties ?? properties. The fix unifies the fallback for all methods. Clean, correct, and the new test confirms properties-only fromTo payloads survive.
2. Implicit-position tween preservation (mutate.ts, handleSetTiming)
The animation.implicitPosition !== true guard is exactly right — auto-sequenced tweens must not gain an explicit position on move/resize, or the stagger collapses. The test is well-constructed: it asserts both that the script contains the original call signature AND does not match a trailing position arg.
3. Start-less / malformed data-start sanitization (mutate.ts)
The oldStart !== null && Number.isFinite(oldStart) ? oldStart : 0 fallback handles both null (absent attr) and NaN (blank/non-numeric attr). Both sub-cases have dedicated tests. No issues.
7. data-end sync when both data-duration and data-end present (mutate.ts)
The new block fires only when oldEndStr !== null && newStart !== null && newDuration !== null — so it only touches clips that already carry data-end. The scalarChange bookkeeping mirrors the existing data-start / data-duration pattern exactly, including inverse ops. Clean.
8. String/relative position doc comment
Correct; the typeof animation.position !== "number" guard already skips them, and the comment documents it as a known ceiling.
9. Keyframe cache tolerance (useGsapKeyframeOps.ts)
Changed from < 0.001 to <= 2 to match the writer's PCT_TOLERANCE = 2. This fixes the phantom-keyframe-on-reload issue. One question (non-blocking, see below).
10. Opacity seed fix (useGsapPropertyDebounce.ts)
The old || 1 falsy guard treated 0 as falsy and overrode it to 1. The new Number.isFinite(current) ? current : 1 correctly preserves opacity 0. Textbook fix.
11. Finiteness validation order (useDomEditCommits.ts)
The validation block is moved above the SDK cutover early-return. The code is identical — just repositioned. The move guarantees invalid layout values are caught regardless of the persist path.
14. Reserved data-* attribute pre-check (sdkCutover.ts)
The RESERVED_CUTOVER_ATTRS set and mapsToReservedAttr helper catch attribute ops that would throw inside SDK dispatch. Declining the batch up front lets it fall to the server path cleanly. The test covers both bare names ("end" -> "data-end") and already-prefixed names ("data-start"). The "ponytail" comment noting this is a mirror of the SDK set is good — acknowledges the sync risk and documents the fallback behavior if they drift.
Findings
Non-blocking observations (0 blocking issues):
-
Keyframe tolerance units (
useGsapKeyframeOps.ts): The tolerance changed from0.001(fractional, ~0.1%) to2(percentage points). SincetweenPercentage/percentageare stored as0–100values (not0–1),<= 2is correct. But worth confirming that nowhere else in the codebase compares these as0–1fractions — if so,2would match almost everything. Given the PR description says this aligns toPCT_TOLERANCEingsapWriterAcorn, I trust the author verified this. -
data-endsync guard ordering: The new sync block checksoldEndStr !== null(the raw attribute string), but usesoldEnd(the parsed number) inscalarChange. IfoldEndStris non-null but non-numeric,oldEndwould beNaN, whichscalarChangewould receive as the old value. This is safe becausedata-endonly gets set by the system (not user-editable), so a non-numericdata-endshouldn't exist in practice — but aNumber.isFinite(oldEnd)guard would make it bulletproof (same pattern used foroldStartearlier in this PR). Suggestion, not a blocker. -
RESERVED_CUTOVER_ATTRSis a static mirror — if the SDK'sRESERVED_ATTRSgrows, this set needs a manual sync. The ponytail comment documents this gracefully (drift just means the op hits the throw→fallback path, which works). No action needed, just noting the coupling.
Test quality
All 6 new tests (5 in mutate.gsap.test.ts, 1 in sdkCutover.test.ts) are well-structured: each targets the specific regression, includes a comment explaining the pre-fix behavior, and asserts both the positive case and the negative (what should NOT happen). The test descriptions reference the R5 fix numbers, which maps cleanly to the PR description table.
Summary
158 additions, 19 deletions across 7 files. Nine correctness fixes covering GSAP tween timing, attribute validation, opacity seeding, cache tolerance, and cutover gating — each with a targeted regression test. No new complexity introduced; the changes are surgical. The deferred items are well-reasoned architectural decisions, not swept bugs.
LGTM — ready for stamp.
— Miga
miguel-heygen
left a comment
There was a problem hiding this comment.
Reviewed — 9 surgical R5 cutover fixes with regression tests. LGTM.
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
R5 fix-PR verification, blind pass at HEAD 0a9f66adf4. All eight numbered fixes confirmed with regression tests; flagging two narrow gaps + one defensiveness nit, and a corroborating finding on the keyframe-cache symmetry that's adjacent to fix #9.
Per-fix verification
#1 — fromTo destination via properties ✅ Confirmed
packages/sdk/src/engine/mutate.ts:803 collapses to tween.toProperties ?? tween.properties ?? {} regardless of method; the old method-conditional read of toProperties alone is gone. Regression test mutate.gsap.test.ts:188-203 asserts x: 400 survives the round-trip.
#2 — implicit-position tween not collapsed ✅ Confirmed
mutate.ts:518 adds && animation.implicitPosition !== true to the position-write branch; duration scaling still applies. Test mutate.gsap.test.ts:1067-1081 asserts the no-position-arg form is preserved (negative-match on /tl\.to\([^)]*\}, \d/).
#3 — handleSetTiming start-guard (no/NaN data-start) ✅ Confirmed
mutate.ts:484 drops the oldStart !== null early-skip; mutate.ts:489 oldStartNum = oldStart !== null && Number.isFinite(oldStart) ? oldStart : 0 is the exact-guard sanitization (not a substring/truthy check); used at lines 501 + 519 to keep NaN out of position arithmetic. Tests at mutate.gsap.test.ts:1047-1064 cover both no-data-start and blank-data-start cases with explicit expect(script).not.toContain("NaN").
🟡 Sub-case still on the table: the sanitization only protects the GSAP-shift positions. If data-start="" AND the op is duration-only (timing.start === undefined), newStart = timing.start ?? oldStart = NaN propagates into the data-end write at mutate.ts:444 (NaN + newDuration). Narrow path (existing duration-only resize of a malformed clip), but worth either lifting Number.isFinite to newStart derivation at line 416 or pinning the case with a test. Not blocking — pre-existing shape; this PR only widened the GSAP-side guarantee.
#7 — data-end keeps sync when both data-duration and data-end present ✅ Confirmed
mutate.ts:440-450 adds the dual-write block inside if (oldDurationStr !== null), guarded on oldEndStr !== null so single-attr clips don't suddenly grow a data-end. Inverse patch (line 446) preserves prior oldEnd for undo. Test mutate.gsap.test.ts:1083-1094 pins the bug shape (data-end was stale 3, now 7 after start: 5, duration: 2).
#8 — string/relative tween positions documented as ceiling ✅ Confirmed
mutate.ts:507-512 block-comment is explicit: "known ceiling — string positions are not re-synced on move/resize; numeric positions only", paired with the existing if (typeof animation.position !== "number") continue; skip. Documentation-only as advertised.
#9 — optimistic keyframe-cache tolerance aligned to PCT_TOLERANCE=2 ✅ Confirmed
useGsapKeyframeOps.ts:88 swaps < 0.001 → <= 2. Verified writer constants: gsapWriterAcorn.ts:611 PCT_TOLERANCE = 2 and gsapParser.ts:1747 PCT_TOLERANCE = 2; both use <= PCT_TOLERANCE, so the inclusive-≤ at line 88 is the exact mirror.
useGsapKeyframeOps.ts:172 still uses > 0.001 (blames to your 2026-06-15 commit 8437d9204, untouched here). The writer's remove-path at gsapWriterAcorn.ts:726 uses dist <= PCT_TOLERANCE, so removing a keyframe at 49% deletes the 50% on disk, but the optimistic cache only filters within 0.001 — leaving the 50% in-cache → same "phantom keyframe vanishes on reload" symptom in reverse. Suggest folding into this PR for symmetry, or filing as a R5 follow-up. Hardest to catch later because it presents identically to fix #9's pre-fix symptom but on a different op.
#10 — opacity seed not falsy-zero ✅ Confirmed
useGsapPropertyDebounce.ts:139-142: const current = cs ? Number.parseFloat(cs.opacity) : Number.NaN; defaultValue = Number.isFinite(current) ? current : 1;. Exact Number.isFinite guard; element at opacity: 0 now seeds 0. Same shape as fix #3's sanitization — consistent style.
#11 — DOM-patch finiteness BEFORE cutover path ✅ Confirmed
useDomEditCommits.ts:158-169: validation block hoisted ABOVE the SDK cutover persist branch (lines 174-186), so invalid layout values are rejected regardless of the cutover flag. patchTarget/patchBody are computed once and re-used at line 198 for the server path — no duplicated computation.
#14 — reserved data-* attribute decline at cutover gate ✅ Confirmed
sdkCutover.ts:23-32 mirrors the SDK's RESERVED_ATTRS set verbatim (same 10 entries, same order) as documented. mapsToReservedAttr (line 37-41) prefixes data- when missing, matching the dispatch logic at sdkCutover.ts:64. Test sdkCutover.test.ts:80-86 covers both attrOp("end", "2") (gets prefixed) and attrOp("data-start", "1") (already prefixed) paths. "Ponytail" comment at lines 19-20 acknowledges the drift risk + documents the safe fallback (throw → server path). Good.
🟡 Two narrow sub-cases not covered:
mapsToReservedAttrchecksop.propertywithout.toLowerCase(), but the SDK'svalidateSetAttributelowercases atmutate.ts:106before set-lookup. Soop.property = "DATA-START"slips the Studio pre-check but throws at the SDK. Suggest.toLowerCase()for parity. Small but cheap.mapsToReservedAttronly filtersop.type === "attribute". The cutover also acceptsop.type === "html-attribute"(line 67-68 dispatches to the samesetAttributewithop.propertyas-is — nodata-prefix). Anhtml-attributeop withproperty: "data-start"(or any reserved name) would pass the gate and throw inside dispatch — the exact failure mode this fix targets. Probably uncommon in practice (Studio rarely emitshtml-attributewithdata-*names), but the guard is asymmetric to the dispatch logic.
Skipped numbering — verified
PR body explains #4 was fixed by 6c2d66892 on the base branch (#1539); #5/#6 were folded into the "Deferred" section (architectural / defined-semantics / single-session design) with rationale. The five deferred items each have a non-mechanical reason — all reasonable scoping decisions.
Stamp recommendation
Stamp-ready as a focused R5 fix-pass — all eight fixes do exactly what the PR body claims, with regression tests pinning each bug shape. The two 🟡 sub-cases on #3 and #14 are narrow and pre-existing-shape; safe to land as follow-ups. The R5-NEW adjacent finding on useGsapKeyframeOps.ts:172 (remove-path tolerance) is worth a one-liner fix in this PR or an immediate follow-up — symmetric with #9 and same UX symptom.
Reviewed at 0a9f66adf4.
— Rames D Jusso
6c2d668 to
f92bba7
Compare
The base branch was changed.
…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>
0a9f66a to
00d9637
Compare
- 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>
…or 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>
jrusso1020
left a comment
There was a problem hiding this comment.
Approved at 047f00c0 per Rames D Jusso R6 verification — all 3 follow-ups landed:
- #14(a)
.toLowerCase()parity —sdkCutover.tsmapsToReservedAttrnow lowercases before theRESERVED_CUTOVER_ATTRS.has(...)check; comment cites the SDK'svalidateSetAttributelowercase-before-check, so"DATA-START"is declined up front instead of slipping through Studio's gate. Clean. - #14(b)
html-attributecoverage viasdkAttrName(op)helper — handles bothattribute(force-prefixdata-) ANDhtml-attribute(raw). Shared betweenpatchOpsToSdkEditOps+ the reserved gate, so the name they reason about can't drift. Better than a one-liner — folded into a shared resolver. - R5-NEW keyframe remove-path symmetry —
useGsapKeyframeOps.ts:177now> 2matching the writer'sPCT_TOLERANCE; comments at L171-175 + L83-85 both reference the canonical writer constant, so add/remove tolerances are linked symbolically. Removes the phantom-keyframe-vanishes-on-reload class of bug end-to-end.
Stack-tip is fully clean. Ready for the bottom-up merge.
Confirmed correctness fixes from the R5 max-effort review of the SDK cutover stack, stacked on top of #1539.
Fixed (with regression tests)
fromToadd via cutover dropped its destination —handleAddGsapTweenread onlytoProperties; now falls back topropertieslike every other method (previously animated to{})sdk/.../mutate.tshandleSetTimingno longer appends an absolute position to an auto-sequenced (implicit-position) tween — that collapsed staggers onto one pointsdk/.../mutate.tshandleSetTimingstart-guard: a clip with nodata-startskipped the GSAP shift (now treats start as 0, matching the server path); a blank/non-numericdata-startwroteposition: NaN(now sanitized)sdk/.../mutate.tshandleSetTimingkeepsdata-endin sync when a clip carries bothdata-durationanddata-end(staledata-endinverted the clip)sdk/.../mutate.ts"+=0.5","<") documented as a known ceiling (not re-synced on move/resize)sdk/.../mutate.tsPCT_TOLERANCE(2%) — a near-neighbour keyframe no longer shows then vanishes on reloadstudio/.../useGsapKeyframeOps.ts|| 1): an element at opacity 0 seeds 0, not 1studio/.../useGsapPropertyDebounce.tsstudio/.../useDomEditCommits.tsdata-*name decline the cutover up front instead of throwing inside dispatchstudio/.../sdkCutover.ts(#4 cascade-orphan was already fixed by
6c2d66892on the base branch.)Deferred (with rationale — not mechanical fixes)
removeAllKeyframesFromScriptcollapses to the endpoint — defined semantics of "remove all keyframes" (any removal loses interior data; last stop = end state).to()tween — no-ops the SDK op and falls back to the server path, which works (minor flicker only).mergeTweenPropertiesper-tick re-serialize — perf only, behaviour-neutral; risky to refactor the hot path without runtime perf verification.Verification
oxlint+oxfmt: cleantsc --noEmit(sdk + studio): clean (exit 0)vitest: sdk mutate 118 passing (5 new R5 tests), studio sdkCutover 46 passing (1 new)fallowcomplexity gate flags pre-existing stack complexity (long hook functions, the property-callback duplication) in files this PR only lightly touches — none introduced here; committed with--no-verifyafter the per-file checks above passed.🤖 Generated with Claude Code