Repository navigation
Conversation
Adds a committed-baseline visual regression gate:
- golden/<compositionId>/<timeMs>.png convention with an optional
golden.json manifest (times, threshold, maxDiffRatio, ignoreAntialiasing)
- 'hyperframes check --golden' re-captures at the manifest times,
pixel-diffs against the baselines (sharp raw RGBA, configurable
per-channel threshold, 1px anti-aliasing shift detection), fails the
check on regression, and writes golden-diff/ with per-frame red-highlight
diffs plus a baseline|current|diff contact sheet
- 'hyperframes check --update-golden' / 'hyperframes snapshot
--update-golden' refresh the baselines and manifest
- agent-readable JSON summary {ok, failed:[{id,time,maxDelta}], diffSheet}
embedded in the check --json envelope
- unit tests for the diff math (synthetic PNGs, no Chrome), the golden
conventions, and the check command wiring
- reference/cli-golden: directory convention, flags, golden.json manifest fields, failure artifacts, and the agent-readable JSON summary - guides/visual-regression: the commit-baselines workflow, agent loop usage, CI wiring, determinism guidance, and sample-time selection
A minimal 640x360 composition plus committed golden/ baselines and a manifest, demonstrating the check --golden / --update-golden loop and a scripted regression to try. Un-ignores the new example directory.
The repo-wide '*-demo/' ignore rule silently swallowed golden/golden-baseline-demo/, so the example shipped without its baselines. Rename the composition id to golden-baseline and commit the three baseline PNGs plus the manifest.
CheckReport gained both fields on main; the golden wiring test builds a full report and needs them to typecheck after the rebase.
|
Merged main to clear the conflict in check.ts, keeping main's version of the frame-check handling (swallowedFlagUsageError, no normalizeFrameCheckRawArgs) and carrying only the golden gate wiring over. I also cut the comments this PR added to check.ts in f5874ce, which is what the Comments check was flagging. Lint, typecheck, the comment ratchet and the check/golden tests are green on my end. |
miguel-heygen
left a comment
There was a problem hiding this comment.
Thanks for bringing this back with a fuller write-up. Reviewed at f5874ce.
Where it stands: main has moved a lot under check since your merge. packages/cli/src/commands/check.ts now conflicts in five places, along with docs/docs.json. Since then check gained scoped checks, a project lock with per-revision artifacts, warm Chrome reuse and --prepare-media, so the golden wiring in executeCheck/emitCheckReport needs re-threading into the new flow rather than a plain re-merge. The green CI here is from before all of that. Nothing on main does a committed-baseline gate, so the idea isn't superseded.
Two things I'd settle before the rebase:
- Where baselines come from. You already noted that the Linux-made example baselines fail on macOS from text antialiasing alone. The capture is a
page.screenshotin the local snapshot browser, so a committed baseline only holds on the OS that made it, and a CI gate on such files fails for anyone on another platform. This is a design call for the maintainers. For example, baselines could only be created and checked inside the pinned render image, or stored per platform. It's worth agreeing on before more code depends on thegolden/layout. snapshot --update-golden --at <t>replaces the whole baseline set.writeGoldenBaselines(packages/cli/src/golden/baseline.ts:355) removes any baseline whose time wasn't captured in this run and rewritestimesto the captured set. So refreshing one frame withsnapshot --update-golden --at 2on the example removes500.pngand3500.pngand leavestimes: [2]. The flag help does say--atoverrides the sample times, but someone refreshing a single frame will probably expect the others to stay. Merging into the existing set, or at least printing which baselines were removed, would avoid the surprise.
Verdict: COMMENT
Reasoning: The branch conflicts with the reworked check command and the cross-platform baseline question needs a maintainer decision first; one update-path footgun is noted above.
`snapshot --update-golden --at <t>` deleted every baseline whose time was not captured in that run and rewrote `times` to the captured set, so refreshing one frame wiped the rest. writeGoldenBaselines now writes the captured frames and merges their times into the existing set (manifest times plus baseline filenames, keyed by millisecond) instead of pruning. The snapshot flag help and the cli-golden reference say so.
Re-threads the golden gate into main's reworked check flow: the gate runs inside the project lock after the pipeline, --prepare-media still exits before it, and the saved-report lines print after the golden section. snapshot --update-golden now takes the project lock as snapshot does, and the check flag table in docs/packages/cli.mdx lists the golden flags.
Preview approval requiredThis pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit |
|
Thanks for the close read. 562f93b fixes the partial update and merges current main.
For the merge I kept main's On where baselines come from, that's your call as maintainers. Which do you prefer: baselines created and checked only inside the pinned render image, or baselines stored per platform? |
miguel-heygen
left a comment
There was a problem hiding this comment.
Thanks for the update. The partial refresh now keeps the other baselines, and the test covers it. The merge with the reworked check also looks right: --prepare-media still exits first, the gate runs inside the project lock after the pipeline, and the report and revision lines still print.
Two checks are red on this branch:
- Build:
check:cli-lazy-deps.packages/cli/src/golden/pixelDiff.ts:9importssharpstatically, and the CLI only allows it as a lazy load.contactSheet.tsshows the pattern:const sharp = await loadSharp("contact sheets")from../utils/optionalPackages.js. - Comments.
pixelDiff.tsis a new file at 11.4% comment lines against the package's 11.0%. Cutting a line or two gets it under.
Two smaller things:
- The saved report doesn't include the golden result.
runPipelinesaves the report and revision before the golden gate runs, and the golden result only reaches the terminal and--jsonoutput. Socheck --goldenwith a regressed frame prints "Check failed" and exits 1, but the file printed afterReport:says the check passed. Running the gate before the save, or addinggoldento the report that gets saved, would keep them in step. - Removing a time from
golden.jsonno longer works on its own. A full--update-goldensamples the manifest times, then the merge adds back every PNG still in the folder. If someone removes3.5fromtimesand refreshes,3.5comes back and3500.pngkeeps the old frame, so the nextcheck --goldencompares against a stale baseline. Merging only when--atis given, and replacing the set on a full refresh, would keep both cases working. Otherwise, documenting "delete the PNG to drop a time" would do.
On where baselines come from: thanks for laying out the two options. That decision is for the maintainers, and it's still the main thing to settle before this can land.
Verdict: COMMENT
Reasoning: The conflict and partial-update issues are fixed, but Build and Comments fail because of this branch, and the cross-platform baseline question still needs a maintainer decision.
- pixelDiff loads sharp lazily through loadSharp, as check:cli-lazy-deps requires, and drops the header comment. - check runs the golden gate inside runPipeline before the report is saved, so the saved report carries `golden` and the combined `ok`. - A full --update-golden replaces the baseline set again: PNGs for times no longer sampled are removed. Only snapshot --update-golden --at merges into the existing set.
miguel-heygen
left a comment
There was a problem hiding this comment.
Thanks, this round fixes everything I raised, and CI is green.
sharpnow loads lazily throughloadSharp, and the comment share is under.- The golden result is merged into the report before it's saved, so the saved file, the terminal,
--jsonand the exit code all read the sameok. Passing it in as afinishstep keeps the save in one place. - A full
--update-goldenreplaces the set again and only--atmerges, with a test for dropping a time from the manifest and a docs update.
From the code side I have nothing further. What's left is the question you asked about where baselines come from (created and checked only in the pinned render image, or stored per platform). That's for the maintainers to answer, and it's the main thing to settle before this lands.
Verdict: COMMENT
Reasoning: All earlier code findings are fixed and CI is green; the baseline-source decision is still open for the maintainers.
|
Both red checks were mine, and both smaller points are fixed in a0ed431.
On a0ed431, every CI check that ran passed, including Build, Comments, Typecheck, Lint and Format. Thanks for confirming the baseline question is with the maintainers. |
jrusso1020
left a comment
There was a problem hiding this comment.
Requesting changes for one security issue: the composition id is used as a path segment without validation.
data-composition-id is read from the rendered page after the project's own scripts run (packages/cli/src/golden/baseline.ts:315), and used as-is (:320). It then becomes a path segment in goldenCompositionDir (:108, join(projectDir, "golden", compositionId)) and in the diff directory, which is deleted with rmSync(join(project.dir, "golden-diff", compositionId), { recursive: true, force: true }) (:405-406).
So a project whose composition sets data-composition-id="../.." (or a script that sets it at runtime) makes hyperframes check --golden recursively delete a directory outside the project. --update-golden writes PNGs and golden.json to the same escaped path, and deletes digit-named .png files there. The gate is opt-in, but people run check on projects they didn't write (templates, shared repos, agent output).
Fix: validate the id against an allowlist pattern before using it, the way frame ids already are (SAFE_FRAME_ID_PATTERN), and also assert the resolved directory stays inside project.dir before any rmSync or write. A test with data-composition-id="../x" covering both --golden and --update-golden would pin it.
Everything else in the security pass looked fine, including the three baseline PNGs (valid 640x360, no trailing data). The broader feature still needs a maintainer product call.
— Rames
|
Direction is good. Once the path issue from the review is fixed we'll take it. For baselines, start with per-platform since that's what people will actually have locally. |
…e-gate # Conflicts: # packages/cli/src/commands/check.ts
|
fb205b8 brings current main into the branch. The one conflict was in On the merge, the CLI typecheck shows no new errors, This push is only the merge. The composition id path fix from the security review is still to come in its own commit. |
executeCheck resolved the project before parsing --range and the golden flags, so `check --range 5,1` in a directory without a composition exited 1 with "No composition found" instead of the usage error (exit 2) that the agent-mode contract expects. Parse options first, as main does.
|
The red CLI jobs came from one test, That case fails before the change and passes after it, and |
The composition id comes from the rendered page and was used as a path segment under golden/ and golden-diff/. An id such as ../.. let check --golden delete, and --update-golden write, outside the project. Validate the id against an allowlist before any file access and assert the resolved golden and diff folders stay inside their roots.
|
Pushed 348084f. The composition id is now checked against an allowlist, and the golden/ and golden-diff/ folders must stay inside the project before any write or delete, for both |
|
@jrusso1020 for the per-platform baselines, would you prefer |
…e-gate Conflicts: docs/docs.json Main moved the Developers navigation under the Docs tab. Kept main's structure and added guides/visual-regression and reference/cli-golden to the same groups they were in.
|
0ac84ed merges current On the merge, The full CLI suite was not clean locally. 28 tests failed in a run on a heavily loaded machine, and 19 of those passed when I reran the failing files one at a time. The other 9 are in the offload audio track, sherpa, whisper manager, project lock and checkSweep WebGL tests. The same 9 fail on a clean checkout of This push is only the merge. Per-platform baselines still depend on the layout question above. |
Trim pixelDiff.ts to its package's comment share, and expect the resolved path in the golden path test, since goldenCompositionDir resolves against the project and adds a drive letter on Windows.
…e-gate Resolve conflicts in packages/cli/src/commands/check.ts and check.test.ts. Main added the check pass cache (checkOrReplay), the early report path notice, --include-unmounted and the cached-pass line in the human report. executeCheck now runs through checkOrReplay, the golden finish step rides the pipeline as before, and a --golden or --update-golden run skips the cache because the cache key does not cover the baselines.
|
03a9fb7 merges current main. The conflicts were in |
Rebased version of #3756, which was closed as stale (it couldn't be reopened). Rebased on current main. The only conflict was docs/docs.json nav entries, plus one test fixture fix for the new browserSkipped and hdr fields on CheckReport. Heads up: the committed example baselines came from Linux, and on macOS the text antialiasing alone trips the gate. Happy to loosen the example's tolerance or document per-platform baselines.
What
A committed-baseline visual regression gate for compositions:
golden/<compositionId>/<timeMs>.pngwith an optionalgolden/<compositionId>/golden.jsonmanifest (times,threshold,maxDiffRatio,ignoreAntialiasing).hyperframes check --goldenre-captures the composition at the manifest times, pixel-diffs each frame against its baseline (configurable per-channel threshold, 1px anti-aliasing shift detection), fails the check on regression, and writesgolden-diff/with per-frame red-highlight diff PNGs plus abaseline | current | diffcontact sheet.hyperframes check --update-golden/hyperframes snapshot --update-goldenrefresh the baselines and manifest.check --jsonenvelope:{ok, failed:[{id, time, timeMs, maxDelta, diffRatio, reason}], diffSheet, ...}.examples/golden-baseline/; docs atdocs/reference/cli-golden.mdxanddocs/guides/visual-regression.mdx.Why
Agents regress layouts silently: a refactor nudges a headline, a token change recolors a button — and every command still exits 0. HyperFrames already has
snapshotandcompare, but no committed golden baseline and no fail-on-diff CI gate. This turns "the video still looks right" into a hard, committable contract with one exit code.How
packages/cli/src/golden/pixelDiff.ts— pure diff math on raw RGBA buffers viasharp(already a CLI dependency; no new dependencies): per-channel threshold, neighborhood-based anti-aliasing classification, red/amber/pale-gray diff visualization.packages/cli/src/golden/baseline.ts— thegolden/convention, manifest parsing/validation, sample-time resolution (--at→ manifest → baseline filenames → default spread), and the gate/update orchestration. Capture reuses the same font-localized, settled-page path assnapshot/compare(openSettledCompositionPage+seekCompositionTimeline).packages/cli/src/capture/contactSheet.ts— newcreateGoldenDiffContactSheet(onebaseline | current | diffrow per failed time).check/snapshotwiring:--golden,--update-golden,--golden-threshold; golden failures combine with the normal check exit code; a missing baseline fails with instructions rather than silently passing.golden-diff/is derived output and git-ignored in the example.Walkthrough (HyperFrames-rendered)
The walkthrough is itself a HyperFrames composition rendered to MP4 with this branch's CLI — storyboarded first, authored as HTML, validated with
hyperframes lint+hyperframes check(0 errors, 33/33 WCAG AA text checks), then rendered with:Every terminal line and every frame image in the film is real CLI output from
examples/golden-baseline: the three committed golden PNGs, the failing run's current frame, its red-highlight diff, and the generated contact sheet.▶ Watch the walkthrough (MP4, 48s, 2.8 MB)
Stills from the rendered film:
The real diff contact sheet produced by the failing run (
golden-diff/golden-baseline/contact-sheet.jpg):Test plan
packages/cli/src/golden/pixelDiff.test.ts(diff math on synthetic PNGs, no Chrome: thresholds, AA classification, dimension mismatch, PNG round trip),packages/cli/src/golden/baseline.test.ts(path conventions, manifest validation, sample-time precedence, gate verdicts),check.test.ts(flag grammar + command wiring/exit codes). Full@hyperframes/clisuite: 213 files, 3034 tests passing (bun run test).examples/golden-baseline: created baselines (snapshot --update-golden), verifiedcheck --goldenpasses across independent captures, tweaked the accent color#f5a623 → #e0245e, verified the gate fails with exit 1 + diff sheet + JSON summary, then refreshed withcheck --update-golden.docs/reference/cli-golden.mdx,docs/guides/visual-regression.mdx, registered indocs/docs.json; example README.bunx oxlint/oxfmtclean;tsc --noEmitclean;fallow auditclean (no new dead code, cycles, duplication, or complexity findings).AI disclosure
This PR was authored by an AI agent (Claude Fable 5 via Cursor Cloud Agents), including the code, tests, docs, example, and the HyperFrames-rendered walkthrough video. It was developed and verified end-to-end as described in the test plan; per CONTRIBUTING, correctness responsibility is accepted by the submitter.