From c31fd794d08723600b864a33c1d4a454d56bf007 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 24 Aug 2026 15:13:47 +0000 Subject: [PATCH 1/3] Write down the stage0 mirror fixed point, because CI taught it to me the expensive way A .dag compiler-authority edit refuses until its regenerated mirror is committed, and the obvious move -- regenerate once, commit -- is wrong for a reason that is the bootstrap rather than a bug: the first candidate is the new .dag emitted by the OLD compiler, so committing it changes what the compiler emits and the next generation differs again. gunbc#9101 took two rounds and my first attempt was rejected by CI carrying only generation one. Records the loop, the verification steps that make "these are the compiler's bytes" checkable rather than asserted, the diff-transport trick for when the loop cannot run where the commit happens, and the branch-versus-merge-ref ruling with its honest limit -- one instance of transfer establishes that it can hold, not that it is automatic. Co-Authored-By: Claude Opus 5 (1M context) --- ...mirror_regeneration_protocol_2026-08-24.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md diff --git a/docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md b/docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md new file mode 100644 index 00000000000..1165e75dca1 --- /dev/null +++ b/docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md @@ -0,0 +1,62 @@ +# Regenerating the stage0 mirror for a `.dag` compiler-authority edit + +Derived from landing one emitter change (gunbc#9101) and having CI reject the first attempt. +Every lane that edits a `src/v1/*.dag` compiler authority pays this cost; this note exists so the +next one does not re-derive it. + +## Why the obvious approach fails + +`claim_executor --required-regen` compares the committed stage0 mirror against a fresh emit and +refuses on drift. So a `.dag` authority edit refuses until its regenerated mirror is committed +beside it. The tempting move is to regenerate once and commit the result. + +**One round is not enough, and the reason is the bootstrap, not a bug.** The first candidate is the +new `.dag` emitted by the **old** compiler. Committing it means the compiler built *from that mirror* +now carries the new behaviour — so when *it* regenerates, it re-emits every other module the change +reaches. Measured on gunbc#9101, whose change altered how folds with an unused element emit: + +``` +ITER=1 RC=1 first_generation_equal=false drift: v1_compiler_emit_rust.rs <- the edited authority +ITER=2 RC=1 first_generation_equal=false drift: std_types.rs <- a module the new emitter now emits differently +ITER=3 RC=0 first_generation_equal=true +``` + +`std_types.rs` `list_length` is a fold with an unused element: `|acc: i64, _: _|` became +`|acc: i64, _|`. It is not unrelated cleanup riding along with the fix; it is the same change +reaching a module the first generation had not yet re-emitted. **The generation-one delta is not the +mirror change** — a PR carrying only it will pass locally and refuse in CI. + +## The protocol + +1. Start from the branch worktree. +2. Run `--required-regen`. On drift: install the **complete** candidate, rebuild the compiler from it, + repeat. +3. Stop only at `RC=0` / `first_generation_equal=true`. +4. Export the **cumulative** diff against the original branch state: `git diff -- src/v1/stage0/src`. +5. Verify before applying — changed-path list, decoded byte count against the producer's own count, + and `git apply --check`. +6. Commit the compiler-authored mirrors beside the `.dag` change. + +**The mirror's only sanctioned author is the compiler.** Every byte above comes from the candidate's +own diff; hand-authoring a mirror to satisfy the gate is the laundering the gate exists to prevent, +and it is indistinguishable from regeneration in the diff alone. That is the reason for step 5's byte +count: it makes "these are the compiler's bytes" checkable rather than asserted. + +## Transport, when the loop cannot run where the commit happens + +A remote runner may have no push credentials, and a local build of `v1-compiler` may not be affordable +(shared memory slice, swap disabled). Mirror files are far too large for a log. What works is moving +the **diff** rather than the files: the loop runs remotely, prints the cumulative diff base64-encoded +with a byte-count header, and the patch is decoded, verified and applied where the commit happens. +gunbc#9101's two generations were 3748 and 555 bytes. + +## Branch tree versus merge ref + +CI regenerates on the **merge ref**; the loop above runs on the **branch tree**. These are different +trees whenever the base has moved, so convergence on one does not entail convergence on the other. + +**Iterate against the branch tree while authoring, and treat merge-ref convergence as a separate +acceptance fact supplied by CI.** gunbc#9101 is one instance where the transfer held — green on the +merge ref with no further drift — which establishes that it *can*, not that it is automatic. A base +that moves under a branch can require another round, and the honest response is another iteration, +not an assumption that the branch result carries. From d153e35a416420f6fbf779b33dd93b7f17363380 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 24 Aug 2026 15:14:20 +0000 Subject: [PATCH 2/3] The three-row table is the exercise, not one command's output; say so --- ...stage0_mirror_regeneration_protocol_2026-08-24.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md b/docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md index 1165e75dca1..f262582916b 100644 --- a/docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md +++ b/docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md @@ -16,11 +16,17 @@ now carries the new behaviour — so when *it* regenerates, it re-emits every ot reaches. Measured on gunbc#9101, whose change altered how folds with an unused element emit: ``` -ITER=1 RC=1 first_generation_equal=false drift: v1_compiler_emit_rust.rs <- the edited authority -ITER=2 RC=1 first_generation_equal=false drift: std_types.rs <- a module the new emitter now emits differently -ITER=3 RC=0 first_generation_equal=true +generation 1 drift: v1_compiler_emit_rust.rs <- the edited authority itself +generation 2 drift: std_types.rs <- a module the new emitter now emits differently +generation 3 first_generation_equal=true ``` +That is the sequence across the whole exercise, not a transcript of one command: generation 1 was +observed and committed on its own, and CI then refused the branch with `drift: std_types.rs`, which is +what sent me back for the loop. The loop itself reported `ITER=1 RC=1 drift: std_types.rs` and +`ITER=2 RC=0 first_generation_equal=true` — the same two remaining rounds, numbered from where it +started. Run the loop from the beginning and it reports all three. + `std_types.rs` `list_length` is a fold with an unused element: `|acc: i64, _: _|` became `|acc: i64, _|`. It is not unrelated cleanup riding along with the fix; it is the same change reaching a module the first generation had not yet re-emitted. **The generation-one delta is not the From a06385ff0cfab4ef49994e534385979e1471a6a1 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 24 Aug 2026 17:44:46 +0000 Subject: [PATCH 3/3] Two corrections: the second drift is not a law, and a byte count is not a digest Both from review in the side thread, both real defects in the note. 1. The note generalized #9101's three-generation history into "one round is never enough". A behaviour change no other seed module exercises can leave generation two identical to generation one -- no second drift, and the second regeneration is STILL mandatory because it is what proves equality. The law is about the equality check, not about expecting a second drift: one drift-producing regeneration may be enough; one regeneration without a subsequent equality check never is. 2. The note claimed a byte count makes "these are the compiler's bytes" checkable. It does not, and this is the step the entire no-hand-authoring claim rests on. A count proves transport length and catches truncation; two different patches can share one, and git apply --check proves applicability rather than equality to the producer's output. A SHA-256 beside the count closes it, and the note now carries the full evidence chain. Co-Authored-By: Claude Opus 5 (1M context) --- ...mirror_regeneration_protocol_2026-08-24.md | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md b/docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md index f262582916b..f54a526a9c1 100644 --- a/docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md +++ b/docs/probes/stage0_mirror_regeneration_protocol_2026-08-24.md @@ -10,8 +10,8 @@ next one does not re-derive it. refuses on drift. So a `.dag` authority edit refuses until its regenerated mirror is committed beside it. The tempting move is to regenerate once and commit the result. -**One round is not enough, and the reason is the bootstrap, not a bug.** The first candidate is the -new `.dag` emitted by the **old** compiler. Committing it means the compiler built *from that mirror* +**One round is not enough to ESTABLISH the mirror change, and the reason is the bootstrap, not a bug.** +The first candidate is the new `.dag` emitted by the **old** compiler. Committing it means the compiler built *from that mirror* now carries the new behaviour — so when *it* regenerates, it re-emits every other module the change reaches. Measured on gunbc#9101, whose change altered how folds with an unused element emit: @@ -29,8 +29,21 @@ started. Run the loop from the beginning and it reports all three. `std_types.rs` `list_length` is a fold with an unused element: `|acc: i64, _: _|` became `|acc: i64, _|`. It is not unrelated cleanup riding along with the fix; it is the same change -reaching a module the first generation had not yet re-emitted. **The generation-one delta is not the -mirror change** — a PR carrying only it will pass locally and refuse in CI. +reaching a module the first generation had not yet re-emitted. + +**Do not generalize that second drift into a law — generalize the equality check instead.** A behaviour +change that no other seed module exercises can leave generation two identical to generation one; then a +second regeneration produces no new drift and is still mandatory, because it is what *proves* equality. +The invariant is: + +``` +one drift-producing regeneration MAY be enough +one regeneration without a subsequent equality check is NEVER enough +``` + +So the rule is not "expect a second drift". It is: **the generation-one delta is not yet established as +the fixed-point mirror change** — install it, rebuild, regenerate until equality. A branch carrying only +generation one may refuse in CI; gunbc#9101's did. ## The protocol @@ -39,21 +52,37 @@ mirror change** — a PR carrying only it will pass locally and refuse in CI. repeat. 3. Stop only at `RC=0` / `first_generation_equal=true`. 4. Export the **cumulative** diff against the original branch state: `git diff -- src/v1/stage0/src`. -5. Verify before applying — changed-path list, decoded byte count against the producer's own count, - and `git apply --check`. +5. Verify before applying — changed-path list, decoded byte count **and SHA-256** against the + producer's own, and `git apply --check`. 6. Commit the compiler-authored mirrors beside the `.dag` change. **The mirror's only sanctioned author is the compiler.** Every byte above comes from the candidate's own diff; hand-authoring a mirror to satisfy the gate is the laundering the gate exists to prevent, -and it is indistinguishable from regeneration in the diff alone. That is the reason for step 5's byte -count: it makes "these are the compiler's bytes" checkable rather than asserted. +and it is indistinguishable from regeneration in the diff alone. That is what step 5's verification is +for: it makes "these are the compiler's bytes" checkable rather than asserted. + +**A byte count alone does not establish that**, and the difference matters because this is the step the +whole no-hand-authoring claim rests on. A count proves transport length — it catches truncation, and it +proves the receiver did not paste an excerpt — but two different patches can share a count, and +`git apply --check` proves applicability, not equality to the producer's output. Print and check a +digest beside the count: + +``` +PATCH_BYTES= +PATCH_SHA256= +``` + +The evidence chain is then unbroken end to end: remote compiler produces the candidate -> remote +`git diff` -> remote byte count and SHA-256 -> base64 transport -> local count and SHA-256 agree -> +`git apply --check` -> `--required-regen` reaches its fixed point. ## Transport, when the loop cannot run where the commit happens A remote runner may have no push credentials, and a local build of `v1-compiler` may not be affordable (shared memory slice, swap disabled). Mirror files are far too large for a log. What works is moving the **diff** rather than the files: the loop runs remotely, prints the cumulative diff base64-encoded -with a byte-count header, and the patch is decoded, verified and applied where the commit happens. +with a byte-count and SHA-256 header, and the patch is decoded, verified and applied where the commit +happens. gunbc#9101's two generations were 3748 and 555 bytes. ## Branch tree versus merge ref