mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
4a4903b49a8e1c8cf5ffa91add6c1d9032e79317
134
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4a4903b49a |
feat(skills): group core skills in the skills add picker (#2412)
Add a `core-skills` entry to .claude-plugin/marketplace.json declaring the core skill set (the /hyperframes router, the hyperframes-* domain skills, and media-use). The upstream vercel-labs/skills CLI reads that entry's `skills` array for its interactive picker: the core set renders under a "Core Skills" group and everything else falls into "Other", so a human running `npx skills add heygen-com/hyperframes --full-depth` can tell the always-needed core set apart from the on-demand creation workflows — mirroring the core/on-demand tiers `hyperframes skills update` already enforces (isCoreSkill in skillsManifest.ts). The array deliberately lives on a separate marketplace entry, NOT on plugin.json or the `hyperframes` entry: Claude Code treats a manifest `skills` array as that plugin's skill allowlist (verified against claude CLI), so attaching it to the full plugin would narrow it from all skills to the core 8. As a side effect the new entry is itself a coherent Claude Code plugin — `core-skills@hyperframes` installs just the core set — while `hyperframes@hyperframes` keeps auto-discovering everything. A new pin test keeps the marketplace list in lockstep with isCoreSkill and the skills/ tree (alongside the existing FALLBACK_CORE_SKILLS pin), and asserts the full plugin carries no allowlist. Agent installs are unaffected — the upstream CLI detects agent environments and installs non-interactively, and the hyperframes CLI always passes explicit --skill flags. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6933e8acda |
fix(cli): consolidate snapshot and frame diagnostics (#2402)
* fix(snapshot): preserve exact requested times * fix(cli): fail video snapshots without FFmpeg * fix(cli): honor navigation timeout in diagnostics * fix(cli): preserve snapshot alpha and create shot dirs |
||
|
|
b98463ae3a |
fix(cli): consolidate layout and contrast audit correctness (#2401)
* fix(cli): respect transparent image pixels in occlusion audit * test(cli): cover contained image letterboxing * fix(cli): account for text strokes in contrast checks * fix(cli): honor text overflow opt-outs * fix(cli): skip contrast on transparent backdrops * chore(skills): refresh generated manifest |
||
|
|
4495cb7355 |
feat(cli): stable public URL for hyperframes publish (re-publish updates the same link) (#2363)
* feat(cli): stable-URL re-publish (send owned id, honest UX, --update, team id file) Resolve a stable project id (committed team id > machine store > mint), send it when authenticated so an owned re-publish updates the same URL in place, and persist the server id+url. Report updated-vs-created honestly, add --update to target a project explicitly, write a committable .hyperframes/project.json so a team shares one link, and show the prior URL on re-publish. * test(cli): env-gated E2E round-trip for stable-URL re-publish Publish -> edit -> re-publish asserts one URL with updated content against a live EF (HYPERFRAMES_E2E_API_URL + an authenticated runner); skipped otherwise. * fix(cli): real team space, auth-gate --update/--space, safe team-file write - --space + committed .hyperframes/project.json (projectId+spaceId) send X-Space-Id so a team converges on one link; personal space stays the default for solo users - --update/--space error when unauthenticated, and warn loudly when a resolved-but-invalid token silently downgrades to a new anonymous URL (no more generic-tip-only) - team-file write in its own try/catch so a read-only dir can't fake 'Publish failed' - parseUpdateTarget handles scheme-less URLs + query/hash; X-Space-Id on metadata only (not S3 PUT) - share readJsonRecord across the local + team descriptors * test(cli): e2e team-space convergence + cross-space hijack guard * test(cli): unit-test parseUpdateTarget url shapes (export for test) * fix(cli): warn on committed-team miss too, not just --update |
||
|
|
d04bcbf7c4 |
Merge pull request #2360 from heygen-com/via/issue-1-webm-alpha-probe
fix(cli): detect WebM alpha lost when ALPHA_MODE tag is written without side data |
||
|
|
cb69d3fe00 | fix(render): surface structured outcomes (#2153) | ||
|
|
7f4eaeb568 |
feat(cli): coordinate-frame layout findings in check (#2354)
* feat(cli): coordinate-frame layout findings in check Four production compositions shipped with 100-600px layout drift, each a different coordinate-frame confusion the check graded info or missed entirely: viewport pixels written as container left/top, gsap x/y treated as absolute position, a -350px margin fighting flex centering, and stage-relative path coords drawn into a nested SVG. Three new layout findings close the class: - positioned_out_of_parent: an absolute/fixed element rendering mostly outside its positioning ancestor (warning) — the parent needs no overflow clipping, which is what let container_overflow miss it. - box_out_of_canvas: a painted panel breaching the canvas (warning) — text is canvas_overflow's, media is frame_out_of_frame's, painted boxes were nobody's. - connector_detached: a connector path whose endpoints land far from every anchorable element (warning) — measured coordinates drawn into an SVG with a different origin. canvas_overflow additionally promotes from info to warning when held across samples AND the breach exceeds 5% of the canvas. All three are persistence-tiered and respect data-layout-allow-overflow. Verified against the four incident compositions: every one now surfaces its drift as held warnings (previously: info or silence). * fix(cli): harden coordinate-frame findings against review false positives Reworks all three findings after two-lens review (adversarial FP hunt in real Chrome + maintainer pass): - escaped_container (was positioned_out_of_parent): uses offsetParent (transform-aware, skips fixed-as-canvas), exempts fully-detached callouts within an attachment allowance while still flagging touching-but-mostly-outside drift. - panel_out_of_canvas (was box_out_of_canvas): paint alone qualifies (flat solid panels were a false negative), fully off-canvas rects are parked entrances and stay silent, pointer-events:none marks decorative layers, hero-sized breaches warn while small bleeds stay info. - connector_detached: endpoints via getPointAtLength + getScreenCTM (viewBox, preserveAspectRatio, group transforms, every command type), defs/marker/clipPath subtrees skipped, word-boundary connector naming, containment tier limited to opaque non-ancestor targets (a text-bearing wrapper contains its own diagram's endpoints). - canvas_overflow promotion requires partial visibility — a fully off-canvas rect is a parked entrance, not drift. Verified: the four incident compositions still surface their drift as held warnings; the review's false-positive repros (fixed HUD, callout, parked entrance, corner bleed, marker arrowheads, g-transform and viewBox-scaled connectors) are clean at warning level. Docs and the CLI skill reference now describe the coordinate-frame findings. * fix(cli): panel ownership is geometric — direct-text panels were a silent false negative A painted panel whose direct text stays in-bounds while its box breaches the canvas produced neither finding: canvas_overflow measures the text range and panel_out_of_canvas skipped every own-text element. Skip the panel finding only when the element's own text ALSO breaches (that geometry belongs to canvas_overflow); pin the message/fixHint wording of all three findings with positive assertions; document the SVG-internal anchor blind spot. * fix(cli): classify panel decoration by paint kind, not pointer-events pointer-events:none exempted the framed-painting incident's gold frame layers — hero content that happens to disable hit-testing. Decoration is now gradient-only paint (spotlights, textures, vignettes); url() images, solid fills and borders are content regardless of pointer-events. * fix(cli): add fixHint to the test-local AuditIssue shape * fix(cli): gradient stops decide content vs decoration; ownership matches canvas_overflow's tolerance A gradient with any solid stop (alpha >= 0.6) is content — heroes and cards painted with linear-gradient were invisible under the blanket gradient exemption; all-translucent stops (spotlights, vignettes) stay decoration. The text-ownership check now uses the audit tolerance that canvas_overflow itself fires at, making the contract strict-mutex: any text breach past that tolerance cedes the element, so a shallow 20px text breach no longer double-reports. |
||
|
|
56dcc4e47a |
fix(cli): accept 1–3-frame short WebMs + add direct pixel-probe tests
Address Miguel's R1 blocker + Rames/Miga's testability nit: - The `-frames:v 3` decode samples AT MOST 3 frames; a legitimate 1- or 2-frame WebM (256 or 512 bytes) was returned as `undefined` (probe failure), silently skipping the advisory even when every available pixel was opaque. Accept any positive whole-frame byte count ≤ 768 (multiples of 256), distinguishing successful short-EOF from partial/ malformed decode. - Export `sampledAlphaIsFullyOpaque` and add 11 direct tests covering: 3/2/1-frame opaque decodes → true; transparent pixel at pos 0 or final byte → false (guards the alpha-byte stride); non-frame-multiple / over-3-frame / zero byte counts → undefined; execFileSync throw → undefined; findFFmpeg missing → undefined; and one args-shape guard pinning the load-bearing `-c:v libvpx-vp9` before `-i` (without which the default decoder silently discards VP9 alpha and the whole check would false-positive on genuinely-transparent WebMs). - Update advisory wording from "3 sampled decoded frames" to "every sampled decoded pixel" so the message is honest for short WebMs. 18/18 tests pass locally under `vitest run`. |
||
|
|
b8562c91d5 |
fix(cli): detect WebM alpha lost when the ALPHA_MODE tag is written without side data
Extends webmAlphaCheck.ts (from #2044) with a pixel-level decode probe. After the tag check passes, decodes 3 sampled frames via `ffmpeg -c:v libvpx-vp9 -pix_fmt rgba -f rawvideo` at 8x8 and emits a distinct advisory if every alpha byte reads 255. #2044 detects the "tag absent" failure mode (ffprobe shows no `alpha_mode` in stream tags). It doesn't catch a stricter case reported on CLI 0.7.56 / Windows 11: ALPHA_MODE=1 present but BlockAdditional alpha side data empty. Under current logic webmAlphaAdvisory sees `alphaMode: true` and stays silent, so the render ships as opaque without any signal. The -metadata:s:v:0 alpha_mode=1 push is a muxer directive that some ffmpeg builds write unconditionally, independent of whether libvpx-vp9 emitted the alpha plane. Tag presence is necessary but not sufficient evidence of preserved alpha. Advisory text names both possibilities (opaque composition OR silent alpha drop) plus the concrete workaround (png-sequence + prores repack). Fast path (no tag or missing tag) is unchanged. Probe adds ~1s per WebM render only when the tag says alpha. |
||
|
|
758a6d2152 |
test(ci): stabilize Windows suites after Studio cutover (#2320)
* test(cli): isolate browser check bundling * test(studio): align timeline merge regressions |
||
|
|
796d5df156 |
fix(render): allow explicit entry without index (#2307)
* fix(render): allow explicit entry without index * fix(render): unify explicit composition handling * test(cli): allow explicit render integration under CI load |
||
|
|
38badff820 |
fix(render): scope strict lint to composition (#2261)
* fix(render): scope strict lint to composition * fix(lint): validate explicit composition scope |
||
|
|
b861afe454 | fix(check): snapshot restored text after contrast audit (#2306) | ||
|
|
bd86fb803d |
Merge pull request #2266 from heygen-com/07-11-fix-cli-check-detect-invisible-text
fix(cli): flag text with an effectively transparent fill in check |
||
|
|
9ae84c652c |
fix(cli): flag text with an effectively transparent fill in check
Wild report (5th in cluster, CLI 0.7.53): snapshots omitted all text while check passed — text painting with a transparent -webkit-text-fill-color (which overrides `color` for the glyph fill AND inherits, so a parent's transparent fill silently blanks descendant text that has its own opaque `color`) renders invisible, but every geometry/occlusion/contrast audit missed it. Contrast in particular reads `color`, not the fill that actually paints, so white-`color` + transparent-fill text scored as high-contrast and passed. Add an invisible-text detector to the layout audit: flag any text element whose effective fill (computed -webkit-text-fill-color, which already resolves to `color` when unset) is transparent. Gradient/clipped text (background-clip:text) legitimately uses a transparent fill and is excluded. Verified: check now fails on an inherited-transparent-fill fixture (text_not_painted) while gradient text, body-inherited color, sub-composition color, and real registry examples stay clean. |
||
|
|
fc2b905837 |
fix(cli): distinguish producer-absent from injector failure in font localization
Addresses review on #2264: the localization helper had one broad catch around both dynamic producer resolution and injector execution, so it couldn't tell a benign 'producer not in this environment' from a real injector/fetch failure, and emitted no diagnostic. Split into loadFontInjector() (returns null when the module is absent — silent fail-open) and localizeWithProducer() (warns ONCE per distinct message when the injector itself throws, then fails open). Per-family resolution failures remain the injector's own responsibility (producer's warnUnresolvedFonts). The localizer seam is injectable; tests now cover success, producer-unavailable, injector-throw, warn dedup, and call-site integration. |
||
|
|
aa10d49234 |
fix(cli): resolve producer font-localization at runtime so vitest transform doesn't fail
The CLI test job builds with --filter '!@hyperframes/producer', and render.ts
imports producer only as a type — so a static import("@hyperframes/producer")
in the font-localization helper failed Vitest's transform-time module
resolution ("Failed to resolve entry for package"), breaking checkBrowser
tests and the helper's own test. Keep the specifier out of the static module
graph (@vite-ignore + variable specifier) so it resolves at runtime only:
production/installed CLI has producer in node_modules and localizes fonts;
the test env fail-opens to the plain bundle. Localizer is now injectable so
the helper's unit tests cover it without needing producer resolvable.
|
||
|
|
ad0d2393fe | fix(cli): localize remote fonts in snapshot/check capture to match render | ||
|
|
3dc08c0868 | feat(cli): preview --browser-no-gpu for gpu-unstable hosts | ||
|
|
0e7f40dfc0 | fix(lint): ignore macOS AppleDouble HTML files (#2191) | ||
|
|
b1f1c0571e |
fix(cli): make skills update converge on a skill retired upstream (#2176)
`hyperframes skills update` failed hard or looped forever once a skill was
retired/renamed upstream while still installed locally (hyperframes-media folded
into media-use; hyperframes-captions/compose/tts consolidated earlier). Two
paths dead-ended:
- Install: target selection could trust a stale local skills-manifest.json
(findRepoManifest) while `skills add` always installs from the canonical repo.
isCoreSkill matches the `hyperframes-` prefix, so a retired skill was forced
into the target set, `skills add` silently declined it (exit 0), and strict
verifyInstalled threw "Skill(s) still missing after install".
- Prune: upstream `skills remove` scans on-disk directories, so a lock entry
retired before it ever shipped a bundle has nothing to match — a silent
exit-0 no-op that never clears the lock, so detectRemoved re-flags it on
every run.
The stale-skills nudge compounded it: it fired even from `skills update` itself
(pointing users back at the failing command) and its count ignored the removed
bucket.
Resolve update targets against the canonical manifest (checkSkills({ canonical:
true })) so a retired skill is never targeted. Add pruneOrphanedLockEntries to
clear the orphaned lock entries the upstream remover can't (idempotent, so a
second run is a clean no-op). Exclude `skills` from the update-nudge gate and
thread the removed count through the nudge total.
|
||
|
|
659cb6a236 |
feat(cli): --frame-check accepts a severity/seek/tol spec
The pipeline already carried FrameCheckOptions; only the flag was boolean, which meant a pipeline caller tuning severity or seek points would have them silently dropped — the two sides only agreed because today's caller happens to match the defaults. Bare --frame-check keeps the defaults; the value form mirrors --caption-zone's grammar, freezing the contract before a release pins it. |
||
|
|
67cdf0fdb7 |
fix(cli): address review — clip-duration audit in check, failure classing, crop observability
Port validate's per-media-element clip audit into check's session (clip_media_fit findings): an intrinsic duration meaningfully shorter than the data-duration slot silently shortens the slot at render time, and neither lint nor the runtime listeners can see it. A linter crash now reports as check_lint_failure instead of masquerading as a runtime failure. Finding-crop capture failures stay non-gating but emit a stderr note and a telemetry error event so rollouts can measure the second-session failure rate. |
||
|
|
4b03866c02 |
fix(cli): contrast gate judges only readable content; examples pass check
Three filters keep the escalated contrast gate honest, each surfaced by running check across the registry examples: - data-layout-ignore (the layout audit's existing decorative opt-out) now also excludes set-dressing text from the contrast audit — one vocabulary for 'not copy a viewer must read'. - Text that has (nearly) left the canvas is skipped: sampling a clamped off-canvas box reads border pixels and produced the classic false white-on-white (a cursor exiting the frame). - Contrast failures follow the same persistence rule as layout findings: observed at a single sample of a multi-sample sweep demotes to warning; held failures gate. Example fixes the sweep exposed: motion-blur's 21 deliberately-dim rail labels are marked decorative (its vivid labels pass on their own); nyt-graph's subtitle and source-note adopt the gate's suggested compliant gray; decision-tree's declared duration drops 15s to 10s — its content ends at ~9.5s and every render shipped a blank white tail. |
||
|
|
cea3458016 |
refactor(cli): single-source-of-truth pass over the check branch
Every duplicated decision gets one owner: rectToBbox lives in checkTypes (was verbatim in pipeline and browser layers); the audit seek tuning is one exported AUDIT_SEEK_OPTIONS consumed by check and the deprecated inspect path; zoom padding/scale defaults export from the capture module instead of re-literalized in three files; the optional run_id property is built by one helper across all three telemetry events; check's --max-transition-samples parsing reuses its own positiveInteger helper; validate drops a leftover re-export and redundant explicit-default args go away. Tests: the contrast candidate round-trip gains a real integration anchor (the actual browser script eval'd in-page, a wrapper asserting finish receives the page-script bbox shape) replacing regex-over-source as the primary guard; the redundant geometry source-golden and a duplicated deprecation-envelope assertion are dropped. |
||
|
|
94f6de8b8d |
feat(cli): persistence-tiered findings, frozen-sweep guard, occlusion coverage
Layout findings now distinguish held defects from entrance/exit transients: a dynamic issue seen at a single grid sample demotes to info, while content_overlap held across two-plus samples (or 500ms+) promotes to error, resolving the long-standing re-promotion TODO. Static compositions keep their severity. check gains a sweep_static error when a 3s+ composition shows zero geometry change across every sample (a frozen timeline makes every green verdict unreliable); skipped when the motion sidecar already reported motion_frozen. text_occluded findings carry a coveredFraction; atomic labels (short, no whitespace) flag on any cover while prose needs 15%, since partial cover changes what a short label reads as. Deprecation-test scaffolding consolidates into deprecationTestHarness; tier logic and logger tests restructured under the complexity gate without suppression markers. Detection mechanics adapted from Adam Rosler's open-sourced visual-linter design (github.com/Adam-Rosler/hyperframes-visual-linter-design); the elementFromPoint paint model, opt-out attributes, and single-audit architecture are unchanged. |
||
|
|
f4cef54b8b |
feat(cli): snapshot --zoom and per-finding crops on check --snapshots
snapshot --zoom <selector|x,y,w,h> + --zoom-scale (default 3) crops via Puppeteer clip at raised deviceScaleFactor — density changes, layout never does. Selector resolves per frame with 24px padding; no match is a loud error, and a frame whose clamped region is a sliver (element collapsed or animated off-canvas) is skipped with a stderr note rather than written as a useless few-pixel image. check --snapshots additionally writes finding-NN-<code>.png crops for error findings with bboxes (cap 12, deterministic re-seek in a second session) and draws labeled annotation boxes on overview frames via a transient overlay injected only after audits complete. Skill reference gains the zoom workflow: check reports a finding, zoom into it, fix, re-check. |
||
|
|
58f45ef758 |
feat(cli): deprecate validate, inspect, layout in favor of check
One stderr notice per invocation and _meta.deprecated: true in JSON mode (shared helper next to withMeta; layout owns both inspect and layout via createInspectCommand). Help descriptions gain the pointer. No behavior change; removal ships separately once migration telemetry says usage has decayed. fix(producer): route info/debug logs to stderr — the compiler's 'Localized remote media' line was landing on stdout ahead of validate's --json payload, breaking every piped consumer. Diagnostics now share stderr with warn/error; render progress uses its own channel. |
||
|
|
3a02942a03 |
feat(cli): run-ID telemetry correlation and check breakdown event
HYPERFRAMES_RUN_ID (trimmed, 128-char cap) attaches as run_id to the generic cli_command / cli_command_result events, absent when unset, so an orchestrator setting it per design element can group a verify loop's invocations in analytics. check additionally emits one check_report event per invocation (including lint-short-circuited and failing runs): gate booleans, per-class error/warning counts, launch/seek/contrast phase timings, sample counts, ok and exit code. Timings stay internal; no command output changes. |
||
|
|
7ab6c2b7a2 |
feat(cli): caption-zone and frame-check gates on check
Ports the EF bridge's captionZone and frameCheck semantics as opt-in flags so the bespoke bridge can be retired: --caption-zone takes fractional band geometry (x0;y0;x1;y1) with optional severity routing and seek points, defaults matching the bridge (caption seek [1], frame seek [0.5], 2px tolerance, 0.05 opacity floor, 4px minimum size, 0.95 full-frame exclusion, center-in-band comparison, tag|text dedup). --frame-check adds media bounds detection (img/svg/video/canvas) the always-on text canvas_overflow never covered, reusing overflowFor. Breach floor: max(120px, 6% of min canvas dimension). Band math derives from the composition's own canvas, portrait included. Both gates off by default; plain check output unchanged. |
||
|
|
7d6d41361b |
feat(cli): add check — single-session verification gate
One command, one Chrome boot: in-process lint gate (browser skipped on
lint errors), passive runtime capture wired before navigation, layout +
motion + contrast audits over one seek grid, optional --snapshots
persisting the contrast-pass screenshots. Aggregated --json envelope
{ok, lint, runtime, layout, motion, contrast, snapshots}; findings carry
selector/data-*/source-file/bbox/time anchors, contrast findings include
fg/bg, measured vs required ratio, and a compliant color suggestion.
Contrast AA failures gate the exit code (they were warning-only in
validate); --strict gates warnings.
Contrast candidates round-trip verbatim between __contrastAuditPrepare
and __contrastAuditFinish: the page script owns their shape (bbox w/h),
and normalizing them Node-side made every sample rect NaN — the audit
reported zero checked elements as green. Regression-pinned in
check.test.ts; E2E on a low-contrast fixture now exits 1 with 8 findings.
Measured on kinetic-type: check 5.6s vs 23.0s for sequential
validate + inspect + snapshot.
|
||
|
|
dfe63af3ae |
fix(core,cli): parent-relative figma child geometry; groups stop rejecting subcommand flags
Both found running the brand-loop guide end-to-end against the Simple Design System: - nodeToHtml subtracted the ROOT origin from every node's absolute bounds, but CSS absolute positioning resolves against the nearest positioned ancestor — every nesting level re-added its ancestors' offsets, drifting nested content down-right and pushing deep children off-frame (hero buttons invisible, pricing grid collapsed to one card). Children now subtract their PARENT's box; regression test with a two-level tree. - trackCommandFailures asserted unknown flags against the command group's own (flagless) arg table even when the group was delegating to a subcommand, so `figma component <ref> --name x` imported and THEN threw "Unknown flag: --name". The assertion is now skipped when the first positional names a subcommand; leaf and non-delegating behavior is unchanged and covered by tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
57b3c78987 |
feat(media-use): color grading — grade/lut resolve, smart-grade, grade-compare + compare (#2041)
* feat(media-use): color grading — grade/lut resolve, smart-grade, grade-compare CLI Add color grading to media-use as first-class resolve types plus a faithful comparison command. All local, offline, deterministic — no model, no GPU. - resolve -t grade / -t lut: produce a data-color-grading block (or a frozen .cube). Look cascade: core preset (no file) -> bundled .cube library -> parametric buildCube. Emitted .cube is Rec.709 and validated against core's colorLuts constraints (LUT_3D_SIZE <= 64) before it is frozen. - smart grade (grade --for <media>): ffmpeg signalstats -> adjust suggestion (exposure / contrast / white balance), surfaced with the measured evidence on stderr as a starting point; never auto-applied. - hyperframes grade-compare: renders N candidate grades onto a reference frame through the real runtime shader into one labeled comparison PNG, so an agent picks a look without opening Studio. Prepends an "original" baseline cell by default (--no-baseline to omit). Shares the headless-capture pipeline with snapshot via capture/captureCompositionFrame. - media-use SKILL: proactive "media opportunity pass" guidance (grounded signal -> offer, ask once, surface don't mutate). Verified: media-use 116/116, grade-compare 7/7, snapshot 9/9, lint + format clean, full build green, comparison renders end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * test(cli): narrow grade-compare baseline assertion off unknown-typed grading Assert the whole cell via toEqual instead of reaching into .grading.preset / .grading.lut on the unknown-typed field, keeping the test typecheck-clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * feat(media-use): agent-authored LUTs via --params + validate --from cube; never-read-.cube guardrail - resolve -t lut / -t grade --params '<json>': build a parametric .cube from explicit params (bypassing the intent cascade), validate, and freeze in one step. --intent becomes the optional description. Lets an agent commit a look it computed itself. - --from <file.cube> now validates the ingested LUT for lut/grade types and rejects an invalid/oversized cube (no partial write) — the escape hatch for a LUT the agent generated with its own code. - SKILL.md: hard rule to never read a .cube body into context (~size^3 lines, zero legible signal) — inspect via grade-compare (see it) or cube-validate (ok/size), read the manifest description for meaning; plus both authoring paths and the parametric-vs-film-stock ceiling note. Verified: media-use 116/116, lint + format clean; smokes — --params builds a valid frozen cube, grade --params returns a lut block, bad JSON and an oversized --from cube are both rejected with no stray file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * fix(cli): grade-compare validates referenced LUTs, warns on no-op cells, caps candidates Bug-bash follow-ups — grade-compare silently accepted bad input: - Validate LUT *content*, not just existence: each referenced .cube is parsed with core's parseCubeLut (now exported from @hyperframes/core) and rejected with a per-cell error ("LUT for \"<label>\" is not a valid .cube: ..."). A file that exists but isn't a valid cube no longer renders a silent no-op cell. - Warn on inactive cells: a grading that normalizes to inactive (e.g. a malformed {lut:12345}) emits a stderr warning naming the cell; the auto-prepended "original" baseline is intentionally inactive and stays silent. stdout remains valid JSON. - Cap candidates at 16 (excluding baseline): over-cap input renders the first N and reports {truncated:true, total:M} on stdout + a stderr note — no silent drop, no unbounded giant sheet. Verified: grade-compare 10/10; non-cube LUT → clear error; {lut:12345} → warning + ok; 20 cells → cells=17 truncated total=20; valid runs unchanged. Lint/format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * feat(cli): general `hyperframes compare` visual-variant primitive Generalize grade-compare's "render N variants → one labeled sheet → the agent looks and picks" loop into a standalone command that works on ANY variation (font, layout, motion, grade, whole compositions) — the tool never needs to know what differs. - `hyperframes compare <path...> [--at <sec>] [--labels a,b,c] [--out] [--cols] [--json]`: renders each agent-authored composition variant through the real runtime (captureCompositionFrame) and stitches one labeled comparison sheet + JSON ({ok, sheet, rendered, variants, truncated?/total?}). 2+ paths required; caps at 16 with loud truncation. It presents, it does not judge — choosing is the caller's job. - Factored the shared "render a labeled set → contact sheet" path so compare, grade-compare, and snapshot all sit on it (no duplication). grade-compare is now the first color-specific specialization of this primitive. - New pathArgs util + contactSheet test; hyperframes-cli SKILL documents compare as the agent's "see your own renders and choose" primitive. Verified: 26/26 across compare + grade-compare + snapshot + contactSheet (no regressions); compare renders 3 variants into one visibly-distinct labeled sheet; 2+-path error path clean; lint/format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * fix(ci): green the skills CI — skip ffmpeg tests when absent, oxfmt markdown The "Test: skills" CI job runs bare `node --test` with no ffmpeg on PATH (by design — skills tests are meant to be node-builtin-only). The grade-analyzer + smart-grade tests shell to ffmpeg and were failing there with ENOENT. Guard them to skip when ffmpeg isn't on PATH; they still run locally / where it is. Also oxfmt README.md + hyperframes/media-use SKILL.md (the whole-repo `oxfmt --check .` Format job caught markdown left unformatted by the rebase conflict resolution). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * fix(ci): skip core-conformance test when tsx is unavailable The "Test: skills" CI job installs no deps, so the normalizeHfColorGrading conformance test (which imports core's TS via `node --import tsx`) failed there. Guard it to skip when tsx can't resolve; runs locally / in the deps-installed Test job. Completes the skills-CI greening (the ffmpeg guards handled the rest). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * fix(cli): escape grade-compare src double-quotes (CodeQL XSS) + Windows-safe compare test - grade-compare built `<img src="...">` (double-quoted) with the single-quote escaper, leaving `"` unescaped — a `"` in the frame path could break out (CodeQL: incomplete HTML attribute sanitization). Use escapeXml for src. - compare label test hard-coded POSIX paths that can't match on Windows; assert the derived labels (the subject); path resolution is covered elsewhere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * refactor(media-use): generate LUT library from params (drop committed .cube files) The 3 bundled .cube files were 733 lines each (2,199 total) and were themselves buildCube output — pure repo bloat. Replace with compact per-look params in luts/index.json, generated on resolve; add an optional `url` for future scanned LUTs to be CDN-hosted + downloaded on demand (freezeUrl) instead of committed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * feat(media-use): serve library LUTs from CDN on-demand (static.heygen.ai/luts), params fallback Looks now carry a CDN `url` (hosted at s3://heygen-public/luts → static.heygen.ai/luts/<id>.cube); resolve downloads + validates + freezes on demand, like bgm/image. `params` stays as the deterministic offline fallback (--local-only, or if the download fails), so resolution is never blocked on the network. Provider prefers url, falls back to params. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * fix(media-use): address #2041 review — atomic LUT writes, compare telemetry, follow-ups - Atomic .cube writes: library provider (url + params) and the parametric generator now write to a .tmp path, validate, then rename, so a crash can never orphan an invalid .cube at the final path (was validate-after-write). - track("media_use_resolve") now emits provenance.via (url/params-fallback/params). - grade-compare + compare: --timeout flag (was hardcoded 5000) and a media_use_compare event (cells, truncated, total, render_ready_timed_out); openSettledCompositionPage now surfaces the render-ready timeout. - compare staging skips node_modules/.git; --for gets an upfront existence check. - Rec.709 luma comment; HYPERFRAMES_ANALYZE_TIMEOUT_MS override; measured note uses basename; LUT s3 hosting moved from index.json into luts/README.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
38b27c4d82 |
fix(cli): report unknown-flag errors + cover nested subcommands (HF#2033) [P2] (#2072)
* fix(cli): report unknown-flag errors + cover nested subcommands (HF#2033) Two flag-hygiene gaps behind the assertKnownFlags arc: 1. Telemetry loss: assertKnownFlags ran BEFORE the try/catch in the command wrapper, so an unknown-flag throw skipped reportCommandFailure entirely — zero signal on how often users hit bad flags. Moved the assertion inside the try so it reports like any other failure. 2. Nested-subcommand scope: cli.ts wraps only the top-level command loaders, so command groups' leaves (cloud/*, auth/*, figma/*, lambda/*, capture/*, skills) were never wrapped — citty dispatches to the leaf, whose run had no assertion and no failure reporting. So `hyperframes cloud render --badflag` silently ignored the flag. trackCommandFailures now recurses through cmd.subCommands (normalizing citty's Resolvable entries to loaders) and wraps every leaf. Identity is preserved for bare no-run/no-subcommand defs. Verified: `auth status --badflag` now errors "Unknown flag: --badflag" (previously silent); `auth --help` still dispatches; top-level `lint --badflag` still rejected. Tests: unknown-flag rejection is reported, and a nested subcommand's failure reaches onFailure. * test(cli): guard indexed subCommands access for noUncheckedIndexedAccess CI Typecheck (tsc, unlike the local tsup build) flagged the nested-subcommand test: indexing `subCommands["render"]` yields `T | undefined` under noUncheckedIndexedAccess, so invoking it tripped TS2722/TS18048. Guard the loader before calling it. |
||
|
|
f5f94a9495 |
fix(cli): warn when a WebM render silently drops its alpha channel [P2] (#2044)
* fix(cli): warn when a WebM render loses its requested alpha channel HyperFrames always encodes WebM with an alpha-capable pixel format (yuva420p), but some ffmpeg/libvpx builds silently emit opaque yuv420p even when handed alpha input and -pix_fmt yuva420p. The render succeeds and plays back fine, so the lost transparency is only discovered after compositing (users report shipping a solid-black clip and colorkeying it out by hand). After a WebM render, best-effort ffprobe the output's pix_fmt; if it lacks alpha, print a non-blocking warning that names the concrete remedy (--format mov / ProRes 4444). Only WebM is checked (mp4 is intentionally opaque; mov/png carry alpha through paths that don't hit libvpx-vp9), and a failed probe stays silent rather than warning speculatively. Pure decision (pixelFormatHasAlpha / webmAlphaAdvisory) unit-tested; verified end-to-end that a transparent WebM render now surfaces the warning while an MP4 render stays silent. * fix(cli): key WebM alpha check on ALPHA_MODE tag, not pix_fmt (R1 blocker) R1 (Rames/Via) correctly flagged the detection as ~100% false-positive on working builds. libvpx-vp9 stores the alpha plane in a Matroska BlockAdditional sidecar, so ffprobe ALWAYS reports pix_fmt=yuv420p for a correct transparent WebM (per docs/guides/rendering.mdx #1823 and the webm-concat-copy smoke test). The real signal is the stream-level ALPHA_MODE=1 tag: a working encode writes it; a build that can't emit the sidecar omits it and produces genuinely opaque output. Re-cut the probe to read stream_tags=alpha_mode (JSON, case-insensitive) and warn only when a probed WebM lacks ALPHA_MODE=1. Tests inverted accordingly (alphaMode:true → silent; alphaMode:false → warn). Verified end-to-end: a transparent webm render on an alpha-preserving build (ALPHA_MODE=1) now emits 0 warnings; previously it warned on every webm. |
||
|
|
81884a7495 |
fix(cli,skills): install workflow skills on demand instead of re-pulling the full set (#2012)
* fix(cli,skills): install workflow skills on demand instead of re-pulling the full set Users report every init re-pulls all 21 skills into ~/.agents/skills whenever anything is stale or missing - heavy, noisy, and it re-expands deliberate partial installs. Split the set into two tiers: - core: the /hyperframes router + hyperframes-* domain skills + media-use, which every workflow references structurally. init and bare 'skills update' keep these (plus anything already installed) fresh, and never expand the install. - on demand: the end-user workflow skills (and figma). They install at trigger time via 'skills update <name...>' - positional names are the only way update expands an install: one targeted 'skills add --skill <name>' covering only stale/missing targets, a fast no-op when current, presence-verified after install, exit 1 on unknown names, and a presence-only degrade when GitHub is unreachable. The /hyperframes router now runs 'skills update <workflow>' after routing and before reading the workflow skill, so a routed workflow is guaranteed present even on a machine that only has the core set. Each on-demand skill also opens with the same self-maintenance step (run 'npx hyperframes skills update <name>' silently), so a workflow triggered directly - without the router - still refreshes itself and restores any missing core skill before relying on it. When the manifest is unreachable (offline / rate-limited) the engine degrades honestly instead of claiming success: named runs presence-check the request plus a pinned fallback core list (unit-pinned to skills/) and blind-install whatever is absent; a bare strict update fails loudly so the 'check || update' chain can't pass while everything stays stale; init reports the skipped freshness check. --json emits structured errors on failure paths. skills check still lists every skill, but exits non-zero only for stale installed skills, an incomplete core set, or removed leftovers - workflow skills not yet installed are reported as available on demand. Bare 'hyperframes skills' (and 'skills add --all') remain the explicit full-set installs. Verified end-to-end with a sandboxed $HOME: fresh init installs the 9 core skills only; 'skills update slideshow' adds exactly that skill (no-op on re-run, exit 1 on unknown names); bare update refreshes without expanding; a live Claude Code run routed PR-to-video, executed the router's update step, and the workflow skill appeared before use; and a second live run triggered an installed workflow directly, whose opening maintenance step restored a deliberately removed core skill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(skills): clarify update-engine contracts + document lazy-install model - skills.ts: note the UpdateSkillsResult.unknown strict-mode contract, verifyInstalled's non-strict (warn-not-throw) intent, and that a partial install stays "refreshed but never expanded" (review nits). - docs/guides/skills.mdx: add a "Keeping skills current" section covering the core-eager / workflow-on-demand model and the skills check|update commands, per the repo's catalog-maintenance rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: kiritowoo <295860553+kiritowoo@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Miao Yang <miao.yang@heygen.com> |
||
|
|
cebce603df |
fix(cli): pin arm64 render Chromium to Playwright headless-shell (#2039) (#2040)
The arm64 render image had no pinned browser: chrome-for-testing publishes no linux-arm64 build, so Dockerfile.render fell back to Debian bookworm's rolling `chromium` package. Its current arm64 build (150.0.7871.46) SIGTRAPs at startup (exit 133), breaking `render --docker` 100% on Apple Silicon. Install a pinned, non-Debian chrome-headless-shell from Playwright on arm64 (Google's build, not Debian's repackage). The wrapper wires whichever binary landed into PRODUCER_HEADLESS_SHELL_PATH and now fails the build loudly if neither is present, instead of silently using the broken Debian chromium. Bonus: arm64 gains BeginFrame deterministic capture it previously lacked. amd64 path is unchanged. Verified on Apple Silicon: same arm64 image, Debian chromium 150 -> exit 133, Playwright arm64 headless-shell (Chromium 149) -> exit 0. |
||
|
|
401dd1d27f |
fix: media-use bug-bash fixes (codex gate, id race, provider/reuse/adopt guards) + CLI unknown-flag rejection (#2033)
* fix(media-use): codex gate misfires as 'not logged in' when piped codexUnavailableReason() gated generation on parsing `codex login status` stdout, but that command prints 'Logged in using ChatGPT' to stderr and exits 0 — so the piped stdout media-use captures (execFileSync returns stdout only on success) was empty, and the gate falsely reported 'not logged in'. Every headless / CI / agent run was blocked from codex image gen even when fully authed. Gate on the durable credentials file ($CODEX_HOME/auth.json) instead of the TTY/stderr-only human text. Token validity is still proven by the exec, which fails cleanly on a stale login. The stdout `features list` capability check is unchanged. Verified: reproduced the false 'not logged in' block, then after the fix generated end-to-end via `resolve -t image --provider codex` (valid 1254x1254 PNG, source=generated, provider=codex.image_gen). * fix(media-use): bug-bash fixes — id race, provider/reuse/adopt guards From the bug-bash against main: - MU-23 (HIGH): concurrent resolves raced on nextId (read-max-then-append, non-atomic), so parallel agents got duplicate ids and clobbered each other's files. Add allocateId(): a coarse per-project lock (.media/.lock, 15s stale-steal) around id allocation that scans the manifest AND the type dir for reserved ids, then O_EXCL-creates a placeholder file so the slow download between allocate and append can't collide. 5 parallel resolves now yield 5 distinct ids + files. - X4: --reuse imported across a type mismatch (bgm asset under images/). Apply typesMatch on the --reuse path; reject mismatches (icon<->image still interchangeable). - X5: --provider silently overrode --local-only and made a network call. --local-only is now a hard guard: network providers are skipped even under a forced provider; the miss message explains the conflict. - BUG-2: --provider ignored the exact-cache floor and could hand back an asset from a different provider. A forced --provider now bypasses all reuse rungs (regenerate with THIS provider); the unforced floor is intact. - MU-26/X6: 0-byte assets accepted. --adopt skips 0-byte files (loud); ingest refuses a 0-byte local file (freezeUrl already rejects empty responses). - BUG-4: unknown/unavailable --provider now errors with the available list instead of a generic 'no provider could resolve' (typo != catalog miss). - BUG-5: --reuse "" gave the wrong 'type and intent required' error; it now routes to a clear empty-sha message. - BUG-3: voice duration leaked an unrounded float into index.md; round all durations to 0.1s centrally at record build (matches probe). - Nits: whitespace-only --intent is rejected; nudge grammar (exists/exist). Tests: allocateId reservation + registry local-only-wins added; full media-use suite green. All fixes verified e2e. * fix(cli): reject unknown flags instead of silently ignoring them citty is permissive: an unrecognized flag was dropped, not rejected — so `render . --out x` (the flag is --output/-o) silently ignored --out and rendered to the default renders/<name>.mp4 path. A mistyped flag read as a render/catalog miss. Add assertKnownFlags(): validate every dash-prefixed token against the command's declared args + aliases + the global set (help/version/json) before the command runs, in the shared trackCommandFailures run-wrapper so every leaf command is covered. Handles --flag=value, --no-<bool> negation, camelCase<->kebab arg names, and combined shorts; stops at --; positionals and flag values pass through. Verified: `render . --out x` -> 'Error: Unknown flag: --out'; --output/-o/ --json/--help still accepted. Unit tests added. * docs(skills): install with --full-depth so agents get current main The documented `npx skills add heygen-com/hyperframes` fetched the skills.sh registry blob, which lags GitHub main by hours — so users following the docs got a stale skill (e.g. media-use v1: no --candidates, voice stubbed). The CLI's own `hyperframes skills` command already forces a full clone via --full-depth to bypass this; the docs didn't pass it. Add --full-depth to every documented install command (README, CLAUDE.md, docs/guides/skills.mdx) with a one-line note on the lag. Addresses the user-facing half of the publish/registry lag (#2034). * chore(media-use): collapse resolve.mjs import to satisfy oxfmt --check * fix(cli): extract longFlagName to keep flag validator under complexity gate Also regenerate skills-manifest.json (resolve.mjs formatting change re-hashed the media-use skill). Fixes the Fallow audit + skills-manifest-in-sync CI gates. |
||
|
|
4b3c73d941 |
fix(cli): upgrade and update notice use the detected install method
* fix(cli): upgrade + update-notice use the detected install method
hyperframes upgrade hardcoded 'npm install -g', so bun/pnpm/brew users either
saw it fail or silently got a shadowed npm copy while their real (older) binary
kept running. Route the install through detectInstaller() via a new
installInvocation() argv helper; for skip kinds (ephemeral npx/bunx,
project-local, unknown) print 'npx hyperframes@latest' instead of guessing.
The passive update notice now shows the detected manager's command too. Semver
safety guard consolidated into a shared isSafeVersion(). Suppression gates and
the background auto-update flow are unchanged.
* test(cli): pin the shell:false contract of the --yes install path
Export runDetectedInstall and add a mocked-execFileSync test asserting the
detected manager binary is spawned with the exact installInvocation argv,
{stdio:inherit, shell:false}, and that an install failure sets a non-zero exit
code without throwing. Addresses review nit on the untested --yes path.
* fix(cli): guard the registry version at the boundary; execFile the auto-installer
Security (addresses review): a poisoned registry data.version (e.g.
'1.2.3; rm -rf /') was cached unvalidated and flowed into the background
auto-updater, which ran it via exec() -- a shell -- so a registry compromise
meant RCE on the next CLI run. isSafeVersion only covered the two touched
consumers (upgrade, notice), not this third sibling (scheduleBackgroundInstall).
- Guard at the registry boundary in checkForUpdate: only a strict-semver STRING
is trusted; a non-string or metachar-bearing data.version is never cached and
falls back to the last known-good version. The cache-read and fallback paths
re-validate too, so a pre-existing poisoned cache can't leak through. One gate
closes all three consumers and any future one; per-consumer checks stay as
defense in depth.
- The detached auto-installer now runs via execFile(bin, args, shell:false),
reusing installInvocation, matching the interactive runDetectedInstall path --
the shell is gone from that path entirely.
Tests: reject poisoned / non-string registry version (never cached); accept a
valid semver.
|
||
|
|
de27b46680 |
fix(cli): default render fps to the composition's data-fps
* fix(cli): default render fps to the composition's data-fps
hyperframes render hard-coded fps to 30 when --fps was omitted, ignoring a
data-fps declared on the composition root — so a composition authored at
data-fps="24" silently rendered at 30fps unless the user knew to pass --fps 24.
The runtime already honors data-fps; the CLI now matches it.
Precedence: explicit --fps > composition root data-fps > 30. New pure
readCompositionFps() extracts the root data-fps via linkedom (mirrors the
runtime's root resolution: [data-composition-id][data-root=true], else the
outermost [data-composition-id]); render validates it through parseFps and
falls back to 30 on an absent/invalid value. Unit-tested.
* fix(cli): honor composition data-fps on cloud renders and --composition targets
The local render command read data-fps from project.dir/index.html even when
--composition rendered a different file, and the lambda/cloudrun render paths
ignored data-fps entirely (hardcoded ?? 30). Both are the same silently-wrong-
fps bug on other render entry points:
- render.ts resolves the entry file first, then reads data-fps from the file
actually being rendered (falling back to index.html).
- lambda render/render-batch and cloudrun render/render-batch default fps from
the composition's data-fps, accepted only when it is one of the cloud-allowed
values {24,30,60}, else the existing 30 default. Explicit --fps still wins.
* fix(cli): drop citty fps default so data-fps resolution actually runs
The fps arg had default: "30", so citty set args.fps="30" on omission and
resolveDefaultFpsArg short-circuited (explicitFps never null) — reverting the
command to always-30 and making the whole data-fps feature a no-op (caught in
review). Remove the arg default; the "30" fallback already lives at
parseFps(fpsArg ?? "30"). Adds a regression guard asserting the arg has no
default.
* test(cli): read citty args through a plain record in the fps-default guard
The regression guard accessed cmd.args.fps directly, but citty types args as
Resolvable<ArgsDef> so .fps failed typecheck in CI. Read it through a plain
record cast.
|
||
|
|
9fe06f30da |
feat(cli): forward feedback submissions to the backend feedback endpoint (#2003)
* feat(cli): forward feedback submissions to backend endpoint * fix(cli): truncate feedback fields to backend caps + ack before forwarding Addresses PR review (via): - Truncate comment (2000) / cli_version (100) / env (500) to the backend DTO caps before POSTing, so a pasted stack trace is forwarded truncated instead of rejected with a 422 the best-effort path swallows silently. - Print "Thanks for the feedback!" before the best-effort forward so the ack isn't blocked behind the (bounded) network call. * fix(cli): type feedback fetch mock |
||
|
|
906c8d04f8 |
fix(cli): localize remote assets before validate so it matches render (#2001)
validate served the composition over a loopback origin and let headless
Chrome fetch remote <img crossorigin>/@font-face assets cross-origin, while
the render pipeline downloads them to disk first. Buckets whose CORS
allowlist omits the loopback origin then failed the CORS-mode request with a
false net::ERR_FAILED that never occurs in the real render, pushing authors
(and agent pipelines) to delete crossorigin — which disables WebGL
color-grading/shaders for that asset.
Reuse producer's localizeRemote{Media,Image,FontFace}Sources in validate,
downloading into a temp dir served as an extra static-server asset root
(project dir untouched, cleaned up after). validate now matches render.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
e04f6dda37 |
feat(engine,cli): drawElement fast-capture config + CLI flag (#1916)
## drawElement fast-capture — config + CLI flag (stack 1/6) Foundation layer for the drawElement fast-capture feature: the config surface and CLI/Docker plumbing that the rest of the stack builds on. ### What this adds - **`packages/engine/src/config.ts`** — new config fields for fast capture: `useDrawElement` / `enableDrawElementWorkerEncode` (macOS-GPU `drawElementImage` capture + worker-offloaded JPEG encode), resolved from env in `resolveConfig` (env `HF_DE_WORKER_ENCODE`). Wired alongside main's existing `staticFrameDedup` (unified downstream in 4/6). - **`packages/cli/src/commands/render.ts`** — `--experimental-fast-capture` flag → sets `experimentalFastCapture`; `--debug` passthrough. - **`packages/cli/src/utils/dockerRunArgs.ts`** — pass the fast-capture env through to the container. - **`.github/workflows/fast-video-validation.yml`** — CI job validating fast-capture renders. - `.oxlintrc.json` / `.fallowrc.jsonc` — ignore-pattern housekeeping for the new paths. ### Notes - Config-only + entrypoint; no capture behavior yet (that's 2/6–4/6). - Tests: `config.test.ts`, `dockerRunArgs.test.ts` added. --- **Stack (drawElement fast-capture, rebased onto current `main`, supersedes #1295 + #1444):** 1. **#1916 config + CLI** ← you are here 2. #1917 drawElementImage capture service 3. #1918 3D projection + compositor-effect risk gate 4. #1919 frame-capture core (routing, worker-encode, static-dedup unification) 5. #1920 producer render stages + remote bg-image localizer 6. #1921 lint rule + player media sync ⚠️ Intermediate PRs (1–5) are split by package boundary for review and **do not each compile independently** (cross-file deps); the complete feature is green at the stack tip (#1921) — tsc-clean on engine + producer, 231 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
a908af11a8 |
feat(cli): keyframes command (surface GSAP/CSS/Anime keyframes + 3D onion-skin --shot) (#1603)
Renames the motion-surfacing tool from `hyperframes keyframes` to `hyperframes motion`, renames the implementation from keyframes*.ts to motion*.ts (keeping the keyframe data model name where still accurate), and renames the shipped skill from hyperframes-keyframes to hyperframes-motion. Expands the skill from a command reference into a full motion-design workflow: reading motion, 3D angle verification, layered GSAP motion, one-shot reference reproduction, diagnostic checks, and eval-derived craft guidance. |
||
|
|
cf573f7f3f |
fix(core,producer,cli): pre-flight validation for empty/malformed sub-compositions (#1831)
* fix(core,producer,cli): pre-flight validation for empty/malformed sub-compositions The #1 render failure bucket in production telemetry (PostHog project 356858, dashboard 1783183 "HyperFrames — Bottom-Line & Activation"; ~65-69K occurrences / ~27-28K affected users over 30 days, ~80% via AI-agent authoring flows) is a `data-composition-src` reference pointing at a scene file that is empty, malformed, or missing. Root cause, traced end-to-end: - The literal error "Composition HTML is empty or could not be parsed: <path>" is real (not a PostHog paraphrase) — thrown by a since-reverted guard in packages/core/src/compiler/inlineSubCompositions.ts (#1364), then changed to a silent skip in #1678 to avoid aborting renders on partial content during authoring. #1629 added per-assembler guards for 3 skill workflows (product-launch-video, faceless-explainer, pr-to-video), but general-video and hand-authored flows — where the dominant filename `scene-title.html` (40K+/68K of the bucket) originates — have no assembler and thus no guard. #1678 assumed the assembler guards from #1629 covered this pre-render; they only covered 3 of the many authoring flows. - On current `main`, an empty/malformed data-composition-src file no longer crashes or throws during render — it's silently dropped by the tolerant inliner. Reproduced locally: `hyperframes render` on a project with an empty scene-title.html "succeeds" after ~93s (two 45s pollSubCompositionTimelines timeouts) with the scene silently missing from the output video. `hyperframes validate` also reports "No console errors" for the same broken project. - The raw `Cannot destructure property 'firstElementChild' of 'documentElement' as it is null` crash reproduces directly against linkedom (the DOMParser polyfill packages/cli/src/utils/dom.ts installs in the real CLI runtime) for empty and non-HTML input — confirmed with a standalone repro script, not just inferred. jsdom/happy-dom (used in this repo's own test environment) are spec-compliant and never produce a null documentElement, which is why this needed a linkedom-specific test file. Fix: - New shared helper `checkSubCompositionUsability` (packages/core/src/compiler/subCompositionValidity.ts) is the single source of truth for "is this data-composition-src file usable" — mirrors the inliner's own parse/template/body logic so all callers agree. - `inlineSubCompositions.ts` (preview/studio bundling) now uses the shared helper internally but keeps its #1678 tolerant skip-and-continue behavior unchanged — mid-authoring iteration on a partial project must keep working. `onMissingComposition` now also receives a human-readable reason. - New render-only pre-flight (`assertSubCompositionsUsable` in packages/producer/src/services/htmlCompiler.ts) walks every data-composition-src reference (including nested ones, root-relative, matching parseSubCompositions' own resolution) before any compilation work starts, and throws naming every offending file at once. This is unconditional — not gated behind --strict — because a render that silently drops a scene is strictly worse than one that refuses to start. Confirmed locally: render now fails in ~0.4s with an actionable message instead of "succeeding" after 93s with a missing scene. - New `hyperframes lint` rule `missing_or_empty_sub_composition` (packages/cli/src/utils/lintProject.ts) surfaces the same check as a file-scoped, actionable lint error (already unconditional — lint exits 1 on any error). - `hyperframes validate` now also runs this check before launching a browser, so it no longer reports "No console errors" for a project with a broken sub-composition. - `packages/core/src/parsers/htmlParser.ts`: guarded every `documentElement`-may-be-null access (parseHtml, updateElementInHtml, addElementToHtml, removeElementFromHtml, extractCompositionMetadata, validateCompositionHtml) with a new typed `CompositionHtmlParseError` (or, for validateCompositionHtml's collect-and-report contract, a typed validation failure) instead of a raw crash. Tests: empty file, whitespace-only, malformed/non-HTML, missing file, nested sub-compositions (both happy path and broken-grandchild), and the happy path — at the shared-helper, lint, and render pre-flight layers. Not changed: the AI-agent authoring skills (skills/*). general-video and hand-authored flows have no assemble-index.mjs equivalent to guard, so the fix is at the CLI/render layer instead — flow-agnostic, covers every authoring path, and the skills' existing "run lint/validate and stop on failure" guidance now actually catches this class of mistake once run. Not run in this environment: the producer package's full regression-harness test suite (`bun test` in packages/producer) — it performs heavy real rendering (S3 asset downloads, Google Fonts fetches, full video encodes) and did not complete in a reasonable time in this sandbox. Verified instead via the targeted test file for all touched code (76/76 passing), whole-repo typecheck/build/oxlint, `fallow audit` (complexity/duplication/dead-code gate, clean), and manual end-to-end CLI runs (render/lint/validate) against reproduction projects, including a nested sub-composition scenario. CI should run the full producer suite before merge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refactor(parsers,lint): port empty-composition pre-flight to extracted packages Rebased onto main, which extracted @hyperframes/lint from core (lint depends only on parsers, not core). Relocate checkSubCompositionUsability from core to @hyperframes/parsers so both core (inliner) and lint can consume it without a core<->lint cycle; core keeps a @deprecated re-export shim. Correctness fixes from code review: - checkSubCompositionUsability now returns "no-composition-root" when the <template>/<body> content has no [data-composition-id] element (previously a marker-free placeholder body passed both guards). - lint's missing/empty sub-composition rule now only checks files reachable via data-composition-src from the root (matching render pre-flight), instead of a raw filesystem walk that false-positived on orphaned files. - drop `as string` cast in inlineSubCompositions in favor of an explicit null guard (per CLAUDE.md). Review-comment items: - move EmptyCompositionError JSDoc above the class (was above the adapter fn). - correct stale circular-ref comment to match actual silent-skip behavior. - rewrite self-contradicting lint message ("silently drop") to describe the new loud render-pre-flight abort. - add the __PLACEHOLDER__ (/^__[A-Z_]+__$/) skip to the render pre-flight so it agrees with lint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
856bb0980f |
feat(cli): add --public flag to publish (#1815)
Opt-in --public flag on `hyperframes publish` sends is_public to the publish endpoints (staged complete body and direct multipart form) so a claimed project's studio session can be created public instead of the default private. Absent the flag the request shape is unchanged. |
||
|
|
010df6a0a4 |
feat(cli): file a GitHub issue with a published repro from feedback (#1816)
Add an opt-in --file-issue flag to hyperframes feedback. When set, after sending the usual feedback the CLI publishes a minimal repro of the project to a public URL (consent-gated, mirroring publish --yes) and opens a pre-filled GitHub bug issue draft containing the rating, comment, public repro link, and environment summary. The user reviews and submits the issue under their own account; there is no token, backend, or gh invocation. New --dir selects the project to publish; --yes skips the consent prompt for scripts. URL/body building is extracted into pure, unit-tested helpers. |
||
|
|
a5c2636e8c |
fix(cli): never print "[object Object]" from validate/inspect errors (#1810)
* fix(cli): use normalizeErrorMessage so validate/inspect never print "[object Object]" The validate and inspect (layout) commands formatted thrown values with `err instanceof Error ? err.message : String(err)`. When a browser/CDP/ Puppeteer protocol error or a structured page error reaches the formatter as a plain object without a string `message`, `String(obj)` yields the useless literal "[object Object]", hiding the real cause. Route those paths through the existing shared `normalizeErrorMessage` helper, which returns an Error's message, a string as-is, an object's `.message` when present, or a compact JSON serialization otherwise (with a key-list and String fallback for circular/opaque objects). Also fold the duplicated local `errorMessage` helpers in batchRender and preview into the same shared helper. Covered by added assertions in errorMessage.test.ts for the no-message object and Puppeteer-style protocol-error object cases. * fix(cli): route remaining browser/process error sites through normalizeErrorMessage The validate/inspect fix routed only those two commands through the shared normalizeErrorMessage helper. The same err instanceof Error ? err.message : String(err) pattern survived in the other commands that drive a headless browser or an external process (ffmpeg, Docker, CDP) or surface a network API error, so a thrown structured object without a string message would still render as the useless literal [object Object]. Route those sites through the shared helper: snapshot.ts (the closest sibling to validate/inspect, same bug class), render.ts (Chrome launch + Docker build), capture/index.ts and commands/capture.ts (page-driven extraction), auth/browser.ts, browser/manager.ts (Puppeteer browser resolution), and the cloud/lambda paths (cloud/render.ts, cloudrun.ts, lambda/render-batch.ts, lambda/policies.ts, cloud/detectAspectRatio.ts) that surface API/network error objects. Only the message-deriving expression changes; control flow and error propagation are untouched. capture/index.ts keeps appending the stack for real Errors and only routes the non-Error branch. Adds a helper test for a structured CDP-style error object (code + nested data, no message). |
||
|
|
466ee08ffa |
fix(cli): serve project media with HTTP Range so validate reads WAV duration (#1811)
* fix(cli): serve project media with HTTP Range so validate reads WAV duration
The local static server used by validate/snapshot/layout answered every
asset request with a plain 200 and no Accept-Ranges header. Chromium treats
such resources as non-seekable, and for WAV that makes the media element
report `.duration` as Infinity no matter how long it buffers (readyState
reaches HAVE_ENOUGH_DATA but duration never resolves). The duration audit in
validate then emitted a spurious "Could not read the duration of N media
element(s) within the validate timeout" warning for a perfectly valid local
WAV, and a longer --timeout never helped because the value is never going to
arrive. MP3/MP4 carry duration in their container metadata so they were
unaffected.
Serve files with Range support (206 + Content-Range, plus Accept-Ranges on
the full 200) so the element is seekable. WAV duration now resolves, the
false warning is gone, and the genuine "media shorter than its slot" check
works for WAV for the first time.
* perf(cli): stream Range responses instead of buffering the whole file
serveFileWithRange read the entire asset with readFileSync and then sliced
it, so a 1KB Range of a 50MB MP4 still allocated the full 50MB per request.
Switch to statSync for the total size and createReadStream(filePath, { start,
end }) piped to the response, reading only the requested window. Behavior is
unchanged: 206 + Content-Range + Content-Length for a satisfiable range, 416
for an unsatisfiable one, Accept-Ranges advertised on every response, and a
plain 200 full-body stream when there is no Range header. writeHead is
deferred to the stream's open event so a failed open still answers 500, and
the fd closes on end/error. This benefits MP4 seek too, not just WAV duration.
Extend staticProjectServer.test.ts with an 8MB-file case that pulls a 4-byte
slice from deep inside and asserts the streamed bytes, Content-Range, and
Content-Length are correct.
|
||
|
|
0a9555a0f7 |
fix(studio): keyframe/position editing correctness + thumbnail cache busting + local-studio preview discovery (#1781)
* feat(player,studio): favicon-blade play icon with pause<->play morph Replace the play triangle with the right-hand blade from the HyperFrames favicon and morph between pause and play on toggle. Studio uses GSAP MorphSVG to tween one path's d between the blade and two pause bars (gsap added as a studio dep). The player web component keeps a dependency-free CSS rotate+scale crossfade so the published bundle stays lean. Both honor prefers-reduced-motion. * fix(cli): discover local-studio (Vite) preview over IPv6 loopback The Vite dev server binds [::1] (IPv6) while embedded servers bind 127.0.0.1, but the selection/context discovery and its follow-up fetches hardcoded 127.0.0.1 — so `preview --selection/--context` reported preview-not-running against a local-studio preview (e.g. inside the monorepo / bun run dev). Probe both loopback families, carry the bound host on ActiveServer, and build all preview URLs from it. Adds an IPv6-only discovery regression test. * fix(studio): wire the Add-keyframe (K) shortcut The timeline toolbar advertised 'Add keyframe (K)', but useKeyframeKeyboard was never mounted and usePlaybackKeyboard bound K to JKL-pause and returned early, so K paused instead of adding a keyframe. Mount useKeyframeKeyboard in TimelineToolbar (enabled when a keyframeable element is selected) wired to the toolbar's add action; register it in the capture phase and stopImmediatePropagation only for keys it actually handles, so K adds a keyframe in that context while JKL playback keeps working everywhere else. * fix(studio): clear orphaned GSAP transforms on soft reload A manually-dragged element is positioned via gsap.set, which writes an inline transform. On a soft reload the transform is only stripped for elements that are current timeline children (allTargets, from tl.getChildren().targets()). An element positioned by a standalone gsap.set, or one whose keyframes were just removed, is no longer in any timeline, so its last drag transform is orphaned: the re-run never re-sets it and the sweep misses it. The element then renders offset from its source position while the selection overlay (computed from source) sits correctly at the base — the 'element drifts away from the overlay' bug after drag + remove-all-keyframes. Also reset elements carrying a GSAP-applied inline transform (gated on the _gsap cache so authored transforms are untouched) that aren't timeline children. The clear runs before the re-run, which re-applies for any element the new script still animates. * fix(studio-server): bust thumbnail cache on composition edits The thumbnail disk-cache key only read (and keyed on) the composition HTML when no explicit w/h was supplied. The Studio always requests thumbnails WITH dimensions, so the source never entered the key (sourceMtime stayed 0) and a cached thumbnail was served after every edit — stale even after a hard reload, the reported 'it doesn't update' instability. Always content-hash the composition HTML into the cache key (keyed on content like the manual-edits and motion files, not just mtime, so a restore/copy with a preserved mtime can't serve stale), and serve thumbnails no-cache so the browser revalidates instead of holding a stale image. Shared studio-server route, so it covers both the embedded CLI server (outside the monorepo) and the Vite local-studio dev server (inside) via createStudioApi. * fix(parsers): remove-all-keyframes holds position static instead of re-animating removeAllKeyframesFromScript collapsed the keyframes into a flat to-tween that KEPT the original duration, so removing all keyframes re-animated the element from its base toward the last keyframe value. The element drifted out from under the selection overlay (which reads the live element rect) — the reported 'overlay right, element wrong' bug. Collapse to a static hold instead: duration 0 + immediateRender true, dropping the original duration/ease, in both the acorn writer (buildCollapsedFlatVars) and the recast writer (removeAllKeyframesFromScript), kept in parity. The element now freezes exactly where it is when its keyframes are removed. * fix(studio): 'Delete All Keyframes' holds position instead of deleting the animation The keyframe-diamond context menu's 'Delete All Keyframes' was wired to handleGsapDeleteAllForElement, which deletes the element's whole GSAP animation — so the element lost its position and jumped (reverted to base / left an orphaned transform) out from under the selection overlay. Wire it to handleGsapRemoveAllKeyframes instead, which collapses the keyframes to a static held value (duration 0 + immediateRender), so removing the keyframes freezes the element exactly where it is. * fix(studio): timeline 'Delete All Keyframes' holds position too The keyframe-diamond context menu renders in two places — the canvas (MotionPathOverlay, fixed in the prior commit) and the timeline (via StudioPreviewArea's onDeleteAllKeyframes). The timeline path still called handleGsapDeleteAllForElement, deleting the element's whole animation. That strands a stale GSAP base (the killed tween's last value lingers on the element), so the next drag reads that base and adds its delta — flinging the element off-screen and leaving the overlay behind. Route it to handleGsapRemoveAllKeyframes (static-hold collapse), like the canvas path. * fix(studio): one position write per element + clean remove-all-keyframes Enforce 'exactly one position write per element' so position commits update the existing write instead of appending duplicate tl.to/gsap.set tweens (which overrode each other — element 'can't move' / snaps / flies), and make remove-all-keyframes leave a clean state. - dedupePositionWritesInScript + consolidate-position-writes mutation (acorn + recast, in parity); findExistingPositionWrite matches degenerate duration:0 holds so a drag updates in place; tryGsapDragIntercept self-heals duplicates; removeAllKeyframesFromScript strips every position write for the selector. - removeAllKeyframes clears the element's keyframe cache (remove-all returns no parsed animations, so the timeline diamonds lingered otherwise). - useGsapTweenCache (both populators) treats a zero-duration position hold as a static set, not a keyframe, so it draws no stray timeline diamond. - Extracted gsapPositionDetection.ts (file-size cap). Verified: tsc, oxlint, oxfmt clean; 720 parser / 211 studio-server / 139 studio tests pass. Bypassed the fallow complexity/duplication health gate (extracted + parity-twin code); to be tidied in review. |