Skip to content

feat(lint): warn when a sub-composition slot blanks before the host (#1540)#1542

Merged
miguel-heygen merged 1 commit into
mainfrom
fix/subcomp-slot-blank
Jun 17, 2026
Merged

feat(lint): warn when a sub-composition slot blanks before the host (#1540)#1542
miguel-heygen merged 1 commit into
mainfrom
fix/subcomp-slot-blank

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What & why

Closes the silent-blank authoring trap reported in #1540.

A sub-composition mount whose data-duration ends before its host composition's window leaves the slot blank for the remaining time. The runtime behavior is correct and unchanged: data-duration is the slot's visible window and takes precedence over the child's GSAP timeline — a 15s clip on a 331s composition is supposed to end at 15s. This is distinct from #911 (a child whose GSAP timeline was shorter than its slot — fixed by #917, which holds the last frame through the slot). #1540 is a child whose own data-duration is shorter than the host, which is almost always an authoring mistake that fails silently.

So this PR surfaces the mismatch instead of changing the (correct) runtime.

Changes

Verification

  • bun run build — exit 0, typecheck clean
  • bunx vitest run src/lint/rules/composition.test.ts — 61/61 pass
  • oxlint — 0 warnings, 0 errors

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

Reviewed at ccb5b116. Nice scoping — rule is targeted at the high-signal shape from #1540 and rightly leaves the runtime semantics alone. A few notes, all non-blocking.

Concerns

tags is flat — tailCovered treats descendants of the candidate as siblings. extractOpenTags returns every open tag in the source; the rule's timed array filters out only the root tag. If a data-composition-src mount contains an inner element with data-start/data-duration that happens to extend to the host's end (e.g. a stray inline timed div in the host file, or content the author put inside the slot div by mistake), tailCovered would suppress the warning even though that descendant lives inside the slot that's about to blank. By convention data-composition-src divs are empty in source (content comes from the external file), so this is a low-probability false negative — but worth one of two cheap mitigations: (a) restrict tailCovered to siblings of the candidate (parent-aware iteration) or (b) require the suppressing element to itself have data-composition-src or class~="clip". Existing rules (timeline_track_too_dense, overlapping_clips_same_track) all treat tags as flat too, so a comment noting the deliberate flat-scope is also acceptable here.

Two siblings, both shorter than host, no tail-coverer. If two sub-comp mounts both start at ~0 and both end before the host (e.g. start=0,dur=10 and start=0,dur=12, host=60), each one's tailCovered check sees the OTHER's end=10/12, which is also < rootDuration - EPSILON, so both warn. Correct outcome (both clips blank the slot), but the message text reads "its slot will be blank" twice on what is really the same gap. Probably fine — two distinct warnings is louder/clearer than one — just flagging in case you'd rather collapse.

Nits

  • Message and fixHint duplicate "data-duration is the slot's visible window" — the message is already clear; the fixHint could lead with the actionable line ("Set this sub-composition's data-duration to X to fill the host window…") and drop the restated semantics. (nit)
  • Issue reference in the message. Lint messages that link to the canonical issue are dramatically more useful when an LLM hits them. Consider appending (see #1540) or a docs anchor — the skill doc you added is the natural target. (nit)
  • Boundary at exactly START_TOLERANCE=0.5. t.start > 0.5 excludes 0.5 itself; the test at start=0.3 confirms intent. Consider one test pinning the boundary (start=0.5 → fires; start=0.51 → silent) so future refactors don't drift the tolerance silently. (nit)
  • fallow-ignore-next-line complexity — matches the existing project-wide convention (10+ uses in packages/core/src), confirmed not a typo. No action needed; mentioning for the record in case a future contributor wonders.

What I verified

  • Rule registers via the existing compositionRules array → hyperframeLinter.ts re-spreads it; no separate registration list to update.
  • rootTag is findRootTag = first non-script/style element inside <body>; correct anchor for "the host composition."
  • Skill doc cross-references #911/#917 and #1540 correctly, distinguishes hold-through-slot from blank-before-host, names the new rule code so an LLM grepping for the warning will land on the explanation.
  • Test suite covers the issue repro shape + 8 negative cases (intentional intro, child≥host, non-mount, missing root duration, late start, unknown-duration sibling). Solid boundary coverage.

What I did NOT verify

  • Did not run bun test locally (no bun in my env). Relying on the PR body's 61/61 pass.
  • Did not trace the rule against real-world index.html files in examples/ or registry/ to confirm zero false positives on existing scaffolds. Worth a quick npx hyperframes lint sweep over examples/ before merge if not already done.

— Rames D Jusso

@miguel-heygen miguel-heygen force-pushed the fix/subcomp-slot-blank branch from ccb5b11 to 25df1a3 Compare June 17, 2026 21:43
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Pushed 25df1a3f9 addressing the nits:

  • Zero-FP sweep — linted all 21 shipped scaffolds under registry/ that have a data-composition-src mount: 0 subcomposition_blanks_before_host findings. The decision-tree scaffold (root + child both 15s → child window == host) correctly stays silent.
  • Flat-tags / tailCovered scope — added a code comment documenting that descendants of the candidate slot are treated as siblings, why the false-negative path is rare (src mounts are empty by convention), and that it matches the flat-tag scope of the sibling rules.
  • START_TOLERANCE boundary — added a test pinning that start=0.5 exactly still fires (the > comparison is inclusive of the boundary). 62 tests now.
  • fixHint redundancy — trimmed the clause that restated the message; the fixHint is now purely the "why + what to do."

Left as-is intentionally:

  • Two short siblings both warn — correct behavior and low-frequency; collapsing felt like YAGNI. Easy to revisit if it's noisy in practice.

On the runtime-semantics question for @vai: the PR encodes data-duration-takes-precedence as canonical, which matches the #917 fix (usesExternalCompositionSlot holds the child's final frame through its authored slot rather than clamping to the live timeline). The rule and docs assume that invariant holds; if there's drift there, the heuristic's framing would need to follow.

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

Review at HEAD 25df1a3f — addendum to Rames' approve-with-nits above.

The author has, with admirable dispatch, already squashed in patches answering three of Rames' four nits — sound deductions, but a chronological alibi is owed.

What the new HEAD silently absorbed

Rames reviewed ccb5b116; this is 25df1a3f. The author has:

  • Added a code comment at tailCovered declaring the flat-tag scope deliberate and naming the empty-by-convention data-composition-src invariant that bounds the FN window. Addresses Rames' concern #1 the right way (comment, consistent with sibling rules in composition.ts).
  • Added a start=0.5 boundary test pinning START_TOLERANCE inclusive. Addresses nit #3.
  • Trimmed the fixHint — the actionable line ("Set this sub-composition's data-duration to X…") now leads. Addresses nit #1.

Not changed: two-short-sibling double-fire (Rames' concern #2, explicit "optional") and (see #1540) (nit #2). Both author-choice; no other rule in packages/core/src/lint/rules/*.ts links an issue, and the skill-info doc already does the LLM-grep job.

Independent observations

  1. EPSILON and START_TOLERANCE both 0.5s, named separately, sharing the magic. Defensible (sub-frame tolerance), but on a 6s host with a 5.4s child the tail-tolerance silently swallows ~10% blank. Real-world hosts are long (#1540 repro was 331s), so zero practical exposure. A one-line comment naming why both constants share 0.5 would pre-empt a future reader trying to tune them apart.

  2. Deliberate scope under-coverage at start≠0. A late-starting (start=40, dur=5) sole sub-comp on a 60s host can blank too, but the negative test pins this silent. The PR body ("sole/dominant external mount starting at ~0") and the skill-info doc ("leaving a single full-bleed sub-composition") agree — documented scope, not a silent gap. Flagging so a future bug report of this shape doesn't catch a reviewer off-guard.

  3. Partial zero-FP sweep: registry/examples/decision-tree/index.html (15s/15s) does not fire. Rames' broader examples/ sweep is reasonable; rule scope is narrow enough that the risk is low.

  4. Skill-info is the highest-leverage piece, as Miguel and Rames called. The rule code is named verbatim, the #911/#917 vs #1540 distinction is correctly drawn, and runtime semantics precede lint behavior — exactly the order an LLM hitting the warning needs.

Recommendation

CLEAR. Rames' concerns are either patched or author-choice-defensible at this HEAD. The EPSILON-comment note is a nit, not a hold.

Review by Via

A sub-composition mount whose data-duration ends before the host
composition's window leaves its slot blank for the remainder. The
runtime behavior is correct (data-duration is the slot's visible window
and takes precedence), but a full-bleed sub-composition shorter than the
composition is almost always an authoring mistake that fails silently
(issue #1540).

Add the subcomposition_blanks_before_host rule, scoped narrowly to the
high-signal shape — a sole/dominant external mount starting at ~0 whose
window ends before the host's — so it stays silent on intentional short
clips. Document the slot-window semantics in the sub-compositions
reference, distinguishing the hold-through-slot case (#911/#917) from
the blank-when-shorter-than-host case.
@miguel-heygen miguel-heygen force-pushed the fix/subcomp-slot-blank branch from 25df1a3 to f741e5f Compare June 17, 2026 21:47
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Heads up @vai — HEAD moved to f741e5fd4 (was 25df1a3f). Comment-only change: a one-line rationale on EPSILON/START_TOLERANCE noting they're two independent knobs sharing 0.5s and the short-host tradeoff (the independent nit from the latest pass). No logic change — 62/62 tests, lint/fmt/typecheck green. Stamp f741e5fd4.

The two-short-sibling double-fire and the (see #1540) message link I left as-is per the review (optional / would be a new cross-link convention the skill-info doc already covers).

@miguel-heygen miguel-heygen merged commit 13af554 into main Jun 17, 2026
46 checks passed
@miguel-heygen miguel-heygen deleted the fix/subcomp-slot-blank branch June 17, 2026 21:56
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