Skip to content

feat(lint): layout rules + hollow-master FP fix + deferred-token carve-out#1226

Open
ukimsanov wants to merge 4 commits into
mainfrom
feat/lint-rules-v3
Open

feat(lint): layout rules + hollow-master FP fix + deferred-token carve-out#1226
ukimsanov wants to merge 4 commits into
mainfrom
feat/lint-rules-v3

Conversation

@ukimsanov
Copy link
Copy Markdown
Collaborator

@ukimsanov ukimsanov commented Jun 5, 2026

Why

Multi-round agent evals on /website surface the same recurring composition bugs across LLM tiers: absolute child widths collapsing inside max-width-only parents (renders correctly in Studio but breaks at engine seek), translate-less left:50%/top:50% centering, GSAP/CSS transition fighting on the same property, word-stagger spans with display:block, nowrap text without bounding width, etc. The existing linter caught structural issues (missing data-composition-id, wrong file paths) but not these "agent visually shipped it green" classes.

A separate bug: master_timeline_orchestrates_sub_compositions was firing as a false positive on agent-authored work that wraps tl.to in helpers like xfade("#beat-1-host", "#beat-2-host", 4.15) — the actual tl.to args are variables so the original regex missed them. And audio_src_not_found was firing on <<tts_*>> / <<music_*>> deferred-token placeholders, breaking the SHIP GATE check on every run because those tokens are resolved post-finish_execution.

What

  • packages/core/src/lint/rules/layout.ts (new): ~22 layout/style rules. Highlights: absolute_width_collapse, absolute_center_missing_translate, hero_absolute_center_maxwidth_only, word_stagger_block_display, nowrap_missing_max_width, gsap_css_transition_conflict.
  • packages/core/src/lint/rules/composition.ts: FP guard for the xfade() helper pattern + FP guard for HyperFrames-native data-track-index scheduling.
  • packages/cli/src/utils/lintProject.ts: deferred-token carve-out for audio_src_not_found covering <<tts_*>>, <<music_*>>, <<audio_*>>, <<sfx_*>>, <<sound_*>>, <<narration_*>>.
  • 72 layout tests + 16 new composition tests + 4 new lintProject tests + async migration to match main's updated lintHyperframeHtml / lintProject signatures.

How

  • One rule per real failure mode — each layout rule corresponds to a specific bug class observed in agent output, with a positive baseline test ("fires when broken") and an FP-guard test ("doesn't fire on the legitimate variant"). Rules are intentionally narrow; broad heuristics cause noise.
  • Pattern-matching helpers, not metric-driven splits — layout.ts has long pattern matchers (CSS selector parsing, tag-tree walks). Splitting them across helpers purely to lower CRAP score would harm readability without changing behavior. Added // fallow-ignore-file complexity with explanation, matching the precedent in packages/engine/src/services/frameCapture.ts.
  • xfade() helper FP guard: for each sub-comp not yet marked orchestrated, check if its data-composition-id OR wrapper id= is mentioned as a #<id> string literal anywhere in the script blob. Catches helper-call arguments without false positives on adjacent ids like #beat-1 inside #beat-12 (escaped regex with proper meta-char handling — addresses the CodeQL escape finding).
  • data-track-index FP guard: when every sub-comp host has data-track-index, treat them as runtime-native track placement (parallel layers) rather than GSAP-seek orchestration. The hollow-master agent bug uses sequential timelines without track indexes; that path still fires. (This unblocked the bundled warm-grain smoke test which uses native track placement.)

Test plan

  • bun run --cwd packages/core test src/lint → 262/262 pass
  • bun run --cwd packages/cli test src/utils/lintProject.test.ts → 55/55 pass
  • bunx oxlint + bunx oxfmt --check → clean
  • CLI smoke (hyperframes init warm-grain → lint) → 0 errors locally (was firing master-tl FP before the data-track-index guard)
  • CodeQL escape finding resolved (proper regex meta-char escape)

…e-out

Adds rules that catch recurring bugs observed across w2h agent eval rounds:

Layout (new file, ~22 rules):
- absolute_width_collapse, absolute_center_missing_translate
- hero_absolute_center_maxwidth_only
- word_stagger_block_display (block on .word inside stagger animation)
- nowrap_missing_max_width, gsap_css_transition_conflict
- + others surfacing bad CSS contracts that render correctly in Studio
  but break under render-engine seek.

Composition:
- master_timeline_orchestrates_sub_compositions: FP guard for the
  xfade() helper pattern. Authors sometimes wrap tl.to in a helper
  (function xfade(outSel, inSel, t) { tl.to(outSel, ...); }) then call
  xfade('#beat-1-host', '#beat-2-host', 4.15). The tl.to args are
  variables so the original regex missed them — now we also match
  '#<host-id>' literal anywhere in the script blob.

CLI:
- lintProject: skip audio_src_not_found for deferred tokens
  (<<tts_*>>, <<music_*>>, <<audio_*>>, <<sfx_*>>, <<sound_*>>,
  <<narration_*>>). Placeholders resolved post-finish; rule
  was breaking the SHIP GATE check.

Tests:
- composition.test.ts: 16 new tests + async migration for all sites
- layout.test.ts: 72 tests covering all new rules
- lintProject.test.ts: 4 tests for deferred-token carve-out
Comment thread packages/core/src/lint/rules/composition.ts Fixed
ukimsanov added 3 commits June 5, 2026 13:56
… data-track-index

- composition.ts:860 — escape regex meta chars properly. Previous escape
  only handled '-'; CodeQL flagged the missing backslash handling. New
  escape covers all ECMA regex meta: .*+?^${}()|[]\\-
- master_timeline_orchestrates_sub_compositions: skip the rule when every
  sub-comp host has data-track-index. That attribute signals HyperFrames
  runtime native track placement (parallel layers), not GSAP-seek
  orchestration. The bundled warm-grain example uses this pattern and was
  hitting the rule as a false positive, breaking CI smoke.
Lint rule functions in layout.ts and composition.ts are intentionally
deep pattern matchers — branching reflects the variety of CSS/JS shapes
they recognize, not hidden complexity. Refactoring purely to lower CRAP
score would split each rule across multiple helpers and hurt readability
without changing behavior. The same pattern is used in
packages/engine/src/services/frameCapture.ts.

lintProject.test.ts: each test sets up its own HTML fixture via the
standard vitest scaffolding; the surface-level clone is the test-style
shape, not extractable without losing per-test clarity (matches the
clone-families ignore on packages/studio/src/player/components/timelineDragDrop.ts).
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.

2 participants