diff --git a/dag/test/claim/required_regen_admission_witness_test.dag b/dag/test/claim/required_regen_admission_witness_test.dag index 7ee2486230e..d677e20b69e 100644 --- a/dag/test/claim/required_regen_admission_witness_test.dag +++ b/dag/test/claim/required_regen_admission_witness_test.dag @@ -3,6 +3,8 @@ module test.claim.required_regen_admission_witness import v2.workflow.required_regen { CommittedNotEmitted, EmittedNotCommitted, + EmittedNotCommittedShadowsHandMaintained, + MirrorMissingShadowsHandMaintainedSource, EmptyCommittedPopulation, EmptyEmitResult, CommittedMirrorNoLongerEmitted, @@ -198,6 +200,53 @@ test fn witness_new_surface_alone_still_refuses() -> Bool { } } +// THE DAMAGING-REMEDY CONTROL, and it has two halves that must hold TOGETHER. +// An emitted basename that collides with hand-authored source under a hand-maintained directory +// refuses under its OWN cause, naming the directory -- because the two remedies attached to +// MirrorMissingForEmittedSurface (install the mirror, or investigate an invented surface) both +// destroy hand-authored code when applied to this class. +// The RED half is the second assertion: a genuine orphan mirror in the SAME run must STILL refuse +// as MirrorMissingForEmittedSurface. A fix that silenced the collision by excluding it from the +// compared population would take the orphan with it, and this test would go green on a wall that +// no longer stands. Separation, not membership: neither path may appear under the other's cause. +test fn witness_hand_maintained_collision_refuses_under_its_own_remedy() -> Bool { + match required_regen_sync_admission( + generated_outcomes: [ + SurfaceMatches { relative_path: "std_algebra.rs" }, + EmittedNotCommitted { relative_path: "brand_new_surface.rs" }, + EmittedNotCommittedShadowsHandMaintained { + relative_path: "shared_fill.rs", + hand_maintained_homes: ["cli_run"] + }, + EmittedNotCommittedShadowsHandMaintained { + relative_path: "mod.rs", + hand_maintained_homes: ["cli_run", "module_path_index"] + } + ], + hand_outcomes: [], + authority_digest: "fnv1a64:deadbeefdeadbeef", + planned_surfaces: 4, + executed_surfaces: 4 + ) { + RequiredRegenSyncRefused { refusals } => + (refusals |> any(r => match r { + MirrorMissingShadowsHandMaintainedSource { entries } => + (entries |> any(e => e == "shared_fill.rs (hand-maintained under cli_run)")) + && (entries |> any(e => + e == "mod.rs (hand-maintained under cli_run, module_path_index)")) + && !(entries |> any(e => e == "brand_new_surface.rs")) + _ => false + })) + && (refusals |> any(r => match r { + MirrorMissingForEmittedSurface { paths } => + (paths |> any(p => p == "brand_new_surface.rs")) + && !(paths |> any(p => p == "shared_fill.rs")) + _ => false + })) + RequiredRegenSyncRunnable { attempt: _ } => false + } +} + test fn witness_hand_unverifiable_refuses_naming_the_entry() -> Bool { match required_regen_sync_admission( generated_outcomes: [SurfaceMatches { relative_path: "std_algebra.rs" }], diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index d9335216399..61a16a790f1 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -1,7 +1,7 @@ //! Host realization for `v2.workflow.required_regen` — committed seed vs fresh emit. use serde::Serialize; -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fs; use std::io::Write; use std::path::Path; @@ -505,7 +505,10 @@ pub fn run_required_regen_fixed_point( return Err("refusal: fixed-point emit produced zero files".to_string()); } let emitted_basenames = generated_basenames_from_emit(&emitted); - if let Some(reason) = validate_compared_populations(&committed_basenames, &emitted_basenames) { + let hand_dir_shadows = hand_maintained_dir_shadows(&workspace.join("src/v1/stage0/src"))?; + if let Some(reason) = + validate_compared_populations(&committed_basenames, &emitted_basenames, &hand_dir_shadows) + { return Err(reason); } let pass2 = tree_digest_from_map(&emitted, &committed_basenames)?; @@ -629,7 +632,10 @@ fn adjudicate_generated_surface( emitted_basenames: &[String], ) -> Result { let committed = committed_generated_basenames(stage0_src)?; - if let Some(reason) = validate_compared_populations(&committed, emitted_basenames) { + let hand_dir_shadows = hand_maintained_dir_shadows(stage0_src)?; + if let Some(reason) = + validate_compared_populations(&committed, emitted_basenames, &hand_dir_shadows) + { return Ok(GeneratedSurfaceAdjudicated::Refused { reason }); } let sync = compare_generated_surfaces(stage0_src, emitted, &committed)?; @@ -841,7 +847,82 @@ fn committed_generated_basenames(stage0_src: &Path) -> Result, Strin Ok(names.into_iter().collect()) } -fn validate_compared_populations(committed: &[String], emitted: &[String]) -> Option { +/// The only seam where `HAND_MAINTAINED_STAGE0_DIRS` reaches the population comparison, and it +/// CLASSIFIES a refusal rather than excluding anything from one. +/// +/// Both compared populations key on basename, and the committed walk enumerates only the top level +/// of the stage0 crate, so a file living inside a hand-maintained DIRECTORY is invisible to it. An +/// emitted basename equal to one of those files therefore lands in `emitted_not_committed`, where +/// the two remedies on offer -- install the produced mirror, or investigate an emitter that +/// invented a surface -- BOTH destroy hand-authored code: installing overwrites it with generated +/// bytes, deleting removes it. This map lets that population be named under its own cause with the +/// remedy that actually applies (de-collide the module name). +/// +/// IT DOES NOT EXCLUDE, AND THAT IS THE WHOLE DESIGN. Teaching `is_hand_maintained_path` about the +/// directory list would drop the colliding path out of the compared population entirely, which +/// silently stops comparing a genuinely generated surface -- a wrong refusal traded for no +/// refusal. Same failure as making the committed walk descend, entered from the other side. +/// +/// ZERO EXPOSURE, stated so this is not read as repairing a live defect. Emitted Rust filenames are +/// flat by construction (`v1.compiler.emit_core_support` `module_to_filename` is split(".") joined +/// with "_" under `rust_source_root()`), so the emitter cannot write into a hand-maintained +/// directory; the collision is reachable only by authoring a module whose bare name equals one of +/// those files. Measured 2026-08-22: zero collisions in the corpus, required-regen green on main +/// at 90986d19469, and no upstream guard was found that would refuse such a module name (the +/// nearest candidates, `gunbc.stage0_rust_source_lifecycle_scaffold` +/// `classified_residue_disjoint_holds` and the generated/hand disposition joins beside it, are +/// scoped to top-level stage0 paths and cannot see a subdirectory file). +fn hand_maintained_dir_shadows(stage0_src: &Path) -> Result>, String> { + let mut shadows = BTreeMap::new(); + for dir_name in HAND_MAINTAINED_STAGE0_DIRS { + let dir = stage0_src.join(dir_name); + if !dir.is_dir() { + continue; + } + collect_dir_shadows(&dir, dir_name, &mut shadows)?; + } + // Sorted so the refusal text is a function of the collision, not of the roster's authoring + // order: two clones must print the same remedy for the same tree. + for homes in shadows.values_mut() { + homes.sort(); + } + Ok(shadows) +} + +fn collect_dir_shadows( + dir: &Path, + home: &str, + shadows: &mut BTreeMap>, +) -> Result<(), String> { + for entry in fs::read_dir(dir).map_err(|e| format!("read dir {}: {e}", dir.display()))? { + let entry = entry.map_err(|e| format!("read dir entry under {}: {e}", dir.display()))?; + let path = entry.path(); + if path.is_dir() { + collect_dir_shadows(&path, home, shadows)?; + continue; + } + if let Some(basename) = path.file_name().and_then(|n| n.to_str()) { + if is_compared_generated_basename(basename) { + // EVERY HOME, NOT THE LAST ONE SEEN. `insert` here was last-write-wins, so one + // basename hand-authored under two hand-maintained directories -- `mod.rs` being + // the obvious candidate -- refused while naming only one of them, sending the + // author to the wrong file. Authority for the list shape: + // `v2.workflow.required_regen` `EmittedNotCommittedShadowsHandMaintained`. + let homes = shadows.entry(basename.to_string()).or_default(); + if !homes.iter().any(|h| h == home) { + homes.push(home.to_string()); + } + } + } + } + Ok(()) +} + +fn validate_compared_populations( + committed: &[String], + emitted: &[String], + hand_dir_shadows: &BTreeMap>, +) -> Option { if committed.is_empty() { return Some("refusal: committed generated population is empty".to_string()); } @@ -851,9 +932,17 @@ fn validate_compared_populations(committed: &[String], emitted: &[String]) -> Op let committed_set: BTreeSet<&str> = committed.iter().map(String::as_str).collect(); let emitted_set: BTreeSet<&str> = emitted.iter().map(String::as_str).collect(); let mut emitted_not_committed = Vec::new(); + let mut shadowing_hand_maintained = Vec::new(); for name in emitted { - if !committed_set.contains(name.as_str()) { - emitted_not_committed.push(name.clone()); + if committed_set.contains(name.as_str()) { + continue; + } + match hand_dir_shadows.get(name.as_str()) { + Some(homes) => shadowing_hand_maintained.push(format!( + "{name} (hand-maintained under {})", + homes.join(", ") + )), + None => emitted_not_committed.push(name.clone()), } } let mut committed_not_emitted = Vec::new(); @@ -885,6 +974,15 @@ fn validate_compared_populations(committed: &[String], emitted: &[String]) -> Op "refusal: committed mirror is no longer emitted — {committed_not_emitted:?}; the emitter stopped producing these surfaces, so either the authority that emitted them was removed on purpose (delete the committed mirror) or it regressed (restore it) — do NOT install anything for this class" )); } + if !shadowing_hand_maintained.is_empty() { + // A THIRD CAUSE BECAUSE THE OTHER TWO REMEDIES DAMAGE THIS CLASS. Authority: + // `v2.workflow.required_regen` `MirrorMissingShadowsHandMaintainedSource`. The refusal is + // not softer than the one above -- the line still stops -- it is the same stop pointed at + // the move that actually resolves the state. + reasons.push(format!( + "refusal: emitted surface collides with hand-maintained source — {shadowing_hand_maintained:?}; the emitted basename addresses a file that is hand-authored under a hand-maintained directory, so BOTH remedies for a missing mirror would damage it (installing overwrites hand-authored code with generated bytes; deleting destroys it). Rename the emitting module so its basename no longer collides, or move the hand-maintained file — do NOT install anything for this class" + )); + } if reasons.is_empty() { None } else { diff --git a/src/v2/workflow/required_regen.dag b/src/v2/workflow/required_regen.dag index dece2e6103a..7d043205fe0 100644 --- a/src/v2/workflow/required_regen.dag +++ b/src/v2/workflow/required_regen.dag @@ -37,6 +37,10 @@ type RegenSurfaceOutcome } | EmittedNotCommitted { relative_path: String } | CommittedNotEmitted { relative_path: String } + | EmittedNotCommittedShadowsHandMaintained { + relative_path: String + hand_maintained_homes: List + } type HandMaintainedOutcome = HandMatches { filename: String } @@ -68,9 +72,36 @@ type HandMaintainedOutcome // the repair before it buys is that the first branch is now ACTIONABLE — the bytes exist on // disk instead of only inside the process refusing — and the second branch stays refused, which // is the arm that matters. +// A FOURTH POPULATION STATE, SPLIT OUT OF THE THIRD FOR THE SAME REASON THE THIRD WAS SPLIT OUT +// OF THE SECOND -- and this one is about the REMEDY, not the diagnosis. +// `MirrorMissingForEmittedSurface` offers exactly two moves: install the produced mirror, or +// investigate an emitter that invented a surface. Both are correct for a genuine orphan and both +// DAMAGE hand-authored source. The compared populations key on BASENAME, and the committed side +// enumerates only the top level of the stage0 crate, so an emitted basename that collides with a +// file living inside a hand-maintained DIRECTORY (`HAND_MAINTAINED_STAGE0_DIRS`) lands in the +// emitted-not-committed set with those two remedies attached: installing overwrites hand-authored +// code with generated bytes, deleting destroys it. The only non-damaging response left to an +// author who reads the refusal and believes it is to ignore the wall -- which is how a correct +// wall decays into advice, fastest for the authors who take it most seriously. +// +// SO THE COLLISION REFUSES UNDER ITS OWN NAME AND ITS OWN REMEDY (de-collide the module name), +// AND IT IS NOT SILENCED. Excluding the colliding path from the compared population instead -- +// by teaching the hand-maintained predicate about the directory list -- would drop a genuinely +// generated surface out of the comparison entirely, which is the false green this split exists +// to avoid: a wrong refusal replaced by no refusal. +// +// ZERO EXPOSURE TODAY, AND THE REASON IS WORTH RECORDING because it is what bounds this variant. +// Emitted Rust filenames are FLAT by construction (`v1.compiler.emit_core_support` +// `module_to_filename` is split(".") |> join("_") under `rust_source_root()`), so the emitter can +// never write INTO a hand-maintained directory; the collision is reachable only through a module +// authored with a bare name equal to one of those directories' files. Measured 2026-08-22: zero +// such collisions in the corpus, and required-regen green on main at 90986d19469 +// (first_generation_equal=true planned=132 executed=132). The variant is a wall for a state one +// .dag file away, not a repair of an observed failure. type RequiredRegenRefusal = GeneratedDrift { paths: List } | MirrorMissingForEmittedSurface { paths: List } + | MirrorMissingShadowsHandMaintainedSource { entries: List } | CommittedMirrorNoLongerEmitted { paths: List } | CompileRefused { reason: String } | HandUnverifiableRefused { entries: List } @@ -120,6 +151,7 @@ fn collect_byte_drift_paths( SurfaceDrifted { relative_path: path } => Cons { head: path, tail: acc } EmittedNotCommitted { relative_path: _ } => acc CommittedNotEmitted { relative_path: _ } => acc + EmittedNotCommittedShadowsHandMaintained { relative_path: _, hand_maintained_homes: _ } => acc SurfaceMatches { relative_path: _ } => acc } } @@ -132,6 +164,7 @@ fn collect_emitted_not_committed_paths( SurfaceDrifted { relative_path: _ } => acc EmittedNotCommitted { relative_path: path } => Cons { head: path, tail: acc } CommittedNotEmitted { relative_path: _ } => acc + EmittedNotCommittedShadowsHandMaintained { relative_path: _, hand_maintained_homes: _ } => acc SurfaceMatches { relative_path: _ } => acc } } @@ -144,6 +177,7 @@ fn collect_committed_not_emitted_paths( SurfaceDrifted { relative_path: _ } => acc EmittedNotCommitted { relative_path: _ } => acc CommittedNotEmitted { relative_path: path } => Cons { head: path, tail: acc } + EmittedNotCommittedShadowsHandMaintained { relative_path: _, hand_maintained_homes: _ } => acc SurfaceMatches { relative_path: _ } => acc } } @@ -157,6 +191,54 @@ fn hand_is_unverifiable(o: HandMaintainedOutcome) -> Bool { } } +// EVERY HOME, NOT THE LAST ONE SEEN. The homes are a LIST because one basename can be +// hand-authored under more than one hand-maintained directory — `mod.rs` is the obvious +// candidate, being the name most likely to appear in two module directories at once. A carrier +// holding a single home would make the extra ones unrepresentable, so the host's map would have +// resolved the collision by last-write-wins and the refusal would name ONE directory while +// silently dropping the others. That refusal still fires and still launders nothing, but it +// sends an author to the wrong file — which is the failure this whole variant exists to prevent, +// one notch quieter (finding: smart-ram-730 on #8867). +// +// The entry renders as "path (hand-maintained under A, B)" so the refusal names both halves of +// the collision: an author cannot act on the basename alone, because the whole point is that ONE +// basename addresses several different files. +fn render_homes(acc: String, home: String) -> String { + if acc == "" { + home + } else { + concat(acc, concat(", ", home)) + } +} + +fn collect_shadowing_entries( + acc: List, + o: RegenSurfaceOutcome +) -> List { + match o { + SurfaceDrifted { relative_path: _ } => acc + EmittedNotCommitted { relative_path: _ } => acc + CommittedNotEmitted { relative_path: _ } => acc + EmittedNotCommittedShadowsHandMaintained { + relative_path: path, + hand_maintained_homes: homes + } => Cons { + head: concat( + path, + concat( + " (hand-maintained under ", + concat( + fold_list(xs: homes, empty: "", cons: fn(a, h) { render_homes(acc: a, home: h) }), + ")" + ) + ) + ), + tail: acc + } + SurfaceMatches { relative_path: _ } => acc + } +} + fn collect_hand_unverifiable( acc: List, o: HandMaintainedOutcome @@ -213,6 +295,11 @@ fn required_regen_sync_admission( empty: no_string_paths, cons: fn(acc, o) { collect_emitted_not_committed_paths(acc: acc, o: o) } ) + let shadowing_entries = fold_list( + xs: generated_outcomes, + empty: no_string_paths, + cons: fn(acc, o) { collect_shadowing_entries(acc: acc, o: o) } + ) let unemitted_mirror_paths = fold_list( xs: generated_outcomes, empty: no_string_paths, @@ -231,11 +318,18 @@ fn required_regen_sync_admission( no_regen_refusals } ), - right: if length(xs: unemitted_mirror_paths) > 0 { - [CommittedMirrorNoLongerEmitted { paths: unemitted_mirror_paths }] - } else { - no_regen_refusals - } + right: list_append( + left: if length(xs: unemitted_mirror_paths) > 0 { + [CommittedMirrorNoLongerEmitted { paths: unemitted_mirror_paths }] + } else { + no_regen_refusals + }, + right: if length(xs: shadowing_entries) > 0 { + [MirrorMissingShadowsHandMaintainedSource { entries: shadowing_entries }] + } else { + no_regen_refusals + } + ) ) if length(xs: population_refusals) > 0 { return RequiredRegenSyncRefused { refusals: population_refusals }