feat(proto): name the generator skew a drift red can only imply - #20
Merged
mattwilkinsonn merged 2 commits intoJul 29, 2026
Merged
Conversation
Contributor
Author
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Greptile SummaryAdds a generator-stamp gate for checked-in
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| tools/stamp-gate/index.ts | Discovers current generated TypeScript sources, invokes the plugin, and reports the stamp-gate result. |
| tools/stamp-gate/stamp-gate.ts | Implements stamp extraction, consistency checks, plugin-version comparison, diagnostics, and process exit mapping. |
| tools/stamp-gate/stamp-gate.test.ts | Covers successful comparison and the gate’s documented failure modes. |
| tools/stamp-gate/moon.yml | Integrates the uncached real-tree check and supporting test and typecheck tasks into Moon CI. |
| .moon/workspace.yml | Registers the stamp-gate package as a workspace project. |
| bun.lock | Adds the stamp-gate workspace package without introducing new third-party dependencies. |
Reviews (4): Last reviewed commit: "refactor(stamp-gate): extract report() a..." | Re-trigger Greptile
mattwilkinsonn
changed the base branch from
compass-agent-1405-protoc-gen-es
to
graphite-base/20
July 29, 2026 01:00
seal-agent
force-pushed
the
compass-agent-1405-stamp-gate
branch
from
July 29, 2026 01:15
119b22f to
dda9229
Compare
mattwilkinsonn
changed the base branch from
graphite-base/20
to
compass-agent-1405-protoc-gen-es
July 29, 2026 01:15
seal-agent
force-pushed
the
compass-agent-1405-stamp-gate
branch
from
July 29, 2026 17:17
dda9229 to
41ab88c
Compare
protoc-gen-es now comes from the pinned nixpkgs, so there is no lockfile pin recording which plugin produced the checked-in code. The only record is the `@generated by protoc-gen-es v<version>` header each generated file carries, and nothing was asserting those headers against anything. drift catches the outcome — a gen tree disagreeing with the plugin on PATH — but reports it as a comment-only diff on a PR that touched neither the schema nor the gen trees. This asserts the stamps agree with each other and with the plugin, so the two causes separate: a devenv.lock bump reads as "baked X != gen-tree stamp Y", and a partial regeneration reads as a named intra-tree disagreement with both paths. The trees come from three separate buf generate invocations, so one lagging while the others restamp is a real state, not a hypothetical. TypeScript rather than a shell gate because the decision has real logic — three failure modes, each needing its own message — and the core is a pure function over an injected runner and injected source texts. That is what makes the two fail-closed behaviours testable rather than asserted: an empty source set fails, because a glob matching nothing would otherwise pass by never looking; and a non-zero plugin exit fails before the banner is parsed, because protoc-gen-es exits 0 and prints its banner when healthy, so a banner-then-die wrapper must not grade as a working plugin. check carries cache:false. Its subject is the plugin on PATH, which is not an input moon can hash — a devenv.lock bump moves the binary underneath an unchanged tree, which is precisely the skew the gate exists to catch, and a cached green would survive exactly the change it must red on. tools/* joins the bun workspaces globs so the package resolves its toolchain from the root install like every other project. Co-Authored-By: seal <noreply@sealedsecurity.com>
Extract the exit-code mapping into a unit-tested report(), pin the GEN_GLOB out-dir invariant, and fix the two-tree count and uncached inputs notes surfaced by review. Co-Authored-By: seal <noreply@sealedsecurity.com>
seal-agent
force-pushed
the
compass-agent-1405-stamp-gate
branch
from
July 29, 2026 17:57
41ab88c to
93b2e26
Compare
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.

Closes SEA-1528. Stacked on #18 — the gate asserts against the
2.12.0regeneration that PR performs, so it cannot pass onmainalone.The other half of #18. That PR moved
protoc-gen-esto the nixpkgs store path; this one supplies the guard that went with it insealed#971, rewritten for a repo with notools/and no moon-invoked root-checks.Why drift is not already this
protoc-gen-esis a store path now, so there is no lockfile pin recording which plugin produced the checked-in code. The only record is the@generated by protoc-gen-es v<version>header each file carries, and nothing was asserting those headers against anything.compass-proto:driftcatches the outcome — a gen tree disagreeing with the plugin on PATH — but renders it as a comment-only diff on a PR that touched neither the schema nor the gen trees. This gate names the cause, and the two causes are different:The second is a real state, not a hypothetical: the trees come from three separate
buf generateinvocations, so one can restamp while the others lag.It already happened once, while #18 was being written
Verifying #18, I proved
driftcould fail by mutating a header, then rangit checkoutto clean up. That restored the committed2.12.1and silently undid the regeneration —git checkoutis revert-to-HEAD, not revert-to-what-I-had, and the file was uncommitted work.The tree it left behind was one file stamped
2.12.1and five stamped2.12.0: exactly the intra-tree disagreement above. It was caught by the wide gate going red, not by anything in the cleanup — anddrift's red said only that a header disagreed with the plugin, which is equally true of adevenv.lockbump nobody caused.So the partial-regeneration case is not a hypothetical this gate is written against in advance. It is a state one routine verification step produced on the first day the store path existed, and the reason it was cheap is that the mutation was still fresh in mind. A month from now the same tree arrives with no such context, and the difference between "your regeneration is half-applied" and "the plugin moved under you" is the difference between a one-command fix and an afternoon.
TypeScript, not a shell gate
AGENTS.md— the decision has real logic (three failure modes, each needing its own message), and the core is a pure function over an injected runner and injected source texts. That is what makes the two fail-closed behaviours testable rather than asserted, and both are ones a shell version drops silently:protoc-gen-esexits 0 and prints its banner when healthy, so a banner-then-die wrapper (wrong arch, missing interpreter) must not grade as a working plugin.Gates — run locally, forced, in this repo
This repo has no CI. Zero workflows, so
CLEANmeans nothing ran. Local testimony.40, up from #18's 35 — the new project contributes
typecheck,test,check,ci,install. The wide gate, because this diff reachespackage.json,bun.lockand.moon/workspace.yml.Proven capable of failing — all three modes, against the real tree
A gate observed only passing is indistinguishable from one that cannot fail, so each named failure was induced against the actual gen trees and the actual plugin, not just in fixtures:
Each restored;
stamp-gate:checkgreen after, gen trees byte-identical to #18.The 9 unit tests cover the same three plus the branches the real tree cannot produce on demand: an unstamped file, a plugin printing no recognisable version, a banner-then-die wrapper, a bystander semver on the plugin's output line, and a prerelease version on both sides.
That last one is a false-red guard rather than a false-green one: nixpkgs does ship prereleases, and a regex accepting only
x.y.zwould report "no version in output" for a perfectly healthy plugin. A false red costs more than a false green here, because a closed door does not get re-tested.checkiscache: falseIts subject is the plugin on PATH, which is not an input moon can hash. A
devenv.lockbump moves the binary underneath an unchanged tree — precisely the skew this gate exists to catch — and a cached green would survive exactly the change it must red on.devenv.lockis ininputsso the task re-runs on a bump, but the cache is off regardless, since the store path can move without that file changing in a way moon relates to this task.Composes with #19's pipeline — checked, not assumed
compass#19(SEA-1507) lands the first real CI this repo has had. Two ways this PR could have collided with it, both verified against its branch rather than reasoned about:The gate runs.
#19's job ismoon run :ci, deliberately not a per-task matrix, sostamp-gate:ciis picked up by existing there — no workflow edit needed, and no enumerated task list to go stale.The plugin resolves.
#19builds CI's PATH fromtools/toolchain/parity.ts --print-nix-attrs, which parsesdevenv.nix— so #18'sprotoc-gen-esentry is installed automatically. Ran#19's parity script against this stack'sdevenv.nix:That is the same store path
stamp-gate:checkcompares against, so the two agree by construction rather than by coincidence. Worth checking because the parity gate asserts per command, not per attribute — a package exposing an unexpected binary name fails there, and finding that out in someone else's PR is expensive.Spec-impact: none
Refs SEA-1405
Restacked onto #18's current head — the Graphite base branch had gone stale
This PR's body has said "Stacked on #18" since it opened, and that was accurate
about intent. What had drifted is the branch GitHub was diffing against:
graphite-base/20captured#18at the point this PR was created.#18thentook a review fix — the without-nix
protoc-gen-esdocumentation and theerror-message branch — and the snapshot did not follow. So this branch was
carrying a stale copy of its own parent:
gt restackresolved the real parent ("Restacked compass-agent-1405-stamp-gate on compass-agent-1405-protoc-gen-es"), and the doc fix is now present on thisbranch rather than absent from it.
Why this mattered beyond tidiness — corrected, because my first statement of it
was wrong. I wrote that the two doc changes "would render as additions here, this
PR appearing to add what its parent already added." False, and measurable:
GitHub diffs a PR against the merge base, not the base-branch tip. The snapshot
was
ahead=1 behind=0— strictly ancestor-side — so it was the merge base, andneither doc ever entered this PR's diff. The apparent-additions failure mode belongs
to a base that is behind on a path the PR touches; this base was ahead of nothing
#20edits.The real defect is the second reason, which is sufficient on its own: a merge
computed against the snapshot merges into a tree where those lines do not exist.
It is a defect in what lands, not in what the diff displays. Same class as the
generated-code finding in
#18— a textually clean operation over the wrong base.Worth recording the shape, since it happened twice in one edit: I caught a
decorative number by measuring it (below) and shipped a decorative mechanism
unmeasured in the same breath. A number reads as checkable and draws the check; a
causal sentence reads as explanation and draws none.
Re-verified after the restack:
One correction to my own first draft of this note, since it is the more useful
half. I wrote that the task count moving 35 → 40 was evidence the base changed —
five tasks entering the graph because the branch now really sits on
#18. Ichecked instead of shipping it, and it is false:
#18is 35 by itself, so the five extra tasks are this PR's owntools/stamp-gatepackage and say nothing about the base. The claim was decorative — the restack was
already established by
gt's own output and by the two doc measurements above —and a decorative number attached to an already-settled conclusion is exactly the
kind nothing pulls on. The base-change evidence is the
README/moon.ymldeltasand the
base=field, not the task count.The witness for this claim no longer exists as a branch
gt restackgarbage-collected the base branch it had created. The drift above isonly checkable because the report named the commit
f490d617rather than only thebranch. A branch name is a mutable pointer and Graphite deletes the ones it makes,
so for any claim about a base, cite the commit — an hour later the branch-name
version of this note would have pointed at nothing.
State line, restated
#20is UNSTABLE (pending), not CLEAN:Graphite / mergeability_checkin_progresswithconclusion=null,Greptile Reviewsuccess, 0 failingchecks. I reported CLEAN in the same message that restacked the branch — a
restack invalidates the state line reporting it, since the push resets every check.