initial code - #2
Merged
Merged
Conversation
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>
miguel-heygen
approved these changes
Mar 21, 2026
jrusso1020
approved these changes
Mar 21, 2026
miguel-heygen
force-pushed
the
initial-code-port
branch
from
March 21, 2026 16:41
46caf94 to
eb11071
Compare
Collaborator
This stack of pull requests is managed by Graphite. Learn more about stacking. |
This was referenced Mar 21, 2026
- 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>
- 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>
vanceingalls
added a commit
that referenced
this pull request
Mar 22, 2026
* 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>
6 tasks
miguel-heygen
added a commit
that referenced
this pull request
Apr 10, 2026
## Summary - Bump `@chenglou/pretext` ^0.0.3 → ^0.0.5 in `packages/core` — fixes **high-severity** algorithmic complexity DoS ([Dependabot #3](https://github.com/heygen-com/hyperframes/security/dependabot/3)) - Bump `vite` ^5.0.0 → ^6.4.2 in `packages/studio` — fixes **medium-severity** path traversal in optimized deps `.map` handling ([Dependabot #2](https://github.com/heygen-com/hyperframes/security/dependabot/2)) ## Test plan - [x] `pnpm --filter @hyperframes/core build` — passes - [x] `pnpm --filter @hyperframes/studio build` — passes (vite 6.4.2, 4631 modules, 3.85s) - [x] `@vitejs/plugin-react@^4.0.0` supports vite 6 (`peerDependencies: vite ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0`)
7 tasks
vanceingalls
added a commit
that referenced
this pull request
Apr 16, 2026
Blockers: - #2: late_init_set false positive on fractional opacity (0.5 matched as 0) Fixed: /opacity\s*:\s*0(?![.\d])/ negative lookahead - #3: scene-1 prefix skip matches scene 10+ (s1- matches s10-) Fixed: extract full number and compare exactly High severity: - #4: autoAlpha not covered by late_init_set Fixed: checks both opacity and autoAlpha - #5: al() crashes on non-hex colors (#fff shorthand, rgb(), null) Fixed: guard + shorthand expansion + NaN fallback - #6: "Full palette" with null bg crashes isDark Fixed: null guard defaults to dark - #7: template literals missed by tl_from_in_multiscene Fixed: regex includes backtick quotes Medium: - #9: no retry limit on eval failures → infinite loop Fixed: max 2 retries, then escalate to user - #10: vague ID convention Fixed: explicit s{N}- prefix rule in multi-scene.md - #11: visual-style.md backward compat Fixed: Step 0b checks both filenames - #13: preview_html script injection Fixed: documented prohibition in design-picker.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 tasks
This was referenced Apr 18, 2026
This was referenced Apr 23, 2026
This was referenced Apr 27, 2026
4 tasks
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…-com#237) ## Summary - Bump `@chenglou/pretext` ^0.0.3 → ^0.0.5 in `packages/core` — fixes **high-severity** algorithmic complexity DoS ([Dependabot #3](https://github.com/heygen-com/hyperframes/security/dependabot/3)) - Bump `vite` ^5.0.0 → ^6.4.2 in `packages/studio` — fixes **medium-severity** path traversal in optimized deps `.map` handling ([Dependabot #2](https://github.com/heygen-com/hyperframes/security/dependabot/2)) ## Test plan - [x] `pnpm --filter @hyperframes/core build` — passes - [x] `pnpm --filter @hyperframes/studio build` — passes (vite 6.4.2, 4631 modules, 3.85s) - [x] `@vitejs/plugin-react@^4.0.0` supports vite 6 (`peerDependencies: vite ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0`)
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…subs Per @vai-bot's review on hf#641: Important #1: dead `src=""` substitution sites ============================================= Now that `bundleToSingleHtml` inlines the runtime IIFE by default, the empty `src=""` placeholder is never emitted in the no-env-var path — the 5 downstream substitution sites that grep for `src=""` were dead. Two of them (studio dev server + studio vite preview) genuinely WANT the placeholder so they can hot-reload a local /api/runtime.js endpoint without re-inlining ~150 KB on every composition edit. Three of them (CLI validate, snapshot, layout) were just doing the same inlining the bundler already does. Resolution: - Add a `runtime: "inline" | "placeholder"` option to `BundleOptions`. Default is "inline" (matches the self-contained-bundle promise the function name makes). The two studio surfaces explicitly pass `{ runtime: "placeholder" }` to opt in. - studioServer.ts + studio/vite.config.ts: pass the option, keep their existing string-replace logic unchanged. - validate.ts + snapshot.ts + layout.ts: delete the now-redundant runtime substitution code (regex never matches the new inlined-runtime shape). Important #2: joinJsChunks ASI hazard ====================================== The new helper appended `;` to chunks not already ending in `;` and joined on `\n`. If a chunk ended with a `// line comment`, the appended semicolon was eaten by the comment, leaving the next chunk's first statement attached to the previous chunk's last expression — exactly the ASI hazard the helper exists to prevent. Fix: append `\n;` instead of `;` for chunks not already terminated. The newline closes the line comment, the standalone `;` becomes the statement separator. For typical chunks (already ending in `;`), output is unchanged — still clean `\n`-joined chunks with no bare-semicolon lines. Also added a trailing `;` to `wrapScopedCompositionScript`'s IIFE close (`})()` → `})();`) so composition scripts join cleanly without falling through to the `\n;` fallback. New test: regression guard at the chunk boundary verifies every inline script body in the bundle parses cleanly via esbuild even when a source JS file ends with a line comment. Verification ============ - `bun run --filter @hyperframes/core test` — 653/653 pass - `bun run --filter @hyperframes/cli test` — 243/243 pass - `bun run --filter @hyperframes/{core,cli,studio} typecheck` — clean - `bunx oxfmt --check` + `bunx oxlint` on all touched files — clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…dows CI
The first dynamic `await import("./render.js")` cold-load takes >5 s on
Windows runners — long enough to blow vitest's default 5 s timeout in
whichever test ran it first. Subsequent imports are <10 ms because the
module is now cached, so only test #1 ever times out.
The downstream failure is more subtle: when test #1 times out, vitest
moves on, but its leaked async function eventually hits the synchronous
`producer.createRenderJob(...)` line and pushes a stale config to
`producerState.createdJobs`. That push lands AFTER test #2's `beforeEach`
clears the array, so test #2's `createdJobs[0]` is the leaked test #1
entry instead of its own. That's why test #2 saw `browserGpuMode: 'software'`
when it expected `'auto'`.
Hoist the import into `beforeAll` (matching the pattern the existing
`parseVariablesArg` and `validateVariablesAgainstProject` describe blocks
in this file already use). Cold-load happens once outside any test's
timeout window, every test stays fast, no leaked promise can corrupt
state.
Failing run: https://github.com/heygen-com/hyperframes/actions/runs/25470257972/job/74732502915
Started failing on main with the merge of heygen-com#642 (auto-detect-browser-gpu),
which added the "forwards browserGpuMode='auto'" test as test #2.
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
The 7 letter shortcuts (J/K/L/I/O/A/E) in usePlaybackKeyboard were gated on `e.code === "Key*"`, which is the physical key position on a US-QWERTY layout. On AZERTY (and other layouts) the physical "KeyA" slot produces e.key="q", so "Jump to in-point" and the rest of the letter shortcuts either fired on the wrong character or not at all. Switch the 7 letter shortcuts to compare `e.key.toLowerCase()` and rename `pressedCodesRef` → `pressedKeysRef` so the K-hold combo (K+J / K+L for frame stepping) is also keyed off the typed character. `Space` and `Arrow*` keep using `e.code` since those codes are layout-independent. Adds a happy-dom test covering QWERTY happy path, AZERTY (physical KeyQ produces e.key="a" → in-point seek fires), AZERTY contrapositive (physical KeyA producing e.key="q" no longer triggers in-point), Shift+I clears in-point, K-hold combo for frame stepping, K release returning the set to clean state, and Space passthrough. Addresses bug #3 in heygen-com#834. Bugs #1 (loop at out-point) and #2 (Jump to in-point forcing pause) live outside this hook (player loop and adapter `seek` respectively) and are left for follow-up PRs.
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…gen-com#842) When the user has the timeline playing and presses A (Jump to in-point) or E (Jump to out-point), the seek seeks to the marker as expected but also pauses the playback. The reporter (and the natural UX) expects playback to keep going from the marker. Root cause sits in two layers: 1. The `seek` callback in `useTimelinePlayer.ts` unconditionally calls `setIsPlaying(false)` and `stopRAFLoop()` whenever the store reports playing. That path is shared with timeline clicks, LayersPanel navigation, and frame stepping — flipping the default would change behavior the rest of the app expects. 2. `wrapTimeline` (the GSAP-timeline-backed adapter) calls `tl.pause()` before `tl.seek(t)`, so even if the callback above stopped pausing, GSAP-driven compositions would still get paused inside the adapter. The fix is opt-in at both layers: - Extend `PlaybackAdapter.seek` with `options?: { keepPlaying?: boolean }`. Default is omitted/false, preserving existing behavior for every caller that doesn't pass the option. - `wrapTimeline.seek` skips the implicit `tl.pause()` when keepPlaying is set. `createStaticSeekPlaybackAdapter` accepts the new signature but is a no-op for the flag (it never paused internally). - `useTimelinePlayer` seek callback grows the same option and forwards it to adapter.seek(time, options). The reset block (stopRAFLoop, setIsPlaying(false), shuttle refs) is gated behind !options.keepPlaying. - Reverse shuttle is always stopped on seek (the RAF reverse tick cannot survive a seek), so keepPlaying is overridden when the shuttle was running backward. Documented with an inline comment. - usePlaybackKeyboard updates its seek param type to match and passes { keepPlaying: true } on the A and E handlers only. Frame stepping (Arrow keys, J/L with K held) keeps the default. Tests (happy-dom): - useTimelinePlayer.seek.test.ts covers the callback in three cases: default seek clears isPlaying, seek with keepPlaying preserves isPlaying=true, and the option from paused state stays paused. - playbackAdapter.test.ts (new) covers wrapTimeline: default seek pauses the GSAP timeline, keepPlaying: true skips the pause, keepPlaying: false is the explicit default. Closes part of heygen-com#834 (sub-bug #2). Sub-bug #1 (playhead should loop to in-point when exceeding out-point) lives in the RAF tick and is left for a follow-up PR. Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com>
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
… back probe overcorrection Empirical investigation of --mode=distributed-simulated against many-cuts revealed that the BeginFrame "hang" attributed earlier to a Chrome 148 SwiftShader compositor wedge was actually a renderChunk bug: discardWarmupCapture was called with frameIndex=slice.startFrame, then captureStage immediately captured frame 0 (relative) of the chunk's range. For chunk 0 (slice.startFrame=0) these two calls produced the same frameTimeTicks. Chrome's HeadlessExperimental.beginFrame deadlocks when called twice in a row with the same frameTimeTicks — the compositor has no new damage to advance for, and the second call hangs until the Puppeteer protocolTimeout fires. Tracing the chunk worker confirmed: warmup call 1 t=0 -> ok warmup call 60 t=1947 -> ok (loop exited) beginFrame call #1 t=2333.33 -> returned, hasData=true, hasDamage=true beginFrame call #2 t=2333.33 -> HANG Fix: discardWarmupCapture skips chunk 0 (no prior frame to prime, and the in-process renderer also has an empty cache at frame 0) and uses slice.startFrame - 1 for chunk N>0 (the actual previous absolute frame, which more accurately matches what the in-process renderer's cache holds at the start of frame N). The engine probe complications I added earlier — multi-step screenshot test, inline data:URL pre-navigation, rastered-bytes assertion — were chasing a phantom and are reverted to the original simple form. chrome-headless-shell @stable on Linux with --use-angle=swiftshader renders BeginFrame screenshots correctly after the warmup loop; what looked like "wedged compositor" was the same frameTimeTicks deadlock masquerading as a Chrome regression. Also lowers the harness's distributed-simulated PSNR floor from 45 dB to 10 dB and switches to using the fixture's own minPsnr for both modes. The 45 dB floor was set against font-variant-numeric's static-content baseline drift (~48 dB), but dynamic compositions like many-cuts produce 34-44 dB baseline drift even in-process — both renderers share the same encoder/JPEG jitter floor, so requiring distributed to clear a tighter threshold than in-process catches no real regression. 10 dB remains as an absolute-pathology guard for fixtures with a permissive authored threshold. Validated end-to-end in `docker:test --mode=distributed-simulated`: font-variant-numeric: PASSED (PSNR ~48 dB, audio correlation 1.000) many-cuts: PASSED (PSNR 37-44 dB across rapid transitions) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…gen-com#1466) (heygen-com#1539) * fix(studio): restore timeline move/resize fallback parity (review heygen-com#1466) The §3.2 sdkTimingPersist rewrite regressed the non-SDK fallback path vs the pre-cutover behavior. Restored, on both fallback entry points (no-session and sdkTimingPersist-returned-unhandled): - Resize live DOM patch dropped the conditional data-playback-start/media-start attr — restored so a start-trim updates the preview's in-point immediately. - Move/resize fallback dropped the GSAP-position sync (shift/scaleGsapPositions) + reloadPreview — restored so server-path edits keep GSAP tweens in sync and refresh the preview (the SDK path folds both into setTiming). - Undo-coalesce drift: fallback enqueueEdit carried no coalesceKey while the SDK branch did — plumbed coalesceKey through persistTimelineEdit so undo granularity is identical on either path. - Documented the hasPbsAdjustment second clause + sdkTimingPersist before-capture transition limitation. Flag-off (dark launch) so this lands as one fix PR at the stack tip rather than restacking the mid-stack §3.2 commit. heygen-com#1500 review items: parity-harness gap already closed at the tip (arc/unroll recast-vs-acorn parity added); blockRemoveRange flagged 'potential' but verified correct (no comma residue on any block position). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sdk): retire duplicate removeGsapKeyframe keyframeIndex variant (review heygen-com#1498) EditOp had two removeGsapKeyframe members with the same discriminant but different shapes (keyframeIndex vs percentage) — TS can't discriminate them and a handler could get the wrong shape. Per both reviewers (option 2): retire the keyframeIndex variant. It had no production caller (Studio dispatches percentage only); removed the dead by-index handleRemoveGsapKeyframe + simplified the dispatcher. resolveKeyframe stays (setGsapKeyframe still uses keyframeIndex). Converted the one by-index test to the percentage API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(studio): gate ALL cutover persist paths on the flag — true dark launch (review heygen-com#1469 finding #6) Only sdkCutoverPersist (style/text/attr) checked STUDIO_SDK_CUTOVER_ENABLED. sdkTimingPersist, dispatchGsapOpAndPersist (every GSAP op) and sdkDeletePersist guarded only on `!sdkSession` — and useSdkSession opens a session by default for shadow/selection, so timing/GSAP/keyframe/delete cutover was ALWAYS live regardless of the flag. Flipping the flag OFF could not disable it, so the data-loss bugs in those paths (single-prop wipe, wrong-keyframe match, tween collapse, arc strip) ship LIVE on merge instead of being dark-launched. Added the flag guard at all three chokepoints → flag OFF returns false → callers fall back to the legacy server path. Makes the stack genuinely dark-launchable: merge is now a no-op in prod, and the remaining cutover correctness bugs become flip-prerequisites rather than merge-blockers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core,sdk): correct 8 GSAP write-path review findings (heygen-com#1539) Eight correctness bugs from the SDK-cutover review. Several were cases where BOTH writers were identically wrong, so the recast-vs-acorn parity suite stayed green; the new tests assert the real-world-correct result, not agreement. - #2 findKfPropByPct: match the CLOSEST keyframe within tolerance, not the first within 2% — removing/updating 50% on 0/49/50/100 no longer hits 49%. - #3 handleSetTiming: shift each tween by the start DELTA and scale duration by the clip-duration RATIO per-tween, instead of writing absolute newStart/ newDuration onto every tween (which collapsed staggers and blew durations). - #4 enableArcPath: insert motionPath via appendRight at the object start so the insertion can't collide with the x/y remove-range end (which made MagicString discard the append and emit '{}'). - #5 splitAnimationsInScript: compute the inherited baseline in a forward pre-pass so the split-spanning midpoint sees earlier tweens (the reverse write loop is kept for stable count-suffixed ids). - #9 unrollDynamicAnimations: preserve non-target loop-body statements (e.g. tl.set initial-state) per iteration instead of overwriting the whole loop. - #10 buildMotionPathObjectCode (both writers): emit the cubic form when segment curviness varies so per-segment curviness survives, not just segments[0]. - #11 readLastWaypointXY: handle UnaryExpression so negative destination coords are recovered when disabling an arc path. - #15 no-bang: removed every `!` non-null assertion in the touched files, replaced with guards/fallbacks. Tests: gsapWriter.reviewFixes.test.ts (#2/#4/#5/#9/#10/#11) and mutate.gsap.test.ts setTiming GSAP-sync block (#3). All fail on the base and pass after the fix; tsc + full core/sdk suites + parity stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(studio): SDK cutover review fixes — merge tween props, stabilize debounce, serialize gsap writes, on-disk undo baseline, self-write identity Addresses 5 SDK-cutover review findings (studio-only): - #1 useGsapPropertyDebounce: editing one GSAP tween property no longer drops the tween's other animated props. setGsapTween REPLACES the property set, so merge the single edit into the tween's CURRENT properties (read from the SDK doc) before dispatching, mirroring the legacy server merge. - #7 useGsapPropertyDebounce: stabilize the flush callback by reading sdk deps from a ref instead of an unmemoized literal, so a parent re-render mid-edit no longer tears down + flushes the debounce (one commit/undo entry per render). - #8 sdkCutover/useGsapScriptCommits: route SDK gsap-write persists through the same per-file keyed serializer the legacy commitMutation uses, so concurrent same-file read-modify-writes can't interleave and lose an edit. - #12 sdkCutover/useTimelineEditing: capture the exact on-disk bytes as the undo 'before' for timing/GSAP persists (matching the style/delete paths) instead of a normalized SDK serialize() re-emit that reformatted the whole file on undo. - #14 useSdkSession/sdkSelfWriteRegistry: discriminate a cutover echo from an undo write by CONTENT identity (registered self-write hash), not just the 2 s timestamp window — an undo write always reloads the SDK session. Tests: useGsapPropertyDebounce(.test), useGsapPropertyDebounceFlush.test, sdkSelfWriteRegistry.test, and new sdkCutover.test cases; each reproduces the review scenario and asserts the corrected behavior (verified red before fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core): extract split/collapse helpers to satisfy no-fallow-ignore rule The #5 (split) and #15 (no-bang guards) fixes pushed splitAnimationsInScript and removeAllKeyframesFromScript over fallow's complexity threshold, and a fallow-ignore had been added to splitAnimationsInScript. Per the hard rule (never ignore — fix), extracted buildSpanningSplit + applyTweenSplit (split) and buildCollapsedFlatVars (collapse), and removed the ignore. Both functions now under threshold; fallow new-only gate reports 0 new findings. Behavior unchanged — core 1811 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(studio): pin dark-launch flag-gate contract (review heygen-com#1539, Rames/Via) flag OFF ⇒ sdkTimingPersist / sdkGsapTweenPersist (GSAP-op chokepoint) / sdkDeletePersist all return false even with a valid session → legacy fallback. The prod flag-flip rests on this contract; sdkCutover.test.ts only mocks the flag TRUE, so a future gate refactor could silently re-enable cutover on flag-off without failing CI. This sibling file mocks it FALSE and locks the three guards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(studio): leading flag-gate on sdkGsapTweenPersist (review heygen-com#1539 nit, Via) The add-op getElement existence check ran before the inner gate, so flag-off did an SDK touch before falling back. Lead with the flag guard to match the other three chokepoints — flag-off is now a clean no-op at every entry point. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): unroll-preservation regressions — non-for loops + AST index substitution (review R2) The #9 unroll-preservation fix had two confirmed regressions: - Non-for loops (forEach/for-of/for-in/while): loopIndexVarName returns null, so substitution no-op'd and preserved siblings kept a now-undefined loop variable (e.g. `item`) → ReferenceError at render. Now returns null for those forms → caller falls back to the blanket loop overwrite (drops siblings, valid code). The #9 fixture only used `for(let i…)` so it never caught this. - substituteLoopIndex did a \bvar\b regex over raw source including string literals, corrupting selectors like ".row-i" → ".row-0". Now AST-based: substitutes only real Identifier uses, skipping string literals and non-computed member/key positions (extracted isIndexBindingPosition helper to stay under the fallow complexity threshold — no ignore added). Two regression tests added (forEach no-dangling-var; for-loop string-literal intact). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sdk,core): unrollDynamicAnimations rejects empty element list (R1 #1501b) An empty `elements` array has no unrolled form — the writer would overwrite the loop/statement with zero tween calls, silently deleting the animation. - gsapWriterAcorn: unrollDynamicAnimations returns the script verbatim on an empty list (no-op instead of a destructive overwrite). - validateOp: reject unrollDynamicAnimations with empty elements as E_INVALID_ARGS so callers get a clean error rather than silent corruption. - Tests: writer no-op on []; validateOp E_INVALID_ARGS on []. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(sdk): cache draft element in applyDraft, drop HTMLElement casts (R1 #1490a) applyDraft runs at 60fps during a drag but re-ran doc.querySelector on every call — the _draftEl/_draftId fields were only consumed by commit/cancel, never to skip the query. Reuse the tracked element when the id matches and the node is still connected; re-query only on id change or detach (iframe reload). Retypes _draftEl to HTMLElement | null (only ever set from querySelector<HTMLElement>), which removes the `as HTMLElement` casts in commitPreview / _clearDraft. Test asserts a repeated same-id drag queries once. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sdk,core): round-3 correctness — unroll AST safety, single-dispatch undo, empty-arg guards, persist decouple Addresses the highest-severity round-3 review findings: - gsapWriterAcorn unroll (R3 #1/#2/#9): the round-2 AST-substitution fix emitted invalid GSAP for object shorthand `{ i }` (→ `{ 0 }`) and shadowed inner bindings (→ `for(let i=0;0<3;0++)`), and silently dropped sibling statements on non-`for` loops (forEach/for-of). The unroll now REFUSES (no-ops, leaving the dynamic loop intact) whenever siblings can't be safely reproduced — a non-`for` loop, an unmodeled statement, or an unsafe index use — instead of dropping or corrupting. Plain `for` loops with safe siblings still unroll. - session single-dispatch undo (R3 #5/#11): _dispatch now reverses the inverse patch list (parity with batch()). A single op emitting order-dependent inverse patches — a nested parent+child removeElement, an aliased multi-target — undid forward and dropped the child subtree / landed on an intermediate value. - materializeKeyframes empty-array (R3 #10): the unguarded twin of the just-fixed unrollDynamicAnimations. Writer no-ops on an empty keyframe list; validateOp rejects it as E_INVALID_ARGS (shared gsapScriptMissing helper). - history:false persist decouple (R3 #4): persist (auto-save) no longer lives inside the history-enable block, so opting out of SDK undo no longer silently disables all disk writes (data-loss trap for heygen-com#1496's flag consumers). Tests: unroll refuse cases (shorthand/shadow/forEach) + safe-for-loop regression; nested removeElement undo; materializeKeyframes writer no-op + validateOp reject; history:false-still-persists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): stripGsapForId re-parses per removal so all tweens for a deleted element are stripped (R3 #3) Animation ids are count-based (positional), so removing one tween renumbers the survivors. stripGsapForId captured every matching id from a single up-front parse then removed against the mutating script — after the first removal the later ids were stale and silently no-op'd, leaving an orphaned tl.to() referencing the just-deleted element. Now re-parse after each removal and strip the first still-matching animation until none remain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): gsap writer — keyframe ease routing, convert preserves delay, addLabel dedup (R3 #7/#8/#12) - #7: updateAnimationInScript routes an ease update on a keyframe tween to keyframes.easeEach (per-keyframe), not a top-level ease that GSAP ignores — the user's keyframe-easing edit was silently a no-op. - #8: convertToKeyframesFromScript now preserves every non-editable vars key (delay/callbacks/stagger/yoyo/…) verbatim via preservedVarsEntries instead of rebuilding from the GsapAnimation object, which had no `delay` field and dropped it — shifting the tween's start time. - #12: addLabelToScript moves an existing same-named label (overwrites its position) instead of appending a duplicate; duplicates made removeLabel over-remove (it deletes every match, including a pre-existing label). Tests: easeEach routing, delay preservation, addLabel move-not-duplicate + hand-authored-dup removal. Updated the old "no dedup contract" corpus test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sdk): handleSetTiming #domId + data-duration sync; validateOp resolves ids + arc/selector (R3 #6/#13, CF2 #15/#16) CF2 #15: handleSetTiming re-synced GSAP tweens only when the selector matched the element's hf-id. The common #domId-targeted tween (authored by the Studio panel) never matched, so moving/resizing a clip via the SDK timing path left its animations unsynced. Now match the tween selector against the DOM id too. CF2 #16: handleSetTiming read/wrote only data-end. Clips authored with data-duration (what the runtime prefers) got a fresh data-end beside a stale data-duration (no playback change) and oldDuration=null collapsed the GSAP duration-scale ratio to 1. Now read duration preferring data-duration, and write back to whichever attribute the clip uses (timingPath gains a "duration" field). R3 #13b: deleteAllForSelector compared selectors with strict === and missed the alternate quote style ([data-hf-id='x'] vs "x"); now quote-insensitive. R3 #6/#13a: validateOp now resolves the animationId for id-bearing GSAP ops (E_TARGET_NOT_FOUND instead of a misleading ok that no-ops at apply), and updateArcSegment validates the arc is enabled + the segment index is in range. Tests: #domId move sync, data-duration resize + scale, quote-insensitive delete, unresolved-id rejection, arc-segment preconditions. Updated the loose-can() test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core,sdk): name the acorn-node type alias; keyToPath round-trips timing.duration (R3 #14) - gsapWriterAcorn: replace the bare `: any` AST-node annotations with the named `type Node = any` alias, matching the established convention in gsapParserAcorn.ts / gsapInline.ts ("acorn ESTree nodes are structurally untyped"). Documents intent and is greppable; type-identical (zero runtime change). A full ESTree typing is a deliberate architecture decision the codebase has not taken and is out of scope here. - patches: keyToPath/timingPath now include the "duration" timing field added for the data-duration resize fix, so a timing.duration override round-trips on T3 replay instead of being dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sdk): cascadeRemoveAnimations re-parses per removal (R4 — SDK twin of #3) cascadeRemoveAnimations captured every matching animation id from a single up-front parse, then removed against the mutating script — the SDK-side twin of the stripGsapForId bug (R3 #3). Animation ids are positional, so removing the first tween for an element renumbered the survivors and the stale later ids no-op'd, orphaning those tweens on the just-removed element. Now re-parse after each removal and strip the first still-matching animation until none remain. Also adds the reviewer's defense-in-depth test: an aliased multi-target setStyle (same id twice) undoes to the original, not the intermediate (exercises the single-dispatch inverse reversal from R3 #5/#11). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…eygen-com#1738) * feat(cli): add skills version check, update, and freshness manifest Give the HyperFrames skill bundle a content fingerprint so agents and users can tell whether installed skills are the latest version, on any platform that can run the CLI. - skills-manifest.json (repo root): per-skill sha256 over the whole skill directory; minimal {source, skills}, no version/timestamp so it is fully deterministic. Generated by scripts/gen-skills-manifest.ts. - `hyperframes skills check` [--json]: compares installed skills to the manifest; exits non-zero when something is outdated (agent/CI gate). - `hyperframes skills update`: thin wrapper over `npx skills update`. - Passive nudge on render/lint/validate when skills are stale (24h cache, same opt-out as the CLI self-update notice). - "latest" resolved via `git ls-remote` + SHA-pinned raw URL to dodge GitHub raw-CDN lag, falling back to the main branch URL. - CI job + lefthook hook keep skills-manifest.json in sync with skills/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): add execFile to child_process mock in skills test skills.test.ts mocks node:child_process but only declared execFileSync and spawn. Loading skills.js transitively loads skillsManifest.ts, which runs promisify(execFile) at module load, so vitest threw on the missing execFile named export. Add a bare stub — these tests never invoke it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): init installs all skills; skills update pulls the full set Make `hyperframes init` the single place skills are pulled in full, and make "update" mean "get everything" rather than "refresh what's there". - init now always installs/refreshes ALL skills (incl. ones not yet present) instead of prompting "Install AI coding skills?" — opt out with `init --skip-skills`. Both the interactive and non-interactive paths pass `--all --yes` so the complete set is fetched. - `hyperframes skills update` switches from `npx skills update` (which only refreshes already-installed skills) to `skills add --all`, so it installs missing skills too — the same install step init runs. - SKILL.md documents init-installs-all and the new update semantics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): skills check treats missing skills as needing an update The full skill set is now the goal (init and `skills update` both pull all, including ones not installed), so a partial install is no longer "a choice" — it's something to fix. - diffSkills: updateAvailable is now true when anything is outdated OR missing (local-only still doesn't count). So `skills check` exits non-zero — and renders "Update:" instead of "up to date" — whenever a skill is missing, not just when one is stale. - The passive render/lint/validate nudge follows suit: it now counts missing alongside outdated ("N skills out of date or missing"), tracked via a new skillsMissingCount cache field. - SKILL.md documents the stricter check. Note: platforms that intentionally vendor only a subset of skills (e.g. a Codex snapshot) will now see check report non-zero. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): install/update skills straight from the GitHub repo `skills add owner/repo` can resolve through the skills.sh registry, which lags behind the repo — so `update` could install a stale version while `check` (which resolves latest directly from GitHub) keeps reporting "outdated", an endless loop. Switch the install source to the full GitHub URL (https://github.com/heygen-com/hyperframes), which makes `skills add` git-clone the repo directly at latest main, bypassing the registry. This covers `hyperframes skills`, `hyperframes skills update`, and `init`'s skill install — all of which go through SOURCES. Now install/update and check agree on what "latest" means. The init "install skills" hint now points at `npx hyperframes skills update` so the manual path uses the same GitHub-direct fetch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): init checks skills against GitHub, installs only when stale `hyperframes init` now runs the skills version check first and only (re)installs when something is outdated or missing — instead of unconditionally re-pulling every time. Re-running init on an already-current project is now a no-op ("skills are already up to date"). - New ensureSkillsCurrent() helper, shared by both the interactive and non-interactive init paths (no duplicated install logic). - The check resolves "latest" straight from GitHub (same source the install uses); best-effort — if it can't reach GitHub it installs anyway. - SKILL.md updated to describe the check-then-install behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(cli): address skills manifest review feedback From the PR review (points 1, 2, 4, 5): 1. Remove the `local-only` skill status. checkSkills only ever hashes manifest-listed skills, so a local-only status could never appear in the end-to-end output — and making it appear would wrongly flag unrelated skills (the `.../skills` dir is shared across sources). diffSkills now reports only on manifest skills; skills on disk that aren't in the manifest are ignored. 2. Drop the redundant per-directory sort in listFilesSorted — the single final out.sort() is what guarantees a deterministic hash (verified: manifest unchanged). 4. resolveLatestManifest local-path detection now uses path.isAbsolute, so Windows absolute paths (C:\...) are treated as local instead of falling through to a remote fetch. 5. fetchManifest validates the response shape (asSkillsManifest) instead of a blind `as` cast, so a CDN error page served as 200 fails with a clear error rather than a cryptic crash later in diffSkills. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): strict skills update + auto-discover any agent host Address PR review (Magi blocker + James/Rames robustness): - Blocker (Magi): `skills update` is the documented recovery path for `skills check || skills update`, but it delegated to installAllSkills() which swallowed missing-npx and failed `skills add` as "skipped", exiting 0 even when nothing changed. Add a strict mode that throws on failure; update sets a non-zero exit (init stays best-effort). New tests simulate a non-zero `skills add` (exit 1) and the success path. - Robustness (James/Rames #2): the upstream `skills` CLI installs into ~72 agent conventions; a hard-coded list (4, or even 11) can't track that. Replace defaultSkillRoots with discoverSkillRoots — it scans cwd + $HOME for any `<host>/skills/<manifest-skill>/SKILL.md` (plus the XDG `.config/<host>/skills`), so detection is structural and future-proof, no closed list. agentFromDir infers the host from the path. - Tests (Rames #3): temp-fixture detection tests for every convention × {project, global}, scope priority, claude-code preference, the no-install case, the --dir override, and an unknown/new host (proving the no-closed-list property). - Docs (Rames #4/#5): SKILL.md notes init's best-effort GitHub round-trip; findRepoManifest climbs 16 levels (was 8) for deep monorepos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): resolve CodeQL file-system race + de-flake Windows npx test Two CI fixes: - CodeQL (high, js/file-system-race) at gen-skills-manifest.ts: the existsSync(outPath) precheck followed by writeFileSync(outPath) is a check-then-write race. Read the committed manifest directly in a try/catch instead (missing/unreadable ⇒ "no committed manifest"), so there's no precheck to race against. Behavior is unchanged. - Windows Tests: npxCommand.test.ts's real `npx --version` smoke test cold-starts slower than vitest's 5s default on Windows runners and timed out. Give the test 60s headroom (and a 30s exec timeout). Kept as a real execution check — mocking would reduce it to a tautology. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): repair garbled npx smoke-test timeout comment The explanatory comment for the 60s timeout was scrambled across the callback/timeout arguments, failing oxfmt --check (and thus preflight, which in turn skipped preview-parity and failed the regression gate). Move it above the it() call so it no longer sits between call arguments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
… + multi-agent mirror (heygen-com#1753) * feat(cli): add skills version check, update, and freshness manifest Give the HyperFrames skill bundle a content fingerprint so agents and users can tell whether installed skills are the latest version, on any platform that can run the CLI. - skills-manifest.json (repo root): per-skill sha256 over the whole skill directory; minimal {source, skills}, no version/timestamp so it is fully deterministic. Generated by scripts/gen-skills-manifest.ts. - `hyperframes skills check` [--json]: compares installed skills to the manifest; exits non-zero when something is outdated (agent/CI gate). - `hyperframes skills update`: thin wrapper over `npx skills update`. - Passive nudge on render/lint/validate when skills are stale (24h cache, same opt-out as the CLI self-update notice). - "latest" resolved via `git ls-remote` + SHA-pinned raw URL to dodge GitHub raw-CDN lag, falling back to the main branch URL. - CI job + lefthook hook keep skills-manifest.json in sync with skills/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): add execFile to child_process mock in skills test skills.test.ts mocks node:child_process but only declared execFileSync and spawn. Loading skills.js transitively loads skillsManifest.ts, which runs promisify(execFile) at module load, so vitest threw on the missing execFile named export. Add a bare stub — these tests never invoke it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): init installs all skills; skills update pulls the full set Make `hyperframes init` the single place skills are pulled in full, and make "update" mean "get everything" rather than "refresh what's there". - init now always installs/refreshes ALL skills (incl. ones not yet present) instead of prompting "Install AI coding skills?" — opt out with `init --skip-skills`. Both the interactive and non-interactive paths pass `--all --yes` so the complete set is fetched. - `hyperframes skills update` switches from `npx skills update` (which only refreshes already-installed skills) to `skills add --all`, so it installs missing skills too — the same install step init runs. - SKILL.md documents init-installs-all and the new update semantics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): skills check treats missing skills as needing an update The full skill set is now the goal (init and `skills update` both pull all, including ones not installed), so a partial install is no longer "a choice" — it's something to fix. - diffSkills: updateAvailable is now true when anything is outdated OR missing (local-only still doesn't count). So `skills check` exits non-zero — and renders "Update:" instead of "up to date" — whenever a skill is missing, not just when one is stale. - The passive render/lint/validate nudge follows suit: it now counts missing alongside outdated ("N skills out of date or missing"), tracked via a new skillsMissingCount cache field. - SKILL.md documents the stricter check. Note: platforms that intentionally vendor only a subset of skills (e.g. a Codex snapshot) will now see check report non-zero. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): install/update skills straight from the GitHub repo `skills add owner/repo` can resolve through the skills.sh registry, which lags behind the repo — so `update` could install a stale version while `check` (which resolves latest directly from GitHub) keeps reporting "outdated", an endless loop. Switch the install source to the full GitHub URL (https://github.com/heygen-com/hyperframes), which makes `skills add` git-clone the repo directly at latest main, bypassing the registry. This covers `hyperframes skills`, `hyperframes skills update`, and `init`'s skill install — all of which go through SOURCES. Now install/update and check agree on what "latest" means. The init "install skills" hint now points at `npx hyperframes skills update` so the manual path uses the same GitHub-direct fetch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): init checks skills against GitHub, installs only when stale `hyperframes init` now runs the skills version check first and only (re)installs when something is outdated or missing — instead of unconditionally re-pulling every time. Re-running init on an already-current project is now a no-op ("skills are already up to date"). - New ensureSkillsCurrent() helper, shared by both the interactive and non-interactive init paths (no duplicated install logic). - The check resolves "latest" straight from GitHub (same source the install uses); best-effort — if it can't reach GitHub it installs anyway. - SKILL.md updated to describe the check-then-install behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(cli): address skills manifest review feedback From the PR review (points 1, 2, 4, 5): 1. Remove the `local-only` skill status. checkSkills only ever hashes manifest-listed skills, so a local-only status could never appear in the end-to-end output — and making it appear would wrongly flag unrelated skills (the `.../skills` dir is shared across sources). diffSkills now reports only on manifest skills; skills on disk that aren't in the manifest are ignored. 2. Drop the redundant per-directory sort in listFilesSorted — the single final out.sort() is what guarantees a deterministic hash (verified: manifest unchanged). 4. resolveLatestManifest local-path detection now uses path.isAbsolute, so Windows absolute paths (C:\...) are treated as local instead of falling through to a remote fetch. 5. fetchManifest validates the response shape (asSkillsManifest) instead of a blind `as` cast, so a CDN error page served as 200 fails with a clear error rather than a cryptic crash later in diffSkills. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): strict skills update + auto-discover any agent host Address PR review (Magi blocker + James/Rames robustness): - Blocker (Magi): `skills update` is the documented recovery path for `skills check || skills update`, but it delegated to installAllSkills() which swallowed missing-npx and failed `skills add` as "skipped", exiting 0 even when nothing changed. Add a strict mode that throws on failure; update sets a non-zero exit (init stays best-effort). New tests simulate a non-zero `skills add` (exit 1) and the success path. - Robustness (James/Rames #2): the upstream `skills` CLI installs into ~72 agent conventions; a hard-coded list (4, or even 11) can't track that. Replace defaultSkillRoots with discoverSkillRoots — it scans cwd + $HOME for any `<host>/skills/<manifest-skill>/SKILL.md` (plus the XDG `.config/<host>/skills`), so detection is structural and future-proof, no closed list. agentFromDir infers the host from the path. - Tests (Rames #3): temp-fixture detection tests for every convention × {project, global}, scope priority, claude-code preference, the no-install case, the --dir override, and an unknown/new host (proving the no-closed-list property). - Docs (Rames #4/#5): SKILL.md notes init's best-effort GitHub round-trip; findRepoManifest climbs 16 levels (was 8) for deep monorepos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): resolve CodeQL file-system race + de-flake Windows npx test Two CI fixes: - CodeQL (high, js/file-system-race) at gen-skills-manifest.ts: the existsSync(outPath) precheck followed by writeFileSync(outPath) is a check-then-write race. Read the committed manifest directly in a try/catch instead (missing/unreadable ⇒ "no committed manifest"), so there's no precheck to race against. Behavior is unchanged. - Windows Tests: npxCommand.test.ts's real `npx --version` smoke test cold-starts slower than vitest's 5s default on Windows runners and timed out. Give the test 60s headroom (and a 30s exec timeout). Kept as a real execution check — mocking would reduce it to a tautology. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): repair garbled npx smoke-test timeout comment The explanatory comment for the 60s timeout was scrambled across the callback/timeout arguments, failing oxfmt --check (and thus preflight, which in turn skipped preview-parity and failed the regression gate). Move it above the it() call so it no longer sits between call arguments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): install skills once globally + symlink-mirror to every agent The previous install path sprayed a full ~6.7MB skill copy into each of the ~70 agent conventions `skills add --all` knows (a fresh init produced 40+ dirs / 341MB, incl. a stray dotless `agent/` from the Eve convention). Install ONCE, globally, as one faithful copy, then symlink it everywhere: - `skills add <url> --skill '*' --global --agent claude-code universal --copy` lands real files in ~/.claude/skills (Claude Code reads this at global priority) and ~/.agents/skills (the shared universal store). - mirrorGlobalSkills() fans that store out to every OTHER installed agent's GLOBAL dir (~/.cursor/skills, goose -> ~/.config/goose/skills, ...) — but only for agents present on the machine (marker dir exists), so nothing is sprayed. Unix: per-skill relative symlink into the store (one source of truth, auto-fresh on update); Windows: copy (symlinks need admin / Developer Mode there — the same fallback upstream and gstack make). Why global: skills are framework-general knowledge, not project content; Claude Code (and most agents) prioritize the personal/global scope, so the global copy is the one actually loaded — and it installs once instead of multiplying per project. The per-agent dir list is GENERATED from upstream's src/agents.ts at a pinned tag (the `skills` package exports nothing importable), committed as agentDirs.generated.ts and resolved env-faithfully at runtime (XDG_CONFIG_HOME / CODEX_HOME / CLAUDE_CONFIG_DIR honored). Regenerate with `bun run --cwd packages/cli gen:agent-dirs` when the pin moves. Covers all 70 agents that define a global dir (eve/promptscript define none); the bare project-dir agents (openclaw, astrbot) are namespaced globally, so the stray-`agent/` footgun is gone. `skills check` now scans global ($HOME) before project (cwd) to match the runtime load order — so it reports on the copy the agent will really use, not a stale project copy a newer global install silently overrides. Test plan: - skills.test.ts: install spawns the global --copy args, never --all; update stays strict + exits non-zero on failure. - skillsMirror.test.ts: Unix relative symlinks, Windows copy, XDG_CONFIG_HOME honored, install-owned stores skipped, marker-gating, idempotent refresh, generated-table shape. - skillsManifest.test.ts: check is global-first. - Full CLI suite green (981); oxlint / oxfmt / tsc clean; gen:agent-dirs --check clean (offline + network produce byte-identical output). - Benchmark (isolated HOME, local CLI): claude+hermes and all 70 agents — ~/.claude + ~/.agents real (19 each), every installed agent's global dir = 19 symlinks into the store, zero spray into unseeded agents, check global-first. (The 9 "outdated" check reports are the separate skills.sh registry lag, not this change.) - .fallowrc.jsonc: exempt the codegen script's inherent parser complexity and the parallel-case duplication in skillsManifest.test.ts (same rationale the config already uses for SlideshowPanel.test.ts / hyperframes-player.test.ts). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): install skills with --full-depth so a fresh install reads as current `skills add <url>` without --full-depth fetches from the skills.sh registry blob ("Fetching skills"), which lags GitHub main by hours — so a freshly installed/updated set read as ~9 skills "outdated" right after install, and `skills update` couldn't fix it (it re-fetched the same stale blob → death loop). --full-depth switches it to a real `git clone` of HEAD ("Cloning repository"), the only path that yields the genuine latest. - Add --full-depth to the global install args. Verified (isolated HOME): blob path → 10 current / 9 outdated; --full-depth → 19 current / 0 outdated. - The clone is heavier than the blob fetch, so set GIT_LFS_SKIP_SMUDGE=1 (skills are text; the repo's LFS objects are unrelated binaries the install doesn't need) and raise the spawn timeout 120s → 300s. - Correct the stale comment that claimed a full URL already bypasses skills.sh — it doesn't; only --full-depth does. Benchmark (skills-bench, local CLI): B.death-loop and J1.init-detect-and-refresh flip FAIL → PASS (install/update/init now 19/0); mirror smoke reports 19 current / 0 outdated. (spine still reflects the raw documented `skills add <slug>` command — the upstream skills.sh path, not this CLI.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(skills): drop --skip-skills from workflow init so new projects refresh skills The creation workflows scaffolded with `hyperframes init … --skip-skills`, which skipped the skills currency check. Now that init installs globally, is a no-op when already current, and pulls the genuine latest (via --full-depth), there's no reason to skip it: removing --skip-skills means every new project runs the check and refreshes the global skill set from GitHub when it's stale. Add a one-line note to each workflow (embedded-captions, faceless-explainer, motion-graphics, music-to-video, pr-to-video, product-launch-video) and the hyperframes-cli + /hyperframes router explaining what init does. skills-manifest.json regenerated by the pre-commit hook to match the edited skill bundles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): scope agent mirror to HyperFrames' own skills, not the whole store mirrorGlobalSkills listed every */SKILL.md in ~/.claude/skills and fanned them out — but that store is shared, so a user's gstack / personal / company Claude skills would get symlinked (and, since linkOrCopy removes the target first, could overwrite a same-named skill) into Cursor / Codex / Goose / etc. Scope the mirror to HyperFrames' own skills via the upstream lock's source attribution — the same definition the prune already uses (skillsAttributedToSource) — never a directory listing. New hyperframesSkillNames() reads the global lock and returns only skills attributed to heygen-com/hyperframes; the mirror intersects that allow-list with what's in the store. Empty (no lock / nothing attributed) → mirror nothing, never everything. Also fixes the cosmetic "director(ies)" log typo (now singular/plural-aware) and extracts the fan-out into mirrorToInstalledAgents() to keep installAllSkills under the complexity gate. Regression: skillsMirror.test.ts asserts a foreign gstack skill in the store is neither mirrored out nor allowed to replace another agent's same-named skill; the skills-bench harness seeds ~/.claude/skills/gstack and asserts it never leaks to any agent. 1045 CLI tests + lint/types/fallow green. Addresses Magi's request-changes on heygen-com#1753. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…gen-com#1829) * feat(telemetry): unify CLI and Studio PostHog identity (Layer 1) Seed the CLI's anonymous distinct_id into Studio at launch so a developer's CLI and their Studio browser session resolve to the same PostHog person. Also unifies Studio's two previously-independent anonymous ids into one source of truth. Uses only the existing anonymous machine id (no new PII). - cli: inject window.__HF_CLI_DISTINCT_ID into the served index.html <head> (mirrors the existing __HF_STUDIO_ENV__ injection) + add a fallback GET /api/telemetry-identity endpoint. Only seeds when CLI telemetry is enabled; empty/no-op otherwise. - studio: new telemetry/distinctId.ts single source of truth; adopts the CLI-seeded id when present, else falls back to the existing per-browser localStorage id. Both Studio clients (studio:* and studio_*/render) now share this one id. * fix(telemetry): keep Studio distinct_id resolver fail-silent on getItem resolveStudioDistinctId read localStorage.getItem() outside a try/catch while every other external access in the module is guarded. In a storage-restricted context where the localStorage reference resolves but getItem throws, the resolver threw — breaking the module's fail-silent contract (telemetry must never break Studio). Guard the reads and treat a throw as "no id". Also drop an unnecessary `as` cast in the test per the repo CLAUDE.md convention (the optional global is already declared). * refactor(telemetry): address review feedback on identity unification - dedup safeLocalStorage/safeSessionStorage into utils/safeStorage.ts, used by both telemetry/config.ts and telemetry/distinctId.ts (Miga #6) - replace redundant `??=` with `=` in the no-storage branch; cachedId is guaranteed null there (Miga #2) - extract buildStudioHeadScripts() so the "identity script before env script" head-injection ordering is a pure, tested invariant (Miga #5) - add tests: head-script ordering + telemetry-off passthrough, and a Studio memoization test proving an adopted CLI id survives a later window.__HF_CLI_DISTINCT_ID reassignment (Rames) - clarify the XSS-escaping comment (both < and / escaped so no </script> sequence can form) (Miga #1)
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
… data-duration is optional (heygen-com#1830) * fix(runtime): auto-infer composition duration for CSS/WAAPI/Lottie so data-duration is optional The #2 render failure bucket ("Composition has zero duration") accounts for ~27K errors / ~7K affected users over 30 days (PostHog project 356858). Root cause: only GSAP timelines got their duration auto-detected — CSS, WAAPI, and Lottie compositions had no source of truth for total duration unless the author remembered to set data-duration on the root element, and the render engine hard-failed capture when neither was present. Adds getInferredDurationSeconds() to the CSS, WAAPI, and Lottie runtime adapters (packages/core/src/runtime/adapters/*.ts) — each reports the longest finite end time it can discover from its own animations (CSS: computed timing offset by data-start; WAAPI: effect.getComputedTiming().endTime; Lottie: totalFrames/frameRate or the player's own duration). Infinite/ unbounded animations correctly return null and still require data-duration. Wires this into the runtime's existing duration-floor resolution (resolveAdapterDurationFloorSeconds in runtime/init.ts), alongside the existing media-duration and authored-composition floors, so window.__hf.duration becomes positive without any author action for finite-duration non-GSAP compositions. Three.js is unchanged — no AnimationClip/AnimationMixer inspection exists in that adapter, so data-duration remains required there. Tightens frameCapture.ts's zero-duration fast-fail gate to also check hf.duration directly (not just the two authored signals), so a composition mid-inference isn't fast-failed before its adapter-derived duration lands. Adds a new lint rule (root_composition_missing_duration_source) that errors only on genuinely non-inferable cases: no animation signal at all, Three.js without data-duration, or an infinite/unbounded CSS or WAAPI animation without data-duration. Deliberately silent on finite CSS/WAAPI/Lottie animations, since the runtime now infers those — an autofix that "inserts the inferred value" was considered and rejected: every case the rule flags has no derivable value (an infinite spinner has no finite end time; a duration-less Three.js scene has nothing to measure), so any autofix would have to fabricate a placeholder, trading a loud correct failure for a silent wrong-length render. Updates the CSS/WAAPI/Lottie/Three adapter skill docs and the hyperframes-core determinism-rules/data-attributes references to document the new optionality and the runtime mechanism backing it. Verified end-to-end against the real render pipeline (not just unit tests): a CSS-only composition with a finite 3s animation, no GSAP timeline, and no data-duration now renders a correct 3.000s MP4 via `hyperframes render` (previously: "Composition has zero duration" failure). The infinite-CSS negative control still fails fast with a clear diagnostic, matching the new lint rule. Adds a file-level fallow health exemption for lottie.ts's pre-existing `seek` handler — unrelated to this change, but its line numbers shifted when new functions were added earlier in the file, tripping fallow's inherited-finding fingerprint (documented pattern already used elsewhere in .fallowrc.jsonc for the same reason). Known limitation: the static WAAPI usage detector in the lint rule (/\.animate\(\s*[\[$A-Za-z_]/) can miss unusual call shapes; it only affects whether the "no signal at all" branch fires, and errs toward NOT flagging (reducing false positives) rather than over-flagging. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(lint): close 3 correctness gaps in root_composition_missing_duration_source - Strip JS/CSS comments before scanning for GSAP/WAAPI/Three/Lottie/CSS animation signals, so a commented-out `.animate()` call or a commented `animation: ... infinite` rule can no longer satisfy the "has a duration source" check and mask a real zero-duration render failure. - Broaden the WAAPI detection regex to also match the object-literal (PropertyIndexedKeyframes) form of `.animate()`, e.g. `el.animate({ opacity: [0,1] }, { duration: 2000 })`, which the previous character class silently missed. Corrected the adjacent comment that incorrectly claimed this shape "can't be a false negative". - Fix hasInfiniteCssAnimation to stop false-positiving on animation NAMEs that merely contain the substring "infinite" (e.g. `infinite-spin`) by anchoring the `infinite` keyword with hyphen-aware boundaries instead of a bare `\b`. Also makes the longhand `animation-name` + separately declared `animation-iteration-count: infinite` pattern detected consistently. Adds targeted unit tests for each fixed false-positive/false-negative. * fix(runtime): keep finite duration signal when an unbounded animation coexists getInferredDurationSeconds in the CSS and WAAPI adapters returned null outright whenever any animation on the composition was unbounded (infinite iteration count), even when other finite animations on the same composition could still supply a valid duration. This disagreed with the new root_composition_missing_duration_source lint rule, which treats any animation-name as sufficient — so a composition mixing a finite fadeIn with a decorative infinite spin passed lint but still failed at render with "zero duration". Unbounded animations are now skipped when computing the max end time instead of short-circuiting the whole calculation. null is only returned when every animation on the composition is unbounded, i.e. there is no finite signal to fall back on at all. Co-Authored-By: Claude <noreply@anthropic.com> * docs(skills): fix table separator width in data-attributes.md oxfmt flagged the merged Composition Root table from the post-rebase merge of the auto-infer-duration docs onto main's reformatted table — the separator row was one dash short of the header width. * fix(lint): keep infinite-CSS duration rule strict but make its message honest Post-review (Vance): after the finite+infinite adapter fix, the runtime infers a length for a mixed finite+infinite CSS composition, but this lint rule still (intentionally) errors on it — an unbounded animation makes the intended total length ambiguous, so we require explicit data-duration. Keep that strictness (lint is advisory by default; it only blocks under --strict, and data-duration is the one duration signal guaranteed correct across every adapter, known and future). But the message wrongly claimed the render "will fail" — false for the mixed case, where the runtime falls back to the finite animation. Rewrite it to describe the ambiguity honestly, correct the rule's block comment, and add a mixed finite+infinite test asserting it still errors with an honest message. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…reaming over auto-parallel (heygen-com#2026) * feat(producer,cli): drawElement priority inversion — single-worker streaming over auto-parallel clamp:parallel eats 50% of local renders (1,326/fortnight; DE engagement stuck at 3.8%) by routing multi-worker renders to unverified screenshot capture. Benchmarks (2026-07-08, 4 comps x W1/W2/W3/W5) show that above the ~900-frame amortization crossover, single-worker VERIFIED drawElement streaming beats screenshot-parallel at EVERY worker count (2,380f: 66s vs 109-127s; 3,600f: 33s vs 39-56s; parallel scaling flattens past W2), while below it DE's fixed init cost loses by <=2.2s. - shouldPreferSingleWorkerDrawElement (exported predicate + 7 unit tests): inverts an AUTO-resolved multi-worker render to workerCount=1 when the comp matches the benchmarked configuration — default-on DE (darwin hardware clamp upstream), no compile gate, no forced-screenshot hint, mp4 output, single-worker streaming eligible, and totalFrames >= HF_DE_SINGLE_MIN_FRAMES (default 900; 0 disables). Explicit --workers N is always honored. - Inverted renders keep the probe session and land on the worker-encode streaming drain — the ONLY path with runtime self-verification, so this moves ~40% of previously-clamped renders onto the verified fast path. Comps that later hit an init-time gate (~1.5% of local renders) render single-worker screenshot streaming; accepted trade. - Telemetry: de_worker_inversion on render_complete (orchestrator -> perfSummary.workerInversion -> CLI), plus the worker_resolution observability checkpoint now records deWorkerInversion. Validation: e2e matrix on 2,381f comp — auto->5 workers inverted to 1, DE verified 4x inf PSNR, RENDER_OK; short comp (360f) auto stays 5-worker; explicit WORKERS=3 honored; HF_DE_SINGLE_MIN_FRAMES=0 disables. Canary suite 7/7 (PSNRs identical). renderOrchestrator tests 86/86. tsc/oxlint/oxfmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(producer): review fixes — inversion routing guards, calibration skip, retry revert Max code-review round on the inversion (13 confirmed findings): - Streaming spawn-failure disk fallback now clamps default-on drawElement (deClampReason=disk_path, DE-mode probe closed) exactly like the pre-capture clamp — previously it carried useDrawElement=true onto the unverified disk path, the hole the verified-path confinement exists to close, newly reachable for every inverted render. - Predicate gained the routing knowledge it was blind to: layered/HDR and shader-transition comps (drawElement never runs there), supersampling (deviceScaleFactor>1 init gate), a probe session whose init gates already disengaged DE, and the PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true explicit parallel-DE opt-in (honored like --workers N). - Eligibility is evaluated BEFORE capture calibration and skips it when the inversion pins workers to 1 regardless of the estimate — the throwaway calibration browser + sample captures cost ~41s on the 2,381-frame benchmark comp (auto render: 111.6s -> 70.1s total). - Self-verify retry reverts the inversion: the re-render returns to the pre-inversion parallel screenshot path (disk) instead of single-worker screenshot streaming, the slowest shape for exactly the comps drawElement damages. - HF_DE_SINGLE_MIN_FRAMES="" (set-but-empty) now falls back to the 900 default instead of aliasing the 0 kill switch. - Timeout advisory uses the RESOLVED worker count — an inverted render that times out no longer prints "Retry with --workers 1" (the configuration that just failed). - Telemetry: deWorkerInversion recorded in capture observability (failed renders are attributable), emitted as literal false when not fired (queryable denominator), and the drawElement perf input shape is one exported DrawElementPerfInput type instead of three copies. - Tests: requestedWorkers undefined (the value production actually passes) + the four new predicate guards; 91/91. Validation: e2e auto render — calibration skipped (deInversionEligible), inversion fires, DE verified 4x inf, total 70.1s (was 111.6s); HF_DE_SINGLE_MIN_FRAMES=0 restores calibration + parallel; canary suite 7/7 (PSNRs identical); tsc/oxlint/oxfmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(producer,cli): review round 2 — loss-cohort telemetry, retry-plan helper, boundary tests - de_worker_inversion is now a tri-state string ("inverted" | "reverted" | "none") instead of a boolean: the self-verify retry marks the render "reverted" rather than resetting to false, so the dashboard can segment the lost-inversion cohort first-class instead of inferring it from deSelfVerifyFallback + frame-count joins (james-russo #1). - The retry rollback is extracted to resolveInversionRetryPlan (pure, exported) with unit coverage: pre-inversion worker-count restore, streaming re-resolution (multi-worker retry -> disk), "reverted" state, null when never inverted (james-russo #2). - WOULD_RESOLVE_MULTI_WORKER named constant replaces the bare sentinel 2 (james-russo #5); minFrames: -1 boundary case added (miga #3). 94/94 renderOrchestrator tests; tsc/oxlint/oxfmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(producer,cli): emit de_pre_inversion_workers for the parallel counterfactual The ramp-down decision needs "did DE beat the parallel render it displaced", not just "did DE beat single-worker screenshot". Emitting the worker count the auto-resolution chose BEFORE the inversion pinned it to 1 makes the parallel counterfactual computable per render (screenshot ms/frame from the verify samples / W x the measured parallel-efficiency curve). Set only when the inversion fired. Smoke: 2,381f auto render -> de_worker_inversion="inverted", de_pre_inversion_workers=5, mode=drawelement, verify armed 4. 99/99 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
## What Fixes five reported false-positive/false-negative patterns in the WCAG contrast audit (`hyperframes validate --contrast`): 1. **SVG fill vs. text color** — foreground read from CSS `color` instead of SVG `fill`. 2. **Cross-component color bleed** — background estimate bleeds into a neighboring panel/layer. 3. **Backdrop-filter glass text** — background estimate misses the blur/tint and reads the raw backdrop. 4. **Partially-overlapping translucent decoration** — a decorative shape inside or partly touching the text's bbox goes undetected. 5. **Solid-fill pill/button** — investigated, did **not** reproduce; already handled correctly by the existing own-background ancestor walk. Not touched. ## Why The audit estimated an element's background two ways: - foreground: always `getComputedStyle(el).color` — wrong for SVG `<text>`/`<tspan>`, which is painted via `fill`, an independent CSS property. - background: a 4px pixel ring sampled just **outside** the text's bounding box, with a fallback to an ancestor's opaque `background-color` for solid pills/buttons. The ring is a proximity heuristic. It's wrong whenever what's immediately outside the text differs from what's actually behind it: - text near the edge of its own panel, with a differently-colored sibling panel/layer just past the bbox — the ring samples the neighbor. - a `backdrop-filter: blur()` glass panel sized only a couple pixels larger than the text — the ring exits the panel into the raw, unblurred, untinted backdrop. - a translucent decoration that only partially overlaps the ring, or sits entirely **inside** the bbox — invisible to the ring regardless of size. ## How **SVG fill (#1):** elements inside an `<svg>` (`el.ownerSVGElement`) now prefer the computed `fill` when it resolves to a solid `rgb()`/`rgba()` color, falling back to `color` for paint values that aren't a plain color (`none`, `context-fill`, gradient/pattern refs). **Cross-comp bleed / glass blur / partial decoration (#2–#4):** replaced the ring-sampling + own-background-ancestor-walk heuristic with a two-phase capture: 1. `__contrastAuditPrepare()` walks the DOM, computes each candidate's foreground (unchanged logic from #1), and **hides that element's own text paint** (`color`/`fill` → `transparent`, layout-neutral — no reflow). 2. The caller takes **one** screenshot with the glyphs invisible (same number of screenshots as before — just moved after the hide instead of before it). 3. `__contrastAuditFinish(imgBase64, time, candidates)` restores the original paint immediately, then samples the **real composited pixels directly inside each element's own bbox** — no proximity heuristic needed, since these are the exact pixels that were behind the glyphs. This is a real architectural change to `contrast-audit.browser.js`'s calling contract (single `__contrastAudit` → `__contrastAuditPrepare`/`__contrastAuditFinish`), with `validate.ts`'s `runContrastAudit` updated to match, including a try/finally restore-safety-net so a mid-loop screenshot/decode failure can't leave a later sample auditing a page with stale hidden text. Mirrored the identical change in `skills/hyperframes-creative/scripts/contrast-report.mjs`, which duplicates the same DOM-walk/sampling logic (not just the WCAG math). There, the **visible** frame for the human-facing overlay image still comes from the producer's normal `captureFrameToBuffer` path (unchanged); only the **background-sampling** capture is a plain `session.page.screenshot()` taken after hiding text — deliberately bypassing `captureFrameToBuffer`, whose static-frame dedup cache knows nothing about the DOM mutation and would hand back a stale pre-mutation buffer. **Solid-fill pill (#5):** reproduced a rounded pill/button with a busy page background outside it. The existing own-background ancestor walk already resolves the pill's declared `background-color` correctly regardless of the rounded corners — confirmed via repro, both before and after this change report the identical (correct) result. No fix needed; left untouched, and this case is covered by the new architecture too (would give the same right answer even without the ancestor-walk fallback). Added `packages/cli/src/commands/contrast-sample.ts` (mirroring the existing `contrast-bg.ts`/`contrast-fg.ts` pattern) hosting the pure sample-rect/grid-point computation, unit tested — the browser-injected scripts can't import it directly, so it's kept in sync by hand, same convention as the rest of this file. ## Test plan - [x] Unit tests: `contrast-fg.test.ts` (SVG fill resolution), `contrast-sample.test.ts` (sample-rect clamping/degenerate cases), plus the full `packages/cli` suite (1424 tests) passes, including an updated `layout-audit.browser.test.ts` case that called the old single-function `__contrastAudit` API directly. - [x] Manual verification — standalone `puppeteer-core` harness against real `chrome-headless-shell`, one minimal HTML fixture per pattern, comparing the audit's reported ratio/verdict against a hand-constructed ground truth: - **SVG fill**: `fill:white` / no `color` on black bg → before: `fg=rgb(0,0,0)` ratio `1:1` (false FAIL); after: `fg=rgb(255,255,255)` ratio `21:1` (correct PASS). - **Cross-comp bleed**: text on a black sibling highlight box 2px larger than the text, white page bg outside it → before: `bg=rgb(255,255,255)` ratio `1.23:1` (false FAIL); after: `bg=rgb(0,0,0)` ratio `17.14:1` (correct PASS). - **Glass blur**: black text on an 18%-white-tinted `backdrop-filter: blur(14px)` panel over a yellow/blue gradient, panel only ~2px larger than the text → before: `bg=rgb(0,64,255)` (raw gradient color, blur/tint completely missed) ratio `3.18:1` (false FAIL); after: `bg=rgb(159,160,165)` (correct blurred/tinted blend) ratio `8.05:1` (correct PASS). - **Partial decoration**: text 92%-covered by a translucent white badge on a dark bg → before: `bg=rgb(16,16,16)` (ring never touches the badge, which sits entirely inside the bbox) ratio `17.45:1` (false PASS); after: `bg=rgb(171,171,171)` (correctly detects the badge) ratio `2.11:1` (correct FAIL). - **Solid pill sanity**: unaffected — `bg=rgb(10,10,10)` ratio `19.8:1` before and after. - [x] End-to-end: ran the actual `hyperframes validate --contrast` CLI command (via `tsx src/cli.ts`) against a real scaffolded project containing all 4 patterns simultaneously — only the genuinely-failing case (the 92%-covered decoration) is reported (`1.09:1`, need `3:1`); the cross-comp-bleed, glass-blur, and solid-pill cases are correctly silent. A second vanilla scaffold with plain white-on-dark text produces zero false positives. - [x] `oxlint`, `oxfmt --check`, and `tsc --noEmit` all pass on the changed files.
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…trial gaps Six findings from a third max-effort code review, focused on the previous commit's fixes: 1. --batch-concurrency N>=2 runs genuinely concurrent renderLocal() calls (Promise.all workers in batchRender.ts), which can't safely share the trial's one process-wide env var + module flag — a row finishing first could tear down the env var/flag mid-render for a sibling row still in flight. Rather than attempt to make shared process-global state safe under real concurrency, added RenderOptions.disableDeParallelRouterTrial and set it whenever batchConcurrency > 1 — the trial simply isn't offered when it can't be evaluated safely. 2. maybeConsumeDeParallelRouterTrial's "outcome === undefined" no-op guard almost never fired: aggregateDrawElement (perfSummary.ts) defaults parallelRouter to the string "none" for every render, whether or not drawElement/the router ever engaged — never undefined. Every ordinary render below the router's own frame threshold (the common case) was ticking the render-count backstop, tripping DE_PARALLEL_ROUTER_TRIAL_MAX_RENDERS after 25 completely unrelated renders that never touched the router. Now treats "none" the same as undefined. 3. isDeParallelRouterTrialBlocked relied solely on shouldTrack(), which memoizes its verdict once per process — during a long --batch run, a `hyperframes telemetry off` issued from another terminal mid-batch would never be observed. Restored a direct config.telemetryEnabled check (read fresh every call, unlike shouldTrack()'s cache) alongside it. 4. maybeConsumeDeParallelRouterTrial's config write had no way to detect a losing race against a concurrent process — added a verify-and-retry loop (write, re-read fresh, retry up to 3x if a concurrent writer landed in between) that narrows the window further without a full file-locking rewrite. 5. The trial could arm before the first-run telemetry disclosure (showTelemetryNotice) was guaranteed to have printed — that notice runs via a fire-and-forget, unawaited dynamic import in cli.ts with no ordering guarantee relative to the render command. Rather than touch that pre-existing async bootstrap chain, gated the trial on config.telemetryNoticeShown: it simply never offers itself on a fresh install's very first invocation. 6. Added a dedicated config.test.ts exercising readConfig/readConfigFresh/ writeConfig through the REAL module (node:fs mocked with an in-memory fake, not a HOME-env hack) — readConfigFresh's cache-bypass and the type-guarded boolean/number parsing had zero coverage through the real implementation before this. Also fixed the test fixture that was supposed to cover finding #2 but used an unrealistic `drawElement: {}` shape instead of the real `{ parallelRouter: "none" }` aggregateDrawElement actually produces. Extracted applyDeParallelRouterOutcome to keep maybeConsumeDeParallelRouterTrial under the repo's complexity gate after adding the retry loop. 11 new/updated tests in render.test.ts (56 total) + 7 new tests in config.test.ts. Verified against fallow's audit gate clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…k order = default paint order) (heygen-com#2380) * feat(studio): mirror canvas z-order actions into timeline lanes, badge z overrides Track order = default paint order; authored z = advanced override. - timelineZMirror.ts: pure resolver mapping a successful z-menu action to a timeline lane move — closest track in the action's direction that is free over the clip's whole span, else a new lane adjacent to the crossed neighbor; temporal-overlap scope (default pending product sign-off, see module doc); visual zone only; same-file reference scoping; persistTrack via the shared authored-space rules. null for non-clips (menu stays z-only) and at-extreme/no-overlap cases. - useCanvasZOrderTimelineMirror.ts: after the z commit resolves, the mirror persists the lane move through the same machinery as a timeline lane drag (optimistic store update, authoredTrack refresh, rollback); inserts reuse commitTrackInsert's renumber via a shared buildTrackInsertEdits core. Both writes share one coalesce key (zReorderCoalesceKey) and fold into ONE undo entry (test proves it over the real history reducer). The mirror never triggers the lane->z stacking sync, so it cannot fight the z values the action just set. - timelineZOverride.ts + TimelineClip badge: clips whose paint order contradicts lane order among temporally-overlapping same-context visual neighbors (laneIsAbove XOR paintsAbove, the stacking-sync predicates) show a 'z' badge — authored z overrides are surfaced instead of silently disagreeing with the timeline. - Timeline.tsx track derivations extracted to useTimelineTrackDerivations (600-line cap). * fix(studio): fold mirrored z-order gestures into one undo entry across slow persists Live verification caught the z write and the mirrored lane write splitting into two undo entries: the mirror runs after the z persist's server round trip, which exceeds editHistory's default 300ms coalesce window under real latency (the unit test's deterministic clock sat inside it). zReorderCoalesceKey now mints a per-gesture-unique key (monotonic seq, the laneChangeGestureSeq precedent) and both records carry coalesceMs Infinity — distinct gestures can never merge, and one gesture always folds regardless of write latency. coalesceMs threaded through the persist chain alongside coalesceKey. Also hardens the existing lane-drag move->z fold, which had the same latent split. Fold test now simulates a 400ms gap (failed before the fix, passes after); a two-separate-gestures test asserts two entries. * feat(studio): flashless lane mirror, z-order menu icons, close-gap track menu - Track-only batch moves (the z-mirror's lane hop and the insert renumber) skip the GSAP fallback round-trip and the preview reload entirely — the renderer never reads data-track-index, and the live DOM patch + optimistic store update cover the UI. Mixed batches keep current behavior. Kills the canvas blink on mirrored Bring/Send actions (live-verified: an iframe-scoped marker survives the whole gesture). - The four z-order menu items get 16px stroke icons (single layer diamond + directional arrow for Forward/Backward; pierced two-layer stack for Front/Back); labels unchanged — they are the industry-standard names. - New track context menu on empty lane space: 'Close gap' (shifts the next clip and every clip after it on that lane left by the clicked gap's width; leading gaps count, so a single clip with empty space before it compacts to 0) and 'Close all gaps' (whole lane contiguous from 0). Pure gap math in timelineGaps.ts; persists through the drag path's atomic batch move (one undo per action); refuses when a clip that must shift is locked; items disable when there is nothing to close. * fix(studio): rebind-only preview sync for unmutated timing edits, classical z-menu order Timing edits that rewrote NO GSAP positions (gap closes and moves of selector-addressed caption clips, zero-delta batches, comps without a rewritable script) full-reloaded the preview — and the rerun-current-scripts attempt was wrong for real compositions: re-executing init-style scripts (three.js scenes, caption engines) is exactly the unsafe case, verified live by doubled init warnings and a fallback reload anyway. The correct observation: when mutated === false the existing __timelines are still valid — only the runtime's clip visibility windows are stale, and the live DOM timing attributes were already patched. So the no-mutation path now runs applySoftReloadFinalization only (seek + __hfForceTimelineRebind + manual-edits reapply), extracted from the soft-reload machinery — zero script execution. This also un-blinks comps with no GSAP script at all, which previously always remounted. Rewritten-script soft reloads, cannot-soft-reload, otherFileChanged, and mutation failures keep their existing behavior. gsapSoftReload's undo/redo restore section moved verbatim to gsapUndoRestore.ts for the 600-line cap. Also: z-order menu items reordered to the classical arrangement (Bring to Front, Bring Forward, Send Backward, Send to Back). Live-verified on a three.js-heavy composition: Close-all-gaps shifted 4 caption clips with correct cumulative amounts, the preview iframe was never remounted (marker survived), and one undo reverted everything. * fix(studio): bound forward/backward mirror to a one-element step User-specified semantic: Bring Forward / Send Backward move the clip past EXACTLY ONE element. The mirror's lane target is now bounded by the next temporally-overlapping element beyond the crossed neighbor: a free lane strictly between the two is taken (closest to the neighbor), and when they are back-to-back a new track is inserted immediately beyond the crossed element — never past the second one. Previously the resolver took the closest free lane anywhere beyond the neighbor, which could carry the track past a second element while the z action only stepped past one — a track/paint contradiction our own zOverride badge would flag. Front/back keep whole-set semantics (past everything; back stays above the audio zone). End-to-end test pins the 3-stacked case through commitZMirrorLaneMove to the persisted renumbered tracks. * feat(studio): permanent gap-menu rows with hover and click-select gap highlights - TrackGapContextMenu always renders both rows; an inapplicable action dims with a tooltip ("No gap here" / lock reason / "No gaps on this track") instead of vanishing into a one-item menu. Width badge only when a gap exists under the pointer. - Hovering an ACTIONABLE row highlights the strip(s) it would close in the timeline: the single gap for Close gap, every current gap (leading included) for Close all gaps. New resolveAllGapIntervals in timelineGaps.ts reports present-state intervals (epsilon-tolerant, overlap-safe), distinct from resolveAllTrackGaps' post-compaction starts. - Click-selecting a single clip paints a quieter tint over its lane's gaps (suppressed for marquee multi-selection and during drags; the gap-menu hover wins on its own lane). Derivation lives in useTimelineGapHighlights with the pure buildTimelineGapStrips exported and unit-tested. - Strips render in TimelineCanvas with the drop-placeholder geometry (row top + clip inset), dashed accent for hover, faint tint for selection. - Timeline.tsx stayed under the 600-line cap by extracting the scroll-viewport plumbing (ResizeObserver width + shortcut-hint sync) into useTimelineScrollViewport, behavior unchanged. * feat(studio): stronger capcut-style timeline zoom steps One button press / pinch gesture now moves the zoom meaningfully: step factors 1.25x/0.8x -> 1.5x/(2/3) (kept reciprocal so in+out round-trips) and pinch sensitivity 0.0035 -> 0.007. Addresses "zooming several times to get anywhere" feedback; cursor anchoring unchanged. * feat(studio): three-way z sync — layers drags mirror timeline lanes, panel tracks live z edits Completes the layers/canvas/timeline sync triangle: the Layers panel was the one surface whose reorders never reached the timeline, and the one that went stale when the other two wrote z flashlessly. - Layers drag -> minimal z + equal-jump lane mirror. handleReorder now uses the canvas menu's realization core via resolveZOrderReposition (one between-z write when a strict gap exists, band-safe scoped renumber otherwise) instead of computeReorderZValues' all-sibling stamp — that helper is deleted, completing the heygen-com#2347 unification follow-up. The drop then mirrors into a timeline lane move through the same machinery as the canvas menu (new resolveRepositionLaneMove: the clip lands on a free lane strictly between its NEW paint neighbors' lanes — nearest clip siblings in the desired render order, decorations skipped — else a track insert at that boundary; audio zone never crossed). Both writes share one per-gesture zReorderCoalesceKey with an unbounded fold window, so a drag is exactly ONE undo entry; useCanvasZOrderTimelineMirror's plumbing is factored into useMirrorLaneMoveCommit and reused by the new useLayerReorderTimelineMirror. A same-slot drop is a hard no-op (new order-equality guard in resolveZOrderReposition). - Panel staleness fix: flashless z commits (skipReload) reload nothing and bump no refreshKey, so the panel's z-sorted order went stale while paused. handleDomZIndexReorderCommit now bumps a store zEditVersion on apply AND rollback; the panel re-collects on it. Verified live: the panel re-sorts the instant a drag commits and again on undo. - Layer click reveal (useLayerRevealOverride): clicking a layer that stays hidden at the current frame (animation-parked opacity, non-clip display/visibility hides, hidden ancestors) temporarily forces the chain visible with live inline styles — exact priors restored on deselect, on another reveal, on play, and on unmount; never persisted (file diff == 0 verified live). Clips keep the existing seek-into-window behavior; the override applies on a short defer so a seek-revealed clip needs none. - layerOrdering's unused hasExplicitZIndex probe (zero callers) removed. Live-verified on a bed copy: a 2-position layers drag wrote exactly one element (z 6->23 + data-track-index 15->2), the timeline lane moved without a reload, and a single Cmd+Z restored the file byte-identically. * feat(studio): full-track selection highlight, borderless gap hover strips - Click-selecting a clip now lights the WHOLE lane minus its clips — leading gap, inter-clip gaps, and the open space after the last clip to the rendered end (new resolveLaneEmptyIntervals; displayDuration threaded into the strip derivation). Still click-only: any drag/resize suppresses the strips, and a marquee multi-select never shows them. - The gap-menu hover strips drop the dashed border (user feedback) — fill only, nudged to 0.18 alpha to keep the same visual weight. * feat(studio): selected layer paints on top via a reader-transparent z lift Clicking a layer in the Layers tab now shows the element as if it were at the very top of the stack while selected — whatever its authored z or panel position — extending the reveal override (which already forced hidden chains visible) with a temporary inline z lift: - liftElementToTop parks the TRUE effective z in data-hf-reveal-prior-z and writes a far-top inline z; a static element gets a layout-preserving position:relative with its prior parked in data-hf-reveal-prior-pos. Only the RENDERER sees the lift: all three studio z readers (readTimelineElementZIndex, getElementZIndex, readEffectiveZIndex) return the parked prior while the attribute is present, so the canvas z-menu, the zOverride badge, the lane mirror, the stacking sync, and the panel sort keep reasoning on the element's real z. - Strictly ephemeral: exact priors restored on deselect / another reveal / play / unmount, each property only while it still holds the value the override wrote (a later real edit is never clobbered). File diff == 0 verified live across a full lift/restore cycle. - A z-reorder commit CONSUMES an active lift (handleDomZIndexReorderCommit reads the parked position for its persist-position:relative static check, then drops the attributes) — the committed z becomes the truth and the later restore is a guarded no-op. * fix(studio): flashless undo/redo — three full-reload causes in the soft-restore path Cmd+Z blinked the canvas on essentially every undo. Three independent causes in applyUndoRestoreToPreview, each sufficient on its own: 1. Master-view path gate: activeCompPath is NULL at the master view, so the 'paths[0] === activeCompPath' eligibility check could never match the index.html restore and every default-view undo full-reloaded at the first gate. Normalized to the codebase-wide 'activeCompPath ?? "index.html"'. 2. Nested identity innerHTML check: the diff compared each identified element's innerHTML, but the composition root wraps every clip — any child change re-detected at the root rejected the restore. Change detection now compares only each element's OWN attribute surface; structure/text integrity is still guaranteed by the normalize-residual whole-doc pass (text nodes, added/removed elements, and un-identified attrs all remain after normalization and force the full reload). 3. id-only identity: elements addressed by data-hf-id / selector (no DOM id) fell outside the diff entirely. Identity is now id OR data-hf-id, with the live sync resolving either. Also stop re-running an UNCHANGED GSAP script: attribute-only restores (z, lane, timing, style — the overwhelmingly common undo) now use the rebind-only finalization (seek + __hfForceTimelineRebind + manual reapply, zero script execution — the same path as flashless timing edits), instead of tearing down and rebuilding live timelines or full-reloading when the script can't be scoped. A restore whose script text genuinely changed still re-runs it via applySoftReload, and structural restores (split/delete) still full-reload. Live-verified on the bed (iframe marker): gap-close undo AND redo both keep the iframe mounted, live DOM lands on the restored values, disk restored byte-identically. * feat(studio): left breathing pad before t=0, double zoom sensitivity again TRACKS_LEFT_PAD (48px) — the horizontal sibling of TRACKS_TOP_PAD: empty lane surface between the sticky gutter and the ruler's 00:00 / the first clips, scrolling WITH the content. - The lanes and the ruler realize it as a plain flow spacer between the sticky gutter cell and the time-mapped content div, so every content-relative computation (clip left = t*pps, beat lines, lane-menu time, clip drag deltas) is untouched by construction. - Canvas-space overlays shift by the pad: playhead (getTimelinePlayheadLeft), gap strips, drop placeholder, snap guide, range highlight, marquee clip rects, beat SVG; the insert line spans the pad. - Every pointer->time inverse subtracts it symmetrically: seekFromX, razor, range/marquee anchors, asset drops, and the zoom-anchor gutter basis; fit pps and the display width account for the consumed viewport width. - Live-verified: t=0 clip edge, the 00:00 tick, and the playhead line center all sit at GUTTER + TRACKS_LEFT_PAD, and a ruler click lands the playhead center exactly under the pointer. Also doubles the timeline zoom sensitivity again (user feedback after feel-testing the first bump): button steps 1.5x/(2/3) -> 2x/0.5, pinch 0.007 -> 0.014. * fix(studio): left pad renders as true empty space, not lane surface The pad before t=0 inherited each row's background and bottom border from the row wrapper, so it read as track lanes. Lane visuals now live on the cells: the sticky gutter keeps its own separator (header column stays delineated), the time-mapped content div carries the row background + separator, and the pad spacer stays transparent — bare shell background, no lines. The new-track insertion line also starts at the pad's end instead of crossing it. * fix(studio): no vertical line in the ruler band before 00:00 The ruler corner's right border drew the header-boundary line through the ruler strip, so the band didn't read as starting at 00:00. Dropped it — the boundary line belongs to the track rows below; the ruler stays completely clean from the panel edge to the first tick, matching the empty left pad. * refactor(studio): remove the timeline z-override badge User decision: the "z" chip on clips never earned its place — dropped entirely (timelineZOverride.ts + test deleted, TimelineClip badge rendering and the zOverrideKeys derivation/threading removed). This also eliminates the review's D2 finding at the root: the badge's cross-document comparison (stackingContextId ?? null collides across source files in the expanded view) produced false positives, and there is no longer a detector to mis-fire. overlapsInTime/paintsAbove lose their export (the badge was their only external consumer); the paint-order predicate itself is unchanged. * fix(studio): collision-free expanded child lanes and host-window gap floors Review findings D1 (blocker) and 4. - D1: buildChildElements assigned expanded children synthetic display rows as `host.track + index` — integers that can EQUAL a real clip's lane in another file (host on 0 with two children puts child #2 on 1). Lane grouping merges purely by track number, so the collision fused clips from different source files into one display lane, and lane-scoped actions (the gap menu) then batch-persisted a foreign file's clip. Children now take FRACTIONS strictly between the host's lane and the next integer — structurally unable to collide with any normalized lane, while still rendering as ordered rows under the host. Regression test pins the reviewer's exact two-file scenario. - Finding 4: gap math compacted toward absolute 0, but an expanded child's display time is host-anchored — close/compact could drag it before its host window and persist a wrong (even negative) local time. All gap functions now take a lane FLOOR (laneGapFloor: 0 for ordinary lanes, the children's expandedParentStart for child lanes — single-origin per lane post-D1), threaded through the menu model, hover highlights, selected-lane strips, and both commits. Close-gap shifts clamp at the gap's own left edge. * fix(studio): scope mirror references, insert writes, and crossed-neighbor identity Review findings 1, 2, and 3. - Finding 1: buildTrackInsertEdits normalized the FULL display set and persisted every shifted clip — writing host-lane numbers into OTHER composition files when expanded children were showing. The renumber write set is now the edited element's own source file (the sanctioned multi-write converges one FILE to lane space, never neighbors' files); foreign clips keep their authored tracks and re-derive display lanes. The locked-clip refusal scopes the same way. Expanded-origin elements refuse the insert outright (a new lane is a host-space renumber, meaningless in the child's file), and the mirrors restrict an expanded child's lane candidates to its own siblings' lanes — a sub-comp child still mirrors WITHIN its sub-comp (persisting the sibling's authored track) but can never land on a host lane with no same-file occupant. authoredTrackForLane's offset fallback rounds: fractional synthetic rows must never leak fractions into data-track-index. - Finding 2: the mirror comparison sets required only sameSourceFile, but a file can contain several CSS stacking contexts and leaf z is only comparable within one. Both resolvers now scope by samePaintScope — same source file AND same stackingContextId (the file check also stops null root contexts of different files from comparing equal in the expanded view). - Finding 3: the crossed-neighbor key was derived without selectorIndex, so duplicate class selectors (.sub) resolved to occurrence 0 — a different clip. The key now carries getSelectorIndex, matching how z-reorder entries derive theirs. * fix(studio): z-to-lane gestures are one serialized transaction gated on durable persists Review findings 5 and 7. - Finding 5: commitDomEditPatchBatches resolved successfully even when the server matched NO patch target — the z write never reached disk (the preview reloads to reconverge) yet the lane mirror still ran, desyncing track order from what actually paints. The commit now resolves a durability report ({allMatched, changed}; the save queue and commit types are generic over the result), and the mirror phase is skipped on allMatched === false. - Finding 7: the z persist rides the DOM-edit save queue while the lane move rides the timeline/SDK path — two queues, so a second rapid gesture's z write could land BETWEEN the first gesture's z and lane phases. Every z-to-lane gesture (canvas z-order menu AND Layers-panel drag) now runs through runZLaneGesture: a single module-level tail that serializes the COMPLETE two-phase transaction, with unit tests for ordering, the durability gate, and queue resilience to failed gestures. The timeline lane-drag's inverse (move-then-z-sync) shares its phases' await ordering already; cross-gesture serialization for that path is noted as follow-up. - LayersPanel's pure sort helpers moved to layersPanelSort.ts (600-line cap). * fix(studio): multi-clip GSAP batch mutations roll back on late failure Review finding 6. finishGroupTimingGsapFallback mutates files sequentially per clip; a late per-clip failure left the earlier rewrites on disk with no aggregate history entry — unreachable by undo. foldGsapMutationIntoHistory already snapshots every touched path before mutating; on a mutation failure it now restores each path whose disk content changed (all-or-nothing batch), reports restore errors without masking the original failure, and rethrows. Regression test drives a two-clip batch whose second rewrite fails and asserts the first clip's write is restored byte-identically. * fix(studio): scope mirror inserts to their lane zone * fix(studio): unify source-scoped clip identity * fix(studio): isolate track insert topology * fix(studio): harden timeline paint synchronization --------- Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…ompound Field signal ts=1784131903 (win32/x64, CLI 0.7.58, 156s UI-heavy): stable ONLY with four flags together — --workers 1 --no-browser-gpu --low-memory-mode + PRODUCER_ENABLE_STREAMING_ENCODE=false. Since --no-browser-gpu and --low-memory-mode already imply screenshot capture, three of the four flags are structurally coupled. Auto-detect the compound at resolveConfig time and disable streaming-encode on the caller's behalf; user explicit-set (PRODUCER_ENABLE_STREAMING_ENCODE or overrides.enableStreamingEncode) always wins. Composition duration is not known at the config layer, so the wire-up passes compositionDurationSec:undefined and the helper reduces to the three-condition compound (platform + softwareGpuForced + workers=1). The 4-arg helper stays exported for downstream callers that DO know duration (e.g., renderOrchestrator) and want the >120s guard. Trade-off documented in code + PR body: false positives possible for short (~<120s) Windows software-GPU single-worker renders. Mitigation is the explicit opt-in escape hatch. Emits a single [hyperframes] log line naming the trigger + how to opt back in, so operators can tell an auto-disable apart from an explicit opt-out. Adds streamingEncodeAutoDisabledOnWin32Compound internal provenance for downstream telemetry. Stack: PR #2 of 9 (base via/protocol-timeout-discoverability). Signed-off-by: Via
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…ch + preflight recompute Addresses R2 CHANGES_REQUESTED from Miga + Rames on PR heygen-com#2529: 1. Sibling-surface gap (blocker): `hyperframes cloudrun render{,-batch}`, `hyperframes lambda render{,-batch}` all advertised the same tier-only aliases (`1080p` / `hd` / `4k` / `uhd`) but normalized them to `landscape` and never set `outputResolutionAspectAgnostic`. The distributed plumbing PR heygen-com#2529 added received `undefined` from those callers, so portrait `1080p` still hit the original aspect-mismatch on Cloud Run / Lambda. Fix: introduce `resolveResolutionFlagPair` in `@hyperframes/parsers` (the single source of truth for the two-step normalize + aspect-agnostic detect) and route every distributed entrypoint through a shared `parseOutputResolutionFlag` CLI util so the alias signal now reaches `SerializableDistributedRenderConfig`. Studio Server keeps its canonical-only HTTP contract; that intent is now pinned in tests. 2. Preflight recompute (hardening): the earlier "downgrade aspect-mismatch" preflight cleared un-remapped mismatches, so IG 4:5 (non-preset aspect, no sibling) and portrait-4K comp + `--resolution 1080p` (remap + downsample) both slipped through to fail late in `resolveDeviceScaleFactor`. Now `checkRenderResolutionPreflight` computes the effective preset via `suggestMatchingPreset` (mirroring the compile stage's `adaptAspectAgnosticResolution`) and re-checks against that — only genuinely-fixable mismatches clear early. New tests pin both regressed input classes. 3. Docker forwarding boundary test (Miga's important #2): pinned `1080p` survives verbatim as `--resolution 1080p` in the Docker args so the in-container CLI can re-run `isAspectAgnosticResolutionAlias`. 4. Doc-nit (Miga): parsers/src/types.ts no longer references the nonexistent `resolveResolutionForComposition` — points at the actual remap helpers. Fallow: cloudrun.ts / lambda.ts share 390 lines of pre-existing structural symmetry (parallel AWS + GCP dispatchers), and lambda/render.ts + render-batch.ts declare parallel RenderArgs interfaces. Both re-flagged after threading the aspect-agnostic field through each surface; ignored with justification in .fallowrc.jsonc. lambda.ts's `run` and lambda/render.ts's `waitForCompletion` are pre-existing CRAP-score hotspots untouched by this PR — added under health.ignore. Co-Authored-By: Claude <noreply@anthropic.com> — Via
3 tasks
This was referenced Aug 11, 2026
3 tasks
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
@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)