Run `fallow fix --auto-fixable` to remove `export` keywords from symbols
fallow's reachability analysis identifies as unused. Keeps only the cases
where the symbol is still referenced internally in its own file (so
removing `export` doesn't surface a new oxlint `no-unused-vars` error).
Result: fallow dead-code findings drop from 276 → 208 (68 fewer unused
exports), with no behavior change — each symbol is still defined and used
exactly the same way within its file.
Reverted ~20 files where fallow's auto-fix would have created cascading
"declared but never used" lint errors — those are cases where the symbol
isn't used at all, and properly cleaning them up means deleting the
declaration, not just dropping `export`. Better to land that as a
separate, narrower PR rather than mixing it into a mechanical de-export.
Also reverted four false positives where fallow missed real consumers:
- `captureCost.ts` (renderOrchestrator has two separate import blocks
from the same module; fallow only saw the first)
- `propertyPanelHelpers.ts`, `domEditingLayers.ts` (real internal uses
fallow's reachability missed)
- `render.ts` (functions imported via `await import()` dynamic import,
which fallow's static analysis doesn't follow)
Test plan: bun run --filter '*' typecheck (clean), oxlint + oxfmt clean,
cli/core/studio/engine vitest suites pass (335 + 917 + 576 + 605 tests).
## Summary
- Regenerate golden baselines for 7 regression tests that were deterministically failing due to stale baselines from before the recent renderer fixes
- Root cause: renderer changes (revert flattenInnerRoot, late-bind polling removal, conditional rebind, sub-comp timeline readiness polling) changed visual output, but baselines were never regenerated
- CI's `Detect changes` job was skipping shards on non-engine commits, masking the failures — making it look like flaky tests when in reality they failed every time shards ran
## Tests regenerated (all in Docker with pinned Chrome 148.0.7778.167)
| Test | Previously failed frames | Baseline age |
|---|---|---|
| `gsap-letters-render-compat` | 86/100 | old |
| `typegpu-adapter` | 76/100 | May 13 |
| `style-7-prod` | 60/100 | PR #368 (very old) |
| `style-15-prod` | 59/100 | May 17 (pre-renderer-fixes) |
| `style-18-prod` | 19/100 | old |
| `style-3-prod` | 7/100 | May 17 (pre-renderer-fixes) |
| `style-8-prod` | 1/100 | borderline |
## Test plan
- [x] All 7 tests pass locally in Docker with 0 failed frames
- [x] Verified deterministic — ran style-7-prod twice, same result both times
- [x] All 8 CI regression shards green
- [x] Other tests (style-1, 2, 4, 5, 6, 9, 10, 11, 12, 13, 16, 17, overlay-montage, sub-composition-video, vignelli-stacking, etc.) still pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Missed in the initial batch — 86/100 frames were failing. Regenerated
in Docker with the same pinned Chrome build. Now passes with 0 failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- [blocker] When playerAdapter is null (GSAP-only runtimes with no
win.__player), the fallback path now uses the best available timeline
adapter instead of returning null. Track timelineAdapter across the
__timeline and __timelines paths, then use it as the base for
createStaticSeekPlaybackAdapter.
- [nit] Remove dead baseAdapter alias — use bestAdapter directly.
- [tests] Add 4 tests: readTimelineDurationFromDocument with
data-hf-authored-duration fallback, createStaticSeekPlaybackAdapter
with seek-only adapter (no renderSeek), and pause lifecycle.
getAdapter() returned the runtime player or GSAP timeline adapter directly
when its duration was > 0, even when the document's timeline (computed from
sub-composition data-start + data-hf-authored-duration attributes) extended
beyond that duration. This capped the seek slider, seek clamping, and
sub-composition visibility at the adapter's shorter value.
Now each adapter path checks whether the document duration exceeds the
adapter's own duration. When it does, the adapter falls through to
createStaticSeekPlaybackAdapter which wraps the runtime player with the
correct effective duration, allowing seeking and preview across the full
timeline range.
When the runtime player's clock duration is smaller than the effective
timeline (computed from data-start + data-hf-authored-duration on
sub-composition elements), the seek is clamped too early and sub-compositions
beyond the clock duration are invisible.
Detect this mismatch in getAdapter() and pad the root GSAP timeline to the
document duration, then force a timeline rebind so the clock updates.
The seek function clamped to adapter.getDuration() which only knows the
root composition's authored duration. Appended timeline elements extend
beyond this range. Compute the effective max from both the adapter duration
and the store's element boundaries so scrubbing reaches the full timeline.
- Move Timing + Media sections above Layout in the Design panel
- Remove LayerTree from Design panel (redundant with Layers tab)
- Replace Rate and Media Start with sliders matching Volume's UX
- Replace Position DetailField with SelectField to match Fit height
- Remove Poster field (not useful for HyperFrames compositions)
- Show absolute filesystem path for Source (resolves symlinks)
- Add Copy button for source path with checkmark feedback
- Preserve element selection on undo/redo instead of clearing it
Move the 7-step production pipeline (Capture → Design → Script → Storyboard
→ VO + Timing → Build → Validate) into its own dedicated guide so it serves
any Hyperframes project, not just website-to-video. Expand each step with
file contents, project layout, gates, and iteration patterns. Reference the
new page from website-to-video, quickstart, prompting, and launch-videos.
Address PR review feedback on #939:
- Pin chunkSize=240 on the golden planDir layout test so the 1-chunk path
through plan() stays exercised after the auto-sizer change. Assert
chunkCount === 1 explicitly (previously just >= 1).
- Add an integration test that runs plan() with chunkSize=undefined and
asserts the auto-sizer produces multi-chunk output end-to-end
(chunkCount=3, encoder.gopSize=10, encoder.chunkSize=10) for the same
30-frame fixture.
- Document the GOP/file-size trade-off on the chunkSize docstring so
adopters who optimize for output bytes know to pin chunkSize.
- Update the resolveChunkPlan docstring formula to reference the operative
variable (resolvedChunkSize) instead of the now-ambiguous chunkSize.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address self-review findings:
- assertPositiveInteger now only runs on the caller-supplied path so the
error message names `configChunkSize` only when the caller actually
passed one. Previously, the assertion fired against `resolvedChunkSize`
on both paths and would have lied about the offending input.
- Drop the call-site comment that narrated the diff/history; the
function docstring already covers the contract.
- Drop the internal-track name and date from the MIN_CHUNK_SIZE rationale
and the test block header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the useEffect that pushed effectiveTimelineDuration into the player
store with an inline derived selector in PlayerControls. The selector computes
Math.max(duration, maxElementEnd) directly from store state, avoiding the
effect-based sync anti-pattern entirely.
Adds a new Media section to the Design panel that appears when a <video>
or <audio> element is selected. Controls include volume (slider),
playback rate, media start offset, loop/muted toggles, and for video:
object-fit, object-position, poster, and has-audio-track toggle.
Extends the source patcher with an "html-attribute" operation type for
native HTML attributes (loop, muted, poster) that don't use the data-
prefix. Adds coalesceKey to attribute commits so rapid slider/scrub
edits merge into a single undo entry.
The seek slider read duration from the player store, which was set from the
iframe adapter's getDuration() — only aware of the root composition's authored
data-duration. Appended sub-compositions (via Blocks panel) extend the timeline
but the slider stayed capped at the original duration.
Sync effectiveTimelineDuration (which accounts for all timeline elements) into
the player store, and prevent adapter callbacks from overwriting a larger
effective duration back down to the authored value.
Previously, plan() defaulted chunkSize to 240 on a `?? DEFAULT_CHUNK_SIZE`
line, so a 660-frame composition with maxParallelChunks=16 ended up at 3
chunks (ceil(660/240)) regardless of the caller's fan-out intent.
When config.chunkSize is undefined, auto-size from maxParallelChunks:
effectiveChunkSize = max(MIN_CHUNK_SIZE, ceil(totalFrames / maxParallelChunks))
MIN_CHUNK_SIZE=10 keeps per-chunk fixed overhead from swamping the
parallelism gain on tiny renders. Explicit numbers, including 240, take
precedence over the auto-sizer — no behavior change for callers that set
chunkSize explicitly.
Surfaced by the lever-1 chunk-scaling benchmark on 2026-05-17.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Configure fallow via .fallowrc.jsonc so its analysis reflects this repo's
real entry surface, then fix the genuine issues it found.
Fallow noise reduction (601 → 276 dead-code findings):
- Ignore docs/, test fixtures, skill test-corpora, registry/, examples/
- Declare worker entry points loaded dynamically by file path
(pngDecodeBlitWorker.ts, shaderTransitionWorker.ts)
- Declare runtime IIFE entry (core/src/runtime/entry.ts) built outside the
import graph by build-hyperframes-runtime-artifact.ts
- Declare bun:test files in producer + aws-lambda as test entries
- Ignore dynamically-resolved deps: tsup external (puppeteer-core, esbuild,
giget), peer/static-file (gsap in player perf tests), workspace deps
hoisted by bun (happy-dom, @hyperframes/*), and @fontsource/* packages
read via readFileSync in generate-font-data.ts
Extract inline build:fonts scripts:
- packages/{cli,producer}/package.json had multi-line `node -e ...` blobs
containing braces that fallow mis-parsed as glob alternate groups. Moved
to dedicated build-fonts.mjs scripts.
Fix duplicate exports:
- Remove dead FileIcon alias in studio/SystemIcons.tsx (FileTreeIcons.tsx
has the real, used one)
- Consolidate ValidationResult: drop the identical duplicate in
gsapParser.ts; both parsers now import from core.types
- Suppress intentional namespace patterns (per-namespace ML manager
exports; CLI per-command 'examples' convention; fileServer.ts test-only
isPathInside which has different symlink semantics from utils/paths.ts)
Break circular dep (studio/components/editor):
- manualEditsDom.ts re-exported clearStudioPathOffset / clearStudioRotation
/ clearStudioBoxSize from manualEditsSnapshot.ts, which imports four
helpers from manualEditsDom.ts — back-edge cycle
- Re-export moved to manualEdits.ts (the package-public barrel) where the
rest of the snapshot re-exports already live; underlying files now form
a clean DAG
Remove genuinely unused deps:
- studio: motion (no imports anywhere), codemirror (umbrella package; the
@codemirror/* sub-packages are used directly)
- cli: mime-types (plus its only consumer src/utils/mime.ts, which was a
hardcoded mime table that didn't use the package), and its now-stale
tsup external entry
Verified: typecheck across core/cli/producer/studio is clean, oxlint
+ oxfmt pass, manualEdits.test.ts (18 tests) and core parser tests (69
tests) still pass.
Deferred follow-ups (real findings, separate PRs):
- 8 circular deps in producer/services/render/stages/ — renderOrchestrator
↔ captureHdr* / captureStage / extractVideosStage form a hub cycle
- ~14 unused files in producer/src/services/ that look like dead
re-export shims to @hyperframes/engine, but aren't in the public
exports map — need to confirm no deep-import consumers before deletion
- waveform.ts complexity hotspot
- Split pan clamping: clampPreviewPan (drag/wheel-pan) stays narrow
(Math.max(0,...) — content pins to center when smaller than viewport).
New clampPreviewPanForZoom (Math.abs) gives the wide range only to
cursor-anchored zoom, preventing middle-mouse drag from pushing content
off-screen at low zoom levels.
- Pin transform-origin invariant: comment on the stage div noting that
resolvePreviewWheelZoom cursor math depends on center-center pivot.
New test verifies a non-center cursor keeps the same content-space
point fixed across a zoom step.
- Remove dead Math.abs(oldScale) > 1e-6 guard — oldScale >= 0.25 always
(clampPreviewZoomPercent floors at MIN_PREVIEW_ZOOM_PERCENT = 25).
- Skip setSettledZoom re-render when the value didn't change — uses a
functional updater that returns the previous state object when all
three fields match, avoiding a React re-render cascade through Player.
- Zoom anchors to cursor position instead of always zooming toward center.
The resolvePreviewWheelZoom function now accepts cursorX/cursorY (offset
from viewport center) and uses the standard zoom-to-point formula to
adjust pan so the content point under the cursor stays fixed.
- Add visible "Reset" button (bottom-right) showing current zoom % when
not at fit zoom. Driven by settledZoom state that updates after the
200ms settle debounce, so no re-renders during active zoom gestures.
- Fix border-expands-inward bug: scaleIframeToFit in the player now uses
offsetWidth/offsetHeight instead of getBoundingClientRect. The latter
returns values inflated by ancestor CSS zoom, causing double-scaling
that made the iframe appear smaller than its container.
- Fix zoom HUD appearing during pan: split applyZoom (shows HUD) from
applyPan (silent) so trackpad/middle-mouse panning no longer flashes
the zoom percentage overlay.
- Fix stale closure performance regression: replace stageSize in effect
dependency arrays with stageSizeRef pattern. The old deps caused wheel
and pointer handlers to re-register on every viewport resize.
- Widen pan clamp range (Math.abs instead of Math.max(0,...)) so content
can float within the viewport when zoomed below fit — required for
zoom-to-cursor to work correctly at any zoom level.
Closes#900
Rename component to caption-texture and bundle 6 popular textures
(lava, marble, metal, wood, concrete, rock). The texture is configurable
via the texture composition variable (default: lava).
Usage: hyperframes render --variables '{"texture":"marble-012"}'
Tested: lava, marble-012, metal-046-b all render correctly with
distinct visual patterns.
The missing_three_script rule only checked <script src> attributes
for Three.js. Now also detects:
- importmap entries defining "three"
- ES module import/from statements referencing "three"
Closes#931
Three changes to fix regression failures without breaking baselines:
1. Revert flattenInnerRoot in producer — use the original innerHTML
inlining that preserves the existing DOM structure. Instead, set
data-hf-authored-id on the HOST element so the scoped proxy can
still rewrite #id selectors for sub-composition scripts.
2. Revert compiled.html baselines to main (no DOM structure changes).
3. Use timeline-count comparison instead of poll duration to decide
whether to call __hfForceTimelineRebind. Compare timeline count
before vs after the poll — rebind only when new timelines appeared
during polling. This correctly identifies async compositions
regardless of fetch speed, while leaving sync compositions
untouched.
The setInterval-based late-bind polling in init.ts caused visual
regressions across all style-prod tests. Even with the sawMissing
guard, the mere presence of the interval registration altered
event loop timing enough to shift rendered frames.
The engine's pollSubCompositionTimelines + conditional
__hfForceTimelineRebind already handles async timeline detection
for renders. The runtime only needs to expose the rebind hook —
it shouldn't poll on its own.
For studio preview of async compositions, the engine's rebind
call (via __hfForceTimelineRebind) is the correct mechanism.
The late-bind polling was unconditionally rebinding on its first
check even when all timelines were already present, causing visual
regressions across style-prod tests. Now tracks sawMissing flag —
only rebinds if the poll previously detected missing timelines that
subsequently appeared. Compositions with synchronous timeline
registration exit the poll immediately with no side effects.
1. Only call __hfForceTimelineRebind() when the timeline poll actually
had to wait (pollDuration > 2 intervals). For compositions with
synchronous timeline registration, the rebind was unnecessary and
shifted render timing, causing PSNR regressions in chat and
gsap-letters-render-compat.
2. Regenerate compiled.html baselines for missing-host-comp-id and
overlay-montage-prod to match the new flattenInnerRoot behavior
(data-composition-id stripped from inlined inner roots, replaced
with data-hf-authored-id).
3. Add late-bind polling to runtime init.ts — after external
compositions load, poll for 5s to detect async timelines that
register after initial binding (e.g. from fetch callbacks).
Review items addressed:
1. Mirror video-failure warning in beginFrame path (was screenshot-only)
2. Fix resolveProjectRelativeSrc escape-fallback to use query-stripped
cleanSrc instead of raw src for the normalize/strip arm
3. Export prepareFlattenedInnerRoot from @hyperframes/core/compiler and
consume in the producer instead of duplicating the implementation
4. Use typed Window cast instead of (window as any) for __hfForceTimelineRebind
5. Regenerate docs/public/catalog-index.json with all 6 map blocks
6. Restore Maps nav group in docs.json (catalog generator had merged
them into Data)
- Replace from:"random" with from:"center" stagger in us-map,
world-map, spain-map — random stagger is non-deterministic across
parallel render workers, causing visual jumps at chunk boundaries.
- Exempt type="importmap" and type="module" inline scripts from the
invalid_inline_script_syntax lint rule. The rule used new Function()
to parse, which rejects import statements and JSON import maps.
Closes#929.
- Cache-bust all map MDX preview video URLs after re-rendering with
the deterministic stagger fix.