Commit Graph
25 Commits
Author SHA1 Message Date
Miguel Ángel 6f677292ae refactor(core): simplify packages/core — dead code, dedup, type safety (#1413)
- Delete unused mediaPreloader module, 5 dead RuntimeState fields,
  emitPerformanceMetric, lintScriptUrls, 5 variable type guards
- Consolidate compiler utilities: unify CSS URL regex, relative URL
  predicate, MIME map, @import regex, bulk asset rewrite delegation
- Cache extractGsapWindows per script (eliminates 2 redundant recast
  parses per lint run), share stripJsComments and script extraction
- Deduplicate GSAP parser: share serializeValue/safeJsKey, centralize
  converted-id fallback (6 sites), keyframe codegen (3 sites),
  waypoint extraction, insert-after-anchor, script hoisting
- Replace 88 bare any annotations with typed AstNode/AstPath interfaces
- Derive RuntimeBridgeControlAction from HyperframeControlAction,
  alias RuntimePickerElementInfo, share macOS font profiler
- Gate generateHyperframesStyles on includeStyles, collapse 4 GSAP
  property mutation cases into 2
- Extract magic numbers into named constants, replace 5 double casts
  with type guards and typed accessors (runtime/globals.ts),
  reduce function complexity in htmlParser and files route
2026-06-13 18:23:36 -04:00
James RussoandClaude Opus 4.8 1c47ba9981 refactor(core): route project paths through a single resolveWithinProject chokepoint (#1398)
Structural follow-up to the symlink-escape fix. The recurring miss (#465
fixed isSafePath but left render.ts; the sweep then turned up play.ts,
htmlBundler, ...) is because containment was enforced by convention —
"remember to call isSafePath after every resolve()" — which a new call site
can silently skip.

Add resolveWithinProject(base, relativePath) -> string | null (resolve +
containment in one call) and route the studio-api + bundler sites through
it, so a caller cannot resolve a project-relative path without the guard:

- studio-api routes/files.ts (read, rename, duplicate, upload-dir), preview.ts
  (sub-comp + static asset), render.ts (composition) — all the
  resolve()+isSafePath() pairs collapse to a single call.
- compiler/htmlBundler.ts: its local safePath helper was exactly this; drop
  it for the shared one.

Left intentionally on isSafePath: files.ts upload (resolves a name against a
validated sub-dir but contains against the project root) and htmlBundler's
CSS @import (resolves against the CSS file's dir, contains against the root) —
these resolve and contain against *different* bases, which the single-base
chokepoint doesn't model.

Exported from @hyperframes/core and re-exported from studio-api/helpers for
back-compat. Adds resolveWithinProject unit tests; all existing studio-api
route tests pass unchanged (behavior is identical — same resolve, same
containment, same reject paths).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 21:10:36 -07:00
James RussoandClaude Opus 4.8 953bab319b fix(core): block symlink-based path escape in studio-api isSafePath (#1397)
* fix(core): block symlink-based path escape in studio-api isSafePath

path.resolve() collapses ./.. but does not dereference symlinks, so a
symlink living inside the project dir but pointing outside it (e.g.
project/link -> /etc) passed the prefix check, letting a downstream
read/write/stat follow it to a file outside the project root. The `..`
traversal case was already blocked; symlink traversal was the gap.

Canonicalize both base and target with realpathSync before comparing.
The target may not exist yet (new-file writes), so canonicalize the
deepest existing ancestor and re-attach the trailing not-yet-existing
segments, which cannot be symlinks at check time. Fail closed if base is
unresolvable.

Adds safePath.test.ts covering: in-base allow, not-yet-existing write
target, `..` escape, existing-file-through-symlink escape, write-target
under a symlinked parent, file-symlink escape, in-base symlink allow,
symlinked-base canonicalization, and base-missing fail-closed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core,cli): route render + play composition paths through isSafePath

Review on #1397 found a third call site with the same vulnerable
startsWith pattern. Apply Rule 2: fix every site sharing the contract
(gate an attacker-influenced path before a symlink-following fs op).

- studio-api routes/render.ts: body.composition (from c.req.json()) was
  checked with `resolved.startsWith(resolve(project.dir) + sep)`, which
  doesn't dereference symlinks — an in-project symlink to an external
  target escaped the project root. Now uses isSafePath().
- cli commands/play.ts: the `/composition/*` server route used
  `filePath.startsWith(project.dir)` with no trailing-separator guard, so
  both a sibling dir sharing the prefix (`<dir>-evil`) and symlink escapes
  passed. Now uses isSafePath() via @hyperframes/core/studio-api (the same
  lazy-import pattern commands/validate.ts already uses).

Tests: render.test.ts gains a "composition path safety" block (in-base
allow, `..` reject, in-project-symlink-to-outside reject, in-project
symlink staying inside allow). The shared render test adapter now points
at a real dir since isSafePath fails closed on an unresolvable base
(production project dirs always exist on disk).

Not in this change: compiler/htmlBundler.ts has the same class at two
sites (safePath helper + inline CSS @import check), but the compiler sits
below studio-api in the dependency graph and can't import isSafePath
without a backwards edge; that fix needs the helper promoted to a neutral
module and is tracked as a follow-up. renderArgs.ts / videoFrameExtractor.ts
carry the trailing-sep guard and a local-CLI/engine-internal threat model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(core): promote isSafePath to a shared module + harden htmlBundler

Per review on #1397: extend the symlink-escape fix to the compiler, and
remove the duplicated path-safety logic.

- Move isSafePath to packages/core/src/safePath.ts (a neutral package-root
  module). studio-api/helpers/safePath.ts re-exports it for back-compat
  (keeping walkDir), and it's now exported from the core entrypoint so
  non-studio-api layers can use it. compiler/ sits below studio-api in the
  dep graph, so it could not import the helper from its old home without a
  backwards edge — the promotion removes that constraint.
- compiler/htmlBundler.ts: route both containment checks (the safePath
  helper and the inline CSS @import check) through isSafePath. The bundler
  reads+inlines these files, so an in-project symlink pointing outside the
  root would otherwise bake external content into the output. All callers
  already skip on a null/false result, so nothing is read on rejection.

Tests: safePath.test.ts moves with the impl; htmlBundler.test.ts gains a
case proving an in-project sub-composition script is inlined while a
script reached through an escaping symlink is not (positive control + leak
assertion).

Deferred (tracked for a dedicated follow-up, see PR thread): the
relative()-based isPathInside family (core/compiler/assetPaths,
producer/services/fileServer, producer/utils/paths and their callers in
the render pipeline) is symlink-blind in the same way, and engine
videoFrameExtractor's asset resolver needs a caller-side gate (its http
downloads land outside the project root, so a single-root check is wrong).
Both are regression-sensitive render-pipeline surfaces that warrant their
own focused, well-tested pass. renderArgs.ts is intentionally left: it is
filesystem-free by design (injected stat) and its threat model is the
user's own --composition CLI arg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(core): hedge symlink tests for Windows + copy before reverse (review nits)

Addresses Via's non-blocking review notes on #1397:

- Wrap every symlinkSync in the new tests with a tryCreateSymlink helper that
  returns false (and the test early-returns) when creation throws, mirroring the
  preview.test.ts convention. Non-symlink-privileged Windows runners no longer
  risk crashing the suite on EPERM.
- safePath.ts: `[...trailing].reverse()` instead of mutating `trailing` in place —
  harmless today (single return) but future-proof against a looping edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:08:35 -07:00
Matt Van HornandMatt Van Horn edd85473e7 feat(producer,core): play animated GIF inputs frame-synced via prep-time VP9 transcode (#1335)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-10 22:39:19 -04:00
Miguel Ángel 29d6f1eac9 fix(render): add end-to-end observability (#1248) 2026-06-06 23:55:58 -04:00
Miguel Ángel a4706da513 fix(cli): localize external assets in publish archive (#1160)
Compositions referencing assets outside the project directory (via ../
paths) produced broken published projects — those files were never
included in the ZIP archive.

localizeExternalAssets() now scans all HTML and CSS files in the archive
for src, href, and url() references that resolve outside the project
dir. For each, it copies the file into the archive under _ext/ and
rewrites the reference to point there.

Handles: src/href attributes, <style> url(), inline style url(),
standalone CSS url(), sub-composition HTML files, deduplication of
the same asset referenced from multiple files.

Shared primitives (CSS_URL_RE, isNonRelativeUrl, isPathInside) extracted
into core/compiler/assetPaths.ts — single source of truth across core,
producer, and CLI.
2026-06-01 21:06:35 -04:00
Miguel Ángel 789d1d4775 fix(studio): cover GSAP editor target-resolution limitations (#1116)
Follow-up to #1115. Makes the Design-panel editor recognise every target
shape real compositions use. The panel stays behind STUDIO_GSAP_PANEL_ENABLED
(default off) — no flag change here.

- Array targets: tl.to([a, b], {...}) resolves to a CSS group selector
  (".a, .b"). The source array is never rewritten — the joined string is for
  display/matching only; edits still touch just the vars object.

- Chained calls: tl.to(a, ...).to(b, ...) — the matcher now walks the member
  chain to its timeline root, so every link is captured (previously only the
  first). Deletion is chain-aware: it splices out the single targeted link and
  re-points the chain instead of dropping the whole statement.

- gsap.utils.toArray("sel") resolves like querySelectorAll, inline or via a
  variable binding.

- Lexical scoping: element-variable resolution is now per-scope (walks the
  enclosing function/program chain) instead of a flat map. Fixes silent
  wrong-resolution when two IIFEs reuse a variable name, and unlocks
  multi-scene files. (Addresses review: flat-binding-scope.)

- forEach/map callback params (items.forEach(el => tl.to(el, …))) and items[i]
  indexing resolve to the collection's selector, so loop-generated tweens are
  editable.

- Panel matching: an element matches a tween when its id/selector is any member
  of a comma-group target, so either element of an array/toArray tween surfaces
  the shared animation.

- Review items: mutation parse failures now console.warn instead of swallowing
  silently; buildTweenStatementCode no longer emits duration on `set`; the
  id-only serialize-side filter is renamed getAnimationsForElementId to
  disambiguate from the panel's id-or-selector matcher; added fromTo round-trip
  and variable-target overlap-lint tests.

Genuinely runtime-only targets (template-literal selectors, unbounded loops)
still skip gracefully — they can't be resolved or matched statically.
2026-05-28 20:57:59 -04:00
Miguel Ángel fb2e21090f feat(studio): GSAP tween editing in Design panel (#1102)
* feat(studio): GSAP tween editing in Design panel

Add a GSAP animation editor to the studio Design panel: select an element,
view and edit its tweens (properties, easing, timing), add/delete animations,
and drag custom bezier speed curves — all persisted back to the composition
HTML. Gated behind VITE_STUDIO_ENABLE_GSAP_PANEL.

Parsing of existing GSAP source now uses a recast + Babel AST parser instead of
regex, giving scope resolution, stable tween IDs, and round-trip preservation of
extras and unresolved raw values.

recast compiles to CommonJS that calls require("fs"), which breaks browser and
Vite SSR bundles. To contain it, @hyperframes/core is split into an isomorphic
layer and a Node-only AST layer:

- gsapSerialize.ts holds the recast-free helpers (serialization, keyframe
  conversion, validation, shared types). htmlParser.ts is now fully isomorphic.
- parseGsapScript and the script-mutation helpers live in gsapParser.ts,
  reachable only via the @hyperframes/core/gsap-parser subpath, loaded
  server-side by the studio-api mutation routes and the linter via dynamic
  import (recast stays external under SSR).
- The barrel and the gsap-constants subpath are recast-free, so studio browser
  bundles never trace recast.

Adds AST parser unit + stress coverage and e2e helpers for the panel.

* fix(lint): await async lintHyperframeHtml in all callers

lintHyperframeHtml became async (gsap rules use dynamic import)
but lintProject and check-hyperframe-static weren't awaiting it,
causing typecheck failures and runtime crashes in CI.

Also wire LintRule type in gsap rules to fix fallow unused-type
finding, and suppress render.ts exported-for-tests symbols.
2026-05-28 19:16:34 -04:00
Miguel Ángel 526709cad2 fix(cli): handle encoded lint asset paths 2026-05-24 16:31:45 -04:00
Miguel Ángel 215811334f fix(producer): accept plain integer fps in createRenderJob
The rational `Fps = { num, den }` refactor in 5dcc89c broke callers
passing `fps: 30` (the form documented in every code example and used
by external consumers). FFmpeg received `undefined/undefined` as the
framerate, causing a cryptic exit-code error.

Add `FpsInput = number | Fps` and `toFps()` normalizer in
@hyperframes/core. `createRenderJob` now accepts both forms —
plain integers are promoted to `{ num, den: 1 }` at the boundary;
`RenderConfig.fps` stays strict `Fps` internally so no downstream
code changes.

Also fixes the producer and engine docs, which showed phantom
`input`/`output` fields on `createRenderJob` and a wrong
`executeRenderJob(job)` signature (missing `projectDir`/`outputPath`
args).

Closes #1031
2026-05-22 13:35:16 -04:00
Theodor Kleynhans 5dcc89c930 feat(cli): accept ffmpeg-style rational fps (NTSC, PAL, slow-mo)
Replaces the rigid `--fps 24|30|60` whitelist with a numeric range and
adds support for ffmpeg-style fractional framerates so NTSC stays exact
end-to-end.

- `--fps 30` keeps working (integer fps)
- `--fps 30000/1001` now means exact NTSC 29.97 (not the lossy decimal)
- `--fps 24000/1001`, `--fps 60000/1001`, `--fps 25/50/120/240` all work
- Decimals like `--fps 29.97` are rejected with a friendly error pointing
  the user at the rational form, since `29.97` and `30000/1001` round
  to different framerates inside ffmpeg

Carries an `Fps = { num: number; den: number }` rational end-to-end:
RenderConfig, EncoderOptions, StreamingEncoderOptions, CaptureOptions,
DockerRenderOptions, Studio API request body, regression-harness
meta.json. The `-r` and `-framerate` ffmpeg args emit the rational form
verbatim (`30000/1001`) so no decimal round-trip happens at the encoder
boundary. Frame-interval math uses `1000 * den / num` ms (33.366… for
NTSC, 33.333… for integer 30).

Helpers live in @hyperframes/core:
- `parseFps(input: string | number): FpsParseResult` — discriminated
  parser used by both the CLI and the Studio API route
- `fpsToFfmpegArg(fps: Fps): string` — emits "30" or "30000/1001"
- `fpsToNumber(fps: Fps): number` — for arithmetic (telemetry, frame
  count, frame-index → time)

Studio API wire format accepts polymorphic `fps: number | string`:
- number → integer fps (`30`)
- string → rational (`"30000/1001"`)
Decimals are rejected; matches the same rule as the CLI.

Existing meta.json fixtures with integer `"fps": 30` continue to load
unchanged — the regression-harness validator now normalizes both number
and string inputs through `parseFps`.
2026-05-09 00:09:15 +02:00
James 2b2281d490 refactor: dedupe resolution presets and clean up 4k stack 2026-05-07 16:58:26 +00:00
JamesandClaude Opus 4.7 09da5db436 refactor(core): apply /simplify findings on validation PR
- core.types.ts: export COMPOSITION_VARIABLE_TYPES, a runtime tuple of
  every CompositionVariableType variant guarded by `as const satisfies
  readonly CompositionVariableType[]`. Adding a new variant to the union
  without also adding it to the tuple becomes a compile error rather
  than silent drift in callers that maintain their own list.
- composition.ts (lint rule): the local
  `new Set(["string","number","color","boolean","enum"])` now derives
  from COMPOSITION_VARIABLE_TYPES instead of duplicating the list.
- index.ts: export COMPOSITION_VARIABLE_TYPES alongside the rest of the
  variable type guards.

Reuse + efficiency reviews otherwise clean. The other reuse finding
(loadProjectHtml helper to dedupe readFileSync + ensureDOMParser across
3 callers) is real but reaches files outside this PR's scope; it's a
better fit as a follow-up cleanup once the variable-feature stack lands.

All 48 composition lint tests + 49 core suite tests still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 20:06:10 +00:00
JamesandClaude Opus 4.7 c1b6efd9c5 feat(core,cli): variable schema validation + lint rules
Two lint rules + render-time validation built on top of the existing
data-composition-variables schema.

Lint rules (packages/core/src/lint/rules/composition.ts):
- invalid_variable_values_json — host's data-variable-values must parse as
  a JSON object. Today the runtime swallows parse failures silently and
  falls back to declared defaults, masking typos.
- invalid_composition_variables_declaration — root <html>'s
  data-composition-variables must parse as an array of objects with
  `id` (string), `type` (one of string/number/color/boolean/enum), `label`
  (string), and `default`. Per-entry findings report which fields are
  missing or invalid.

Both rules read attributes via a new `readJsonAttr` helper in lint/utils.ts.
The existing `readAttr` regex `["']([^"']+)["']` truncates JSON-in-attribute
values at the first internal quote (e.g. `data-variable-values='{"x":"y"}'`
captures only `{`); `readJsonAttr` alternates double-vs-single-quoted
branches with quote-specific char classes so JSON values round-trip cleanly.
A second helper `findHtmlTag` returns the actual <html> open tag (where
data-composition-variables lives) — distinct from `findRootTag` which
returns the first in-body composition element.

Render-time validation (packages/core/src/runtime/validateVariables.ts):
- validateVariables(values, declarations) returns a structured array of
  issues: undeclared keys, type mismatches, enum-out-of-range values.
  Pure / sync; works in any environment.
- formatVariableValidationIssue(issue) renders a one-line user-facing
  string for CLI output.
- Both exported from @hyperframes/core for studio/tooling reuse.

CLI integration (packages/cli/src/commands/render.ts):
- New --strict-variables flag. Default behavior: print warnings and
  continue. With --strict-variables: print warnings then exit 1.
- New `validateVariablesAgainstProject(indexPath, values)` helper:
  reads the project's index.html, runs extractCompositionMetadata to
  pull the declared schema, validates the CLI's --variables payload
  against it. ensureDOMParser polyfill for Node-side parsing (same
  pattern as compositions.ts).

Tests:
- 11 new validateVariables unit tests covering happy path, undeclared
  keys, type mismatches (string/number/boolean/color/enum), enum range,
  multiple-issue aggregation, and formatter output.
- 11 new composition.test.ts cases for both lint rules: parse errors,
  shape errors, per-entry validation, unknown types, missing fields,
  positive cases.
- 5 new render.test.ts cases for validateVariablesAgainstProject:
  no-declarations, happy path, undeclared, type-mismatch, missing-file.
- All 646 core tests + 213 cli tests still green.

Docs:
- docs/packages/cli.mdx — added --strict-variables flag row.

This is PR 3 of a 4-PR stack. PR 4 ships skill/scaffold distribution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 20:05:33 +00:00
JamesandClaude Opus 4.7 c0d75a5268 feat(core,cli,engine,producer): add getVariables() helper and --variables render flag
Adds the parametrized-render primitive from hf#592 by reusing the existing
data-composition-variables schema as the source of declared defaults.

- Runtime helper window.__hyperframes.getVariables() (also exported from
  @hyperframes/core) reads data-composition-variables defaults from the
  document root and merges window.__hfVariables (CLI override) on top.
  Returns Partial<T> for typed access; supports a generic for editor
  ergonomics. Same code path runs in dev preview and at render time.
- CLI render --variables '<json>' / --variables-file <path> populates the
  override. Mutually exclusive; fail-fast on conflicting flags, missing
  file, unparseable JSON, or non-object payloads. parseVariablesArg is
  exported as a pure function so validation paths stay unit-testable.
- Engine injects window.__hfVariables via evaluateOnNewDocument before
  any page script runs, so the helper sees the merged values on its
  first call. Empty payloads are skipped to avoid pointless init scripts.
- Producer threads variables through RenderConfig and into the engine's
  CaptureOptions; Docker mode forwards --variables to the in-container
  CLI invocation via dockerRunArgs.

Composition authors declare variables once on the root <html> element:

  <html data-composition-variables='[
    {"id":"title","type":"string","label":"Title","default":"Hello"}
  ]'>

and read them in any composition script:

  const { title } = window.__hyperframes.getVariables();

A render with `--variables '{"title":"Q4 Report"}'` overrides the default
without modifying the composition source. Missing keys fall through to
the declared defaults, so dev preview and CLI renders without --variables
behave identically.

This is PR 1 of a 4-PR stack. Sub-comp per-instance scoping (carrying
host data-variable-values through the inlined sub-comp's getVariables()
call) lands in PR 2; schema validation and lint in PR 3; skill / scaffold
distribution in PR 4.

Tests: 9 new unit tests for getVariables() (jsdom), 11 new CLI tests
covering parseVariablesArg validation paths and Docker passthrough,
2 new dockerRunArgs assertions for the --variables flag. All existing
tests green (core 611, cli 208, engine 519).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:18:13 +00:00
Miguel Ángel 15ee63c6e7 fix: harden CLI edge-case repros (#591)
## 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 #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.
2026-05-02 00:00:08 +02:00
Miguel Ángel 072814e65c fix(core,cli,ci): harden runtime resolution + inline constant + smoke test (#458)
Guard buildHyperframesRuntimeScript() against missing entry.ts so it
returns null instead of crashing with esbuild stderr output. Add
getHyperframeRuntimeScript() that returns the pre-built IIFE as a
baked-in string constant — no esbuild, no file I/O, no import.meta.url.

Consolidate CLI runtime source resolution into a single module with
a clear priority chain: esbuild from source (dev) → inlined constant
(production) → pre-built artifact file (fallback).

Add CI smoke test that npm-packs the CLI, installs globally, runs
hyperframes preview, and asserts no stderr errors + runtime endpoint
returns JS.

Bump version to 0.4.16.
2026-04-23 21:44:26 +02:00
James Russo 969474e843 feat(cli): registry resolver + installer (#254)
## What

PR 3/17 of the catalog system rollout. Introduces the registry resolver/installer abstraction. No UX change — `init --template` still works identically. Stacks on #253.

**New module: `packages/cli/src/registry/`**
- `remote.ts` — fetches manifests (`registry.json`, `registry-item.json`) and item files from a GitHub-hosted registry. 24h cache on manifests; item files stream straight to `destDir`
- `resolver.ts` — `listRegistryItems`, `loadAllItems` (parallel fetch for picker UX), `resolveItem` (single-item fetch with `Available:` error)
- `installer.ts` — `assertSafeTarget` (runtime path-traversal guard) + `installItem` (parallel file download with up-front validation; all-or-nothing semantics)
- `index.ts` — barrel

**Registry content:**
- `registry/registry.json` — top-level manifest in PR 1's `RegistryManifest` shape. 8 examples
- `registry/examples/<id>/registry-item.json` — per-item manifest for each existing example, generated from legacy `templates.json` + HTML data-attribute probing
- `registry/examples/templates.json` — **deleted**, replaced by the above

**Compat layer:**
- `packages/cli/src/templates/{remote,generators}.ts` — thin shims that delegate to `../registry/`, keeping `init.ts`'s existing imports stable. `init.ts` doesn't move to the new API until PR 5 where it's part of a larger UX pass

**Tooling:**
- `scripts/generate-registry-items.ts` — idempotent one-off generator for this PR, kept in-repo for future example additions (`--only <name>` flag)

Design doc: [Hyperframes Catalog System](https://www.notion.so/heygen/Hyperframes-Catalog-System-Design-Plan-341449792c69813f899dcd53b4c0383a). Tracker entry in local `hyperframes-catalog-plan.md`.

## Why

Every future PR (`hyperframes add`, seed blocks, seed components, custom registries) otherwise has to keep piling onto the ad-hoc fetch + `cpSync` pattern in the old `fetchRemoteTemplate`. The new module is the single place that understands the registry wire format and file layout. **This is also where PR 1's schema comes alive.**

## How

### Scope-trimmed from the plan

- **No transitive dependency resolution yet.** Examples have no deps today. `resolveItem` doesn't walk `registryDependencies`; PR 5 adds that when blocks/components need it.
- **No ajv schema validation yet.** TS types + runtime path-traversal guard are the only safety nets. Full JSON-Schema validation lands when the registry starts accepting third-party content (PR 14 / custom registries).
- **init.ts refactor deferred to PR 5.** Compat shims keep this PR small and reviewable. PR 5 rewrites init alongside adding the `add` command.

### Safety

- `assertSafeTarget` rejects absolute paths, `..` segments, Windows drive letters, and any target that `path.resolve` shows to escape `destDir`. Mirrors the PR 1 schema `pattern`/`not.anyOf` on `target`, but runs at install-time so a registry that bypasses schema validation still can't write outside the project
- Up-front validation in `installItem` means a malformed item fails **before** any file is written. Atomic-ish semantics: all files land or none do

### Caching

- 24h manifest cache lives at `~/.hyperframes/cache/` per existing convention, but now keyed by `<baseUrl>__<kind>__<name>.json` so PR 14 custom registries can coexist

## Test plan

- [x] `bun run test` in `packages/cli`: **70 passed** (was 57 on #253, +13). Same 4 pre-existing failures (SRT/VTT whisper normalizer + `lintProject` clean-project test) — identical to main. No regressions
- [x] **Resolver unit tests (8):** filter by type, parallel load with fail-safe, resolve-by-name with `Available:` error message, unreachable-registry handling
- [x] **Installer unit tests (5):** accepts simple relative paths, rejects `..` segments, rejects Unix absolute paths, rejects Windows drive letters, permits `.` and dotfile-like names
- [x] **Smoke test**: `hyperframes init /tmp/x --template blank` (bundled code path, unchanged) works end-to-end
- [x] `bunx oxfmt --check` + `bunx oxlint`: clean
- [x] Pre-commit typecheck (core + studio): clean. CLI typecheck has 2 pre-existing errors (`render.ts`, `studioServer.ts` — unrelated `"mov"` format issue on main)
- [ ] **Smoke test remote fetch (`--template warm-grain`)** — verifiable only post-merge; registry paths live on `main` after this PR lands

## Breaking / migration

**No end-user-visible UX change.** `init --template <name>` still works the same way. Internally, `templates.json` is gone and the CLI now reads `registry.json` + `registry-item.json` per example.

Installed CLIs on old versions (`hyperframes@0.1.0`–`0.3.0`) already broke at PR 2 merge (see #253 rollout note). The next CLI release after this lands (`0.3.1`+) is the full fix.

## Commits

1. `generate-registry-items.ts` + generated manifests + deleted `templates.json`
2. Resolver + installer + compat shims
3. Unit tests

(All squashed into one commit on this branch; see `git log feat/registry-resolver ^refactor/registry-examples-dir`.)

## Stacks on

#253 — base branch. When #253 merges, this rebases onto `main`.

## Next in stack

PR 4 — `feat(cli)!: rename --template to --example`. Single clean cut, no alias. Tiny PR (~150 lines) that mostly updates `init.ts`'s argument schema, help text, and docs. Depends on this PR so the new flag name can be applied against the refactored code path.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-13 20:41:23 -07:00
James RussoandClaude Opus 4.6 eb338ae859 feat(core): add registry schema + TS types (#252)
* feat(core): add registry schema + TS types

PR 1/17 of the catalog system rollout. Foundation for a shadcn-style
registry with three item tiers: examples (full projects), blocks
(sub-compositions), components (effect snippets).

## What

- TS types: RegistryItem (discriminated union of ExampleItem/BlockItem/
  ComponentItem), RegistryManifest, FileTarget, ItemType, FileType
- JSON Schemas: schemas/registry.json, schemas/registry-item.json
- Compile-time exhaustiveness asserts on ITEM_TYPES/FILE_TYPES so adding
  to the TS union without updating the constant stops compiling
- Drift-guard test: schema enums must equal ITEM_TYPES/FILE_TYPES by
  set-equality; exactly 2 distinct type enums in registry-item.json
- Public API via new ./registry export path plus re-exports from root;
  schemas exposed via ./schemas/registry.json export for external tooling

## Why

- Every downstream PR (resolver, installer, hyperframes add, docs
  codegen, CI previews, skill, catalog command) builds on these types
- Getting the shape right now avoids painful migrations later

## How

- Discriminated union enforces that components do not have dimensions
  or duration and examples/blocks must have them (schema mirrors via
  if/then/else on the type discriminant)
- target path pattern rejects .. segments, Unix absolute paths, and
  Windows drive letters (defense-in-depth; CLI validates at runtime in
  PR 3)
- name pattern requires alphanumeric start and end (no trailing hyphens)
- Optional metadata: version, author, license, deprecated, minCliVersion
- additionalProperties: false on nested objects (catches typos on
  critical fields) but relaxed on top-level RegistryItem (allows
  third-party custom metadata in PR 15 custom registries)

## Test plan

- [x] Unit tests: 11 new tests covering type guards, discriminant
      narrowing, schema/TS drift guards, schema \$id sanity, optional
      metadata acceptance, and compile-time checks (via @ts-expect-error)
- [x] bun run test in packages/core: 445 passed (was 434 on main,
      +11 from this PR)
- [x] bunx oxfmt and bunx oxlint: clean
- [x] bun run typecheck: clean
- [ ] Manual testing: N/A (types + schemas only)
- [ ] Documentation updated: per-item doc pages land in PR 9 (codegen
      from these manifests); guide updates in PR 10+

## Breaking / migration

None. Pure additive — new module, new export paths, no existing
surface touched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(core): remove version field + hyperframes:demo file type

Address review feedback from Miguel:

- Remove `version` from RegistryItemBase + schema. Per shadcn model,
  the registry is versioned by git tags, not per-item. The adversarial
  review added it; the original design doc was correct.
- Remove `hyperframes:demo` from FileType union + FILE_TYPES constant
  + schema. Demo files exist on disk for the CI preview pipeline but
  are NOT installed to user projects and should not appear in
  registry-item.json files[]. Neither shadcn nor Remotion has a
  dedicated demo file type — demos are just compositions.
- Add `required: ["type"]` to the if-condition in the schema's
  allOf discriminant (Miguel's nit — makes the condition self-
  contained)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): add shader-transitions to Dockerfile.test

PR #251 added packages/shader-transitions/ to the workspace but didn't
update Dockerfile.test to COPY its package.json. This caused
`bun install --frozen-lockfile` to fail in the regression Docker build:
bun saw a lockfile referencing @hyperframes/shader-transitions but the
package.json wasn't present in the container, so it wanted to remove
the entry — triggering "lockfile had changes."

Verified: Docker build passes with `--no-cache` after this fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:18:37 -07:00
Miguel Ángel e2c8ed5d83 refactor(core): replace cheerio with linkedom to drop deprecated whatwg-encoding (#187)
## Summary

- `cheerio` pulls `encoding-sniffer` → `whatwg-encoding@3.1.1` (deprecated), causing a warning on every `npm install -g hyperframes`
- `linkedom` was already bundled into the CLI via tsup `noExternal` and has zero deprecated transitive deps
- Rewrote `htmlBundler.ts` and `subComposition.ts` to use standard DOM APIs via `linkedom`
- Added a `parseHTMLContent` helper that wraps HTML fragments in a full document structure (required for `linkedom` to populate `document.body`)
- Removed `cheerio` from `cli` dependencies and tsup `external` list
- Replaced `cheerio` with `linkedom` in `core` `optionalDependencies`

## Test plan

- [x] All 411 tests pass (`bun run test` in `packages/core`)
- [x] Full monorepo build succeeds (`bun run build`)
- [x] TypeScript typecheck passes
2026-04-03 16:06:30 +02:00
Miguel Ángel 1681350ac4 fix(preview): rewrite sub-composition asset urls in styles (#174)
## Summary
- rewrite CSS `url(...)` asset paths from sub-compositions before styles are hoisted into bundled/master preview output
- rewrite standalone sub-composition preview HTML so `src`/`href` paths keep resolving correctly under the preview root `<base>`
- add regression tests for bundled CSS asset rewriting and standalone sub-composition preview rewriting

## Root cause
Standalone composition previews reused the project `<head>` with a preview-root `<base href="/api/projects/:id/preview/">`, but the sub-composition body still contained `../...` asset references. Those escaped the preview route and 404ed. Separately, bundled preview already rewrote `<img src="../...">` paths but left hoisted CSS asset references like `@font-face src: url("../font.woff2")` untouched.

## Validation
- `pnpm --filter @hyperframes/core exec vitest run src/compiler/htmlBundler.test.ts src/studio-api/helpers/subComposition.test.ts`
- `pnpm --filter @hyperframes/core exec tsc --noEmit`
- `pnpm --filter @hyperframes/producer exec tsc --noEmit` *(blocked by pre-existing `packages/producer/src/services/deterministicFonts.ts` importing missing generated file `./fontData.generated.js` in the worktree install)*
- browser verification with `agent-browser` against the local preview server using `/Users/miguel07code/dev/test-hyperframes/heygen-promo`

## Browser proof
Verified the fixed preview routes in-browser after seeking to visible frames:
- standalone composition preview
- bundled master preview
2026-04-01 18:13:38 +02:00
Miguel Ángel 7265d0adfd fix(producer): rewrite relative asset paths when inlining sub-compositions (#166)
## Summary

- **Fixes**: `<img src="../icon.svg">` and similar `../` relative asset references in sub-compositions resolve to 404 after inlining into the root document
- **Unifies**: Both `hyperframes preview` (core bundler) and `hyperframes render` (producer) now use the same shared logic — no duplication

## Root cause

When inlining a sub-composition at `compositions/scene.html` into root `index.html`, a relative path like `../icon.svg` is correct from `compositions/` (it points to project root) but after inlining, `../` escapes the project directory.

## Fix

Extracts path rewriting into a shared `rewriteSubCompPaths.ts` utility in `@hyperframes/core`, used by both the bundler and the producer. **Only rewrites paths starting with** **`../`** — plain relative paths like `assets/foo.svg` are already correct from the root perspective and must not be rewritten (this was the regression cause in `overlay-montage-prod`: sub-composition asset refs like `assets/notch.svg` were incorrectly being rewritten to `compositions/assets/notch.svg`).

## Regression fix

The earlier version of this fix (now in history) rewrote ALL relative paths including `assets/foo.svg`, breaking the `overlay-montage-prod` regression test. This PR fixes that by scoping rewrites to `../`\-prefixed paths only.

## Test plan

- [x] `../icon.svg` in sub-composition renders correctly in both preview and render
- [x] `assets/foo.svg` (no `../`) in sub-composition still resolves correctly — not rewritten
- [x] `overlay-montage-prod` regression test passes
- [x] All other regression shards pass
2026-04-01 00:13:55 +02:00
ef6225da1d feat(core): add fitTextFontSize utility for pixel-accurate text measurement (#152)
Add @chenglou/pretext dependency and fitTextFontSize() utility that uses
canvas measureText to compute the largest font size that fits text within
a given width. Replaces character-count heuristics with actual font-aware
measurement.

- New fitTextFontSize() in @hyperframes/core/text, exposed on window.__hyperframes
- Generalized for all text elements (captions, titles, etc.), not just captions
- Unit tests (mocked pretext) + browser integration test (real Chromium canvas)
- Updated captions skill docs with usage, exit guarantee, and self-lint patterns

Co-authored-by: James <james.russo@heygen.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:14:13 -07:00
Vance Ingalls 20be2ea1c2 style: apply oxfmt baseline formatting across all source files (#25)
## Summary
- Run `oxfmt .` across the entire codebase to establish formatted baseline
- 299 files changed — mechanical formatting only, no logic changes
- Double quotes, semicolons, 2-space indent, trailing commas, 100 print width

Part 3/4 of [VA-851](https://linear.app/heygen/issue/VA-851/pre-migration-configure-eslint-prettier-and-conventional-commits)

## Test plan
- [x] `pnpm format:check` — all 426 files pass
- [x] `pnpm -r typecheck` — all packages pass
- [x] `pnpm build` — all packages build
- [x] All 348 tests pass
2026-03-23 17:15:14 -07:00
Vance IngallsandClaude Opus 4.6 9f8e5ba5a1 initial code (#2)
* feat: initial code port from hyperframes-internal

Port all OSS-ready packages from the internal monorepo:
- @hyperframes/core — shared types, HTML generation, GSAP utilities, runtime
- @hyperframes/cli — CLI for creating, previewing, and rendering compositions
- @hyperframes/engine — framework-agnostic rendering engine (BeginFrame + FFmpeg)
- @hyperframes/producer — video rendering pipeline (Puppeteer + FFmpeg)
- @hyperframes/ui-player — browser-based video player component
- @hyperframes/studio — composition editor (React frontend + Hono backend)

Includes regression test suite with Docker-based test harness.

All HeyGen-internal references, deployment infrastructure, and
proprietary assets have been removed. Package names migrated
from @app/* to @hyperframes/*.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scrub internal codenames and stale references from OSS port

- Replace static.heygen.ai runtime URLs in test fixtures
- Remove internal CDN publish script (publish-hyperframe-runtime.ts)
- Replace sandbox-studio, sandbox-interceptor, __magicEditRuntime
  with neutral names (studio, hyperframe-runtime, __hyperframeRuntime)
- Fix stale Vault API / localhost references in docs
- Remove broken deprecated_studio link

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove remaining internal codenames and stale references

- Delete stale producer README.md and PIPELINE.md (referenced nonexistent files)
- Replace "Cerberus" codename with "HyperFrames" in test design reviews
- Replace magic-edit postMessage identifiers with hf-preview/hf-parent
- Rename debug-magic-edit-timeline.ts to debug-timeline.ts
- Replace "Motion Cut" with "HyperFrames" in Timeline comments
- Fix studio/CLI references to nonexistent archive package
  (use local data/projects/ dir, stub render proxy)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 22:43:56 -07:00