mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
v0.7.44
27
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e52bfc246a |
Merge pull request #2053 from heygen-com/vi/figma-loop-fixes
fix(core,cli,lint): close the figma brand-token loop — runtime CSS variables, --name, snippet lint |
||
|
|
4fc619e4f1 |
Merge remote-tracking branch 'origin/main' into feat/lint-gsap-non-transform-motion
# Conflicts: # skills-manifest.json |
||
|
|
def276524b |
fix(core,cli,lint): close the figma brand-token loop — runtime CSS variables, --name, snippet lint
Brand-loop live test (SDS duplicate, plans/figma/brand-loop-test-plan.md) proved the recolor chain end-to-end and surfaced three gaps: - runtime now defines every declared composition variable as a CSS custom property (document root at init + scoped sub-comp hosts in the loader), so imported var(--slug, literal) fills resolve live — without this the frozen literal always won and variable-driven rebranding could not propagate. Slug kept byte-compatible with the figma importer (parity test). render --variables overrides win. - figma component --name: variant frames are often all named 'Platform=Desktop' and slug-collided across imports. - imported fragments carry data-hf-snippet and the project linter skips composition-root rules for them. - /figma skill documents the field-tested non-Enterprise tokens path (MCP get_variable_defs joined with REST boundVariables ids). Shared-helper extractions (injectScopedStyles, flattenedRoot module, parseHostVariableValues, rasterizeFallback, shapeCss) satisfy the dedup/complexity audit the runtime changes tripped. Validated live: brand-loop renders purple from the attribute alone (no manual :root); 118 figma + 662 runtime/compiler + 331 lint tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
992a9b6607 |
feat(lint,player): fast-capture lint rule + player media sync (#1921)
* feat(engine): drawElementImage capture service * feat(engine): 3D projection + compositor-effect risk gate * fix(engine): gate filter drop-shadow wherever blur gates (review) detectCssEffectRisk documented drop-shadow as a ~29dB damage case but only detected blur( in its three scan paths — a drop-shadow comp stayed on the fast path despite the gate's own correctness contract. Detect drop-shadow( in computed styles, stylesheet rules, and GSAP tween vars, pinned by a focused test that runs the real page-side closure against a DOM shim (computed / stylesheet / tween coverage + blur regression + effect-free null). Addresses miguel-heygen's blocker on #1918. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(engine): frame-capture core — fast-capture routing, worker-encode, dedup extension # Conflicts: # packages/engine/src/services/screenshotService.ts * fix(engine): document HF_FORCE_DRAWELEMENT as diagnostic-only; make armStaticDedup idempotent (review) Addresses miguel-heygen's blockers on #1919: - HF_FORCE_DRAWELEMENT promoted from a stale "SCRATCH/Uncommitted" comment to a documented diagnostic flag: it exists for upstream-Chromium repro work (gate-vs-API isolation, crbug 521861819 149-vs-151) and R&D on gated effect classes; renders under it may be damaged BY DESIGN since it bypasses gates whose thresholds encode measured damage. Never production; the safety-net blank guard also stands down under it so diagnostic frames arrive unmodified. - armStaticDedup is now idempotent: the drawElement init path arms dedup before canvas injection, then initializeSession called it again — the second run overwrote the armed state with skipReason="capture_mode" (captureMode is "drawelement" by then), producing contradictory telemetry (armed frames + a skip reason), and re-ran the verification seeks on the fallback path. It now no-ops once staticFrames or a skip decision exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(producer): fast-capture render stages + remote bg-image localizer * feat(lint,player): fast-capture lint rule + player media sync --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
afa7f292fb |
feat(lint): warn when <html dir> can blank render output (#1893)
* feat(lint): flag dir="rtl" on <html> as a confirmed silent render failure Two independent reports diagnosed the same exact bug: dir="rtl" (or any non-ltr value) on <html> renders correctly in preview/snapshot but produces a fully blank/black video from render, with no other lint/validate/inspect check catching it - output file size (far smaller than expected) was the only tell for both reporters. Both independently confirmed the same fix: drop dir from <html>, keep lang, and scope direction: rtl to individual text-containing elements via CSS instead. Could not empirically verify the render pipeline's own root cause in this session (headless Chrome screenshot capture is unreliable in this sandboxed environment - even a baseline, non-RTL capture timed out), so this ships the safe, already-confirmed advisory rather than guessing at a runtime fix. Both reporters explicitly asked for exactly this: "deserves a lint rule or render-time warning." * fix(lint): only flag valid non-ltr html dir values |
||
|
|
c7b34d5c65 |
fix(lint): stop scene-exit hard-kill rules from contradicting gsap_animates_clip_element (#1846)
Two independent post-release feedback reports of the same contradiction:
scene_layer_missing_visibility_kill / gsap_exit_missing_hard_kill tell you to
add `tl.set(selector, { visibility: "hidden" }, t)` on an exiting scene
element, but when that element is also class="clip", the exact tl.set they
recommend is then flagged by gsap_animates_clip_element (the framework
already owns visibility/display on clip elements). One report worked around
it by wrapping the scene's content in an inner non-clip div and asked that
the fix hint mention that pattern.
Both rules now detect when the exiting/flagged selector is a clip element
(scene_layer_missing_visibility_kill checks the tag's class list directly;
gsap_exit_missing_hard_kill reuses the clipIds/clipClasses maps already built
in its enclosing rule) and, only in that case, point at the inner-wrapper
pattern instead of a tl.set on the clip element itself. Non-clip targets are
unaffected — same fix hint as before.
|
||
|
|
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. |
||
|
|
d2f1adc2af |
fix(lint): recognize computed-key window.__timelines registrations (#1874)
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. |
||
|
|
b7eb0dfb5a |
fix(lint): mention src: local() for system fonts in font_family_without_font_face (#1859)
Two independent post-release feedback reports of this rule hard-erroring
on OS system fonts (Hiragino Sans, Microsoft YaHei) that have no
downloadable file. Both asked for the same thing, in slightly different
words: a documented way to satisfy the check for a font that's genuinely
OS-bundled, not missing.
That way already exists and already works — extractFontFaceFamilies only
looks at the font-family declaration inside @font-face, never the src
value, so `@font-face { font-family: 'X'; src: local('X'); }` already
passes. One report found this themselves; the other didn't. The gap was
discoverability: the fixHint only described bundling a real font file, so
nobody would think to try `local()` unless they already knew about it.
Considered and rejected a broader fix: adding CJK system-font names to the
shared FONT_ALIAS_MAP (the mechanism that already exempts Latin system
fonts like Segoe UI/Verdana by aliasing them to a bundled fallback font).
That map has no CJK-equivalent bundled font to alias to (only Japanese has
one, noto-sans-jp) — aliasing "Microsoft YaHei" (Simplified Chinese) to a
Japanese font would silently swap in the wrong glyph shapes for shared Han
characters, and would specifically break distributed/Lambda rendering
(where system-font capture is disabled, per system_font_will_alias's own
comment) by removing the warning that currently prompts a real fix. The
local() message fix has none of that risk: it changes no detection logic,
only points at an already-correct existing escape hatch.
Tests: local() font-face no longer flags (proves the advice is accurate,
not just documented); fixHint contains "local(". 308 lint tests pass.
|
||
|
|
3569293c66 |
fix(lint): float slop in overlapping_clips_same_track compare (#1851)
parseFloat('0.1') + parseFloat('0.2') = 0.30000000000000004, so authored
adjacencies whose sum is exact in decimal drift a few ulps and fire the
overlap rule under the strict compare.
Compare on end - start > 1e-6 instead — 11 orders above the worst observed
drift, 4 below one 60fps frame.
|
||
|
|
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. |
||
|
|
24edb15095 |
fix(runtime): auto-infer composition duration for CSS/WAAPI/Lottie so data-duration is optional (#1830)
* fix(runtime): auto-infer composition duration for CSS/WAAPI/Lottie so data-duration is optional The #2 render failure bucket ("Composition has zero duration") accounts for ~27K errors / ~7K affected users over 30 days (PostHog project 356858). Root cause: only GSAP timelines got their duration auto-detected — CSS, WAAPI, and Lottie compositions had no source of truth for total duration unless the author remembered to set data-duration on the root element, and the render engine hard-failed capture when neither was present. Adds getInferredDurationSeconds() to the CSS, WAAPI, and Lottie runtime adapters (packages/core/src/runtime/adapters/*.ts) — each reports the longest finite end time it can discover from its own animations (CSS: computed timing offset by data-start; WAAPI: effect.getComputedTiming().endTime; Lottie: totalFrames/frameRate or the player's own duration). Infinite/ unbounded animations correctly return null and still require data-duration. Wires this into the runtime's existing duration-floor resolution (resolveAdapterDurationFloorSeconds in runtime/init.ts), alongside the existing media-duration and authored-composition floors, so window.__hf.duration becomes positive without any author action for finite-duration non-GSAP compositions. Three.js is unchanged — no AnimationClip/AnimationMixer inspection exists in that adapter, so data-duration remains required there. Tightens frameCapture.ts's zero-duration fast-fail gate to also check hf.duration directly (not just the two authored signals), so a composition mid-inference isn't fast-failed before its adapter-derived duration lands. Adds a new lint rule (root_composition_missing_duration_source) that errors only on genuinely non-inferable cases: no animation signal at all, Three.js without data-duration, or an infinite/unbounded CSS or WAAPI animation without data-duration. Deliberately silent on finite CSS/WAAPI/Lottie animations, since the runtime now infers those — an autofix that "inserts the inferred value" was considered and rejected: every case the rule flags has no derivable value (an infinite spinner has no finite end time; a duration-less Three.js scene has nothing to measure), so any autofix would have to fabricate a placeholder, trading a loud correct failure for a silent wrong-length render. Updates the CSS/WAAPI/Lottie/Three adapter skill docs and the hyperframes-core determinism-rules/data-attributes references to document the new optionality and the runtime mechanism backing it. Verified end-to-end against the real render pipeline (not just unit tests): a CSS-only composition with a finite 3s animation, no GSAP timeline, and no data-duration now renders a correct 3.000s MP4 via `hyperframes render` (previously: "Composition has zero duration" failure). The infinite-CSS negative control still fails fast with a clear diagnostic, matching the new lint rule. Adds a file-level fallow health exemption for lottie.ts's pre-existing `seek` handler — unrelated to this change, but its line numbers shifted when new functions were added earlier in the file, tripping fallow's inherited-finding fingerprint (documented pattern already used elsewhere in .fallowrc.jsonc for the same reason). Known limitation: the static WAAPI usage detector in the lint rule (/\.animate\(\s*[\[$A-Za-z_]/) can miss unusual call shapes; it only affects whether the "no signal at all" branch fires, and errs toward NOT flagging (reducing false positives) rather than over-flagging. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(lint): close 3 correctness gaps in root_composition_missing_duration_source - Strip JS/CSS comments before scanning for GSAP/WAAPI/Three/Lottie/CSS animation signals, so a commented-out `.animate()` call or a commented `animation: ... infinite` rule can no longer satisfy the "has a duration source" check and mask a real zero-duration render failure. - Broaden the WAAPI detection regex to also match the object-literal (PropertyIndexedKeyframes) form of `.animate()`, e.g. `el.animate({ opacity: [0,1] }, { duration: 2000 })`, which the previous character class silently missed. Corrected the adjacent comment that incorrectly claimed this shape "can't be a false negative". - Fix hasInfiniteCssAnimation to stop false-positiving on animation NAMEs that merely contain the substring "infinite" (e.g. `infinite-spin`) by anchoring the `infinite` keyword with hyphen-aware boundaries instead of a bare `\b`. Also makes the longhand `animation-name` + separately declared `animation-iteration-count: infinite` pattern detected consistently. Adds targeted unit tests for each fixed false-positive/false-negative. * fix(runtime): keep finite duration signal when an unbounded animation coexists getInferredDurationSeconds in the CSS and WAAPI adapters returned null outright whenever any animation on the composition was unbounded (infinite iteration count), even when other finite animations on the same composition could still supply a valid duration. This disagreed with the new root_composition_missing_duration_source lint rule, which treats any animation-name as sufficient — so a composition mixing a finite fadeIn with a decorative infinite spin passed lint but still failed at render with "zero duration". Unbounded animations are now skipped when computing the max end time instead of short-circuiting the whole calculation. null is only returned when every animation on the composition is unbounded, i.e. there is no finite signal to fall back on at all. Co-Authored-By: Claude <noreply@anthropic.com> * docs(skills): fix table separator width in data-attributes.md oxfmt flagged the merged Composition Root table from the post-rebase merge of the auto-infer-duration docs onto main's reformatted table — the separator row was one dash short of the header width. * fix(lint): keep infinite-CSS duration rule strict but make its message honest Post-review (Vance): after the finite+infinite adapter fix, the runtime infers a length for a mixed finite+infinite CSS composition, but this lint rule still (intentionally) errors on it — an unbounded animation makes the intended total length ambiguous, so we require explicit data-duration. Keep that strictness (lint is advisory by default; it only blocks under --strict, and data-duration is the one duration signal guaranteed correct across every adapter, known and future). But the message wrongly claimed the render "will fail" — false for the mixed case, where the runtime falls back to the finite animation. Rewrite it to describe the ambiguity honestly, correct the rule's block comment, and add a mixed finite+infinite test asserting it still errors with an honest message. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
cf573f7f3f |
fix(core,producer,cli): pre-flight validation for empty/malformed sub-compositions (#1831)
* fix(core,producer,cli): pre-flight validation for empty/malformed sub-compositions The #1 render failure bucket in production telemetry (PostHog project 356858, dashboard 1783183 "HyperFrames — Bottom-Line & Activation"; ~65-69K occurrences / ~27-28K affected users over 30 days, ~80% via AI-agent authoring flows) is a `data-composition-src` reference pointing at a scene file that is empty, malformed, or missing. Root cause, traced end-to-end: - The literal error "Composition HTML is empty or could not be parsed: <path>" is real (not a PostHog paraphrase) — thrown by a since-reverted guard in packages/core/src/compiler/inlineSubCompositions.ts (#1364), then changed to a silent skip in #1678 to avoid aborting renders on partial content during authoring. #1629 added per-assembler guards for 3 skill workflows (product-launch-video, faceless-explainer, pr-to-video), but general-video and hand-authored flows — where the dominant filename `scene-title.html` (40K+/68K of the bucket) originates — have no assembler and thus no guard. #1678 assumed the assembler guards from #1629 covered this pre-render; they only covered 3 of the many authoring flows. - On current `main`, an empty/malformed data-composition-src file no longer crashes or throws during render — it's silently dropped by the tolerant inliner. Reproduced locally: `hyperframes render` on a project with an empty scene-title.html "succeeds" after ~93s (two 45s pollSubCompositionTimelines timeouts) with the scene silently missing from the output video. `hyperframes validate` also reports "No console errors" for the same broken project. - The raw `Cannot destructure property 'firstElementChild' of 'documentElement' as it is null` crash reproduces directly against linkedom (the DOMParser polyfill packages/cli/src/utils/dom.ts installs in the real CLI runtime) for empty and non-HTML input — confirmed with a standalone repro script, not just inferred. jsdom/happy-dom (used in this repo's own test environment) are spec-compliant and never produce a null documentElement, which is why this needed a linkedom-specific test file. Fix: - New shared helper `checkSubCompositionUsability` (packages/core/src/compiler/subCompositionValidity.ts) is the single source of truth for "is this data-composition-src file usable" — mirrors the inliner's own parse/template/body logic so all callers agree. - `inlineSubCompositions.ts` (preview/studio bundling) now uses the shared helper internally but keeps its #1678 tolerant skip-and-continue behavior unchanged — mid-authoring iteration on a partial project must keep working. `onMissingComposition` now also receives a human-readable reason. - New render-only pre-flight (`assertSubCompositionsUsable` in packages/producer/src/services/htmlCompiler.ts) walks every data-composition-src reference (including nested ones, root-relative, matching parseSubCompositions' own resolution) before any compilation work starts, and throws naming every offending file at once. This is unconditional — not gated behind --strict — because a render that silently drops a scene is strictly worse than one that refuses to start. Confirmed locally: render now fails in ~0.4s with an actionable message instead of "succeeding" after 93s with a missing scene. - New `hyperframes lint` rule `missing_or_empty_sub_composition` (packages/cli/src/utils/lintProject.ts) surfaces the same check as a file-scoped, actionable lint error (already unconditional — lint exits 1 on any error). - `hyperframes validate` now also runs this check before launching a browser, so it no longer reports "No console errors" for a project with a broken sub-composition. - `packages/core/src/parsers/htmlParser.ts`: guarded every `documentElement`-may-be-null access (parseHtml, updateElementInHtml, addElementToHtml, removeElementFromHtml, extractCompositionMetadata, validateCompositionHtml) with a new typed `CompositionHtmlParseError` (or, for validateCompositionHtml's collect-and-report contract, a typed validation failure) instead of a raw crash. Tests: empty file, whitespace-only, malformed/non-HTML, missing file, nested sub-compositions (both happy path and broken-grandchild), and the happy path — at the shared-helper, lint, and render pre-flight layers. Not changed: the AI-agent authoring skills (skills/*). general-video and hand-authored flows have no assemble-index.mjs equivalent to guard, so the fix is at the CLI/render layer instead — flow-agnostic, covers every authoring path, and the skills' existing "run lint/validate and stop on failure" guidance now actually catches this class of mistake once run. Not run in this environment: the producer package's full regression-harness test suite (`bun test` in packages/producer) — it performs heavy real rendering (S3 asset downloads, Google Fonts fetches, full video encodes) and did not complete in a reasonable time in this sandbox. Verified instead via the targeted test file for all touched code (76/76 passing), whole-repo typecheck/build/oxlint, `fallow audit` (complexity/duplication/dead-code gate, clean), and manual end-to-end CLI runs (render/lint/validate) against reproduction projects, including a nested sub-composition scenario. CI should run the full producer suite before merge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refactor(parsers,lint): port empty-composition pre-flight to extracted packages Rebased onto main, which extracted @hyperframes/lint from core (lint depends only on parsers, not core). Relocate checkSubCompositionUsability from core to @hyperframes/parsers so both core (inliner) and lint can consume it without a core<->lint cycle; core keeps a @deprecated re-export shim. Correctness fixes from code review: - checkSubCompositionUsability now returns "no-composition-root" when the <template>/<body> content has no [data-composition-id] element (previously a marker-free placeholder body passed both guards). - lint's missing/empty sub-composition rule now only checks files reachable via data-composition-src from the root (matching render pre-flight), instead of a raw filesystem walk that false-positived on orphaned files. - drop `as string` cast in inlineSubCompositions in favor of an explicit null guard (per CLAUDE.md). Review-comment items: - move EmptyCompositionError JSDoc above the class (was above the adapter fn). - correct stale circular-ref comment to match actual silent-skip behavior. - rewrite self-contradicting lint message ("silently drop") to describe the new loud render-pre-flight abort. - add the __PLACEHOLDER__ (/^__[A-Z_]+__$/) skip to the render pre-flight so it agrees with lint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
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 |
||
|
|
27a7f37494 |
fix(lint): recognize Three.js loaded via ESM URL/path imports (#1805)
The missing_three_script rule only treated a bare import from 'three' as loading Three.js, so ESM imports whose specifier is a URL or path (e.g. .../+esm CDN builds, esm.sh/three, unpkg three.module.js, or a local three.module.js) were not recognized. Compositions using THREE. with such an import got a false blocking error, pushing authors onto the deprecated UMD global build. Generalize the module-import detection to count any import/from whose specifier contains "three" (case-insensitive), matching the existing loose /three/i treatment of <script src>. Bare 'three', importmap, and <script src> paths are unchanged; the specifier must still contain "three", so unrelated imports do not satisfy it. |
||
|
|
11432ea8ce |
fix(lint): stop font_family_without_font_face flagging system-ui stacks and var() (#1796)
The font_family_without_font_face rule raised a blocking error on two legitimate, very common cases: 1. The `-apple-system, BlinkMacSystemFont` system-ui stack. These two tokens are the cross-browser incantation for the platform UI font (synonyms of `system-ui`); they name no font file, so demanding an @font-face for them is wrong. They appear in almost every CSS reset. 2. `font-family: var(--x)` indirection. The shared family extractor took the literal `var(--heading)` as a font name. The linter cannot statically resolve a custom property, so it must not flag it. Fix at the root: add the two system-ui synonyms to GENERIC_FAMILIES, and skip any parenthesised (function) token in the shared family extractor so both this rule and system_font_will_alias stop misreading var(). A real undeclared font sitting in the same stack is still flagged. |
||
|
|
036e3660cd |
fix(lint): stop overlapping_gsap_tweens flagging distinct unresolved targets (#1798)
* fix(lint): stop overlapping_gsap_tweens flagging distinct unresolved targets
The GSAP parser assigns the sentinel `__unresolved__` to any tween whose
target it cannot statically resolve to a concrete element (a computed
variable, a helper call, etc.). The overlap check compared tweens by that
target string, so two tweens aimed at completely different elements via
unresolvable selectors (e.g. `#s0 .hl .w` and `#s1 .hl .w` produced by a
helper) both collapsed to `__unresolved__` and were reported as
overlapping, a false positive.
An unresolved target is an unknown element: two of them are not provably
the same element, so an overlap between them cannot be asserted. Skip
overlap analysis when the target is the sentinel. Genuine overlaps on a
resolved element are still flagged.
* fix(lint): guard gsap_exit_missing_hard_kill against the unresolved-target sentinel
The overlap rule already skips tweens whose target collapses to the
__unresolved__ sentinel, but the sibling exit rule in the same file did not.
A scene-boundary exit on an unresolved target could emit a finding like
GSAP exit on "__unresolved__" ... with a meaningless tl.set("__unresolved__", ...)
fix hint. An unresolved target is an unknown element: you cannot assert a
missing hard kill on it, so skip the window early in the loop, mirroring the
overlap rule. Exits on resolved selectors are still flagged.
|
||
|
|
a4303137cb |
fix: storyboard-angle review follow-ups (M1 bg-on-clip, B3 slideshow, parser guard, CLI fixes) (#1791)
* fix(skills): storyboard review — bg-on-clip rule, slideshow output, parser parity guard Addresses the storyboard-angle review (jrusso1020): - M1 (invisible text): frame-worker.md (x3) + SKILL.md Step 5 (x3) now require a frame's full-bleed background on a class=clip layer, never the #root / data-composition-id element (the root is clip-gated to its scene window, so a background on it is not a dependable ground and dark text can land on the black host body). The assembler already paints frame.md's canvas onto index #root as the base ground; the per-frame clip rides on top. - B3 (slideshow truncates to slide 1): slideshow/SKILL.md gains an Output section (decks render via 'present'; 'render index.html' captures only the first composition; linear main-line MP4 export is deferred). - Parser drift: vendoredParity.test.ts guards the three vendored storyboard.mjs copies (byte-identical + parse-parity with @hyperframes/core). - skills-manifest.json regenerated for the edited SKILL.md files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): storyboard review — lint, validate help, snapshot, inspect, capture, render Addresses the CLI findings from the storyboard-angle review (jrusso1020): - lint (@hyperframes/lint): accept vendor-prefixed system-font keywords -apple-system / BlinkMacSystemFont so a system stack with a generic fallback no longer trips font_family_without_font_face (+ test). - help: list 'validate' under Project in 'hyperframes --help' (was runnable but undocumented). - snapshot: honor -o/--output (the flag did not exist; output was hardcoded to snapshots/). The dir is resolved once and threaded through capture + contact sheet + Gemini. - snapshot: split font status into loaded / error / unused with a one-line summary; only a real 'error' is reported as FAILED (an unrequested @font-face is 'unused', not a contradiction with 'loaded'). - inspect: suppress text_occluded across a scene-to-scene crossfade (occluder in a different data-composition-id mount while a scene is mid-fade); a same-scene or two-settled-scenes overlap still flags. - inspect: suppress content_overlap between in-flow siblings governed by the same flex/grid container (tight stacks / number lockups are layout slop). - capture: record source resolution (videoWidth/Height) in video-manifest.json alongside the DOM display box; consumers size off the source dims. - render: warn when the target carries a slideshow island (render captures only the first scene, so the MP4 is truncated to slide 1; use 'present'). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1faeba4aa5 |
feat(lint): error on crossorigin on media (breaks preview) (#1793)
`crossorigin` on <video>/<audio> forces a CORS-checked fetch. The server-side renderer downloads media directly (no CORS) so renders always work, but Studio preview runs in the browser — a media host that omits Access-Control-Allow-Origin silently fails the load, so the media shows blank/black in preview while the render looks fine, hiding the bug. Plain displayed media never needs crossorigin; it's only required to read pixels/samples back (canvas/WebGL texture, WebAudio createMediaElementSource) and only when the host is known CORS-enabled. New rule media_crossorigin_breaks_preview flags it as an error with that guidance. Claude-Session: https://claude.ai/code/session_01NsmfF5FzhqXY6hZ8buXgUE Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
74f9c31b3f |
fix(producer,lint): id-less media renders blank wash instead of footage (#1790)
* 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. |
||
|
|
1c0eef835f |
refactor(skills,lint): migrate kinetic-letter-in off letterSpacing; document rule design
The kinetic-letter-in motion-primitive (a music-to-video reference authors copy) animated the word's letterSpacing as a settle — a reflow tween that micro-stutters under seek-by-frame capture, and outside the registry scan so the rule never fired on it. The chars are already per-glyph spans, so migrate the settle to a per-glyph x spread ((0.04em − −0.04em) × 280px = 22.4px/gap, centered about index 3), with a comment naming the hazard and the rule. Render-verified: faithful, smooth. Document two intentional design choices in gsap_non_transform_motion so future readers don't read them as misses: - No per-line/per-file suppression by design — the stance is fix-the-motion, not silence-the-rule; every plain-DOM case has a faithful transform equivalent. - set() is skipped intentionally: a set() that seats an integer-snapped layout position before a later transform tween is a single from-state frame, not motion. Also hoist loadParseGsapScript() above the per-script loop (the other async rules do the same; dynamic-import cache makes it equivalent, but the placement no longer reads as load-bearing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
189d8582f4 |
fix(lint): catch fromTo from-object layout/reflow props in gsap_non_transform_motion
The rule sourced a tween's animated props from anim.properties only — the acorn
parser's to-vars. A fromTo() exposes its first ("from") vars object separately as
anim.fromProperties, so a layout or reflow prop animated only in the from-object
(e.g. tl.fromTo("#t", { left: 100, letterSpacing: "0.3em" }, { opacity: 1 }))
escaped the rule entirely and shipped stuttering. fromTo is the most common tween
form, so this was a real recall hole.
Union fromProperties into the checked property set; add the field to the lint
parse type. Registry re-scan unchanged (0 comps animate a layout prop only in a
from-object today), so no collateral.
Known remaining gaps (documented, not fixed): standalone gsap.fromTo only scans
its first vars object via regex (and aborts on nested braces); roundProps:true
(boolean form) is dropped by the parser. Both are rare and the clean fix is
disproportionate to the rule's planned sunset when drawElement render lands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
6e21072f44 |
fix(lint,skills): make the reflow-prop fix faithful, not a lossy scale swap
The previous fixHint and skill guidance told agents to "settle via scale" for text-reflow props — but uniform scale resizes the glyphs, it does not change the gaps between them, so swapping a letterSpacing tween for scale lints clean while silently animating a different thing. Make the guidance faithful per property: - fontSize -> scale (same visual, sub-pixel smooth). - letterSpacing / wordSpacing -> split the text into per-character elements and animate each glyph's x (the spread); uniform scale is NOT equivalent. Or hold the value statically if it's a minor flourish. Add a "preserve the intent" principle to gsap-transforms-and-perf.md: a fix must reproduce the same start/end state and trajectory and be verified against the ORIGINAL render, not just pass lint — lint-clean-and-smooth is not the bar, faithful-and-smooth is. Redo the vignette demo title with the faithful per-glyph x spread (15.12px = (0.32em-0.18em)*108px, centered about 8 chars). Render-verified: settled endpoint matches the original at 46.5 dB, settle is smooth (24/24 unique frames). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
082280e8de |
feat(lint): flag text-reflow props in gsap_non_transform_motion
Extend the rule beyond positional layout props to the text-reflow props letterSpacing / wordSpacing / fontSize. Animating them reflows text and snaps glyph positions to the pixel grid, so a slow ease-out tail micro-stutters exactly like left/top — measured on a real composition, a slow letterSpacing "settle" rendered 19/30 unique frames vs 30/30 for the transform-driven motion in the same piece. They have no transform replacement (fix: settle via scale or hold the value), and the snap happens during browser layout, upstream of the canvas raster, so they are never html-in-canvas-exempt. width/height stay excluded (legit animated uses — progress bars, reveals). Restructure the finding's message/fixHint to compose per category (positional -> x/y; reflow -> scale/hold; roundProps -> remove) instead of a two-branch ternary. Skill guidance (gsap-transforms-and-perf.md) broadened: "layout property" includes reflow; letterSpacing/fontSize named as the settle-trap. Migrate the one surfaced positive: registry/components/vignette/demo.html title settle letterSpacing -> a subtle scale settle (render-verified 20/20 unique frames, smooth). Blast radius across the registry was this one comp, zero false positives. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
619a603eab |
feat(lint): add gsap_non_transform_motion rule, migrate registry comps to transforms
Animating layout properties (left/right/top/bottom/margin*) or using
roundProps snaps motion to integer device pixels. Under the seek-by-frame
capture engine this is invisible at high per-frame deltas (fast tweens) but
visibly stutters at low deltas (slow tweens / ease-out tails): sub-pixel
movement rounds to the same pixel for several frames, then jumps a whole
pixel. Transforms (x/y/scale) interpolate sub-pixel and stay smooth.
New rule gsap_non_transform_motion (error):
- Sources timeline tweens from the acorn parser's full animation list, so
label- and relative-positioned tweens (tl.to(..., "label") / "+=1") are
covered and real AST keys avoid phantom-prop / nested-brace misreads.
- Exempts html-in-canvas elements (a <canvas layoutsubtree> ancestor): they
are rasterized from sub-pixel getComputedStyle and do not integer-snap.
Per-element resolution, so a grouped glass+text tween still flags the
plain-DOM half. roundProps is never exempt (it rounds before the style).
- Uses Object.hasOwn for the layout-prop lookup, skips set(), and merges
standalone gsap.* calls.
Migrated 9 registry compositions off layout-prop animation onto transforms,
each render-verified visually identical:
- liquid-glass-{notification,widgets,media-controls,context-menu}: plain-DOM
text overlays -> transforms; glass panels stay on left/top (canvas-exempt).
- vignelli: curtain wipes left:% -> xPercent.
- lt-mask-reveal: sweep left:% -> x px.
- flowchart, flowchart-vertical, decision-tree: cursor paths left/top -> x/y.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
6aaab32ccb |
refactor: make @hyperframes/lint depend only on parsers (#1773)
* refactor: make @hyperframes/lint depend only on parsers, not core Relocates the leaf utilities lint pulled from core — URL/asset-path helpers, font aliases, and the slideshow manifest parser — into the standalone @hyperframes/parsers base, and drops @hyperframes/core from lint's dependencies. Core keeps back-compat re-export stubs at the old paths, so producer/studio/cli are unchanged. Why: lint was the lightweight validator from #1749, but depending on core transitively pulled studio-server (hono) and bpm-detective — irrelevant to linting. Now installing @hyperframes/lint pulls only parsers + postcss, and the core<->lint dependency cycle is gone. - parsers main entry stays browser-safe (pure utils only); the node:path asset helpers live behind the new @hyperframes/parsers/asset-paths subpath - slideshow parser exposed via @hyperframes/parsers/slideshow * feat(lint): add browser entry; harden CSS url() regex (ReDoS) @hyperframes/lint/browser — a fully client-side rule engine (lintHyperframeHtml, lintMediaUrls, shouldBlockRender) with zero node: builtins, so browser-only editors can validate compositions with no Node.js and no server round-trip. Closes the browser-validation ask on #1749. - shouldBlockRender extracted from the fs-bound project.ts into its own pure module so the browser entry stays node-free - pure composition primitives (data types, font aliases, URL helper) exposed via a new recast-free @hyperframes/parsers/composition subpath, so the browser bundle tree-shakes out the GSAP/recast machinery (verified: esbuild platform=browser bundles with 0 node builtins) - lint built with a platform:browser tsup pass — compile-time guarantee the browser entry never pulls a node builtin - harden CSS_URL_RE against polynomial ReDoS (CodeQL js/polynomial-redos); behavior-preserving, verified against existing tests + an old/new parity check - parsers/lint marked sideEffects:false |
||
|
|
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. |