diff --git a/dag/gunbc/ci_layer_roots.dag b/dag/gunbc/ci_layer_roots.dag index 1ef7d484178..1952bd99a3c 100644 --- a/dag/gunbc/ci_layer_roots.dag +++ b/dag/gunbc/ci_layer_roots.dag @@ -195,6 +195,23 @@ data required_v2_emission_dissolution: DissolutionCondition = unbound_dissolutio // sized against a TEMPORARY CONDITION: emitter repairs that clear a widely-reached site return // many entries to admissibility at once. // +// THE FAULT GOES INTO THE ENTRY'S OWN EMITTED MODULE, AND THAT IS A LOAD-BEARING CHOICE RATHER +// THAN AN IMPLEMENTATION DETAIL. The selector first shipped preferring any declared member OTHER +// than the entry, on the reasoning that a dependency is the stronger subject because it proves +// the verdict reaches past the entry's own bytes. Measured over this whole roster, that rule +// mutated a SHARED-CORE module for every single entry -- the emitted runtime in one case, which +// is in every closure there is. Each verdict was honestly discriminating and each one established +// `cargo refused when the shared core was broken`, never `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. Its ABSENCE is therefore a typed refusal naming the entry, never a +// fallback to some other member: falling back would hand the phase a discriminating verdict +// computed over precisely the tree that is broken. +// // A FAILED RESTORE IS TERMINAL FOR THE RUN. If the mutation arm's byte-exact restore does not // hold, the phase stops at that entry and reports every later one as NotExecuted; it is not a // per-entry finding the siblings continue past, and it is not recoverable by re-running the diff --git a/dag/gunbc/emitted_closure_compile_seed_growth.dag b/dag/gunbc/emitted_closure_compile_seed_growth.dag index cda8c3a8835..57618d7f767 100644 --- a/dag/gunbc/emitted_closure_compile_seed_growth.dag +++ b/dag/gunbc/emitted_closure_compile_seed_growth.dag @@ -9,7 +9,7 @@ import gunbc.seed_growth { SeedGrowthJustification } // the obligation it declares, rather than inside gunbc.seed_growth_admission, which owns the // ROSTER and the join and would otherwise accumulate every lane's rows in the module that // adjudicates them. -data emitted_closure_compile_seed_growth_note: String = "Seed-growth obligation for the host behind the required emit-compile phase.\n\nWHAT THE CHANGE IS. The v2-emission phase emits one entry's closure and stops at the emitter. Its own header in cli_run.rs enumerates what it therefore cannot see, and the first item is 'a rustc error in the emitted tree (nothing here compiles the emission)'. DESIGN's Building-&-checks section carries a declared rung drop headed 'A BLOCKING EMIT-STAGE DIAGNOSTIC CAN SIT ON MAIN INDEFINITELY WITH NO REQUIRED PHASE THAT FAILS', whose restoration trigger reads: this row retires when a required phase EMITS over a closure that reaches call sites -- the compile re-add on the queue the floor cut created. This host is that phase: same producer (compile_entry_emission), the emitted files written as a crate, cargo run over it.\n\nWHY IT IS NOT A SECOND EMITTER. The phase calls the SAME transaction the emission phase calls and adds one stage after it. A green there and a green here cannot be two facts about two emissions, which is exactly the property a separately-authored probe would have given up.\n\nWHY THE MANIFEST IS DERIVED AND NOT AUTHORED. The corpus already carries a hand-concatenated probe manifest (tools.self_host_curated_seed_linked_harness cssl_v1_compiled_probe_lib_cargo_toml), marked scaffold debt in its own module for being concat-authored TOML. Consuming it from a merge-blocking gate would have pinned that debt open on the required path, and authoring a second one would have been new scaffolding the operator declined on 2026-08-25. The manifest here is rendered from the modeled cargo authorities instead -- extdeps.rust.version render_cargo_package_header_prefix for the package header, v1.compiler.stage0_crates stage0_foundation_runtime_dependencies for the seed's own runtime dependency set, and render_stage0_crate_dep per row -- so no new markup is authored at all.\n\nNO IMPL BLOCK, AND THAT IS DELIBERATE. Every item in the file is a free function or a type, because an impl method has no DeclarationRef spelling -- std.decl_ref offers WholeDeclaration or NamedField and neither names a method on an impl block -- so methods would have grown the class gunbc.seed_growth_admission reports as seed_growth_uncitable_item_keys. v1_compiler.declaration_index took the same route for the same reason. Uncitable items added by this change: ZERO.\n\nHAND-ITEM DELTA: enumerated below rather than counted. src/v1/stage0/src/cli_run.rs adds no declaration -- one #[path] mod line and one re-export list -- and src/v1/stage0/src/bin/claim_executor.rs adds one variant to an existing exhaustive enum and one phase body inside an existing function; both dispositions are ExistingSeedItemModified, and listing them would net a modification into an addition census.\n\nWHY THIS ROSTER DRIFTED ONCE, RECORDED SO IT IS NOT REPEATED. Review 56685 found it citing PROBE_PACKAGE_NAME, which resolves to nothing: an earlier revision carried a package-name CONSTANT, the cargo-fingerprint-aliasing repair replaced it with the per-entry function probe_package_name, and the receipt was not updated with the code. An audit of the whole roster then found 1 stale name and 16 unaccounted declarations -- every one of the 16 added by a LATER repair in this same PR (probe_root, the selection digests, retention, the probe-line attribution fix, two tests), each of which grew the file without growing its receipt. That is the exact failure the carrier exists to catch, committed inside the carrier. The roster is now exact against the file: 47 rows, 47 declarations, zero stale, zero unaccounted. THE STANDING HAZARD IS THAT IT IS A HAND ROSTER BESIDE ITS SUBJECT, so it can only be re-verified, never trusted: it drifts silently on the next declaration added, and nothing in the required run compares the two. Its dissolution is the same v1-hand-queue-drain lane this obligation already names.\n\nWHAT THE EXECUTED EVIDENCE IS, because the seed's own unit tests are not it. The Rust suite was removed from CI on 2026-07-11, so nothing under #[cfg(test)] executes on the merge path and none of it may be cited as coverage. The executed evidence is the phase itself: establish_discriminating_red injects one type error into one emitted file on EVERY required run, requires it to fail alone, restores the bytes and requires the green back -- and a mutation that fails to go red is a PHASE FAILURE, not a note. That is DESIGN 4b's authorable-RED question answered by execution rather than by inspection, and it is why a green from this phase carries information that a bare cargo-exit-status phase would not." +data emitted_closure_compile_seed_growth_note: String = "Seed-growth obligation for the host behind the required emit-compile phase.\n\nWHAT THE CHANGE IS. The v2-emission phase emits one entry's closure and stops at the emitter. Its own header in cli_run.rs enumerates what it therefore cannot see, and the first item is 'a rustc error in the emitted tree (nothing here compiles the emission)'. DESIGN's Building-&-checks section carries a declared rung drop headed 'A BLOCKING EMIT-STAGE DIAGNOSTIC CAN SIT ON MAIN INDEFINITELY WITH NO REQUIRED PHASE THAT FAILS', whose restoration trigger reads: this row retires when a required phase EMITS over a closure that reaches call sites -- the compile re-add on the queue the floor cut created. This host is that phase: same producer (compile_entry_emission), the emitted files written as a crate, cargo run over it.\n\nWHY IT IS NOT A SECOND EMITTER. The phase calls the SAME transaction the emission phase calls and adds one stage after it. A green there and a green here cannot be two facts about two emissions, which is exactly the property a separately-authored probe would have given up.\n\nWHY THE MANIFEST IS DERIVED AND NOT AUTHORED. The corpus already carries a hand-concatenated probe manifest (tools.self_host_curated_seed_linked_harness cssl_v1_compiled_probe_lib_cargo_toml), marked scaffold debt in its own module for being concat-authored TOML. Consuming it from a merge-blocking gate would have pinned that debt open on the required path, and authoring a second one would have been new scaffolding the operator declined on 2026-08-25. The manifest here is rendered from the modeled cargo authorities instead -- extdeps.rust.version render_cargo_package_header_prefix for the package header, v1.compiler.stage0_crates stage0_foundation_runtime_dependencies for the seed's own runtime dependency set, and render_stage0_crate_dep per row -- so no new markup is authored at all.\n\nNO IMPL BLOCK, AND THAT IS DELIBERATE. Every item in the file is a free function, a type, or the one private MODULE that carries the mutation subject's privacy boundary, because an impl method has no DeclarationRef spelling -- std.decl_ref offers WholeDeclaration or NamedField and neither names a method on an impl block -- so methods would have grown the class gunbc.seed_growth_admission reports as seed_growth_uncitable_item_keys. v1_compiler.declaration_index took the same route for the same reason. Uncitable items added by this change: ZERO.\n\nTHE ONE `mod` ITEM, DISPOSITIONED RATHER THAN GLOSSED. `entry_own_subject` is a private module and is the only item in this file that is neither a free function nor a type. It exists because Rust privacy is MODULE-scoped: a private field on a struct declared beside its constructor is a wall against other modules and mere convention within the declaring one, so the carrier and its single constructor sit inside a submodule and everything else in the file is outside that boundary. Without it, `MutationSubject { rust_module: ... }` compiles anywhere in the file and the shared-core substitution this phase exists to prevent is unwritable only by agreement. IT IS CITABLE and it is enumerated: a module is a whole declaration, so `WholeDeclaration` names it and the uncitable-item count stays ZERO. Its members are enumerated beside it -- the type, the constructor, and the accessor -- which is why the accessor is a free function inside the boundary rather than an impl method.\n\nHAND-ITEM DELTA: enumerated below rather than counted. src/v1/stage0/src/cli_run.rs adds no declaration -- one #[path] mod line and one re-export list -- and src/v1/stage0/src/bin/claim_executor.rs adds one variant to an existing exhaustive enum and one phase body inside an existing function; both dispositions are ExistingSeedItemModified, and listing them would net a modification into an addition census.\n\nWHY THIS ROSTER DRIFTED ONCE, RECORDED SO IT IS NOT REPEATED. Review 56685 found it citing PROBE_PACKAGE_NAME, which resolves to nothing: an earlier revision carried a package-name CONSTANT, the cargo-fingerprint-aliasing repair replaced it with the per-entry function probe_package_name, and the receipt was not updated with the code. An audit of the whole roster then found 1 stale name and 16 unaccounted declarations -- every one of the 16 added by a LATER repair in this same PR (probe_root, the selection digests, retention, the probe-line attribution fix, two tests), each of which grew the file without growing its receipt. That is the exact failure the carrier exists to catch, committed inside the carrier. The roster is now exact against the file: 55 rows, 55 declarations, zero stale, zero unaccounted. THE CENSUS COUNTS EVERY ITEM KIND AT EVERY INDENT -- const, static, fn, struct, enum, type and mod -- with exactly one stated exclusion, the `#[cfg(test)] mod tests` harness itself, whose member tests ARE enumerated individually. The exclusion is written down because an unstated narrowing is how the two preceding defects survived: a scan of only column-0 and 4-space indents could not see an impl method, and a scan omitting `mod` could not see the privacy boundary. THE STANDING HAZARD IS THAT IT IS A HAND ROSTER BESIDE ITS SUBJECT, so it can only be re-verified, never trusted: it drifts silently on the next declaration added, and nothing in the required run compares the two. Its dissolution is the same v1-hand-queue-drain lane this obligation already names.\n\nWHAT THE EXECUTED EVIDENCE IS, because the seed's own unit tests are not it. The Rust suite was removed from CI on 2026-07-11, so nothing under #[cfg(test)] executes on the merge path and none of it may be cited as coverage. The executed evidence is the phase itself: establish_discriminating_red injects one type error into one emitted file on EVERY required run, requires it to fail alone, restores the bytes and requires the green back -- and a mutation that fails to go red is a PHASE FAILURE, not a note. That is DESIGN 4b's authorable-RED question answered by execution rather than by inspection, and it is why a green from this phase carries information that a bare cargo-exit-status phase would not." data emitted_closure_compile_seed_growth_justification: SeedGrowthJustification = SeedGrowthJustification { hand_authored_declarations: [ @@ -23,9 +23,13 @@ data emitted_closure_compile_seed_growth_justification: SeedGrowthJustification DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "cargo_verdict_summary", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "cargo_verdict_probe_line", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "cargo_verdict_stderr_tail", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "entry_own_subject", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "MutationSubject", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "MutationSubjectRefusal", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "mutation_subject_rust_module", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "subject_rust_module", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "mutation_subject_name", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "mutation_subject_refusal_summary", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "MutationVerdict", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "mutation_verdict_discriminated", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "mutation_verdict_summary", field: WholeDeclaration }, @@ -54,8 +58,12 @@ data emitted_closure_compile_seed_growth_justification: SeedGrowthJustification DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "acquire_probe_root_lock", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "run_required_emit_compile", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "manifest_carries_the_modeled_dependency_rows", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "mutation_prefers_a_closure_member_over_the_entry", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "mutation_falls_back_to_the_entry_and_names_it", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "probe_tree", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "the_subject_is_the_entry_own_module_past_a_leading_shared_member", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "the_subject_is_the_entry_own_module_when_it_is_declared_first", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "an_entry_module_missing_from_the_closure_refuses_rather_than_substituting", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "an_entry_module_declared_without_a_file_refuses_as_a_write_defect", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "an_unreadable_closure_manifest_refuses_on_its_own_cause", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "an_unreached_entry_is_not_a_pass", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "the_probe_root_name_is_composed_in_exactly_one_place", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.emitted_closure_compile_host", decl_name: "a_failed_restore_is_not_masked_by_a_non_terminal_fault_verdict", field: WholeDeclaration }, diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index fa67688cd28..f6b2262e107 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -3,8 +3,6 @@ use std::fs; use std::path::PathBuf; use std::process::ExitCode; -#[cfg(test)] -use v1_compiler::cli_run::workspace_root; use v1_compiler::cli_run::PhaseProfile; fn require_value(args: &[String], idx: usize, flag: &str) -> Result { diff --git a/src/v1/stage0/src/emitted_closure_compile_host.rs b/src/v1/stage0/src/emitted_closure_compile_host.rs index d126d5f925f..48d143d3482 100644 --- a/src/v1/stage0/src/emitted_closure_compile_host.rs +++ b/src/v1/stage0/src/emitted_closure_compile_host.rs @@ -165,30 +165,171 @@ pub fn cargo_verdict_stderr_tail(verdict: &CargoVerdict) -> &str { } } -/// WHICH FILE THE FAULT WENT INTO, carried rather than inferred. +/// WHICH FILE THE FAULT WENT INTO -- AND IT IS THE ENTRY'S OWN EMITTED MODULE, BY CONSTRUCTION. /// -/// A closure member is the stronger subject -- it establishes that the cargo verdict reaches -/// past the entry's own bytes into the closure the entry pulled in, which is the property -/// DESIGN's row turns on. `EntryModule` is the honest fallback for a closure whose only member -/// is the entry, and naming it means a reader can tell the weaker measurement from the stronger -/// one instead of assuming the stronger. -#[derive(Debug, Clone)] -pub enum MutationSubject { - ClosureMember { rust_module: String }, - EntryModule { rust_module: String }, +/// THIS TYPE USED TO OFFER A CHOICE, AND THE CHOICE WAS THE DEFECT. The selector 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 at the landing of the phase, 8 of 8 entries mutated a +/// shared member -- 7 x `std_error_primitives` and 1 x `v1_rt`, the emitted runtime, which is in +/// every closure. 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`, and never +/// `THIS entry's own closure is what was compiled` -- total at the level examined, blind one +/// level down. +/// +/// WHY THE ENTRY'S OWN MODULE IS THE ONLY SUBJECT WORTH THE FAULT. It is the one member NOTHING +/// ELSE REFERENCES: the other members are its dependencies, and a dependency does not import the +/// root. So it is exactly the file 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. Mutating a shared core +/// member cannot distinguish that case, because the shared member is reached whether or not the +/// entry's own bytes are in the tree at all. +/// +/// SO THE CARRIER HOLDS ONE THING AND HAS NO SPELLING FOR THE OTHER. `mutation_subject` is the +/// only constructor and it derives the module from the ENTRY, so `a shared member carried the +/// fault` is unwritable here rather than merely unselected -- construction over validation, and +/// there is no fallback arm to accept the bad case (DESIGN 5). Its absence is a typed refusal +/// naming the entry (`MutationSubjectRefusal`), never a silent substitution. +/// +/// THE PRIVACY BOUNDARY IS NAMED, BECAUSE RUST'S IS MODULE-SCOPED AND NOT FUNCTION-SCOPED. A +/// private field on a struct declared beside its constructor is a wall against OTHER modules and +/// mere convention within this one -- the sole-constructor finding this repository already +/// records, which is that such a wall governs WHO constructs and says nothing inside the +/// declaring module. So the carrier and its one constructor live in the submodule below and +/// everything else in this file is OUTSIDE that boundary: `MutationSubject { rust_module: ... }` +/// written anywhere else here does not compile, rather than compiling and being discouraged by a +/// comment. That is the difference between structurally impossible and review diligence, and it +/// is one `mod` block. +pub use entry_own_subject::{mutation_subject, subject_rust_module, MutationSubject}; + +mod entry_own_subject { + use super::MutationSubjectRefusal; + use std::path::Path; + + #[derive(Debug, Clone)] + pub struct MutationSubject { + /// PRIVATE, and the module wrapping it is what makes that mean something: the only route + /// to a value of this type from anywhere in the file is `mutation_subject`, which derives + /// the name from the ENTRY and cannot be handed any other module. + rust_module: String, + } + + /// A FREE FUNCTION AND NOT AN `impl` METHOD, and the reason is a receipt rather than taste. + /// `std.decl_ref` offers `WholeDeclaration` or `NamedField` and neither names a method on an + /// impl block, so a method is UNCITABLE: it cannot appear in this file's seed-growth roster, + /// and the roster's own census would then be silently short by exactly the items it cannot + /// spell. `gunbc.emitted_closure_compile_seed_growth` states that this file adds ZERO + /// uncitable items, and an `impl` here would have made that receipt false while the census + /// still reported a clean total. Inside the privacy boundary it reads the private field for + /// the same reason the constructor does, so nothing is given up by not being a method. + pub fn subject_rust_module(subject: &MutationSubject) -> &str { + subject.rust_module.as_str() + } + + /// THE FAULT GOES INTO THE ENTRY'S OWN EMITTED MODULE, OR NOWHERE. + /// + /// There is no member-preferring arm and no fallback: the module name is DERIVED from the + /// entry, so the selector has nothing to select. What it decides is only whether that one + /// module is present, and absence is returned as a typed refusal naming the entry rather than + /// substituted for. The substitution is what this function used to do, and it is what made + /// every verdict on the roster a statement about the shared core (see `MutationSubject`). + pub fn mutation_subject( + crate_dir: &Path, + entry_module: &str, + ) -> Result { + let lib_rs = crate_dir.join("src/lib.rs"); + let declared = match super::closure_modules(&lib_rs) { + Ok(modules) => modules, + Err(detail) => { + return Err(MutationSubjectRefusal::ClosureManifestUnreadable { + lib_rs: lib_rs.display().to_string(), + detail, + }) + } + }; + // DECLARED AND WRITTEN ARE TWO FACTS AND BOTH ARE REQUIRED. A `pub mod` line with no file + // behind it does not compile, and a file no `pub mod` line reaches is not in the closure + // at all -- so checking only one of them would admit a subject that is not actually part + // of what cargo compiled, and the fault would then prove nothing about the closure. + if !declared.iter().any(|m| m == entry_module) { + return Err(MutationSubjectRefusal::EntryModuleNotDeclared { + entry_module: entry_module.to_string(), + declared, + }); + } + let path = crate_dir.join(format!("src/{entry_module}.rs")); + if !path.is_file() { + return Err(MutationSubjectRefusal::EntryModuleFileMissing { + entry_module: entry_module.to_string(), + path: path.display().to_string(), + }); + } + Ok(MutationSubject { + rust_module: entry_module.to_string(), + }) + } } pub fn mutation_subject_rust_module(subject: &MutationSubject) -> &str { - match subject { - MutationSubject::ClosureMember { rust_module } => rust_module.as_str(), - MutationSubject::EntryModule { rust_module } => rust_module.as_str(), - } + entry_own_subject::subject_rust_module(subject) +} + +/// The SUBJECT KIND, printed rather than inferred. There is one kind and the log still says it, +/// so a reader of a receipt line never has to know this file to know what was mutated -- and if +/// a second kind is ever admitted, every receipt already carries the field that distinguishes it. +pub fn mutation_subject_name(_subject: &MutationSubject) -> &'static str { + "EntryOwnModule" } -pub fn mutation_subject_name(subject: &MutationSubject) -> &'static str { - match subject { - MutationSubject::ClosureMember { .. } => "ClosureMember", - MutationSubject::EntryModule { .. } => "EntryModule", +/// WHY THE ENTRY'S OWN MODULE WAS NOT AVAILABLE TO CARRY THE FAULT. +/// +/// Three causes with three different owners, kept apart rather than collapsed into one string: +/// an unreadable manifest is a probe-crate defect, a module the manifest never declares is an +/// EMISSION defect (the closure did not carry its own root), and a declared module with no file +/// is a WRITE defect. Collapsing them would send a reader looking in the wrong place, which is +/// the state-space conflation DESIGN names. +/// +/// MEASURED AS REACHABLE-BUT-EMPTY, WHICH IS A HEALTHY QUIET GUARD AND NOT A DECORATION: all 8 +/// rostered entries emit their own module as its own `.rs`, so no arm here fires today. The +/// mechanism that produces it plainly exists -- a dropped or renamed root is what this phase is +/// for -- and a fixture authors every arm directly, so its RED is authorable (DESIGN 4b). +#[derive(Debug, Clone)] +pub enum MutationSubjectRefusal { + ClosureManifestUnreadable { + lib_rs: String, + detail: String, + }, + EntryModuleNotDeclared { + entry_module: String, + declared: Vec, + }, + EntryModuleFileMissing { + entry_module: String, + path: String, + }, +} + +pub fn mutation_subject_refusal_summary(refusal: &MutationSubjectRefusal) -> String { + match refusal { + MutationSubjectRefusal::ClosureManifestUnreadable { lib_rs, detail } => format!( + "EntryModuleAbsent/ClosureManifestUnreadable lib_rs={lib_rs} detail={detail} — the \ + emitted crate's own module list could not be read, so the entry's own module can \ + neither be found nor ruled out; nothing else may carry the fault in its place" + ), + MutationSubjectRefusal::EntryModuleNotDeclared { + entry_module, + declared, + } => format!( + "EntryModuleAbsent/EntryModuleNotDeclared entry_module={entry_module} \ + declared=[{}] — the emitted closure does not declare the entry's OWN module. That \ + is the reference-closed drop this phase exists to catch: the crate compiles because \ + every remaining member is a dependency of the missing root. Mutating a member \ + instead would report Discriminated over exactly the tree that is broken", + declared.join(",") + ), + MutationSubjectRefusal::EntryModuleFileMissing { entry_module, path } => format!( + "EntryModuleAbsent/EntryModuleFileMissing entry_module={entry_module} path={path} — \ + the closure declares the entry's own module and no file was written for it" + ), } } @@ -205,6 +346,12 @@ pub enum MutationVerdict { /// The fault went in and cargo still compiled the tree. THE INSTRUMENT IS NOT MEASURING /// WHAT IT CLAIMS TO. NotDiscriminating { detail: String }, + /// THE ENTRY'S OWN EMITTED MODULE WAS NOT THERE TO CARRY THE FAULT, and no other module + /// stood in for it. Its own arm rather than a `NotAttempted` reason string, because this is + /// not `the tree had nowhere to put a fault` -- it is a POSITIVE FINDING ABOUT THE EMISSION, + /// with a different owner and a different repair, and it is the exact case a silent fallback + /// to a shared member would have reported as `Discriminated`. + SubjectRefused { refusal: MutationSubjectRefusal }, /// The fault produced a red, and the restore did not return the tree to the state it /// started in -- either the bytes differ, or the restored tree does not compile. The red is /// then unattributable: it may be residue rather than the fault. @@ -227,6 +374,12 @@ pub fn mutation_verdict_summary(verdict: &MutationVerdict) -> String { format!("NotDiscriminating detail={detail}") } MutationVerdict::RestoreFailed { detail } => format!("RestoreFailed detail={detail}"), + MutationVerdict::SubjectRefused { refusal } => { + format!( + "SubjectRefused {}", + mutation_subject_refusal_summary(refusal) + ) + } MutationVerdict::Discriminated { subject, red_line } => format!( "Discriminated subject={} module={} red={red_line}", mutation_subject_name(subject), @@ -492,11 +645,15 @@ fn run_cargo(crate_dir: &Path, workspace: &Path) -> CargoVerdict { } /// The rust module basenames the emitted `lib.rs` declares, in its own order. -fn closure_modules(lib_rs: &Path) -> Vec { - let Ok(content) = std::fs::read_to_string(lib_rs) else { - return Vec::new(); - }; - content +/// +/// AN UNREADABLE MANIFEST IS RETURNED, NOT RENDERED AS AN EMPTY CLOSURE. The empty vector reads +/// identically to `this crate declares no modules`, and a caller asking whether the entry's own +/// module is declared would then answer `no` for a crate it never managed to read -- the +/// execution-provenance loss DESIGN names, in the one place it would misattribute an emission +/// defect to a filesystem one. +fn closure_modules(lib_rs: &Path) -> Result, String> { + let content = std::fs::read_to_string(lib_rs).map_err(|e| e.to_string())?; + Ok(content .lines() .filter_map(|line| { line.trim() @@ -504,27 +661,7 @@ fn closure_modules(lib_rs: &Path) -> Vec { .and_then(|rest| rest.strip_suffix(';')) .map(|m| m.trim().to_string()) }) - .collect() -} - -/// Pick the file the fault goes into: a closure member other than the entry where one exists, -/// the entry itself otherwise. -fn mutation_subject(crate_dir: &Path, entry_module: &str) -> Option { - let modules = closure_modules(&crate_dir.join("src/lib.rs")); - if let Some(member) = modules - .iter() - .find(|m| m.as_str() != entry_module && crate_dir.join(format!("src/{m}.rs")).is_file()) - { - return Some(MutationSubject::ClosureMember { - rust_module: member.clone(), - }); - } - if crate_dir.join(format!("src/{entry_module}.rs")).is_file() { - return Some(MutationSubject::EntryModule { - rust_module: entry_module.to_string(), - }); - } - None + .collect()) } /// THE DISCRIMINATING RED, ESTABLISHED BY MUTATION AND RESTORED BEFORE THE PHASE REPORTS. @@ -538,13 +675,12 @@ fn establish_discriminating_red( workspace: &Path, entry_module: &str, ) -> MutationVerdict { - let Some(subject) = mutation_subject(crate_dir, entry_module) else { - return MutationVerdict::NotAttempted { - reason: format!( - "no writable emitted module under {} to carry the fault", - crate_dir.display() - ), - }; + // NO FALLBACK ARM. A closure missing its own entry module is the finding, not an obstacle to + // route around -- substituting any other member here would hand the phase a `Discriminated` + // verdict computed over precisely the tree that is broken. + let subject = match mutation_subject(crate_dir, entry_module) { + Ok(subject) => subject, + Err(refusal) => return MutationVerdict::SubjectRefused { refusal }, }; let path = crate_dir.join(format!("src/{}.rs", mutation_subject_rust_module(&subject))); let original = match std::fs::read_to_string(&path) { @@ -1119,7 +1255,8 @@ pub fn run_required_emit_compile( /// ITSELF: `establish_discriminating_red` runs on every required run, and a mutation that fails /// to go red stops the line. What these add is the discrimination the in-run arm cannot perform /// on itself -- that a non-`Discriminated` mutation is a FAILURE rather than a note, and that -/// the fault prefers a closure member over the entry. +/// the fault targets the ENTRY'S OWN emitted module, with its absence refused rather than +/// substituted for. #[cfg(test)] mod tests { use super::*; @@ -1150,36 +1287,122 @@ mod tests { assert!(!manifest.contains("[lib]")); } - /// A closure member is preferred over the entry, because it is the stronger subject. - #[test] - fn mutation_prefers_a_closure_member_over_the_entry() { - let dir = std::env::temp_dir().join(format!("emit_compile_subject_{}", std::process::id())); + /// One emitted crate on disk, authored by the caller, so each test states the shape it means. + fn probe_tree(tag: &str, lib_rs: &str, files: &[&str]) -> PathBuf { + let dir = std::env::temp_dir().join(format!( + "emit_compile_{tag}_{}_{:?}", + std::process::id(), + std::thread::current().id() + )); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(dir.join("src")).expect("src"); - std::fs::write( - dir.join("src/lib.rs"), - "pub mod std_logic;\npub mod v2_std_node;\n", - ) - .expect("lib"); - std::fs::write(dir.join("src/std_logic.rs"), "// member\n").expect("member"); - std::fs::write(dir.join("src/v2_std_node.rs"), "// entry\n").expect("entry"); - let subject = mutation_subject(&dir, "v2_std_node").expect("a subject"); - assert!(matches!(subject, MutationSubject::ClosureMember { .. })); + std::fs::write(dir.join("src/lib.rs"), lib_rs).expect("lib"); + for file in files { + std::fs::write(dir.join(format!("src/{file}.rs")), "// emitted\n").expect("member"); + } + dir + } + + /// THE SUBJECT IS THE ENTRY'S OWN MODULE EVEN WHEN A SHARED MEMBER IS DECLARED FIRST. + /// + /// This is the measured shape of 7 of the 8 rostered entries: a shared-core member first, the + /// entry second-to-last, the emitted runtime last. The old selector took the first member that + /// was not the entry and therefore mutated `std_error_primitives` here -- a verdict about the + /// shared core wearing this entry's name. + #[test] + fn the_subject_is_the_entry_own_module_past_a_leading_shared_member() { + let dir = probe_tree( + "shared_first", + "pub mod std_error_primitives;\npub mod v2_std_node;\npub mod v1_rt;\n", + &["std_error_primitives", "v2_std_node", "v1_rt"], + ); + let subject = mutation_subject(&dir, "v2_std_node").expect("the entry's own module"); + assert_eq!(mutation_subject_rust_module(&subject), "v2_std_node"); + assert_eq!(mutation_subject_name(&subject), "EntryOwnModule"); + let _ = std::fs::remove_dir_all(&dir); + } + + /// AND WHEN IT IS DECLARED FIRST, which is the other measured shape (2 of 8: `std_abi`, + /// `std_logic`). Stated as its own case because ORDER IS NOT THE MECHANISM in either + /// direction: the tempting repair -- mutate the LAST module -- picks `v1_rt`, the emitted + /// runtime, in all 8, which is strictly more shared than what it replaced. + #[test] + fn the_subject_is_the_entry_own_module_when_it_is_declared_first() { + let dir = probe_tree( + "entry_first", + "pub mod std_logic;\npub mod v1_rt;\n", + &["std_logic", "v1_rt"], + ); + let subject = mutation_subject(&dir, "std_logic").expect("the entry's own module"); assert_eq!(mutation_subject_rust_module(&subject), "std_logic"); let _ = std::fs::remove_dir_all(&dir); } - /// A closure whose only member is the entry falls back to the entry AND SAYS SO, so the - /// weaker measurement is legible as the weaker one. + /// THE ABSENT ENTRY MODULE REFUSES AND NAMES THE ENTRY -- IT DOES NOT FALL BACK. + /// + /// This is the whole point of the change, and it is the case a fallback would have reported + /// as `Discriminated`: the closure has lost its own root and still compiles, because every + /// remaining member is a dependency of the missing root and nothing references it back. + #[test] + fn an_entry_module_missing_from_the_closure_refuses_rather_than_substituting() { + let dir = probe_tree( + "entry_dropped", + "pub mod std_error_primitives;\npub mod v1_rt;\n", + &["std_error_primitives", "v1_rt"], + ); + let refusal = mutation_subject(&dir, "v2_std_node").expect_err("no substitution"); + match &refusal { + MutationSubjectRefusal::EntryModuleNotDeclared { + entry_module, + declared, + } => { + assert_eq!(entry_module, "v2_std_node"); + assert_eq!(declared, &["std_error_primitives", "v1_rt"]); + } + other => panic!("wrong refusal: {other:?}"), + } + let summary = mutation_subject_refusal_summary(&refusal); + assert!(summary.contains("v2_std_node"), "{summary}"); + // The members that WERE available are named, so a reader can see what a fallback would + // have chosen and that nothing chose it. + assert!(summary.contains("std_error_primitives"), "{summary}"); + let _ = std::fs::remove_dir_all(&dir); + } + + /// DECLARED AND WRITTEN ARE TWO FACTS. A `pub mod` line with no file behind it is a write + /// defect, not an emission one, and it gets its own refusal for that reason. + #[test] + fn an_entry_module_declared_without_a_file_refuses_as_a_write_defect() { + let dir = probe_tree( + "entry_unwritten", + "pub mod v2_std_node;\npub mod v1_rt;\n", + &["v1_rt"], + ); + let refusal = mutation_subject(&dir, "v2_std_node").expect_err("no substitution"); + assert!(matches!( + refusal, + MutationSubjectRefusal::EntryModuleFileMissing { .. } + )); + let _ = std::fs::remove_dir_all(&dir); + } + + /// AN UNREADABLE MANIFEST IS NOT AN EMPTY CLOSURE. Rendering it as one would report the + /// EMISSION arm -- `the entry's own module is not declared` -- for a crate nobody managed to + /// read, sending the reader to the emitter over a filesystem fault. #[test] - fn mutation_falls_back_to_the_entry_and_names_it() { - let dir = std::env::temp_dir().join(format!("emit_compile_solo_{}", std::process::id())); + fn an_unreadable_closure_manifest_refuses_on_its_own_cause() { + let dir = std::env::temp_dir().join(format!( + "emit_compile_no_manifest_{}_{:?}", + std::process::id(), + std::thread::current().id() + )); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(dir.join("src")).expect("src"); - std::fs::write(dir.join("src/lib.rs"), "pub mod v2_std_node;\n").expect("lib"); - std::fs::write(dir.join("src/v2_std_node.rs"), "// entry\n").expect("entry"); - let subject = mutation_subject(&dir, "v2_std_node").expect("a subject"); - assert!(matches!(subject, MutationSubject::EntryModule { .. })); + let refusal = mutation_subject(&dir, "v2_std_node").expect_err("no manifest, no subject"); + assert!(matches!( + refusal, + MutationSubjectRefusal::ClosureManifestUnreadable { .. } + )); let _ = std::fs::remove_dir_all(&dir); } @@ -1272,6 +1495,15 @@ mod tests { MutationVerdict::RestoreFailed { detail: "d".to_string(), }, + // THE REFUSAL IS A PHASE FAILURE, not a note beside a green baseline. A closure that + // lost its own entry module is exactly the emission defect this phase exists to + // catch, so an entry reaching this arm must never be reported as measured. + MutationVerdict::SubjectRefused { + refusal: MutationSubjectRefusal::EntryModuleNotDeclared { + entry_module: "v2_std_node".to_string(), + declared: vec!["v1_rt".to_string()], + }, + }, ] { let outcome = EmitCompileOutcome::Measured { entry: "e.dag".to_string(), @@ -1286,18 +1518,27 @@ mod tests { emit_compile_outcome_summary(&outcome) ); } + // THE SUBJECT IS OBTAINED THE ONLY WAY IT CAN BE: through `mutation_subject`, over a + // real tree. An earlier revision of this test wrote `MutationSubject { rust_module: .. }` + // directly, and moving the carrier behind a privacy boundary turned that line into a + // COMPILE ERROR (`E0451: field rust_module is private`) rather than a comment nobody + // reads. That refusal is the executed evidence that the wall is structural inside this + // file and not merely conventional -- Rust privacy is module-scoped, so a private field + // declared beside its constructor would have left this literal compiling. + let dir = probe_tree("passing_subject", "pub mod std_logic;\n", &["std_logic"]); + let subject = mutation_subject(&dir, "std_logic").expect("the entry's own module"); let discriminated = EmitCompileOutcome::Measured { entry: "e.dag".to_string(), crate_dir: "/tmp/x".to_string(), emitted_files: 1, baseline: green, mutation: MutationVerdict::Discriminated { - subject: MutationSubject::ClosureMember { - rust_module: "std_logic".to_string(), - }, + subject, red_line: "error[E0308]".to_string(), }, }; assert!(emit_compile_outcome_passed(&discriminated)); + assert!(emit_compile_outcome_summary(&discriminated).contains("subject=EntryOwnModule")); + let _ = std::fs::remove_dir_all(&dir); } }