Skip to content

regen receipt: the fixed-point pass may reference prior evidence, not impersonate it - #8650

Merged
briansrls merged 4 commits into
mainfrom
session/smart-ram-730-regen-receipt
Aug 20, 2026
Merged

briansrls merged 4 commits into
mainfrom
session/smart-ram-730-regen-receipt

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

The fixed-point regen pass may reference prior evidence, not impersonate it

Closes an owner-less defect on the required CI path, routed by deep-ant-102.

The specimen is in production, not a fixture

Run 32341236470 on main at bd239370923 — the run this fleet cited as proof the mirror convergence was good:

required-regen-fixed-point: fixed_point_equal=true first_generation_equal=true

first_generation_equal is printed by the pass that does not measure it.

The two regen passes are separate process invocations sharing one receipt file under target/. A single flat eight-field record forced the second pass to populate fields it had never measured, and the only source available was the receipt the first pass left on disk — so four of six were copied verbatim: committed_generated_digest, first_generation_equal, changed_paths, candidate_artifact. The product is stamped with the second pass's commit_sha while carrying the first pass's answers: internally consistent, schema-valid, and silent about which tree four of its fields describe.

It is true in that run — pass 1 ran minutes earlier at the same commit. That is why it survived. A field that is usually right is the hardest kind to find, because correctness under observation is exactly what stops anyone asking whether it was measured.

Where it is reachable

path status why
CI latent actions/checkout's default clean removes the ignored target/ each run — measured as two consecutive main runs each compiling 105 crates starting at proc-macro2, where a warm tree compiles zero
local reachable today nothing requires pass 1 to have run in this process, at this commit, or at all

The CI arm is unreachable by a checkout default nobody declared — one cache-reuse change from live on a required path. The local arm is the ordinary workflow: iterate on the determinism half alone over a warm target/, and you get today's commit_sha carrying yesterday's changed_paths.

The repair makes it unwritable, not detectable

RegenReceipt becomes two variants. FirstGeneration carries only what pass 1 measures. FixedPoint carries the pass-2 digest, its equality, and an explicit PriorReceiptRef — and has no first_generation_equal field, so there is nothing to copy and no check to pass. DESIGN §4b structural impossibility, one rung above the validation that would otherwise sit here.

Computing all six in pass 2 was the alternative and is worse: it would make pass 2 re-derive first_generation_equal against the committed tree, which is pass 1's question — fusing two authorities into one row (§3).

A reference is only honest if it names its subject, so PriorReceiptRef carries the commit_sha its evidence was measured at, and the host refuses when that differs from HEAD. Without that arm, a PriorReceiptRef is the same defect with better vocabulary.

Three things the split exposed, not in the original report

  • Pass 1 was writing fixed_point_equal: false — not a measurement at all. The first pass never asks that question, so a literal false asserted a negative answer where the honest content was not asked. The same conflation as the impersonation, in the opposite direction, sitting right beside it.
  • The accessors return Option, not bool: "did not measure" and "measured false" are different states and a bool cannot hold both.
  • RegenReceiptStored deserializes only the first-generation shape, so a second pass building on another second pass's receipt refuses at parse time rather than by a check someone must remember to write.

The RED is proven by execution

Per-row through claim_batch on the real consumer, with a mutation control:

arm positive control RED no-reference exit
real predicate PASS PASS PASS 0
predicate → constant true PASS FAIL PASS 1

Only the RED broke and both controls stayed green, so the witness discriminates on the predicate rather than on anything ambient.

What is not covered

The host refusal arm has no executing evidence. Reaching it needs a receipt file planted at a chosen commit_sha before the binary runs, and no witness form here writes a file before running a process. Stated in the witness header rather than left to be discovered. Next-rung trigger: a witness form that can stage a fixture file for a wet run. Until then that arm rests on review, which is strictly weaker than the predicate does.

Known residue, named rather than swept: the population-refusal path still writes "refused:population" sentinel digests and first_generation_equal: false, both meaning "not asked". The honest repair is a refusal variant carrying no digest fields, which is wider than the impersonation this closes.

Note on CI

Main is currently red on required-regen for an unrelated reason — #8614 left v1_compiler_emit_rust.rs drifted from its authority, owned and being fixed by vivid-pike-765. This branch is cut from 4cec10f66a3 and will inherit that red until main is green. It is not this change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FwPMTY6Myy3scaMNn33cg5

gunbc-ci-auto-heal and others added 2 commits August 20, 2026 07:57
… impersonate it

A RECEIPT MAY REFERENCE PRIOR EVIDENCE BUT MAY NOT IMPERSONATE PRIOR EVIDENCE AS
SOMETHING IT MEASURED ITSELF (operator ruling, 2026-08-20).

THE SPECIMEN IS IN PRODUCTION, not a fixture. Run 32341236470 on main at
bd23937 -- the run this fleet cited as proof the mirror convergence is good:

  required-regen-fixed-point: fixed_point_equal=true first_generation_equal=true

first_generation_equal is printed by the pass that does not measure it. The two
regen passes are separate process invocations sharing one receipt file under
target/, and a single flat eight-field record forced the second pass to populate
fields it had never measured. The only source was the receipt the first pass left
on disk, so four of six were copied verbatim: committed_generated_digest,
first_generation_equal, changed_paths, candidate_artifact. The product is stamped
with the SECOND pass commit_sha while carrying the FIRST pass answers -- internally
consistent, schema-valid, and silent about which tree four of its fields describe.

It is TRUE in that run, because pass 1 had run minutes earlier at the same commit.
That is why it survived: a field that is usually right is the hardest kind to find,
since correctness under observation is exactly what stops anyone asking whether it
was measured.

WHERE IT IS REACHABLE. In CI the arm is currently unreachable -- actions/checkout's
default clean removes the ignored target/ each run, measured as two consecutive main
runs each compiling 105 crates starting at proc-macro2, where a warm tree compiles
zero. But that is a property of a checkout default nobody declared, one cache-reuse
change from live on a required path. It is reachable TODAY on the ordinary local
path: nothing requires pass 1 to have run in this process, at this commit, or at
all, so a developer iterating on the determinism half alone over a warm target/ gets
today's commit_sha carrying yesterday's changed_paths.

THE SHAPE MAKES IT UNWRITABLE RATHER THAN DETECTABLE. RegenReceipt is now two
variants. FirstGeneration carries only what pass 1 measures. FixedPoint carries the
pass-2 digest, its equality, and an explicit PriorReceiptRef -- and has NO
first_generation_equal field, so there is nothing to copy and no check to pass
(DESIGN 4b structural impossibility, one rung above the validation that would
otherwise sit here). Computing all six in pass 2 was the alternative and is worse:
it would make pass 2 re-derive first_generation_equal against the committed tree,
which is pass 1's question, fusing two authorities into one row (DESIGN 3).

A REFERENCE IS ONLY HONEST IF IT NAMES ITS SUBJECT, so PriorReceiptRef carries the
commit_sha its evidence was measured at, and the host REFUSES when that differs from
HEAD. Without that arm a PriorReceiptRef is the same defect with better vocabulary.

THREE THINGS THE SPLIT EXPOSED that were not in the original report:

  - Pass 1 was writing `fixed_point_equal: false`. Not a measurement at all -- the
    first pass never asks that question, so a literal false asserted a NEGATIVE
    ANSWER where the honest content was NOT ASKED. Same conflation as the
    impersonation, in the opposite direction, sitting right beside it.
  - The accessors return Option, not bool: "did not measure" and "measured false"
    are different states and a bool cannot hold both.
  - RegenReceiptStored deserializes ONLY the first-generation shape, so a second
    pass building on another second pass's receipt refuses AT PARSE TIME rather
    than by a check someone must remember to write.

THE RED IS PROVEN BY EXECUTION, per-row through claim_batch on the real consumer:

  real predicate            PASS control · PASS RED · PASS no-reference   exit 0
  predicate -> constant true PASS control · FAIL RED · PASS no-reference   exit 1

Only the RED broke and both controls stayed green, so the witness discriminates on
the predicate rather than on anything ambient.

WHAT THE WITNESS DOES NOT COVER, stated in its header rather than left to be found:
the HOST refusal arm. Reaching it needs a receipt file planted at a chosen commit_sha
before the binary runs, and no witness form here writes a file before running a
process. Next-rung trigger: a witness form that can stage a fixture file for a wet
run. Until then that arm rests on review, which is strictly weaker.

KNOWN RESIDUE, named rather than swept: the population-refusal path still writes
"refused:population" sentinel digests and first_generation_equal: false, both
meaning "not asked". The honest repair is a refusal variant carrying no digest
fields, which is wider than the impersonation this closes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwPMTY6Myy3scaMNn33cg5
…er that hid the false claim

Review on #8650 caught a comment asserting a property the code did not deliver —
the exact class this PR exists to close, in the repair itself.

THE CLAIM WAS FALSE. The schema-bump comment said a stale `.v1` receipt "fails to
deserialize into the new shape". serde ignores unknown fields by default, and a v1
record carries every field the reader required plus the removed `fixed_point_equal`,
so it parsed cleanly and the bump changed nothing.

AND IT WAS WORSE THAN THE REVIEW STATED. `schema` was written three times and read
ZERO times — no comparison anywhere in the module. The version string was
decoration. Writing a version nobody compares is the same defect as the
impersonation this module closes: an artifact asserting a property that nothing
establishes.

ONE ADJACENT CLAIM WAS TRUE and is kept rather than swept out with the false one:
deserialization does refuse a `fixed_point` receipt, because that shape lacks four
fields the reader requires. Two neighbouring claims, one sound, one not.

CONSTRUCTION RATHER THAN A SOFTENED COMMENT, which was the reviewer's first option
and the better one:

  - `deny_unknown_fields` on the carrier, so a stale v1 record refuses on the
    orphan field it carries;
  - an explicit schema equality check in `read_receipt`, so the version is COMPARED
    rather than merely written;
  - `RegenReceiptStored` DELETED. It was a second representation of RegenReceipt
    (DESIGN 3 nickname) and it was precisely where the false claim hid: a
    hand-mirrored field list accepts any JSON containing those fields, so it read a
    v1 record as happily as a v2 one. `read_receipt` now deserializes the REAL
    carrier and destructures it, so reader and writer cannot drift — there is one
    shape, and the duplicate that made the drift possible is gone rather than
    corrected.

The `fixed_point` rejection is now an explicit match arm even though it was already
true by construction, because a missing-field parse error reports the SYMPTOM — a
field name — rather than the cause.

cargo check clean, with a negative control: injecting a bogus field yields 2 errors
and exit 101, so the green discriminates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwPMTY6Myy3scaMNn33cg5
@gunbai-bot

gunbai-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 1407bfed863, and the finding was right — it is the exact class this PR exists to close, occurring in the repair itself. Thank you for catching it.

It was worse than the review states, in a way that changes the fix. I went to soften the comment and checked the code first: schema was written three times and read zero times. No comparison anywhere in the module. So the v1→v2 bump accomplished nothing at all — the version string was decoration. Writing a version nobody compares is the same defect as the impersonation this PR closes: an artifact asserting a property that nothing establishes.

One adjacent claim was true, and I kept it rather than sweeping it out alongside the false one: deserialization does refuse a fixed_point receipt, because that shape lacks four fields the reader requires. Two neighbouring claims, one sound, one not.

I took your first option — construction — rather than the softened comment:

  • #[serde(deny_unknown_fields)] on the carrier, so a stale v1 record refuses on the orphan fixed_point_equal it carries.
  • An explicit schema equality check in read_receipt, so the version is compared rather than merely written.
  • RegenReceiptStored deleted. This is the part your finding led me to that I would not have seen otherwise: that struct was a second representation of RegenReceipt — a §3 nickname — and it was precisely where the false claim hid. A hand-mirrored field list accepts any JSON containing those fields, so it read a v1 record as happily as a v2 one. read_receipt now deserializes the real carrier and destructures it, so reader and writer cannot drift. The duplicate that made the drift possible is gone rather than corrected.

The fixed_point rejection is now an explicit match arm even though it was already true by construction, because a missing-field parse error reports the symptom — a field name — instead of the cause.

cargo check clean, with a negative control: injecting a bogus field yields 2 errors and exit 101, so the green discriminates rather than being a cache hit.

On the comment length: taken. They are rationale rather than restated structure, but I will watch it.

CI note: main is currently red on required-regen for an unrelated reason — #8614 left v1_compiler_emit_rust.rs drifted from its authority, owned by vivid-pike-765. This branch inherits that until main is green; it is not this change.

— sent from smart-ram-730

@gunbai-bot

gunbai-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

The red here is main's, not this change

witnesses fails at 1407bfe on the required regen step (run 32347259194):

required-regen: first_generation_equal=false planned=129 executed=129
required-regen: FAIL generated surface drift: v1_compiler_emit_rust.rs

One file, and this PR does not touch it. v1_compiler_emit_rust.rs drifted from its authority when #8614 merged at 07:13 with a mirror synced by function-level splice rather than full regen. Main has failed identically on every commit since — green at 5fbbbeb707f, red at 5a71831dcf6 and 4cec10f66a3. This branch is cut from 4cec10f66a3 and inherits it. Owned by vivid-pike-765, who is landing a tool-generated regen candidate.

The check I ran before saying so, because I predicted this failure and a prediction is a poor reason to believe a result: this PR edits src/v1/stage0/src/required_regen_host.rs, which is inside the 129-file compared population. If that file were a generated mirror, hand-editing it would be precisely the violation the drift gate exists to refuse. It is not — it is entry 25 of the 36 in HAND_MAINTAINED_STAGE0_FILES (bootstrap_stage0_crate_layout_generated.rs), so the edit is legitimate and produces no drift of its own. That is consistent with the run: exactly one drifted file, and it is not one of mine.

Nothing about this PR's own content is implicated. I am not regenerating another session's owned projection into this branch to go green — a hand-edited mirror would pass while being the violation, and the real fix lands on main shortly and would conflict.

Holding until main is green.

— sent from smart-ram-730

@gunbai-bot

gunbai-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Correction: I described #8614's cause wrongly above

My earlier comment on this PR said #8614 "synced its stage0 mirror by function-level splice plus rustfmt rather than a full regen." That is false, and swift-moth-294 — who owns those files — refuted it. I verified their refutation against the commit rather than taking it:

$ git show --stat --format='' 5a71831dcf6
 docs/probes/...rustc_wrapper_cache_impurity_2026-08-19.md | 116 +++++++++
 src/v1/05_emit_rust.dag                                   |  12 ++-
 2 files changed, 126 insertions(+), 2 deletions(-)

Two files. Zero stage0 mirrors. There was no splice, partial or otherwise.

What is actually true: #8614 changed the authority (05_emit_rust.dag) and did not regenerate the mirror in the same commit. That is still the cause of the drift, and everything this comment thread says about the red being inherited rather than mine is unaffected.

Why the distinction is not pedantic — it inverts a conclusion. A spliced mirror would be a corrupted hybrid, making any measurement taken from main invalid. A mirror that merely lacks #8614 is clean: it carries #8528 and #8570 (both of which regenerated in the same commit) and is missing exactly one change. That makes main today a valid pre-#8614 measurement arm, available only while it is red. I had advised waiting for green, which would have discarded that arm permanently.

Where the false claim came from, because it is worth knowing and it caught three lanes independently: the squashed commit message contains both statements. Line 30 says the change was "synced into its committed mirror ... by function-level splice + rustfmt, not full regen." Line 112 says "That splice is NOT included in this commit," and line 116 that the mirror "is reverted to its pre-splice, currently-committed" state. Squash-merge concatenates a branch's per-commit messages, so a mid-branch claim and its own later retraction both survive, in that order. Anyone reading the first occurrence gets the version that was true mid-branch and false at merge.

I was the third lane to carry it. Recording it here rather than quietly editing, since the wrong version was already read.

— sent from smart-ram-730

gunbai-bot Bot pushed a commit that referenced this pull request Aug 20, 2026
…e the measurement

Review finding (smart-ram-730 on #8639): `measure_generated_drift` re-typed the same
five-call sequence `run_required_regen` already performs --

    compile_stage0
    committed_generated_basenames
    generated_basenames_from_emit
    validate_compared_populations
    compare_generated_surfaces

-- so one fact, WHICH MIRRORS DRIFTED, had two producers and nothing kept them in step.

The receipt is on the record and is why this is worth fixing while the copies still agree:
#8618 repaired a defect INSIDE `compare_generated_surfaces` -- the committed side was being
normalized, making the comparison `normalize(normalize(x))` against `normalize(x)`, a
false-positive drift with no reachable green. A repair landing in one of two copies leaves the
other answering the old way, and "the copies agree today" is exactly what makes a duplication
easy to leave in place until it costs something.

What actually differs between the two callers is the FAILURE POLICY, not the measurement:
`run_required_regen` routes a refusal to `regen_refusal_outcome`, which writes a receipt and
returns `Ok` carrying failures, while the drift gate wants `Err`. So `measure_generated_surface`
performs the sequence once and returns `Measured { .. }` or `Refused { reason }`, and each caller
applies its own policy at the call site -- one `match`, not a second copy of the five calls above
it. `emitted` and `committed` come back in the value because the regen path needs them for the
candidate tree and its digests, and recomputing them would run the whole emit a second time.

`emitted_basenames` is returned too, rather than derived again by the caller for its `executed=`
count. Leaving that one out would have fixed the duplication at the top and reintroduced a smaller
one a level down.

NOT DONE HERE, deliberately: `run_required_regen_fixed_point` shares four of these five calls and
is a partial third copy. It is left alone for two reasons. It skips `compare_generated_surfaces`
because it only needs a digest, so routing it through this function would add a rustfmt-per-file
comparison it does not need; and #8650 is restructuring that exact function, so editing it here
trades a real duplication for a merge resolution in a generated-adjacent file. Raised with that
PR's author instead of taken silently.
gunbc-ci-auto-heal and others added 2 commits August 20, 2026 13:07
…on arm

review 54058 (non-blocking): the sentence "None would mean the first pass built
the wrong variant, so it prints `unmeasured`" appeared verbatim in two adjacent
comment blocks. Merged into one block preserving all four distinct facts: both
values are measured by this pass; the read goes through accessors rather than a
variant match because `required_regen_host` is private to `cli_run` (usable, not
nameable); the accessors return Option because the sibling variant does not
measure these fields; and a None therefore prints `unmeasured` rather than
defaulting to a plausible-looking value.

Comment-only. No semantic change, so no mirror moves (DESIGN 4c: semantic passes
receive the annotation-erased projection).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwPMTY6Myy3scaMNn33cg5
@gunbai-bot

gunbai-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 0afa41d3eb6. The duplicate was real — the sentence appeared verbatim in two adjacent comment blocks. Merged into one, preserving all four distinct facts rather than deleting the second block wholesale: both values are measured by this pass; the read goes through accessors rather than a variant match because required_regen_host is private to cli_run (usable, not nameable); the accessors return Option because the sibling variant does not measure these fields; and a None therefore prints unmeasured rather than defaulting to a plausible-looking value. Comment-only, so no mirror moves.

One note on the citation, offered because it is a live instance of a standing rule rather than a complaint about the review.

The finding cited src/v1/stage0/src/bin/claim_executor.rs:236. The actual location is 10514 and 10518. That is not merge drift — I did merge origin/main into this branch between the review and the fix, but that shifted line numbers by hundreds, not by ten thousand. Line 236 in every version of this file is inside the floor-batch-clamp arity refusal, which has nothing to do with regen receipts.

So the finding's content was exactly right and independently verifiable, and its position was never right. I found it in one grep on a distinctive phrase, so the cost here was seconds — but it is precisely the decay DESIGN §3 describes when it rules that citations name the symbol, not the position: "any edit above the cited line silently invalidates it, so the citation rots without anyone touching it or the thing it names." A symbol (claim_executor.rs, the Ok(outcome) arm of the required-regen-fixed-point branch) would have survived both the merge and the mis-derivation.

Worth flagging because a positional citation that is wrong from the start is harder to detect than one that rots: a stale line still points somewhere plausible, and a reader who trusts it reads the wrong code and concludes the finding is bogus. Here the finding was good.

— sent from smart-ram-730

@briansrls
briansrls merged commit 2259cf8 into main Aug 20, 2026
1 of 2 checks passed
@briansrls
briansrls deleted the session/smart-ram-730-regen-receipt branch August 20, 2026 16:16
gunbai-bot Bot pushed a commit that referenced this pull request Aug 20, 2026
…he extracted reporters

THE CONFLICT IS THE SAME BLOCK AS LAST TIME, and taking a side would have been a
silent correctness loss in the more dangerous direction.

#8646 substantially extended the floor's reporting: `offered / routed /
declined_long / declined_live` on the summary line, and TWO NEW BLOCKING CAUSES,
`route_gap` and `stale_route_gap`, appearing in both the per-row printing and the
cleanliness conjunction. Taking my side of the conflict would have kept the
extraction and dropped all of it — including the two causes — so
`--required-floor` AND `--required-ci` would have reported green on a route gap
that main refuses. A merge that greens a run main reds is worse than a conflict.

RESOLVED BY RE-DERIVING RATHER THAN HAND-MERGING. I took main's inline block
verbatim, split it at the conjunction, and rebuilt `report_required_floor_outcome`
and `required_floor_outcome_is_clean` from it mechanically — the same extraction
this PR performs, re-run against main's newer content. That is why the seven-term
conjunction is exactly main's seven terms rather than my five plus two I
remembered to add.

#8650 reshaped `RegenReceipt` into an enum whose fields became Option-returning
accessors, which broke my composed run's phase code. Adapted: `first_generation_
equal` and `fixed_point_equal` now print `unmeasured` rather than a plausible
default when the pass built the other variant, matching what main's own arms do.

ONE NEW ACCESSOR, and it is deliberately TOTAL: `RegenReceipt::
candidate_generated_digest()`. Both variants measure a candidate digest, so there
is no arm without one and no `Option` for a reader to misinterpret as
"unmeasured" — unlike its siblings, which are Option because the other variant
genuinely does not measure that fact. Its consumer is the in-memory pass-1
handoff, the thing this PR exists to make possible: `run_required_regen_fixed_
point` has always taken `pass1_digest: Option<String>`, and before the phases
shared a process there was no way to supply it.

The generated workflow conflicted with NO markers — that is
`generated_artifact_merge_driver` refusing rather than picking a side, exactly as
designed. Regenerated from the authority instead of hand-resolved.

VERIFIED PRESENT AFTER THE MERGE, both directions: main's route_gap (7),
stale_route_gap (4), offered=, and #8642's CiWitnessVerdict and memo receipt;
mine's required_ci_mode, run_v1_src_dag_parse, and both extracted reporters, with
the two new causes confirmed inside the cleanliness function. Three consolidation
witnesses re-run green. `cargo check --all-targets` and `cargo fmt --all --check`
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Aug 20, 2026
…— CI today runs NO regen, NO fixed-point, NO behavioral receipt (#8657)

* Mirror-drift gate: ask which side moved, not whether the mirror equals its authority

`--required-regen` asks whether the committed stage0 mirrors EQUAL what their
`.dag` authority emits. On main that question has no closing move: the regen cut
deleted the writer, so the only green a contributor can reach is to hand-edit a
generated mirror -- the exact act a drift gate exists to refuse. A required gate
whose only path to green is the violation it guards against does not enforce the
rule, it manufactures the workaround under a green check.

`--required-mirror-drift` asks a question a contributor can close. Per drifted
path, against the git merge base:

  drifted AND this change touched the mirror  -> sideways mirror move (refuse)
  drifted AND this change did not touch it    -> must carry an authored
                                                 disposition, else refuse
  drifted on neither count                    -> silent

Plus the join run backwards: a disposition row whose path does NOT drift is
stale and refuses, because otherwise the carrier is a one-way ledger that
accretes rows asserting debt that no longer exists.

Membership is DERIVED from the required-regen comparator every run; the only
authored input is the per-row disposition in `gunbc.stage0_mirror_debt`. So a
row cannot add to or remove from the measured population -- forgetting a
judgement refuses loudly, and no edit to the carrier can silence a real drift.
The baseline is resolved from git, printed, and asserted: there is no fallback
to HEAD, under which the touched set would be empty and every hand-edited mirror
would reclassify as pre-existing debt.

The stale arm alone carries a derived precondition. It is the only arm whose
false verdict DESTROYS something -- the other two fail toward refusing, this one
fails toward deleting a correct row, and it did exactly that on its first live
run against `v1_compiler_parse.rs` on a subject fifteen commits behind the
commit that created that drift. When the subject does not contain main's tip,
stale rows are counted and named under `stale_rows_unreadable` and do not
refuse; nothing is silenced, so the deficit stays rankable.

This gate writes no receipt. Every fact it reports is computed in the invocation
that reports it, from the tree that invocation is looking at.

* One producer for the drift fact: run_required_regen and the gate share the measurement

Review finding (smart-ram-730 on #8639): `measure_generated_drift` re-typed the same
five-call sequence `run_required_regen` already performs --

    compile_stage0
    committed_generated_basenames
    generated_basenames_from_emit
    validate_compared_populations
    compare_generated_surfaces

-- so one fact, WHICH MIRRORS DRIFTED, had two producers and nothing kept them in step.

The receipt is on the record and is why this is worth fixing while the copies still agree:
#8618 repaired a defect INSIDE `compare_generated_surfaces` -- the committed side was being
normalized, making the comparison `normalize(normalize(x))` against `normalize(x)`, a
false-positive drift with no reachable green. A repair landing in one of two copies leaves the
other answering the old way, and "the copies agree today" is exactly what makes a duplication
easy to leave in place until it costs something.

What actually differs between the two callers is the FAILURE POLICY, not the measurement:
`run_required_regen` routes a refusal to `regen_refusal_outcome`, which writes a receipt and
returns `Ok` carrying failures, while the drift gate wants `Err`. So `measure_generated_surface`
performs the sequence once and returns `Measured { .. }` or `Refused { reason }`, and each caller
applies its own policy at the call site -- one `match`, not a second copy of the five calls above
it. `emitted` and `committed` come back in the value because the regen path needs them for the
candidate tree and its digests, and recomputing them would run the whole emit a second time.

`emitted_basenames` is returned too, rather than derived again by the caller for its `executed=`
count. Leaving that one out would have fixed the duplication at the top and reintroduced a smaller
one a level down.

NOT DONE HERE, deliberately: `run_required_regen_fixed_point` shares four of these five calls and
is a partial third copy. It is left alone for two reasons. It skips `compare_generated_surfaces`
because it only needs a digest, so routing it through this function would add a rustfmt-per-file
comparison it does not need; and #8650 is restructuring that exact function, so editing it here
trades a real duplication for a merge resolution in a generated-adjacent file. Raised with that
PR's author instead of taken silently.

* Behavioral receipt: demand-directed selection and a refusal-bounded corpus fragment

CI proves the committed mirrors equal what the authority emits, and that the
emit repeats. It never COMPILES the emitted candidate, let alone runs it -- the
regen host spawns exactly rustfmt, rustfmt and git. So the whole promotion story
rests on bytes, and DESIGN §7 says a byte-identical fixed point is explicitly
NOT the goal.

This lands the selection and corpus-derivation half of an executing behavioral
receipt.

SELECTION is demand-directed and derived. The subject is the modules whose .dag
authority moved in this diff; the authority-to-mirror mapping is read off each
mirror's own `// Source module:` header, never an authored roster. If the merge
base will not resolve it REFUSES rather than widening to the whole population:
two compiler builds across 129 modules is a budget denominated in the repository
rather than in the change.

THE CORPUS is derived from the authority's declared surface and REFUSES where it
cannot be. Closed nullary enums, Bool and records over them are finite-closed;
Int windows and bounded-length Lists are not. A sampled corpus for the remainder
would let the mode report a receipt for every module while a behaviour change
hides in an unsampled cell -- a receipt that usually cannot fail, which is worse
than a refusal, because a refusal is counted and ranks while a usually-passing
receipt reports as done.

THE DOMAIN IS REPORTED AS A DERIVED FACT, not a label: cardinality always, and
for bounded cases the bound itself. `Exhaustive` is reserved for the finite-
closed case. This is not pedantry -- an earlier revision of this work described a
corpus as exhaustive when four of its seven function groups were bounded
approximations of infinite domains, and printing the bound is what exposed that
`content_hash_is_lower_hex_code_point` was being enumerated over [-2,2], a window
containing no hex digit at all.

TYPES RESOLVE THROUGH THE IMPORT GRAPH, because a type's shape decides
derivability and its address does not. Measured control that this resolved rather
than widened: std.content_hash refuses 26 of 27 functions before and after, while
std.pareto moves by exactly one -- axis_comparison, whose only blocker was that
`Ordering` is declared in std.algebra.

Not yet built: the two-build differential itself. What is here is the subject
selection and the corpus plan, both green by execution with discriminating arms
(empty selection stays empty; a String-heavy authority refuses; an authority with
no emitted mirror is excluded by name, not silently).

* wip: Int equivalence-class partition replaces the bounded window

* wip: route all three readers through the grammar-owned parser

* wip: drop dead literal import

* wip: restore PayloadCoproduct variant and use ErrorNode.diagnostic

* Reach the parser through its own constructors, and refuse payload coproducts by name

Three compile fixes, two of which are the same mistake at different scales.

PayloadCoproduct was declared and constructed but had no arm in derive_parameter_domain.
Without it a declared, CLOSED, payload-carrying coproduct refuses with 'not a closed type
declared by this authority' -- false for that population, and the exact misdirection this
branch exists to remove, reintroduced in a narrower form.

The source-index map is an im::HashMap, not a std::HashMap. Building it with
v1_rt::rc_empty_map / rc_map_insert, as the emitted caller does, rather than naming the
concrete type here: reaching for the representation is this file asserting something the
parser owns, which is the same defect as re-implementing its reader, one level down.

* Read declarations, parameters, fields and generic arguments off the parse tree

The grammar-backed reader landed with four wrong assumptions about node shape. Each was
found by measurement, not reasoning, and the last three shared ONE root cause.

WRONG: a function is a `Connective::Arrow` child.
RIGHT: Arrow marks a `Callable` TYPE EXPRESSION. A declaration is a function when it carries a
body AND a resolved return type. Selecting on Arrow matched nothing and every module reported
parsed=0.

WRONG: a parameter's, field's, or generic argument's type hangs off `type_annotation`.
RIGHT: it is a CHILD. This single mistake produced three unrelated-looking symptoms: every
parameter typed as the empty string, so all 514 corpus refusals named the same empty type and the
blocker histogram collapsed to one meaningless row; `List<AxisComparison>` rendered as bare
`List`, which then failed the `List<` test and fell through to "not a closed type declared by this
authority", which is why the first histogram had NO list row despite lists being the second
largest blocker; and every record dropped out of the type environment, so `DominanceTally` -- a
Conj record sitting in the same module -- was also refused as "not a closed type". Three wrong
refusal messages, each sending a reader to a repair that was not needed. That is the misdirection
this fragment exists to remove, produced by the fragment itself.

WRONG: a body distinguishes a function from a `data` row.
RIGHT: both carry bodies. `data no_names: List<NonEmptyStr> = []` reports ta=Some inf=none; every
function reports ta=None inf=Resolved. A constant's declared type lives in `type_annotation`, a
function's return type in `inferred`.

MEASURED RESULT, all three criteria fixed before the run:

  std.pareto        fn_lines=33 parsed=33   axis_comparison exhaustive(|domain|=6)
  std.content_hash  fn_lines=27 parsed=27   refused 26 of 27 -- control held exactly
  corpus            declared=585 parsed=585 -- zero disagreements across 44 modules

The declared-versus-parsed counter is kept, not retired. It has now caught three defects: the line
reader's 14 missing signatures, the Arrow mistake, and the data over-count -- in both directions.
Two readers of one fact are duplication when both are trusted and a cheap falsifier when one is
under test.

The partition arm also stopped being nearly empty, and the new hit is the one that indicts the
deleted bounded window most directly:

  content_hash_is_lower_hex_code_point  literals {48,57,97,102}  reps {47,48,49,56,57,58,96,97,98,101,102,103}

Those are '0','9','a','f' and their boundaries. The window this replaced enumerated that same
function over [-2,2] -- five values containing no hex digit at all -- and reported it beside
genuine coverage.

* Two-build behavioral differential: compile the candidate, run the derived corpus, compare

CI proves the committed mirrors equal what the authority emits and that the emit repeats. It
never COMPILES the candidate, let alone runs it. This does both.

Seed transcript from the tree as committed; the emitted candidate is then written over its
mirror, the crate rebuilt, and the SAME driver run again. One function produces both transcripts,
so they cannot differ because of how they were produced. The mirror is restored BEFORE the result
is interpreted -- no early return can leave a candidate installed, which would silently corrupt
every later measurement including the drift gate's.

Refused is a third verdict, not a soft pass. A missing candidate, a driver that will not compile,
an empty corpus: each is ignorance, and an empty comparison is indistinguishable from a passing
one unless it has its own arm.

The corpus enumeration now yields VALUES, not a cardinality. The count is values.len(). A count
computed beside an enumeration is a second producer of one fact, and it is exactly how the
earlier revision could report a corpus it had never executed. The superseded
derive_parameter_domain is DELETED rather than left callable -- keeping the count-only route
beside the executing one preserves the reporting path this change exists to remove.

Candidate bytes come from emitted_generated_sources, which routes through the same
measure_generated_surface the drift gate and regen use, so the bytes a receipt compiles are the
bytes the gate compared.

4096 tuples per function is a refusal, not a sample: a receipt that runs a subset while reporting
the whole is fabricated output, and an unbounded Cartesian product is the cheapest way to get one.

* The receipt executes: one spelling of the module under test, and both arms discriminate

The driver aliased the module for CALLS while the enumerated constructor VALUES were rendered
against the bare module name -- one module referred to two ways, and only one spelling resolved.
Fully qualified from a single string derived from the artifact's own basename; the alias is gone,
so calls and constructors cannot drift apart.

MEASURED, both arms, digest-guarded, emitted bytes moved in each:

  ARM 1  behaviour-preserving (compare_int rewritten to test > first)
         std.pareto EQUIVALENT over 22 derived calls

  ARM 2  behaviour-changing (LowerIsBetter/Greater => Same instead of Worse)
         std.pareto DIVERGENT over 22 derived calls
         seed:      axis_comparison(AxisGoal::LowerIsBetter, Ordering::Greater) = Worse
         candidate: axis_comparison(AxisGoal::LowerIsBetter, Ordering::Greater) = Same

The corpus is DERIVED from the authority's declared surface, not authored. The divergence names
the exact call rather than a count, and it is the call predicted in advance from the seed
transcript.

This is what CI does not do. required-regen spawns rustfmt, rustfmt and git; it never compiles
the candidate, let alone runs it. So promotion evidence today is byte-equality, and a byte
comparison cannot tell a rename from a semantic change -- which is why DESIGN section 7 says a
byte-identical fixed point is explicitly NOT the goal and names behavioural equivalence on a
discriminating corpus instead.

WHAT THIS DOES NOT CLAIM: equivalence over the TYPE. It is equivalence over the derived corpus --
5 of std.pareto's 33 functions, 22 calls -- where each domain is exhaustive in the sense its arm
states: a closed finite domain, or a partition within which the function provably cannot
distinguish values. The other 28 refuse, each naming the type that defeated it, and they are
counted rather than sampled.

Three earlier runs REFUSED rather than reporting equivalence: a candidate looked up in the wrong
key space, then a driver that would not compile, twice. A differential that answered EQUIVALENT
in any of those states would have passed both arms while comparing nothing.

* Enroll the receipt's own two arms against a controlled fixture (WIP probe)

* Enroll the selftest step in the witness workflow authority (yml regen pending)

* Regenerate witnesses.yml from its authority: the selftest step, derived not hand-added

* Census: which types defeat derivation, ranked by the work that would unlock

The differential answers ONE candidate. This answers the prior question -- across every module
the seed actually carries, how much of each surface can be covered at all, and what stands in
the way of the rest.

Ranked by the TYPE responsible rather than by refusal count, because the type is the unit of
work: grounding one type unlocks every function whose only obstacle was that type, and counting
refusals would rank the same fix once per site.

Runs no build and installs no candidate. It exits SUCCESS on any population deliberately -- a
census that refused would be a gate, and nothing here establishes what the right coverage is.
The population is derived from the mirrors' own `// Source module:` headers, and a module whose
authority source cannot be read is REPORTED rather than skipped: a census that silently drops
what it cannot read reports a smaller corpus as a cleaner one.

* Census: name the roots, not a missing authority — 55 of 127 was a scoping fact wearing a refusal's clothes

* A refusal's identity is the work it names, not the sentence it prints

The census ranked on the formatted refusal message, and the top row came back

     1500  x (used outside a literal comparison, so its value reaches the result ...)

which is every parameter in the corpus that happens to be named `x`, collapsed into one row that
names no type and no work. A parameter name is not a unit of work. The string was doing double
duty as an identity and as prose, and it was wrong at the identity job -- the ranking that is
supposed to decide what to ground next was ranking spellings.

RefusalCause is now typed, and `describe()` is DERIVED from it, so the sentence and the ranking
key cannot disagree. Two consequences fall out of the carrier rather than being coded twice:

  - the Int class keys WITHOUT the parameter name (the name stays in the message, for locating
    it), so one class is one row instead of as many rows as there are spellings;
  - a refusal reached through a record field ranks as its INNER cause, because grounding the
    inner type unlocks every record that embeds it -- counting the wrapper separately would split
    one piece of work across as many rows as there are embedders.

* Two review points, and the out-of-scope arm reports coverage rather than a count

Review nit, real: a rustfmt-mangled continuation left `the generated surface is<18 spaces>no longer
flat` in the basename-collision refusal. Rewrapped.

The census's out-of-scope arm now leads with COVERAGE planned/total and names the roots it was
given. A count read alone looks like a rounding error; the same shape at a wrong root set is a
hole centred on whatever nobody scanned, and this exact arm has already BEEN that hole once --
it swallowed 55 of 127 modules as "no authority" when the truth was that src/v1 is not a scanned
root. If the fraction is large, the root set is the finding, not the corpus.

* The fixture was inside the compile closure: move it out of src/v1 to fixtures/

MEASURED, and it is the answer to why the corpus probe's arms both exited 1 with no verdict:

  behavioral-receipt: refused: surface population mismatch —
    emitted_not_committed=["receipt_fixture.rs"] committed_not_emitted=[]

Not a merge-base problem at all. `regen_source_roots()` seeds EVERY .dag under src/v1 into the
stage0 compile closure, so putting the fixture authority there made it a compiled, emitted module
with no committed mirror -- and the generated surface stopped matching its committed population.

The refusal is CORRECT and I am not weakening it. A .dag under src/v1 means "compile me", and
this authority must never be emitted: the entire value of a controlled fixture is that its input
and its expected outcome are independently authored, and an emitted fixture shares a producer
with the thing it is testing.

So the fixture moves to fixtures/receipt_fixture -- where, as it turns out, four sibling fixtures
already live. The placement was wrong twice: inside a root that means compile-me, and outside the
directory the repository already uses for exactly this.

WHAT THIS WOULD HAVE COST: the same refusal fires on `--required-regen`, which is a witnesses.yml
step, so this PR would have failed CI on a path unrelated to anything it claims. It surfaced only
because the arms harness stopped fabricating a baseline and started printing its output whole --
two fixes that were about something else entirely.

* "not a closed type declared by this authority" was reached ONLY when the type was not found

The arm's text parses as "declared, but not closed". The branch is taken only when the type is
absent from the type environment entirely. Those are different facts, and the difference decides
whether anyone can act.

It cost a wrong conclusion immediately. Node topped the corpus ranking at 798 under that label
and I read it as the one big groundable item in the census -- the thing to ground before stopping.
It is nothing of the sort: v1.std.core Node is a 20-field record carrying an unbounded String, a
recursive List<Node>, and self-reference. Infinite three independent ways. No grounding reaches it.

So the arm splits, and the split is decided against the corpus rather than against the module:

  TypeNotVisibleHere       — some module declares it; this module's reader could not see it.
                             An import-closure gap in the reader, and real work someone can do.
  TypeNotDeclaredAnywhere  — no module declares it. Outside what the authority carries.

`declared_type_names` derives the corpus-wide set once, so the discriminator is a measurement
rather than a guess about why a lookup missed.

This is the fourth refusal in this lane to name the wrong cause, and the second inside the tool
whose entire purpose is to stop that -- after `x` at 1500 and the 55 modules reported as having
no authority. The pattern is stable enough to state: when a refusal is written, the message gets
the author's intent while the branch gets the code's condition, and nobody re-reads the branch.

* The split was invisible in its own output: put the kind in the ranking key

The two new arms both keyed on the bare type name, so the ranked list printed EXACTLY what it
printed before the correction -- Node 798, unchanged -- and a reader would have concluded the
split found nothing. A distinction that does not reach the report is not a distinction.

`declared_anywhere` is corpus-global, so a given type falls entirely into one bucket and the tag
is stable per type rather than a source of fragmentation.

* Cross-check the type reader the way the function reader is already cross-checked

Node ranked 798 as "undeclared anywhere in corpus" while src/v1/00_core.dag declares
`type Node {` in plain sight, and nothing in the output said the reader had skipped it. The
function reader has carried an authored-lines-versus-parsed-count cross-check since the line
reader was replaced -- precisely because two readers of one fact make a miss visible -- and the
type reader had none.

It does now: type_lines versus types_read, per module and in total, with the modules whose counts
disagree named. A gap means every refusal citing those types is measuring THIS READER rather than
the corpus, which is the difference between a census and a fiction.

* Name the declarations the type reader missed, not just how many: a count says a form was missed, the names say which

* Print every type-reader gap, not the worst twelve: a truncated census of a census is the same defect one level up

* Report the node SHAPE of missed declarations: three wrong shape assumptions is enough

* Dump what a record field actually carries, and re-home a doc paragraph onto its subject

SHAPE, measured: DominanceTally is connective=Conj with children=2, ParetoEntry with children=4 --
the declaration parses correctly and the field COUNT is right. But the field node's own children
are empty, so `f.children.iter().next()` is None, filter_map drops every field, and the
`fields.is_empty()` guard drops the record.

My earlier repair moved the defect rather than removing it. A parameter's type IS its child --
verified, and still true. A record FIELD's type is somewhere else, and reading it as a child was
the mirror image of reading it through type_annotation. So the probe now prints the field's name,
child count, connective, type_annotation and inferred, instead of my guessing a fourth time.

Review 54078: the "Every arm here REFUSES" paragraph was documenting measure_generated_drift's
refusal policy while sitting immediately above emitted_generated_sources, so rustdoc attached it
to the wrong function and the drift gate lost its policy doc. Moved onto its subject rather than
separated by a blank line -- a blank line would leave the paragraph orphaned between two functions
it does not describe.

* Both type-reader defects at once: field types come from `inferred`, and the wildcard is gone

DEFECT ONE, the one hiding behind the other. A record FIELD's declared type lives in `inferred`,
not in `children`. A PARAMETER's type is its child -- that is true and stays true -- and reading a
field the same way returned nothing for every field of every record, so filter_map emptied the
list and the `fields.is_empty()` guard dropped the whole declaration. std.pareto read 6 of 13
types and ZERO of its 7 records. Measured from the tree (connective=Conj, children=2,
field.children=0, type_annotation=None, inferred=true), not assumed for a fourth time.

A partially-read record now refuses as a whole, naming the fields responsible. Enumerating only
the fields that resolved would build constructor expressions missing fields -- which do not
compile -- and assert a domain that is false.

DEFECT TWO, which an exhaustiveness fix alone would have made invisible. The match closed with
`_ => {}`. Every other declaration form -- opaque types, aliases, generic shapes -- was silently
dropped, and a dropped declaration was then reported as "no module in the corpus declares this
type": a positive claim about the corpus manufactured out of the reader's own silence. That is the
empty-observation narrow with a wildcard for a cause, and it put a 798-row fiction at the top of a
ranked list that a ruling was made on.

The wildcard is gone. Every remaining form registers as DeclaredNotEnumerable and refuses by name.
Most of those answers are still "cannot enumerate" -- an opaque type has no constructor set -- but
the answer is now SAID rather than inferred from an absence, and it ranks correctly at zero work.

A catch-all over a CLOSED vocabulary discards precisely the guarantee closure exists to provide,
silently, in the seed, where nothing enforces the exhaustiveness the substrate would.

* The cross-check caught my own fix in one run: types_read=6509 against type_lines=957

Removing the wildcard made the arm register EVERY module child as a type declaration -- functions
and data rows included -- so the type environment went from 70% empty to nearly 7x over-full. The
declared-versus-parsed counter found it on the first run after the change, which is exactly the
job it was added for, and it found it in the direction nobody watches: a reader reporting MORE
than the source declares.

Both numbers were wrong for the same underlying reason: the arm had no notion of which module
children are type declarations. It filtered implicitly on Disj/Conj before, which was too narrow;
it now filters on nothing, which is too wide.

So the discriminator gets MEASURED. This prints the distinct (is_type, connective, body, params,
inferred, children) shapes of one module's children, grouped, with examples -- because every
attempt to name that discriminator from memory has been wrong, four times in a row.

* The discriminator, measured: a type declaration has no BODY

Grouped shape census over one module's children, unambiguous:

  is_type=true   conn=Conj/Disj     body=false  params=0  inferred=false
  is_type=false  conn=NoConnective  body=true   (compare_int, tally_verdict, no_names)

A function or a data row carries a body; a type declaration does not. Without this test the arm
had no notion of its own subject, which is the single cause of BOTH failures: filtering
implicitly on two connectives was too narrow, dropping 70% of declarations; filtering on nothing
was too wide, registering every function as a type at 6509 read against 957 authored. Neither was
a bug in the filter -- both were its absence, one defect with two presentations.

* The cross-check was manufacturing its own false positives on generic declarations

All 11 remaining gaps had type_lines EQUAL to types_read, and every missed name was generic:
Magma<T>, Map<key,, Result<ok,, IntegerOverflowSemantics<E>. The reader registers the bare name;
my authored-name extractor split on whitespace, `{` and `=` but not `<`, so it compared
`Magma<T>` against `Magma` and reported a gap that did not exist.

That is the one failure mode a falsifier must not have: it spends exactly the attention it exists
to direct. Cutting at `<` closes it, in both copies of the extractor.

WHAT THE 11 ALSO ESTABLISH, which is why they were worth chasing rather than waving through: the
opaque and alias forms READ CORRECTLY. std.types 79 of 79, std.integer 19 of 19, std.algebra 28
of 28 -- the forms that were absent from the module the discriminator was derived from. So the
rule is BODY-ABSENCE ALONE. Connective is not part of the filter; it only selects the
classification once a declaration has been admitted. That is stronger than the rule the original
sample supported, and it is now checked against the forms that sample did not contain.

* Enroll the plan mode per-PR with a declared cap and a printed denominator (WIP: yml regen next)

* Enroll the receipt against REAL modules, per-PR, capped and with its denominator printed

Review 54089 and the operator ruling agree: the selftest proves the receipt's ARMS discriminate on
a controlled fixture, and nothing was running the receipt against a real module. That is
specification-without-execution one level down -- the "prove modules replaceable end-to-end"
promise exercised only on the fixture.

PER-PR RATHER THAN ON A CADENCE, and the deciding argument is attribution, not cost: a behavioural
divergence found on a cadence lands on a window of many commits, and recovering which one caused
it costs far more than the minutes the cadence saved. Divergence is exactly the class where a
narrow window is the whole value. Not on-demand either -- a mode nobody runs is the inert tier.

AFFORDABLE BECAUSE THE COST IS CONDITIONAL, not because it is small. The mode selects on CHANGED
authorities and exits on an empty selection, so a PR touching no authority module with an emitted
mirror pays nothing.

TWO CONDITIONS, both from rules this branch already paid for:

  A DECLARED CAP, REFUSED ABOVE RATHER THAN SAMPLED. Above three selected modules the run refuses,
  naming them. Checking the first few and reporting a pass is the absorbing fallback exactly: the
  deficit's frequency goes to zero by construction and nobody learns the gate stopped covering
  things.

  THE DENOMINATOR, PRINTED EVERY RUN. A green means the DERIVED CALLS in the selected modules
  agreed -- never that a module is behaviourally equivalent. A bare PASS gets read as promotion
  evidence within a week. The empty selection says so in words too, rather than passing silently.

The step is emitted from gunbc.witness_floor_workflow witness_behavioral_receipt_step and
regenerated through the modeled actuator; the yml diff is exactly the five intended lines.

* A killed run's residue is now loud: refuse if the mirror is dirty before measuring

Review 54094 names the real hazard: the differential installs a candidate over the committed
mirror and restores it on every path, but a process killed between those two leaves the candidate
in the tree. The next run would then read that candidate AS the committed bytes and compare it
against itself -- answering EQUIVALENT, which is the worst available wrong answer: a green that
means nothing, produced by the one mechanism whose whole value is being trusted.

The residue cannot be prevented; no arrangement of writes survives SIGKILL. So it is made loud
instead. A dirty mirror path is a typed refusal naming the recovery command, not a warning and
not a silent read.

This is the same move as everywhere else in this branch: where a bad state cannot be made
unwritable, it must at least be undetectable-to-nobody. CI re-clones and would not have hit it;
a developer running the mode locally after an interrupted run would have, and would have been
handed a green.

* Delete the mirror-drift gate from this PR: it is inert AND reads a carrier that does not exist

Review 54096 found it and the finding is stronger than "unenrolled". `--required-mirror-drift` had
no invocation anywhere, and it reads `dag/gunbc/stage0_mirror_debt.dag`, which is not in the tree
on this branch or on main. So it is not a gate awaiting enrollment; it is a gate that cannot run.
DESIGN §6 names exactly this -- a mechanism whose whole purpose is enforcement, landed with no
execution site, is coverage by illusion, and an inert lens is itself a lie.

Deleted rather than enrolled, for a reason beyond the missing carrier: the open question about
this gate is what it answers that the regen step does not, and wiring it before that is answered
would create the fork rather than find it. Two mechanisms answering one question is the shape this
repository keeps having to undo.

WHAT IS KEPT, because the gate's genuinely useful half was never gate-specific: the
single-producer refactor of `required_regen_host` stands -- `measure_generated_surface` is still
the one producer of the emit-and-compare fact, and `emitted_generated_sources` still routes
through it, which is what stops the receipt from re-emitting its own candidate. `git_stdout` is
kept as a shared helper, documented as such, because the receipt resolves its baseline with it.

WHAT THIS ALSO FIXES, and it is why the diff is this large: `receipt-mode` was branched from
gunbc#8639's head, so #8657 CONTAINED that PR in full. The drift gate was #8639's subject, not
this one's, and two open pull requests carrying one body of code is the same single-authority
problem at the branch level.

Also from review 54096: the per-run module cap now states what kind of number it is. It is a
POLICY BUDGET -- one of DESIGN §5's four sanctioned grounds for a literal in a merge-blocking
check -- and the resource is CI wall clock, since each selected module costs a full v1-compiler
release build. It carries its dissolution condition: the cap is raised when the differential stops
rebuilding the whole crate per candidate, not before.

* A baseline that IS the head is no observation at all, not an empty selection

Review 54102 found the vacuous arm: this job also triggers on push to main, and there
`git merge-base origin/main HEAD` resolves to HEAD, so the diff compares the commit against
itself, no authority can appear changed, and the run passes without ever compiling a candidate.

That is the empty-observation narrow by its named specimen -- a push whose baseline ref IS the
pushed ref -- and it is the mirror of the absorbing fallback: a widen is merely expensive, a
narrow is silently uncovered. A gate that cannot fail on a trigger is worse than absent on it,
because it emits a green nobody can distinguish from a real one.

BOTH HALVES CLOSED, because closing either alone leaves the other reachable:

  The MODE refuses when base equals head, naming the state rather than substituting a baseline.
  `nothing changed` and `I could not see what changed` are different states with different
  remedies, so they get different answers. It does not guess at HEAD~1 either -- inventing a
  baseline to keep a check alive is how the vacuous pass got written in the first place.

  The STEP declares that its subject is a pull request, so the invocation that produces the
  degenerate baseline does not happen on the trigger known to produce it.

The condition is the construction move and the refusal is the wall behind it: the first stops it
occurring, the second makes it loud if it occurs some other way.

* Regenerate witnesses.yml: the receipt step declares its subject is a pull request

* Regenerate witnesses.yml on merged main: the two receipt steps atop main's env rows

* The consolidation witness's positive control keys on the composed step's identity, not its phase list

Folding the behavioral receipt into --required-ci grew the step's name to say so, and
that reddened w_RED_the_retired_step_names_do_not_return -- a witness about RETIRED
STEP NAMES, which has nothing to say about how many phases the composed run has.

Its positive control read the full literal "Required CI: parse, regen, regen
determinism, witness floor". A control that breaks whenever an unrelated phase is
added is measuring the wrong thing: it makes every phase addition edit a witness that
does not own the fact. Keying on the prefix keeps exactly the property the control
needs -- the composed step exists and was read -- and still fails if that step is
removed or renamed out of its family, which is what the negative arms detect.

Measured: floor run 32410310024 was planned=9810 executed=9810 failed=1, this witness
the only failure; every other phase passed, receipt-selftest and receipt included.

* The annotation belongs above the declaration, not inside the body (DESIGN 4c)

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
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.

1 participant