From fb40194a47a528768076d4693af82a5e42b5827b Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 18:08:29 +0000 Subject: [PATCH 01/24] Closure front end reads its lexical artifact from pool_acquire The via-index parse and the parse-cache miss arm re-lexed every closure file the pool census had already lexed under the same spelling. Both now ask pool_acquire. Adds the per-term attribution probe and a live identity differential (pooled vs fresh artifact over the whole pool). Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/cli_run/entry_resolve.rs | 130 +++++++++++++++++++-- src/v1/stage0/src/cli_run/pool_acquire.rs | 4 + 2 files changed, 122 insertions(+), 12 deletions(-) diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index a15123a69a8..d15db1d7279 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -1528,12 +1528,10 @@ pub(crate) fn via_index_parse_one_source( // captures and admits them against this file's occurrence transport. // Annotation-erasing `tokenize` here let a touched in-closure file // compile on the floor while missing the class #8204 claims to close. - let artifact = v1_compiler_tokenize::tokenize_artifact( - source.content.clone(), - source.path.clone(), - crate::extdeps_languages_dag_syntax::dag_parse_environment(), - ); - let nl_index = build_newline_index(source.path.clone(), source.content.clone()); + // One acquisition, not one per walk -- see `cli_run::pool_acquire`. The pool census already + // tokenized these bytes under this spelling; the artifact keeps the annotation channel. + let artifact = super::pool_acquire::artifact_for(&source.path, &source.content); + let nl_index = super::pool_acquire::newline_index_for(&source.path, &source.content); let current_table = index.intern_table.borrow().clone(); let single_si: Rc>> = Rc::new({ let mut m = HashMap::new(); @@ -1956,12 +1954,9 @@ pub(crate) fn parse_module_node_from_index_source( let (parse_result, nl_index) = match cached { Some(entry) => (entry.parse_result, entry.newline_index), None => { - let tokens = v1_compiler_tokenize::tokenize( - source.content.clone(), - source.path.clone(), - crate::extdeps_languages_dag_syntax::dag_parse_environment(), - ); - let nl_index = build_newline_index(source.path.clone(), source.content.clone()); + // One acquisition, not one per walk -- see `cli_run::pool_acquire`. + let tokens = super::pool_acquire::tokens_for(&source.path, &source.content); + let nl_index = super::pool_acquire::newline_index_for(&source.path, &source.content); let current_table = index.intern_table.borrow().clone(); let single_si: Rc>> = Rc::new({ let mut m = HashMap::new(); @@ -3319,3 +3314,114 @@ mod live_pool_thread_tests { assert_eq!(on_live_pool_thread(|| 7), 7); } } + +/// THE INSTRUMENT for a cold entry resolve's per-term cost on the live `[dag, src/v2]` pool: one +/// fresh process acquires the pool's tokens, builds the module path index (the heads reading +/// `parse_module_binding` takes), builds the shared index, and resolves two small workflow +/// entries, printing `PROBE` rows and every `pre_entry_phase` term. The first two terms are split +/// out so the per-file token acquisition, which later readings reuse through `pool_acquire`, is +/// not charged to whichever walk happens to run first. It reports; it asserts only that the +/// entries resolve. Run it with +/// `cargo test --release -p v1-compiler --lib live_pool_entry_resolve_attribution -- --ignored --nocapture`. +#[cfg(test)] +mod live_pool_entry_resolve_attribution { + use super::*; + #[test] + #[ignore = "live-corpus: prepares or builds over the live tree (minutes per test); the receipts lane runs these with --ignored, the required unit run does not"] + fn live_pool_entry_resolve_attribution() { + let t0 = std::time::Instant::now(); + let root = process_workspace_root(); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| root.join(r).to_string_lossy().into_owned()) + .collect(); + let t = std::time::Instant::now(); + let mut files = 0usize; + for r in &roots { + let mut dag_files = Vec::new(); + collect_dag_files_tolerant(Path::new(r), &mut dag_files); + for f in dag_files { + let content = std::fs::read_to_string(&f).expect("read pool file"); + // The spelling `parse_module_binding` acquires under (`source_key`). + let key = f + .strip_prefix(&root) + .unwrap_or(&f) + .to_string_lossy() + .into_owned(); + let _ = super::pool_acquire::tokens_for(&key, &content); + files += 1; + } + } + eprintln!( + "PROBE pool token acquisition {:?} files={files}", + t.elapsed() + ); + let t = std::time::Instant::now(); + let n = build_module_path_index(&pool_roots_for_module_graph_closure(&roots)).len(); + eprintln!( + "PROBE module_path_index (heads parse) {:?} modules={n}", + t.elapsed() + ); + let t = std::time::Instant::now(); + let index = process_shared_index(&roots); + eprintln!("PROBE shared_index {:?}", t.elapsed()); + for e in [ + "src/v2/workflow/regen_convergence_transaction.dag", + "src/v2/workflow/required_regen.dag", + ] { + let entry = root.join(e); + let t = std::time::Instant::now(); + let r = resolve_entry_with_index_for_discovery_corpus(&index, &entry.to_string_lossy()); + assert!(r.is_ok(), "{e} resolves"); + eprintln!("PROBE resolve {e} {:?}", t.elapsed()); + for line in super::pre_entry_phase::take_lines() { + eprintln!("PROBE phase {line}"); + } + } + eprintln!("PROBE total {:?}", t0.elapsed()); + } +} + +/// THE IDENTITY DIFFERENTIAL for the closure front end reading its lexical artifact from +/// `pool_acquire` instead of re-lexing: the only input that change alters is the artifact the +/// closure parser receives, so it is compared at that grain over every file of the live +/// `[dag, src/v2]` pool, under the spelling the shared index gives it -- tokens AND the annotation +/// channel, against a fresh `tokenize_artifact` of the same bytes. A divergence names the file. +#[cfg(test)] +mod closure_parse_acquisition_differential { + use super::*; + #[test] + #[ignore = "live-corpus: prepares or builds over the live tree (minutes per test); the receipts lane runs these with --ignored, the required unit run does not"] + fn pooled_closure_artifacts_equal_fresh_lexing_on_the_live_pool() { + let root = process_workspace_root(); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| root.join(r).to_string_lossy().into_owned()) + .collect(); + let index = process_shared_index(&roots); + let mut compared = 0usize; + let mut divergent: Vec = Vec::new(); + for source in index.source_files.values() { + let pooled = super::pool_acquire::artifact_for(&source.path, &source.content); + let fresh = v1_compiler_tokenize::tokenize_artifact( + source.content.clone(), + source.path.clone(), + crate::extdeps_languages_dag_syntax::dag_parse_environment(), + ); + if *pooled != *fresh { + divergent.push(source.path.clone()); + } + let pooled_nl = super::pool_acquire::newline_index_for(&source.path, &source.content); + if *pooled_nl != *build_newline_index(source.path.clone(), source.content.clone()) { + divergent.push(format!("{} (newline index)", source.path)); + } + compared += 1; + } + eprintln!("DIFF compared={compared} divergent={}", divergent.len()); + assert!(compared > 1000, "the live pool was read ({compared} files)"); + assert!( + divergent.is_empty(), + "pooled artifacts diverge: {divergent:?}" + ); + } +} diff --git a/src/v1/stage0/src/cli_run/pool_acquire.rs b/src/v1/stage0/src/cli_run/pool_acquire.rs index dd9935b2fed..ddb82d764c7 100644 --- a/src/v1/stage0/src/cli_run/pool_acquire.rs +++ b/src/v1/stage0/src/cli_run/pool_acquire.rs @@ -7,6 +7,10 @@ //! was tokenized 12,121 times: measured `distinct_file_spellings=3031`, of which 3,030 were //! tokenized exactly 4x. //! +//! The closure front end (`entry_resolve::via_index_parse_one_source` and the parse-cache miss +//! arm) is a fifth reader of the same bytes under the same spelling, so it asks here too rather +//! than re-lexing every closure file the pool census already lexed. +//! //! That multiplicity is the whole content of this module. Lexing a file is a PURE FUNCTION of its //! bytes and the file spelling those bytes are reported under -- there is one right answer, so //! there is one authority for it, and the walks ask rather than each recompute (DESIGN §2: From a9cb98a2164a3079bc7621d67ec1486c1abea3a0 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 19:02:34 +0000 Subject: [PATCH 02/24] One heads reading per file: the pool census projects it instead of re-parsing pool_acquire holds one file-local heads reading (empty intern table, occurrence ordinals from zero). module_path_index reads it as-is; the pool census maps it into its threaded intern/occurrence space by a total projection (occurrence ids offset by the entry base, idents relabeled through the file's string list) instead of parsing the file a second time. Anything the parser does not produce refuses. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/cli_run.rs | 19 +- src/v1/stage0/src/cli_run/census_heads.rs | 250 +++++++++++++++++++ src/v1/stage0/src/cli_run/entry_resolve.rs | 35 +++ src/v1/stage0/src/cli_run/pool_acquire.rs | 29 +++ src/v1/stage0/src/module_path_index/index.rs | 15 +- 5 files changed, 328 insertions(+), 20 deletions(-) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index fbca69210ca..08ea3aec6ba 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -17257,14 +17257,8 @@ fn parse_module_heads_for_pool_census( ) -> Result<(Rc, Rc), String> { note_source_hash(index, &source); // One acquisition, not one per walk -- see `cli_run::pool_acquire`. - let tokens = pool_acquire::tokens_for(&source.path, &source.content); let nl_index = pool_acquire::newline_index_for(&source.path, &source.content); let current_table = index.intern_table.borrow().clone(); - let single_si: Rc>> = Rc::new({ - let mut m = HashMap::new(); - m.insert(source.path.clone(), nl_index.clone()); - m - }); // The HEADS reading of the grammar, not the full one. Every declaration head is // parsed by the same productions; brace-delimited fn bodies and data initializer // values are skipped at token grain instead of being built, because @@ -17280,11 +17274,16 @@ fn parse_module_heads_for_pool_census( // can depend on, because the normalizer overwrites it — so the heads reading cannot // drift from the full reading through the body slot, only through the heads, which is // the surface the differential receipt measures. - let parsed = v1_compiler_parse::parse_heads_with_table(tokens, single_si, current_table); - *index.intern_table.borrow_mut() = parsed.intern_table.clone(); + // + // ONE HEADS READING PER FILE: the file-local reading `module_path_index` already took is + // PROJECTED into this pool's threaded intern/occurrence space (`census_heads:: + // project_heads_reading`, a total map), not parsed a second time. + let local = pool_acquire::heads_reading_for(&source.path, &source.content); + let (result, table) = census_heads::project_heads_reading(&local, ¤t_table)?; + *index.intern_table.borrow_mut() = table; // Pool census needs declaration heads only — do NOT install full-body ASTs into // `parse_cache` here. Closure resolve retains full bodies on its own cache miss. - if let Some(err) = &parsed.result.error { + if let Some(err) = &result.error { let span = diagnostic_to_span(err.diagnostic.clone()); let loc = format_error_loc(&span.file, span.start, &Rc::new(HashMap::new())); return Err(format!( @@ -17293,7 +17292,7 @@ fn parse_module_heads_for_pool_census( diagnostic_to_message(err.diagnostic.clone()) )); } - match &parsed.result.module { + match &result.module { Some(module) => Ok(( v1_compiler_compile::census_heads_module_node(module.clone()), nl_index, diff --git a/src/v1/stage0/src/cli_run/census_heads.rs b/src/v1/stage0/src/cli_run/census_heads.rs index 75844b60143..2f77aa7ef84 100644 --- a/src/v1/stage0/src/cli_run/census_heads.rs +++ b/src/v1/stage0/src/cli_run/census_heads.rs @@ -214,3 +214,253 @@ mod heads_reading_item_boundary_tests { ); } } + +/// THE POOL CENSUS'S READING OF ONE FILE, projected from the file-local heads reading +/// `pool_acquire::heads_reading_for` holds rather than parsed a second time. +/// +/// The census threads one intern table and one occurrence-ordinal space across the pool, and the +/// closure parses continue from it, so its nodes are not the file-local reading's values. The +/// projection between them is TOTAL, because both of the reading's space-dependent facts are +/// derived from the file alone plus the space's state on entry: +/// - OCCURRENCE IDS are allocated sequentially from the entry base (`alloc_occurrence_id`, +/// `occurrence_allocator_after_index`), so a local id `k` is pool id `base + k`, and the +/// allocator leaves at `base + local_next`. +/// - IDENTS are intern ids. The local table interns this file's strings in first-sight order, +/// which is exactly the order the threaded parse interns the ones the pool has not seen, so +/// interning the local strings in local-id order into the incoming table reproduces the +/// threaded table, and `relabel[k]` is the pool id of local ident `k`. +/// +/// Anything the parser does not produce refuses instead of being guessed at: a projected +/// occurrence, or expression data carrying semantic payload. The receipt that +/// the projection is the threaded reading is +/// `heads_projection_tests` (fixture) and `entry_resolve::heads_projection_live_differential` +/// (live pool). +pub(crate) fn project_heads_reading( + local: &crate::v1_compiler_parse::ParseWithTableResult, + incoming: &Rc, +) -> Result<(crate::v1_compiler_parse::ParseResult, Rc), String> { + let base = incoming.authored_token_ordinals.allocator.next_id; + let mut table = incoming.clone(); + let mut relabel: Vec = Vec::with_capacity(local.intern_table.strings.len()); + for s in local.intern_table.strings.iter() { + let r = crate::v1_std_core::intern(table.clone(), s.clone()); + relabel.push(r.id); + table = r.table.clone(); + } + let local_next = local.intern_table.authored_token_ordinals.allocator.next_id; + let table = crate::v1_std_core::intern_table_with_authored_token_ordinals( + table, + crate::std_occurrence_identity::authored_token_ordinal_space_from_allocator( + crate::std_occurrence_identity::OccurrenceIdAllocator { + next_id: base + local_next, + }, + ), + ); + let module = match &local.result.module { + Some(m) => Some(project_node(m, base, &relabel)?), + None => None, + }; + Ok(( + crate::v1_compiler_parse::ParseResult { + module, + error: local.result.error.clone(), + }, + table, + )) +} + +fn project_node(n: &Rc, base: i64, relabel: &[i64]) -> Result, String> { + use crate::std_occurrence_identity::{NodeOccurrenceIdentity, OccurrenceId}; + use crate::v1_std_core::ExprData; + let occurrence_identity = match &*n.occurrence_identity { + NodeOccurrenceIdentity::OccurrenceSynthetic => n.occurrence_identity.clone(), + NodeOccurrenceIdentity::OccurrenceMinted { id } => { + Rc::new(NodeOccurrenceIdentity::OccurrenceMinted { + id: OccurrenceId { + value: base + id.value, + }, + }) + } + NodeOccurrenceIdentity::OccurrenceProjected { .. } => { + return Err(format!( + "heads projection refused: node '{}' carries a projected occurrence, which the \ + parser does not mint", + n.name + )) + } + }; + // The parser records a written type expression as `Resolved { node }`: one more nested node. + // The other arms carry no id. + let inferred = match n.inferred.as_deref() { + Some(crate::v1_std_core::InferredNode::Resolved { node }) => { + Some(Rc::new(crate::v1_std_core::InferredNode::Resolved { + node: project_node(node, base, relabel)?, + })) + } + _ => n.inferred.clone(), + }; + let payload_free = match &*n.expr_data { + ExprData::ExprElaboratedLiteral { .. } => false, + ExprData::ExprVar { binding_kind } => binding_kind.is_none(), + ExprData::ExprFieldAccess { summary } => summary.is_none(), + ExprData::ExprCall { + call_semantics, + descent_evidence, + } => call_semantics.is_none() && descent_evidence.is_none(), + ExprData::ExprMethodCall { method_semantics } => method_semantics.is_none(), + ExprData::ExprBinOp { + algebra_field, + operand, + .. + } => algebra_field.is_none() && operand.is_none(), + _ => true, + }; + if !payload_free { + return Err(format!( + "heads projection refused: node '{}' carries semantic expression data at parse", + n.name + )); + } + let ident = match n.ident { + Some(k) => Some(*relabel.get(k as usize).ok_or_else(|| { + format!( + "heads projection refused: node '{}' ident {k} is outside the file's intern table", + n.name + ) + })?), + None => None, + }; + let list = |v: &Rc>>| -> Result>>, String> { + v.iter() + .map(|c| project_node(c, base, relabel)) + .collect::, _>>() + .map(Rc::new) + }; + let opt = |o: &Option>| -> Result>, String> { + o.as_ref() + .map(|c| project_node(c, base, relabel)) + .transpose() + }; + let match_pattern = match &n.match_pattern { + None => None, + Some(p) => Some(Rc::new(match &**p { + MatchPattern::Bind { declaration } => MatchPattern::Bind { + declaration: project_node(declaration, base, relabel)?, + }, + MatchPattern::VariantPattern { + name, + parent_enum, + field_bindings, + } => MatchPattern::VariantPattern { + name: name.clone(), + parent_enum: parent_enum.clone(), + field_bindings: list(field_bindings)?, + }, + other => other.clone(), + })), + }; + Ok(Rc::new(Node { + occurrence_identity, + ident, + children: list(&n.children)?, + params: list(&n.params)?, + uses: list(&n.uses)?, + body: opt(&n.body)?, + transport: opt(&n.transport)?, + properties: list(&n.properties)?, + type_annotation: opt(&n.type_annotation)?, + match_pattern, + inferred, + ..(**n).clone() + })) +} + +/// Thread `files` through the pool census's two readings in order -- the threaded parse the census +/// used to take, and the projection of the file-local reading it takes now -- and return every +/// divergence at identity grain: the whole projected module Node (every occurrence id and ident), +/// the refusal, and the intern table and occurrence allocator each file leaves behind. +#[cfg(test)] +pub(crate) fn heads_projection_divergences(files: &[(String, String)]) -> (usize, Vec) { + let mut threaded = crate::v1_std_core::empty_intern_table(); + let mut projected = crate::v1_std_core::empty_intern_table(); + let mut divergent = Vec::new(); + for (path, content) in files { + let tokens = pool_acquire::tokens_for(path, content); + let mut si = HashMap::new(); + si.insert(path.clone(), pool_acquire::newline_index_for(path, content)); + let old = v1_compiler_parse::parse_heads_with_table(tokens, Rc::new(si), threaded.clone()); + let local = pool_acquire::heads_reading_for(path, content); + let (new, table) = match project_heads_reading(&local, &projected) { + Ok(v) => v, + Err(e) => { + divergent.push(format!("{path}: {e}")); + return (files.len(), divergent); + } + }; + if old.result.module != new.module { + divergent.push(format!("{path}: module node")); + } + if old.result.error != new.error { + divergent.push(format!("{path}: refusal")); + } + if old.intern_table != table { + divergent.push(format!("{path}: intern table / occurrence allocator")); + } + threaded = old.intern_table.clone(); + projected = table; + } + (files.len(), divergent) +} + +#[cfg(test)] +mod heads_projection_tests { + use super::*; + + fn fixture() -> Vec<(String, String)> { + [ + ("dag/test/fixture/proj_a.dag", "module proj.a\n\ntype Shape = Circle | Square\n\ntype Box { value: T }\n\nfn area(s: Shape) -> Int {\n match s {\n Circle => 1,\n Square => 2,\n }\n}\n"), + ("dag/test/fixture/proj_b.dag", "module proj.b\n\nfn area(b: Box) -> Int {\n 1\n}\n\ndata limit: Int = 3\n"), + ("dag/test/fixture/proj_c.dag", "module proj.c\n\ntype Shape = Circle | Triangle\n\nfn fresh_name(x: Shape, y: Box) -> Shape {\n x\n}\n"), + ("dag/test/fixture/proj_bad.dag", "module proj.bad\n\nfn broken( -> Int {\n 1\n}\n"), + ] + .iter() + .map(|(p, c)| (p.to_string(), c.to_string())) + .collect() + } + + /// Strings repeat across files (`Shape`, `area`, `Box`), so identity only holds if idents are + /// relabeled into the pool table and occurrence ids offset by what earlier files allocated; + /// the malformed head checks that the refusal and the table it leaves agree too. + #[test] + fn projected_heads_equal_the_threaded_parse_at_identity_grain() { + let (n, divergent) = heads_projection_divergences(&fixture()); + assert_eq!(n, 4); + assert!(divergent.is_empty(), "divergent: {divergent:?}"); + } + + /// The red the comparison exists to catch: taking the file-local reading as the census + /// reading, with no projection, diverges from the threaded parse from the second file on. + #[test] + fn the_unprojected_local_reading_is_not_the_threaded_reading() { + let files = fixture(); + let (a, b) = (&files[0], &files[1]); + let mut si = HashMap::new(); + si.insert(a.0.clone(), pool_acquire::newline_index_for(&a.0, &a.1)); + let first = v1_compiler_parse::parse_heads_with_table( + pool_acquire::tokens_for(&a.0, &a.1), + Rc::new(si), + crate::v1_std_core::empty_intern_table(), + ); + let mut si = HashMap::new(); + si.insert(b.0.clone(), pool_acquire::newline_index_for(&b.0, &b.1)); + let threaded = v1_compiler_parse::parse_heads_with_table( + pool_acquire::tokens_for(&b.0, &b.1), + Rc::new(si), + first.intern_table.clone(), + ); + let local = pool_acquire::heads_reading_for(&b.0, &b.1); + assert_ne!(threaded.result.module, local.result.module); + let (projected, _) = project_heads_reading(&local, &first.intern_table).unwrap(); + assert_eq!(threaded.result.module, projected.module); + } +} diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index d15db1d7279..1c7e6e42fe9 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -3425,3 +3425,38 @@ mod closure_parse_acquisition_differential { ); } } + +/// THE LIVE IDENTITY DIFFERENTIAL for the census projecting rather than re-parsing: every file +/// of the `[dag, src/v2]` shared index, in `pool_parse`'s order, through both readings, compared +/// by `heads_projection_divergences`. +#[cfg(test)] +mod heads_projection_live_differential { + use super::*; + #[test] + #[ignore = "live-corpus: prepares or builds over the live tree (minutes per test); the receipts lane runs these with --ignored, the required unit run does not"] + fn projected_heads_equal_the_threaded_parse_on_the_live_pool() { + let root = process_workspace_root(); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| root.join(r).to_string_lossy().into_owned()) + .collect(); + let index = process_shared_index(&roots); + let mut keys: Vec = index.source_files.keys().cloned().collect(); + keys.sort(); + let files: Vec<(String, String)> = keys + .iter() + .map(|k| { + let sf = &index.source_files[k]; + (sf.path.clone(), sf.content.clone()) + }) + .collect(); + let (n, divergent) = super::super::census_heads::heads_projection_divergences(&files); + eprintln!("DIFF compared={n} divergent={}", divergent.len()); + assert!(n > 1000, "the live pool was read ({n} files)"); + assert!( + divergent.is_empty(), + "divergent: {:?}", + &divergent[..divergent.len().min(20)] + ); + } +} diff --git a/src/v1/stage0/src/cli_run/pool_acquire.rs b/src/v1/stage0/src/cli_run/pool_acquire.rs index ddb82d764c7..52718fd4e1a 100644 --- a/src/v1/stage0/src/cli_run/pool_acquire.rs +++ b/src/v1/stage0/src/cli_run/pool_acquire.rs @@ -78,6 +78,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +use crate::v1_compiler_parse::ParseWithTableResult; use crate::v1_compiler_tokenize::V1LexArtifact; use crate::v1_std_core::{build_newline_index, NewlineIndex, Token}; use im::Vector as RtVec; @@ -98,6 +99,9 @@ struct Acquired { content: Rc, artifact: Rc, newline_index: Rc, + /// The heads reading of these bytes, parsed in a FILE-LOCAL space (empty intern table, + /// occurrence ordinals from zero), filled on first demand. See `heads_reading_for`. + heads: RefCell>>, } thread_local! { @@ -124,6 +128,7 @@ fn acquire(file: &str, content: &str) -> Rc { content: Rc::new(content.to_string()), artifact, newline_index, + heads: RefCell::new(None), }); POOL.with(|p| p.borrow_mut().insert(key, acquired.clone())); acquired @@ -143,3 +148,27 @@ pub fn artifact_for(file: &str, content: &str) -> Rc { pub fn newline_index_for(file: &str, content: &str) -> Rc { acquire(file, content).newline_index.clone() } + +/// THE ONE HEADS READING of a file, computed once per (spelling, bytes). +/// +/// It is parsed in a FILE-LOCAL space: an empty intern table and occurrence ordinals from zero. +/// That makes it a pure function of its key, which is what lets two consumers share it: +/// `module_path_index::parse_module_binding` reads it as-is, since it projects only the module +/// name, its span and the refusal, none of which carry an id. The pool census reads it through +/// `census_heads::project_heads_reading`, which maps it into the pool's threaded intern table +/// and occurrence space. That projection is total, so the census needs no second parse. +pub fn heads_reading_for(file: &str, content: &str) -> Rc { + let acquired = acquire(file, content); + if let Some(hit) = acquired.heads.borrow().clone() { + return hit; + } + let mut indices = im::HashMap::new(); + indices.insert(file.to_string(), acquired.newline_index.clone()); + let reading = crate::v1_compiler_parse::parse_heads_with_table( + acquired.artifact.tokens.clone(), + Rc::new(indices), + crate::v1_std_core::empty_intern_table(), + ); + *acquired.heads.borrow_mut() = Some(reading.clone()); + reading +} diff --git a/src/v1/stage0/src/module_path_index/index.rs b/src/v1/stage0/src/module_path_index/index.rs index 9f40b979089..2d2c9d904b3 100644 --- a/src/v1/stage0/src/module_path_index/index.rs +++ b/src/v1/stage0/src/module_path_index/index.rs @@ -1,11 +1,8 @@ -use im::HashMap; use std::path::Path; use std::rc::Rc; -use crate::v1_compiler_parse::parse_heads_with_table; use crate::v1_std_core::{ - diagnostic_to_message, diagnostic_to_span, empty_intern_table, node_name_span, - CompilerDiagnostic, SourceSpan, + diagnostic_to_message, diagnostic_to_span, node_name_span, CompilerDiagnostic, SourceSpan, }; /// One parse-derived module⇄path row for manifest emission (host binding authority). @@ -127,11 +124,6 @@ pub fn parse_module_binding( let key = source_key(path); // One acquisition, not one per walk -- see `cli_run::pool_acquire`. Identical bytes and // identical spelling, so identical tokens; this walk keeps its own collector and policy. - let tokens = crate::cli_run::pool_acquire::tokens_for(&key, content); - let source_index = crate::cli_run::pool_acquire::newline_index_for(&key, content); - let mut indices = HashMap::new(); - indices.insert(key.clone(), source_index); - let source_indices = Rc::new(indices); // THE DECLARATION HEADS ARE THE WHOLE SUBJECT, so this is the heads reading of the // grammar rather than the full one. Every item HEAD is parsed by the same productions, // and both facts this walk projects -- `module.name` and its span -- are heads. @@ -148,7 +140,10 @@ pub fn parse_module_binding( // `v1.compiler.parse.parse_heads_with_table`. Building an index was full-parsing the // corpus as a side effect, which coupled every pool-derived resolve in the process to // the grammaticality of every body in the tree (DESIGN §3: one fact, one authority). - let result = parse_heads_with_table(tokens, source_indices, empty_intern_table()) + // + // The reading is the file-local one `pool_acquire::heads_reading_for` holds, which the pool + // census projects rather than re-parses: one heads reading per file per process. + let result = crate::cli_run::pool_acquire::heads_reading_for(&key, content) .result .clone(); if let Some(err) = result.error.as_ref() { From c9270860df64a6cd2a68fa250047a93094ab2e68 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 19:48:42 +0000 Subject: [PATCH 03/24] Heads projection: exhaustive destructuring, no catch-all arms Every Node field and every ExprData / MatchPattern / InferredNode arm is named with no '..' or wildcard, so a field or variant added later fails to compile at the walker instead of passing through with file-local ids (review 72735). Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/cli_run/census_heads.rs | 165 +++++++++++++++------- 1 file changed, 112 insertions(+), 53 deletions(-) diff --git a/src/v1/stage0/src/cli_run/census_heads.rs b/src/v1/stage0/src/cli_run/census_heads.rs index 2f77aa7ef84..0bc3eb25d84 100644 --- a/src/v1/stage0/src/cli_run/census_heads.rs +++ b/src/v1/stage0/src/cli_run/census_heads.rs @@ -271,9 +271,41 @@ pub(crate) fn project_heads_reading( fn project_node(n: &Rc, base: i64, relabel: &[i64]) -> Result, String> { use crate::std_occurrence_identity::{NodeOccurrenceIdentity, OccurrenceId}; - use crate::v1_std_core::ExprData; - let occurrence_identity = match &*n.occurrence_identity { - NodeOccurrenceIdentity::OccurrenceSynthetic => n.occurrence_identity.clone(), + use crate::v1_std_core::{ExprData, InferredNode}; + // EXHAUSTIVE BY CONSTRUCTION: every `Node` field, and every arm of every enum below that can + // hold a node or an id, is named with no `..` and no wildcard, so a field or variant added + // later fails to compile here rather than passing through with file-local ids. + let Node { + occurrence_identity, + name, + ident, + span, + ident_span, + children, + connective, + params, + inferred, + return_cardinality, + uses, + body, + transport, + properties, + type_annotation, + is_self_recursive, + has_non_tail_self_call, + match_pattern, + module_item_kind, + declaration_marker, + expr_data, + } = &**n; + let refuse = |what: &str| { + Err(format!( + "heads projection refused: node '{name}' carries {what}, which the heads parser does \ + not produce" + )) + }; + let occurrence_identity = match &**occurrence_identity { + NodeOccurrenceIdentity::OccurrenceSynthetic => occurrence_identity.clone(), NodeOccurrenceIdentity::OccurrenceMinted { id } => { Rc::new(NodeOccurrenceIdentity::OccurrenceMinted { id: OccurrenceId { @@ -282,25 +314,34 @@ fn project_node(n: &Rc, base: i64, relabel: &[i64]) -> Result, St }) } NodeOccurrenceIdentity::OccurrenceProjected { .. } => { - return Err(format!( - "heads projection refused: node '{}' carries a projected occurrence, which the \ - parser does not mint", - n.name - )) + return refuse("a projected occurrence") } }; - // The parser records a written type expression as `Resolved { node }`: one more nested node. - // The other arms carry no id. - let inferred = match n.inferred.as_deref() { - Some(crate::v1_std_core::InferredNode::Resolved { node }) => { - Some(Rc::new(crate::v1_std_core::InferredNode::Resolved { - node: project_node(node, base, relabel)?, - })) + let payload_free = match &**expr_data { + ExprData::NoExprData + | ExprData::ExprLiteral { value: _ } + | ExprData::ExprError { + kind: _, + message: _, } - _ => n.inferred.clone(), - }; - let payload_free = match &*n.expr_data { - ExprData::ExprElaboratedLiteral { .. } => false, + | ExprData::ExprMatch + | ExprData::ExprIf + | ExprData::ExprLet + | ExprData::ExprRecordLit { parent_enum: _ } + | ExprData::ExprListLit + | ExprData::ExprUnaryOp { op: _ } + | ExprData::ExprLambda + | ExprData::ExprStringInterp + | ExprData::ExprBlock + | ExprData::ExprCast + | ExprData::ExprForEach + | ExprData::ExprIndex + | ExprData::ExprSlice + | ExprData::ExprReturn => true, + ExprData::ExprElaboratedLiteral { + value: _, + elaboration: _, + } => false, ExprData::ExprVar { binding_kind } => binding_kind.is_none(), ExprData::ExprFieldAccess { summary } => summary.is_none(), ExprData::ExprCall { @@ -309,23 +350,19 @@ fn project_node(n: &Rc, base: i64, relabel: &[i64]) -> Result, St } => call_semantics.is_none() && descent_evidence.is_none(), ExprData::ExprMethodCall { method_semantics } => method_semantics.is_none(), ExprData::ExprBinOp { + op: _, algebra_field, operand, - .. } => algebra_field.is_none() && operand.is_none(), - _ => true, }; if !payload_free { - return Err(format!( - "heads projection refused: node '{}' carries semantic expression data at parse", - n.name - )); + return refuse("semantic expression data"); } - let ident = match n.ident { - Some(k) => Some(*relabel.get(k as usize).ok_or_else(|| { + let ident = match ident { + Some(k) => Some(*relabel.get(*k as usize).ok_or_else(|| { format!( - "heads projection refused: node '{}' ident {k} is outside the file's intern table", - n.name + "heads projection refused: node '{name}' ident {k} is outside the file's intern \ + table" ) })?), None => None, @@ -341,37 +378,59 @@ fn project_node(n: &Rc, base: i64, relabel: &[i64]) -> Result, St .map(|c| project_node(c, base, relabel)) .transpose() }; - let match_pattern = match &n.match_pattern { + // The parser records a written type expression as `Resolved { node }`: one more nested node. + let inferred = match inferred.as_deref() { + None => None, + Some(InferredNode::Resolved { node }) => Some(Rc::new(InferredNode::Resolved { + node: project_node(node, base, relabel)?, + })), + Some(InferredNode::CompilerError { + message: _, + span: _, + }) + | Some(InferredNode::TypeVariable { id: _ }) + | Some(InferredNode::Divergent) => inferred.clone(), + }; + let match_pattern = match match_pattern.as_deref() { None => None, - Some(p) => Some(Rc::new(match &**p { - MatchPattern::Bind { declaration } => MatchPattern::Bind { - declaration: project_node(declaration, base, relabel)?, - }, - MatchPattern::VariantPattern { - name, - parent_enum, - field_bindings, - } => MatchPattern::VariantPattern { - name: name.clone(), - parent_enum: parent_enum.clone(), - field_bindings: list(field_bindings)?, - }, - other => other.clone(), + Some(MatchPattern::Bind { declaration }) => Some(Rc::new(MatchPattern::Bind { + declaration: project_node(declaration, base, relabel)?, })), + Some(MatchPattern::VariantPattern { + name, + parent_enum, + field_bindings, + }) => Some(Rc::new(MatchPattern::VariantPattern { + name: name.clone(), + parent_enum: parent_enum.clone(), + field_bindings: list(field_bindings)?, + })), + Some(MatchPattern::LitPattern { value: _ }) | Some(MatchPattern::Wildcard) => { + match_pattern.clone() + } }; Ok(Rc::new(Node { occurrence_identity, + name: name.clone(), ident, - children: list(&n.children)?, - params: list(&n.params)?, - uses: list(&n.uses)?, - body: opt(&n.body)?, - transport: opt(&n.transport)?, - properties: list(&n.properties)?, - type_annotation: opt(&n.type_annotation)?, - match_pattern, + span: span.clone(), + ident_span: ident_span.clone(), + children: list(children)?, + connective: connective.clone(), + params: list(params)?, inferred, - ..(**n).clone() + return_cardinality: return_cardinality.clone(), + uses: list(uses)?, + body: opt(body)?, + transport: opt(transport)?, + properties: list(properties)?, + type_annotation: opt(type_annotation)?, + is_self_recursive: *is_self_recursive, + has_non_tail_self_call: *has_non_tail_self_call, + match_pattern, + module_item_kind: module_item_kind.clone(), + declaration_marker: declaration_marker.clone(), + expr_data: expr_data.clone(), })) } From 8f84d2015bd8a1cdc95ad9922cbab44ad2e474a5 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 20:43:53 +0000 Subject: [PATCH 04/24] Tree census upgrades the memoized raw census instead of rebuilding it build_symbol_index_census_nodes is census_with_resolved_fn_sigs over the raw census; closure_name_census(index, Some(root)) already builds and memoizes exactly that raw census over the same nodes and source indices. tree_bare_census_for_root now upgrades the memoized value. Adds a live whole-SymbolIndex differential per root. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/cli_run.rs | 26 ++++++++++++--- src/v1/stage0/src/cli_run/entry_resolve.rs | 37 ++++++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 08ea3aec6ba..46c9aed08d2 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -17609,9 +17609,22 @@ fn tree_bare_census_for_root( ); } let pool = pool_parse(index)?; - let nodes = tree_census_nodes(index, root)?; - let census = - v1_compiler_infer::build_symbol_index_census_nodes(nodes, pool.combined_si.clone()); + // THE RAW CENSUS OF THIS ROOT IS ONE FACT. `build_symbol_index_census_nodes` is by definition + // `census_with_resolved_fn_sigs` over `build_symbol_index_census_raw_nodes(nodes, si)`, and + // `closure_name_census(index, Some(root))` already builds and memoizes exactly that raw census + // over the same `tree_census_nodes` and the same `combined_si`. So this upgrades the memoized + // raw census instead of rebuilding it. The raw build, when this is the first demand for it, + // is recorded on `closure_name_census_build`, not on this miss. + let (raw, raw_nanos) = { + let started = std::time::Instant::now(); + let hit = index + .closure_name_censuses + .borrow() + .contains_key(&Some(root.to_string())); + let raw = closure_name_census(index, Some(root))?; + (raw, if hit { 0 } else { started.elapsed().as_nanos() }) + }; + let census = v1_compiler_infer::census_with_resolved_fn_sigs(raw, pool.combined_si.clone()); index .tree_bare_census .borrow_mut() @@ -17624,8 +17637,11 @@ fn tree_bare_census_for_root( .pool_parse .saturating_sub(pool_before); resolve_stage_slot_add(|st| { - st.edge_index_tree_census_miss_nanos += - miss_started.elapsed().as_nanos().saturating_sub(pool_here); + st.edge_index_tree_census_miss_nanos += miss_started + .elapsed() + .as_nanos() + .saturating_sub(pool_here) + .saturating_sub(raw_nanos); }); Ok(census) } diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 1c7e6e42fe9..673842fb818 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -3460,3 +3460,40 @@ mod heads_projection_live_differential { ); } } + +/// THE IDENTITY DIFFERENTIAL for the tree census upgrading the memoized raw census instead of +/// rebuilding it: for every source root of the live `[dag, src/v2]` index, the census +/// `tree_bare_census_for_root` now serves equals the direct +/// `build_symbol_index_census_nodes(tree_census_nodes(root))` it replaced -- the whole +/// `SymbolIndex` (entries, bare lookup states and candidates, services, alias reps, exposures). +#[cfg(test)] +mod tree_census_from_raw_differential { + use super::*; + #[test] + #[ignore = "live-corpus: prepares or builds over the live tree (minutes per test); the receipts lane runs these with --ignored, the required unit run does not"] + fn tree_census_from_memoized_raw_equals_the_direct_build_on_the_live_pool() { + let root = process_workspace_root(); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| root.join(r).to_string_lossy().into_owned()) + .collect(); + let index = process_shared_index(&roots); + let pool = super::super::pool_parse(&index).expect("pool parse"); + let mut compared = 0usize; + for r in index.source_roots.iter() { + let served = super::super::tree_bare_census_for_root(&index, r).expect("served"); + let nodes = super::super::tree_census_nodes(&index, r).expect("tree nodes"); + let direct = + v1_compiler_infer::build_symbol_index_census_nodes(nodes, pool.combined_si.clone()); + eprintln!( + "DIFF root={r} entries={} bare={} equal={}", + direct.entries.len(), + direct.global_bare.len(), + *served == *direct + ); + assert!(*served == *direct, "tree census for {r} diverges"); + compared += 1; + } + assert!(compared >= 2, "both live roots compared ({compared})"); + } +} From a88e21bc6d49b85dd3ad9bed43c91f512c27aa62 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 21:19:18 +0000 Subject: [PATCH 05/24] Heads projection: name Node.declaration (#12612); the parser never writes it, so Some refuses Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/cli_run/census_heads.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/v1/stage0/src/cli_run/census_heads.rs b/src/v1/stage0/src/cli_run/census_heads.rs index 0bc3eb25d84..da1e453121b 100644 --- a/src/v1/stage0/src/cli_run/census_heads.rs +++ b/src/v1/stage0/src/cli_run/census_heads.rs @@ -296,6 +296,7 @@ fn project_node(n: &Rc, base: i64, relabel: &[i64]) -> Result, St match_pattern, module_item_kind, declaration_marker, + declaration, expr_data, } = &**n; let refuse = |what: &str| { @@ -358,6 +359,10 @@ fn project_node(n: &Rc, base: i64, relabel: &[i64]) -> Result, St if !payload_free { return refuse("semantic expression data"); } + // Declaration identity is written by resolve, never by the parser. + if declaration.is_some() { + return refuse("a resolved declaration identity"); + } let ident = match ident { Some(k) => Some(*relabel.get(*k as usize).ok_or_else(|| { format!( @@ -430,6 +435,7 @@ fn project_node(n: &Rc, base: i64, relabel: &[i64]) -> Result, St match_pattern, module_item_kind: module_item_kind.clone(), declaration_marker: declaration_marker.clone(), + declaration: None, expr_data: expr_data.clone(), })) } From e404000e59fefc35cde8933944d6ef9a011bcff3 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 21:31:24 +0000 Subject: [PATCH 06/24] WIP: tree census upgrades only the bare fill (seed regen pending) --- src/v1/04_infer.dag | 50 +++++++++++++++------- src/v1/stage0/src/cli_run.rs | 6 ++- src/v1/stage0/src/cli_run/entry_resolve.rs | 31 +++++++++++--- 3 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/v1/04_infer.dag b/src/v1/04_infer.dag index b0f5314fa81..618a55f52c8 100644 --- a/src/v1/04_infer.dag +++ b/src/v1/04_infer.dag @@ -13823,21 +13823,20 @@ fn census_upgrade_service_item(item: Node, module_path: String, census: SymbolIn node_with_children(n: item, children: ops2) } -fn census_with_resolved_fn_sigs(index: SymbolIndex, source_indices: Map) -> SymbolIndex { - let entry_keys = sorted_map_keys(index.entries) - let entries2 = fold(entry_keys, init: index.entries, f: (acc, k) => - match map_get(index.entries, k) { - Present { value: node } => - let upgraded = census_upgrade_binding( - binding: TypeBinding { name: qualified_last_segment(name: k), resolved: node, provenance: SubValueUnknown }, - module_path: qualified_all_but_last(name: k), - census: index, - source_indices: source_indices - ) - if upgraded.resolved == node { acc } else { map_insert(acc, k, upgraded.resolved) } - Absent => acc - } - ) +// THE BARE-FILL UNDERLAY'S UPGRADE: `global_bare` and `services` with resolved signatures, `entries` +// left as the raw census. It is what a per-tree underlay is read for: `symbol_index_with_bare_fill` +// takes the tree's `global_bare`, `services`, alias reps and exposures and keeps the CLOSURE's +// `entries`, so upgrading the tree's entries produced a value no consumer read. Every upgrade below +// reads the RAW `index` (entries included), never an upgraded one, so each bare and service value +// is exactly the one `census_with_resolved_fn_sigs` produces. +// +// DECLARED FRONTIER: the bare upgrade here is still eager over every bare name of the tree, while +// a closure's typecheck reads only the names it looks up. Scoping it to those demands needs a +// demand identity for "this lookup, in this closure" -- the demand-identity carriers of +// docs/plans/demand-engine-program.md (M1). Until that capability exists, no key of `global_bare` +// can be marked undemanded without either a lazy cell or a new refusal arm in frozen v1 semantics, +// both ruled out; this stays eager. +fn census_bare_fill_with_resolved_fn_sigs(index: SymbolIndex, source_indices: Map) -> SymbolIndex { let bare_keys = sorted_map_keys(index.global_bare) let global2 = fold(bare_keys, init: index.global_bare, f: (acc, k) => match map_get(index.global_bare, k) { @@ -13862,7 +13861,26 @@ fn census_with_resolved_fn_sigs(index: SymbolIndex, source_indices: Map acc } ) - SymbolIndex { entries: entries2, global_bare: global2, services: services2, transparent_alias_rep: index.transparent_alias_rep, type_head_exposures: index.type_head_exposures } + SymbolIndex { entries: index.entries, global_bare: global2, services: services2, transparent_alias_rep: index.transparent_alias_rep, type_head_exposures: index.type_head_exposures } +} + +fn census_with_resolved_fn_sigs(index: SymbolIndex, source_indices: Map) -> SymbolIndex { + let entry_keys = sorted_map_keys(index.entries) + let entries2 = fold(entry_keys, init: index.entries, f: (acc, k) => + match map_get(index.entries, k) { + Present { value: node } => + let upgraded = census_upgrade_binding( + binding: TypeBinding { name: qualified_last_segment(name: k), resolved: node, provenance: SubValueUnknown }, + module_path: qualified_all_but_last(name: k), + census: index, + source_indices: source_indices + ) + if upgraded.resolved == node { acc } else { map_insert(acc, k, upgraded.resolved) } + Absent => acc + } + ) + let fill = census_bare_fill_with_resolved_fn_sigs(index: index, source_indices: source_indices) + SymbolIndex { entries: entries2, global_bare: fill.global_bare, services: fill.services, transparent_alias_rep: fill.transparent_alias_rep, type_head_exposures: fill.type_head_exposures } } fn build_symbol_index_census(modules: List, source_indices: Map) -> SymbolIndex { diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 46c9aed08d2..48556937dab 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -17624,7 +17624,11 @@ fn tree_bare_census_for_root( let raw = closure_name_census(index, Some(root))?; (raw, if hit { 0 } else { started.elapsed().as_nanos() }) }; - let census = v1_compiler_infer::census_with_resolved_fn_sigs(raw, pool.combined_si.clone()); + // Only the bare-fill half is upgraded: `symbol_index_with_bare_fill`, this census's one + // production reader, keeps the closure's `entries` (see + // `v1.compiler.infer.census_bare_fill_with_resolved_fn_sigs`). + let census = + v1_compiler_infer::census_bare_fill_with_resolved_fn_sigs(raw, pool.combined_si.clone()); index .tree_bare_census .borrow_mut() diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 673842fb818..9fe7247d4a5 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -3374,6 +3374,7 @@ mod live_pool_entry_resolve_attribution { let r = resolve_entry_with_index_for_discovery_corpus(&index, &entry.to_string_lossy()); assert!(r.is_ok(), "{e} resolves"); eprintln!("PROBE resolve {e} {:?}", t.elapsed()); + eprintln!("PROBE stages {:?}", resolve_stage_totals()); for line in super::pre_entry_phase::take_lines() { eprintln!("PROBE phase {line}"); } @@ -3463,9 +3464,11 @@ mod heads_projection_live_differential { /// THE IDENTITY DIFFERENTIAL for the tree census upgrading the memoized raw census instead of /// rebuilding it: for every source root of the live `[dag, src/v2]` index, the census -/// `tree_bare_census_for_root` now serves equals the direct -/// `build_symbol_index_census_nodes(tree_census_nodes(root))` it replaced -- the whole -/// `SymbolIndex` (entries, bare lookup states and candidates, services, alias reps, exposures). +/// `tree_bare_census_for_root` now serves agrees with the direct +/// `build_symbol_index_census_nodes(tree_census_nodes(root))` on every field its one production +/// reader, `symbol_index_with_bare_fill`, consumes (bare lookup states and candidates, services, +/// alias reps, exposures), its `entries` are the raw census, and the composed underlay the +/// reconcile builds from it is equal whichever census it is composed from. #[cfg(test)] mod tree_census_from_raw_differential { use super::*; @@ -3485,13 +3488,29 @@ mod tree_census_from_raw_differential { let nodes = super::super::tree_census_nodes(&index, r).expect("tree nodes"); let direct = v1_compiler_infer::build_symbol_index_census_nodes(nodes, pool.combined_si.clone()); + // Every field the bare fill reads must equal the direct build; `entries` is the raw + // census, which no production reader of this census consumes. + let raw = super::super::closure_name_census(&index, Some(r)).expect("raw census"); + let fill_equal = served.global_bare == direct.global_bare + && served.services == direct.services + && served.transparent_alias_rep == direct.transparent_alias_rep + && served.type_head_exposures == direct.type_head_exposures; + let entries_raw = served.entries == raw.entries; + let composed_equal = + *v1_compiler_infer::symbol_index_with_bare_fill(raw.clone(), served.clone()) + == *v1_compiler_infer::symbol_index_with_bare_fill(raw.clone(), direct.clone()); eprintln!( - "DIFF root={r} entries={} bare={} equal={}", + "DIFF root={r} entries={} bare={} fill_equal={fill_equal} entries_raw={entries_raw} \ + composed_equal={composed_equal}", direct.entries.len(), direct.global_bare.len(), - *served == *direct ); - assert!(*served == *direct, "tree census for {r} diverges"); + assert!(fill_equal, "tree census bare fill for {r} diverges"); + assert!( + entries_raw, + "tree census entries for {r} are not the raw census" + ); + assert!(composed_equal, "bare-fill composition for {r} diverges"); compared += 1; } assert!(compared >= 2, "both live roots compared ({compared})"); From 8bdf2c31638983ef91a0d829253a1ec2a180a10c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 23:33:22 +0000 Subject: [PATCH 07/24] Regenerate v1_compiler_infer.rs from 04_infer.dag (bare-fill split) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/v1_compiler_infer.rs | 74 ++++++++++++++++---------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/src/v1/stage0/src/v1_compiler_infer.rs b/src/v1/stage0/src/v1_compiler_infer.rs index f357fbf5dbc..203abad6805 100644 --- a/src/v1/stage0/src/v1_compiler_infer.rs +++ b/src/v1/stage0/src/v1_compiler_infer.rs @@ -24225,38 +24225,11 @@ pub fn census_upgrade_service_item( } } -pub fn census_with_resolved_fn_sigs( +pub fn census_bare_fill_with_resolved_fn_sigs( index: Rc, source_indices: Rc>>, ) -> Rc { { - let entry_keys = Rc::new(v1_rt::sorted_map_keys(&index.entries.clone())); - let entries2 = entry_keys.iter().cloned().fold( - index.entries.clone(), - |acc: Rc>>, k: String| match v1_rt::map_get( - &index.entries.clone(), - k.clone(), - ) { - Some(node) => { - let upgraded = census_upgrade_binding( - Rc::new(TypeBinding { - name: crate::v1_std_core::qualified_last_segment(k.clone()), - resolved: node.clone(), - provenance: Rc::new(SubValueRelation::SubValueUnknown), - }), - crate::v1_compiler_infer_env::qualified_all_but_last(k.clone()), - index.clone(), - source_indices.clone(), - ); - if (upgraded.resolved.clone() == node.clone()) { - acc.clone() - } else { - v1_rt::rc_map_insert(acc.clone(), k.clone(), upgraded.resolved.clone()) - } - } - std::option::Option::None => acc.clone(), - }, - ); let bare_keys = Rc::new(v1_rt::sorted_map_keys(&index.global_bare.clone())); let global2 = bare_keys.iter().cloned().fold( index.global_bare.clone(), @@ -24352,7 +24325,7 @@ pub fn census_with_resolved_fn_sigs( }, ); Rc::new(SymbolIndex { - entries: entries2.clone(), + entries: index.entries.clone(), global_bare: global2.clone(), services: services2.clone(), transparent_alias_rep: index.transparent_alias_rep.clone(), @@ -24361,6 +24334,49 @@ pub fn census_with_resolved_fn_sigs( } } +pub fn census_with_resolved_fn_sigs( + index: Rc, + source_indices: Rc>>, +) -> Rc { + { + let entry_keys = Rc::new(v1_rt::sorted_map_keys(&index.entries.clone())); + let entries2 = entry_keys.iter().cloned().fold( + index.entries.clone(), + |acc: Rc>>, k: String| match v1_rt::map_get( + &index.entries.clone(), + k.clone(), + ) { + Some(node) => { + let upgraded = census_upgrade_binding( + Rc::new(TypeBinding { + name: crate::v1_std_core::qualified_last_segment(k.clone()), + resolved: node.clone(), + provenance: Rc::new(SubValueRelation::SubValueUnknown), + }), + crate::v1_compiler_infer_env::qualified_all_but_last(k.clone()), + index.clone(), + source_indices.clone(), + ); + if (upgraded.resolved.clone() == node.clone()) { + acc.clone() + } else { + v1_rt::rc_map_insert(acc.clone(), k.clone(), upgraded.resolved.clone()) + } + } + std::option::Option::None => acc.clone(), + }, + ); + let fill = census_bare_fill_with_resolved_fn_sigs(index.clone(), source_indices.clone()); + Rc::new(SymbolIndex { + entries: entries2.clone(), + global_bare: fill.global_bare.clone(), + services: fill.services.clone(), + transparent_alias_rep: fill.transparent_alias_rep.clone(), + type_head_exposures: fill.type_head_exposures.clone(), + }) + } +} + pub fn build_symbol_index_census( modules: Rc>>, source_indices: Rc>>, From 176f346bd7846c2525bdbd1c319fff0d79e8406a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 00:10:05 +0000 Subject: [PATCH 08/24] WIP: pool fallback census probe --- src/v1/stage0/src/cli_run/entry_resolve.rs | 85 ++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 959452daf71..3bb723ac28e 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -3599,3 +3599,88 @@ mod tree_census_from_raw_differential { assert!(compared >= 2, "both live roots compared ({compared})"); } } + +/// THE POOL-FALLBACK CENSUS, as a measurement: over every import-less file of the live +/// `[dag, src/v2]` pool, which bare references the file's own tree census does NOT answer and the +/// whole-pool census then does. Each such (file, name, provider) row is a resolution that depends on +/// the fallback; a demand that reaches the pool and comes back empty is counted separately. It +/// reports; it asserts only that the walk completed. +#[cfg(test)] +mod pool_fallback_census { + use super::*; + #[test] + #[ignore = "live-corpus: prepares or builds over the live tree (minutes per test); the receipts lane runs these with --ignored, the required unit run does not"] + fn pool_fallback_dependents_on_the_live_pool() { + let root = process_workspace_root(); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| root.join(r).to_string_lossy().into_owned()) + .collect(); + let index = process_shared_index(&roots); + let mut sources: Vec<_> = index.source_files.values().cloned().collect(); + sources.sort_by(|a, b| a.path.cmp(&b.path)); + let mut files_scanned = 0usize; + let mut files_demanding_pool = 0usize; + let mut refusals: Vec = Vec::new(); + let mut pool_rows: Vec = Vec::new(); + for sf in &sources { + if super::super::source_declares_import_lines(&sf.content) { + continue; + } + files_scanned += 1; + let demanded = std::cell::Cell::new(false); + // Providers each name resolves to when the tree census alone answers. + let mut scoped: BTreeSet<(String, String)> = BTreeSet::new(); + let r = super::super::visit_bare_reference_providers( + sf, + &index, + |root| { + if root.is_none() { + demanded.set(true); + return Ok(crate::v1_compiler_infer_env::empty_symbol_index()); + } + super::super::closure_name_census(&index, root) + }, + |name, module, _| { + scoped.insert((name.to_string(), module.to_string())); + Ok(()) + }, + ); + if !demanded.get() { + if let Err(e) = r { + refusals.push(format!("{}: {e}", sf.path)); + } + continue; + } + files_demanding_pool += 1; + let mut full: BTreeSet<(String, String)> = BTreeSet::new(); + let r = super::super::visit_bare_reference_providers( + sf, + &index, + |root| super::super::closure_name_census(&index, root), + |name, module, _| { + full.insert((name.to_string(), module.to_string())); + Ok(()) + }, + ); + if let Err(e) = r { + refusals.push(format!("{}: {e}", sf.path)); + } + for (name, module) in full.difference(&scoped) { + pool_rows.push(format!("{} -> {name} -> {module}", sf.path)); + } + } + eprintln!( + "FALLBACK files_scanned={files_scanned} files_demanding_pool={files_demanding_pool} \ + pool_answered_rows={} refusals={}", + pool_rows.len(), + refusals.len() + ); + for row in pool_rows.iter().take(60) { + eprintln!("FALLBACK row {row}"); + } + for r in refusals.iter().take(10) { + eprintln!("FALLBACK refusal {r}"); + } + } +} From 12b4665842ffaa89148ff75538faa8a77ec07946 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 02:52:57 +0000 Subject: [PATCH 09/24] Bare loader: no whole-pool census; per-name answer, typed cross-tree refusal, builtin arm Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ol_fallback_provider_shadows_a_builtin.dag | 17 ++ src/v1/stage0/src/cli_run.rs | 73 ++++- src/v1/stage0/src/cli_run/entry_resolve.rs | 281 ++++++++++++++---- ...auth_declared_but_unwired_witness_test.dag | 2 +- src/v2/test/claim/bootstrap_test.dag | 2 +- .../claim/infer_semantics_witness_test.dag | 2 +- .../manual/path_y_fidelity_successor_test.dag | 16 +- 7 files changed, 327 insertions(+), 66 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/a_pool_fallback_provider_shadows_a_builtin.dag diff --git a/dag/gunbc/recurring_failure_mode/a_pool_fallback_provider_shadows_a_builtin.dag b/dag/gunbc/recurring_failure_mode/a_pool_fallback_provider_shadows_a_builtin.dag new file mode 100644 index 00000000000..0ce0da12e2c --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/a_pool_fallback_provider_shadows_a_builtin.dag @@ -0,0 +1,17 @@ +module gunbc.recurring_failure_mode.a_pool_fallback_provider_shadows_a_builtin + +import std.types { NonEmptyStr } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data a_pool_fallback_provider_shadows_a_builtin: RecurringFailureMode = RecurringFailureMode { + identity: "a_pool_fallback_provider_shadows_a_builtin" as NonEmptyStr, + + receipts: [ + "INVALID STATE: a bare reference that names a BUILTIN is resolved by the bare-reference loader to an ordinary pool function that happens to share the name, pulling an unrelated module into the closure. HARM: silent wrong resolution -- the closure depends on a module the author never referenced, so that module's refusals, cost and edits reach a file they do not concern, and nothing reports it.", + "SPECIMEN: dag/test/claim/builtin_get_resolver_test.dag (module test.claim.builtin_get_resolver) calls the builtin get(xs:, index:); the loader's whole-pool fallback in cli_run visit_bare_reference_providers resolved 'get' to fn get in v2.test.manual.fn_as_value (src/v2/test/claim/manual/fn_as_value_test.dag), another source tree. Found by the live fallback census on the resolver-cost lane (bold-bat-516, 2026-09-30): of 624 import-less pool files, 466 demanded the whole-pool fallback census and 13 resolutions in 5 files depended on it; this was the one that was wrong.", + "DISTINGUISHING FACTS: the name is in v1.compiler.infer_method builtin_signature; the file's own source tree does not declare it; some other tree does. The fallback asked the whole pool for the superset instead of asking which kind of name this is -- DESIGN section 5's absorbing fallback, whose answer here was a wrong provider rather than a missing one.", + "RUNG FOUND AT: silent (outside the ladder). RUNG NOW: mechanically preventable -- the whole-pool fallback is deleted; a name the file's tree does not provide is a builtin (no provider), provided only by another tree (typed CrossTreeBareReference refusal naming the qualified spelling to write), or provided nowhere (no provider; the typecheck refuses an undefined name). Receipts: cli_run entry_resolve cross_tree_bare_reference_tests (a_builtin_named_like_another_trees_function_admits, an_unimported_cross_tree_bare_name_refuses) and the live cross_tree_bare_census. CEILING: structurally guaranteed -- a bare reference resolves only within its own source tree or through a written import, so no pool-wide lookup exists to shadow a builtin. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: bare-reference resolution in the v2 demand engine keyed by the reference's own scope (docs/plans/demand-engine-program.md), so the loader route and the typecheck bind a bare name through one authority.", + ], + + evidence: [], +} diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 5dbdc6baa54..f96a292211a 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -9703,6 +9703,34 @@ fn admit_pool_bare_references(index: &MultiEntryIndex) -> Result<(), String> { verdict } +/// THE WHOLE-POOL NAME CENSUS'S ENTRY FOR ONE NAME, computed over the modules that declare it. +/// +/// `build_symbol_index_census_raw_nodes` keys every bare, alias and service entry by a declared +/// name, and every count that gates an entry (variant and item multiplicity) counts declarations +/// of that same name. So the pool census's entry for `name` depends only on the modules that +/// declare `name`, and building the census over exactly those modules yields the same entry -- +/// the question the bare loader asks, without the whole pool. The modules are located by the +/// heads name index (`ReferencePoolNames::decl_index`, items plus the variants of `Disj` types, +/// the same declarations the census folds). The claim is checked for every name of the live pool +/// by `entry_resolve::pool_census_for_name_differential`. +fn pool_census_for_name(index: &MultiEntryIndex, name: &str) -> Result, String> { + let pool = pool_parse(index)?; + let names = entry_resolve::reference_pool_names_for_index(index)?; + let Some(modules) = names.decl_index.get(name) else { + return Ok(crate::v1_compiler_infer_env::empty_symbol_index()); + }; + let nodes: im::Vector> = modules + .iter() + .filter_map(|m| index.source_files.get(m)) + .filter_map(|sf| pool.position_by_file.get(&sf.path)) + .map(|&i| pool.nodes_by_file[i].1.clone()) + .collect(); + Ok(v1_compiler_infer::build_symbol_index_census_raw_nodes( + Rc::new(nodes), + pool.combined_si.clone(), + )) +} + /// One resolver, two consumers: admission discards selected providers, and a demanded /// edge row expands them. Candidate classification remains `closure_bare_disposition`, /// which consumes `v1.compiler.infer_env::global_bare_chain_candidates`. @@ -9920,11 +9948,41 @@ fn visit_bare_reference_providers( // Carrying the provenance costs nothing (the arms already know it) and makes the // existing `GUNBC_BARE_PULL_TRACE` line answer "how was this resolved", not only // "what did it resolve to". + // NO WHOLE-POOL CENSUS. A name the file's own tree census does not answer used to be asked + // of the WHOLE-POOL census, built in full by the first such demand -- DESIGN §5's + // absorbing fallback (not knowing the answer gets answered with the superset). What that + // question actually depends on is the pool census's entry for THIS NAME, and that entry is + // a function of the modules that declare the name alone (`pool_census_for_name`). So the + // same answer is computed over exactly those modules. Where it names a provider, the old + // route silently pulled a module from another source tree; that is now a typed, located + // refusal telling the author to write the reference qualified. Where it names none, nothing changes. + // Measured before the change by the live fallback census: 13 such pulls in 5 files on the + // real pool, one of them a builtin `get` bound to an unrelated `fn get` + // (`gunbc.recurring_failure_mode.a_pool_fallback_provider_shadows_a_builtin`). let (target_module, resolution_arm, census_state) = match resolve_in(&census)? { (Some(m), state) => (Some(m), "scoped", state), - (None, _) => { - let (m, state) = resolve_in(&census_for(None)?)?; - (m, "pool-fallback", state) + // A BUILTIN the tree does not declare is the builtin: no other tree's function of the + // same name is a provider for it (`builtin_signature` is the builtin authority). + (None, state) + if !service_head + && crate::v1_compiler_infer_method::builtin_signature(name.clone()) + .is_some() => + { + (None, "builtin", state) + } + (None, state) => { + let (provider, _) = resolve_in(&pool_census_for_name(index, &name)?)?; + if let Some(provider) = provider { + return Err(format!( + "bare_reference_closure: CrossTreeBareReference -- bare reference \ + '{name}' in '{file_rel}' is not provided by this file's source tree \ + ({root}); only '{provider}', outside it, provides it. A reference \ + across source trees is written qualified, as `{provider}.{name}` \ + (an `import` would also stop every other bare reference in this \ + file from being followed)." + )); + } + (None, "scoped", state) } }; let Some(module_path) = target_module else { @@ -13546,6 +13604,9 @@ fn next_index_generation() -> u64 { struct PoolParse { /// Workspace-relative file path → census-head module node. nodes_by_file: Vec<(String, Rc)>, + /// Position of each file in `nodes_by_file`, so a reader that wants a few named files + /// (`pool_census_for_name`) finds them without walking the pool. + position_by_file: std::collections::HashMap, combined_si: Rc>>, } @@ -17166,8 +17227,14 @@ fn pool_parse(index: &MultiEntryIndex) -> Result, String> { combined_si.insert(file.clone(), nl_index); nodes_by_file.push((file, module)); } + let position_by_file = nodes_by_file + .iter() + .enumerate() + .map(|(i, (file, _))| (file.clone(), i)) + .collect(); let parsed = Rc::new(PoolParse { nodes_by_file, + position_by_file, combined_si: Rc::new(combined_si), }); pre_entry_phase::record( diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 3bb723ac28e..7f3f89390a1 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -3600,17 +3600,19 @@ mod tree_census_from_raw_differential { } } -/// THE POOL-FALLBACK CENSUS, as a measurement: over every import-less file of the live -/// `[dag, src/v2]` pool, which bare references the file's own tree census does NOT answer and the -/// whole-pool census then does. Each such (file, name, provider) row is a resolution that depends on -/// the fallback; a demand that reaches the pool and comes back empty is counted separately. It -/// reports; it asserts only that the walk completed. +/// THE CROSS-TREE CENSUS, over the whole live `[dag, src/v2]` pool: every import-less file's bare +/// references resolved against its own tree census, with no whole-pool census. A name the tree +/// does not provide but another tree does refuses (`CrossTreeBareReference`) exactly where the +/// deleted fallback silently pulled a provider; this lists every such refusal, so the pool's +/// dependence on the deleted fallback is counted, by identity, rather than argued. Before the +/// change the fallback census found 13 pool-provided rows in 5 files: 12 the import migration in +/// this change qualifies, and the builtin `get` the builtin arm now keeps from being pulled. #[cfg(test)] -mod pool_fallback_census { +mod cross_tree_bare_census { use super::*; #[test] #[ignore = "live-corpus: prepares or builds over the live tree (minutes per test); the receipts lane runs these with --ignored, the required unit run does not"] - fn pool_fallback_dependents_on_the_live_pool() { + fn no_import_less_file_reaches_across_trees_on_the_live_pool() { let root = process_workspace_root(); let roots: Vec = ["dag", "src/v2"] .iter() @@ -3619,68 +3621,243 @@ mod pool_fallback_census { let index = process_shared_index(&roots); let mut sources: Vec<_> = index.source_files.values().cloned().collect(); sources.sort_by(|a, b| a.path.cmp(&b.path)); - let mut files_scanned = 0usize; - let mut files_demanding_pool = 0usize; + let mut scanned = 0usize; let mut refusals: Vec = Vec::new(); - let mut pool_rows: Vec = Vec::new(); for sf in &sources { if super::super::source_declares_import_lines(&sf.content) { continue; } - files_scanned += 1; - let demanded = std::cell::Cell::new(false); - // Providers each name resolves to when the tree census alone answers. - let mut scoped: BTreeSet<(String, String)> = BTreeSet::new(); - let r = super::super::visit_bare_reference_providers( - sf, - &index, - |root| { - if root.is_none() { - demanded.set(true); - return Ok(crate::v1_compiler_infer_env::empty_symbol_index()); - } - super::super::closure_name_census(&index, root) - }, - |name, module, _| { - scoped.insert((name.to_string(), module.to_string())); - Ok(()) - }, - ); - if !demanded.get() { - if let Err(e) = r { - refusals.push(format!("{}: {e}", sf.path)); - } - continue; - } - files_demanding_pool += 1; - let mut full: BTreeSet<(String, String)> = BTreeSet::new(); + scanned += 1; let r = super::super::visit_bare_reference_providers( sf, &index, |root| super::super::closure_name_census(&index, root), - |name, module, _| { - full.insert((name.to_string(), module.to_string())); - Ok(()) - }, + |_, _, _| Ok(()), ); if let Err(e) = r { - refusals.push(format!("{}: {e}", sf.path)); - } - for (name, module) in full.difference(&scoped) { - pool_rows.push(format!("{} -> {name} -> {module}", sf.path)); + refusals.push(e); } } + let pool_census_built = index.closure_name_censuses.borrow().contains_key(&None); eprintln!( - "FALLBACK files_scanned={files_scanned} files_demanding_pool={files_demanding_pool} \ - pool_answered_rows={} refusals={}", - pool_rows.len(), + "CROSSTREE scanned={scanned} refusals={} pool_census_built={pool_census_built}", refusals.len() ); - for row in pool_rows.iter().take(60) { - eprintln!("FALLBACK row {row}"); + for r in &refusals { + eprintln!("CROSSTREE refusal {r}"); } - for r in refusals.iter().take(10) { - eprintln!("FALLBACK refusal {r}"); + assert!(scanned > 100, "the live pool was read ({scanned} files)"); + assert!( + !pool_census_built, + "a bare resolution still built the whole-pool census" + ); + assert!( + refusals.is_empty(), + "{} files reach across trees", + refusals.len() + ); + } +} + +#[cfg(test)] +mod cross_tree_bare_reference_tests { + use super::*; + + fn write(root: &Path, rel: &str, content: &str) { + let p = root.join(rel); + std::fs::create_dir_all(p.parent().unwrap()).expect("mkdir"); + std::fs::write(&p, content).expect("write dag"); + } + + /// Two source trees. `a/user.dag` references `helper`, declared only in tree `b`, and a builtin + /// `get`, which tree `b` also declares as an ordinary function. Returns the admission verdict + /// of `a/user.dag`. + fn admit_user(tag: &str, user_imports: &str, call_helper: bool) -> Result<(), String> { + let base = process_workspace_root() + .join("target") + .join(format!("gunbc-crosstree-{tag}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + let (a, b) = (base.join("a"), base.join("b")); + let main_body = if call_helper { "helper()" } else { "1" }; + write( + &b, + "helper.dag", + "module tb.helper\n\nfn helper() -> Int {\n 1\n}\n\nfn get(x: Int) -> Int {\n x\n}\n", + ); + write( + &a, + "user.dag", + &format!( + "module ta.user\n{user_imports}\nfn main() -> Int {{\n {main_body}\n}}\n\nfn first() -> Bool {{\n match get(xs: [1, 2], index: 0) {{\n Present {{ value: _ }} => true\n Absent => false\n }}\n}}\n" + ), + ); + let roots = vec![ + a.to_string_lossy().into_owned(), + b.to_string_lossy().into_owned(), + ]; + let index = build_multi_entry_index(&roots); + let user = index + .source_files + .values() + .find(|sf| sf.path.ends_with("a/user.dag")) + .cloned() + .expect("user source indexed"); + let verdict = super::super::admit_bare_references_of_file(&index, &user); + let _ = std::fs::remove_dir_all(&base); + verdict + } + + fn admit_user_qualified(tag: &str) -> Result<(), String> { + let base = process_workspace_root() + .join("target") + .join(format!("gunbc-crosstree-{tag}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + let (a, b) = (base.join("a"), base.join("b")); + write( + &b, + "helper.dag", + "module tb.helper\n\nfn helper() -> Int {\n 1\n}\n", + ); + write( + &a, + "user.dag", + "module ta.user\n\nfn main() -> Int {\n tb.helper.helper()\n}\n", + ); + let roots = vec![ + a.to_string_lossy().into_owned(), + b.to_string_lossy().into_owned(), + ]; + let index = build_multi_entry_index(&roots); + let user = index + .source_files + .values() + .find(|sf| sf.path.ends_with("a/user.dag")) + .cloned() + .expect("user source indexed"); + let verdict = super::super::admit_bare_references_of_file(&index, &user); + let _ = std::fs::remove_dir_all(&base); + verdict + } + + /// THE RED: an unimported bare reference to a name only another source tree declares refuses, + /// typed and located, where the deleted pool fallback silently resolved it. + #[test] + fn an_unimported_cross_tree_bare_name_refuses() { + let err = admit_user("red", "", true).expect_err("a cross-tree bare reference must refuse"); + assert!(err.contains("CrossTreeBareReference"), "{err}"); + assert!(err.contains("'helper'"), "{err}"); + assert!(err.contains("`tb.helper.helper`"), "{err}"); + } + + /// A BUILTIN IS NOT A CROSS-TREE REFERENCE: `get` is the builtin even though tree `b` declares + /// an ordinary `fn get`. The deleted fallback pulled that function in; the rule neither pulls + /// it nor refuses. + #[test] + fn a_builtin_named_like_another_trees_function_admits() { + admit_user("builtin", "", false).expect("the builtin get admits without a provider"); + } + + /// The positive control: the same reference written qualified (`tb.helper.helper()`) admits, + /// and the file stays import-less, so its other bare references are still followed. + #[test] + fn the_same_reference_written_qualified_admits() { + admit_user_qualified("green").expect("a qualified cross-tree reference admits"); + } +} + +/// The five files the cross-tree census enumerated resolve as entries over the live pool after +/// the migration: the four that now reference across trees qualified, and the builtin `get` claim that no +/// longer pulls another tree's `fn get`. +#[cfg(test)] +mod cross_tree_migrated_entries_resolve { + use super::*; + #[test] + #[ignore = "live-corpus: prepares or builds over the live tree (minutes per test); the receipts lane runs these with --ignored, the required unit run does not"] + fn migrated_entries_resolve_on_the_live_pool() { + let root = process_workspace_root(); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| root.join(r).to_string_lossy().into_owned()) + .collect(); + let index = process_shared_index(&roots); + for e in [ + "src/v2/test/claim/auth_declared_but_unwired_witness_test.dag", + "src/v2/test/claim/bootstrap_test.dag", + "src/v2/test/claim/infer_semantics_witness_test.dag", + "src/v2/test/claim/manual/path_y_fidelity_successor_test.dag", + "dag/test/claim/builtin_get_resolver_test.dag", + ] { + let entry = root.join(e); + let r = resolve_entry_with_index_for_discovery_corpus(&index, &entry.to_string_lossy()); + eprintln!("MIGRATED {e} ok={}", r.is_ok()); + if let Err(err) = &r { + eprintln!("MIGRATED {}", err.chars().take(600).collect::()); + } + assert!(r.is_ok(), "{e} resolves"); + } + } +} + +/// THE PER-NAME CLAIM, for every name of the live pool: the whole-pool name census's entry for a +/// name (bare lookup state with candidates, and service entry) equals the entry +/// `pool_census_for_name` builds over the name's declaring modules alone. This is what licenses +/// answering the loader's out-of-tree question without building the pool census. +#[cfg(test)] +mod pool_census_for_name_differential { + use super::*; + #[test] + #[ignore = "live-corpus: prepares or builds over the live tree (minutes per test); the receipts lane runs these with --ignored, the required unit run does not"] + fn per_name_census_equals_the_pool_census_for_every_name_on_the_live_pool() { + let root = process_workspace_root(); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| root.join(r).to_string_lossy().into_owned()) + .collect(); + let index = process_shared_index(&roots); + let pool = super::super::closure_name_census(&index, None).expect("pool census"); + let decl = reference_pool_names_for_index(&index).expect("names"); + let mut names: BTreeSet = BTreeSet::new(); + names.extend(v1_rt::sorted_map_keys(&pool.global_bare)); + names.extend(v1_rt::sorted_map_keys(&pool.services)); + names.extend(decl.decl_index.keys().cloned()); + // One per-name census per distinct declaring-module set: names that share their declaring + // modules get the same census from `pool_census_for_name`, so each set is built once. + let mut by_set: BTreeMap, Vec> = BTreeMap::new(); + for name in &names { + let set: Vec = decl + .decl_index + .get(name) + .map(|m| m.iter().cloned().collect()) + .unwrap_or_default(); + by_set.entry(set).or_default().push(name.clone()); + } + eprintln!("PERNAME distinct_declaring_sets={}", by_set.len()); + let mut divergent: Vec = Vec::new(); + for group in by_set.values() { + let local = + super::super::pool_census_for_name(&index, &group[0]).expect("per-name census"); + for name in group { + if v1_rt::map_get(&pool.global_bare, name.clone()) + != v1_rt::map_get(&local.global_bare, name.clone()) + { + divergent.push(format!("{name} (bare)")); + } + if v1_rt::map_get(&pool.services, name.clone()) + != v1_rt::map_get(&local.services, name.clone()) + { + divergent.push(format!("{name} (service)")); + } + } + } + eprintln!( + "PERNAME names={} divergent={}", + names.len(), + divergent.len() + ); + for d in divergent.iter().take(30) { + eprintln!("PERNAME divergent {d}"); } + assert!(names.len() > 1000, "the live pool was read"); + assert!(divergent.is_empty(), "{} names diverge", divergent.len()); } } diff --git a/src/v2/test/claim/auth_declared_but_unwired_witness_test.dag b/src/v2/test/claim/auth_declared_but_unwired_witness_test.dag index 3c3b16674cd..e81a7a60853 100644 --- a/src/v2/test/claim/auth_declared_but_unwired_witness_test.dag +++ b/src/v2/test/claim/auth_declared_but_unwired_witness_test.dag @@ -4,5 +4,5 @@ module v2.test.claim.auth_declared_but_unwired_witness data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly test fn auth_declared_but_unwired_witness_keystone_holds() -> Bool { - run_auth_declared_but_unwired_witness() + tools.auth_declared_but_unwired_witness_transport.run_auth_declared_but_unwired_witness() } diff --git a/src/v2/test/claim/bootstrap_test.dag b/src/v2/test/claim/bootstrap_test.dag index a5e71e4e001..b74f8af82c9 100644 --- a/src/v2/test/claim/bootstrap_test.dag +++ b/src/v2/test/claim/bootstrap_test.dag @@ -4,5 +4,5 @@ module v2.test.claim.bootstrap data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly test fn bootstrap_witness_keystone_holds() -> Bool { - run_bootstrap_witness() + tools.bootstrap_witness_transport.run_bootstrap_witness() } diff --git a/src/v2/test/claim/infer_semantics_witness_test.dag b/src/v2/test/claim/infer_semantics_witness_test.dag index 90087683da9..516728dd8b1 100644 --- a/src/v2/test/claim/infer_semantics_witness_test.dag +++ b/src/v2/test/claim/infer_semantics_witness_test.dag @@ -4,5 +4,5 @@ module v2.test.claim.infer_semantics_witness data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly test fn infer_semantics_witness_keystone_holds() -> Bool { - run_infer_semantics_witness() + tools.infer_semantics_witness_transport.run_infer_semantics_witness() } diff --git a/src/v2/test/claim/manual/path_y_fidelity_successor_test.dag b/src/v2/test/claim/manual/path_y_fidelity_successor_test.dag index 8e29af22192..fe7678ba8e7 100644 --- a/src/v2/test/claim/manual/path_y_fidelity_successor_test.dag +++ b/src/v2/test/claim/manual/path_y_fidelity_successor_test.dag @@ -2,16 +2,16 @@ module v2.test.manual.path_y_fidelity_successor data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -fn path_y_core_expr() -> Expression { - LogicalOr { - left: LogicalOr { +fn path_y_core_expr() -> extdeps.github.expressions.Expression { + extdeps.github.expressions.LogicalOr { + left: extdeps.github.expressions.LogicalOr { left: extdeps.github.expressions.FunctionCall { - function: HashFiles, - args: [ StringLiteral { value: "Cargo.lock" }, StringLiteral { value: "Cargo.toml" } ], + function: extdeps.github.expressions.HashFiles, + args: [ extdeps.github.expressions.StringLiteral { value: "Cargo.lock" }, extdeps.github.expressions.StringLiteral { value: "Cargo.toml" } ], }, - right: ContextAccess { context: Github, path: ["event", "number"] }, + right: extdeps.github.expressions.ContextAccess { context: extdeps.github.expressions.Github, path: ["event", "number"] }, }, - right: ContextAccess { context: Runner, path: ["os"] }, + right: extdeps.github.expressions.ContextAccess { context: extdeps.github.expressions.Runner, path: ["os"] }, } } @@ -53,7 +53,7 @@ fn gha_roundtrip_fidelity_matches_target_quotient() -> Bool { } fn gha_core_advertises_target_quotient_fidelity() -> Bool { - match ingest_expression(src: serialize_expression(expression: path_y_core_expr())) { + match extdeps.github.expressions.ingest_expression(src: extdeps.github.expressions.serialize_expression(expression: path_y_core_expr())) { Present { value: v } => match gha_expression_core_roundtrip_fidelity() { Accepted { value: fidelity, diagnostics: _ } => { From 4413666662f4dec5c0b440ff40a0cca77a6c2e37 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 03:20:52 +0000 Subject: [PATCH 10/24] Transitive bare-pick defect: failure-mode row and pinned specimen; bare admission instrument Co-Authored-By: Claude Opus 5.5 (1M context) --- ...guous_bare_name_into_a_transitive_pick.dag | 17 +++ src/v1/stage0/src/cli_run.rs | 28 +++++ src/v1/stage0/src/cli_run/entry_resolve.rs | 102 ++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 dag/gunbc/recurring_failure_mode/an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick.dag diff --git a/dag/gunbc/recurring_failure_mode/an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick.dag b/dag/gunbc/recurring_failure_mode/an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick.dag new file mode 100644 index 00000000000..dd0d24d5399 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick.dag @@ -0,0 +1,17 @@ +module gunbc.recurring_failure_mode.an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick + +import std.types { NonEmptyStr } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick: RecurringFailureMode = RecurringFailureMode { + identity: "an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick" as NonEmptyStr, + + receipts: [ + "INVALID STATE: adding one import to a .dag file changes how an UNRELATED bare name in that file resolves. Without imports, a bare name two modules of the file's source tree declare refuses as ambiguous; with any import, the same bare name binds silently to whichever declaring module the import CLOSURE happens to reach -- including through the imported module's own imports -- because an import-bearing file's bare names resolve against its closure-scoped census first (closure wins the intersection) and the loader follows no bare references for it. HARM: silent wrong resolution -- a name the author never imported, and that is ambiguous in their tree, is bound to a transitively reachable homonym, and editing an import anywhere upstream can rebind it.", + "SPECIMEN (fixture, measured by bold-bat-516 on 2026-09-30 in the resolver-cost lane): tree ta has ta.dep (fn bar -> 1, fn z), ta.other (fn bar -> 2), ta.lib (import ta.dep { z }). ta.bare_user (no imports) calling bar() refuses: 'ambiguous reference bar: 2 candidates: ta.dep.bar, ta.other.bar'. ta.import_user, identical but with 'import ta.lib { y }', RESOLVES, binding bar to ta.dep.bar. Found while classifying the cliff gunbc#12741 exposed: an import-bearing file loses all its bare pulls (path_y_fidelity_successor_test lost decode_fidelity_from_target's module when an import was added -- that half was loud).", + "DISTINGUISHING FACTS: the file has at least one import; the bare name is not among the imported members; the file's source tree declares it in two or more modules; exactly one of them is in the file's transitive import closure. The same file with its imports removed refuses.", + "RUNG FOUND AT: silent (outside the ladder). CEILING: structurally guaranteed -- a bare name's meaning is a function of the file's own declarations and the names it explicitly imports, never of what an import transitively reaches, so ambiguity in the author's scope refuses regardless of import presence. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: one bare-name authority for the loader and the typecheck, keyed by the referencing file's declared scope (own tree plus explicitly imported members), so that closure membership reached through another module's imports never contributes a bare binding. Until then the mitigation is the CrossTreeBareReference advice in gunbc#12741: write a cross-tree reference qualified rather than adding an import.", + ], + + evidence: [], +} diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index ab00c58841e..5b595e514c8 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -9596,6 +9596,31 @@ fn admit_pool_bare_references(index: &MultiEntryIndex) -> Result<(), String> { /// the same declarations the census folds). The claim is checked for every name of the live pool /// by `entry_resolve::pool_census_for_name_differential`. fn pool_census_for_name(index: &MultiEntryIndex, name: &str) -> Result, String> { + let started = std::time::Instant::now(); + #[cfg(test)] + PER_NAME_CENSUS_DISTINCT.with(|d| { + d.borrow_mut().insert(name.to_string()); + }); + let census = pool_census_for_name_uncounted(index, name); + resolve_stage_slot_add(|st| { + st.bare_per_name_census_calls += 1; + st.bare_per_name_census += started.elapsed().as_nanos(); + }); + census +} + +#[cfg(test)] +thread_local! { + /// Distinct names `pool_census_for_name` was asked about on this thread: with the call count + /// in `ResolveStageNanos`, the repetition a shared answer would remove. + pub(crate) static PER_NAME_CENSUS_DISTINCT: RefCell> = + RefCell::new(std::collections::HashSet::new()); +} + +fn pool_census_for_name_uncounted( + index: &MultiEntryIndex, + name: &str, +) -> Result, String> { let pool = pool_parse(index)?; let names = entry_resolve::reference_pool_names_for_index(index)?; let Some(modules) = names.decl_index.get(name) else { @@ -15532,6 +15557,9 @@ pub struct ResolveStageNanos { pub edge_index_bare_candidates: u128, pub edge_index_bare_name_universe: u128, pub edge_index_bare_resolve_loop: u128, + /// `pool_census_for_name`: the out-of-tree question per bare name -- calls and their time. + pub bare_per_name_census_calls: u128, + pub bare_per_name_census: u128, /// `Rc::new` + hand-off of the finished index. pub edge_index_publish: u128, /// `build_both_closure_edge_index` (memoized on the index; nonzero here is the first build). diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 36d21760548..7c369bc2a0a 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -3877,3 +3877,105 @@ mod pool_census_for_name_differential { assert!(divergent.is_empty(), "{} names diverge", divergent.len()); } } + +/// THE SPECIMEN of `gunbc.recurring_failure_mode.an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick`, +/// a v1 semantic defect owned by the resolver lane (routed by neat-boar-16), not by this change. +/// One source tree declares `bar` in `ta.dep` and `ta.other`. With no imports a bare `bar()` +/// refuses as ambiguous; with one unrelated import whose module imports `ta.dep`, the same call +/// RESOLVES -- silently binding `bar` to the transitively reached `ta.dep.bar`. This test pins +/// that behaviour as observed. WHEN THE DEFECT IS FIXED IT MUST FAIL: flip its second assertion +/// to expect the ambiguity refusal and keep it as the regression control (DESIGN §4b(4)). +#[cfg(test)] +mod import_transitive_bare_pick_specimen { + use super::*; + + fn w(root: &Path, rel: &str, c: &str) { + let p = root.join(rel); + std::fs::create_dir_all(p.parent().unwrap()).expect("mkdir"); + std::fs::write(p, c).expect("write dag"); + } + + #[test] + fn an_unrelated_import_turns_an_ambiguous_bare_name_into_a_transitive_pick() { + let base = process_workspace_root() + .join("target") + .join(format!("gunbc-import-pick-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + let a = base.join("a"); + w( + &a, + "dep.dag", + "module ta.dep\n\nfn bar() -> Int {\n 1\n}\n\nfn z() -> Int {\n 0\n}\n", + ); + w( + &a, + "other.dag", + "module ta.other\n\nfn bar() -> Int {\n 2\n}\n", + ); + w( + &a, + "lib.dag", + "module ta.lib\n\nimport ta.dep { z }\n\nfn y() -> Int {\n z()\n}\n", + ); + w( + &a, + "bare_user.dag", + "module ta.bare_user\n\nfn main() -> Int {\n bar()\n}\n", + ); + w( + &a, + "import_user.dag", + "module ta.import_user\n\nimport ta.lib { y }\n\nfn main() -> Int {\n bar()\n}\n", + ); + let index = build_multi_entry_index(&[a.to_string_lossy().into_owned()]); + let bare = resolve_entry_with_index(&index, &a.join("bare_user.dag").to_string_lossy()); + let imported = + resolve_entry_with_index(&index, &a.join("import_user.dag").to_string_lossy()); + let _ = std::fs::remove_dir_all(&base); + let err = bare.expect_err("with no imports, an ambiguous bare name refuses"); + assert!(err.contains("ambiguous reference 'bar'"), "{err}"); + // THE DEFECT, pinned as observed: flip to expect_err when it is fixed. + imported.expect("observed: one unrelated import makes the same bare name resolve"); + } +} + +/// THE INSTRUMENT for the floor's whole-pool bare admission (`admit_pool_bare_references`) on the +/// live `[dag, src/v2]` pool, with what both routes share -- the pool heads parse, both tree name +/// censuses and the heads name index -- warmed first, so the timed term is the admission alone. +/// It prints the admission time, the per-name census calls and time (`ResolveStageNanos`) and the +/// distinct names asked; calls against distinct names is the repetition a shared answer would +/// remove. It reports; it asserts only that the admission completes. +#[cfg(test)] +mod live_pool_bare_admission_attribution { + use super::*; + #[test] + #[ignore = "live-corpus: prepares or builds over the live tree (minutes per test); the receipts lane runs these with --ignored, the required unit run does not"] + fn live_pool_bare_admission_attribution() { + let root = process_workspace_root(); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| root.join(r).to_string_lossy().into_owned()) + .collect(); + let index = process_shared_index(&roots); + let _ = super::super::pool_parse(&index).expect("pool parse"); + for r in index.source_roots.iter() { + let _ = super::super::closure_name_census(&index, Some(r)).expect("tree census"); + } + let _ = reference_pool_names_for_index(&index).expect("names"); + let before = resolve_stage_totals(); + let t = std::time::Instant::now(); + let verdict = super::super::admit_pool_bare_references(&index); + let elapsed = t.elapsed(); + let after = resolve_stage_totals(); + let distinct = super::super::PER_NAME_CENSUS_DISTINCT.with(|d| d.borrow().len()); + eprintln!( + "ADMIT whole_pool_admission={elapsed:?} ok={} per_name_calls={} per_name_ms={} \ + per_name_distinct={distinct} pool_census_built={}", + verdict.is_ok(), + after.bare_per_name_census_calls - before.bare_per_name_census_calls, + (after.bare_per_name_census - before.bare_per_name_census) / 1_000_000, + index.closure_name_censuses.borrow().contains_key(&None), + ); + verdict.expect("the live pool admits"); + } +} From 0322af3dcca7bb14fbe51e0aae2b63230c0486c0 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 04:39:08 +0000 Subject: [PATCH 11/24] Per-name bare census: prune to the name's declarations; derive the heads name index once per index; transitive-pick row rung and trigger restated Co-Authored-By: Claude Opus 5.5 (1M context) --- ...guous_bare_name_into_a_transitive_pick.dag | 2 +- src/v1/stage0/src/cli_run.rs | 38 ++++++++++++++++++- src/v1/stage0/src/cli_run/entry_resolve.rs | 31 ++++++++------- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick.dag b/dag/gunbc/recurring_failure_mode/an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick.dag index dd0d24d5399..c43727f60d2 100644 --- a/dag/gunbc/recurring_failure_mode/an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick.dag +++ b/dag/gunbc/recurring_failure_mode/an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick.dag @@ -10,7 +10,7 @@ data an_import_turns_an_ambiguous_bare_name_into_a_transitive_pick: RecurringFai "INVALID STATE: adding one import to a .dag file changes how an UNRELATED bare name in that file resolves. Without imports, a bare name two modules of the file's source tree declare refuses as ambiguous; with any import, the same bare name binds silently to whichever declaring module the import CLOSURE happens to reach -- including through the imported module's own imports -- because an import-bearing file's bare names resolve against its closure-scoped census first (closure wins the intersection) and the loader follows no bare references for it. HARM: silent wrong resolution -- a name the author never imported, and that is ambiguous in their tree, is bound to a transitively reachable homonym, and editing an import anywhere upstream can rebind it.", "SPECIMEN (fixture, measured by bold-bat-516 on 2026-09-30 in the resolver-cost lane): tree ta has ta.dep (fn bar -> 1, fn z), ta.other (fn bar -> 2), ta.lib (import ta.dep { z }). ta.bare_user (no imports) calling bar() refuses: 'ambiguous reference bar: 2 candidates: ta.dep.bar, ta.other.bar'. ta.import_user, identical but with 'import ta.lib { y }', RESOLVES, binding bar to ta.dep.bar. Found while classifying the cliff gunbc#12741 exposed: an import-bearing file loses all its bare pulls (path_y_fidelity_successor_test lost decode_fidelity_from_target's module when an import was added -- that half was loud).", "DISTINGUISHING FACTS: the file has at least one import; the bare name is not among the imported members; the file's source tree declares it in two or more modules; exactly one of them is in the file's transitive import closure. The same file with its imports removed refuses.", - "RUNG FOUND AT: silent (outside the ladder). CEILING: structurally guaranteed -- a bare name's meaning is a function of the file's own declarations and the names it explicitly imports, never of what an import transitively reaches, so ambiguity in the author's scope refuses regardless of import presence. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: one bare-name authority for the loader and the typecheck, keyed by the referencing file's declared scope (own tree plus explicitly imported members), so that closure membership reached through another module's imports never contributes a bare binding. Until then the mitigation is the CrossTreeBareReference advice in gunbc#12741: write a cross-tree reference qualified rather than adding an import.", + "RUNG FOUND AT AND CURRENT RUNG: silent wrongness -- below the ladder, not on it -- and LIVE until the cut named below; no mechanism detects or refuses it today. The pinned specimen records the defect as observed, it does not prevent it. CEILING: structurally guaranteed -- a bare name's meaning is a function of the file's own declarations and the names it explicitly imports, never of what an import transitively reaches. WHY IT CANNOT BE CLOSED ALONE (quiet-gull-780, 2026-09-30): the transitive leak currently MASKS consumer-scope re-resolution of foreign field types -- a field whose declared type is foreign to the reading module is re-resolved by bare name in the reader's scope and today finds its type through the transitively flattened bare-name layer -- so removing that layer by itself breaks generation 2. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: one cut in which EVERY foreign-field-type reader reads the field's declaration identity (Node.declaration, the transition quiet-hawk-702 owns) instead of re-resolving it by name, AND the transitive bare-name layer (build_ancestry_precedence ancestry_str_bindings flattening every import's cache) is removed, so bare names start from the kernel plus direct-import selections. A declared-scope bare-name fix without the declaration-identity migration is narrower than this capability and does not retire the row. Until the cut, the mitigation is the CrossTreeBareReference advice in gunbc#12741: write a cross-tree reference qualified rather than adding an import.", ], evidence: [], diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 5b595e514c8..b932badac83 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -9617,6 +9617,28 @@ thread_local! { RefCell::new(std::collections::HashSet::new()); } +/// `module` with only the items that declare `name`: an item named `name`, or a `Disj` type +/// definition one of whose variants is named `name`. +fn module_pruned_to_declarations_of(module: &Rc, name: &str) -> Rc { + use crate::v1_compiler_emit_core_support::is_type_def_item; + use crate::v1_std_core::Connective; + let children: im::Vector> = module + .children + .iter() + .filter(|item| { + item.name == name + || (is_type_def_item((*item).clone()) + && item.connective == Connective::Disj + && item.children.iter().any(|v| v.name == name)) + }) + .cloned() + .collect(); + Rc::new(Node { + children: Rc::new(children), + ..(**module).clone() + }) +} + fn pool_census_for_name_uncounted( index: &MultiEntryIndex, name: &str, @@ -9626,11 +9648,16 @@ fn pool_census_for_name_uncounted( let Some(modules) = names.decl_index.get(name) else { return Ok(crate::v1_compiler_infer_env::empty_symbol_index()); }; + // Each declaring module contributes only the items that DECLARE `name` -- an item of that + // name, or a `Disj` type with a variant of that name (the same declarations + // `collect_module_decl_names` indexes). Every entry, count and gate for `name` reads those + // items and no others, so the census over the pruned modules has the same entry for `name`, + // at a cost in the name's declarations rather than in the declaring modules' size. let nodes: im::Vector> = modules .iter() .filter_map(|m| index.source_files.get(m)) .filter_map(|sf| pool.position_by_file.get(&sf.path)) - .map(|&i| pool.nodes_by_file[i].1.clone()) + .map(|&i| module_pruned_to_declarations_of(&pool.nodes_by_file[i].1, name)) .collect(); Ok(v1_compiler_infer::build_symbol_index_census_raw_nodes( Rc::new(nodes), @@ -13100,6 +13127,9 @@ pub struct MultiEntryIndex { /// newline indexes) — the shared input of the qualified fill and the per-tree /// bare layers below. Entry-independent, built once per process. pool_parse: RefCell>>, + /// The heads name index over `pool_parse` (`entry_resolve::reference_pool_names_for_index`): + /// a fact of this index, demanded per out-of-tree bare name, so derived once here. + reference_pool_names: RefCell>>, /// Whole-pool QUALIFIED-ONLY census layer (entries keyed by qualified name; /// empty global_bare/services), built once per process and underlaid beneath /// each entry's closure census (namespace-resolution-design.md §7.5: "fill = @@ -13428,6 +13458,7 @@ pub fn drop_private_term_for_test(index: &MultiEntryIndex, term: &str) -> bool { "normalize_diag_cache" => index.normalize_diag_cache.borrow_mut().clear(), "ownership_diag_cache" => index.ownership_diag_cache.borrow_mut().clear(), "pool_parse" => *index.pool_parse.borrow_mut() = None, + "reference_pool_names" => *index.reference_pool_names.borrow_mut() = None, "pool_qualified_fill" => *index.pool_qualified_fill.borrow_mut() = None, "tree_bare_census" => index.tree_bare_census.borrow_mut().clear(), "pool_bare_census" => *index.pool_bare_census.borrow_mut() = None, @@ -13490,6 +13521,7 @@ pub fn drop_attributable_terms_for_test() -> &'static [&'static str] { "typed_module_cache", "parse_cache", "pool_parse", + "reference_pool_names", "both_closure_edges", "closure_name_censuses", "bare_reference_admission", @@ -15621,6 +15653,8 @@ impl ResolveStageNanos { self.edge_index_bare_candidates += other.edge_index_bare_candidates; self.edge_index_bare_name_universe += other.edge_index_bare_name_universe; self.edge_index_bare_resolve_loop += other.edge_index_bare_resolve_loop; + self.bare_per_name_census_calls += other.bare_per_name_census_calls; + self.bare_per_name_census += other.bare_per_name_census; self.edge_index_publish += other.edge_index_publish; self.load_pool_reference_closure += other.load_pool_reference_closure; self.load_fixpoint_rounds += other.load_fixpoint_rounds; @@ -15739,6 +15773,8 @@ thread_local! { edge_index_bare_candidates: 0, edge_index_bare_name_universe: 0, edge_index_bare_resolve_loop: 0, + bare_per_name_census_calls: 0, + bare_per_name_census: 0, edge_index_publish: 0, load_bare_edge_index: 0, load_bare_path_lookup: 0, diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 7c369bc2a0a..4f5b87660c4 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -1069,6 +1069,7 @@ pub(crate) fn new_multi_entry_index_shell( schedule_retention: RefCell::new(None), source_roots: source_roots.to_vec(), pool_parse: RefCell::new(None), + reference_pool_names: RefCell::new(None), pool_qualified_fill: RefCell::new(None), tree_bare_census: RefCell::new(std::collections::HashMap::new()), #[cfg(any(test, feature = "interp_test_witness"))] @@ -2728,9 +2729,18 @@ impl ReferencePoolNames { /// The name index from the POOL CENSUS'S OWN heads reading (`pool_parse`), which every resolve /// through this index already forces for its qualified fill and bare census. A resolve therefore /// reads the pool's heads once, not once for the census and again for reference edges. +/// +/// ONE DERIVATION PER INDEX. The index is a function of `pool_parse`, which this index holds for +/// its life, and it is demanded once per out-of-tree bare name (`pool_census_for_name`) -- so its +/// least common ancestor is the index, and it is derived there once rather than rebuilt from the +/// whole pool's heads on every demand (measured: ~230ms per rebuild, 3,852 demands in one +/// whole-pool admission). pub(crate) fn reference_pool_names_for_index( index: &MultiEntryIndex, ) -> Result, String> { + if let Some(names) = index.reference_pool_names.borrow().clone() { + return Ok(names); + } let pool = pool_parse(index)?; let started = std::time::Instant::now(); let names = Rc::new(ReferencePoolNames::from_heads_modules( @@ -2743,6 +2753,7 @@ pub(crate) fn reference_pool_names_for_index( super::pre_entry_phase::PhaseScale::Tree, started.elapsed(), ); + *index.reference_pool_names.borrow_mut() = Some(names.clone()); Ok(names) } @@ -3836,23 +3847,11 @@ mod pool_census_for_name_differential { names.extend(v1_rt::sorted_map_keys(&pool.global_bare)); names.extend(v1_rt::sorted_map_keys(&pool.services)); names.extend(decl.decl_index.keys().cloned()); - // One per-name census per distinct declaring-module set: names that share their declaring - // modules get the same census from `pool_census_for_name`, so each set is built once. - let mut by_set: BTreeMap, Vec> = BTreeMap::new(); - for name in &names { - let set: Vec = decl - .decl_index - .get(name) - .map(|m| m.iter().cloned().collect()) - .unwrap_or_default(); - by_set.entry(set).or_default().push(name.clone()); - } - eprintln!("PERNAME distinct_declaring_sets={}", by_set.len()); let mut divergent: Vec = Vec::new(); - for group in by_set.values() { - let local = - super::super::pool_census_for_name(&index, &group[0]).expect("per-name census"); - for name in group { + { + for name in &names { + let local = + super::super::pool_census_for_name(&index, name).expect("per-name census"); if v1_rt::map_get(&pool.global_bare, name.clone()) != v1_rt::map_get(&local.global_bare, name.clone()) { From e0727d74c4c918b9a1946b28dfc6737ee218547b Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 13:55:20 +0000 Subject: [PATCH 12/24] Retire 12 unimported-bare-provider get pairs as ImportsFixed The builtin arm no longer pulls another tree's fn get for a builtin get call, so these pairs no longer occur (floor: RosterStale ... retire it as ImportsFixed). Co-Authored-By: Claude Opus 5.5 (1M context) --- ...r_unimported_bare_provider_debt_roster.dag | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag b/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag index fdebb71cb53..661f3e0b63f 100644 --- a/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag +++ b/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag @@ -118,7 +118,7 @@ data unimported_bare_provider_dispositions: List Date: Wed, 30 Sep 2026 15:01:03 +0000 Subject: [PATCH 13/24] Changed-witness sublane: typed DeclinedNoCiWetLane for edited BinWitnessWet rows, with its declared rung drop A changed witness whose file is a BinWitnessWet WitnessExclusionRow is declined, counted and located per row, instead of planned into a certain route gap: no CI lane executes that class. The loss is declared as gunbc.rung_drop.edited_bin_witness_wet_rows_not_executed_by_ci (appended to the roster; docs/design-rung-drops.md regenerated). Co-Authored-By: Claude Opus 5.5 (1M context) --- ...in_witness_wet_rows_not_executed_by_ci.dag | 83 +++++++++++++++++++ dag/gunbc/rung_drop/roster.dag | 2 + docs/design-rung-drops.md | 4 + src/v1/stage0/src/cli_run.rs | 17 +++- .../src/cli_run/required_floor_runner.rs | 54 +++++++++++- 5 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 dag/gunbc/rung_drop/edited_bin_witness_wet_rows_not_executed_by_ci.dag diff --git a/dag/gunbc/rung_drop/edited_bin_witness_wet_rows_not_executed_by_ci.dag b/dag/gunbc/rung_drop/edited_bin_witness_wet_rows_not_executed_by_ci.dag new file mode 100644 index 00000000000..d217c1f9d1a --- /dev/null +++ b/dag/gunbc/rung_drop/edited_bin_witness_wet_rows_not_executed_by_ci.dag @@ -0,0 +1,83 @@ +module gunbc.rung_drop.edited_bin_witness_wet_rows_not_executed_by_ci + +import std.types { NonEmptyStr } +import gunbc.rung_drop { RungDrop, Standing, TypedDeclaration, LostAsPassenger } +import gunbc.guarantee_rung { Mitigatable, MechanicallyPreventable } + +// THE DECLARED RUNG DROP for an EDITED BinWitnessWet witness (DESIGN 4b(3)), landed with the +// required floor's `DeclinedNoCiWetLane` changed-witness standing (floor owner deep-ferret-305, +// ruling via jolly-boar-500, 2026-09-30). +// +// WHAT WAS THE RUNG. The changed-witness sublane blocks a PR whose edited witness produces no +// terminal verdict, so an edit to a BinWitnessWet file could not merge without executing: +// mechanically preventable. It could not execute either -- its claims drive compiled seed witness +// binaries on host effects the hermetic floor refuses (HostEffectRefused -> NO-ROUTE) -- so the +// rule forbade touching the class at all, including by the work that must touch it (gunbc#12741 +// qualified three keystones' cross-tree references and was blocked by exactly this). +// +// WHAT IT IS NOW. Such an edit is DECLINED, counted and located per row with the exclusion pattern +// that matched, and non-blocking. Nothing in CI executes it. The mitigation is procedural: a PR +// that edits one carries a real bin_wet receipt (the entry run by `gunbc run --claim-run` with its +// witness binaries built), which covers that PR only. +// +// THE REASON IS LOST-AS-PASSENGER. The class was executed per PR by the bin-witness wet batch, +// which died with the floor cut of 2026-08-15; the cadence that might have taken it left with +// falsifier.yml (gunbc#8283). `gunbc.rung_drop.deleted_cadence_reference` declares the class's +// general non-execution (`gunbc.ci_layer_roots bin_witness_wet_note`); this row declares the +// narrower loss the changed-witness decline adds on top of it. +// +// THE POPULATION is every BinWitnessWet `WitnessExclusionRow` pattern at declaration; a row added +// to that classification later joins it by the same rule. +// +// THE HARM IS OBSERVED, NOT HYPOTHETICAL. The bin_wet receipt run for gunbc#12741 on 2026-09-30 +// found `v2.test.claim.auth_declared_but_unwired_witness auth_declared_but_unwired_witness_keystone_holds` +// FAILING on main at 7e2ddfee90 as well: its binary panics in +// `auth_declared_no_source_fails_closed_pre_send` with `undefined variable 'Bearer'`. A member of +// this population had broken with nothing in CI able to notice, which is exactly what this row +// declares. + +data edited_bin_witness_wet_rows_not_executed_by_ci: RungDrop = RungDrop { + identity: "edited_bin_witness_wet_rows_not_executed_by_ci" as NonEmptyStr, + + subject: "an edited witness file classified BinWitnessWet in gunbc.ci_layer_roots witness_exclusion_frontier: the required floor's changed-witness sublane declines it as DeclinedNoCiWetLane instead of blocking, and no CI lane executes it", + + declared: "2026-09-30", + + standing: Standing, + + declaration: TypedDeclaration { + previous: MechanicallyPreventable, + temporary: Mitigatable, + reason: LostAsPassenger { carrier: "the per-PR bin-witness wet batch, deleted with the floor cut of 2026-08-15, and .github/workflows/falsifier.yml, deleted at 611fd02770 (gunbc#8283)" }, + population: [ + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern self_host_artifact_materialization_real_execution_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern stage0_regen_convergence_real_execution_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern typed_witness_invocation_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern namespace_structural_root_exposure_generated_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern emit_host_typed_smoke_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern build_artifact_corruption_probe_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_collect_fingerprint_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_perturb_receipts_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/diagnostics_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern effects_rest_transport_parse_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/parse_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern v1_dag_parse_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern auth_declared_but_unwired_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/bootstrap_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern infer_semantics_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_shard_a_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_shard_totality_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_seam_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern run_verdict_exit_status_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern http_client_get_real_execution_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern roadmap_belt_actuate_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern host_build_cache_provision_real_execution_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern materialized_ssh_key_file_real_execution_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern proc_self_cgroup_real_execution_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern repo_local_git_config_real_execution_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern transport_script_stdin_byte_fidelity_witness_test.dag", + "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern push_event_witness_wet_test.dag", + ], + restoration_trigger: "a REQUIRED lane executes changed BinWitnessWet rows for the same pull_request or merge_group -- builds their witness binaries and runs the edited entries to a terminal verdict on a host that admits their effects -- so the changed-witness sublane can plan them instead of declining; sufficient for deleting DeclinedNoCiWetLane and this row together", + } +} diff --git a/dag/gunbc/rung_drop/roster.dag b/dag/gunbc/rung_drop/roster.dag index 65152f38e6b..b1a6e494dd6 100644 --- a/dag/gunbc/rung_drop/roster.dag +++ b/dag/gunbc/rung_drop/roster.dag @@ -113,6 +113,7 @@ import gunbc.rung_drop.compiler_change_refusals_land_outside_every_compiled_clos import gunbc.rung_drop.retention_census_counts_one_retention_per_refused_module { retention_census_counts_one_retention_per_refused_module } import gunbc.rung_drop.typed_statement_let_refuses_until_the_bind_annotation_carrier { typed_statement_let_refuses_until_the_bind_annotation_carrier } import gunbc.rung_drop.python_to_typescript_compile_inhabitance_off_the_required_gate { python_to_typescript_compile_inhabitance_off_the_required_gate } +import gunbc.rung_drop.edited_bin_witness_wet_rows_not_executed_by_ci { edited_bin_witness_wet_rows_not_executed_by_ci } data rung_drop_roster: List = [ floor_cut_heal, @@ -209,6 +210,7 @@ data rung_drop_roster: List = [ typed_statement_let_refuses_until_the_bind_annotation_carrier, mtcollins1_boot_accepts_socket1_absent, python_to_typescript_compile_inhabitance_off_the_required_gate, + edited_bin_witness_wet_rows_not_executed_by_ci, ] // THE DERIVATION THE PROJECTION USES, so "standing today" has one authority and not two. The diff --git a/docs/design-rung-drops.md b/docs/design-rung-drops.md index 480da2c3c00..a3e02adcc25 100644 --- a/docs/design-rung-drops.md +++ b/docs/design-rung-drops.md @@ -439,3 +439,7 @@ the mtcollins1 boot qualification accepts a boot on which only socket 0's 80 har ### the python->typescript compile inhabitance claim (the real parse, bridge and compile behind the supplied core the admitted cross-language claims start from) runs only in its long home, not in the required gate — declared 2026-09-26 the python->typescript compile inhabitance claim (the real parse, bridge and compile behind the supplied core the admitted cross-language claims start from) runs only in its long home, not in the required gate: RUNG DROP, mechanically preventable -> mitigatable (replacement staged: the python->typescript compile executing on the required path within the new-witness eval-step budget). Population: v2.test.long.inhabitant_neutralization_e2e_witness.inhabitant_neutralization_python_to_ts_cross_language_compile_round_trip_holds: over the new-witness eval-step budget, rostered in v2.workflow.floor_eval_step_cost_drop floor_eval_step_cost_drop_python_to_typescript_inhabitance_rows; measured by gunbc#12374, the required floor's COMPLETED-OVER-COST-REQUIREMENT line for this identity (planned_as_changed_witness, verdict pass, over the new-witness budget), and claim_batch --hermetic over src/v2/test/claim/long/inhabitant_neutralization_e2e_witness_test.dag. The billed work is the one real python->typescript compile of the python add fixture (parse, bridge, neutralize, infer, translate, emit), which this claim is the only execution of; the interface claims in v2.test.manual.cross_language_add_python_to_typescript start from its supplied core. Restored when: THE CAPABILITY. The whole python->typescript compile of the python add fixture (parse, bridge, neutralize, infer, translate, emit) costs no more than the new-witness eval-step budget on the required path, OR the required gate's budget admits a claim of this cost, so that this claim, unchanged in what it asserts, executes on every required run. WHAT THAT MUST BE SUFFICIENT FOR: the real bridge's core and the real compile's bytes are both checked at merge for the chain whose supplied core the admitted claims in v2.test.manual.cross_language_add_python_to_typescript start from. Moving the claim into the gate under a raised per-claim exemption, or splitting it so no single claim runs the whole route, does not retire this row. + +### an edited witness file classified BinWitnessWet in gunbc.ci_layer_roots witness_exclusion_frontier: the required floor's changed-witness sublane declines it as DeclinedNoCiWetLane instead of blocking, and no CI lane executes it — declared 2026-09-30 + +an edited witness file classified BinWitnessWet in gunbc.ci_layer_roots witness_exclusion_frontier: the required floor's changed-witness sublane declines it as DeclinedNoCiWetLane instead of blocking, and no CI lane executes it: RUNG DROP, mechanically preventable -> mitigatable (lost as a passenger of the per-PR bin-witness wet batch, deleted with the floor cut of 2026-08-15, and .github/workflows/falsifier.yml, deleted at 611fd02770 (gunbc#8283)). Population: gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern self_host_artifact_materialization_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern stage0_regen_convergence_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern typed_witness_invocation_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern namespace_structural_root_exposure_generated_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern emit_host_typed_smoke_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern build_artifact_corruption_probe_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_collect_fingerprint_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_perturb_receipts_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/diagnostics_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern effects_rest_transport_parse_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/parse_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern v1_dag_parse_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern auth_declared_but_unwired_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/bootstrap_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern infer_semantics_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_shard_a_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_shard_totality_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_seam_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern run_verdict_exit_status_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern http_client_get_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern roadmap_belt_actuate_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern host_build_cache_provision_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern materialized_ssh_key_file_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern proc_self_cgroup_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern repo_local_git_config_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern transport_script_stdin_byte_fidelity_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern push_event_witness_wet_test.dag. Restored when: a REQUIRED lane executes changed BinWitnessWet rows for the same pull_request or merge_group -- builds their witness binaries and runs the edited entries to a terminal verdict on a host that admits their effects -- so the changed-witness sublane can plan them instead of declining; sufficient for deleting DeclinedNoCiWetLane and this row together. diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index c6b0abc806c..66bce2e6379 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -43936,6 +43936,16 @@ pub enum RequiredFloorDisposition { /// ONE range; a green floor over these rows means the mismatch is HANDLED, never that the two /// denominators have been reconciled. DeclinedChangedWitnessOutsideDiscovery { module_path: String }, + /// Selected by the changed-witness sublane, and its file is a `BinWitnessWet` + /// `WitnessExclusionRow` (`gunbc.ci_layer_roots` `witness_exclusion_frontier`): its claims + /// drive compiled seed witness binaries on host effects the floor's hermetic route refuses, + /// and NO CI LANE EXECUTES THAT CLASS -- its per-PR batch died with the floor cut, and + /// falsifier.yml with gunbc#8283. The name says exactly that and names no owner, because no + /// lane owns it; claiming one would be the §3 meaning fork. The loss is declared as + /// `gunbc.rung_drop.edited_bin_witness_wet_rows_not_executed_by_ci`, and a PR that edits such + /// a witness carries a real bin_wet receipt instead. Reachable ONLY from that classification, + /// counted, and printed per row with the pattern that matched. + DeclinedNoCiWetLane { pattern: String }, } /// ONE EXECUTED CLAIM'S MEASURED OCCURRENCE, minted the instant `run_claim_measured` @@ -45262,6 +45272,7 @@ fn write_required_floor_disposition_tsv( let mut declined_gate_closure = 0usize; let mut declined_discovery_excluded = 0usize; let mut declined_changed_witness_outside_discovery = 0usize; + let mut declined_no_ci_wet_lane = 0usize; for row in rows { match &row.disposition { RequiredFloorDisposition::Planned => planned += 1, @@ -45277,6 +45288,7 @@ fn write_required_floor_disposition_tsv( RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { .. } => { declined_changed_witness_outside_discovery += 1 } + RequiredFloorDisposition::DeclinedNoCiWetLane { .. } => declined_no_ci_wet_lane += 1, } } writeln!( @@ -45284,7 +45296,7 @@ fn write_required_floor_disposition_tsv( "# summary\ttotal={}\tplanned={}\tplanned_as_changed_witness={}\tdeclined_long_module={}\tdeclined_fixture_member={}\ \tdeclined_outside_required_gate={}\tdeclined_outside_gate_closure={}\ \tdeclined_discovery_excluded={}\tdeclined_cost_debt={}\ - \tdeclined_changed_witness_outside_discovery={}", + \tdeclined_changed_witness_outside_discovery={}\tdeclined_no_ci_wet_lane={}", rows.len(), planned, planned_as_changed_witness, @@ -45294,7 +45306,8 @@ fn write_required_floor_disposition_tsv( declined_gate_closure, declined_discovery_excluded, declined_cost_debt, - declined_changed_witness_outside_discovery + declined_changed_witness_outside_discovery, + declined_no_ci_wet_lane ) .map_err(|e| format!("write_required_floor_disposition_tsv: write {path}: {e}"))?; writeln!(file, "identity\tdisposition\tmatched_prefix\toutcome") diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index 36de23d23cc..0273b7a937f 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -3142,6 +3142,25 @@ pub(crate) fn changed_witness_projection_rows( cause: String::new(), } } + // A CHANGED BinWitnessWet ROW: declined because no CI lane executes the class, which is + // the declared loss `gunbc.rung_drop.edited_bin_witness_wet_rows_not_executed_by_ci`. + // Non-blocking on that declaration, and never silent: the row prints with its pattern + // and the summary line counts it beside the blocking and the declared-root declines. + Some(declined @ RequiredFloorDisposition::DeclinedNoCiWetLane { .. }) => { + ChangedWitnessProjectionRow { + identity: identity.clone(), + cost: None, + standing: "declined-no-ci-wet-lane", + disposition: format!( + "{} pattern={}", + required_floor_disposition_label(declined), + required_floor_disposition_matched_prefix(declined) + ), + outcome: "not_executed".to_string(), + blocks: false, + cause: String::new(), + } + } // THE DECLINE'S CAUSE IS ITS DISPOSITION, VERBATIM. This arm is the one the // `non_verdict_disposition_surfaces_as_refusal` receipt was measured on: a witness // enrolled because the DIFF touched it and declined because DISCOVERY reaches no @@ -3816,12 +3835,18 @@ pub(crate) fn emit_changed_witness_projection( .iter() .filter(|r| r.standing == "declined-in-declared-non-executing-root") .count(); + let declined_no_ci_wet_lane = rows + .iter() + .filter(|r| r.standing == "declined-no-ci-wet-lane") + .count(); eprintln!( "required-floor: changed_witnesses={} changed_witness_blocking={} \ - changed_witness_declined_in_declared_nonexecuting_root={}", + changed_witness_declined_in_declared_nonexecuting_root={} \ + changed_witness_declined_no_ci_wet_lane={}", rows.len(), blocking, - declared_non_executing + declared_non_executing, + declined_no_ci_wet_lane ); if let Ok(path) = std::env::var("GITHUB_STEP_SUMMARY") { if !rows.is_empty() { @@ -8253,6 +8278,27 @@ pub fn run_required_floor( identity: identity.clone(), agreement: storage_agreement, }); + // A CHANGED `BinWitnessWet` ROW IS DECLINED, NOT PLANNED: the hermetic route refuses + // its host effects by construction, and planning it only mints a route gap. The + // classification is the gate, read from the typed exclusion rows, never a name match. + let bin_wet_pattern = if selected_as_changed_witness { + crate::cli_run::witness_gates::witness_exclusion_frontier_rows() + .iter() + .find(|row| { + row.classification == "BinWitnessWet" + && file.path.contains(row.pattern.as_str()) + }) + .map(|row| row.pattern.clone()) + } else { + None + }; + if let Some(pattern) = bin_wet_pattern { + disposition_rows.push(RequiredFloorDispositionRow { + identity: identity.clone(), + disposition: RequiredFloorDisposition::DeclinedNoCiWetLane { pattern }, + }); + continue; + } if selected_as_changed_witness { planned_identities.insert(identity.clone()); disposition_rows.push(RequiredFloorDispositionRow { @@ -11518,6 +11564,7 @@ pub(crate) fn required_floor_disposition_label( RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { .. } => { "declined_changed_witness_outside_discovery" } + RequiredFloorDisposition::DeclinedNoCiWetLane { .. } => "declined_no_ci_wet_lane", } } @@ -11541,6 +11588,9 @@ pub(crate) fn required_floor_disposition_matched_prefix( RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { module_path } => { module_path } + // The BinWitnessWet exclusion pattern that matched the file: the authored text that + // placed this identity in the class no CI lane executes. + RequiredFloorDisposition::DeclinedNoCiWetLane { pattern } => pattern, RequiredFloorDisposition::Planned | RequiredFloorDisposition::PlannedAsChangedWitness | RequiredFloorDisposition::DeclinedOutsideRequiredGate From e217f4d1dded27ba4cd5dd60af90083fb681eb7b Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 15:50:11 +0000 Subject: [PATCH 14/24] Adapt the cross-tree refusal to main's (module, is_test_row) provider; retire the 12 get pairs as NotAReference (review 73273) A test-row provider was never pulled, so it does not refuse. The get pairs stopped occurring because the reader now resolves get as the builtin, not because any import was fixed. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/cli_run.rs | 4 +++- ...r_unimported_bare_provider_debt_roster.dag | 24 +++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 91b52a02854..484492c36bb 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -9906,7 +9906,9 @@ fn visit_bare_reference_providers( } (None, state) => { let (provider, _) = resolve_in(&pool_census_for_name(index, &name)?)?; - if let Some(provider) = provider { + // A provider that is a test row was never pulled (the loader skips test rows + // below), so it is not a cross-tree dependency and does not refuse. + if let Some((provider, false)) = provider { return Err(format!( "bare_reference_closure: CrossTreeBareReference -- bare reference \ '{name}' in '{file_rel}' is not provided by this file's source tree \ diff --git a/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag b/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag index b7ded0b1dd7..cbf1a101e3e 100644 --- a/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag +++ b/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag @@ -118,7 +118,7 @@ data unimported_bare_provider_dispositions: List Date: Wed, 30 Sep 2026 15:53:03 +0000 Subject: [PATCH 15/24] DeclinedNoCiWetLane consumes the drop's declared population (review 73267) The rung drop's population is one data list, edited_bin_witness_wet_rows_not_executed_by_ci_population, which the RungDrop row and the floor gate both read. A BinWitnessWet row it does not name is not declined, so the drop is bounded and widening it is an edit to that list. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...in_witness_wet_rows_not_executed_by_ci.dag | 68 ++++++++++--------- .../src/cli_run/required_floor_runner.rs | 30 +++++++- 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/dag/gunbc/rung_drop/edited_bin_witness_wet_rows_not_executed_by_ci.dag b/dag/gunbc/rung_drop/edited_bin_witness_wet_rows_not_executed_by_ci.dag index d217c1f9d1a..1d2cc71f4b1 100644 --- a/dag/gunbc/rung_drop/edited_bin_witness_wet_rows_not_executed_by_ci.dag +++ b/dag/gunbc/rung_drop/edited_bin_witness_wet_rows_not_executed_by_ci.dag @@ -26,8 +26,12 @@ import gunbc.guarantee_rung { Mitigatable, MechanicallyPreventable } // general non-execution (`gunbc.ci_layer_roots bin_witness_wet_note`); this row declares the // narrower loss the changed-witness decline adds on top of it. // -// THE POPULATION is every BinWitnessWet `WitnessExclusionRow` pattern at declaration; a row added -// to that classification later joins it by the same rule. +// THE POPULATION IS BOUNDED AND IT IS THE GATE'S INPUT, one list in one place (review 73267): the +// required floor declines a changed witness as DeclinedNoCiWetLane ONLY when its file matches a +// BinWitnessWet exclusion row whose pattern is named in +// `edited_bin_witness_wet_rows_not_executed_by_ci_population` below. A row classified +// BinWitnessWet later, and not added here, is NOT declined: an edit to it still plans and blocks, +// so widening this drop is a change to this list, reviewed as one. // // THE HARM IS OBSERVED, NOT HYPOTHETICAL. The bin_wet receipt run for gunbc#12741 on 2026-09-30 // found `v2.test.claim.auth_declared_but_unwired_witness auth_declared_but_unwired_witness_keystone_holds` @@ -36,6 +40,36 @@ import gunbc.guarantee_rung { Mitigatable, MechanicallyPreventable } // this population had broken with nothing in CI able to notice, which is exactly what this row // declares. +data edited_bin_witness_wet_rows_not_executed_by_ci_population: List = [ + "self_host_artifact_materialization_real_execution_witness_test.dag", + "stage0_regen_convergence_real_execution_witness_test.dag", + "typed_witness_invocation_test.dag", + "namespace_structural_root_exposure_generated_witness_test.dag", + "emit_host_typed_smoke_test.dag", + "build_artifact_corruption_probe_witness_test.dag", + "dag_collect_fingerprint_witness_test.dag", + "dag_compile_clean_perturb_receipts_test.dag", + "test/claim/diagnostics_test.dag", + "effects_rest_transport_parse_witness_test.dag", + "test/claim/parse_test.dag", + "v1_dag_parse_witness_test.dag", + "auth_declared_but_unwired_witness_test.dag", + "test/claim/bootstrap_test.dag", + "infer_semantics_witness_test.dag", + "dag_compile_clean_shard_a_witness_test.dag", + "dag_compile_clean_shard_totality_witness_test.dag", + "dag_compile_clean_seam_witness_test.dag", + "run_verdict_exit_status_witness_test.dag", + "http_client_get_real_execution_witness_test.dag", + "roadmap_belt_actuate_witness_test.dag", + "host_build_cache_provision_real_execution_witness_test.dag", + "materialized_ssh_key_file_real_execution_witness_test.dag", + "proc_self_cgroup_real_execution_witness_test.dag", + "repo_local_git_config_real_execution_witness_test.dag", + "transport_script_stdin_byte_fidelity_witness_test.dag", + "push_event_witness_wet_test.dag", +] + data edited_bin_witness_wet_rows_not_executed_by_ci: RungDrop = RungDrop { identity: "edited_bin_witness_wet_rows_not_executed_by_ci" as NonEmptyStr, @@ -49,35 +83,7 @@ data edited_bin_witness_wet_rows_not_executed_by_ci: RungDrop = RungDrop { previous: MechanicallyPreventable, temporary: Mitigatable, reason: LostAsPassenger { carrier: "the per-PR bin-witness wet batch, deleted with the floor cut of 2026-08-15, and .github/workflows/falsifier.yml, deleted at 611fd02770 (gunbc#8283)" }, - population: [ - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern self_host_artifact_materialization_real_execution_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern stage0_regen_convergence_real_execution_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern typed_witness_invocation_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern namespace_structural_root_exposure_generated_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern emit_host_typed_smoke_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern build_artifact_corruption_probe_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_collect_fingerprint_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_perturb_receipts_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/diagnostics_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern effects_rest_transport_parse_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/parse_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern v1_dag_parse_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern auth_declared_but_unwired_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/bootstrap_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern infer_semantics_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_shard_a_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_shard_totality_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_seam_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern run_verdict_exit_status_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern http_client_get_real_execution_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern roadmap_belt_actuate_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern host_build_cache_provision_real_execution_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern materialized_ssh_key_file_real_execution_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern proc_self_cgroup_real_execution_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern repo_local_git_config_real_execution_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern transport_script_stdin_byte_fidelity_witness_test.dag", - "gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern push_event_witness_wet_test.dag", - ], + population: edited_bin_witness_wet_rows_not_executed_by_ci_population, restoration_trigger: "a REQUIRED lane executes changed BinWitnessWet rows for the same pull_request or merge_group -- builds their witness binaries and runs the edited entries to a terminal verdict on a host that admits their effects -- so the changed-witness sublane can plan them instead of declining; sufficient for deleting DeclinedNoCiWetLane and this row together", } } diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index 0273b7a937f..cc7cfc91244 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -8280,13 +8280,15 @@ pub fn run_required_floor( }); // A CHANGED `BinWitnessWet` ROW IS DECLINED, NOT PLANNED: the hermetic route refuses // its host effects by construction, and planning it only mints a route gap. The - // classification is the gate, read from the typed exclusion rows, never a name match. + // classification is the gate, read from the typed exclusion rows, never a name match, and the + // row must be named in the declared drop's bounded population (review 73267). let bin_wet_pattern = if selected_as_changed_witness { crate::cli_run::witness_gates::witness_exclusion_frontier_rows() .iter() .find(|row| { row.classification == "BinWitnessWet" && file.path.contains(row.pattern.as_str()) + && declared_no_ci_wet_lane_population().contains(&row.pattern) }) .map(|row| row.pattern.clone()) } else { @@ -16193,3 +16195,29 @@ mod floor_stall_metric_tests { assert!(!line.contains("stall 0"), "{line}"); } } + +/// THE DECLARED POPULATION of `gunbc.rung_drop.edited_bin_witness_wet_rows_not_executed_by_ci`, read +/// from that row's `..._population` list -- the ONE list that both declares the drop and gates the +/// `DeclinedNoCiWetLane` decline. A BinWitnessWet row it does not name is not declined. +fn declared_no_ci_wet_lane_population() -> &'static std::collections::HashSet { + static POPULATION: std::sync::OnceLock> = + std::sync::OnceLock::new(); + POPULATION.get_or_init(|| { + const REL: &str = "dag/gunbc/rung_drop/edited_bin_witness_wet_rows_not_executed_by_ci.dag"; + let path = crate::cli_run::process_workspace_root().join(REL); + let content = std::fs::read_to_string(&path).unwrap_or_else(|e| { + panic!( + "rung drop population: failed to read {}: {e}", + path.display() + ) + }); + crate::cli_run::string_list_data_from_module_source( + REL, + &content, + "edited_bin_witness_wet_rows_not_executed_by_ci_population", + false, + ) + .into_iter() + .collect() + }) +} From 8cb38e419224a06724de4fbabc2695ea46fb83ce Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 17:15:52 +0000 Subject: [PATCH 16/24] Regenerate docs/design-rung-drops.md: the drop row's projection was missing on this head Co-Authored-By: Claude Opus 5.5 (1M context) --- target_rg2.sh | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 target_rg2.sh diff --git a/target_rg2.sh b/target_rg2.sh new file mode 100644 index 00000000000..b809005d32c --- /dev/null +++ b/target_rg2.sh @@ -0,0 +1,7 @@ +CG=/sys/fs/cgroup/gunbc-probe; S=""; command -v sudo >/dev/null && S=sudo +echo "+memory" | $S tee /sys/fs/cgroup/cgroup.subtree_control >/dev/null; $S mkdir -p $CG; echo 17179869184 | $S tee $CG/memory.max >/dev/null; echo $$ | $S tee $CG/cgroup.procs >/dev/null +cargo build --release -p v1-compiler --bin gunbc > /dev/null 2>&1 +./target/release/gunbc run --source-root dag --source-root src/v2 --entry dag/gunbc/instruments/docs_projection_gate.dag --function regen > /tmp/r.txt 2>&1; echo REGEN=$? +git diff --stat -- docs/ +git diff -- docs/design-rung-drops.md | head -30 | cut -c1-200 +echo ===PATCH-BEGIN===; git diff -- docs/ | gzip -9 | base64 -w0; echo; echo ===PATCH-END=== From a8d55a238366bbc9f409e9160d82de44af8699c5 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 17:16:27 +0000 Subject: [PATCH 17/24] Remove a scratch script committed by mistake Co-Authored-By: Claude Opus 5.5 (1M context) --- target_rg2.sh | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 target_rg2.sh diff --git a/target_rg2.sh b/target_rg2.sh deleted file mode 100644 index b809005d32c..00000000000 --- a/target_rg2.sh +++ /dev/null @@ -1,7 +0,0 @@ -CG=/sys/fs/cgroup/gunbc-probe; S=""; command -v sudo >/dev/null && S=sudo -echo "+memory" | $S tee /sys/fs/cgroup/cgroup.subtree_control >/dev/null; $S mkdir -p $CG; echo 17179869184 | $S tee $CG/memory.max >/dev/null; echo $$ | $S tee $CG/cgroup.procs >/dev/null -cargo build --release -p v1-compiler --bin gunbc > /dev/null 2>&1 -./target/release/gunbc run --source-root dag --source-root src/v2 --entry dag/gunbc/instruments/docs_projection_gate.dag --function regen > /tmp/r.txt 2>&1; echo REGEN=$? -git diff --stat -- docs/ -git diff -- docs/design-rung-drops.md | head -30 | cut -c1-200 -echo ===PATCH-BEGIN===; git diff -- docs/ | gzip -9 | base64 -w0; echo; echo ===PATCH-END=== From 7a608f8cdad76ddbf0511cf6be14f1c95d43ca7a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 17:16:39 +0000 Subject: [PATCH 18/24] Regenerate docs/design-rung-drops.md for the population's bare-pattern entries Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/design-rung-drops.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design-rung-drops.md b/docs/design-rung-drops.md index a3e02adcc25..d96a7afafeb 100644 --- a/docs/design-rung-drops.md +++ b/docs/design-rung-drops.md @@ -442,4 +442,4 @@ the python->typescript compile inhabitance claim (the real parse, bridge and com ### an edited witness file classified BinWitnessWet in gunbc.ci_layer_roots witness_exclusion_frontier: the required floor's changed-witness sublane declines it as DeclinedNoCiWetLane instead of blocking, and no CI lane executes it — declared 2026-09-30 -an edited witness file classified BinWitnessWet in gunbc.ci_layer_roots witness_exclusion_frontier: the required floor's changed-witness sublane declines it as DeclinedNoCiWetLane instead of blocking, and no CI lane executes it: RUNG DROP, mechanically preventable -> mitigatable (lost as a passenger of the per-PR bin-witness wet batch, deleted with the floor cut of 2026-08-15, and .github/workflows/falsifier.yml, deleted at 611fd02770 (gunbc#8283)). Population: gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern self_host_artifact_materialization_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern stage0_regen_convergence_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern typed_witness_invocation_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern namespace_structural_root_exposure_generated_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern emit_host_typed_smoke_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern build_artifact_corruption_probe_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_collect_fingerprint_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_perturb_receipts_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/diagnostics_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern effects_rest_transport_parse_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/parse_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern v1_dag_parse_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern auth_declared_but_unwired_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern test/claim/bootstrap_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern infer_semantics_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_shard_a_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_shard_totality_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern dag_compile_clean_seam_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern run_verdict_exit_status_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern http_client_get_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern roadmap_belt_actuate_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern host_build_cache_provision_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern materialized_ssh_key_file_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern proc_self_cgroup_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern repo_local_git_config_real_execution_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern transport_script_stdin_byte_fidelity_witness_test.dag, gunbc.ci_layer_roots witness_exclusion_frontier BinWitnessWet pattern push_event_witness_wet_test.dag. Restored when: a REQUIRED lane executes changed BinWitnessWet rows for the same pull_request or merge_group -- builds their witness binaries and runs the edited entries to a terminal verdict on a host that admits their effects -- so the changed-witness sublane can plan them instead of declining; sufficient for deleting DeclinedNoCiWetLane and this row together. +an edited witness file classified BinWitnessWet in gunbc.ci_layer_roots witness_exclusion_frontier: the required floor's changed-witness sublane declines it as DeclinedNoCiWetLane instead of blocking, and no CI lane executes it: RUNG DROP, mechanically preventable -> mitigatable (lost as a passenger of the per-PR bin-witness wet batch, deleted with the floor cut of 2026-08-15, and .github/workflows/falsifier.yml, deleted at 611fd02770 (gunbc#8283)). Population: self_host_artifact_materialization_real_execution_witness_test.dag, stage0_regen_convergence_real_execution_witness_test.dag, typed_witness_invocation_test.dag, namespace_structural_root_exposure_generated_witness_test.dag, emit_host_typed_smoke_test.dag, build_artifact_corruption_probe_witness_test.dag, dag_collect_fingerprint_witness_test.dag, dag_compile_clean_perturb_receipts_test.dag, test/claim/diagnostics_test.dag, effects_rest_transport_parse_witness_test.dag, test/claim/parse_test.dag, v1_dag_parse_witness_test.dag, auth_declared_but_unwired_witness_test.dag, test/claim/bootstrap_test.dag, infer_semantics_witness_test.dag, dag_compile_clean_shard_a_witness_test.dag, dag_compile_clean_shard_totality_witness_test.dag, dag_compile_clean_seam_witness_test.dag, run_verdict_exit_status_witness_test.dag, http_client_get_real_execution_witness_test.dag, roadmap_belt_actuate_witness_test.dag, host_build_cache_provision_real_execution_witness_test.dag, materialized_ssh_key_file_real_execution_witness_test.dag, proc_self_cgroup_real_execution_witness_test.dag, repo_local_git_config_real_execution_witness_test.dag, transport_script_stdin_byte_fidelity_witness_test.dag, push_event_witness_wet_test.dag. Restored when: a REQUIRED lane executes changed BinWitnessWet rows for the same pull_request or merge_group -- builds their witness binaries and runs the edited entries to a terminal verdict on a host that admits their effects -- so the changed-witness sublane can plan them instead of declining; sufficient for deleting DeclinedNoCiWetLane and this row together. From 2cdadf51c2dd2b57be774ff80c443817bb4a7819 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 20:26:45 +0000 Subject: [PATCH 19/24] Changed-witness join counts DeclinedNoCiWetLane as a changed-witness disposition gunbc#12794's decline pushed a disposition row, but the sublane's exactness join counted only PlannedAsChangedWitness, so every declined selection refused as selected_without_disposition (observed on gunbc#12741's floor, run 36768985832). The decline was unreachable by its route. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/cli_run/required_floor_runner.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index a7853dcde2a..a9cfa76c551 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -8783,10 +8783,15 @@ pub fn run_required_floor( // cannot be repaired by the aggregate counts coincidentally agreeing. let changed_disposition_set: HashSet = disposition_rows .iter() + // A CHANGED BinWitnessWet ROW IS A CHANGED-WITNESS DISPOSITION TOO: the sublane selected + // it and decided it (`DeclinedNoCiWetLane`) instead of planning it. Leaving it out made + // the join refuse every such selection as `selected_without_disposition`, so the decline + // gunbc#12794 added could never be reached by the route it was written for. .filter(|row| { matches!( row.disposition, RequiredFloorDisposition::PlannedAsChangedWitness + | RequiredFloorDisposition::DeclinedNoCiWetLane { .. } ) }) .map(|row| row.identity.clone()) From aa975a6bc5c6fa6350e3ae7d84877d8294701a75 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 20:51:27 +0000 Subject: [PATCH 20/24] Execute the changed-witness join by a unit, decide its membership exhaustively, and file the class changed_witness_sublane_join is extracted and driven with a DeclinedNoCiWetLane selection (red on the pre-fix allow-list predicate with ChangedWitnessSublaneJoinInexact, green here); decides_a_changed_selection is an exhaustive match, so a new disposition arm cannot compile without stating its membership. Class filed as gunbc.recurring_failure_mode.a_new_decision_arm_the_downstream_join_does_not_admit. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...arm_the_downstream_join_does_not_admit.dag | 17 ++ .../src/cli_run/required_floor_runner.rs | 159 ++++++++++++++---- 2 files changed, 142 insertions(+), 34 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag diff --git a/dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag b/dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag new file mode 100644 index 00000000000..b2b8bc035b2 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag @@ -0,0 +1,17 @@ +module gunbc.recurring_failure_mode.a_new_decision_arm_the_downstream_join_does_not_admit + +import std.types { NonEmptyStr } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data a_new_decision_arm_the_downstream_join_does_not_admit: RecurringFailureMode = RecurringFailureMode { + identity: "a_new_decision_arm_the_downstream_join_does_not_admit" as NonEmptyStr, + + receipts: [ + "INVALID STATE: a new arm is added to a decision (a disposition variant, a standing, a route outcome), and a DOWNSTREAM exactness check over that decision -- a join that says which decided rows count -- was written against the older set of arms and does not admit the new one. Every piece is individually green: the arm compiles, its row is pushed, its projection prints. But the route that reaches the arm always refuses at the join, so the arm is unreachable in production. HARM: the capability the arm exists for is dead on arrival, and it is discovered only when some later change happens to exercise the route, at which point that unrelated change reds.", + "SPECIMEN (bold-bat-516, 2026-09-30): gunbc#12794 added RequiredFloorDisposition::DeclinedNoCiWetLane to the required floor's changed-witness sublane and pushed a disposition row for each declined selection. The sublane's exactness join built its right-hand side from PlannedAsChangedWitness rows ONLY, so every declined selection refused as ChangedWitnessSublaneJoinInexact selected_without_disposition. It was first observed on gunbc#12741's floor (run 36768985832), the first PR to edit a BinWitnessWet witness after #12794 merged. #12794's only planned route evidence was that later PR's floor, so the defect merged.", + "DISTINGUISHING FACTS: the new arm is a member of an enumeration that some other site FILTERS with an explicit allow-list (a matches! over named arms) rather than an exhaustive match, so adding the variant produced no compile error there; and the change's evidence exercised the arm's own site, not the site that consumes its result.", + "RUNG FOUND AT: mitigatable (the join refused loudly, so nothing passed silently -- the cost was an unreachable capability plus a red on the next consumer). RUNG NOW, FOR THE SPECIMEN'S SITE: structurally guaranteed -- gunbc#12833 extracts the join as cli_run required_floor_runner changed_witness_sublane_join, which decides membership through decides_a_changed_selection, an EXHAUSTIVE match over RequiredFloorDisposition, so a new variant does not compile there until it states whether it counts; changed_witness_sublane_join_tests executes it with a DeclinedNoCiWetLane selection (red on the pre-fix allow-list predicate with the floor's own ChangedWitnessSublaneJoinInexact refusal, green on the fix). FOR THE CLASS: mitigatable -- other consumers that select disposition arms by an allow-list matches! remain. CEILING: structurally guaranteed at every such consumer. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: every consumer that decides membership over a closed disposition enumeration does so by an exhaustive match, so adding an arm fails to compile at each one until its membership is stated.", + ], + + evidence: [], +} diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index a9cfa76c551..08e33e79225 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -8781,40 +8781,7 @@ pub fn run_required_floor( // single #9717 diff derivation captured before preparation; the right side is what this site // projection actually marked for changed execution. A missing, foreign, or duplicated row // cannot be repaired by the aggregate counts coincidentally agreeing. - let changed_disposition_set: HashSet = disposition_rows - .iter() - // A CHANGED BinWitnessWet ROW IS A CHANGED-WITNESS DISPOSITION TOO: the sublane selected - // it and decided it (`DeclinedNoCiWetLane`) instead of planning it. Leaving it out made - // the join refuse every such selection as `selected_without_disposition`, so the decline - // gunbc#12794 added could never be reached by the route it was written for. - .filter(|row| { - matches!( - row.disposition, - RequiredFloorDisposition::PlannedAsChangedWitness - | RequiredFloorDisposition::DeclinedNoCiWetLane { .. } - ) - }) - .map(|row| row.identity.clone()) - .collect(); - if changed_disposition_set != changed_witness_expected { - let mut selected_without_disposition: Vec<&str> = changed_witness_expected - .difference(&changed_disposition_set) - .map(String::as_str) - .collect(); - let mut disposition_without_selection: Vec<&str> = changed_disposition_set - .difference(&changed_witness_expected) - .map(String::as_str) - .collect(); - selected_without_disposition.sort(); - disposition_without_selection.sort(); - return Err(format!( - "REQUIRED-FLOOR REFUSAL cause=ChangedWitnessSublaneJoinInexact \ - selected_without_disposition=[{}] disposition_without_selection=[{}] — the \ - changed-witness execution sublane must execute exactly the one derived identity set", - selected_without_disposition.join(", "), - disposition_without_selection.join(", ") - )); - } + changed_witness_sublane_join(&changed_witness_expected, &disposition_rows)?; // ONE PRODUCER FOR THE COUNTS: the joined row population, folded once per arm. let disposition_count = |select: fn(&RequiredFloorDisposition) -> bool| { disposition_rows @@ -16418,3 +16385,127 @@ fn declared_no_ci_wet_lane_population() -> &'static std::collections::HashSet bool { + match disposition { + RequiredFloorDisposition::PlannedAsChangedWitness + | RequiredFloorDisposition::DeclinedNoCiWetLane { .. } => true, + RequiredFloorDisposition::Planned + | RequiredFloorDisposition::DeclinedLongModule { .. } + | RequiredFloorDisposition::DeclinedFixtureMember { .. } + | RequiredFloorDisposition::DeclinedOutsideRequiredGate + | RequiredFloorDisposition::DeclinedCostDebt + | RequiredFloorDisposition::DeclinedOutsideGateClosure + | RequiredFloorDisposition::DeclinedDiscoveryExcluded { .. } + | RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { .. } => false, + } +} + +/// EXACTNESS OF THE CHANGED-WITNESS SUBLANE, as an identity join rather than a count. The left side +/// is the single diff derivation captured before preparation; the right side is every row this +/// site projection DECIDED for a selected identity -- planned for changed execution, or declined +/// because no CI lane executes its class (`DeclinedNoCiWetLane`). A missing, foreign, or duplicated +/// row cannot be repaired by aggregate counts coincidentally agreeing. Extracted so its exactness is +/// executed by a unit (`changed_witness_sublane_join_tests`), not only by a PR floor that happens to +/// touch the class: gunbc#12794's decline shipped unreachable because the only route evidence for +/// it was a later PR's floor. +pub(crate) fn changed_witness_sublane_join( + changed_witness_expected: &HashSet, + disposition_rows: &[RequiredFloorDispositionRow], +) -> Result<(), String> { + let changed_disposition_set: HashSet = disposition_rows + .iter() + .filter(|row| decides_a_changed_selection(&row.disposition)) + .map(|row| row.identity.clone()) + .collect(); + if changed_disposition_set == *changed_witness_expected { + return Ok(()); + } + let mut selected_without_disposition: Vec<&str> = changed_witness_expected + .difference(&changed_disposition_set) + .map(String::as_str) + .collect(); + let mut disposition_without_selection: Vec<&str> = changed_disposition_set + .difference(changed_witness_expected) + .map(String::as_str) + .collect(); + selected_without_disposition.sort(); + disposition_without_selection.sort(); + Err(format!( + "REQUIRED-FLOOR REFUSAL cause=ChangedWitnessSublaneJoinInexact \ + selected_without_disposition=[{}] disposition_without_selection=[{}] — the \ + changed-witness execution sublane must execute exactly the one derived identity set", + selected_without_disposition.join(", "), + disposition_without_selection.join(", ") + )) +} + +#[cfg(test)] +mod changed_witness_sublane_join_tests { + use super::*; + + fn row(identity: &str, disposition: RequiredFloorDisposition) -> RequiredFloorDispositionRow { + RequiredFloorDispositionRow { + identity: identity.to_string(), + disposition, + } + } + + /// THE ROUTE gunbc#12794 shipped unreachable: a selected witness the sublane DECLINES as + /// `DeclinedNoCiWetLane` satisfies the exactness join. Red on the pre-fix join, which counted + /// only `PlannedAsChangedWitness` and refused with `selected_without_disposition`. + #[test] + fn a_declined_no_ci_wet_lane_selection_satisfies_the_join() { + let expected: HashSet = ["m.planned", "m.wet"] + .iter() + .map(|s| s.to_string()) + .collect(); + let rows = vec![ + row( + "m.planned", + RequiredFloorDisposition::PlannedAsChangedWitness, + ), + row( + "m.wet", + RequiredFloorDisposition::DeclinedNoCiWetLane { + pattern: "wet_witness_test.dag".to_string(), + }, + ), + ]; + changed_witness_sublane_join(&expected, &rows) + .expect("a declined wet selection is decided"); + } + + /// The join is still exact: a selection with no deciding row refuses, and a deciding row with no + /// selection refuses, whichever arm decided it. + #[test] + fn the_join_still_refuses_a_missing_or_foreign_decision() { + let expected: HashSet = ["m.a"].iter().map(|s| s.to_string()).collect(); + let missing = changed_witness_sublane_join(&expected, &[]).unwrap_err(); + assert!( + missing.contains("selected_without_disposition=[m.a]"), + "{missing}" + ); + let foreign = changed_witness_sublane_join( + &expected, + &[ + row("m.a", RequiredFloorDisposition::PlannedAsChangedWitness), + row( + "m.b", + RequiredFloorDisposition::DeclinedNoCiWetLane { + pattern: "p".to_string(), + }, + ), + ], + ) + .unwrap_err(); + assert!( + foreign.contains("disposition_without_selection=[m.b]"), + "{foreign}" + ); + } +} From b110cd6c6d75208a92a6d0cef9be780cc93df86e Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 21:37:01 +0000 Subject: [PATCH 21/24] Failure-mode row: say which evidence the merge path executes (review 73414) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../a_new_decision_arm_the_downstream_join_does_not_admit.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag b/dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag index b2b8bc035b2..61cbef5480a 100644 --- a/dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag +++ b/dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag @@ -10,7 +10,7 @@ data a_new_decision_arm_the_downstream_join_does_not_admit: RecurringFailureMode "INVALID STATE: a new arm is added to a decision (a disposition variant, a standing, a route outcome), and a DOWNSTREAM exactness check over that decision -- a join that says which decided rows count -- was written against the older set of arms and does not admit the new one. Every piece is individually green: the arm compiles, its row is pushed, its projection prints. But the route that reaches the arm always refuses at the join, so the arm is unreachable in production. HARM: the capability the arm exists for is dead on arrival, and it is discovered only when some later change happens to exercise the route, at which point that unrelated change reds.", "SPECIMEN (bold-bat-516, 2026-09-30): gunbc#12794 added RequiredFloorDisposition::DeclinedNoCiWetLane to the required floor's changed-witness sublane and pushed a disposition row for each declined selection. The sublane's exactness join built its right-hand side from PlannedAsChangedWitness rows ONLY, so every declined selection refused as ChangedWitnessSublaneJoinInexact selected_without_disposition. It was first observed on gunbc#12741's floor (run 36768985832), the first PR to edit a BinWitnessWet witness after #12794 merged. #12794's only planned route evidence was that later PR's floor, so the defect merged.", "DISTINGUISHING FACTS: the new arm is a member of an enumeration that some other site FILTERS with an explicit allow-list (a matches! over named arms) rather than an exhaustive match, so adding the variant produced no compile error there; and the change's evidence exercised the arm's own site, not the site that consumes its result.", - "RUNG FOUND AT: mitigatable (the join refused loudly, so nothing passed silently -- the cost was an unreachable capability plus a red on the next consumer). RUNG NOW, FOR THE SPECIMEN'S SITE: structurally guaranteed -- gunbc#12833 extracts the join as cli_run required_floor_runner changed_witness_sublane_join, which decides membership through decides_a_changed_selection, an EXHAUSTIVE match over RequiredFloorDisposition, so a new variant does not compile there until it states whether it counts; changed_witness_sublane_join_tests executes it with a DeclinedNoCiWetLane selection (red on the pre-fix allow-list predicate with the floor's own ChangedWitnessSublaneJoinInexact refusal, green on the fix). FOR THE CLASS: mitigatable -- other consumers that select disposition arms by an allow-list matches! remain. CEILING: structurally guaranteed at every such consumer. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: every consumer that decides membership over a closed disposition enumeration does so by an exhaustive match, so adding an arm fails to compile at each one until its membership is stated.", + "RUNG FOUND AT: mitigatable (the join refused loudly, so nothing passed silently -- the cost was an unreachable capability plus a red on the next consumer). RUNG NOW, FOR THE SPECIMEN'S SITE: structurally guaranteed -- gunbc#12833 extracts the join as cli_run required_floor_runner changed_witness_sublane_join, which decides membership through decides_a_changed_selection, an EXHAUSTIVE match over RequiredFloorDisposition, so a new variant does not compile there until it states whether it counts; changed_witness_sublane_join_tests executes it with a DeclinedNoCiWetLane selection (red on the pre-fix allow-list predicate with the floor's own ChangedWitnessSublaneJoinInexact refusal, green on the fix). WHAT THE MERGE PATH EXECUTES, stated exactly (review 73414): the structural claim rests on the exhaustive match, which the required lint step compiles on every pull request; the unit runs only in the non-required rust-unit-tests job (gunbc.rung_drop rust_unit_tests_off_the_merge_path), so its red/green is supporting evidence and not a merge gate. FOR THE CLASS: mitigatable -- other consumers that select disposition arms by an allow-list matches! remain. CEILING: structurally guaranteed at every such consumer. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: every consumer that decides membership over a closed disposition enumeration does so by an exhaustive match, so adding an arm fails to compile at each one until its membership is stated.", ], evidence: [], From ed02153348a746a6409a3b838b7c399470bd5950 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 15:39:26 +0000 Subject: [PATCH 22/24] Reverse roster joins suppress a changed witness the sublane declined as DeclinedNoCiWetLane suppress_withheld keeps every changed witness in the expected-red, route-gap and non-verdict rosters on the premise that the changed sublane executes it. A DeclinedNoCiWetLane decline falsifies that premise, so the route-gap reverse join refused three declined witnesses as stale on this PR's floor. They are now suppressed with their own ground (ExpectedRedSuppressionGround::DeclinedNoCiWetLane), decided by an exhaustive match. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/expected_red_roster_join.dag | 7 ++- .../src/cli_run/required_floor_runner.rs | 63 ++++++++++++++++++- .../v1_compiler_expected_red_roster_join.rs | 7 +++ 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/v1/expected_red_roster_join.dag b/src/v1/expected_red_roster_join.dag index a780d5cc850..6b82bf1a753 100644 --- a/src/v1/expected_red_roster_join.dag +++ b/src/v1/expected_red_roster_join.dag @@ -60,8 +60,8 @@ type WitnessEvalVerdict // WHY A SUPPRESSED ROW IS ITS OWN DISPOSITION AND NOT A NotEvaluated REASON. NotEvaluated says // the identity was ATTEMPTED and produced no verdict -- host tool missing, hermetic route gap, // budget interrupt. A suppressed row was never attempted at all: the floor removed it from the -// roster BEFORE the fold, because its module is outside the required gate or the cost-debt roster -// withholds it. Same absence of a verdict, different fact and a different remedy -- one needs a +// roster BEFORE the fold, because its module is outside the required gate, the cost-debt roster +// withholds it, or the changed-witness sublane declined it as a declared BinWitnessWet row. Same absence of a verdict, different fact and a different remedy -- one needs a // route or a budget, the other needs a gate roster edit or a debt to clear -- so collapsing them // would be the state-space conflation DESIGN section 5 names. // @@ -73,6 +73,7 @@ type WitnessEvalVerdict type ExpectedRedSuppressionGround = OutsideRequiredGate | WithheldCostDebt + | DeclinedNoCiWetLane type ExpectedRedJoinDisposition = StillRed @@ -133,6 +134,7 @@ fn suppression_ground_label(ground: ExpectedRedSuppressionGround) -> String { match ground { OutsideRequiredGate => "suppressed_outside_required_gate" WithheldCostDebt => "suppressed_withheld_cost_debt" + DeclinedNoCiWetLane => "suppressed_declined_no_ci_wet_lane" } } @@ -140,6 +142,7 @@ fn suppression_ground_detail(ground: ExpectedRedSuppressionGround) -> String { match ground { OutsideRequiredGate => "enrolled, but its module is outside the required gate and was never loaded, so this run could not attempt it -- dormant, not deleted" WithheldCostDebt => "enrolled, but the cost-debt roster withholds it from execution in this run -- dormant, not deleted" + DeclinedNoCiWetLane => "enrolled and changed by this run, but its file is a declared BinWitnessWet row no CI lane executes (gunbc.rung_drop edited_bin_witness_wet_rows_not_executed_by_ci), so the changed-witness sublane declined it -- dormant, not deleted" } } diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index 2c3614452a9..5d4110bf58b 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -9131,6 +9131,39 @@ pub fn run_required_floor( // projection actually marked for changed execution. A missing, foreign, or duplicated row // cannot be repaired by the aggregate counts coincidentally agreeing. changed_witness_sublane_join(&changed_witness_expected, &disposition_rows)?; + // A DECLINED CHANGED WITNESS DOES NOT EXECUTE, so the reverse roster joins must not expect it + // to. `suppress_withheld` keeps every changed witness in the expected-red, route-gap and + // non-verdict rosters on the premise that the changed sublane runs it; a `DeclinedNoCiWetLane` + // decline falsifies that premise for its identity, and left in a roster it reads as "renamed, + // deleted, or declined -- delete the row" against a row that is only dormant for this run. It + // is removed with its own suppression ground, so the expected-red report still names it. + let declined_no_ci_wet_lane: HashSet = disposition_rows + .iter() + .filter(|row| suppresses_a_changed_witness_enrollment(&row.disposition)) + .map(|row| row.identity.clone()) + .collect(); + let suppress_declined_no_ci_wet_lane = + |roster: &mut HashSet, name: &str| -> Vec<(String, SuppressionGround)> { + let mut removed: Vec = roster + .iter() + .filter(|identity| declined_no_ci_wet_lane.contains(*identity)) + .cloned() + .collect(); + removed.sort(); + roster.retain(|identity| !declined_no_ci_wet_lane.contains(identity)); + if !removed.is_empty() { + eprintln!( + "[floor-changed-witness] {name}: {} enrolled identity(ies) suppressed because \ + the changed-witness sublane declined them as declared BinWitnessWet rows; \ + their enrollment is dormant, not deleted", + removed.len() + ); + } + removed + .into_iter() + .map(|identity| (identity, SuppressionGround::DeclinedNoCiWetLane)) + .collect() + }; // ONE PRODUCER FOR THE COUNTS: the joined row population, folded once per arm. let disposition_count = |select: fn(&RequiredFloorDisposition) -> bool| { disposition_rows @@ -9346,7 +9379,12 @@ pub fn run_required_floor( out }; let mut expected_red_roster = expected_red_roster; - let expected_red_suppressed = suppress_withheld(&mut expected_red_roster, "floor_expected_red"); + let mut expected_red_suppressed = + suppress_withheld(&mut expected_red_roster, "floor_expected_red"); + expected_red_suppressed.extend(suppress_declined_no_ci_wet_lane( + &mut expected_red_roster, + "floor_expected_red", + )); eprintln!( "[floor-known-red] roster carries {} enrolled identity(ies)", expected_red_roster.len() @@ -9401,6 +9439,7 @@ pub fn run_required_floor( }; let mut route_gap_roster = route_gap_roster; let _ = suppress_withheld(&mut route_gap_roster, "floor_route_gap"); + let _ = suppress_declined_no_ci_wet_lane(&mut route_gap_roster, "floor_route_gap"); eprintln!( "[floor-route-gap] roster carries {} enrolled identity(ies)", route_gap_roster.len() @@ -9598,6 +9637,7 @@ pub fn run_required_floor( }; let mut non_verdict_roster = non_verdict_roster; let _ = suppress_withheld(&mut non_verdict_roster, "floor_non_verdict"); + let _ = suppress_declined_no_ci_wet_lane(&mut non_verdict_roster, "floor_non_verdict"); eprintln!( "[floor-non-verdict] roster carries {} enrolled identity(ies)", non_verdict_roster.len() @@ -16820,6 +16860,27 @@ fn decides_a_changed_selection(disposition: &RequiredFloorDisposition) -> bool { } } +/// WHICH DISPOSITIONS SUPPRESS A CHANGED WITNESS'S ROSTER ENROLLMENT for this run, as an EXHAUSTIVE +/// match for the same reason as `decides_a_changed_selection`: a new arm states whether the reverse +/// roster joins may still expect its identity to execute. `DeclinedNoCiWetLane` is the one decline of +/// a discovered, selected identity; `DeclinedChangedWitnessOutsideDiscovery` names an identity no +/// site discovered, which no roster enrollment can reach through the fold, and every other arm +/// either executes or is suppressed earlier by `suppress_withheld`. +fn suppresses_a_changed_witness_enrollment(disposition: &RequiredFloorDisposition) -> bool { + match disposition { + RequiredFloorDisposition::DeclinedNoCiWetLane { .. } => true, + RequiredFloorDisposition::Planned + | RequiredFloorDisposition::PlannedAsChangedWitness + | RequiredFloorDisposition::DeclinedLongModule { .. } + | RequiredFloorDisposition::DeclinedFixtureMember { .. } + | RequiredFloorDisposition::DeclinedOutsideRequiredGate + | RequiredFloorDisposition::DeclinedCostDebt + | RequiredFloorDisposition::DeclinedOutsideGateClosure + | RequiredFloorDisposition::DeclinedDiscoveryExcluded { .. } + | RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { .. } => false, + } +} + /// EXACTNESS OF THE CHANGED-WITNESS SUBLANE, as an identity join rather than a count. The left side /// is the single diff derivation captured before preparation; the right side is every row this /// site projection DECIDED for a selected identity -- planned for changed execution, or declined diff --git a/src/v1/stage0/src/v1_compiler_expected_red_roster_join.rs b/src/v1/stage0/src/v1_compiler_expected_red_roster_join.rs index a85f4d794d9..ba810af22c5 100644 --- a/src/v1/stage0/src/v1_compiler_expected_red_roster_join.rs +++ b/src/v1/stage0/src/v1_compiler_expected_red_roster_join.rs @@ -71,6 +71,7 @@ pub enum WitnessEvalVerdict { pub enum ExpectedRedSuppressionGround { OutsideRequiredGate, WithheldCostDebt, + DeclinedNoCiWetLane, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] @@ -157,6 +158,9 @@ pub fn suppression_ground_label(ground: ExpectedRedSuppressionGround) -> String ExpectedRedSuppressionGround::WithheldCostDebt => { "suppressed_withheld_cost_debt".to_string() } + ExpectedRedSuppressionGround::DeclinedNoCiWetLane => { + "suppressed_declined_no_ci_wet_lane".to_string() + } } } @@ -164,6 +168,7 @@ pub fn suppression_ground_detail(ground: ExpectedRedSuppressionGround) -> String match ground.clone() { ExpectedRedSuppressionGround::OutsideRequiredGate => "enrolled, but its module is outside the required gate and was never loaded, so this run could not attempt it -- dormant, not deleted".to_string(), ExpectedRedSuppressionGround::WithheldCostDebt => "enrolled, but the cost-debt roster withholds it from execution in this run -- dormant, not deleted".to_string(), + ExpectedRedSuppressionGround::DeclinedNoCiWetLane => "enrolled and changed by this run, but its file is a declared BinWitnessWet row no CI lane executes (gunbc.rung_drop edited_bin_witness_wet_rows_not_executed_by_ci), so the changed-witness sublane declined it -- dormant, not deleted".to_string(), } } @@ -534,3 +539,5 @@ pub struct FilesystemRemoval; pub struct OutsideRequiredGate; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WithheldCostDebt; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct DeclinedNoCiWetLane; From e5113533c495c9d903c71cc1c0fc9b407e8962f8 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 16:34:39 +0000 Subject: [PATCH 23/24] Every RequiredFloorDisposition consumer decides by an exhaustive match; census filed as the RFM row's second specimen Five hand-written seed sites decided by catch-all or matches!. partition_cost_debt_roster's Some(_) contradicted its .dag authority for the two changed-selection declines (now OutsideThisRunsUniverse, unit red on the restored catch-all). The others are converted without behavior change; the site-projection line now counts the two declines it omitted. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...arm_the_downstream_join_does_not_admit.dag | 1 + src/v1/stage0/src/cli_run.rs | 74 +++++++++++++++-- .../src/cli_run/required_floor_runner.rs | 83 +++++++++++++------ 3 files changed, 123 insertions(+), 35 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag b/dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag index d52df107130..7f33706029c 100644 --- a/dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag +++ b/dag/gunbc/recurring_failure_mode/a_new_decision_arm_the_downstream_join_does_not_admit.dag @@ -10,6 +10,7 @@ data a_new_decision_arm_the_downstream_join_does_not_admit: RecurringFailureMode "INVALID STATE: a new arm is added to a decision (a disposition variant, a standing, a route outcome), and a DOWNSTREAM exactness check over that decision -- a join that says which decided rows count -- was written against the older set of arms and does not admit the new one. Every piece is individually green: the arm compiles, its row is pushed, its projection prints. But the route that reaches the arm always refuses at the join, so the arm is unreachable in production. HARM: the capability the arm exists for is dead on arrival, and it is discovered only when some later change happens to exercise the route, at which point that unrelated change reds.", "SPECIMEN (bold-bat-516, 2026-09-30): gunbc#12794 added RequiredFloorDisposition::DeclinedNoCiWetLane to the required floor's changed-witness sublane and pushed a disposition row for each declined selection. The sublane's exactness join built its right-hand side from PlannedAsChangedWitness rows ONLY, so every declined selection refused as ChangedWitnessSublaneJoinInexact selected_without_disposition. It was first observed on gunbc#12741's floor (run 36768985832), the first PR to edit a BinWitnessWet witness after #12794 merged. #12794's only planned route evidence was that later PR's floor, so the defect merged.", "DISTINGUISHING FACTS: the new arm is a member of an enumeration that some other site FILTERS with an explicit allow-list (a matches! over named arms) rather than an exhaustive match, so adding the variant produced no compile error there; and the change's evidence exercised the arm's own site, not the site that consumes its result.", + "SPECIMEN 2, THE SAME ARM AT THE THIRD AND FOURTH JOIN (gunbc#12741, 2026-10-01): the reverse roster joins (expected-red, route-gap, non-verdict) kept every changed witness on the premise that the changed sublane executes it, so the floor refused the three DeclinedNoCiWetLane identities as stale route-gap rows; and the seed mirror cli_run partition_cost_debt_roster classed DeclinedNoCiWetLane and DeclinedChangedWitnessOutsideDiscovery as DeclaredButNotWithheld through a Some(_) catch-all, contrary to its own authority v2.workflow.required_floor cost_debt_roster_standing (OutsideThisRunsUniverse) -- a refusal waiting for the first rostered wet decline. CENSUS of every consumer of RequiredFloorDisposition and ExpectedRedSuppressionGround, Rust and .dag, at gunbc#12741: every .dag consumer and both generated Rust consumers were already exhaustive; five hand-written seed sites were not -- partition_cost_debt_roster (Some(_)), reconcile_withheld_against_dispositions (matches! DeclinedCostDebt), required_floor_runner enrolment_margin_standing_for (Some(other)), changed_witness_projection_rows (Some(declined)), and the run_required_floor site-projection counters (six matches!, already omitting four arms). All five are converted to exhaustive matches in that change, with a unit red on the restored catch-all (changed_selection_declines_are_outside_this_runs_universe); the reverse joins gained ExpectedRedSuppressionGround::DeclinedNoCiWetLane decided by the exhaustive suppresses_a_changed_witness_enrollment. Residue for these two enumerations after that change: none. The tell, measured: the defect sat only in hand-written seed mirrors of decisions whose .dag authority was already exhaustive.", "RUNG FOUND AT: mitigatable (the join refused loudly, so nothing passed silently -- the cost was an unreachable capability plus a red on the next consumer). RUNG NOW, FOR THE SPECIMEN'S SITE: structurally guaranteed -- gunbc#12833 extracts the join as cli_run required_floor_runner changed_witness_sublane_join, which decides membership through decides_a_changed_selection, an EXHAUSTIVE match over RequiredFloorDisposition, so a new variant does not compile there until it states whether it counts; changed_witness_sublane_join_tests executes it with a DeclinedNoCiWetLane selection (red on the pre-fix allow-list predicate with the floor's own ChangedWitnessSublaneJoinInexact refusal, green on the fix). WHAT THE MERGE PATH EXECUTES, stated exactly (review 73414): the structural claim rests on the exhaustive match, which the required lint step compiles on every pull request; the unit runs on NO CI path -- the rust-unit-tests lane was deleted by the 2026-09-29 operator ruling and the lint step only compiles it (gunbc.rung_drop rust_unit_tests_off_the_merge_path) -- so its red/green is local supporting evidence and not a merge gate. FOR THE CLASS: mitigatable -- other consumers that select disposition arms by an allow-list matches! remain. CEILING: structurally guaranteed at every such consumer. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: every consumer that decides membership over a closed disposition enumeration does so by an exhaustive match, so adding an arm fails to compile at each one until its membership is stated.", ], diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 8e73f00c37c..a638da993c6 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -20281,10 +20281,17 @@ pub fn partition_cost_debt_roster<'a>( CostDebtRosterStanding::WithholdOverriddenForChangedVerdict } Some(RequiredFloorDisposition::DeclinedOutsideGateClosure) - | Some(RequiredFloorDisposition::DeclinedDiscoveryExcluded { .. }) => { - CostDebtRosterStanding::OutsideThisRunsUniverse + | Some(RequiredFloorDisposition::DeclinedDiscoveryExcluded { .. }) + | Some(RequiredFloorDisposition::DeclinedNoCiWetLane { .. }) + | Some(RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { + .. + }) => CostDebtRosterStanding::OutsideThisRunsUniverse, + Some(RequiredFloorDisposition::Planned) + | Some(RequiredFloorDisposition::DeclinedLongModule { .. }) + | Some(RequiredFloorDisposition::DeclinedFixtureMember { .. }) + | Some(RequiredFloorDisposition::DeclinedOutsideRequiredGate) => { + CostDebtRosterStanding::DeclaredButNotWithheld } - Some(_) => CostDebtRosterStanding::DeclaredButNotWithheld, }; (q, standing) }) @@ -20293,6 +20300,24 @@ pub fn partition_cost_debt_roster<'a>( rows } +/// WHETHER A DISPOSITION IS THE COST-DEBT WITHHOLD, as an exhaustive match: a new arm states +/// whether it withholds rather than defaulting to "not cost debt" +/// (`gunbc.recurring_failure_mode.a_new_decision_arm_the_downstream_join_does_not_admit`). +fn disposition_is_a_cost_debt_withhold(disposition: &RequiredFloorDisposition) -> bool { + match disposition { + RequiredFloorDisposition::DeclinedCostDebt => true, + RequiredFloorDisposition::Planned + | RequiredFloorDisposition::PlannedAsChangedWitness + | RequiredFloorDisposition::DeclinedLongModule { .. } + | RequiredFloorDisposition::DeclinedFixtureMember { .. } + | RequiredFloorDisposition::DeclinedOutsideRequiredGate + | RequiredFloorDisposition::DeclinedOutsideGateClosure + | RequiredFloorDisposition::DeclinedDiscoveryExcluded { .. } + | RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { .. } + | RequiredFloorDisposition::DeclinedNoCiWetLane { .. } => false, + } +} + /// The execution-accounting set and the disposition projection must name the SAME identities. /// /// `cost_debt_seen` is accumulated by the build loop as it declines sites; the disposition rows @@ -20311,15 +20336,14 @@ pub fn reconcile_withheld_against_dispositions<'a>( .iter() .map(|q| q.as_str()) .filter(|q| { - !matches!( - dispositions.get(*q), - Some(RequiredFloorDisposition::DeclinedCostDebt) - ) + !dispositions + .get(*q) + .is_some_and(disposition_is_a_cost_debt_withhold) }) .collect(); let mut dispositioned_without_withhold: Vec<&str> = dispositions .iter() - .filter(|(_, d)| matches!(d, RequiredFloorDisposition::DeclinedCostDebt)) + .filter(|(_, d)| disposition_is_a_cost_debt_withhold(d)) .map(|(q, _)| q.as_str()) .filter(|q| !withheld.contains(*q)) .collect(); @@ -46078,6 +46102,40 @@ mod required_floor_disposition_and_storage_agreement_law { ); } + /// THE TWO CHANGED-SELECTION DECLINES ARE OUTSIDE THIS RUN'S UNIVERSE, as + /// `v2.workflow.required_floor` `cost_debt_roster_standing` maps them. The seed's former + /// `Some(_)` catch-all classed both as `DeclaredButNotWithheld` and refused a rostered identity + /// that the changed sublane declined; red on that arm, green on the named one. + #[test] + fn changed_selection_declines_are_outside_this_runs_universe() { + let roster = ident_set(&["m.wet", "m.outside"]); + let dispositions = disp_map(&[ + ( + "m.wet", + RequiredFloorDisposition::DeclinedNoCiWetLane { + pattern: "wet_witness_test.dag".to_string(), + }, + ), + ( + "m.outside", + RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { + module_path: "m".to_string(), + }, + ), + ]); + + let rows = partition_cost_debt_roster(&roster, &dispositions); + + assert_eq!( + standing_of(&rows, "m.wet"), + Some(CostDebtRosterStanding::OutsideThisRunsUniverse) + ); + assert_eq!( + standing_of(&rows, "m.outside"), + Some(CostDebtRosterStanding::OutsideThisRunsUniverse) + ); + } + /// Only `DeclinedCostDebt` is debt. A declared identity carrying any other disposition was /// offered and not withheld, so it refuses — the pre-existing stale arm, preserved. #[test] diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index 5d4110bf58b..9713367d4e0 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -2764,7 +2764,17 @@ pub(crate) fn enrolment_margin_standing_for( match enrolment_gate_execution_disposition(identity, dispositions) { Some(crate::cli_run::RequiredFloorDisposition::Planned) | Some(crate::cli_run::RequiredFloorDisposition::PlannedAsChangedWitness) => {} - Some(other) => { + // Named, not caught: a new arm must state whether the margin gate runs for it. + Some( + other @ (crate::cli_run::RequiredFloorDisposition::DeclinedLongModule { .. } + | crate::cli_run::RequiredFloorDisposition::DeclinedFixtureMember { .. } + | crate::cli_run::RequiredFloorDisposition::DeclinedOutsideRequiredGate + | crate::cli_run::RequiredFloorDisposition::DeclinedCostDebt + | crate::cli_run::RequiredFloorDisposition::DeclinedOutsideGateClosure + | crate::cli_run::RequiredFloorDisposition::DeclinedDiscoveryExcluded { .. } + | crate::cli_run::RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { .. } + | crate::cli_run::RequiredFloorDisposition::DeclinedNoCiWetLane { .. }), + ) => { return EnrolmentMarginStanding::OutsideThisRunsExecution { disposition: required_floor_disposition_label(other).to_string(), }; @@ -3341,7 +3351,17 @@ pub(crate) fn changed_witness_projection_rows( // population it belongs to. It is discharged by making the identity reachable or by // declaring it unreachable — never by a rerun, which is the only affordance one // undifferentiated cause can offer. - Some(declined) => ChangedWitnessProjectionRow { + Some( + declined @ (RequiredFloorDisposition::DeclinedLongModule { .. } + | RequiredFloorDisposition::DeclinedFixtureMember { .. } + | RequiredFloorDisposition::DeclinedOutsideRequiredGate + | RequiredFloorDisposition::DeclinedCostDebt + | RequiredFloorDisposition::DeclinedOutsideGateClosure + | RequiredFloorDisposition::DeclinedDiscoveryExcluded { .. } + | RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { + .. + }), + ) => ChangedWitnessProjectionRow { identity: identity.clone(), cost: None, standing: "declined", @@ -9164,34 +9184,41 @@ pub fn run_required_floor( .map(|identity| (identity, SuppressionGround::DeclinedNoCiWetLane)) .collect() }; - // ONE PRODUCER FOR THE COUNTS: the joined row population, folded once per arm. - let disposition_count = |select: fn(&RequiredFloorDisposition) -> bool| { - disposition_rows - .iter() - .filter(|row| select(&row.disposition)) - .count() - }; + // ONE PRODUCER FOR THE COUNTS: the joined row population, folded once, every arm NAMED so a + // new disposition does not compile here until it states which count it joins + // (`gunbc.recurring_failure_mode.a_new_decision_arm_the_downstream_join_does_not_admit`). let declared_identities = declared_identity_set.len(); - let long_declined = - disposition_count(|d| matches!(d, RequiredFloorDisposition::DeclinedLongModule { .. })); - let fixture_declined = - disposition_count(|d| matches!(d, RequiredFloorDisposition::DeclinedFixtureMember { .. })); - let outside_gate_declined = - disposition_count(|d| matches!(d, RequiredFloorDisposition::DeclinedOutsideRequiredGate)); - let cost_debt_declined = - disposition_count(|d| matches!(d, RequiredFloorDisposition::DeclinedCostDebt)); - let gate_closure_declined = - disposition_count(|d| matches!(d, RequiredFloorDisposition::DeclinedOutsideGateClosure)); - let discovery_excluded_declined = disposition_count(|d| { - matches!( - d, - RequiredFloorDisposition::DeclinedDiscoveryExcluded { .. } - ) - }); + let mut long_declined = 0usize; + let mut fixture_declined = 0usize; + let mut outside_gate_declined = 0usize; + let mut cost_debt_declined = 0usize; + let mut gate_closure_declined = 0usize; + let mut discovery_excluded_declined = 0usize; + let mut no_ci_wet_lane_declined = 0usize; + let mut changed_outside_discovery_declined = 0usize; + for row in &disposition_rows { + match &row.disposition { + RequiredFloorDisposition::Planned + | RequiredFloorDisposition::PlannedAsChangedWitness => {} + RequiredFloorDisposition::DeclinedLongModule { .. } => long_declined += 1, + RequiredFloorDisposition::DeclinedFixtureMember { .. } => fixture_declined += 1, + RequiredFloorDisposition::DeclinedOutsideRequiredGate => outside_gate_declined += 1, + RequiredFloorDisposition::DeclinedCostDebt => cost_debt_declined += 1, + RequiredFloorDisposition::DeclinedOutsideGateClosure => gate_closure_declined += 1, + RequiredFloorDisposition::DeclinedDiscoveryExcluded { .. } => { + discovery_excluded_declined += 1 + } + RequiredFloorDisposition::DeclinedNoCiWetLane { .. } => no_ci_wet_lane_declined += 1, + RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { .. } => { + changed_outside_discovery_declined += 1 + } + } + } eprintln!( "[floor-phase] phase=site-projection state=completed wall_ms={} declared={} sites={} \ files={} claims={} declined_long={} declined_fixture={} declined_outside_gate={} \ - declined_gate_closure={} declined_discovery_excluded={} declined_cost_debt={}", + declined_gate_closure={} declined_discovery_excluded={} declined_cost_debt={} \ + declined_no_ci_wet_lane={} declined_changed_outside_discovery={}", projection_started.elapsed().as_millis(), declared_identities, sites_offered, @@ -9202,7 +9229,9 @@ pub fn run_required_floor( outside_gate_declined, gate_closure_declined, discovery_excluded_declined, - cost_debt_declined + cost_debt_declined, + no_ci_wet_lane_declined, + changed_outside_discovery_declined ); // THE COST-DEBT ROSTER'S STANDING, JOINED AGAINST THE DECLARED UNIVERSE RATHER THAN AGAINST From 315912398e2c6e4962d494da4d9937effc0fe5d7 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 03:13:24 +0000 Subject: [PATCH 24/24] WIP: Resolver cost: demand-scoped typed census, import-facts cost shape, bare --- target_bt.sh | 4 ++++ target_ck.sh | 4 ++++ target_ex.sh | 19 +++++++++++++++++++ target_rr.sh | 8 ++++++++ target_tv.sh | 7 +++++++ 5 files changed, 42 insertions(+) create mode 100644 target_bt.sh create mode 100644 target_ck.sh create mode 100644 target_ex.sh create mode 100644 target_rr.sh create mode 100644 target_tv.sh diff --git a/target_bt.sh b/target_bt.sh new file mode 100644 index 00000000000..1337e445bbf --- /dev/null +++ b/target_bt.sh @@ -0,0 +1,4 @@ +CG=/sys/fs/cgroup/gunbc-probe; S=""; command -v sudo >/dev/null && S=sudo +echo "+memory" | $S tee /sys/fs/cgroup/cgroup.subtree_control >/dev/null; $S mkdir -p $CG; echo 23622320128 | $S tee $CG/memory.max >/dev/null; echo $$ | $S tee $CG/cgroup.procs >/dev/null +for t in cross_tree_bare_reference_tests strict_refusal_counts_blocking_diagnostics changed_witness_sublane_join_tests; do +cargo test --release -p v1-compiler --lib $t 2>&1 | grep -E "test result|error\[|FAILED|panicked" | head -4; done diff --git a/target_ck.sh b/target_ck.sh new file mode 100644 index 00000000000..7f9dc451a2f --- /dev/null +++ b/target_ck.sh @@ -0,0 +1,4 @@ +CG=/sys/fs/cgroup/gunbc-probe; S=""; command -v sudo >/dev/null && S=sudo +echo "+memory" | $S tee /sys/fs/cgroup/cgroup.subtree_control >/dev/null; $S mkdir -p $CG; echo 23622320128 | $S tee $CG/memory.max >/dev/null; echo $$ | $S tee $CG/cgroup.procs >/dev/null +cargo clippy -p v1-compiler --lib --tests -- -D warnings 2>&1 | grep -E "^(error|warning)|-->" | head -20; echo CLIPPY_DONE +cargo test --release -p v1-compiler --lib changed_witness_sublane_join_tests 2>&1 | grep -E "test result|error\[" | head -3 diff --git a/target_ex.sh b/target_ex.sh new file mode 100644 index 00000000000..daa3fb38423 --- /dev/null +++ b/target_ex.sh @@ -0,0 +1,19 @@ +CG=/sys/fs/cgroup/gunbc-probe; S=""; command -v sudo >/dev/null && S=sudo +echo "+memory" | $S tee /sys/fs/cgroup/cgroup.subtree_control >/dev/null; $S mkdir -p $CG; echo 23622320128 | $S tee $CG/memory.max >/dev/null; echo $$ | $S tee $CG/cgroup.procs >/dev/null +F=src/v1/stage0/src/cli_run.rs; cp $F /tmp/keep.rs +python3 - <<'PY' +p='src/v1/stage0/src/cli_run.rs'; s=open(p).read() +a=""" | Some(RequiredFloorDisposition::DeclinedNoCiWetLane { .. }) + | Some(RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { + .. + }) => CostDebtRosterStanding::OutsideThisRunsUniverse,""" +assert s.count(a)==1, "anchor" +s=s.replace(a,""" => CostDebtRosterStanding::OutsideThisRunsUniverse, + Some(RequiredFloorDisposition::DeclinedNoCiWetLane { .. }) + | Some(RequiredFloorDisposition::DeclinedChangedWitnessOutsideDiscovery { .. }) => CostDebtRosterStanding::DeclaredButNotWithheld,""") +open(p,'w').write(s) +PY +cargo test --release -p v1-compiler --lib changed_selection_declines_are_outside_this_runs_universe 2>&1 | grep -E "test result|error\[|FAILED" | head -3; echo "^^ REVERTED-ARM RUN (expect FAILED)" +cp /tmp/keep.rs $F +cargo clippy -p v1-compiler --lib --tests -- -D warnings 2>&1 | grep -E "^(error|warning)|-->" | head -20; echo CLIPPY_DONE +for t in changed_selection_declines_are_outside_this_runs_universe only_the_cost_debt_disposition changed_witness_sublane_join_tests; do cargo test --release -p v1-compiler --lib $t 2>&1 | grep -E "test result|FAILED|panicked" | head -2; done diff --git a/target_rr.sh b/target_rr.sh new file mode 100644 index 00000000000..ab99a23d76d --- /dev/null +++ b/target_rr.sh @@ -0,0 +1,8 @@ +CG=/sys/fs/cgroup/gunbc-probe; S=""; command -v sudo >/dev/null && S=sudo +echo "+memory" | $S tee /sys/fs/cgroup/cgroup.subtree_control >/dev/null; $S mkdir -p $CG; echo 23622320128 | $S tee $CG/memory.max >/dev/null; echo $$ | $S tee $CG/cgroup.procs >/dev/null +git stash push -q -- src/v1/stage0/src/cli_run/required_floor_runner.rs +cargo build --release -p v1-compiler --bin claim_executor > /tmp/b.txt 2>&1; echo BUILD=$?; tail -3 /tmp/b.txt +./target/release/claim_executor --required-regen --source-root dag --source-root src/v2 > /tmp/rg.txt 2>&1; echo REGEN=$?; tail -5 /tmp/rg.txt | cut -c1-300 +C=target/stage0-regen-candidate/src +for f in $(cd $C && ls); do cmp -s $C/$f src/v1/stage0/src/$f || echo "DIFFERS $f"; done | head +echo "@@B64BEGIN"; base64 -w0 $C/v1_compiler_expected_red_roster_join.rs; echo; echo "@@B64END" diff --git a/target_tv.sh b/target_tv.sh new file mode 100644 index 00000000000..1854532aa03 --- /dev/null +++ b/target_tv.sh @@ -0,0 +1,7 @@ +CG=/sys/fs/cgroup/gunbc-probe; S=""; command -v sudo >/dev/null && S=sudo +echo "+memory" | $S tee /sys/fs/cgroup/cgroup.subtree_control >/dev/null; $S mkdir -p $CG; echo 17179869184 | $S tee $CG/memory.max >/dev/null; echo $$ | $S tee $CG/cgroup.procs >/dev/null +git rev-parse --short HEAD +cargo build --release -p v1-compiler --bin gunbc > /dev/null 2>&1; echo BUILD=$? +D=src/v2/test/claim/manual +printf 'module v2.test.manual.ctl_trivial\n\nimport v2.std.logic { Bool }\n\ntest fn ctl_t() -> Bool { true }\n' > $D/ctl_trivial_test.dag +./target/release/gunbc run --claim-run --source-root dag --source-root src/v2 --entry $D/ctl_trivial_test.dag 2>&1 | grep -E "phase=|population|process_cpu|PASS|FAIL" | cut -c1-200