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/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/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..c43727f60d2 --- /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 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/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.rs b/src/v1/stage0/src/cli_run.rs index 4957b2633b4..a638da993c6 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -9598,6 +9598,86 @@ 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 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()); +} + +/// `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, +) -> 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()); + }; + // 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| 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), + 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`. @@ -9822,11 +9902,43 @@ 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)?)?; + // 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 \ + ({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, is_test_row)) = target_module else { @@ -13216,6 +13328,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 = @@ -13553,6 +13668,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, @@ -13615,6 +13731,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", @@ -13725,6 +13842,9 @@ pub(crate) fn multi_entry_index_sharing_control( 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>>, } @@ -15756,6 +15876,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). @@ -15817,6 +15940,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; @@ -15935,6 +16060,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, @@ -17347,8 +17474,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( @@ -20148,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) }) @@ -20160,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 @@ -20178,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(); @@ -45945,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/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index bd397f26288..3f64a6cb62c 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -1168,6 +1168,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"))] @@ -2904,9 +2905,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( @@ -2919,6 +2929,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) } @@ -4029,6 +4040,358 @@ mod heads_parse_count { } } +/// 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 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 no_import_less_file_reaches_across_trees_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 scanned = 0usize; + let mut refusals: Vec = Vec::new(); + for sf in &sources { + if super::super::source_declares_import_lines(&sf.content) { + continue; + } + scanned += 1; + let r = super::super::visit_bare_reference_providers( + sf, + &index, + |root| super::super::closure_name_census(&index, root), + |_, _, _| Ok(()), + ); + if let Err(e) = r { + refusals.push(e); + } + } + let pool_census_built = index.closure_name_censuses.borrow().contains_key(&None); + eprintln!( + "CROSSTREE scanned={scanned} refusals={} pool_census_built={pool_census_built}", + refusals.len() + ); + for r in &refusals { + eprintln!("CROSSTREE 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()); + let mut divergent: Vec = Vec::new(); + { + 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()) + { + 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()); + } +} + +/// 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"); + } +} + /// THE MODULE-LEVEL CLASSES A LEAVE-ONE-OUT READING CAN REMOVE WHOLE. Each is one field of every /// `TypedModule`, dropped for ALL modules at once, so a structure one module links from another /// (a TypeEnv parent, an interface import) goes with its class rather than surviving through the 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..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", @@ -9131,34 +9151,74 @@ 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)?; - // 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() - }; + // 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, 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, @@ -9169,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 @@ -9346,7 +9408,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 +9468,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 +9666,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 +16889,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; 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: _ } => { 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 f3b79ac0c32..320dcec960c 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