feat(studio): consolidate into single OSS-ready NLE editor - #4
Closed
miguel-heygen wants to merge 1 commit into
Closed
feat(studio): consolidate into single OSS-ready NLE editor#4miguel-heygen wants to merge 1 commit into
miguel-heygen wants to merge 1 commit into
Conversation
Replace legacy frontend/backend split with single Vite-powered package. Absorb @hyperframes/ui-player into studio/src/player/. Features: - NLE layout: preview + timeline + composition drill-down (3 levels) - Source editor with CodeMirror (HTML/CSS/JS) - Bundled preview via @hyperframes/core/compiler - File watcher for hot reload on external edits - Playback speed control (0.25x-2x) - Extensible via slots (previewOverlay, timelineFooter) - Path traversal protection on all API endpoints - Support both hf-*/magic-edit-* postMessage sources
6 tasks
5 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>
This was referenced Apr 27, 2026
This was referenced May 6, 2026
teamkareem
referenced
this pull request
in teamkareem/hyperframes
May 6, 2026
…atch binding
Three issues in `bundleToSingleHtml` reported via Abhay's LLM-based code-validity
eval against the bundled output. Each is independently small; they share a single
PR because they're all artifacts of the bundler-output shape.
1. Empty `src=""` runtime placeholder (real bug)
`htmlBundler.ts:injectInterceptor` emitted
`<script data-hyperframes-preview-runtime="1" src=""></script>`
when no `HYPERFRAME_RUNTIME_URL` was configured. Empty `src` resolves to the
page URL itself; Chrome flags this as an infinite-fetch hazard. Three other
consumers (studioServer, validate, snapshot) post-process the placeholder to
substitute either a real URL or an inlined body — `bundleToSingleHtml` did
not, so the bundle wasn't actually self-contained despite the function name.
Fix: when no URL is configured, inline the runtime IIFE directly via
`getHyperframeRuntimeScript()`. Otherwise emit `src=…` as before.
2. Bare-semicolon lines between joined JS chunks (cosmetic)
Three sites used `chunks.join("\n;\n")` (body-script coalesce, local JS,
composition scripts) which produced a lone `;` on its own line between
chunks. Valid JS but a code smell. Replace with a `joinJsChunks()` helper
that ensures each chunk ends in `;` and joins on `\n`.
3. Empty `catch (_err) {}` in compositionScoping.ts (lint-noisy)
The `_err` underscore prefix signals "intentionally swallowed" but bundle-time
linters often don't honor that convention. Replaced with `catch { /* ... */ }`
(no binding, explanatory comment) — same behavior, no rule fires.
Tests: 2 new regression guards (runtime-not-empty-src, no-bare-semi) plus
existing tests updated to reflect the new inlined-runtime shape (the previous
"runtime block must not contain getElementById" assertion no longer holds
because the inlined body itself uses getElementById; replaced with a more
specific "author script not merged into runtime tag" check).
Issue #4 from the original report (Unterminated string at line 1111 col 18,
char 65497) was not directly reproducible after applying these fixes — esbuild
parses all 4 inline scripts in the rebundled output cleanly. The unterminated-
string symptom was likely a downstream artifact of the bare-semicolon joining
or the empty-src placeholder confusing the lint tool. If the original symptom
persists on a clean re-run against the fixed bundle, will open a follow-up PR
with a focused repro.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tasks
This was referenced May 12, 2026
Merged
Merged
This was referenced May 15, 2026
This was referenced May 17, 2026
3 tasks
meefs
pushed a commit
to meefs/hyperframes
that referenced
this pull request
Jul 16, 2026
Field signals ts=1784139267 (win32 15-injection blank later clips, injection-count-scaled) + ts=1784144554 (darwin 147-authored-clip visibility gap, authored-clip-count-scaled). check/snapshot passes, final MP4 has blank/missing clips. Extends heygen-com#2474's videoCount:0 probe with per-clip captured-vs-expected frame ratio + threshold fail-loud gate at render finalization. Stack: PR heygen-com#4 of 9 (base via/darwin-goto-nav-timeout-hint). Signed-off-by: Via <via@heygen.com>
This was referenced Jul 16, 2026
This was referenced Jul 21, 2026
This was referenced Jul 25, 2026
3 tasks
This was referenced Jul 27, 2026
ukimsanov
added a commit
that referenced
this pull request
Aug 5, 2026
…omplete
HoverVideo now: (1) exposes a focusable button that toggles/plays sound by
keyboard, touch and pointer — hover is an enhancement, not the only path;
(2) gates the source behind an IntersectionObserver so offscreen cards no longer
download/decode, releasing the buffer on exit; (3) still blocks autoplay under
reduced motion while allowing voluntary playback via the button. Routed all 30
Thirty-Days cards through it (hasAudio={false} for the 12 silent ones) so no raw
<video autoPlay> bypasses the motion guard. Flagged by Magi (P1 #4/#5/#6).
This was referenced Aug 5, 2026
ukimsanov
added a commit
that referenced
this pull request
Aug 5, 2026
…tart layers - edit-operations: dispatch() does not consult can() (session.ts:608 -> applyOp with no validation; a no-timeline addGsapTween/addLabel is a no-op but a missing target still writes via selector fallback). Use Rames's wording: call can() first and skip on failure — no false 'applies nothing' guarantee. - timing-and-animation: the second E_NO_GSAP_TIMELINE site — gated setGsapTween on an error it cannot return and called shipped parser code 'a later phase'. Rewrote to gate addGsapTween (which can return it); dropped the stale can() comment in types.ts:595. - html-schema: data-playback-start is read by runtime, Studio and CLI (Studio also writes it, timelineEditingHelpers.ts); only the compile path is media-start only. Document the layered precedence instead of calling it runtime-only. Round-2 findings from Magi (#2/#3/#4) and Rames.
dahans-msft2
referenced
this pull request
in dahans-msft2/hyperframes
Aug 6, 2026
…atch binding
Three issues in `bundleToSingleHtml` reported via Abhay's LLM-based code-validity
eval against the bundled output. Each is independently small; they share a single
PR because they're all artifacts of the bundler-output shape.
1. Empty `src=""` runtime placeholder (real bug)
`htmlBundler.ts:injectInterceptor` emitted
`<script data-hyperframes-preview-runtime="1" src=""></script>`
when no `HYPERFRAME_RUNTIME_URL` was configured. Empty `src` resolves to the
page URL itself; Chrome flags this as an infinite-fetch hazard. Three other
consumers (studioServer, validate, snapshot) post-process the placeholder to
substitute either a real URL or an inlined body — `bundleToSingleHtml` did
not, so the bundle wasn't actually self-contained despite the function name.
Fix: when no URL is configured, inline the runtime IIFE directly via
`getHyperframeRuntimeScript()`. Otherwise emit `src=…` as before.
2. Bare-semicolon lines between joined JS chunks (cosmetic)
Three sites used `chunks.join("\n;\n")` (body-script coalesce, local JS,
composition scripts) which produced a lone `;` on its own line between
chunks. Valid JS but a code smell. Replace with a `joinJsChunks()` helper
that ensures each chunk ends in `;` and joins on `\n`.
3. Empty `catch (_err) {}` in compositionScoping.ts (lint-noisy)
The `_err` underscore prefix signals "intentionally swallowed" but bundle-time
linters often don't honor that convention. Replaced with `catch { /* ... */ }`
(no binding, explanatory comment) — same behavior, no rule fires.
Tests: 2 new regression guards (runtime-not-empty-src, no-bare-semi) plus
existing tests updated to reflect the new inlined-runtime shape (the previous
"runtime block must not contain getElementById" assertion no longer holds
because the inlined body itself uses getElementById; replaced with a more
specific "author script not merged into runtime tag" check).
Issue #4 from the original report (Unterminated string at line 1111 col 18,
char 65497) was not directly reproducible after applying these fixes — esbuild
parses all 4 inline scripts in the rebundled output cleanly. The unterminated-
string symptom was likely a downstream artifact of the bare-semicolon joining
or the empty-src placeholder confusing the lint tool. If the original symptom
persists on a clean re-run against the fixed bundle, will open a follow-up PR
with a focused repro.
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
## 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
Field signals ts=1784139267 (win32 15-injection blank later clips, injection-count-scaled) + ts=1784144554 (darwin 147-authored-clip visibility gap, authored-clip-count-scaled). check/snapshot passes, final MP4 has blank/missing clips. Extends heygen-com#2474's videoCount:0 probe with per-clip captured-vs-expected frame ratio + threshold fail-loud gate at render finalization. Stack: PR #4 of 9 (base via/darwin-goto-nav-timeout-hint). Signed-off-by: Via <via@heygen.com>
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/ui-playerintostudio/src/player/@hyperframes/core/compilerpreviewOverlay,timelineFooter)hf-*/magic-edit-*postMessage sourcesTest plan
cd packages/studio && pnpm devstarts the studio