Skip to content

build: content-gate every committed generated artifact, not just three - #683

Open
mparrett wants to merge 3 commits into
mainfrom
build/content-gate-all-artifacts
Open

build: content-gate every committed generated artifact, not just three#683
mparrett wants to merge 3 commits into
mainfrom
build/content-gate-all-artifacts

Conversation

@mparrett

@mparrett mparrett commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

make check-generated verified three of the six committed generated artifacts. It regenerated through lgprimgen and lgbgen directly, and three artifacts — pkg/ir/op_generated.go, pkg/rt/ir_bridge_generated.go and pkg/rt/core/ir/data/generated.lg — are produced only by scripts/generate.lg, so a gate that never ran it could not see them drift. That matters because a stale committed artifact means the bytecode and gogen_ir engines run different versions of the IR pipeline, which is the failure parity-full exists to catch and the gate exists to prevent.

This replaces #634, which patched cmd/lginterop/prims_emit.go — a file #639/#640 moved to internal/primgen/, where the import-grouping bug it fixed has since been fixed independently. The thesis survived the move; the diff did not. Closing that one in favor of this.

A second defect, found while widening the gate

make generate did not regenerate the corefns registrar. It was reachable only through the Makefile's core_compiled.lgb prerequisite, an mtime rule that stops firing once the committed registrar is newer than its sources. So make generate left drift in place while check-generated — which regenerated the registrar directly — reported it stale. The remediation the failure printed ("run make generate") could not fix the failure it printed it for.

That also means a gate regenerating solely through make generate would have passed corefns drift silently, so this had to be fixed for the widening to be sound.

Change

  • GENERATED-TRACKED lists the six committed artifacts. The gate stashes them, regenerates through the full make generate, and cmps each. Driving the whole pipeline rather than the two generators is what widens the coverage.
  • generate.lg emits the corefns registrar explicitly, so GENERATED-TRACKED and generate.lg are the same list.
  • The gate is non-mutating on every path. It restores the working-tree bytes on pass, on failure, and on generator error, so a failure no longer strands a half-regenerated multi-file tree with no printed way back.
  • It reports every stale artifact instead of exiting at the first, and a failing generator surfaces the underlying error rather than make[1]: *** [generate] Error 1.
  • docs/contribution-policy.md described the gate as bundle-and-lowered-tree only, which was already stale against main.

On the check-generated-manifest prerequisite

Dropping it does not change whether a stale digest fails CI. It still does, in the build job (which runs check-generated-manifest directly) and in TestGeneratedArtifactsAreFresh. Neither is touched here.

What changes is that a stale digest no longer aborts check-generated before the content gate can run. The digest is a proxy; the cmp is the check that binds sources to artifacts, and the proxy should not suppress the answer.

pkg/rt/generated.sums is stashed and restored rather than compared, because lgbgen's writeBundle refreshes it on every regeneration — without that, a check-* target would mutate a tracked file.

Verification

  • Passes on current main, reporting all six artifacts in lockstep, with the tree byte-identical afterwards.
  • Catches injected drift in each of the five text artifacts, including the three the previous gate could not see, and reports all five in a single run.
  • Restores the exact pre-gate bytes on failure, confirmed by digest.
  • The corefns defect confirmed both ways: before the generate.lg change, injected drift survived make generate; after it, the file is regenerated.
  • make check-generated-manifest still passes standalone.

Note on the recipe

It runs under plain /bin/sh with no -e. The makeplus branch that sets SHELL := bash and -e -o pipefail is skipped whenever go is already on PATH, which is every CI run. mktemp and cp failures are checked explicitly and a trap removes the temp dir on interrupt.

Overlap with #641

#641 also adds corefns generation to generate.lg. That one line will conflict; whichever lands second drops it in the rebase.

mparrett and others added 2 commits August 5, 2026 07:14
The corefns registrar (#640) was reachable only through the Makefile's
core_compiled.lgb prerequisite, which is an mtime rule. Once the committed
registrar is newer than its sources that rule stops firing, so `make generate`
left injected drift in place while `check-generated` — which regenerated the
registrar directly — reported it stale. The remediation the failure printed
("run make generate") could not fix the failure it printed it for.

Verified by injecting drift into the committed registrar: before this change
`make generate` left it untouched, after it the file is regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`make check-generated` verified three of the six committed generated artifacts.
It regenerated through `lgprimgen` and `lgbgen` directly, and three artifacts —
`pkg/ir/op_generated.go`, `pkg/rt/ir_bridge_generated.go` and
`pkg/rt/core/ir/data/generated.lg` — are produced only by `scripts/generate.lg`,
so a gate that never ran it could not see them drift.

Replace the three copy-pasted cmp stanzas with a single loop over a
`GENERATED-TRACKED` list, regenerating through the full `make generate`. Using
the whole pipeline rather than the two generators is what widens the coverage;
`GENERATED-TRACKED` and `generate.lg` are now the same list.

The gate is also non-mutating on every path. It stashes the working-tree bytes
first and restores them on pass, on failure, and on generator error, so a
failure no longer strands a half-regenerated multi-file tree that the reader has
to `git checkout` back. It reports every stale artifact rather than exiting at
the first, and a failing generator now surfaces the underlying error instead of
`make[1]: *** [generate] Error 1`.

`pkg/rt/generated.sums` is stashed and restored rather than compared, because
lgbgen's writeBundle refreshes it on every regeneration. `check-generated-manifest`
is no longer a prerequisite: the digest is a proxy, and a stale proxy should not
abort the run before the content gate can give the real answer. This does not
change whether a stale digest fails CI — the `build` job runs
`check-generated-manifest` directly, as does `TestGeneratedArtifactsAreFresh`,
and neither is touched here.

Verified: passes clean on `main` reporting all six; catches injected drift in
each of the five text artifacts, including the three the previous gate could not
see; leaves the tree byte-identical to what it found in both the pass and the
fail case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nnunley

nnunley commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Ran the gate rather than reading it. On this head all six GENERATED-TRACKED artifacts
report OK and the tree is clean afterward; injecting drift into pkg/ir/op_generated.go
reports exactly that file, still reports the other five, exits non-zero, and leaves
op_generated.go, core_compiled.lgb and generated.sums byte-identical to what the gate
found. The non-mutating claim holds on the failure path.

The coverage gap is real: op_generated.go, ir_bridge_generated.go and
ir/data/generated.lg come only from generate.lg, so the old gate — which drove lgprimgen
and lgbgen directly — could not see them drift. The corefns defect is real too, and the fix
is the right one: the printed remediation could not fix the failure it printed.

One bug. The comment at Makefile:318-320 says the tree is restored on every path, but
the trap at :356 is rm -rf "$tmp" on EXIT INT TERM — it deletes the stash without
restoring. Ctrl-C during the ~2min regeneration therefore leaves a half-regenerated tree
and destroys the only copy of the committed bytes, which is the one case where you most
want it back. Restore-then-remove fixes it. Related: the restore loop at :362/:375 uses
bare cp with no || exit under a shell without -e, so a failed restore is silent, while
the stash loop does check.

Ordering, and this one is on me. #641 is mine and it makes make generate selective —
staleOutputs in pkg/genmanifest/depmanifest.go hashes inputs and only os.Stats
outputs for readiness, never their content. Compose that with this gate and a stale committed
output whose inputs are unchanged means generate skips the stage, so the cmp compares the
file to itself and passes — blind to the drift the gate exists to catch. Your injected-drift
check passes today only because generation is unconditional.

The two also both add a corefns line to scripts/generate.lg, and merging them auto-merges
cleanly into a tree containing both stages. I would rather land this first and take the
burden on #641: it rebases, resolves the duplicate stage, and has to force a full
regeneration under the gate — or teach the staleness check to hash output content.

One bug I found by running the gate rather than reading it, and it is caused by this PR.
Dropping the check-generated-manifest prerequisite leaves $(GO) as the only one, and
check-generated is not .PHONY in either version. On main the prerequisite has no matching
file, so make always rebuilds and the omission is invisible; without it, a file named
check-generated in the repo root makes the target up to date:

$ make check-generated          # on this head, with a stray ./check-generated present
make: `check-generated' is up to date.
EXIT=0

$ mv check-generated /tmp/ && make check-generated
OK: core_compiled.lgb in lockstep with source.
EXIT=0

Both exit 0; one verified six artifacts and the other verified nothing. go build ./cmd/check-generated
produces exactly that filename, and my working copy had one in it, which is how I hit it.
Adding check-generated and check-generated-manifest to a .PHONY fixes it.

@mparrett
mparrett requested a review from nnunley August 11, 2026 16:48
Ctrl-C during regeneration deleted the recovery copy without restoring the
tree. A stray ./check-generated binary also made the gate a silent no-op.

Co-authored-by: Cursor <cursoragent@cursor.com>
@mparrett

Copy link
Copy Markdown
Collaborator Author

@nnunley Both bugs fixed in 8bc0888.

The EXIT/INT/TERM trap now restores from the stash before removing it, and each restore cp fails loud (|| exit 1) the same way the stash loop does. check-generated and check-generated-manifest are .PHONY — confirmed a stray ./check-generated no longer makes the gate a silent no-op.

Agreed on landing this ahead of #641; the gate needs unconditional generate until that side forces a full regen (or hashes output content). Happy to take the duplicate generate.lg corefns line as a clean rebase conflict when #641 moves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants