Commit Graph
10 Commits
Author SHA1 Message Date
James Russo 7a294f1956 fix(lint): address render preflight review feedback (#2739) 2026-07-22 22:59:12 -04:00
Xuanru Li f3d2100663 feat(lint): seek-order safety and SVG draw-on rules for GSAP timelines (#2611)
## 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.
2026-07-16 23:22:18 -07:00
Miguel Ángel ada878fdcd fix(lint): consolidate lint and audit correctness (#2413)
* fix(lint): stop CSS comments in <style> from manufacturing phantom root tags

extractOpenTags scans raw source text with a flat regex that has no
concept of <style>/<script> block boundaries, so a CSS comment like
`/* <g> wrapper */` inside a <style> block reads as a real open tag.
findRootTag consumes that flat tag list and only skips tags literally
named script/style/meta/link/title, so the phantom <g> tag (not in
that skip list) wins the "first non-ignored body tag" search and gets
returned as the composition root instead of the real one that follows.

This manufactured root_missing_composition_id and root_missing_dimensions
(the phantom tag has neither) plus head_leaked_text (the leaked-text
scan slices up to the phantom tag's position, landing inside the
<style> block before its real closing tag, so the raw CSS text reads
as leaked markup) on an otherwise valid sub-composition — reported
with an exact bisected repro: a <template>-wrapped SVG sub-composition
whose <style> block comments reference an inner <g> element.

Fix: compute <style>/<script> content spans up front (reusing the
existing extractBlocks + STYLE_BLOCK_PATTERN/SCRIPT_BLOCK_PATTERN) and
skip any TAG_PATTERN match that falls inside one, before it ever
reaches findRootTag or any other extractOpenTags consumer. Same shape
as the prior fix for a leading <svg> defs block being mistaken for the
root (8ee4b7df) — this closes a sibling gap in the same function.

Test: new regression case with a <style> block containing a `/* <g> */`
comment ahead of an <svg data-composition-id> root, asserting none of
the three findings fire. Full lint package suite (318 tests) passes.

* feat(lint): flag duplicate data-composition-id values

Declaring data-composition-id on more than one element (commonly the <meta>
tag from the quickstart template AND the root <div> added to satisfy
root_missing_composition_id) is a silent collision: `compositions --json`
returns two entries for the same id (one duration:0) and inspect/snapshot
crash with "Cannot read properties of undefined (reading totalDuration)".
Lint passed clean through all of it.

New rule `duplicate_composition_id`: group elements by data-composition-id
value and error on any value shared by 2+ elements, naming the id and calling
out the meta-vs-root collision in the fixHint. 3 tests: dup fires, single id
passes, two distinct ids don't collide. (Implemented via Codex; verified
independently: 111 lint tests pass, oxfmt/oxlint clean.)

* fix(audits): avoid caption false positives

* fix(lint): ignore proxy-label tween overlaps

* fix(cli): preserve the five-percent text audit floor

* fix(lint): preserve proxy identity across lexical scopes

* fix(cli): audit only directly painted text

* fix(lint): compare live composition ids canonically

* fix(lint): preserve expanded proxy identities

* fix(cli): measure directly painted text geometry

* fix(lint): preserve first duplicate attribute value

* fix(lint): keep shared proxy identity across helpers

* fix(parsers): preserve expanded proxy identity

* fix(lint): decode composition IDs consistently
2026-07-14 20:13:01 -04:00
Miguel Ángel c18391b98c fix(lint): honor data-no-timeline on root compositions (#2228) 2026-07-11 18:32:15 -04:00
Miguel Ángel 05c3b5503f fix(lint): parse HTML structure without regex (#2223)
* fix(lint): ignore scripts inside quoted attributes

* Address PR review feedback (#2223)

- replace repeated attribute scanner with quoted tag ranges
- remove the Fallow complexity finding

* refactor(lint): parse HTML structure with htmlparser2

* fix(lint): traverse template style sources

* test(lint): cover nested template style sources
2026-07-11 00:03:07 -04:00
Miguel Ángel d51fa7eba2 fix(lint): flag digit-leading element ids (#2222) 2026-07-10 23:03:17 -04:00
Miguel Ángel 8ee4b7dfda fix(lint): stop a leading <svg> defs block from being mistaken for the composition root (#1867)
Two independent post-release feedback reports of the same mechanism: a
leading <svg> block (icon/gradient/filter <defs>, referenced by url(#id)
elsewhere in the document) placed before the real [data-composition-id]
root manufactures root_missing_composition_id + root_missing_dimensions
on an otherwise-correct composition. Moving the <svg> after the root
cleared both findings for each reporter.

findRootTag returned the first body child that wasn't script/style/meta/
link/title, unconditionally — <svg> was never in that skip list, so a
leading defs-only <svg> got treated as the root.

Fix: skip a leading <svg> when it carries none of the composition markers
itself (data-composition-id/data-width/data-height), so an intentionally
SVG-rooted composition is still eligible as the root. The first attempt at
this only skipped the <svg> open tag, which surfaced a second bug:
extractOpenTags is a flat, nesting-unaware scan, so the very next tag it
returns after skipping <svg> is the svg's own nested child (<defs>,
<filter>, ...), not the sibling after </svg>. Track the svg's closing tag
position and skip every tag before it, not just the <svg> tag itself.

Tests: skips a leading svg defs block (no false root findings); still
treats an <svg> as the root when data-composition-id/data-width/
data-height are declared directly on it. Full lint suite (308 tests) passes.
2026-07-02 17:45:09 -07:00
Miguel Ángel a908af11a8 feat(cli): keyframes command (surface GSAP/CSS/Anime keyframes + 3D onion-skin --shot) (#1603)
Renames the motion-surfacing tool from `hyperframes keyframes` to `hyperframes motion`,
renames the implementation from keyframes*.ts to motion*.ts (keeping the keyframe data
model name where still accurate), and renames the shipped skill from
hyperframes-keyframes to hyperframes-motion. Expands the skill from a command
reference into a full motion-design workflow: reading motion, 3D angle verification,
layered GSAP motion, one-shot reference reproduction, diagnostic checks, and
eval-derived craft guidance.
2026-07-01 19:51:55 -07:00
Miguel Ángel d1038918bb fix(lint): catch visible markup comments (#1819)
* fix(lint): catch visible markup comments

* test(lint): cover visible comment exemptions

* fix(lint): harden visible comment scan
2026-06-30 20:49:04 -07:00
Miguel Ángel 98d0bdd73c refactor: extract @hyperframes/lint from core (#1756)
* refactor: extract @hyperframes/lint package from core

Moves all lint rules, hyperframeLinter, lintProject, and related types
from packages/core/src/lint/ into a new standalone packages/lint package.

Core keeps a thin re-export stub at @hyperframes/core/lint for backward
compatibility. Consumer imports (cli lint command, producer hyperframeLint)
are updated to import from @hyperframes/lint directly.

Depends on @hyperframes/parsers (PR #1755).

* fix: restore postcss-selector-parser in core (sourceMutation.ts still uses it)

* fix(ci): add parsers+lint to Dockerfile and build before preview tests

* chore: update bun.lock after restoring postcss-selector-parser dep

* test(cli): update lintProject test for string-dir signature from @hyperframes/lint

* refactor(core): single-source the lint engine in @hyperframes/lint

Delete core's byte-identical copy of the lint rule engine and re-point
staticGuard at @hyperframes/lint, so the render-time render-gate and the
studio preview share one rule engine instead of two copies that could
silently diverge. Back-compat preserved via the @hyperframes/core/lint stub.

Addresses review feedback on the dual-copy footgun.
2026-06-27 01:16:40 -04:00