Skip to content

fix: harden CLI edge-case repros#591

Merged
miguel-heygen merged 7 commits intomainfrom
fix/cli-repro-edge-cases
May 1, 2026
Merged

fix: harden CLI edge-case repros#591
miguel-heygen merged 7 commits intomainfrom
fix/cli-repro-edge-cases

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented May 1, 2026

Problem

I reproduced the selected open issue batch one by one and confirmed the reports were valid. The fixes all touch the CLI/runtime capture boundary, then the follow-up regression run exposed one over-broad runtime change in sub-composition host visibility and one CI-only baseline trap.

Closes #590, #589, #588, #587, #586, and #584.

What this fixes

CLI/runtime edge cases

  • Makes the GSAP infinite-repeat lint rule ignore JavaScript comments, so literal repeat:-1 text in comments is not flagged.
  • Lets the compositions CLI inspect <template> content, count visual-only template descendants, estimate simple GSAP durations, and suppress root data-start warnings in sub-composition lint mode.
  • Preserves runtime bootstrap scripts when body scripts are coalesced, and injects the runtime into a real <head> when source HTML has no head.
  • Keeps <template>-wrapped sub-compositions: data-composition-src loader and compositions CLI report 0 elements / black render (HF 0.4.41) #589 fixed by loading and rendering template-wrapped sub-composition content, while restoring host visibility to the shorter of the authored parent clip window and the child composition live timeline.
  • Resolves snapshot/validate viewport size from root data-width / data-height instead of falling back to 1920x1080.
  • Skips fully off-frame text boxes during contrast sampling and bounds-checks ring samples so contrast output no longer emits null:1 / NaN:1.
  • Marks muted videos as data-has-audio="false" in the core timing compiler, which fixes the same-src muted <video> + separate <audio> StaticGuard case.
  • Keeps user-authored hf-seek listeners reachable during capture by preventing author scripts from being merged into the runtime bootstrap path.

Shared helper cleanup

  • Removes the stale producer-local timing compiler duplicate; producer compilation now consumes the core timing compiler.
  • Centralizes HTML document helpers in core: fragment parsing, embedded runtime stripping, head/body script injection, and early-head injection.
  • Centralizes the CLI layout/snapshot static HTML server.
  • Adds browser-safe core subpath helpers for Lottie readiness and CLI screenshot clip calculation; Studio's Vite config keeps the screenshot clip helper self-contained so clean-checkout test startup does not value-import core .ts source.
  • Replaces the engine parity-contract copy with a core re-export.
  • De-duplicates render-job cleanup and Studio static file-serving callbacks.

Regression hardening

  • Replaces the embedded-runtime script stripping regex with a script-tag scanner that handles closing tags like </script >.
  • Escapes inline script bodies before wrapping them in <script> tags, so authored </script and <!-- text cannot break out of the injected wrapper script.
  • Shares media-duration clamping between core and producer, with a 50 ms tolerance for ffprobe precision drift between local and CI media stacks.
  • Pins the affected style fixture SFX durations in source so style-1 and style-9 compile deterministically.
  • Restores the vfr-screen-recording video golden to the CI-stable baseline; the current CI failure showed the Linux render matches the old golden, while the locally refreshed macOS golden was the mismatch.

Root cause

The CLI paths had accumulated assumptions that held for simple direct-root landscape compositions but not for current composition patterns: DOM queries did not enter template content, snapshot/validate used a fixed viewport, runtime and author scripts shared a coalescing bucket, and timing compilation treated every video as audio-bearing unless authors manually overrode it.

The style shard failures were not product regressions. Local and CI media probing disagreed on the short SFX clip duration by about 45 ms, and the compiler was clamping authored durations to the locally probed value. The shared clamp tolerance preserves explicit author/source durations for small probe precision differences while still clamping real overflows.

The vfr fast-shard failure was a bad baseline refresh: CI actual frames matched the old vfr-screen-recording baseline at 40+ dB PSNR, but mismatched the macOS-refreshed golden at ~18-22 dB. The fix is to keep the Docker/Linux-stable video golden and only retain the deterministic compiled snapshot change.

The sub-composition regression came from treating a host's authored parent window as the only visibility boundary. That made settled child overlays stay visible after their own live GSAP timeline ended. The corrected runtime behavior respects both contracts: parent clips still bound where the host can appear, and the child live timeline can end the host earlier.

Verification

Local checks

  • bunx oxfmt --check packages/core/src/runtime/init.ts packages/core/src/runtime/init.test.ts
  • bunx oxlint packages/core/src/runtime/init.ts packages/core/src/runtime/init.test.ts
  • bun run --cwd packages/core test src/runtime/init.test.ts
  • bun run --cwd packages/cli test src/commands/compositions.test.ts src/utils/compositionViewport.test.ts
  • bun run build:hyperframes-runtime
  • bun run --cwd packages/producer test --keep-temp --sequential style-12-prod style-5-prod
  • bun run --cwd packages/producer test --sequential vfr-screen-recording hdr-hlg-regression style-7-prod
  • bun run --cwd packages/core test src/compiler/htmlCompiler.test.ts src/compiler/timingCompiler.test.ts src/index.test.ts
  • bunx oxfmt --check packages/core/src/compiler/timingCompiler.ts packages/core/src/compiler/htmlCompiler.ts packages/core/src/compiler/htmlCompiler.test.ts packages/core/src/compiler/index.ts packages/core/src/index.ts packages/core/src/index.test.ts packages/producer/src/services/htmlCompiler.ts
  • bunx oxlint packages/core/src/compiler/timingCompiler.ts packages/core/src/compiler/htmlCompiler.ts packages/core/src/compiler/htmlCompiler.test.ts packages/core/src/compiler/index.ts packages/core/src/index.ts packages/core/src/index.test.ts packages/producer/src/services/htmlCompiler.ts
  • bun run --cwd packages/core typecheck
  • bun run --cwd packages/producer typecheck
  • bun run --cwd packages/producer test --sequential style-1-prod style-9-prod
  • bun run --filter @hyperframes/studio test with packages/core/dist temporarily hidden to simulate clean-checkout config loading
  • git diff --check

CI artifact checks

  • Inspected failed run 25225854394 job 73969147096: style-1 failed only on click-sfx 1.044898 vs 1 duration/end.
  • Inspected failed run 25225854394 job 73969147061: style-9 failed only on SFX 1.044898-based duration/end mismatches.
  • Inspected failed run 25225854394 job 73969147048: vfr-screen-recording compilation/audio passed, visual failed after comparing against the macOS-refreshed golden.
  • Compared the first 10 uploaded CI vfr failure frames against the restored old baseline; minimum PSNR was 40.444705, above the fixture threshold of 28.

Repro checks

  • bun packages/cli/src/cli.ts lint /tmp/hf-590-repro now passes without gsap_infinite_repeat.
  • bun packages/cli/src/cli.ts snapshot /tmp/hf-587-repro --at 0.5 --timeout 1000 now writes a 1080x1920 PNG.
  • bun packages/cli/src/cli.ts validate /tmp/hf-588-repro --timeout 500 no longer emits null:1 / NaN:1 contrast output.
  • bun packages/cli/src/cli.ts validate /tmp/hf-586-repro --timeout 500 --contrast false no longer emits the muted-video StaticGuard contract error.
  • bun packages/cli/src/cli.ts compositions /tmp/hf-589-gsap-repro now reports foo 0.5s 1920x1080 1 element.
  • bun packages/cli/src/cli.ts snapshot /tmp/hf-589-gsap-repro --at 0.25 --timeout 2000 captures the expected template-backed red frame.
  • bun packages/cli/src/cli.ts snapshot /tmp/hf-584-repro --at 0.5,1.5 --timeout 500 captures the expected post-seek green frame.

Browser verification

  • Refreshed the local side-by-side comparison page at qa-artifacts/pr-591-video-compare/index.html.
  • Served the comparison page locally and used agent-browser to load style-12-prod, play both videos quickly to the failed window, pause, and inspect the side-by-side frame.
  • Browser proof screenshot: qa-artifacts/pr-591-video-compare/browser-proof/fixed-style12-labeled.png.
  • Browser proof recording: qa-artifacts/pr-591-video-compare/browser-proof/fixed-style12.webm.
  • Earlier Studio proof artifacts remain local-only: qa-artifacts/dedupe-refactor-preview.png, qa-artifacts/dedupe-refactor-preview-after-play.png, qa-artifacts/dedupe-refactor-preview.webm.

Notes

  • Browser proof and CI diagnostic artifacts are intentionally local-only and not committed.
  • Studio's Vite config intentionally keeps the thumbnail clip helper inline because Vite/Vitest config startup runs through Node's loader before package source .ts imports are transformed.
  • The committed PR diff changes vfr-screen-recording/output/compiled.html but no longer changes vfr-screen-recording/output/output.mp4 relative to main.
  • I attempted a local linux/amd64 Docker validation to mirror CI, but the local Docker build was blocked by Debian package download failures. The arm64 Docker image also cannot launch the x64 Puppeteer headless shell under OrbStack. The vfr baseline decision is therefore based on the uploaded CI artifact comparison above.
  • I kept this validated issue batch in one PR because the fixes overlap the same CLI/runtime capture surfaces.

@miguel-heygen miguel-heygen marked this pull request as ready for review May 1, 2026 12:23
@miguel-heygen miguel-heygen marked this pull request as draft May 1, 2026 12:28
@miguel-heygen miguel-heygen marked this pull request as ready for review May 1, 2026 12:29
@miguel-heygen miguel-heygen marked this pull request as draft May 1, 2026 12:52
Comment thread packages/core/src/compiler/htmlDocument.ts Fixed
Comment thread packages/core/src/compiler/htmlDocument.ts Fixed
@miguel-heygen miguel-heygen marked this pull request as ready for review May 1, 2026 12:52
@miguel-heygen miguel-heygen marked this pull request as draft May 1, 2026 13:12
@miguel-heygen miguel-heygen marked this pull request as ready for review May 1, 2026 19:04
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big PR — solid intent on the dedup, but blocking a stamp on one regression that breaks studio test startup on a clean checkout. Walking through what I see:

What's good

The refactor is well-motivated. The producer's services/timingCompiler.ts literally carried a comment "Keep this aligned with core timing compiler behavior" — explicit duplicate that's now gone (-212 lines). The centralize shared html helpers commit nets -118 lines across 21 files; the engine and producer each lost ~60-85 lines of duplicated fileServer.ts logic. Real cross-package consolidation, not just syntactic.

Each of the 6 closed issues (#584-590) has a fix with a test. Mapping:

Issue Fix landing site Test
#590 GSAP infinite-repeat false-positive on JS comments lint/rules/gsap.ts:stripJsComments (state-machine, preserves strings/template literals) gsap.test.ts:"does not error on repeat: -1 inside JavaScript comments"
#589 Template-wrapped sub-comp 0 elements cli/commands/compositions.ts:parseSubComposition (uses template.content when present) compositions.test.ts:"reads template-wrapped sub-composition contents"
#588 Contrast NaN contrast-audit.browser.js viewport bounds-check + sample bounds-check (browser-eval; covered by sandbox tests)
#587 Snapshot/validate viewport cli/utils/compositionViewport.ts (NEW) compositionViewport.test.ts
#586 Muted video + audio StaticGuard htmlBundler.ts runtime bootstrap script protection htmlBundler.test.ts:"does not merge author scripts into the runtime bootstrap placeholder"
#584 hf-seek listeners unreachable during capture runtime/init.ts (covered by init.test.ts cases that were updated)

The blocker

Studio test suite fails to start on a clean checkout with this PR.

@hyperframes/studio test: failed to load config from packages/studio/vite.config.ts
@hyperframes/studio test: TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"
  for packages/core/src/studio-api/helpers/screenshotClip.ts

Root cause: the centralize shared html helpers commit added a value-level import to packages/studio/vite.config.ts:

import { getElementScreenshotClip } from "@hyperframes/core/studio-api/screenshot-clip";

The new @hyperframes/core/studio-api/screenshot-clip export resolves to .ts source (./src/studio-api/helpers/screenshotClip.ts). When vitest reads vite.config.ts, it goes through Node's regular module loader before any TS transform, and Node can't load a .ts file directly. The type-only import next to it (import type { ScreenshotClip }) is fine because types erase, but the value import doesn't.

Symmetric proof:

  • bun run --filter @hyperframes/studio test on origin/main: ✅ 256/256 passes (19 files)
  • Same command on PR head: ❌ test runner crashes at config load (0 tests run)
  • Same command after I commented out just the getElementScreenshotClip value import in vite.config.ts: ✅ 256/256 passes again

Two ways to fix

  1. Inline getElementScreenshotClip back into vite.config.ts. It's ~15 lines; losing the dedup for this one function (which exists only to be page.evaluate'd, so it has to be self-contained anyway) is way cheaper than breaking studio test startup.
  2. Move the helper to a code path that's pre-built to .js (or expose it from a dist/ path so the .ts isn't on the package's import surface). More architectural; could be deferred to a follow-up.

Suggest (1) for this PR.

Smaller smells worth flagging (non-blocking)

  • Bundling 6 fixes in one commit (b0e77b8d) makes bisection impossible. The visibility-clamp change in runtime/init.ts in that commit was unintentional (none of the 6 closed issues touch composition-host visibility) and required a follow-up commit bca99a0d "restore sub-composition visibility clamp" to revert. Per-issue commits would have made this catch-and-revert trivial; bundled, it surfaced as a behavior change buried inside an unrelated fix commit. Future bug-bash PRs would benefit from one-commit-per-issue.
  • Miguel's PR description says "5 issues" but closes 6 (the Closes #584-#590 list — six issues). Minor copy fix.
  • Other test suites pass cleanly: @hyperframes/core 601/601, @hyperframes/cli 196/196, @hyperframes/engine clean. So the regression is studio-only.

tl;dr

LGTM as a refactor and as a bug-fix bundle for #584-590. Blocked on the studio test regression — fix the vite.config.ts import (suggest inlining getElementScreenshotClip) and I'll restamp.

— Review by Rames Jusso

@miguel-heygen
Copy link
Copy Markdown
Collaborator Author

@jrusso1020 addressed the Studio startup blocker in 72bacf4f.

  • packages/studio/vite.config.ts no longer value-imports @hyperframes/core/studio-api/screenshot-clip; the thumbnail page.evaluate clip helper is inline again so config startup does not depend on Node loading core source .ts.
  • Updated the PR body so it no longer claims the Studio screenshot helper is deduped.
  • Verified bun run --filter @hyperframes/studio test with packages/core/dist temporarily hidden to simulate clean-checkout config loading: 19 files / 256 tests passed.
  • Also ran bunx oxfmt --check packages/studio/vite.config.ts, bunx oxlint packages/studio/vite.config.ts, and bun run --cwd packages/studio typecheck.

@miguel-heygen miguel-heygen requested a review from jrusso1020 May 1, 2026 21:34
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker resolved — re-stamping. Fix commit 72bacf4f "fix: avoid source import in studio vite config" applies option (1) from my prior review: removed both imports of @hyperframes/core/studio-api/screenshot-clip from packages/studio/vite.config.ts, declared a local ScreenshotClip interface, and inlined the helper body into page.evaluate. Clean.

The shared getElementScreenshotClip in packages/core/src/studio-api/helpers/screenshotClip.ts is correctly preserved — it's still used by production studio-api routes through the runtime path; the inline duplicate only lives in dev-time vite.config.ts where it has to be self-contained for page.evaluate anyway. ~15 LOC duplication for a clean Node-loader-vs-TS-source boundary is the right trade.

Verified locally:

  • @hyperframes/studio test: 19 files / 256/256 tests pass (was crashing on config load before)
  • @hyperframes/core: 601/601 still pass
  • @hyperframes/cli: 196/196 still pass

— Re-review by Rames Jusso

@miguel-heygen miguel-heygen merged commit 15ee63c into main May 1, 2026
44 of 54 checks passed
@miguel-heygen miguel-heygen deleted the fix/cli-repro-edge-cases branch May 1, 2026 22:00
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.

lint regex gsap_infinite_repeat false-positives on JS comments containing the literal "repeat:-1" (HF 0.4.41)

3 participants