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>
This commit is contained in:
Vance Ingalls
2026-06-17 17:15:16 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 8c981a451a
commit e57e75b9b4
7 changed files with 195 additions and 29 deletions
@@ -77,6 +77,26 @@ describe("shouldUseSdkCutover", () => {
expect(shouldUseSdkCutover(true, true, "hf-abc", [htmlAttrOp("class", "foo")])).toBe(true);
});
it("returns false for an attribute op that maps to a reserved data-* name", () => {
// {type:'attribute', property:'end'} → 'data-end', which the SDK's
// validateSetAttribute rejects. Decline the batch so it takes the server
// path cleanly instead of throwing inside dispatch and falling back per op.
expect(shouldUseSdkCutover(true, true, "hf-abc", [attrOp("end", "2")])).toBe(false);
expect(shouldUseSdkCutover(true, true, "hf-abc", [attrOp("data-start", "1")])).toBe(false);
});
it("declines a case-variant reserved attribute (SDK lowercases before checking)", () => {
// attribute op "END" → "data-END" → lower → "data-end" (reserved).
expect(shouldUseSdkCutover(true, true, "hf-abc", [attrOp("END", "2")])).toBe(false);
});
it("declines an html-attribute op whose raw name is reserved", () => {
// html-attribute ops aren't data-prefixed, so a raw reserved name must still
// be caught (the SDK throws on it just the same).
expect(shouldUseSdkCutover(true, true, "hf-abc", [htmlAttrOp("data-end", "3")])).toBe(false);
expect(shouldUseSdkCutover(true, true, "hf-abc", [htmlAttrOp("DATA-START", "1")])).toBe(false);
});
it("returns true when ops mix all supported types", () => {
expect(
shouldUseSdkCutover(true, true, "hf-abc", [