* 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
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.
WINDOW_TIMELINE_ASSIGN_PATTERN only matched window.__timelines["literal"]
or window.__timelines.prop, so registrations via a computed key like
window.__timelines[spec.id] (used by the code-particle-assemble and
code-3d-extrude registry blocks) went undetected. That made
gsap_timeline_not_registered false-fire on correctly registered timelines,
and let root_composition_missing_duration_source wrongly demand an
explicit data-duration on compositions that already have one.
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.
* fix(producer,lint): id-less media renders blank wash instead of footage
A timed <video>/<audio> identified only by a Studio-stamped `data-hf-id`
(no real `id`) rendered as a flat white/grey wash with dropped audio, and
lint stayed silent so it surfaced only at render.
Root cause, two layers:
- lint `readAttr(tag, "id")` used a `\b` boundary, which treats the hyphen
in `data-hf-id="…"` as a word break — so reading "id" matched the trailing
`id="…"` inside `data-hf-id` and returned a phantom id. `media_missing_id`
therefore never fired for media carrying only a data-hf-id. Switched to a
`(?<![\w-])` lookbehind so a short name can't match the tail of a longer
hyphenated attribute (also fixes "width" matching `data-width`, etc.).
- the render pipeline identifies media by the real `el.id`: frame extraction
keys injected stills as `__render_frame_<id>__`, the runtime frame-swap
matches on `el.id`, and the audio mixer selects `audio[id][src]`. An empty
`el.id` meant injected frames/audio never matched. compileForRender now
assigns a stable positional id to every id-less timed media element before
any stage parses or serves the HTML.
Adds a producer regression fixture (video with data-hf-id, no id) and a lint
test covering the data-hf-id/id collision. Baseline mp4 generated separately.
* test(producer): baseline for video-hfid-no-id regression fixture
Golden compiled.html + output.mp4 (generated on linux/amd64 in the
Dockerfile.test image). Compare-mode passes: compilation, visual (0 failed
frames), and audio (correlation 1.000). A regression to the blank-wash
behaviour fails the visual check.
* 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.