Commit Graph
453 Commits
Author SHA1 Message Date
Vance Ingalls 71fd96bbf1 Merge pull request #2854 from heygen-com/feat/canary-rollouts
feat(core): percentage-based canary rollouts + calibration experiment
2026-08-03 22:35:37 -07:00
Miguel Ángel 4e7fcf7f2a fix(core): resolve sub-composition sibling asset paths everywhere (#2994)
Extends the studio-preview fix to the render path and the asset-discovery
utilities, which share the same resolver and had the same defect.

`rewriteAssetPath` takes an optional `assetExists` probe. A plain relative ref
authored in a sub-composition (`_shared.css`, `clip.mp4`) is re-pointed at the
composition's own directory when that sibling exists on disk; project-root refs
with no sibling (the registry's `assets/logo.png` convention) stay as authored.
Callers that can see the filesystem supply the probe, so the module stays free
of node:fs.

Also fixes a second defect in the inliner: `<head>` <link> hrefs and external
script srcs are hoisted into the root document but never went through the
rewrite at all, so even the documented `../` form escaped the project and 404'd
at render time.

Wired into the preview bundler, the producer compiler, the studio preview
builder, the HEVC preview lint, the project lint's asset scans, publish proxy
baking, and media-treatment source resolution.
2026-08-03 21:13:35 -07:00
Xinyu YANG 70cc4f59a1 fix(cli): stop bare --frame-check from swallowing the next flag (#2966)
citty parses string options greedily, so a bare --frame-check consumed
the following flag as its value (--caption-zone silently dropped,
--json disabled) and failed with an error blaming --caption-zone.

Normalize rawArgs at the check command boundary (bare --frame-check
followed by a flag or at end becomes --frame-check=), attribute
frame-check spec errors to --frame-check, and detect dash-prefixed
values with corrective guidance.

Fixes #2965
2026-08-04 04:03:04 +02:00
Miguel Ángel d3607606ee fix(cli): validate capture budget milliseconds 2026-07-31 20:30:07 +00:00
Miguel Ángel 49091e6142 fix(cli): propagate live capture budget 2026-07-31 20:30:07 +00:00
Miguel Ángel 45aead84c9 test(cli): cover bounded capture stages 2026-07-31 20:30:07 +00:00
Miguel Ángel 765a5ae83f fix(cli): bound capture runtime stages 2026-07-31 20:30:07 +00:00
Vance IngallsandClaude Opus 5 3f69a2c635 fix(cli,core,studio): close 15 review findings + 2 R5 blockers
R5 blockers
- Negative install-state latch was cached for the process lifetime, but
  only `true` is monotonic across processes. A long-lived preview server
  held a stale `false` and could re-enrol after another process tripped
  the breaker. Only the positive is cached now; `false` re-reads.
- The real breaker writer used writeConfig(), which collapses
  {ok:true, mirrored:false} to success, so a run that mirrored nothing
  reported done with the latch only on the erasable store. It consumes
  writeConfigWithResult and retries until both stores carry it.

Bucketing integrity
- Storage-restricted Studio profiles all bucketed on the literal
  "anonymous": computed against the shipped hash, 100% of them were
  enrolled in calibration-50 rather than 50%, and they merged into one
  PostHog person. Per-session random id instead — persists nothing.
- bucketSeed had read/write authority backwards: install-state is
  write-once authoritative, but readConfig took config.json's blindly, so
  the stores could hold different seeds until a re-mint flipped every
  cohort. Merged on read, like the latch.
- An unwritable ~/.hyperframes with no config.json re-minted per call,
  re-rolling the seed on every command, and the "cohorts will not be
  stable" warning was unreachable on that path.
- A corrupt PRE-MOVE state file was never deleted, so a machine reset
  with `rm -rf ~/.hyperframes` reported predecessorFound/stateFileCorrupt
  forever — poisoning the exact metric this work exists to produce.

Opt-out honoring
- CLI canary decisions memoized per process, so `hyperframes telemetry
  disable` during a running preview server was ignored for hours while
  the server kept serving pre-opt-out decisions. The memo is keyed on the
  telemetry posture.
- shouldTrack() memoized, contradicting policy.ts's documented "not
  memoized" contract that policy.test.ts asserts.
- The Studio override path resolved the bucket unit eagerly as an
  argument, minting and PERSISTING a tracking id for an opted-out profile
  — a value evaluateCanary discards unread.
- Storage reads could throw out of telemetry into a post-commit catch
  block, reporting an already-committed edit as failed.
- readConfig printed an unsilenceable stderr warning on every invocation
  for installs that opted out of telemetry entirely.

Host split
- isLoopbackHost rejected 0.0.0.0, so the documented
  HYPERFRAMES_PREVIEW_HOST LAN mode silently lost CLI→Studio identity
  stitching and split one user across two PostHog persons. Identity is
  now allowed when the operator explicitly opted into LAN binding.
- Corrected the comment claiming the guard refuses spoofed Hosts: a
  non-browser client sets Host freely. It is a browser DNS-rebinding
  mitigation, not access control, and now says so.

Semantics and test hygiene
- percentage:100 did not mean everyone — exclude and no_unit_id sat above
  the fast path, so the registry's "delete the entry at 100" step was an
  unstaged flip for CI and seedless installs.
- CLI cohort adoption returned before evaluateCanary, dropping Studio's
  own webdriver exclusion.
- overdueCanaries() was asserted against wall-clock time, so the whole
  core suite would go red on 2026-09-15 for every unrelated PR; and `>`
  against midnight made a canary overdue ON its sunset date.
- Statistical assertions ran on unseeded randomUUID() populations tight
  enough to fail ~1 run in 200. Seeded.

Also: broke a config -> policy -> transport -> config import cycle by
moving POSTHOG_API_KEY to a leaf module.

Tests: 2347 CLI (bundle absent), 3153 Studio, 1450 core. Fault injection
covers the latch, seed authority, LAN identity, webdriver exclusion and
the anonymous-bucket fix. Two pre-existing tests asserted behaviour these
findings identify as wrong (shouldTrack memoization, 100%-excludes-CI)
and were rewritten with the reasoning stated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 01:27:37 -07:00
James Russo 3a6b7f0612 fix: align local WebGPU capture behavior (#2907)
* fix: align local WebGPU capture behavior

* fix: address WebGPU capture review feedback

* fix: retain overlapping GPU seek work

* fix: satisfy runtime seek completion types

* fix: drain concurrent GPU seek work

* fix: prevent WebGPU capture barrier starvation

* fix: keep WebGPU presentation active during render seeks
2026-07-30 21:52:30 -07:00
Vance Ingalls 2e4c2c4407 Merge pull request #2109 from heygen-com/fix/prompt-guide-validation-bugs
docs: Prompt Guide as a novice-to-capstone arc + text corrections from validation
2026-07-30 05:09:56 -07:00
Vance Ingalls 73ebc7c621 docs: address prompt guide review findings 2026-07-30 04:54:27 -07:00
Vance Ingalls 941167dd3c Merge pull request #2874 from heygen-com/feat/breaker-carryover
feat(cli): roll circuit-breaker state over across config wipes
2026-07-29 17:40:04 -07:00
Vance Ingalls b2e7d76d67 Merge pull request #2875 from heygen-com/feat/de-short-inversion
feat(producer): short-comp DE inversion band — baseline release (telemetry only, routing off)
2026-07-29 17:37:57 -07:00
Vance IngallsandClaude Opus 5 4dbf0d90b0 fix(producer): fail closed when no live element count is available
R4 review finding, and the comment I wrote in R3 was simply wrong: the
probe session is NOT running for every render. probeStage's needsBrowser
gate launches one only for unknown duration, unresolved compositions, or
specific media cases — and hasRuntimeInsertedMedia matches only
createElement("video"|"audio"), never createElement("span"). So the exact
shape that motivated the live-DOM fix (a known-duration, media-free
caption comp building thousands of nodes in script) gets NO probe, falls
back to the static source scan, reads as ~2 elements, and could enter the
applied cohort at 40k live nodes. The R3 fix measured the right thing but
only for the population that already had a probe.

Now the count carries provenance and the band fails closed:

- resolveCompositionElementCount returns { count, source: "live" |
  "static" }. Only "live" — an actual DOM measurement — may open the band.
- resolveDeShortBand gains a third decisive outcome, "unmeasured", for
  the static case. It deliberately does NOT report skipped_elements: a
  static undercount is not a real oversize observation, and putting it in
  the control arm would contaminate the DiD just as putting it in the
  treatment arm would. Neither cohort; never routes.
- composition_element_count_source ships alongside the count, so the
  fleet rate of "static" sizes the population a future
  conditional-probe-launch would unlock — which is the data PR B needs to
  decide whether that launch cost is worth paying.

Regression coverage walks the real chain rather than a full render, using
the production functions in pipeline order: probeRequiresBrowser (newly
extracted from the inline needsBrowser expression, so the gate is
testable at all) returns false for the caption-comp shape → the resolver
reports static and a count under the ceiling → the band reports
unmeasured, not applied. Fault injection confirms it bites: removing the
one guard line fails exactly these three tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:02:26 -07:00
Miguel Ángel fdc5932897 fix(cli): honor check navigation timeout (#2860)
* fix(cli): honor check navigation timeout

* test(cli): clarify diagnostic timeout precedence
2026-07-29 20:50:20 +02:00
Vance IngallsandClaude Opus 5 23854f7c6a feat(producer): surface init telemetry from parallel workers — the band's missing motion axis
The routing surface the short-comp benchmarks validated is (motion x DOM
size x frames). After the baseline release, fleet telemetry carries DOM
size (composition_element_count) and frames on every render — but the
motion proxy, observability_init_tween_count, has 0% coverage on the
exact renders the band routes: parallel workers' console buffers (and so
the [FrameCapture:INIT] line the summary parses) only propagate to the
orchestrator on FAILURE. Single-worker screenshot renders report it;
the multi-worker clamp bucket never does. Verified against 7d of fleet
data: 35k screenshot renders carry tween counts, 0 of 9,600 band renders.

Fix rides the one channel parallel workers already return on success —
the per-worker CapturePerfSummary. Sessions record initTelemetry on
every init path; the perf summary now carries it; the orchestrator
max-merges across workers (same multi-session semantics the console
parser uses) and feeds it to the observability summary as a structured
fallback, console lines still refining when present.

With this, every band render carries full coordinates — (elements,
tweens, frames, path, speed) — which buys two reads: regressing wild DE
speed against element count on the existing 900+ inversions validates
the bench's 0.50ms/element slope BEFORE the routing flip, and any
post-flip misroute can be reproduced locally by feeding its telemetry
row straight into gen-crossover-comp's knobs (--movers ~ tween count,
--static ~ element count) and re-benching.

(Also drops a now-stale fallow suppression in render.ts — the test-only
reset export it guarded gained real test importers, so the issue it
suppressed no longer exists and the gate flags the leftover.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 10:54:24 -07:00
Vance IngallsandClaude Opus 5 0749cd9ff8 feat(producer): open the DE single-worker inversion to short comps under an element ceiling
31% of fleet renders (24h, v0.7.78+) are DE-eligible comps clamped to
parallel screenshot purely because they sit under the 900-frame inversion
floor — the median fleet render is ~250-600 frames, below every DE entry
threshold. This opens a 250-899 frame band, gated on composition size.

Measured, not assumed. A controlled sweep (fixed synthetic content,
{250,400,600,900}f, single-DE vs parallel-screenshot-W4, 3 reps, capture
mode verified per row, AC power, load-gated) showed single-DE winning
1.16-1.24x at every size — but only for content in constant motion. A
follow-up 2x2 found motion and DOM size pull in OPPOSITE directions, so
neither alone predicts the winner (ratio = ss4/de1, >1 means DE wins):

     24 movers /     0 nodes -> 1.05
    320 movers /     0 nodes -> 1.24
    320 movers /  7000 nodes -> 1.09
     24 movers /  7000 nodes -> 0.96
     24 movers / 20000 nodes -> 0.71
     24 movers / 40000 nodes -> 0.55

DE's wall-clock scales ~0.50ms/element against parallel screenshot's
~0.22ms — drawElement repaints the whole tree per frame while fan-out
amortizes it — so the downside is NOT bounded and a bare floor drop would
have handed a 1.8x regression to large comps. Since motion only ever helps
DE, an element ceiling calibrated at the lowest-motion case is safe at
every motion level; crossover there is ~3.9k, and the default sits at 2500.

The predicate is untouched; the call site picks the floor. Above the
ceiling, or at 900+ frames, behaviour is bit-identical to today — the
change can only add inversions in the new band, never remove one.

Instrumentation, since this ships at full exposure rather than cohorted:
`composition_element_count` on EVERY render (the fleet distribution of the
gate variable is unknown — without it we cannot tell whether 2500 opens the
band for most short comps or almost none, nor re-derive the threshold from
real content), and `de_short_band` = applied | skipped_elements, unset when
the frame count made the band irrelevant, so a fleet perf shift is
attributable to this change rather than to content mix.

Safety is unchanged and already proven on this path: per-frame PSNR
self-verify with screenshot fallback, exactly as the 900+ band has shipped
default-on. Knobs: HF_DE_SHORT_MIN_FRAMES, HF_DE_SHORT_MAX_ELEMENTS (0
disables the band).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 02:36:40 -07:00
Vance IngallsandClaude Opus 5 dfe92b2aab feat(cli): roll circuit-breaker state over across config wipes
The DE parallel-router breaker's tripped state lived in the same config
file as the install id, so the most common identity reset — deleting
~/.hyperframes — also re-enrolled the machine into an experimental path
that had already failed on it.

Mirror exactly two facts into a machine-local state file
(~/.local/state/hyperframes/install-state.json) that a config wipe does
not touch:

- markerAt: written unconditionally on every install, so the fraction of
  fresh mints that find it directly measures recoverable id churn
  (config wiped, machine persisted) vs unrecoverable (fresh
  machine/container/new user). Emitted as install_predecessor_found on
  telemetry events; absent (not false) on configs predating the field.
- deParallelRouterTrialFired: a breaker tripped by a previous install
  stays tripped for the new one. Config corruption takes the same mint
  path, so it survives that too.

The file deliberately holds NO identity — no anonymousId, no counters.
A wiped config still gets a fresh id unconditionally; only the safety
fact about the machine survives. Sync happens inside writeConfig so no
breaker write site can forget it; failures are swallowed (telemetry
must never break the CLI) but leave the memo unset so a later write
retries. `hyperframes telemetry` lists the state path for transparency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 00:46:41 -07:00
Miguel Ángel 9bbb6d50a0 fix: offset nested template video timing (#2859)
* fix: offset nested template video timing

* test(producer): cover nested sequential video render

* fix: share canonical nested media timing
2026-07-29 03:36:04 +02:00
Xuanru LiandCursor 3a7950fd63 feat(check): add data-layout-allow-caption-zone waiver (#2853)
* feat(check): add data-layout-allow-caption-zone waiver

Opt intentional lower-third copy out of caption_zone_collision.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check): address caption-zone waiver review nits

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(skills): document caption-zone waiver on CLI agent path

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(cli): document caption-zone waiver under check, not inspect

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 15:56:43 -07:00
Miguel Ángel 880021411d fix(cli): make telemetry opt-out durable (#2852)
* fix(cli): make telemetry opt-out durable

* fix(cli): make telemetry status trustworthy
2026-07-28 20:35:54 +02:00
Vance Ingalls 59b259d039 Merge pull request #2841 from heygen-com/07-27-feat_engine_open_drawelement_fast_capture_to_win32_hardware_gpu
feat(engine): open drawElement fast capture to Windows hardware GPU
2026-07-28 04:47:07 -07:00
WaterrrForever d287e5244c fix(cli): persist authoring skill in hyperframes.json for durable render attribution (#2762)
* fix(cli): persist authoring skill in hyperframes.json for durable render attribution

authoring_skill was stamped only on the first render through a workflow
passing --skill, so re-renders, `npm run render`, --batch, existing-project
renders, and general-video lost it — leaving 77-96% of real-human render
volume un-attributed and the skills-penetration metric misleadingly low.

Persist the owning skill in hyperframes.json: `init --skill` stamps it at
creation, `render` resolves the flag then falls back to the stored value, and
an explicit --skill seeds it (seed-once, never overwriting the creating
workflow's identity). Activate all render-producing creation workflows to
declare their skill at init.

Forward-only: does not rewrite historical telemetry.

* fix(cli): patch hyperframes.json in place when seeding the authoring skill

seedProjectAuthoringSkill is the only writer that touches an already existing
hyperframes.json — every other writeProjectConfig call site is guarded to write
only when the file is absent, which made the whole-file overwrite safe by
construction. Round-tripping the seed through normalizeConfig broke that: it
rebuilds the object from a field whitelist with no rest-spread, so any key
outside the schema was silently dropped, a media block was materialized in
projects that never had one, and key order was rewritten. hyperframes.json is
normally committed, so a render introduced a diff the user never asked for, and
any field added to the schema later would be deleted by a render on an older
CLI.

Parse the raw JSON, set authoringSkill, write it back, reusing the file's own
indentation. Unknown keys and formatting survive; the only delta is the key
being added. A corrupt config is now left untouched instead of clobbered.

Seed-once semantics are unchanged, still normalized so a hand-edited garbage
slug neither reaches telemetry nor wedges the seed.

Reported independently by both reviewers on #2762.

* fix(cli): create the docker build context with mkdtempSync

The `--docker` build context was created at a guessable path derived from
`Date.now()` in the world-writable OS temp dir. Another local user can
pre-create or symlink that path and have the build read a Dockerfile they
control. mkdtempSync gets a random suffix and 0o700 from the kernel, and it
creates the directory itself, so the separate mkdirSync goes away.

Pre-existing on main (alert #432, 2026-06-04, packages/cli/src/commands/render.ts),
surfaced against this branch only because the seed commit shifted line numbers in
the same file. Fixed here to unblock the CodeQL gate on #2762 rather than left for
a follow-up; the remaining 10 js/insecure-temporary-file alerts elsewhere in the
repo are untouched and still want their own pass.

* fix(cli): drop the check-then-use race when seeding the authoring skill

The seed tested for the config with existsSync and then wrote, which is a
check-then-use race: the file can be created or swapped between the check and
the write (CodeQL js/file-system-race).

Read once and branch on the failure reason instead. Only ENOENT creates a
config from scratch; any other read failure (permissions, I/O) now leaves an
existing file alone rather than overwriting it with a default, so this is also
strictly safer than the version it replaces.

Also replaces the `as Record<string, unknown>` assertion with an isJsonObject
type guard, per the repo's no-assertion convention.

Behaviour unchanged: all 4 seed regression tests still pass, and the
create/preserve/seed-once/corrupt-untouched paths were re-verified end to end.
2026-07-28 19:27:09 +08:00
Vance IngallsandClaude Opus 5 4520cd240b fix(engine): bucket gpu_renderer + cover the failure cohort (review)
Three review findings on the win32 drawElement PR:

1. gpu_renderer shipped the raw UNMASKED_RENDERER_WEBGL string — unbounded,
   driver-authored, GPU-model-specific, and |-joined across parallel
   sessions, i.e. high cardinality by construction, against this file's own
   convention of sanitizing engine-sourced strings (deGateReason is a
   bucket; error messages go through redactTelemetryString). Now bucketed at
   the source by classifyGpuRenderer to <backend>/<vendor>
   (metal/apple, d3d11/nvidia, swiftshader/other, ...), which is the whole
   analytic signal the win32 rollout needs and nothing else. The raw string
   never leaves the engine.

2. gpu_renderer reached render_complete only, so a crashed render — the
   cohort the field exists to attribute — carried no backend. It now rides
   RenderCaptureObservability (deGpuRenderer, sourced from the live probe
   session like the de_* counters), so both render_complete and
   render_error carry it and a hard failure still reports its GPU backend.
   On render_complete the perfSummary value still wins by spread order.

3. Restore the fallow-ignore-next-line suppression above
   __resetDeParallelRouterTrialStateForTests: CLI test files are not fallow
   entry points, so removing it fails the CI dead-code audit (local
   pre-commit passed only because of its changed-file scope).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 00:55:10 -07:00
Vance IngallsandClaude Opus 5 cb30157ebb feat(engine): open drawElement fast capture to Windows hardware GPU
Widen the default-on drawElement clamp from darwin-only to darwin|win32
(still requiring a non-software-GPU browser). The darwin restriction was a
validation envelope, not an architectural limit — the CanvasDrawElement
Chrome flag ships on every platform, and every safety layer that made the
macOS default-on release (v0.7.38) survivable is platform-neutral:
compile-time gates, the SwiftShader init gate, per-render worker-encode
self-verification with screenshot fallback, and the blank guard. Worst case
on an unvalidated D3D11 backend is the same as on Metal: verify catches a
bad frame and the render re-runs on the screenshot baseline.

Why now: 30-day telemetry shows ~206k non-CI hardware-GPU Windows renders
(~78% of the win32 fleet, 18k installs) held on the slow screenshot path by
the clamp — the second-largest perf population after macOS, carrying ~1,550
capture-hours/month in the DE-eligible >=700-frame band alone at a measured
~2x speedup opportunity.

Instrumentation for the new cohort: drawElement session init now records the
raw WebGL UNMASKED_RENDERER_WEBGL string (detectSwiftShader generalized to
detectGpuBackend — same single evaluate, the string was previously read and
discarded) and threads it session -> CapturePerfSummary -> RenderPerfSummary
-> render_complete as `gpu_renderer`. drawElement damage proved
compositor-backend-specific throughout the macOS rollout, so D3D11-cohort
failures must cluster by ANGLE backend + GPU vendor (NVIDIA/AMD/Intel), not
just `os`.

The two DE clamp branches are extracted into a pure, unit-tested
`resolveDefaultDrawElement` (platform + GPU mode + worker-encode + explicit
opt-in), which also drops resolveConfig's cyclomatic complexity. The win32
streaming-encode compound tests collapse onto one shared helper.

Linux stays excluded: that fleet is headless/Docker SwiftShader, where DE
has no speedup and known rendering defects. Kill switches unchanged:
PRODUCER_EXPERIMENTAL_FAST_CAPTURE=false, --experimental-fast-capture=false.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 00:00:21 -07:00
Xuanru LiandCursor 209e6e0148 feat(check): opt-in --layout proseCoverageFloor (#2834)
* feat(check): opt-in --layout proseCoverageFloor for text_occluded

Keep the default prose coverage floor at 0.15 for all callers, and allow
stricter agents (e.g. Zephyr) to lower it via --layout "proseCoverageFloor=0.05"
without changing other layout gates.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(check): collapse --layout comments and docs to one line

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check): strict proseCoverageFloor parse + pin 0.07 floor tests

Reject trailing-garbage fractions that Number.parseFloat would accept, and
pin the existing ~0.07 coverage fixture for default vs floor=0.05 (atomic
labels unchanged) plus a collectLayout forwarding assertion.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check): share parseNumberStrict across layout and frame-check

Sweep the sibling --frame-check tol parser (and caption fractions) onto the
same strict Number() helper so trailing garbage cannot prefix-parse.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 16:27:09 -07:00
Somansh Reddy e3636db07e docs(send-to-guide): enhance turns are free; render is the paid step (#2827)
* docs(send-to-guide): enhance turns are free; render is the paid step

The shipped pricing model is import + enhance turns free, only the final
render charged (a monthly free-render credit, then per-minute). The guide
labeled enhance as 'the paid step', which misstates the model to the
authoring agent. Move the paid label to Render.

* docs(send-to-guide): state the tiered render billing contract + pin it in the guide test

Address review: the pricing line must teach Claude the real tiered contract,
not a single universal free render. Per heygen-server usage_limits.py: FREE
accounts get 3 renders/month (then blocked, not billed); paid plans are charged
20 credits per rendered minute at completion. Enhance turns are free.

Also pin the invariant in sendToGuideContract.test.ts: assert Enhance=free /
Render=paid + the tiered figures, and a negative assertion blocking the retired
'Enhance ... paid step' wording from returning.
2026-07-27 12:48:22 -07:00
Miguel Angel Simon Sierra 597c14a887 fix(telemetry): send feedback as plain events, not PostHog surveys
CLI and Studio feedback were emitted as `survey sent` with `$survey_*`
properties, so every rating was ingested as a PostHog survey response even
though no survey definition, targeting, or popover backs them.

Emit `cli_render_feedback` and `studio_feedback` with plain `rating` /
`comment` properties instead. Same fields, same call sites, same opt-out.
2026-07-27 16:46:51 +02:00
Xuanru Li 75ed99e1d4 fix(check): elongated pivot drift + counterfactual connector_detached (#2819) 2026-07-26 21:43:42 -07:00
ukimsanov 20f4bde46a test(cli): harden media treatment parity 2026-07-26 01:29:05 -07:00
ukimsanov c1dde28980 fix(cli): address media treatment review findings 2026-07-26 01:25:42 -07:00
ukimsanov 6d5961b802 feat(cli): expose agent-native color grading 2026-07-26 01:25:42 -07:00
Xuanru LiandClaude Opus 4.8 72e2f08f15 feat(lint): dense motion re-sampling for content_overlap (#2746)
* feat(lint): dense motion re-sampling for content_overlap

Transient text-on-text collisions during continuous motion (e.g. an
orbiting label card crossing the center card) overlap for a fraction of
a second that the sparse 9-point layout grid seeks straight past. The
content_overlap detector is correct; it just never gets a sample at the
crossing moment. Rerun ONLY content_overlap on an 8fps grid (text-only,
cheap) when the composition animates; findings feed the existing
persistence tiering unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(lint): unconditional dense content_overlap pass + honor 500ms floor

Round-1 blocker: the dense motion-overlap re-pass was gated on sparse-grid
geometry fingerprints changing, so an animation aliased to the sparse grid
(identical fingerprints, yet colliding between samples) bypassed the pass —
exactly the transient false-negative it was built to catch. Remove the gate:
the dense pass now runs unconditionally (bounded, text-only), driven by the
composition timeline rather than a fingerprint heuristic.

Round-2 follow-ups:
- Persistence-tier drift: at 8fps, occurrences>=2 spans only ~125ms, not the
  ~500ms the design intends, and it short-circuited before the ms floor.
  content_overlap promotion now requires BOTH occurrences>=2 AND a literal
  firstSeen..lastSeen span >= 500ms, so the wall-clock floor is honored at any
  sampling density. Comment block updated to match.
- Sample cap scales to hold a true 8fps grid up to ~75s (raised 120 -> 600)
  with an explicit note that longer comps degrade below 8fps to stay bounded.

Tests:
- Replaced the trivial "warning at every sample" test with a real between-grid
  regression: a collision living only inside (3.5,4.5) — a gap the sparse grid
  seeks past — is detected and, held ~750ms, promoted to error.
- Replaced the now-invalid "skips when static" test with one asserting the
  dense pass runs even when sparse fingerprints are identical (aliased motion).
- Added a tiering regression: two dense occurrences spanning ~125ms stay a
  warning (not error). Both new guards verified red before the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(lint): settle-free geometry seek for dense content_overlap pass

The dense overlap re-pass did up to OVERLAP_MAX_SAMPLES full-settle seeks
(120ms paint settle each, ~72s of pure sleep at the ceiling) even though
collectOverlap only reads getBoundingClientRect geometry, valid
synchronously after the timeline setTime. Add a settle-free
DENSE_GEOMETRY_SEEK_OPTIONS + driver.seekGeometry used only by the dense
loop; the base grid keeps full-settle driver.seek.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(lint): document + cover content_overlap 500ms boundary for sparse callers

The occurrences>=2 AND heldMs>=500 promotion rule is a semantics change for
sparse callers (--samples 20, --at, short comps) whose two samples can land
<500ms apart. Document the change in the tiering comment and add boundary
tests: 499ms span stays warning, 500ms span promotes to error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(lint): make dense content_overlap seek genuinely geometry-only

Per review: DENSE_GEOMETRY_SEEK_OPTIONS only overrode settleMs, still
inheriting animationFrameSettle:double + waitForFontsMs:500 → ~3 frame
waits + font wait per seek → ~30s at the 600-sample cap. Geometry
(getBoundingClientRect) is valid synchronously post-setTime, so drop all
post-seek waits (animationFrameSettle:none, waitForFontsMs:0, settleMs:0).
Add options-level regression locking the geometry-only contract. Also fix
a stale comment name (detectMotionTextOverlap → collectMotionOverlapSamples).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style: collapse multi-line comments to single lines

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:29:24 -07:00
Miguel Ángel 3b3d4f559c test(cli): move render module collection outside hooks (#2780) 2026-07-25 16:13:20 +02:00
ukimsanov 4582881d00 feat(cli): add agent-first media treatment tools 2026-07-24 18:42:07 -07:00
Xuanru Li e710a1686f feat(lint): add off_pivot_rotation hub-referenced layout check (#2744)
## What it catches

A gauge needle / clock hand / dial pointer / radar sweep that rotates about the **wrong pivot** — the recovered center-of-rotation sits far from the dial hub (e.g. `transform-origin` at the needle base or SVG element edge instead of the dial center). Visually the needle "wobbles" or orbits off-axis instead of sweeping cleanly about the hub.

This is a genuine gap in the current checks: `rotation_pivot_drift` (#2741) provably **cannot** catch it — a correct sweeping needle's bbox-center orbits identically to a broken one, so only a **dial-hub reference** distinguishes them. This is the separate hub-referenced check that analysis called for.

## How it works

- Sampler maps 2 material endpoints per frame via `getScreenCTM` (honors the actual rendered transform, independent of `svgOrigin`).
- Resolves the dial hub = shared center of the modal set of static concentric circles, or the arc-center of the largest static near-circular path (Kasa circle fit).
- Fits a circle to the endpoint trajectory to recover the true center-of-rotation; flags drift `> 0.35 * pointer_length`. One warning per hub.
- Never fires without a resolvable hub. Walks the rotation reference to the composition root (not the `<svg>`) so a pointer rotated by a `div` ancestor is measured correctly.
- Multi-body guard: `>= 2` bodies at distinct angular positions on one hub = orbit/atom system, not a dial → suppressed.

## Corpus evidence (autonomous geometry-fuzz run, 81 fuzzed diagrams)

- **7 / 7 true positives, 0 false positives across all 81 samples.**
- Assigned TPs: fuzz005, fuzz017, fuzz032. Bonus TPs: fuzz044, fuzz056, fuzz068, fuzz080.
- **The Gemini-3.6 video-judge itself MISSED all 4 bonus TPs** (`vlm_has_defects: false`) — the deterministic hub-reference check beats the VLM on this defect class.
- FPs driven to 0 by the two principled guards above: fuzz016 (planet arc rotated by a `div` ancestor) cleared by root-walk; fuzz055 (atom) cleared by the multi-body guard.
- fuzz080 reads as a false positive to the connector check but is a true positive here — confirms the architectural boundary between the two checks is drawn correctly.

## Validation

- Autonomous Gemini-3.6 **video**-judge fuzz run to surface candidate defects, then a **deterministic FP sweep** across all 81 rendered compositions (not VLM-gated — code inspection is the arbiter, since the VLM both over- and under-calls this class).
- 9 unit tests (`checkPipeline.offPivotRotation.test.ts`) + full check suite pass; `bun run build` green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-24 17:38:58 -07:00
Vance Ingalls 4889b779c9 fix(engine): lazy-promisify execFile in psnr so partial child_process mocks don't crash at import 2026-07-23 17:57:15 -07:00
Xuanru Li 222aec45ab feat(lint): add rotation_pivot_drift layout check (#2741)
## What
New cross-sample layout check `rotation_pivot_drift` — flags a rotating element that should spin **in place** but pivots about the **wrong point** (e.g. a wheel whose spokes use a hardcoded px `transformOrigin` instead of `svgOrigin`/`%`, so they swing off-center while every existing check still passes).

Motivating prod case: a portrait ad's spoked-wheel whose `#spokes` rotated about `transformOrigin:"250px 250px"` in a resized 460px container — spokes detached from the hub, shipped clean because no rule inspects rotation.

## How
- `layout-audit.browser.js`: `window.__hyperframesRotationSample()` reports each visible transformed element's bbox center + decoded rotation angle per layout sample. Skips `[data-layout-allow-orbit]`.
- `checkPipeline.ts`: accumulates samples across the seek grid; `detectRotationPivotDrift()` (modeled on `detectSweepStatic`) flags an element that (a) actually spins (angle spread > 20° over ≥3 samples), (b) is size-stable (bbox width ratio ≤ 1.6), and (c) whose bbox **center** drifts > `max(10% of its size, 2% of min viewport dim)`. Emits `warning`; not persistence-tiered (not demoted to info).

## FP guards
Real rotation required, ≥3 samples, size stability, `data-layout-allow-orbit` exemption, min area ~2500px². Center-drift (not bbox size) is the discriminator, so a correctly-centered spinner reads drift ≈ 0.

## Validation (`check --json`)
| Fixture | Expected | Result |
|---|---|---|
| off-transformOrigin spoked wheel | fire | **fired — 109px drift on `#spokes`** |
| non-spinning comps (node diagram, device tree) | clean | clean, no FP |
| correctly-centered spinner (`svgOrigin`) | clean | clean (spins 162°, drift 0) |
| `data-layout-allow-orbit` off-origin spinner | clean | clean (exempt) |
| off-`svgOrigin` control, no opt-out | fire | fired — 251px drift |

No false positives. `tsc --noEmit` clean, `oxlint` clean, `check.test.ts` + `layout-audit.browser.test.ts` = 112/112 pass.

## Note
`ROTATION_MAX_SIZE_RATIO` is 1.6 (not 1.3): a rotating anisotropic shape's axis-aligned bbox inherently oscillates (8-spoke star ~1.32×, square 1.41×), so a tighter ratio rejects legitimate targets. Center-drift stays the real discriminator; thin swinging bars are excluded.

Follow-up: a `detectRotationPivotDrift` unit test via the fake driver's `collectRotationSample` (mirroring the sweep_static tests).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-23 01:44:46 -07:00
Vance Ingalls 12e599a6ba fix(engine): realistic worker memory budget + sizing/feedback telemetry 2026-07-21 14:31:14 -07:00
James ab8b50afd7 refactor(repo): resolve changed-code audit 2026-07-20 11:59:57 -07:00
James 3222f1f31d refactor(cli): decompose render phases 2026-07-20 10:34:06 -07:00
James 619406a23b fix(cli): complete process lifecycle migration 2026-07-20 09:04:03 -07:00
James 00b88abeb7 test(cli): align timeout failure with result contract 2026-07-20 09:04:03 -07:00
James 3aa2404747 refactor(cli): centralize process lifecycle 2026-07-20 09:04:03 -07:00
James 942db16fcd fix(cloudrun): publish adapter contract 2026-07-18 20:17:01 -04:00
Miguel Ángel e64a22893a fix(studio): preserve alpha proxy playback (#2625)
* fix(studio): preserve alpha proxy playback

* test(cli): pin VP8 alpha proxy pre-resolution

* fix(studio): emit textarea field-sizing CSS

* test(studio): verify textarea CSS generation
2026-07-17 21:56:27 -04:00
James e73304fb0e feat(cli): make cloud archives size-aware 2026-07-17 18:44:58 -04:00
James 65f2e2927c refactor(producer): unify render requests 2026-07-17 16:22:23 -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
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