fix(studio): kill false-positive shadow GSAP fidelity mismatches#1507
Conversation
- 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
left a comment
There was a problem hiding this comment.
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, so3.1 vs 3.0999999999999996(delta ~4e-16) absorbs,2 vs 1(delta 1, tolerance 2e-6) still flags. Tight enough that0.5 vs 0.49would 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. makeSelectorResolverchange correctly handles the no-data-hf-idcase via a per-nodeWeakMap-keyednode:<n>token, namespaced withhfid:/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 ponytail —
querySelectorfirst-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 aconsole.warnor telemetry tick ifquerySelectorAll(sel).length > 1, so when it bites you'll see it in production rather than as a phantom mismatch. Not a blocker. nodeKeyscapture — the closure-capturednextNodecounter andWeakMapare 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
numericEqualbody into a sharedrelEqualinsdkShadowNumeric.tsand re-imports it here. Tolerance formula is preserved verbatim (1e-6 * Math.max(1, |a|, |b|)). TheextractGsapScriptexport 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
left a comment
There was a problem hiding this comment.
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

What
Kills two false-positive classes in the SDK shadow GSAP value-fidelity diff (
sdkShadowGsapFidelity.ts).numericEqualcompared exactly, so SDK-computed3.0999999999999996vs server3.1flagged as drift. Now a relative epsilon (abs(a-b) <= 1e-6 * max(1,|a|,|b|)); real2vs1still flags.[data-hf-id="X"](SDK writer) vs.class/#id(server writer) for the same element produced phantompresent/absentpairs.makeSelectorResolvernow keys tweens by resolved element (incl. nodes with nodata-hf-id), unifying the forms.Why
Surfaced by production SDK-shadow parity telemetry —
gsap_fidelitywas 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