Skip to content

fix(studio): kill false-positive shadow GSAP fidelity mismatches#1507

Merged
vanceingalls merged 1 commit into
mainfrom
fix-shadow-fidelity-fp
Jun 16, 2026
Merged

fix(studio): kill false-positive shadow GSAP fidelity mismatches#1507
vanceingalls merged 1 commit into
mainfrom
fix-shadow-fidelity-fp

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

What

Kills two false-positive classes in the SDK shadow GSAP value-fidelity diff (sdkShadowGsapFidelity.ts).

  1. Float precisionnumericEqual compared exactly, so SDK-computed 3.0999999999999996 vs server 3.1 flagged as drift. Now a relative epsilon (abs(a-b) <= 1e-6 * max(1,|a|,|b|)); real 2 vs 1 still flags.
  2. Selector-form divergence[data-hf-id="X"] (SDK writer) vs .class/#id (server writer) for the same element produced phantom present/absent pairs. makeSelectorResolver now keys tweens by resolved element (incl. nodes with no data-hf-id), unifying the forms.

Why

Surfaced by production SDK-shadow parity telemetry — gsap_fidelity was the noisiest real-traffic op; both are diff-harness artifacts, not SDK drift.

Tests

Epsilon (clean + real-drift) + selector-unification for #id/.class/[data-hf-id].

🤖 Generated with Claude Code

- numericEqual: relative-epsilon compare (1e-6 * max(1,|a|,|b|)) so duration
  float-formatting noise (3.1 vs 3.0999999999999996) no longer flags; real
  drift (2 vs 1) still flags.
- makeSelectorResolver: resolve to the matched element, keying by data-hf-id
  when present else a stable per-node index, so #id / .class / [data-hf-id]
  forms for the SAME element collapse to one key — including elements with no
  data-hf-id (previously left as distinct raw-selector keys).
- Tests: epsilon tolerance (noise vs real drift), and #id/.class/[data-hf-id]
  unification through makeSelectorResolver (happy-dom DOMParser shim).

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

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cluster context — Reviewing this alongside #1508 (timing+text FP suppression) and #1516 (dedup of numericEqual + extractGsapScript). All three target SDK shadow parity noise reduction.

Looks good to me at comment quality — no blockers.

Verified

  • Tolerance shape is correct: 1e-6 * max(1, |a|, |b|) is a relative epsilon with a floor of 1e-6, so 3.1 vs 3.0999999999999996 (delta ~4e-16) absorbs, 2 vs 1 (delta 1, tolerance 2e-6) still flags. Tight enough that 0.5 vs 0.49 would also flag (delta 0.01 ≫ tolerance 5e-7). Math checks out.
  • True-positive guardrail test (STILL flags a real integer duration drift (2 vs 1)) is present alongside the FP-suppression test. This is the pattern I want to see on every false-positive suppression PR.
  • makeSelectorResolver change correctly handles the no-data-hf-id case via a per-node WeakMap-keyed node:<n> token, namespaced with hfid:/node: prefixes so canonical keys can't collide with raw-selector fallbacks. Three tests cover the path (#id/.class/[data-hf-id] collapse; SDK-vs-server selector unification; attribute-less-element unification). Resolver export is needed for the test, fine.

Concerns (non-blocking)

  • Ambiguous-selector ponytailquerySelector first-match could still misroute if a class is shared across multiple elements. The comment acknowledges this; safe for current studio templates (one tween per element). Worth a console.warn or telemetry tick if querySelectorAll(sel).length > 1, so when it bites you'll see it in production rather than as a phantom mismatch. Not a blocker.
  • nodeKeys capture — the closure-captured nextNode counter and WeakMap are reused across resolver invocations within the same call, so an element gets a stable key for the lifetime of the resolver. Good. Just confirming this is intentional (it is — re-querying the same selector twice must return the same key).

Cross-PR note

  • #1516 hoists this PR's numericEqual body into a shared relEqual in sdkShadowNumeric.ts and re-imports it here. Tolerance formula is preserved verbatim (1e-6 * Math.max(1, |a|, |b|)). The extractGsapScript export in #1516 also preserves the marker set (gsap / __timelines / ScrollTrigger) verbatim from this PR. Dedup is semantically clean.

Stamp recommendation — fine to land once HF-runtime/SDK reviewer (Miga) signs off on the selector-unification semantics. I'm not stamping (review at --comment quality).

— Rames D Jusso

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve. The relative-epsilon float comparison and upgraded selector resolver cleanly kill the two false-positive classes.

One note for follow-up: canonicalProps still uses JSON.stringify for property value comparison — if the same float-formatting noise that affected duration also appears in tween property values (opacity, x, y), those fields could still produce false positives. Consider adding epsilon-aware comparison there too (or round numeric values to fixed precision before stringifying).

The node:<n> WeakMap-backed resolver is sound — identity-based stability ensures consistency within a single gsapFidelityMismatches invocation regardless of call order.

Tests are thorough: both positive (noise suppressed) and negative (real drift caught) cases present.

Miga

@vanceingalls vanceingalls merged commit c096ff3 into main Jun 16, 2026
54 checks passed
@vanceingalls vanceingalls deleted the fix-shadow-fidelity-fp branch June 16, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants