Commit Graph
3000 Commits
Author SHA1 Message Date
James a7d6f7f7dc fix(producer): retain streaming retry after OOM 2026-07-17 19:14:04 -04:00
James e9604270b2 test(producer): preserve parallel plan watchdog coverage 2026-07-17 19:14:04 -04:00
James 6a84e95c68 refactor(producer): make capture plans immutable 2026-07-17 19:14:04 -04:00
James Russo 4a2c9eeedb feat(cli): make cloud archives size-aware (#2622)
## What

Make HyperFrames cloud project archives smaller and easier to diagnose. Cloud rendering and publishing now honor a project-level `.hyperframesignore`, exclude root-level render output directories by default, and expose `cloud render --dry-run` archive diagnostics.

## Why

Real user projects can contain generated snapshots, old renders, and large source assets that are not needed at render time. Those files can push the compressed project beyond the 200 MB direct-upload limit without helping the cloud render.

## How

- Apply safe root-level defaults for `/renders/` and `/snapshots/` while preserving existing development exclusions.
- Parse `.hyperframesignore` with gitignore-compatible comments, globs, and negation.
- Add `cloud render --dry-run` and JSON output for compressed size, file count, upload-limit status, and the ten largest included files.
- Improve upload-size errors and document a reference-audit workflow for agents before adding narrow ignore rules.
- Share the filtered archive implementation with `hyperframes publish`.

## Test plan

- [x] Unit tests added/updated
- [x] Manual testing performed
- [x] Documentation updated (if applicable)

Validation:
- `bun run test` in `packages/cli`: 2,068 passed, 2 skipped.
- `bun run lint`: passed repository lint, tracked-artifact, workspace-contract, skills, and mirror checks.
- `bun run typecheck` in `packages/cli`: passed.
- Manual dry-run against `batch-api-launch`: archive reduced from approximately 201 MB to 95,688,226 bytes (103 files), below the 200 MB limit.
2026-07-17 19:07:53 -04:00
James e73304fb0e feat(cli): make cloud archives size-aware 2026-07-17 18:44:58 -04:00
James Russo 43b52f7d6e Merge pull request #2162 from heygen-com/07-10-refactor_producer_unify_render_requests
refactor(producer): unify render requests
2026-07-17 18:07:16 -04:00
James 1a66c881b8 fix(producer): preserve render request config contracts 2026-07-17 17:22:34 -04:00
James fc4e9a6c0e fix(producer): validate render request engine snapshots 2026-07-17 16:22:23 -04:00
James 25dd4cc8c0 fix(producer): omit absent render request options 2026-07-17 16:22:23 -04:00
James c251db02d0 fix(producer): validate render request wire contracts 2026-07-17 16:22:23 -04:00
James 65f2e2927c refactor(producer): unify render requests 2026-07-17 16:22:23 -04:00
Miguel Ángel 2577aaffeb fix(studio): ignore stale failed sidecars for existing renders (#2621)
* fix(studio): ignore stale failed sidecars for existing renders

* style: format stale render metadata test
2026-07-17 16:21:33 -04:00
Somansh Reddy 7acabbcdeb docs(guides): resolve the Send-to fidelity contradiction (preserve substance, adapt form) (#2620)
* docs(guides): resolve the fidelity contradiction (preserve substance, adapt form)

The guide called the rebuild 'lossy by nature' while the checklist demanded 'content
match the brief exactly', with no rule for what to preserve vs adapt. On a live import
CD kept fonts/palette/hero but genericized the source's real figures (2.4M signals/sec,
+240% spike) into vague phrases. Resolve around one principle: substance (real copy,
exact palette/fonts, distinctive figures/data, product names, signature visuals) is
preserved verbatim; only the form (static page to timed multi-scene motion) adapts.

* test(guides): pin the Send-to fidelity contract against silent regression

Address review on #2620: the resolved 'preserve substance, adapt form' instruction is
load-bearing LLM-facing prompt text, but validate-docs only proves syntax. Add a semantic
pin (mirroring packages/cli figma skillContent.test.ts) asserting the three positive
concepts are present and the two retired contradictory phrases ('lossy by nature',
'content match the brief exactly') cannot silently return.

* style(guides): oxfmt the Send-to fidelity-contract test

Wrap the readFileSync call to oxfmt form (printWidth 100); unblocks required Format + its
preflight/preview-regression cascade on #2620.
2026-07-17 12:30:48 -07:00
Miguel Ángel 209784ab27 fix(producer): credit held video tails in coverage gate (#2606)
* fix(producer): credit held video tails in coverage gate

* test(producer): decouple makeExtracted's durationSeconds default from delivered frame count

Defaulting durationSeconds to delivered/fps made any test modeling a
delivery shortfall silently report full coverage unless it remembered
to override durationSeconds afterward. Default to Infinity instead so
callers fall into the 'no usable source duration' branch (full slot
required) unless they explicitly pass a duration.
2026-07-17 14:16:17 -04:00
Miguel Ángel 2b65b4efce fix(studio): harden composition timeline reliability (#2615)
* fix(studio): preserve composition playback continuity

* feat(studio): drag compositions into the timeline

* fix(studio): collapse expanded composition move aliases

* fix(studio): make timeline cuts atomic

* fix(studio): group inspector gesture history

* test(studio): cover masked text selection

* fix(studio): harden composition timeline reliability

* fix(studio): satisfy CI source gates

* fix(studio): harden composition mutation requests
2026-07-17 14:15:30 -04:00
Miguel Ángel 2be8a62c00 fix(engine): stop compositing phantom duplicates on captureBeyondViewport (#2607)
* fix(core): stop the async media-metadata rebind once render capture starts seeking

scheduleMetadataDurationHydration re-resolves and can swap the captured
GSAP timeline off a debounced loadedmetadata/durationchange event, fully
uncoordinated with the producer's own per-frame renderSeek calls. When a
full-length <video>'s metadata resolves after capture has already begun
(slow I/O, Docker), this races the deterministic BeginFrame capture loop
and can reflow sub-composition state mid-render, producing phase-offset
duplicate content in captured frames (#2550).

Render-mode duration correction already happens deterministically during
the probe stage before capture starts, so once renderSeek has been called
once there is nothing left for this self-correction to do — gate it off
for the rest of the session.

* fix(core): scope the metadata-rebind guard to actual render/export pages

renderSeek isn't capture-exclusive — Studio's own preview iframe falls
back to it for compositions whose timeline overhangs every native
adapter's duration. Gating the HF#2550 fix on renderCaptureSeekStarted
alone silently disabled the metadata-driven duration self-correction for
that live-scrub case too, where it's still needed. Require the render/
export page signal (window.__HF_EXPORT_RENDER_SEEK_CONFIG, set only by
the producer's fileServer.ts) alongside it, and add a regression test
covering the Studio-preview case.

* fix(engine): stop requesting beyond-viewport capture for video comps that don't need it

Root-caused HF#2550 by reproducing the reporter's public repro end-to-end
(not just the timeline-rebind mechanism from the earlier commits in this
branch) on native Linux: instrumented the actual DOM state during a real
capture session and confirmed the sub-composition never double-mounts —
getBoundingClientRect and the timeline's own local time both match the
single, correct DOM tree throughout. The phantom second copy only exists
in the captured screenshot pixels.

Bisected it to captureBeyondViewport: resolveVideoCaptureBeyondViewport
(#1094's tall-portrait fix) forces `Page.captureScreenshot`'s beyond-viewport
path on for any render with a native <video>, regardless of whether the
page's content actually overflows the declared capture height. On
SwiftShader that beyond-viewport path can composite a stale, vertically
offset paint of the page alongside the fresh one for content that fits
entirely within the viewport — producing exactly the reported phase-offset
duplicate. Disabling captureBeyondViewport (repro's video still present)
eliminates the duplicate outright; re-enabling it reproduces the duplicate
byte-for-byte, isolating it as the actual cause.

Adds pageContentExceedsCaptureHeight, a ground-truth measurement of the
page's actual scrollHeight against the requested capture height, and wires
it into initializeSession to downgrade captureBeyondViewport back to false
once the page is settled and it's confirmed unnecessary — the "reliable
clip predictor" the original #1094 fix's ponytail comment flagged as
missing. This keeps #1094's fix intact for content that genuinely
overflows while closing the SwiftShader ghosting hazard for the (common)
case of video that fits inside its own viewport.

* test(producer): add HF#2550 video+sub-composition regression fixture

Checks in the reporter's confirmed real-world reproduction (media
regenerated via ffmpeg testsrc2, matching their public repro repo) as a
regression fixture, with a golden baseline rendered against the fix.

Verified end-to-end via the project's own Docker regression harness:
- Rendering this fixture with the fix produces the golden baseline
  (clean, single flowchart instance, captureBeyondViewport correctly
  downgraded).
- Direct CLI renders (not through this harness) against unpatched code
  reproduce the reported phantom-duplicate artifact reliably (10/10).

Caveat documented in meta.json: the underlying bug is timing-dependent.
Two harness runs against unpatched code, using this same fixture, did
not reproduce the artifact (0/2) — the harness's in-process render path
apparently doesn't hit the same race window a direct CLI process does on
this host. This fixture is a best-effort regression guard and a
preserved real-world repro, not the sole protection — the deterministic
guard is packages/engine/src/services/screenshotService.test.ts's
pageContentExceedsCaptureHeight unit tests, which exercise the actual
fix logic directly.

Also adds an .gitattributes LFS rule for this fixture's source
index.html (744 KB — carries the real project's embedded base64
assets, over the largefiles hook's 500 KB non-LFS limit).

* fix: route HF#2550 fixture binaries through LFS (were committed raw)

filter.lfs.clean/smudge were locally configured as a no-op "cat" in
this repo's shared .git/config, silently disabling LFS filtering for
every worktree. The previous commit's large binaries (output.mp4,
compiled.html, source index.html, source video) landed as raw blobs
instead of LFS pointers as a result. Ran `git lfs install --local
--force` to restore the correct filter commands, then re-staged the
affected files so they commit as proper LFS pointers.

* fix(engine): address capture viewport review feedback
2026-07-17 11:59:45 -04:00
Somansh Reddy 8bfc676881 docs(guides): redirect Send-to imports to the Send-to guide (#2619)
* docs(guides): redirect Send-to imports to the Send-to guide

Claude Design's only doc-discovery tool is web search, which ranks this Template-First
(Download-ZIP) guide first; the Send-to guide is not indexed, so Send-to imports get
authored against the wrong flow. A body link here lets CD's fetch of this page unlock a
fetch of the correct guide (its URL now appears in a prior fetch result).

* docs(guides): add Send-to redirect note to the Claude Design landing page

The /guides/claude-design landing page (claude-design.mdx) is the indexed nav page and
points only at the Download-ZIP instruction file; a Send-to author landing here got no
pointer to the Send-to guide. Mirror the redirect from claude-design-hyperframes.md.
2026-07-17 08:49:21 -07:00
James Russo ad02d00b8d Merge pull request #2161 from heygen-com/07-10-refactor_engine_type_capture_failures
refactor(engine): type capture failures
2026-07-17 11:46:57 -04:00
James 5da9f7ab3d fix(engine): avoid polynomial capture failure regex 2026-07-17 05:22:53 -04:00
James acb3d81b99 refactor(engine): type capture failures 2026-07-17 04:10:30 -04:00
Vance Ingalls c268f5ba85 chore: release v0.7.61 v0.7.61 2026-07-17 01:07:09 -07:00
Miguel Ángel 0a66671fc5 feat(media-use): add video generation (HeyGen avatar-video + local LTX fallback) (#2614)
* fix(media-use): tag HeyGen TTS generation with attribution header

Centralizes the X-HeyGen-Client-Source header into HEYGEN_CLIENT_SOURCE_ARGV
in heygen-cli.mjs and reuses it in heygen-search.mjs (dropping the duplicated
inline literal) so voice-provider's `voice speech create` call carries it too.
The generation call was previously untagged, making media-use TTS usage
invisible in HeyGen's billing/analytics warehouse; the read-only `voice list`
discovery call intentionally stays untagged.

* feat(media-use): add local LTX video generate provider

* feat(media-use): add HeyGen avatar-video generate provider

* feat(media-use): register video as a real provider type

* docs(media-use): document the wired video type and full HeyGen tagging coverage

resolve --type video is now the default path (HeyGen avatar video first,
local LTX fallback, sign-in nudge on auth failure) instead of a manual
recipe; correct the claim that only search requests are tagged now that
TTS and avatar-video generation carry the attribution header too.

* fix(media-use): wire --avatar-id/--voice-id CLI flags and close video-provider auth/cache gaps

- resolve.mjs never implemented the --avatar-id/--voice-id override that
  operations.md documented, so following the docs crashed with
  ERR_PARSE_ARGS_UNKNOWN_OPTION; wire the flags through to ctx.
- defaultAvatarId/defaultStarfishVoiceId cached a failed discovery lookup
  as a permanent null, disabling heygen.video after one transient miss;
  cache only a truthy id, matching the same fix in voice-provider.mjs's
  defaultVoiceId.
- the avatar-video onboarding nudge only fired on a video-create failure,
  never when avatar/voice discovery itself was unauthenticated (the
  common unauthenticated case) -- propagate the discovery failure reason
  so onboarding fires either way.
- dedupe the CLI-shelling JSON helper (heygen-cli.mjs's new runHeygenJson)
  and the local-model argv-template builder (local-models.mjs's new
  buildArgv) instead of leaving byte-identical copies in each provider.

* fix(media-use): address avatar-video PR review feedback

- heygenVideoGenerate short-circuits after the first discovery-call
  failure instead of always attempting both avatar list and voice list,
  so an unauthenticated caller gets one onboarding message and one
  provider-error telemetry ping instead of a double-fire.
- runHeygenJson logs a diagnostic when a CLI call succeeds but returns
  unparseable JSON, instead of silently returning null.
- dedupe the "avatar video is free" onboarding string into one constant
  (was duplicated across three call sites).

* fix(media-use): match review-requested naming and message conventions

- export AVATAR_VIDEO_SIGNIN_MESSAGE from heygen-video-provider.mjs so
  the test imports the canonical string instead of redeclaring it.
- runHeygenJson's non-JSON diagnostic now matches heygen-search.mjs's
  existing wording ("returned non-JSON output").
2026-07-17 03:56:57 -04:00
Vance Ingalls f084a7217d Merge pull request #2502 from heygen-com/task4-start-resolver-export
feat(core): export runtime start resolver with documentRef parameter
2026-07-17 00:47:42 -07:00
Vance Ingalls ac51891eb0 Merge pull request #2600 from heygen-com/fix/studio-slideshow-tab-gate
fix(studio): hide Slideshow tab for non-slideshow comps + default the flat inspector on
2026-07-17 00:46:50 -07:00
Vance Ingalls 1ddf9cc331 Merge pull request #2563 from heygen-com/via/thumbnail-id-escape
fix(studio,runtime): CSS.escape ids so digit-leading selectors don't crash
2026-07-17 00:43:31 -07:00
Vance Ingalls a7c0fa4fe5 Merge pull request #2564 from heygen-com/via/win-workdir-env
feat(cli): surface extract-cache dir in doctor + add --frames-cache-dir sugar
2026-07-17 00:43:06 -07:00
Vance Ingalls cf64f24e63 Merge pull request #2569 from heygen-com/via/macos12-dyld-crash-hint
fix(cli): surface HYPERFRAMES_BROWSER_PATH hint on macOS <13 chrome-headless-shell dyld crash
2026-07-17 00:40:14 -07:00
Vance Ingalls 53b3621437 fix(studio): short-circuit slideshow-island detection with a substring check (review N2)
slideshowIslandRegex scanned the full file content on every editingFile
change even for the common non-slideshow case. Gate it behind a plain
substring check on SLIDESHOW_ISLAND_TYPE first — cheap, and avoids the
full-content RegExp pass for files that plainly have no island.

Added a test for the still-open behavior this preserves: a malformed
island (invalid JSON) still trips the substring check and the regex,
so the tab stays discoverable rather than silently disappearing.
2026-07-17 00:37:39 -07:00
Vance Ingalls a4167ede07 feat(studio): default the flat inspector on
Flip STUDIO_FLAT_INSPECTOR_ENABLED's default from false to true — the
bug-fix pass on the flat inspector is complete (right-aligned values,
Stroke width/style split, promote-badge overlap, Layout/Style section
gating for non-visual elements like audio). VITE_STUDIO_FLAT_INSPECTOR_ENABLED=false
still opts back into the legacy panel.

Updates the two tests that asserted the old false default: the flag's
own default test, and the "classic PropertyPanel input coverage" suite,
which relied on that default to reach the legacy panel and now mocks it
explicitly (mirroring the adjacent "flat" suite's existing pattern).
2026-07-17 00:37:39 -07:00
Vance Ingalls 67696cd8de fix(studio): hide the Slideshow tab and panel for non-slideshow compositions
The Slideshow tab rendered unconditionally, showing the branching editor
for any composition regardless of whether it was actually a slideshow —
a plain video comp offered a tab with nothing meaningful to edit.

Gate it on the composition carrying the slideshow JSON island
(<script type="application/hyperframes-slideshow+json">), the same
definitive marker the CLI's `present` command already requires (it
refuses to run without one). Presence-only, not full manifest
validation, so a malformed island still surfaces the tab rather than
disappearing entirely. Also bounce rightPanelTab off "slideshow" to
"renders" if the active composition stops being a slideshow while that
tab is open (e.g. switching files), since its button would otherwise
vanish with no way back to it.

Extracted the gating + scene-list derivation into useSlideshowTabState
to keep StudioRightPanel.tsx under the 600-LOC gate.
2026-07-17 00:37:39 -07:00
Vance Ingalls 8eccc6e9a3 Merge pull request #2599 from heygen-com/via/composition-structure-mandate
feat(skills): add COMPOSITION_STRUCTURE to feedback repro packet + soft-warn lint
2026-07-17 00:36:36 -07:00
Miguel Ángel e8371a7acc feat(media): alpha-capable authoring proxies (#2598)
* feat(media): alpha-capable authoring proxies

Alpha sources were refused a proxy before the codec map ever asked whether the
browser could decode them, so a ProRes 4444 alpha file (which no browser
previews at all) rendered black forever, while an alpha WebM (which previews
fine) was already covered by the browser-safe check on the next line. The alpha
veto earned nothing and cost the one case that needed help.

Alpha is now a target-codec choice rather than a veto: alpha sources transcode
to VP9 + yuva420p in WebM, everything else keeps the existing H.264/MP4 path
byte for byte. Only files no browser can preview are proxied, which is the rule
the runtime already followed everywhere else.

WebM cannot carry AAC, so the VP9 path uses Opus and drops the MP4-only
faststart flag. PROXY_PARAMS_VERSION moves to v3 so clients stop serving the
previously cached proxies.

Safari does not decode VP9 alpha and still shows black for alpha sources, as it
does today: this is better on Chromium and Firefox and no worse anywhere.

* fix(media): infer proxy variant for rescue

* fix(media): preserve alpha proxy hardening after restack
2026-07-17 03:26:55 -04:00
Miguel Ángel 8c1b6c5154 docs(media): document automatic proxying for hostile codecs (#2596)
* docs(media): document automatic proxying for hostile codecs

Describes the shipped behavior: which input codecs render, that live preview
auto-proxies what the browser cannot decode, where the cache lives, and how to
turn it off. Carries the skills notes and the hardening design documents.

* docs(media): align proxy guidance with runtime
2026-07-17 03:01:56 -04:00
Miguel Ángel 35eff5038b feat(cli): bake proxies into published archives (#2595)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly

* feat(cli): let projects opt out of automatic proxying

Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and
forwards the resolved value into the studio and preview servers and the vite
adapter. Lands before the runtime slice that turns auto-proxying on, so the
switch exists before there is any behavior to switch off.

* fix(cli): align media config schema

* feat(core): swap undecodable video to its proxy at runtime

Adds the browser-side half: before first load the runtime consults the injected
codec map and swaps a hostile source to its proxy, and if a video still reports
zero decodable width it rescues it reactively. An HEVC file carrying AAC fires
no error event, so zero videoWidth, not the error event, is the reliable signal.
Audio elements and alpha sources are never proxied, render mode never proxies,
and each swap evicts the element's stale sync state and reports once.

This completes the loop: auto-proxying is live for preview and studio from here.
The opt-out (media.autoProxy, --no-proxy) shipped in the previous slice.

* feat(cli): serve proxies from play, present, and the static project server

Adds proxy negotiation to the CLI-side servers and gives play byte-range
serving it never had, so a swapped video can seek. The static project server
behind check, snapshot, compare and friends injects the codec map once, so all
of its callers inherit the behavior; snapshot forwards its own proxy flag.

* fix(cli): serve proxies for camera formats

* feat(cli): resolve proxies before check's timed browser phase

check pre-resolves hostile assets so a cold transcode cannot exhaust the
render-ready budget, and surfaces the runtime's proxy diagnostics as findings
so a swap is visible rather than silent.

* feat(cli): bake proxies into published archives

Published pages are static, so there is no server to negotiate with: publish
transcodes proxies for hostile assets into the archive and rewrites the video
sources that point at them. Audio elements keep their originals, since audio
decodes independently of the video codec.

Splits the archive build from the zip step so publish can transform between
them. cloud render keeps calling the unchanged composition and still uploads
originals, which its regression test pins.

* fix(cli): harden proxy pre-resolution

* fix(cli): surface publish proxy outcomes

* test(cli): remove ffmpeg from archive guard

* test(cli): normalize publish fixture path
2026-07-17 02:50:28 -04:00
Xuanru Li 4ad582606b feat(lint): flag relative-value second writers and tl.set initial hides (#2612)
## What

Part 2 of the GSAP seek-safety rules (stacks on #2611): the two rules that touch existing catalog content and required reconciliation with an existing rule.

- `gsap_relative_value_second_writer` (error) — a relative var value (`y: "-=15"`) on a property whose target has another writer **active at the relative tween's start**. The relative base is captured at tween init, which reads a different partial state per seek path: sequential seek inits it mid-entrance, a cold render worker inits it at the entrance's end state, and the element teleports at chunk boundaries (production case: all scene nodes jumping ~20px mid-scene). Writers that complete strictly before the start are safe (children render in start-time order within a seek pass — verified against gsap 3.15.0) and are not flagged; neither are single-writer relatives, `from()`/`fromTo()`, build-time `gsap.set`, or relative position parameters (`"+=0.5"`). Selector resolution bails on combinators and cross-composition scoping rather than guessing. Findings aggregate per tween pair and report the overlap window.
- `gsap_timeline_set_initial_hide` (warning) — initial-state hiding via `tl.set(target, vars, 0)` on a paused timeline is not rendered while the playhead sits at exactly 0, so frame 0 shows the unhidden state (verified against gsap 3.15.0: opacity stays 1 after `tl.time(0)`, applies only past 0). Exempt when the target is already hidden by authored CSS/inline styles or a standalone `gsap.set()`, and only sets preceding every tween in source order qualify (mutated position variables resolve to their initial binding in the parser — outro hard-kills don't masquerade as position-0 sets).
- Reconciliation: `gsap_fullscreen_overlay_starts_visible`'s fixHint previously recommended exactly the flagged `tl.set(sel, {opacity:0}, 0)` pattern; it now recommends authored CSS hiding or immediate `gsap.set()`.
- Docs for the full rule family in `docs/packages/lint.mdx`.

## Corpus impact (the reason this is its own PR)

These two rules are the ones that fire on repo-shipped content:

- `gsap_relative_value_second_writer`: 4 errors in `gooey-metaball`, all genuine overlaps. Measured with gsap 3.15.0: ballD diverges **3.31 xPercent / 1.99 yPercent (~8px/5px at 240px ball size)** between sequential and cold seek — a permanent base offset that appears as a teleport at a chunk boundary. Real but modest; happy to fix the block in a follow-up (start the drift at the entrance's end, or use absolute `fromTo`).
- `gsap_timeline_set_initial_hide`: 10 warnings across the catalog after the CSS-hidden exemption (down from 54 pre-narrowing); spot-checked as genuine frame-0 pops with no authored hide (e.g. `vfx-text-cursor` `#phrase-b`).

Adversarially reviewed the same way as #2611 (393-composition corpus + gsap semantics experiments); FP classes fixed and locked as negative tests: precede-only second writers, descendant/cross-composition selector mis-joins, CSS-hidden re-assertions, mutated position variables.

## Tests

Full `packages/lint` suite green at 440 tests including multi-composition roots; `tsc`, oxlint, fallow audit clean.
2026-07-16 23:49:45 -07:00
Xuanru Li f3d2100663 feat(lint): seek-order safety and SVG draw-on rules for GSAP timelines (#2611)
## What

Five lint rules (plus one extended core pattern) for GSAP defect classes that pass every existing check but break rendered output — the narrow, corpus-clean half of what was originally one PR (split per review; part 2 with the two catalog-touching rules stacks on top as #2612).

- `gsap_repeat_refresh_relative_value` (error) — `repeatRefresh: true` + relative value re-captures and accumulates per iteration; a cold seek into iteration N skips the accumulation (verified with gsap 3.15.0: sequential 47.5 vs cold 17.5).
- `gsap_function_value_hazard` (error/warning) — function values that call a method on the first parameter (GSAP passes `(index, target, targets)` — the first param is a number, so `(el) => el.getTotalLength()` throws and aborts the seeked frame) or measure the DOM: transform-sensitive reads (`getBoundingClientRect`, `getComputedStyle`, `gsap.getProperty`) are errors; transform-invariant layout reads (`offsetWidth`, `getBBox`, ...) are warnings. Pure-index arithmetic, `gsap.utils.wrap/distribute`, dataset/attribute reads, and closures over build-time constants are exempt.
- `gsap_callback_dom_measurement` (warning) — DOM layout measurement reachable from `tl.add()`/`tl.call()`/`eventCallback`/`onStart|onUpdate|...` via a two-hop named-function scan. The capture path seeks with `suppressEvents: false`, so callbacks re-fire on every seek and measured geometry is seek-order-dependent. `gsap.getProperty`-driven derived output (scramble/typewriter patterns) is exempt.
- `svg_measure_before_path_d` (error/warning) — `getTotalLength()` on a `<path>` with no static `d`: error when no `d` assignment exists anywhere (returns 0 in Chrome, silently killing dash animations); warning when assignments exist only inside function bodies. Recognizes `setAttribute`, GSAP `attr: { d }`, and CSS `d: path()`.
- `svg_drawon_css_dasharray_conflict` (error) — GSAP `strokeDasharray` on an element whose CSS declares a multi-component `stroke-dasharray`. GSAP merges per component, so `strokeDasharray: pathLength` computes to `"641.4px, 10px"` — the gap stays 10px, the hide-then-reveal hides only 10px, and the line stays visible all scene with a crawling notch. One of this repo's own producer fixtures has this exact bug (true positive from the corpus run).
- `gsap.utils.random()` and `"random(...)"` string tween values added to `non_deterministic_code` (core) — each worker inits independently, so the same tween resolves different randoms across chunks.

Both motivating production bugs (nodes teleporting at chunk boundaries; a draw-on line visible all scene) are minimally reproduced in the tests.

## Review hardening

Two independent adversarial reviews ran before submission: a false-positive hunt over all 393 compositions in this repo plus 23 constructed adversarial snippets (with gsap 3.15.0 semantics experiments), and a maintainer-conventions pass. Fixed FP classes are locked in as negative tests: all-interpolation template ids, GSAP attr-plugin `d` writes, getProperty-driven callbacks, transform-invariant marquee reads.

Corpus residue for these five rules: **1 error (a genuine dasharray bug in a producer fixture, happy to fix in a follow-up) and 2 warnings** (real layout reads in callbacks).

## Tests

`packages/lint` green at this commit in isolation; `tsc`, oxlint, fallow audit clean.

## Notes for reviewers

- All rules follow the file's conservative philosophy: anything not statically resolvable is skipped; false negatives over false positives.
- Open question: should the cold-seek family gate on `HyperframeLinterOptions.distributed` (error when distributed, warning otherwise), following the `system_font_will_alias` precedent? Happy to wire either way.
2026-07-16 23:22:18 -07:00
Via 490642b78a fix(feedback-lint): tighten census value scoping and defect-keyword matching
Addresses C1-C7 from Rames's adversarial review + N2/N3 nits:

- C1 (positionFixed): inline probe now value-scopes to 'fixed' — previously
  fired on any position value (absolute, relative, sticky), producing a
  false-positive anatomy that would mislead maintainers pattern-matching
  the 'sub-comp + position:fixed capture' bug family.

- C2 (overflowHidden): symmetric fix — inline path now catches
  style='overflow: hidden' via the value-scoped probe, matching the
  <style>-tag branch. Also handles overflow-x/overflow-y variants.

- C3 (VISUAL_DEFECT_KEYWORDS): drop 'render' — CLI's primary command is
  'hyperframes render', so build/perf/hang reports were triggering an
  inappropriate COMPOSITION_STRUCTURE: nudge on the most common failure
  mode. Rely on the more specific tokens (black, blank, flicker, corrupt,
  wrong frame) to identify actual visual defects.

- C4 (mentionsVisualDefect): compile keywords into a word-bounded regex.
  'blackboard', 'blanket', 'visualize', 'corruptible' no longer false-
  positive. Accepted tradeoff: plural forms ('flickers') don't match.

- C5 (marker case-normalization): REPRO COMMAND: / COMPOSITION_STRUCTURE:
  checks now case-insensitive, matching mentionsVisualDefect's
  normalization. Reporters using 'Repro command:' or lowercase
  'composition_structure:' get credit for compliance.

- C6 (background/mask shorthand): inline branch previously required the
  longhand 'background-image:' / 'mask-image:' — style='background:
  url(bg.png)' silently returned false. Now checks both longhand AND
  shorthand-with-url() inline forms.

- C7 (usesGsap docstring): trim promise of data-gsap-* attribute scanning
  that detectGsap never implemented — attribute scan lives outside the
  <script>-only detection path.

- N2 (EMPTY_VALUES): include 'inherit', 'revert', 'revert-layer' — a
  style='position: inherit' is authored intent to defer, not authored
  intent to place.

- N3 (input size cap): early-exit to a zero census on HTML > 20 MB
  rather than feeding linkedom a hostile input. Not expected in normal
  usage; guard for future callers that might pass raw user uploads.

Extends the test locks: 6 new census tests (value-scoping, size cap) and
5 new lint tests (word-boundary rejections, render-noise rejections,
lowercase-marker acceptance). All existing tests unchanged in intent —
only the 'flickers' plural in one test updated to 'flicker' to reflect
the new word-boundary rule.

No behavior change to the wire path: lint is still soft-warn, census is
still never called from feedback.ts, no new dependencies.
2026-07-17 06:22:00 +00:00
Miguel Ángel 0561adc11c feat(cli): resolve proxies before check's timed browser phase (#2594)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly

* feat(cli): let projects opt out of automatic proxying

Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and
forwards the resolved value into the studio and preview servers and the vite
adapter. Lands before the runtime slice that turns auto-proxying on, so the
switch exists before there is any behavior to switch off.

* fix(cli): align media config schema

* feat(core): swap undecodable video to its proxy at runtime

Adds the browser-side half: before first load the runtime consults the injected
codec map and swaps a hostile source to its proxy, and if a video still reports
zero decodable width it rescues it reactively. An HEVC file carrying AAC fires
no error event, so zero videoWidth, not the error event, is the reliable signal.
Audio elements and alpha sources are never proxied, render mode never proxies,
and each swap evicts the element's stale sync state and reports once.

This completes the loop: auto-proxying is live for preview and studio from here.
The opt-out (media.autoProxy, --no-proxy) shipped in the previous slice.

* feat(cli): serve proxies from play, present, and the static project server

Adds proxy negotiation to the CLI-side servers and gives play byte-range
serving it never had, so a swapped video can seek. The static project server
behind check, snapshot, compare and friends injects the codec map once, so all
of its callers inherit the behavior; snapshot forwards its own proxy flag.

* fix(cli): serve proxies for camera formats

* feat(cli): resolve proxies before check's timed browser phase

check pre-resolves hostile assets so a cold transcode cannot exhaust the
render-ready budget, and surfaces the runtime's proxy diagnostics as findings
so a swap is visible rather than silent.

* fix(cli): harden proxy pre-resolution
2026-07-17 02:05:25 -04:00
Miguel Ángel a6df35f891 fix(gcp-cloud-run): include all workspace build dependencies (#2608)
Fixes #2601.

Reproduced v0.7.60 from the tag: Docker frozen install failed because five workspace manifests were omitted; after adding manifests, a fresh build failed because dependency dist artifacts were absent.

Changes:
- Copy all workspace manifests and required source trees.
- Build core, parsers, lint, SDK, sdk-playground, studio-server, and engine before producer/adapter.
- Add deterministic manifest/source coverage check.

Verification:
- `bun run --cwd packages/gcp-cloud-run test:dockerfile-workspaces`
- `docker build --progress=plain -f packages/gcp-cloud-run/Dockerfile -t hf-2601-fixed3 .` (success; image built).

Source issue: https://github.com/heygen-com/hyperframes/issues/2601
2026-07-17 01:18:52 -04:00
James Russo 57d3bf4960 refactor(engine): manage child process lifecycles (#2160)
* refactor(engine): manage child process lifecycles

* fix(engine): preserve child reaping after runtime errors

* fix(engine): untrack child processes on exit
2026-07-17 01:17:53 -04:00
Miguel Ángel 8e162921bc fix(cli): resolve the producer alias in the CLI vitest config (#2609)
`main`'s Test job is red: studioServer.test.ts fails to collect with "Failed to
resolve entry for package @hyperframes/producer".

studioServer.ts has long carried `await import("@hyperframes/producer")`, but
nothing under test imported that module until #2591 added a suite that does.
Vite's import analysis resolves the specifier at transform time, and the CI test
job builds only parsers, lint, studio-server and core, so the package has no
dist to resolve and the whole file fails to collect. It passes locally only
because a full `bun run build` happens to build the producer first.

The tsup build already aliases this specifier to the producer's source, because
the CLI bundles the producer rather than depending on it at runtime. The test
config now resolves it the same way, so the tooling agrees with the build. The
neighbouring @hyperframes/core alias exists for the same class of reason.

Config only: no source change, so the shipped bundle still inlines the producer
and installed users are unaffected.

Verified by reproducing the CI condition locally (producer dist removed):
studioServer.test.ts fails without this change and passes with it, and the built
cli.js still contains no runtime import of @hyperframes/producer.
2026-07-16 23:43:04 -04:00
Paweł b44bc4ffaf fix(registry): position flowchart typing beats with label-relative offsets (#2549)
The flowchart and flowchart-vertical blocks read tl.labels["hold5"] back
from the timeline to schedule the "Pythom" -> "Python" typing correction.
The render compiler executes composition scripts under a GSAP proxy whose
timeline exposes no readable .labels map, so this line throws
("Composition script failed ... Cannot read properties of undefined
(reading 'hold5')") before window.__timelines is assigned. The block is
never registered: renders show only the dotted-grid background and every
render pays the 45s sub_timeline_readiness_timeout.

Use GSAP's own label-relative position syntax ("hold5+=<offset>") instead,
matching every other step in these files. Identical timing, no .labels read.

Verified with hyperframes@0.7.60 render: block was blank before, animates
after; the readiness timeout disappears.
2026-07-16 23:17:15 -04:00
Via 8f90fd9ec1 refactor(feedback): extract lint-warning loop to satisfy Fallow CRAP threshold
The added `for...of` loop over `lintFeedbackComment` warnings pushed the
`run` function's cyclomatic complexity from 4 to 5, landing the CRAP
score at exactly the 30.0 Fallow threshold. Extract the loop into
`printFeedbackLintWarnings` so `run` stays a flat driver — the helper
carries the incidental complexity.

No behavior change; all 29 unit tests + typecheck + oxlint + oxfmt +
local `fallow audit --base origin/main` pass clean.
2026-07-17 03:12:04 +00:00
Miguel Ángel 74b4f1e8c3 feat(cli): serve proxies from play and the static project server (#2593)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly

* feat(cli): let projects opt out of automatic proxying

Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and
forwards the resolved value into the studio and preview servers and the vite
adapter. Lands before the runtime slice that turns auto-proxying on, so the
switch exists before there is any behavior to switch off.

* fix(cli): align media config schema

* feat(core): swap undecodable video to its proxy at runtime

Adds the browser-side half: before first load the runtime consults the injected
codec map and swaps a hostile source to its proxy, and if a video still reports
zero decodable width it rescues it reactively. An HEVC file carrying AAC fires
no error event, so zero videoWidth, not the error event, is the reliable signal.
Audio elements and alpha sources are never proxied, render mode never proxies,
and each swap evicts the element's stale sync state and reports once.

This completes the loop: auto-proxying is live for preview and studio from here.
The opt-out (media.autoProxy, --no-proxy) shipped in the previous slice.

* feat(cli): serve proxies from play, present, and the static project server

Adds proxy negotiation to the CLI-side servers and gives play byte-range
serving it never had, so a swapped video can seek. The static project server
behind check, snapshot, compare and friends injects the codec map once, so all
of its callers inherit the behavior; snapshot forwards its own proxy flag.

* fix(cli): serve proxies for camera formats
2026-07-16 23:02:01 -04:00
Miguel Ángel 39b588cbd0 feat(core): swap undecodable video to its proxy at runtime (#2592)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly

* feat(cli): let projects opt out of automatic proxying

Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and
forwards the resolved value into the studio and preview servers and the vite
adapter. Lands before the runtime slice that turns auto-proxying on, so the
switch exists before there is any behavior to switch off.

* fix(cli): align media config schema

* feat(core): swap undecodable video to its proxy at runtime

Adds the browser-side half: before first load the runtime consults the injected
codec map and swaps a hostile source to its proxy, and if a video still reports
zero decodable width it rescues it reactively. An HEVC file carrying AAC fires
no error event, so zero videoWidth, not the error event, is the reliable signal.
Audio elements and alpha sources are never proxied, render mode never proxies,
and each swap evicts the element's stale sync state and reports once.

This completes the loop: auto-proxying is live for preview and studio from here.
The opt-out (media.autoProxy, --no-proxy) shipped in the previous slice.
2026-07-16 23:01:39 -04:00
Miguel Ángel 6458807066 feat(cli): let projects opt out of automatic proxying (#2591)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly

* feat(cli): let projects opt out of automatic proxying

Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and
forwards the resolved value into the studio and preview servers and the vite
adapter. Lands before the runtime slice that turns auto-proxying on, so the
switch exists before there is any behavior to switch off.

* fix(cli): align media config schema
2026-07-16 23:01:14 -04:00
Miguel Ángel 67eab59f44 feat(studio-server): serve H.264 proxies from the preview route (#2590)
* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly
2026-07-16 23:00:48 -04:00
Via 0aaac7aa30 feat(skills): add composition-structure block + soft-warn feedback lint
Extend the CLI feedback reproduction packet (#2498) with a fifth
mandated field, `COMPOSITION_STRUCTURE:`, and enforce presence of
`REPRO COMMAND:` / `COMPOSITION_STRUCTURE:` at feedback-submit time.

- Skill + reference now specify `COMPOSITION_STRUCTURE:` — a
  privacy-preserving structural anatomy (element census + attribute
  presence + timeline shape + delta + defect location) — required for
  any rating <=7 that describes a visual defect.
- `buildCompositionCensus()` + `renderCompositionCensusBlock()`
  auto-fill the block from composition HTML so agents don't ask the
  human user to hand-count `<video>` / `<img>` / sub-comp mounts.
  Counts + presence flags only — no file paths, no src URLs, no user
  text.
- `hyperframes feedback` soft-warns (never blocks) when a non-10
  `--comment` is missing `REPRO COMMAND:`, and when a rating-<=7
  visual-defect comment is missing `COMPOSITION_STRUCTURE:`. The
  warning points at the auto-census helper so agents remediate
  themselves.
- `coreSkillContent.test.ts` locks the new literal in both the skill
  and the reference file, following #2498's pattern.

Extends #2498. Follow-up: no change to `doctorSummary` generation, no
change to the feedback-submission API endpoint, no refactor of
#2498's doc-content Jest test.

Signed-off-by: Via
2026-07-17 02:58:11 +00:00
Via d05c899c88 fix(fallow): suppress line-shifted pre-existing findings + simplify CSS.escape polyfill
Fallow audit failed on the parent PR (#2563) with 8 findings, all of them
tracing back to line-shift fingerprint invalidation on pre-existing complexity/
duplication, plus one new-but-easily-simplified CRAP finding on the CSS.escape
polyfill in picker.test.ts.

Actions:

- picker.ts: 5 pre-existing inherited-complexity findings (isEffectivelyHidden,
  isPickableElement, buildElementLabel, getPickCandidatesFromPoint,
  pickManyAtPoint). All in the file at the parent SHA. The one-line
  buildElementSelector edit (+ 3-line comment) shifted every function below
  it, re-triggering the fingerprint. Added to health.ignore with rationale.

- screenshotClip.ts + vite.browser.ts: 19-line clip-computation clone that
  pre-dates this PR — the try/catch guard around querySelectorAll shifted
  screenshotClip.ts's clone-start line, re-flagging the inherited duplication.
  Added both files to duplicates.ignore with rationale (splitting the clone
  would require crossing puppeteer's page.evaluate serialization boundary).

- picker.test.ts CSS.escape polyfill: simplified from a 15-line char-by-char
  loop (CRAP 56.3, cyclo 14) to a compact regex + leading-digit special case
  (~4 cyclo). Still handles the digit-leading case this PR's regression test
  needs (`#0` -> `#\30 `); the round-trip through querySelector still asserts
  the element is picked back. All 16 picker tests + 3 screenshotClip tests
  still pass locally.

Change by Via
2026-07-17 02:53:07 +00:00
Via 9fc7390c2b fix(cli): import EXTRACT_CACHE_DIR_DISABLED_ALIASES in render.ts
Addresses Miga's SSOT review on #2564. The render command was
inlining the disable-alias list (["off","none","false","0"]) instead
of importing the exported constant, defeating the drift-safety the
constant exists to provide. Also switches the flag description string
to interpolate the alias set from the constant for consistency.

_— Via_
2026-07-17 02:52:04 +00:00