test(studio): gate edit accuracy in CI against the base branch baseline - #4765
Conversation
17ccc38 to
d2b3984
Compare
100bde4 to
a36929e
Compare
d2b3984 to
adc50c8
Compare
e01e479 to
9f685b1
Compare
adc50c8 to
1f83860
Compare
9f685b1 to
78119a6
Compare
1f83860 to
90fbb68
Compare
78119a6 to
0776411
Compare
90fbb68 to
70a12df
Compare
0776411 to
bf246b0
Compare
70a12df to
eb2fb47
Compare
bf246b0 to
b666b26
Compare
Work in progress: the ratchet and its tests; the CI job follows.
Shares the built CLI from the Build job, re-runs twice each case that passes on the base branch and failed, and posts one sticky comment from the gate job.
… banking them The shards re-ran only regressions, so one lucky pass of a base-failing case failed the gate as unbanked and was never listed as unstable. Every case whose verdict differs from the base branch, either way, is now re-run twice and judged 2 of 3. A render error fails the gate like an unsettled snapshot, a malformed baseline.json fails the job instead of reading as empty, and the header is 3 lines.
b666b26 to
41d5be1
Compare
jrusso1020
left a comment
There was a problem hiding this comment.
The ratchet itself is sound. Requesting changes on one thing: the path filter lets the bench's own dependencies skip the gate.
What works
- The base result is not recomputed. Both the shard and gate jobs read
baseline.jsonfrom the base branch tip (git fetch --depth=1 origin $BASE_REF, thengit show FETCH_HEAD:). A missing file becomes{"cases":{}}, and a file that doesn't parse throws (ratchet.mjs:104). That's the right way round. - Flake handling: any case whose verdict differs from the base in either direction gets two more runs, and it takes 2 of 3 to change the verdict (
ratchet.mjs:40). A case that fails 1 of 3 still passes and is listed as unstable. The tests cover both directions (ratchet.test.mjs:57-82). - The gate fails closed:
- A lost shard shows up as
missing, and the passing-count check catches it (ratchet.mjs:55,60). - No results at all throws.
set +eonly captures the exit code, and the last step fails on it.continue-on-erroris only on the sticky comment.!cancelled()means a failed shard still reaches the gate.
- A lost shard shows up as
- Case ids are
[a-z0-9-], so building the--filterregex from the flipped ids is safe. The re-runs drop--shard, which is correct since the ids already come from this shard. - The
entry()extraction inreport.mjsis byte-for-byte the old inline mapper.
Blocker: the filter misses code the bench runs (.github/workflows/ci.yml:162-174)
render.mjs:11 imports ../../../../producer/src/index.js directly, and render is one of the five gated metrics (ratchet.mjs:10). Yet none of these are in edit_accuracy:
packages/producer/**andpackages/engine/**packages/parsers/**, which Studio and the CLI both bundlepackages/player/**
On the Studio side, only components/editor and components/nle are listed. That leaves out components/EditorShell.tsx, components/panels, components/dock, contexts/, and App.tsx.
A regression in any of those paths merges without the job running. baseline.json on main still says those cases pass, so the next PR that does touch a filtered path fails the gate for someone else's change. That's the "unrelated PR blocked" outcome this gate is built to avoid. Add at least packages/producer/src/**, packages/engine/src/**, packages/parsers/src/** and packages/player/src/**, and widen the Studio entry to packages/studio/src/** unless cost rules that out.
Important: this is not a merge gate yet
Studio: edit accuracy gate isn't in main's required status checks. The ruleset lists Build, Test, Typecheck, regression, and the others, but not this one. So a red gate is advisory until the check is added to the ruleset. That's a repo setting rather than a code change, but the PR title's promise depends on it. Once it is required, the path filter becomes a skip, and a skip reports as passing.
Important: "unbanked" can block unrelated PRs (ratchet.mjs:53,62)
About 346 of the 660 base cases fail accurate(). If one of them sits near the 0.5px limit and passes 2 of 3 on an unrelated PR, that PR fails until its author commits the artifact's baseline.json. A case flaking near 50% could then swing back as regressed on the next PR. This is working as designed (the tests at :84-88 assert it), and 2 of 3 makes it rare. But it could put newly passing cases in the PR comment without failing, or bank them from main.
Nit
The base baseline is fetched when the job runs, while the checkout is the merge ref made when the workflow was triggered. If main banks a case in between, that case shows as regressed until a re-run.
Verdict: REQUEST CHANGES
Reasoning: The ratchet, flake handling and fail-closed behaviour are right. The path filter leaves out the producer the bench imports directly, and other packages it runs, so a regression can skip the gate and fail a later, unrelated PR instead.
— Rames Jusso
Edit accuracy: 434 passing here, 314 on the base branchThe gate passes. Newly passing (120)
|
…dio and CLI build inputs
…t or exports change
jrusso1020
left a comment
There was a problem hiding this comment.
Re-review at 36500de2. The blocker from my change request at 41d5be14 is resolved.
- Path filter (
ci.yml:162-193): it now covers all ofpackages/studio/src,cli/src, and thesrcof producer, engine, parsers, player, lint and sdk. It also covers the Studio and CLI build inputs and each bench package'spackage.jsonand subpath exports. Producer, whichrender.mjsimports as source, is in. - Core build: the shard now builds core before the bench runs (
ci.yml:987-988). The producer and engine source need core's generated files, so this was needed as well. - Baseline: 120 cases move to accurate and none move back. All 660 case ids are unchanged, so the count goes from 314 to 434. The gate run for this head (run 36787129352, head
36500de2) reports "434 passing here, 314 on the base branch. The gate passes." All 8 shards passed too, so nothing is unbanked or overclaimed.
Nit: @hyperframes/shader-transitions is a Studio dependency (packages/studio/package.json:102) but isn't in the filter. It's unlikely to move the gated metrics.
Still open, and not code: Studio: edit accuracy gate is not a required check on main yet. My earlier note about newly passing cases blocking an unrelated PR stands as a design trade-off; it isn't blocking.
Verdict: APPROVE
Reasoning: The filter now covers everything the bench builds or imports. The banked baseline matches a green gate run at this exact head.
— Rames Jusso
What
A CI gate for the edit accuracy benchmark. On every pull request that touches the manual-edit path, the full grid runs in 8 shards against the built CLI, and one gate job compares the result with the base branch's
baseline.json. It posts a single sticky comment, and it fails the check when accuracy falls.Stacked on the render drift PR (#4764), which is stacked on the benchmark (#4760).
Why
The benchmark only protects the score if a PR cannot quietly lower it. This makes "every later edit PR must not lower the score" a required check rather than a convention.
Related work
Refs #4760 and #4764.
How
Jobs (
ci.yml)edit_accuracy, covers only the edit path: the Studio editor, NLE, hooks, utils, player and webmcp sources; studio-server, core and the CLI server sources; the bench itself;bun.lock; andci.yml.packages/cli/distfor the shards (kept for one day).Studio: edit accuracy (i/8):prepare-ffmpeg-binbefore the install;hyperframes browser ensure, so Studio and the producer run in the engine's pinned headless shell;--grid full --shard i/8 --jobs 2.ratchet.mjs flipped).Studio: edit accuracy gate:baseline.jsonbuilt from the first run of each shard, ready to commit when cases newly pass.The rule (
ratchet.mjs). One slow or noisy run must not fail a PR, and a case that flips must never pass unnoticed.baseline.jsonthat does not parse fails the job. CI writes an empty baseline only when the base branch has none.baseline.json;baseline.jsonmarks as passing a case that fails here.Rounding.
baseline.jsonstores values rounded up to 0.01 (a change on #4760 and #4764), so a stored value is within 0.5 px exactly when the measured one is. Rounding to nearest had read seven 0.502 px results as passes.One source of truth.
report.mjsnow exportsentry(), the per-case shapebaseline.jsonholds, and the gate reads the same projection. Regenerating the baseline throughentry()from the main run's results reproduces the committedbaseline.jsonbyte for byte. The gate run against that baseline with the same results reports 314 passing on both sides and passes.Test plan
ratchet.test.mjs(9 tests, Vitest):flippedto regressions again fails the test;accurate()fails the test;actionlintonci.yml, oxlint, oxfmt,fallow audit --base origin/mainand the comment ratchet pass.Before
Without a gate, the benchmark only produced a table that nobody had to act on:
After
The gate's comment from the dry run: a case that passes on the base branch, failed 3 of 3 runs here, and fails the check.