Skip to content

feat(lint): seek-order safety and SVG draw-on rules for GSAP timelines#2611

Merged
xuanruli merged 2 commits into
mainfrom
lint/gsap-seek-safety-and-drawon
Jul 17, 2026
Merged

feat(lint): seek-order safety and SVG draw-on rules for GSAP timelines#2611
xuanruli merged 2 commits into
mainfrom
lint/gsap-seek-safety-and-drawon

Conversation

@xuanruli

@xuanruli xuanruli commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

Five lint rules (plus one extended core pattern) for GSAP defect classes that pass every existing check but break rendered output — the narrow, corpus-clean half of what was originally one PR (split per review; part 2 with the two catalog-touching rules stacks on top as #2612).

  • gsap_repeat_refresh_relative_value (error) — repeatRefresh: true + relative value re-captures and accumulates per iteration; a cold seek into iteration N skips the accumulation (verified with gsap 3.15.0: sequential 47.5 vs cold 17.5).
  • gsap_function_value_hazard (error/warning) — function values that call a method on the first parameter (GSAP passes (index, target, targets) — the first param is a number, so (el) => el.getTotalLength() throws and aborts the seeked frame) or measure the DOM: transform-sensitive reads (getBoundingClientRect, getComputedStyle, gsap.getProperty) are errors; transform-invariant layout reads (offsetWidth, getBBox, ...) are warnings. Pure-index arithmetic, gsap.utils.wrap/distribute, dataset/attribute reads, and closures over build-time constants are exempt.
  • gsap_callback_dom_measurement (warning) — DOM layout measurement reachable from tl.add()/tl.call()/eventCallback/onStart|onUpdate|... via a two-hop named-function scan. The capture path seeks with suppressEvents: false, so callbacks re-fire on every seek and measured geometry is seek-order-dependent. gsap.getProperty-driven derived output (scramble/typewriter patterns) is exempt.
  • svg_measure_before_path_d (error/warning) — getTotalLength() on a <path> with no static d: error when no d assignment exists anywhere (returns 0 in Chrome, silently killing dash animations); warning when assignments exist only inside function bodies. Recognizes setAttribute, GSAP attr: { d }, and CSS d: path().
  • svg_drawon_css_dasharray_conflict (error) — GSAP strokeDasharray on an element whose CSS declares a multi-component stroke-dasharray. GSAP merges per component, so strokeDasharray: pathLength computes to "641.4px, 10px" — the gap stays 10px, the hide-then-reveal hides only 10px, and the line stays visible all scene with a crawling notch. One of this repo's own producer fixtures has this exact bug (true positive from the corpus run).
  • gsap.utils.random() and "random(...)" string tween values added to non_deterministic_code (core) — each worker inits independently, so the same tween resolves different randoms across chunks.

Both motivating production bugs (nodes teleporting at chunk boundaries; a draw-on line visible all scene) are minimally reproduced in the tests.

Review hardening

Two independent adversarial reviews ran before submission: a false-positive hunt over all 393 compositions in this repo plus 23 constructed adversarial snippets (with gsap 3.15.0 semantics experiments), and a maintainer-conventions pass. Fixed FP classes are locked in as negative tests: all-interpolation template ids, GSAP attr-plugin d writes, getProperty-driven callbacks, transform-invariant marquee reads.

Corpus residue for these five rules: 1 error (a genuine dasharray bug in a producer fixture, happy to fix in a follow-up) and 2 warnings (real layout reads in callbacks).

Tests

packages/lint green at this commit in isolation; tsc, oxlint, fallow audit clean.

Notes for reviewers

  • All rules follow the file's conservative philosophy: anything not statically resolvable is skipped; false negatives over false positives.
  • Open question: should the cold-seek family gate on HyperframeLinterOptions.distributed (error when distributed, warning otherwise), following the system_font_will_alias precedent? Happy to wire either way.

Five new rules motivated by two production renders (a dead pulse
animation from measure-before-d, and a draw-on line visible for its
whole scene), plus non-deterministic value detection:

- gsap_repeat_refresh_relative_value (error): repeatRefresh + relative
  value accumulates per iteration; a cold render worker seeking
  non-linearly into iteration N skips the accumulation.
- gsap_function_value_hazard (error/warning): function-valued tween
  vars that read transform-sensitive geometry (getBoundingClientRect /
  getComputedStyle / gsap.getProperty — error), transform-invariant
  layout (offsetWidth, getTotalLength, ... — warning; deterministic
  while the measured layout never animates), or call a method on the
  first (index) parameter (throws at init — error). Pure-index
  arithmetic, gsap.utils.wrap, and closures over constants are not
  flagged.
- gsap_callback_dom_measurement (warning): DOM measurement reachable
  from tl.add/tl.call/eventCallback/onStart-style callbacks (two-hop
  through named functions). The capture path seeks with
  suppressEvents=false, so callbacks re-fire on every seek against
  whatever DOM state the worker's own seek order produced.
  gsap.getProperty-driven derived-output callbacks (scramble text,
  typewriter cursors) are per-frame deterministic and excluded. The
  callback site is reported in the structured selector field.
- svg_drawon_css_dasharray_conflict (error): GSAP strokeDasharray
  writes on elements whose CSS declares a multi-component
  stroke-dasharray — GSAP merges dash lists per component, the CSS gap
  survives, and the hide-then-draw-on trick silently fails. A static
  two-component GSAP value (the fix form) is not flagged; templates
  with no literal id segment are treated as unresolved.
- svg_measure_before_path_d (error/warning): getTotalLength() on a
  <path> with no static d attribute — error when no d assignment
  (setAttribute('d') or GSAP attr plugin) exists anywhere, warning
  when the assignment exists only inside a function body.
  createElementNS-built paths and unresolved variables are skipped.
- non_deterministic_code gains gsap.utils.random() and GSAP
  "random(...)" string tween values — each render worker initializes
  independently, so random values diverge across chunks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mintlify

mintlify Bot commented Jul 17, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟢 Ready View Preview Jul 17, 2026, 5:03 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@mintlify

mintlify Bot commented Jul 17, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟡 Building Jul 17, 2026, 5:03 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@xuanruli
xuanruli marked this pull request as ready for review July 17, 2026 05:21
@xuanruli
xuanruli force-pushed the lint/gsap-seek-safety-and-drawon branch from 056171c to e2fbc02 Compare July 17, 2026 05:30

Copy link
Copy Markdown
Contributor Author

@xuanruli xuanruli changed the title feat(lint): jump-seek safety and SVG draw-on rules for GSAP timelines feat(lint): seek-order safety and SVG draw-on rules for GSAP timelines Jul 17, 2026

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SSOT Review: feat(lint): jump-seek safety and SVG draw-on rules for GSAP timelines

PR #2611 | 5 files | +1667/-41


SSOT inventory

New concept / helper Source of truth Consumers Verdict
RELATIVE_TWEEN_VALUE / isRelativeTweenValue Regex constant + predicate gsap_relative_value_second_writer Single owner
TRANSFORM_SENSITIVE_READ Regex constant gsap_function_value_hazard See observation 1
TRANSFORM_INVARIANT_READ Regex constant gsap_function_value_hazard See observation 1
CALLBACK_MEASUREMENT_PATTERN Regex constant collectMeasuringFunctionNames, expressionReachesMeasurement See observation 1
indexTagsByToken(tags) Extracted function (was inline) 4 rules (overlay, second-writer, initial-hide, dasharray-conflict, measure-before-d) Clean extraction
collectCssOpacityZeroSelectors(styles, tags) Extracted function (was inline) 2 rules (overlay-starts-visible, initial-hide) Clean extraction
selectorResolvesFaithfully(selector) Predicate targetsShareElement Single owner
targetsShareElement(a, b, tagsByToken) Function gsap_relative_value_second_writer Single owner
matchBalanced(source, openIndex, open, close) Generic string helper enclosingObjectLiteral, collectNamedFunctionBodies, collectFunctionBodyRanges, callback rule Single owner
enclosingObjectLiteral(source, index) Function gsap_repeat_refresh_relative_value Single owner
sliceExpression(source, start) Function collectNamedFunctionBodies, callback rule Single owner
parseFunctionValueSource(code) Function gsap_function_value_hazard, gsap_callback_dom_measurement Single owner
escapeRegExp(value) Standard utility 3 helpers Single owner
NUMBER_METHODS Set constant callsNonNumberMethodOnFirstParam Single owner
callsNonNumberMethodOnFirstParam(fn) Function gsap_function_value_hazard Single owner
collectTimelineVarNames(source) Function gsap_callback_dom_measurement Single owner
collectNamedFunctionBodies(source) Function gsap_callback_dom_measurement Single owner
collectMeasuringFunctionNames(bodies) Function gsap_callback_dom_measurement Single owner
expressionReachesMeasurement(expr, measuring) Function gsap_callback_dom_measurement Single owner
resolveScriptElementTokens(source, tags) Function svg_drawon_css_dasharray_conflict, svg_measure_before_path_d Single owner
elementLevelTokens(tokens, tagsByToken) Function svg_drawon_css_dasharray_conflict Single owner
isMultiComponentDasharray(value) Predicate svg_drawon_css_dasharray_conflict, gsapDasharrayValueLooksMultiComponent Single owner
gsapDasharrayValueLooksMultiComponent(valueSource) Predicate svg_drawon_css_dasharray_conflict Single owner
collectFunctionBodyRanges(source) Function svg_measure_before_path_d Single owner
indexInsideAnyRange(index, ranges) Predicate svg_measure_before_path_d Single owner
global flag on LintParsedGsap + GsapWindow Interface field gsap_timeline_set_initial_hide Properly threaded
gsap_fullscreen_overlay_starts_visible fixHint fixHint string Existing rule Reconciled
gsap.utils.random() patterns 2 entries in non_deterministic_code patterns array core rule Single owner

What I checked

  1. Rule ID uniqueness -- all 7 new rule codes are unique across gsap.ts and core.ts.
  2. Helper extraction correctness -- indexTagsByToken and collectCssOpacityZeroSelectors were extracted from inline blocks in existing rules and now serve both the original rule and new rules. The extracted logic matches the removed inline code.
  3. fixHint reconciliation -- gsap_fullscreen_overlay_starts_visible no longer recommends tl.set("${selector}", { opacity: 0 }, 0) (which the new gsap_timeline_set_initial_hide would flag). Updated to recommend gsap.set(...) (outside the timeline). Comment documents the cross-rule invariant.
  4. global flag pipeline -- added to LintParsedGsap interface, threaded through extractGsapWindows to GsapWindow, consumed correctly by gsap_timeline_set_initial_hide to exempt standalone gsap.set() calls.
  5. Measurement regex relationships -- TRANSFORM_SENSITIVE_READ, TRANSFORM_INVARIANT_READ, CALLBACK_MEASUREMENT_PATTERN encode overlapping DOM measurement API sets. Reviewed the intentional exclusion of gsap.getProperty from the callback pattern.
  6. Sentinel-value scan -- no truthy fallbacks where null/undefined/0/false/empty carry distinct meaning. isRelativeTweenValue guards against non-string types before regex. numberValue returns null explicitly for unparsable values.
  7. Side-effect audit -- every rule produces findings; no mutation of shared state. indexTagsByToken and collectCssOpacityZeroSelectors are recomputed per rule invocation (no cross-rule cache sharing).
  8. Temporal overlap logic in gsap_relative_value_second_writer -- verified that other.end <= win.position correctly excludes completed-before-start writers, and that standalone gsap.set() (end = 0) is implicitly handled by the temporal check.
  9. from()/fromTo() exemption in gsap_relative_value_second_writer -- correctly applied to the relative tween (win), not the other writer. from/fromTo resolve relative values at build time (immediateRender), so the exemption is correct.
  10. gsap.utils.random() pattern accuracy -- the string-form regex ["'\x60]random\(\s*[-\d[] requires a digit, minus sign, or bracket after the paren, avoiding false positives on prose strings mentioning random(.
  11. Two-hop closure comment vs. code in collectMeasuringFunctionNames -- verified mismatch (see observation 2).
  12. enclosingObjectLiteral scope in gsap_repeat_refresh_relative_value -- verified false positive risk (see observation 3).
  13. GSAP attr-plugin d assignment regex in svg_measure_before_path_d -- verified it cannot match through nested braces (see observation 4).

Blocking SSOT issues

None found.

Non-blocking observations

1. Measurement regex triplication (low risk)

Three co-located regex constants encode overlapping knowledge about which DOM APIs are measurement calls:

  • TRANSFORM_SENSITIVE_READ = getBoundingClientRect, getComputedStyle, gsap.getProperty
  • TRANSFORM_INVARIANT_READ = getTotalLength, getBBox, offsetWidth, offsetHeight, clientWidth, clientHeight
  • CALLBACK_MEASUREMENT_PATTERN = union of the above minus gsap.getProperty

The relationship is documented in comments but not enforced in code. If a future contributor adds a new measurement API (e.g., getClientRects), they need to update 1-3 constants. The constants are co-located and well-commented, so this is manageable, but composing CALLBACK_MEASUREMENT_PATTERN programmatically from a shared API list would eliminate the duplication:

const MEASUREMENT_APIS = ["getBoundingClientRect", "getTotalLength", "getBBox", /* ... */];

Not blocking -- the current structure is clear and the risk is bounded by proximity.

2. Comment says "two-hop" but code does up to 4-hop

collectMeasuringFunctionNames comment says "Two-hop closure" but the fixpoint loop runs up to 3 passes of transitive propagation on top of the initial direct-measurement set, covering up to 4-hop call chains. The code is correct (deeper transitive closure is strictly better); the comment understates it.

3. gsap_repeat_refresh_relative_value scans nested function bodies

enclosingObjectLiteral returns the full vars object including nested arrow function bodies. The check !/["'\x60][+-]=/.test(objectLiteral) would match a relative-value-shaped string inside an onComplete callback's body, not just in the tween vars themselves. In practice this is very unlikely -- GSAP string "+=..." values don't naturally appear inside callback bodies -- but it's a theoretical false positive path.

4. GSAP attr-plugin d regex doesn't cross nested braces

The regex for detecting gsap.set(var, { attr: { d: ... } }) uses [^{}]* which cannot match through a nested object literal preceding attr. For example, { ease: { power: 2 }, attr: { d: "M..." } } would not match because [^{}]* stops at the { of ease. This produces a false negative (missed d assignment), which conservatively escalates severity from warning to error -- safe but worth noting.

5. svg_drawon_css_dasharray_conflict fixHint backtick nesting

The fixHint at the end of the rule:

"or set the full two-component value in GSAP: `strokeDasharray: `${len} ${len}``."

The nested backticks render as literal characters (it's a regular string, not a template). In lint output or docs this may look confusing with double-backtick sequences. Consider escaping for clarity or using a different delimiter.

Verdict

Approve. The PR is well-structured for SSOT:

  • Two inline code blocks correctly extracted into shared helpers (indexTagsByToken, collectCssOpacityZeroSelectors) with no leftover duplication.
  • Cross-rule fixHint reconciliation between gsap_fullscreen_overlay_starts_visible and the new gsap_timeline_set_initial_hide is explicitly documented and correct.
  • The global flag is properly threaded from parser interface through windows to rule logic.
  • All 7 new rule IDs are unique.
  • No stale plumbing, no sentinel-value hazards, no duplicated decisions.

The non-blocking observations are improvement opportunities, not correctness issues.

--- Miga

@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.

First-look adversarial review at HEAD 056171c1 (base main). Reviewed on a fresh worktree; dispatched parallel adversaries on each rule surface + core additions + docs + corpus-sampling, then verified the load-bearing findings inline (severity misclassification, enclosingObjectLiteral scope walk, varsCallbackPattern reach, function-value regex shapes).

Findings

Blockers: none.

Concerns: 2 mediums (severity misclassification in svg_measure_before_path_d; descendant-selector FP hazard in svg_drawon_css_dasharray_conflict) + 6 lows spread across the 5 rules + core additions. All inline.

Nits: 2 (missing GSAP callback names; malformed nested backticks in fixHint).

Green surface (verified)

  • Conservative philosophy holds — every rule bails on selector ambiguity, cross-composition scoping, and non-statically-resolvable expressions rather than guessing. resolveScriptElementTokens (gsap.ts:884-888) + descendant-selector bails + composition-scoped bails all catch what they claim.
  • Rule 4 suppressEvents: false premise verified end-to-end: packages/cli/src/capture/captureCompositionFrame.ts:223packages/core/src/runtime/init.ts:2256-2275,2612 — undefined seek options resolve suppressEvents === true to false; capture-time callbacks fire. Rule's rationale holds.
  • Rule 6 corpus true-positive verified: packages/producer/tests/software-beginframe-yoyo-compositor/src/index.html:191 declares .trail-path { ... stroke-dasharray: 10, 10; ... } and :333,371 do gsap.set(path, { strokeDasharray: length }) where path gets setAttribute("class", "trail-path") at :328. All rule-6 resolution pieces align — will fire as claimed.
  • Rule 3 exemption structure: numeric-safe method list (NUMBER_METHODS at gsap.ts:1867-1874) covers toFixed/toString/toPrecision/toExponential/toLocaleString/valueOf. gsap.utils.wrap/distribute opaque via __raw: from parser — well-scoped.
  • Two-commit split (e2fbc020c for the 5 rules + core; 056171c18 for the stacked-PR-#2612 pieces) is clean — this PR is self-contained and doesn't depend on #2612.
  • Docs coverage (13 lines in docs/packages/lint.mdx) — rule ids match code verbatim, no hallucinated examples.

What I didn't verify

  • Full 393-composition run against the shipped rules — didn't rebuild + bun lint the corpus. Trusting the "5 errors, 12 warnings" corpus claim structurally (verified the producer-fixture dasharray one; the gooey-metaball 4-error claim is against #2612's rule).
  • Wall-clock cost of the additional pattern scans on the largest scripts — likely negligible for a static lint, unmeasured.
  • Whether hyperframes lint --strict behavior is intended for these severities (rule 4 warning, rule 5 error/warning split, rule 6 error).

Merge gate

The rules are conservatively scoped, well-tested in their positive paths, and the reconciliation with the stacked-PR pieces is coherent. The two mediums are real correctness concerns worth Xuanru's attention before ship — 5-B1 misclassifies severity/message on cross-target d-assignments, 6-B3 fires an ERROR on legitimate scoped-descendant dashes. The lows are worth addressing but non-blocking. On the "distributed gating" open question — matches the fonts.ts:199 precedent, would recommend adopting for the cold-seek family.

LGTM on the mechanism; holding 5-B1 + 6-B3 as substantive concerns worth Xuanru's decision before ship. Leaving as COMMENTED.

Review by Rames D Jusso

Comment thread packages/lint/src/rules/gsap.ts Outdated
Comment thread packages/lint/src/rules/gsap.ts
Comment thread packages/lint/src/rules/gsap.ts
Comment thread packages/lint/src/rules/gsap.ts Outdated
Comment thread packages/lint/src/rules/gsap.ts Outdated
Comment thread packages/lint/src/rules/gsap.ts
Comment thread packages/lint/src/rules/gsap.ts
Comment thread packages/lint/src/rules/core.ts Outdated
Comment thread packages/lint/src/rules/gsap.ts Outdated
Comment thread packages/lint/src/rules/gsap.ts Outdated
@xuanruli
xuanruli requested a review from miguel-heygen July 17, 2026 05:59

@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.

R2 delta review from e2fbc020cc2d9ac0b (+209 lines gsap.ts, +70 lines gsap.test.ts, +11 lines core.ts, new +66 lines core.test.ts). Every R1 finding is addressed with both code and test — clean pass.

R1 concerns → R2 disposition

# R1 concern R2 disposition
5-B1 svg_measure_before_path_d used dAssignments.length > 0 script-wide → wrong varName filtering 🟢 Fixed: dAssignments.some((a) => a.varName === varName). Test svg_measure_before_path_d: ERROR when only a different path has a d assignment locks it in.
6-B3 svg_drawon_css_dasharray_conflict folded .decorative-frame .wire to .wire in multiDashTokens → FP at error on any element matching .wire 🟢 Fixed: combinator/whitespace groups (/[\s>+~]/) are now skipped when building multiDashTokens. Test svg_drawon_css_dasharray_conflict: does NOT flag a descendant-scoped CSS dasharray covers exactly the pattern I flagged.
6-B1 writerPattern inspected only the first vars object → fromTo(sel, {from}, {strokeDasharray: 100}) writer went undetected 🟢 Fixed: method === "fromTo" now iterates [firstVars, secondVars]; also accepts kebab-case "stroke-dasharray".
R2 enclosingObjectLiteral walked to nearest { and matchBalanced grabbed the outer body including nested callback vars → repeat-refresh rule FP'd on relative values inside onComplete: () => gsap.to(...) 🟢 Fixed: new objectLiteralHasTopLevelRelativeValue tracks depth char-by-char and only accepts [+-]= at depth 1. Test gsap_repeat_refresh_relative_value: does NOT flag relative values nested in callback vars verifies.
4-B2 varsCallbackPattern scanned the whole script, matching bare onStart: in any object → callback-hazard FP on non-GSAP objects 🟢 Fixed: new isInsideGsapTweenVars(source, index, timelineVars) guard walks back to the enclosing { and confirms it belongs to gsap.{set,to,from,fromTo,timeline}(...).
4-B1 varsCallbackPattern was missing onInterrupt and onOverwrite 🟢 Fixed: regex now \bon(?:Start|Update|Complete|Repeat|ReverseComplete|Interrupt|Overwrite)\s*:\s*. Also, eventCallbackPattern is now correctly scoped per timeline var (was previously loose).
R3-a callsNonNumberMethodOnFirstParam required trailing \( → property reads like i.length escaped 🟢 Fixed: renamed firstParamMemberAccessHazard; regex dropped the trailing \(, then reports any member access that isn't a number-method call.
R3-b parseFunctionValueSource split by , and only stripped = default → destructured / type-annotated first params (({index}, target), (i: number, t)) parsed as bogus first-param name 🟢 Fixed: new normalizeFirstParam strips = default, : TypeAnnotation, and rejects {/[ (destructuring) via null return.
core-F6 /["']random(\s*[-\d[]/didn't match"+=random(...)"/"-=random(...)"` 🟢 Fixed: pattern now ["'](?:[+-]=)?random(\s*[-\d[]. Test detects prefixed '+=random(...)' string tween values` locks it in.
nit Nested backticks inside a template literal in the svg_drawon_css_dasharray_conflict fixHint 🟢 Fixed: fixHint switched to single quotes with literal ${len} ${len} text.

Additional net-new checks I ran on R2 code

  • Infinite repeat: -1 (via extractGsapWindows — shared with #2612): now sets end = Number.POSITIVE_INFINITY for non-set tweens with negative repeat, so downstream rules over windows[] see a well-typed end. formatTime in #2612's rule 1 renders . Consistent.
  • resolvedStart ?? position migration in extractGsapWindows: catches string-position tweens ("+=1", "<") whose absolute time the parser resolves. Was a continue skip before — now they participate in overlap detection. Broad correctness win.
  • firstParamMemberAccessHazard still reports the FIRST hazard and bails: i.toString(16) + i.foo reports foo, i.toString(16) alone passes. Correct.

What I didn't verify

  • Did not run the test suite locally against c2d9ac0b; trusting CI (green on Lint/Test/Preflight at this SHA per statusCheckRollup).
  • Did not enumerate every unchanged rule against every prior R1 line — this delta review scopes to the R1-flagged surfaces plus scan of net-new code.

LGTM from my side. All 10 prior findings resolved with matching tests. Leaving as COMMENTED — approval is James's call.

Review by Rames D Jusso

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact head c2d9ac0b.

I read the complete four-file diff and rechecked the seek-order, selector-scope, SVG draw-on, and callback-measurement paths against the focused regression coverage. The current head closes the previously reported severity and scope gaps, has no unresolved review threads, is mergeable, and all applicable required checks are green.

Verdict: APPROVE
Reasoning: The current head addresses the prior review findings without introducing a blocking correctness issue, and required CI is green.

— Magi

@xuanruli
xuanruli merged commit f3d2100 into main Jul 17, 2026
43 checks passed
@xuanruli
xuanruli deleted the lint/gsap-seek-safety-and-drawon branch July 17, 2026 06:22
xuanruli added a commit that referenced this pull request Jul 17, 2026
…es (#2612)

## What

Part 2 of the GSAP seek-safety rules (stacks on #2611): the two rules that touch existing catalog content and required reconciliation with an existing rule.

- `gsap_relative_value_second_writer` (error) — a relative var value (`y: "-=15"`) on a property whose target has another writer **active at the relative tween's start**. The relative base is captured at tween init, which reads a different partial state per seek path: sequential seek inits it mid-entrance, a cold render worker inits it at the entrance's end state, and the element teleports at chunk boundaries (production case: all scene nodes jumping ~20px mid-scene). Writers that complete strictly before the start are safe (children render in start-time order within a seek pass — verified against gsap 3.15.0) and are not flagged; neither are single-writer relatives, `from()`/`fromTo()`, build-time `gsap.set`, or relative position parameters (`"+=0.5"`). Selector resolution bails on combinators and cross-composition scoping rather than guessing. Findings aggregate per tween pair and report the overlap window.
- `gsap_timeline_set_initial_hide` (warning) — initial-state hiding via `tl.set(target, vars, 0)` on a paused timeline is not rendered while the playhead sits at exactly 0, so frame 0 shows the unhidden state (verified against gsap 3.15.0: opacity stays 1 after `tl.time(0)`, applies only past 0). Exempt when the target is already hidden by authored CSS/inline styles or a standalone `gsap.set()`, and only sets preceding every tween in source order qualify (mutated position variables resolve to their initial binding in the parser — outro hard-kills don't masquerade as position-0 sets).
- Reconciliation: `gsap_fullscreen_overlay_starts_visible`'s fixHint previously recommended exactly the flagged `tl.set(sel, {opacity:0}, 0)` pattern; it now recommends authored CSS hiding or immediate `gsap.set()`.
- Docs for the full rule family in `docs/packages/lint.mdx`.

## Corpus impact (the reason this is its own PR)

These two rules are the ones that fire on repo-shipped content:

- `gsap_relative_value_second_writer`: 4 errors in `gooey-metaball`, all genuine overlaps. Measured with gsap 3.15.0: ballD diverges **3.31 xPercent / 1.99 yPercent (~8px/5px at 240px ball size)** between sequential and cold seek — a permanent base offset that appears as a teleport at a chunk boundary. Real but modest; happy to fix the block in a follow-up (start the drift at the entrance's end, or use absolute `fromTo`).
- `gsap_timeline_set_initial_hide`: 10 warnings across the catalog after the CSS-hidden exemption (down from 54 pre-narrowing); spot-checked as genuine frame-0 pops with no authored hide (e.g. `vfx-text-cursor` `#phrase-b`).

Adversarially reviewed the same way as #2611 (393-composition corpus + gsap semantics experiments); FP classes fixed and locked as negative tests: precede-only second writers, descendant/cross-composition selector mis-joins, CSS-hidden re-assertions, mutated position variables.

## Tests

Full `packages/lint` suite green at 440 tests including multi-composition roots; `tsc`, oxlint, fallow audit clean.
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.

4 participants