The shape gate: a CI refusal that keeps hand-coded shape measurement out - #200
Merged
Conversation
This was referenced Aug 31, 2026
Owner concern, 2026-08-31: "if you have any other profiling per-language that is hand coded ... I don't trust this and it will drift. We have to lock this in." bench/tools/shapegate extracts the shape vocabulary from bench/corpus and refuses four things: a corpus identifier named under bench/, a timing primitive anywhere outside the sanctioned runner and tool directories, a bench-shaped source path outside them, and a shape's wire size written down as a literal. bench/SHAPE-GATE.allow is the complete register of what does not yet comply, with exact counts that ratchet: growth fails, and so does leaving a count too high once the debt is paid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rule text moves out of the error value and under it, which is where a reader wants it anyway. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#199 deleted family rt and the §1.3 hand-coded shapes, #204 retired the §1.2 example-corpus rows from every runner, #201 deleted the decomp.cpp scratch harness. The ledger's ratchet caught all of it: nine stale counts, seven entries matching nothing. Name hits under bench/ drop from 2080 to 146, and of the 29 remaining in the nine runner legs NOT ONE is hand-coded shape measurement — every one is a comment, a generated-symbol callsite, or an English word that is also a corpus field name. The nine legs are shape-blind. The gate now guards a clean tree instead of a shrinking debt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gafferongames
force-pushed
the
shape-gate
branch
from
August 31, 2026 13:36
20881b7 to
351ca51
Compare
Two findings from re-proving the gate on a fully provisioned tree. dist/ is the Makefile's pinned toolchain drop — the Dart SDK, the JDK, OTP, Elixir. It is gitignored and absent on CI, which uses setup-dart/setup-java, so the shape-gate job never met it. On a developer machine that followed the Makefile's own dist/ instructions the gate refused with 42 findings, every one of them inside a downloaded toolchain: the Dart SDK ships lib/core/stopwatch.dart and Mix ships profile.fprof.ex. `make shape-gate` was unusable on exactly the trees that can run the whole bench. dist/ joins the skipDirs list. The package doc gains the limitation #198 demonstrated: this gate guards MEASUREMENT, not CORRECTNESS. The Go emitter wrote a fixed scalar array twice — 32 wire bytes where the other eight languages write 16 — and Go-to-Go round-trips passed clean because both ends shared the defect. A shape-blind runner driving a defective emitter is still shape-blind and still passes here. Issue #203 records the corpus blind spot. It also now says plainly that a shape name in a comment counts as a hit, which is deliberate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shape-gate job was appended after cpp-lock's explanatory comment and before cpp-lock itself, orphaning the comment above the wrong job. Same two jobs, comments back with their own. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Owner concern, 2026-08-31, verbatim: "My concern is if you have any other profiling per-language that is hand coded. I don't trust this and it will drift. We have to lock this in."
This is the lock. An estate-wide audit ran alongside it; the findings are at the bottom.
THE DEBT IS PAID — the gate now guards a clean tree
When this gate was written on
main@6e1002bits register carried 2080 name hits across twelve runner files, because every one of the nine legs hand-wrote the corpus shapes: a familyrttranscription against the runtime API, plus a familygenpinned initializer and LCG vary mapping per shape per language. Nine hand-written approximations of one benchmark. That was the drift the owner named.Three PRs landed since:
rtand the §1.3 hand-coded shapes from every legbench/results/harness-contract-air/decomp.cpp, the scratch harness this PR had flagged for the owner's rulingThe ratchet caught all of it, in the direction that matters. The rebased gate refused with nine stale counts and seven entries matching nothing — debt paid without the register saying so, which is a failure by design. The register is now rewritten to the true numbers.
Across the nine runner legs the count falls from 1946 to 29, and — the part worth saying loudly —
Every remaining hit in the nine legs is one of exactly three kinds, and every ledger line says which:
// Java codecs over bench/corpus/Bench.schema's BenchMixed.Write/Read. It writes no field and pins no valuebench_datadriven<bench::BenchMixed>( "bench_mixed", ... )extrain "one extra untimed pass"The nine legs are shape-blind. The estate has reached the owner's ruling — there is only a single schema bench:
Bench.schema— and this gate's job changes from shrinking a debt to holding a clean tree. Any leg that grows one hand-written field, one pin, or one wire size fails on the next push.What is left on the register is the corpus tooling (one deliberate copy, by design), the §1.5 golden pinner's bench-shaped path, and two numeric false positives. Nineteen entries, all named, all reasoned.
The rule, made mechanical
The estate has exactly one sanctioned benchmark: this repo's data-driven bench. Shape knowledge belongs in
bench/corpus/*.schemaand in the code the compiler generates from it. Hand-written runners are fine and are the design. Hand-written measurement of a schema shape is not, anywhere.bench/tools/shapegaterefuses four things:namesbench/timingpathsconstsbench/The vocabulary is extracted from the corpus, not written into the checker, so renaming a field starts guarding the new name in the same commit. The wire constants come from the committed goldens' own file sizes, so no comment format or emitter behaviour has to hold for them to be right. If either extraction comes back empty the gate refuses to run rather than passing blind.
bench/SHAPE-GATE.allowis the complete register of everything that does not comply, each line with an exact count and a reason. The count is a ratchet in both directions: more hits fails because the debt grew; fewer hits also fails, printing the number to write, because the debt was paid and the register must say so; and an entry matching no file fails as rot. That is not a claim — it is what happened on this rebase, and it is what produced the numbers above.make shape-gatelocally. CI jobshape-gate, on push and pull_request — unlikecpp-lockthis is a standing property of the tree, not a review-window freeze.Negative control, re-proven on the rebased head
A gate proven on an older tree is proven on an older tree. All four plants were re-run against
main@0514fc4+ this branch, each reverted to green after. Pasted verbatim.Controls A and B are the originals. Controls C and D needed new subjects — the originals planted into
bench/go/rt.goandbench/elixir/runner.exs's hand-written shape block, and #199 deleted both. Their replacements exercise the same two ledger directions on files that exist today, and are noted where they differ.A. A plausible hand-coded bench in
internal/perf/mixed_bench.c—clock_gettime, a 438-byte buffer:B. Shape code sneaked into a sanctioned runner (
bench/dart/main.dart, ledger cap now 3, not 39 — the cap fell with the debt, so the plant is caught by a much tighter fence than before):C. Ledger rot — a ledgered file removed without touching the register. Changed subject: the original deleted
bench/go/rt.goto simulate #199; #199 has landed and that file is gone, so the plant now deletesbench/elixir/main.exs.D. Debt paid, register not lowered. Changed subject: the original removed four hand-written field names from
bench/elixir/runner.exs; there are no longer four hand-written field names in any leg to remove — which is the point of this whole rebase. The equivalent plant rewrites the one comment inbench/java/Main.javathat names the shape, dropping its count from 6 to 5:The refuse-to-run-blind paths, also tested — the gate must refuse rather than pass silently when an extraction comes back empty. Three runs against synthetic roots:
All three exit 1. On the real tree both extractions are non-empty: 161 corpus identifiers, 731 spellings, and the wire constants 49 110 112 135 156 160 392 438 3504 read from the four committed goldens (
bench_ints14 B,bench_bits20 B,bench_packet49 B,bench_mixed438 B) plus the schema's declared bit totals. The 14- and 20-byte shapes fall below theminConstfloor and are guarded by name only, exactly as the package doc says.Green on the rebased head as committed:
make testpasses locally on the rebased head with the full pinned toolchain.gofmt,go vet ./...andgo mod tidy -diffclean.One defect the re-proof found:
dist/Worth recording, because it was invisible on CI and would have hit every developer.
dist/is the Makefile's pinned toolchain drop — the Dart SDK, the JDK, OTP, Elixir — gitignored, and absent on CI, which usessetup-dart/setup-javainstead. So theshape-gatejob never met it. On a machine that had followed the Makefile's owndist/instructions,make shape-gaterefused with 42 findings, every one of them inside a downloaded toolchain: the Dart SDK shipslib/core/stopwatch.dart, Mix shipsprofile.fprof.ex, OTP shipsclock_gettime.The gate was unusable on exactly the trees that can run the whole bench, and green CI could never have told anyone.
dist/joinsskipDirsalongsidegenerated,vendorandnode_modules, with a comment saying why. Found only because the re-proof ran on a fresh clone provisioned the way the Makefile documents.What this gate CANNOT see
Stated plainly, because a gate that oversells itself is worse than none. Also in the package doc, where the next reader will find it.
x,if,delta,chatandhealthare all corpus field names and guarding them would fire on English prose. A bench written using only those names would pass — it would also be unreadable.dist/,generated/,vendor/,node_modules/,build/,target/. Nothing there is committed, so nothing there can drift into the repo, but the gate is blind inside them by construction.Covering the other repos
Nine sibling repos are deleting their in-repo benches (
serialize#105,serialize.c#57,serialize.cs#36,serialize.dart#3,serialize.elixir#2,serialize.go#54,serialize.java#2,serialize.js#9,serialize.rs#66). Nothing stops a tenth from appearing.The cheap cover, not built here: lift the timing and paths checks — the two that need no corpus — into a reusable workflow in
mas-bandwidth/.github, and have each family repo call it. Those two checks are the ones that generalise;namesandconstsneed a corpus and belong only here. That is a separate PR in a separate repo and should be ruled on separately.The register today
Nineteen entries. Ten runner lines (comment / callsite / prose, as classified above), five corpus-tooling lines, two numeric false positives, two
pathslines for the §1.5 golden pinner.The register names, deliberately and permanently until superseded:
bench/tools/variantgenandrealpacket-gen(one copy of shape code each, which is the design — one copy replacing nine; #195's emitter supersedes variantgen),bench/tools/inline-gate.sh(shape names as symbol-map labels and disassembly fixtures — no serialization, no clock), andtest/bench/(the §1.5 golden pinner: correctness code with no clock, which is exactly the line this gate draws).Two numeric entries are honest false positives, kept visible rather than special-cased:
bench/java/Main.javacarries three references to GitHub issue #156, which collides with a corpus shape's declared 156-bit total, andbench/c/bench_main.cstates the 438-byte golden size in a comment. Special-casing either would put a hole in the numeric check to buy tidiness.Resolved since the original PR:
bench/results/harness-contract-air/decomp.cpp— the 294-line hand-coded harness this PR flagged for the owner's ruling, held on the register under all four checks so it could not be forgotten. #201 deleted it and kept the.md, which was the recommendation. Its four ledger lines went stale on this rebase and the gate demanded their removal by name.Files touched
bench/tools/shapegate/main.go,bench/SHAPE-GATE.allow,.github/workflows/ci.yml(one job),Makefile(one target appended),bench/README.md(one section appended). Nothing else.🤖 Generated with Claude Code