Skip to content

The emit-compile fault's subject is the entry's OWN emitted module, and its absence is a typed refusal - #9442

Merged
briansrls merged 9 commits into
mainfrom
session/vivid-badger-113-work
Aug 27, 2026
Merged

briansrls merged 9 commits into
mainfrom
session/vivid-badger-113-work

Conversation

@briansrls

@briansrls briansrls commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #9405, which has now merged. This PR is exactly three commits over four files, rebuilt onto current main.

The defect

The emit-compile phase's mutation machinery is sound: one fault, one file, must fail alone, byte-exact restore, green back. The subject selector was not. mutation_subject took the first declared module where m != entry_module — which is a shared-core member in every closure that has one.

Measured over the whole roster (run 33055948820, all 8 rostered entries):

  • 7 × std_error_primitives
  • 1 × v1_rt — the emitted runtime, present in every closure

So 8 of 8 entries mutated a shared-core module. Every arm was honestly Discriminated; there was no missing arm to notice. The verdict established cargo ran and refused when the shared core was broken, never this entry's own closure is what was compiled. The corpus name for this is total at the level examined, blind one level down.

Why it matters concretely: the entry's own module is the one member nothing else references — the others are its dependencies, and a dependency does not import the root. So it is exactly what a faulty emission could drop while the crate still compiled. A partial drop that breaks a reference is already caught by the baseline; the uncaught case is a reference-closed drop, and the entry's own leaf is the canonical one.

The fix

MutationSubject is now a struct whose only constructor derives the module from the entry, and it sits behind a module boundary — Rust privacy is module-scoped, so a private field declared beside its constructor is a wall against other modules and mere convention inside the declaring one (this repository's own sole-constructor finding: such a wall governs who constructs and says nothing inside the module that declares the type). With the carrier in a private submodule, the rest of this file is outside that boundary, so MutationSubject { rust_module: … } written anywhere else here does not compile.

The wall bit immediately, which is the executed evidence that it is real: the first build after the move refused this file's own test with error[E0451]: field rust_module is private. That test now obtains its subject the only way anything can — through mutation_subject, over a real tree — and asserts the receipt line says subject=EntryOwnModule. So "unwritable" is earned rather than asserted: structurally impossible, not review diligence at a boundary nobody named.

Absence is MutationVerdict::SubjectRefused carrying a typed MutationSubjectRefusal that names the entry. There is no fallback arm, because a fallback accepts exactly the bad case: it would hand the phase a Discriminated verdict computed over precisely the tree that is broken. SubjectRefused fails the entry like every other non-Discriminated arm.

The refusal's three arms are kept apart because their owners differ, not for completeness:

arm owner remedy
ClosureManifestUnreadable probe crate filesystem / probe root
EntryModuleNotDeclared the emitter the closure lost its own root
EntryModuleFileMissing the write stage declared, never written

closure_modules stops rendering an unreadable lib.rs as an empty vector for the same reason — that reading reports the emission arm for a filesystem fault (execution-provenance loss).

The log still says which kind was mutated (subject=EntryOwnModule module=…) rather than leaving it to be inferred.

What was preserved

Untouched: one fault in one file failing alone; the baseline above it and the byte-exact restore below it as the two controls; the red required to name the injected symbol; every non-discriminating arm stopping the line with its own typed cause; and RestoreFailed adjudicated before every fault verdict — the source-order test that pins that ordering still passes. SubjectRefused returns before the fault is injected, so there is nothing to restore on that path.

Reachability, stated honestly

Measured 8/8 entries emit their own module as its own .rs, so the refusal arm is reachable-but-empty by design — a healthy quiet guard, not a decoration. It is not a check whose RED is unauthorable: every arm is authored directly at the fixture boundary and executes.

Two wrong turns the measurement already killed

  1. "lib.rs dependency order puts the shared core first." False — the entry is second-to-last in 6 of 8 but first in 2 of 8 (std_abi, std_logic). Order was never the mechanism; the != entry_module filter was, and it stepped over the entry explicitly wherever it sat. Both shapes are pinned as separate tests.
  2. "Just mutate the LAST module." Catastrophic — v1_rt is last in all 8. That rule picks the emitted runtime every time: strictly worse than what it replaced.

Carriers

  • gunbc.ci_layer_roots — the subject rule stated as policy beside the roster it governs.
  • gunbc.emitted_closure_compile_seed_growth — declaration rows updated: MutationSubjectRefusal and mutation_subject_refusal_summary added, the two retired tests replaced by the six new ones, and two tests that were never enrolled (the_probe_root_name_is_composed_in_exactly_one_place, a_failed_restore_is_not_masked_by_a_non_terminal_fault_verdict) added.

Also folds in one held citation fix from #9405: the features comment cited stage0_partition_row_features where the code calls stage0_features_for_crate_kind.


Rebuilt onto main after #9405 squash-merged

#9405 landed as squash 107304a5792, which made this branch conflict add/add on the two files that existed only in that PR — my branch carried its nine original commits as history, main carried the flattened equivalent, so git saw two independent additions rather than a divergence. Rebuilt from main with these three commits replayed on top.

Two resolutions worth naming:

  • The .rs conflict was my own citation fix, which A required CI phase that compiles an emitted closure, with its red established by mutation #9405 had already made on main and made better (it names both stage0_features_for_crate_kind and stage0_partition_row_features and says which reaches which). Main's side taken; mine is superseded, so the "folds in one held citation fix" line in the first commit message no longer describes this diff.
  • The seed-growth roster was taken from main wholesale — A required CI phase that compiles an emitted closure, with its red established by mutation #9405 replaced it with an audited exact roster after a REQUEST_CHANGES found a row citing a symbol that resolves to nothing — and then re-derived against this branch's .rs mechanically rather than by reading the diff, which is how that audit found 16 unaccounted rows. Result after the edit: 53 declarations, 53 rows, no duplicates, stale set empty, missing set empty. The delta is exactly what this change implies — the 2 retired tests out, MutationSubjectRefusal, mutation_subject_refusal_summary and the 6 new tests in.

Re-verified on the rebuilt branch: cargo fmt --all --check clean, claim_executor binary builds (not just --lib — that gap is what the third commit repairs), 10/10 tests pass.

@gunbai-bot gunbai-bot Bot changed the title emit-compile mutates the SHARED CORE 8/8 — make the subject the entry's OWN module, with absence a typed refusal (measured: present 8/8, so no fallback arm is needed) The emit-compile fault's subject is the entry's OWN emitted module, and its absence is a typed refusal Aug 27, 2026
@gunbai-bot

gunbai-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Base note. This PR's head sits on top of session/deep-gull-307 (#9405), which is not yet merged — so the diff against main currently shows #9405's change as well. My own change is exactly commit 71c03f7b15. Once #9405 merges I will merge main in and the diff here reduces to that one commit. Held in draft until then.

@gunbai-bot

gunbai-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Executed receipt: 8/8 now mutate the entry's own module

claim_executor --required-emit-compile --source-root dag --source-root src/v2, run cold on a fresh probe root, head bb4be5d406 (merged to e83fcbeb005). PHASE_EXIT=0.

entry files baseline mutation subject
dag/gunbc/ci_layer_roots.dag 34 status=0 EntryOwnModule gunbc_ci_layer_roots
dag/gunbc/scm/load_standing.dag 9 status=0 EntryOwnModule gunbc_scm_load_standing
dag/extdeps/uri.dag 10 status=0 EntryOwnModule extdeps_uri
dag/std/measure.dag 24 status=0 EntryOwnModule std_measure
dag/std/node.dag 15 status=0 EntryOwnModule std_node
dag/std/content_hash.dag 9 status=0 EntryOwnModule std_content_hash
dag/std/abi.dag 9 status=0 EntryOwnModule std_abi
dag/std/logic.dag 6 status=0 EntryOwnModule std_logic

subject=EntryOwnModule — 8. Occurrences of ClosureMember, std_error_primitives or module=v1_rt as a subject — 0.

That is the exact inversion of the measured defect: the same eight entries previously mutated a shared-core member 8/8 (7 × std_error_primitives, 1 × v1_rt). Each entry's module= now equals its own emitted root, including both orderings — std_abi and std_logic, where the entry is declared first, and the six where it sits second-to-last.

Every red is attributed. Each mutation verdict quotes the diagnostic naming EMIT_COMPILE_MUTATION_PROBE, so the red is the injected fault rather than a pre-existing error or a killed cargo — and each is followed by a byte-exact restore whose green the phase requires. Line numbers differ per entry (1819, 45, 840, 1504, 121, 307, 31, …), which is itself evidence the fault landed in a different file each time rather than in one shared module.

Selection: universe=4105 selected=8 not_selected=4097, remainder retained. Context: 8 declared entries reach 47 modules.

One thing this receipt does not claim

The refusal arm (SubjectRefused) fires zero times here, by design — all 8 entries emit their own module. It is reachable-but-empty, not unreachable: every arm is authored and executes at the fixture boundary (an_entry_module_missing_from_the_closure_refuses_rather_than_substituting, an_entry_module_declared_without_a_file_refuses_as_a_write_defect, an_unreadable_closure_manifest_refuses_on_its_own_cause). A healthy quiet guard.

@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 27, 2026 13:58
Brian Searls added 3 commits August 27, 2026 16:27
… with absence a typed refusal

The mutation machinery was sound; the SUBJECT SELECTOR was not. It took the first
declared module that was not the entry, which is a shared-core member in every
closure that has one. Measured over the whole roster: 8 of 8 entries mutated a
shared member -- 7 x std_error_primitives, 1 x v1_rt, the emitted runtime, which
is in every closure. Every arm was honestly Discriminated and there was no
missing arm to notice, so the phase established "cargo refused when the shared
core was broken" while reading as "this entry's closure is what was compiled":
total at the level examined, blind one level down.

The entry's own module is the one member NOTHING ELSE REFERENCES -- the others
are its dependencies, and a dependency does not import the root -- so it is
exactly the file a faulty emission can drop while the crate still compiles. A
drop that breaks a reference is already caught by the baseline; the uncaught
case is a REFERENCE-CLOSED drop, and the entry's own leaf is the canonical one.

MutationSubject is now a struct whose only constructor derives the module from
the entry, so "a shared member carried the fault" is unwritable rather than
merely unselected. Absence is MutationVerdict::SubjectRefused carrying a typed
MutationSubjectRefusal that names the entry -- never a fallback, because falling
back accepts precisely the bad case. Its three arms are kept apart because their
owners differ: an unreadable manifest is a probe-crate defect, an undeclared
entry module is an EMISSION defect, a declared module with no file is a WRITE
defect. closure_modules stops rendering an unreadable lib.rs as an empty closure
for the same reason -- that reading reports the emission arm for a filesystem
fault.

Measured 8/8 present, so the refusal arm is reachable-but-empty by design: a
healthy quiet guard, with its RED authorable and authored at the fixture
boundary.

Also folds in one held citation fix: the features comment cited
stage0_partition_row_features where the code calls stage0_features_for_crate_kind.
… and delete the unused import

Rust privacy is MODULE-scoped, not function-scoped, so a private field on a
struct declared beside its constructor is a wall against other modules and mere
convention within the declaring one -- the repository's own sole-constructor
finding, which is that such a wall governs WHO constructs and says nothing
inside the module that declares the type. The previous commit's comment claimed
a shared member carrying the fault was unwritable; it was unwritable across the
module boundary and one struct literal away anywhere in this file.

MutationSubject and mutation_subject now live in a private submodule and are
re-exported. The rest of the file is outside that boundary, so the literal stops
compiling rather than being discouraged by a comment.

THE WALL BIT IMMEDIATELY, WHICH IS THE EXECUTED EVIDENCE THAT IT IS REAL: the
first build after the move refused this file's own test with E0451, field
rust_module is private. That test now obtains its subject the only way anything
can -- through mutation_subject, over a real tree -- and asserts the receipt
line says subject=EntryOwnModule.

Also deletes the unused #[cfg(test)] import of workspace_root in claim_executor:
all seven uses are fully qualified, so the bare use was dead. Warning-only and
invisible to CI, which runs no Rust suite; folded in here rather than reopening
the PR beneath this one.
…eted use, not the next one

Deleting only the `use v1_compiler::cli_run::workspace_root;` line left its
`#[cfg(test)]` attribute sitting above `use ...PhaseProfile;`, which is
unconditional -- so claim_executor stopped compiling outside a test build
(E0433). The attribute goes with the import it gated.

Caught by running the binary rather than by the test suite: cargo test --lib
builds the LIBRARY, so ten green tests were reported over a bin that did not
exist. CI's build lane compiles the binary and would have caught it; the local
verification was the wrong target.
@gunbai-bot
gunbai-bot Bot force-pushed the session/vivid-badger-113-work branch from bb4be5d to 42446da Compare August 27, 2026 16:32
…113-work

# Conflicts:
#	src/v1/stage0/src/bin/claim_executor.rs
@gunbai-bot

gunbai-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

The CI red on this PR is inherited from main, not from this change.

The floor lane fails on dag/product/fabric/contention.dag, which is byte-identical to main on this branch (verified diff = 0, as is supply.dag). Cause is a semantic merge conflict: one change renamed grant_duration_seconds → grant_duration_bound_seconds and made it partial (Second?) behind a new GrantDuration type; #9395's contention module was written against the old name. Each was green on its own base. The repair is gunbc#9488 (owned by silent-bear-842, approved and in flight) — not mine to make, and deliberately so: the accessor is partial precisely so no default can be substituted for an unobserved duration.

A second floor failure — a duplicate declaration in fleet_fan_wiring_witness.dag — was on this branch, from carrying an older main. Fixed on main by #9497 and cleared here by merging current main (e400ba92fc); declaration count is back to 1.

Also worth recording for anyone reading the failure list: it is a prefix, not a population. The declarations phase stops at the first refusal, so the five contention sites quoted today were the reachable ones; warm-hawk-909 found a sixth (offer_quoted_total_for_grant now returning a QuotedTotal coproduct, consumed raw). Repairing only the listed five would have gone red again and read as a fresh defect.

State of this change, re-verified on the merged tree b4b495b57f: roster 53 declarations / 53 rows with no duplicates and both differences empty, claim_executor binary builds clean, 10/10 tests, cargo fmt --all --check clean. Two approvals, no request-changes. It should go green once #9488 lands and this branch merges main.

— sent from vivid-badger-113

…he hand-Rust receipt AND hid from its census

review 56892 (codex, REQUEST_CHANGES) found that the privacy-boundary commit
added `impl MutationSubject { fn rust_module }`, while
gunbc.emitted_closure_compile_seed_growth states in as many words that this file
carries NO IMPL BLOCK, that every item is a free function or a type, and that it
adds ZERO uncitable items. std.decl_ref offers WholeDeclaration or NamedField
and neither names a method on an impl block, so a method cannot appear in the
roster at all -- the receipt was materially false.

The finding was correct and understated. My own census scanned only column 0 and
4-space indents, so the 8-space impl method was INVISIBLE to it: measured,
`rust_module` is the single item an any-indent scan finds that the old scan
missed. So the 53/53 reported three times today was clean precisely because the
instrument could not see the one item that broke the claim -- a complete-looking
count over a population the instrument had silently narrowed.

Both halves repaired. The accessor is now a free function INSIDE the privacy
boundary, so the wall is unchanged -- it still reads the private field from
within the declaring module, and MutationSubject is still constructible only
through mutation_subject -- while being citable. It is enrolled in the roster,
and the census now scans any indent so a nested or impl item cannot hide from it
again.

Measured after the change: 54 declarations, 54 rows, no duplicates, stale empty,
missing empty, and zero impl blocks in the file. Binary builds, 10/10 tests.
@gunbai-bot

gunbai-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixed in 0b8305d348, taking the first of the two options review 56892 offered (free function within the privacy boundary, enumerated).

The finding was correct. gunbc.emitted_closure_compile_seed_growth states this file carries no impl block, that every item is a free function or a type, and that it adds zero uncitable items — and std.decl_ref offers only WholeDeclaration or NamedField, neither of which names a method on an impl block. My impl MutationSubject { fn rust_module } made that receipt materially false.

It was also understated, and the extra part is the more useful half. My own census scanned only column-0 and 4-space indents, so the 8-space impl method was invisible to it. Measured: rust_module is the single item an any-indent scan finds that the old scan missed. So the 53/53 I reported three times today was clean precisely because the instrument could not see the one item that broke the claim — a complete-looking count over a population the instrument had silently narrowed. That is the same shape as the fail-fast finding list discussed on this PR earlier, one level in.

Both halves repaired:

  • The accessor is a free function inside the privacy boundary (subject_rust_module). The wall is unchanged — it still reads the private field from within the declaring module, and MutationSubject remains constructible only through mutation_subject, so nothing is given up by not being a method. Zero impl blocks in the file.
  • Enrolled in the roster, and the census now scans any indent, so a nested or impl item cannot hide from it again.

Measured after the change: 54 declarations / 54 rows, no duplicates, stale and missing both empty, 0 impl blocks. claim_executor binary builds, 10/10 tests, fmt clean.

Note the unrelated floor-lane red on this branch is still main's contention/supply break (#9488), not this change — details in the comment above.

— sent from vivid-badger-113

review 56899 (codex, REQUEST_CHANGES) found the test module's doc comment still
claiming the tests establish "that the fault prefers a closure member over the
entry" -- the exact guarantee this PR deletes -- while the construction it
describes now refuses rather than substitutes. Prose in the implementing file
asserting the old invariant is worse than a stale comment elsewhere: it is the
authority a reader consults for what the code guarantees.

Swept the file and both .dag carriers rather than patching the cited line, since
one reported finding is not a population. Two matches: this one, and a probe-root
sentence about a temp dir fallback that is unrelated and correct. Nothing else
asserts the superseded behaviour.

Binary builds, 10/10 tests, fmt clean.
@gunbai-bot

gunbai-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixed in f09e22cde5. review 56899 was right and this one is worse than a stale comment elsewhere in the tree.

The test module's doc comment still claimed the tests establish "that the fault prefers a closure member over the entry" — the exact guarantee this PR deletes — sitting in the file that implements the replacement. A reader consulting the implementing file for what the code guarantees would have taken away the superseded invariant, which is the single-authority problem rather than a cosmetic one. The construction it contradicts is correct as written: mutation_subject refuses via SubjectRefused and has no fallback arm.

Now reads: the fault targets the entry's own emitted module, with its absence refused rather than substituted for.

I swept the whole file and both .dag carriers rather than patching the cited line, on the principle established earlier in this PR that a reported finding is a lower bound rather than a population. Two matches: this one, and an unrelated (correct) sentence about the probe root falling back to a temp dir. Nothing else asserts the superseded behaviour.

Binary builds, 10/10 tests, fmt clean.

For the record on this PR's review history — both codex findings were real defects I introduced and my own checks could not see: the impl method contradicted the hand-Rust receipt and was invisible to a census that only scanned column-0 and 4-space indents, and this comment is invisible to any mechanical check, since no gate reads prose. That is the division DESIGN describes — internal review finds missing tests, external review finds missing checks.

— sent from vivid-badger-113

…e, disposition it, and state the census scope

review 56907 (codex, REQUEST_CHANGES) found three ways the seed-growth receipt
was false as landed, and all three were correct:

  - `mod entry_own_subject` was not enumerated at all
  - "Every item in the file is a free function or a type" does not cover a module
  - the roster claimed "47 rows, 47 declarations" against a file that had grown

All three are repaired. The module is enrolled as a row -- a module IS citable,
since WholeDeclaration names it, so the uncitable-item count genuinely stays
ZERO and that claim needed no weakening. The shape sentence now names the one
private module explicitly, and a new paragraph dispositions WHY it exists rather
than glossing it: Rust privacy is module-scoped, so without the submodule the
shared-core substitution this phase prevents would be unwritable only by
agreement instead of by construction. The count is now 55/55.

THE ROOT CAUSE IS SHARED WITH THE TWO PRECEDING FINDINGS AND IS THE PART WORTH
FIXING. My census had blind spots invisible in its own output: it scanned only
column-0 and 4-space indents, so it could not see an impl method; then it omitted
`mod`, so it could not see the privacy boundary. Both times it reported a clean,
exact-looking total. The census now covers every item kind at every indent --
const, static, fn, struct, enum, type, mod -- and its ONE remaining exclusion is
written into the receipt itself: the `#[cfg(test)] mod tests` harness, whose
member tests are enumerated individually. An unstated narrowing is how the first
two survived, so the scope is now stated rather than silently widened a third
time.

Binary builds, 10/10 tests, fmt clean.
@gunbai-bot

gunbai-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixed in 2a868c864c. All three points of review 56907 were correct.

  • mod entry_own_subject was not enumerated — now a roster row. A module is citable (WholeDeclaration names it), so the uncitable-item count genuinely stays zero and that claim needed no weakening; the receipt simply wasn't saying so.
  • "Every item is a free function or a type" didn't cover a module — the sentence now names the one private module, and a new paragraph dispositions it rather than glossing: Rust privacy is module-scoped, so without the submodule the shared-core substitution this phase prevents would be unwritable only by agreement instead of by construction.
  • "47 rows, 47 declarations" was stale — now 55/55, zero stale, zero unaccounted.

The root cause is shared with the two preceding findings, and that is the part actually worth fixing. My census had blind spots invisible in its own output: it scanned only column-0 and 4-space indents, so it could not see an impl method; then it omitted mod, so it could not see the privacy boundary. Both times it reported a clean, exact-looking total — three findings from one instrument that could not see what it was missing.

So the census now covers every item kind at every indent (const, static, fn, struct, enum, type, mod), and its one remaining exclusion is written into the receipt: the #[cfg(test)] mod tests harness, whose member tests are enumerated individually. An unstated narrowing is how the first two survived, so the scope is stated rather than silently widened a third time.

Binary builds, 10/10 tests, fmt clean. The floor-lane red remains main's contention/supply break (#9488), unrelated to this diff.

— sent from vivid-badger-113

@briansrls
briansrls merged commit d8a261f into main Aug 27, 2026
3 checks passed
@briansrls
briansrls deleted the session/vivid-badger-113-work branch August 27, 2026 22:50
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