From 6f50b747f451e837996e9dbb5289cfdce52c384c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 03:29:50 +0000 Subject: [PATCH 1/8] floor: one base-tree commit per run for every base-side reader; discharge a_roster_edit_judged_against_the_base_tip_not_the_merge_base Co-Authored-By: Claude Opus 5.5 (1M context) --- ...gainst_the_base_tip_not_the_merge_base.dag | 29 +++ src/v1/stage0/src/cli_run.rs | 142 +++++++---- src/v1/stage0/src/cli_run/non_fold_residue.rs | 3 +- .../src/cli_run/required_floor_runner.rs | 238 ++++++++++++++++-- 4 files changed, 334 insertions(+), 78 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag diff --git a/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag new file mode 100644 index 00000000000..012a8731c4a --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag @@ -0,0 +1,29 @@ +module gunbc.recurring_failure_mode.a_roster_edit_judged_against_the_base_tip_not_the_merge_base + +import std.types { NonEmptyStr } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data a_roster_edit_judged_against_the_base_tip_not_the_merge_base: RecurringFailureMode = RecurringFailureMode { + identity: "a_roster_edit_judged_against_the_base_tip_not_the_merge_base" as NonEmptyStr, + + receipts: [ + "INVALID STATE: a required-floor judgment of a ROSTER EDIT reads the base side at the base ref's TIP, while the floor's declared comparison is a merge-base one. A branch that edits the roster and then falls behind main is judged as if it authored every roster change main made after the branch point, in reverse. The change it is charged with is not in its diff, and the merge that would land it does not contain that change either: git's three-way merge keeps main's later edit to rows the branch did not touch.", + + "OCCURRENCES, 2026-10-05, two PRs from one parent lane in one hour. gunbc#13333 (clever-raven-680) and gunbc#13341 (sunny-ant-198) each edited v2.workflow.floor_unimported_bare_provider_debt_roster. Main then retired roadmap_launch_deployment_receipt rows. Both floors went red with RosterRetirementChanged (Retired -> ActiveDebt) on rows neither diff touched, and both went green after merging origin/main with no change to the fix. Each lane spent a diagnosis round on a red its diff did not cause.", + + "THE MECHANISM, read from the code, not inferred from the symptom. v1_compiler.cli_run required_floor_runner interface_consumer_planning matches floor_diff_comparison_readout and, on the FreezeBaselineComparison MergeBase arm, resolves `git merge-base base head` before reading the base tree; its comment states the floor's window is the relation the affected-set diff was taken under. unimported_bare_provider_gate and the cost-debt edit-budget arm of the claim planner instead call floor_diff_comparison_readout()?.base() and read the roster at that ref through v2.workflow.floor_unimported_bare_provider_debt unimported_bare_provider_roster_at_base (git.Core.Show at the tip). The same readout therefore yields two different base trees in one run, depending on which consumer asks.", + + "HARM AND RUNG. As observed, it is a LOUD false red: the line stops, typed and located, on a fact the diff did not author. That is §5-shaped but mis-aimed, and the cost is real: diagnosis rounds, and a standing incentive to merge main reflexively instead of reading the refusal. The SILENT arm is reachable in the same class through an admission reading. A roster whose base-tip membership decides what a change ADMITS (gunbc#13332's cost_debt_admitted_identities, at its head 103a8ea847, reads floor_diff_comparison_readout()?.base() the same way) charges a behind branch with admitting every row main retired since the branch point. That is spurious work at best, and a refusal attributed to the wrong change at worst. Found at rung 1, mitigated only by authors merging main.", + + "WHY THIS IS NOT stale_claim_survives_its_own_correct_edit. That row is a dependent assertion left un-updated after a correct premise edit. Here no assertion is stale; the base READ is taken at the wrong commit, so a correct roster and a correct rule still disagree.", + + "CEILING 3, structurally guaranteed, with the reason: FreezeBaselineComparison already carries the relation as a closed coproduct, so the base commit a roster judgment reads is decidable from the readout alone. Carrying the RESOLVED base commit on the readout, and deleting the bare base() accessor as a route to a tree, leaves no consumer able to read the tip under a merge-base relation. It does not reach 4 because a consumer handed a commit can still pass a different one to git; the construction removes the accessor that invites it, not git's ability to read any ref.", + + "NEXT-RUNG TRIGGER (a CAPABILITY, not an artifact), classified CAN CLIMB NOW BUT UNBUILT: every required-floor consumer that reads a base-side tree obtains the base COMMIT from one resolution of floor_diff_comparison_readout that applies the MergeBase arm, so interface planning, the unimported-bare-provider roster edit, the cost-debt edit budget and cost-debt admission read the same commit in one run. A per-site merge-base call added to one gate does not discharge this row; it fixes one site and leaves the accessor standing for the next.", + + "RECOGNITION RULE AND REVIEW TELL: a refusal naming rows the diff did not touch, which clears after merging main with no change to the diff, is this class until shown otherwise. In review, grep `.base()` on the comparison readout: every caller that reads a tree with it, rather than printing it, is a member.", + + "DISCHARGED, 2026-10-05 (crisp-raven-17), by the trigger above and at the grain it names. v1_compiler.cli_run FreezeBaselineComparison resolve_window is now the one place the relation is read for a tree: it returns ResolvedComparisonWindow { base_tree, head }, the base ref under two-dot and the departure point under merge-base, refusing FreezeBaselineUnobservable or FreezeBaselineUnrelatedHistory rather than degrading to a ref. v1_compiler.cli_run required_floor_runner FloorBaseTree resolves that window at most once per floor run and is lent to every base-tree reader: interface_consumer_planning, unimported_bare_provider_gate, the cost-debt edit budget in run_required_floor, floor_base_test_decl_census, floor_base_file_read and through it non_fold_residue_changed_row_subjects. The frozen path-deferral gate collect_frozen_path_deferral_additions_for takes the same resolve_window instead of its own copy of the arm. The bare base() accessor is deleted; base_ref() remains for printing and its doc says it is not a tree, so a reader re-adding a tip read now has to spell a ref it was told not to use. gunbc#13332's cost_debt_admitted_identities read the tip the same way and had not landed when this did; it no longer compiles against base(), so whichever of the two lands second must take FloorBaseTree, loudly rather than silently. EVIDENCE AND ITS RUNG, stated no higher than it executes: required_floor_runner pure_producer_share_tests roster_edit_is_judged_at_the_merge_base_not_the_base_tip builds a real history (branch departs at P, main then moves a retirement the branch never touched), asserts the tip reading refuses RosterRetirementChanged as the executed contrast, asserts the window's base tree is P and nothing refuses, and keeps a positive control where the branch really un-retires the row and still refuses. That test is a Rust unit test, which runs on no CI path while gunbc.rung_drop rust_unit_tests_off_the_merge_path stands, so the regression evidence is mechanically preventable only once that drop is restored; the construction itself (one window, no tree accessor) is what this row's ceiling asked for.", + ], + evidence: [], +} diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index a8fdc567fb5..0d145687665 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -25027,13 +25027,89 @@ pub enum FreezeBaselineComparison { }, } +/// THE TWO COMMITS A BASE-TREE READER MAY TAKE, resolved from a `FreezeBaselineComparison` by +/// `resolve_window` and by nothing else. `base_tree` is the commit whose TREE is the base side of +/// the comparison: the base ref itself under two-dot, the departure point under merge-base. Every +/// reader of a base-side file takes this commit, so no reader can choose a relation for itself -- +/// the defect `gunbc.recurring_failure_mode` `a_roster_edit_judged_against_the_base_tip_not_the_merge_base` +/// records, where two readers took the base TIP while a third applied the merge-base arm and a +/// roster row main retired after the branch point read as this change's edit. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedComparisonWindow { + pub base_tree: String, + pub head: String, +} + impl FreezeBaselineComparison { - fn base(&self) -> &str { + /// The base REF as the authority named it. For printing and locating only: it is not a tree. + /// A reader that needs base-side content takes `resolve_window`'s `base_tree`, because under + /// merge-base mode this ref's tree carries every change the base made after the head departed. + pub(crate) fn base_ref(&self) -> &str { match self { Self::Direct { base, .. } | Self::MergeBase { base, .. } => base, } } + /// THE BASE COMMIT, with the relation applied -- the one place the arm is read for a tree. + /// Two-dot compares the exact base; merge-base compares the commit the head departed from. + /// The match may never grow a default: imposing merge-base on a two-dot arm passes growth on a + /// rewritten push, and reading the tip on a merge-base arm attributes the base's own later + /// edits to the change. Each refusal carries a typed cause and never degrades to a ref. + pub(crate) fn resolve_window( + &self, + root: &std::path::Path, + ) -> Result { + let run = |args: &[&str]| -> Result { + std::process::Command::new("git") + .args(args) + .current_dir(root) + .output() + .map_err(|e| format!("git {args:?}: {e}")) + }; + let resolve = |rev: &str, role: &str| -> Result { + let out = run(&[ + "rev-parse", + "--verify", + "--quiet", + &format!("{rev}^{{commit}}"), + ])?; + if !out.status.success() { + return Err(format!( + "cause=FreezeBaselineUnobservable {role}={rev} — one endpoint of the \ + comparison could not be read, so the base side is unobservable. \ + Could-not-read and permits-this are different states and this refuses \ + rather than conflating them. Fetch the ref, or set GUNBC_CI_DIFF_BASE to a \ + resolvable rev." + )); + } + Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) + }; + let base = self.base_ref(); + let base_commit = resolve(base, "base")?; + let head = resolve(self.head(), "head")?; + let base_tree = match self { + Self::Direct { .. } => base_commit, + Self::MergeBase { .. } => { + let merge_base = run(&["merge-base", &base_commit, &head])?; + let fork_point = String::from_utf8_lossy(&merge_base.stdout) + .trim() + .to_string(); + if !merge_base.status.success() || fork_point.is_empty() { + return Err(format!( + "cause=FreezeBaselineUnrelatedHistory base={base} — this comparison is \ + merge-base mode, so the base side is the commit the head DEPARTED from, \ + and git merge-base named no common ancestor. No-common-ancestor and \ + permits-this are different states and this refuses rather than \ + conflating them. (Under two-dot mode the same history is directly \ + comparable and does NOT reach here.)" + )); + } + fork_point + } + }; + Ok(ResolvedComparisonWindow { base_tree, head }) + } + fn head(&self) -> &str { match self { Self::Direct { head, .. } | Self::MergeBase { head, .. } => head, @@ -25064,7 +25140,7 @@ pub fn collect_frozen_path_deferral_additions_for( let located = |msg: String| -> String { format!( "{msg} (comparison base={} head={} kind={} mode={})", - comparison.base(), + comparison.base_ref(), comparison.head(), comparison.kind(), match comparison { @@ -25073,59 +25149,15 @@ pub fn collect_frozen_path_deferral_additions_for( } ) }; - let base = comparison.base(); - let resolve = |rev: &str, role: &str| -> Result { - let out = run(&[ - "rev-parse", - "--verify", - "--quiet", - &format!("{rev}^{{commit}}"), - ])?; - if !out.status.success() { - return Err(located(format!( - "WITNESS ADMISSION REFUSAL cause=FreezeBaselineUnobservable {role}={rev} — the \ - frozen path-deferral roster is a monotone debt contract and one endpoint of its \ - comparison could not be read, so growth cannot be ruled out. Could-not-read and \ - permits-this are different states and this arm refuses rather than conflating \ - them. Fetch the ref, or set GUNBC_CI_DIFF_BASE to a resolvable rev." - ))); - } - Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) - }; - let base_commit = resolve(base, "base")?; - let head_commit = resolve(comparison.head(), "head")?; - - // THE BASELINE COMMIT, and the whole content of the relation half: two-dot compares the exact - // base, merge-base compares the departure point. Choosing one for both is a fork of the - // authority's own decision, and the direction it fails matters — imposing merge-base on a - // two-dot arm passes growth (a fail-open), so this match may never grow a default. - let baseline_commit = match comparison { - FreezeBaselineComparison::Direct { .. } => base_commit, - FreezeBaselineComparison::MergeBase { .. } => { - let merge_base = run(&["merge-base", &base_commit, &head_commit])?; - if !merge_base.status.success() { - return Err(located(format!( - "WITNESS ADMISSION REFUSAL cause=FreezeBaselineUnrelatedHistory base={base} — \ - this comparison is merge-base mode, so the roster is monotone against the \ - commit the head DEPARTED from, and git merge-base found no common ancestor. \ - No-common-ancestor and permits-this are different states and this arm refuses \ - rather than conflating them. (Under two-dot mode the same history is directly \ - comparable and does NOT reach here.)" - ))); - } - let fork_point = String::from_utf8_lossy(&merge_base.stdout) - .trim() - .to_string(); - if fork_point.is_empty() { - return Err(located(format!( - "WITNESS ADMISSION REFUSAL cause=FreezeBaselineUnrelatedHistory base={base} — \ - git merge-base succeeded but named no commit, so the baseline is unobservable \ - and growth cannot be ruled out." - ))); - } - fork_point - } - }; + let base = comparison.base_ref(); + // THE BASELINE COMMIT is the window's base tree: the relation is applied once, by the + // comparison itself, for this gate and every floor base-tree reader alike. + let ResolvedComparisonWindow { + base_tree: baseline_commit, + head: head_commit, + } = comparison + .resolve_window(root) + .map_err(|cause| located(format!("WITNESS ADMISSION REFUSAL {cause} (frozen path-deferral roster: a monotone debt contract, so growth cannot be ruled out)")))?; // THE CURRENT SIDE IS THE SELECTED HEAD, not an ambient one, and this runs BEFORE any arm that // can permit. Reading the live filesystem keeps an uncommitted local roster edit in scope (the diff --git a/src/v1/stage0/src/cli_run/non_fold_residue.rs b/src/v1/stage0/src/cli_run/non_fold_residue.rs index 8e5bd9d3d82..2b6f3632ac6 100644 --- a/src/v1/stage0/src/cli_run/non_fold_residue.rs +++ b/src/v1/stage0/src/cli_run/non_fold_residue.rs @@ -533,8 +533,9 @@ pub(crate) fn non_fold_residue_classify_row_subject( /// The subjects of every roster row the diff added or deleted, read at the floor's resolved diff /// base and classified by exact path. Refuses when the base roster cannot be read. pub(crate) fn non_fold_residue_changed_row_subjects( + base_tree: &crate::cli_run::required_floor_runner::FloorBaseTree, ) -> Result, String> { - let base_rows = match floor_base_file_read(NON_FOLD_RESIDUE_AUTHORITY_REL)? { + let base_rows = match floor_base_file_read(base_tree, NON_FOLD_RESIDUE_AUTHORITY_REL)? { None => Vec::new(), Some(content) => { non_fold_residue_units_from_module_source(NON_FOLD_RESIDUE_AUTHORITY_REL, &content) 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 a0c4aef0ce1..3045553403c 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -713,6 +713,59 @@ pub(crate) fn floor_diff_comparison_readout() -> Result>, +} + +impl FloorBaseTree { + pub(crate) fn unresolved() -> Self { + Self { + window: std::cell::OnceCell::new(), + } + } + + fn window(&self) -> Result<&(FreezeBaselineComparison, ResolvedComparisonWindow), String> { + self.window + .get_or_init(|| { + let comparison = floor_diff_comparison_readout()?; + let window = comparison.resolve_window(&process_workspace_root())?; + Ok((comparison, window)) + }) + .as_ref() + .map_err(Clone::clone) + } + + /// The commit whose tree is the base side of this run's comparison. + pub(crate) fn commit(&self) -> Result<&str, String> { + Ok(self.window()?.1.base_tree.as_str()) + } + + /// The resolved head commit of this run's comparison. + pub(crate) fn head(&self) -> Result<&str, String> { + Ok(self.window()?.1.head.as_str()) + } + + /// The base REF and the base-tree commit, for a located line. Never a tree route. + pub(crate) fn located(&self) -> Result { + let (comparison, window) = self.window()?; + Ok(format!( + "base_ref={} base_tree={}", + comparison.base_ref(), + window.base_tree + )) + } +} + pub(crate) fn floor_diff_baseline_readout() -> Result<(String, String), String> { use v1_interpreter::Value; let roots = default_source_roots(); @@ -894,16 +947,19 @@ pub(crate) fn cost_debt_changed_witness_ceilings( /// One file's content at the resolved diff base (`v2.workflow.floor_diff_observe` /// `floor_run_base_file_read`): `Ok(None)` when the base does not carry the path, `Err` when the /// listing or show refused -- a refusal is never read as an absent or empty file. -pub(crate) fn floor_base_file_read(path: &str) -> Result, String> { +pub(crate) fn floor_base_file_read( + base_tree: &FloorBaseTree, + path: &str, +) -> Result, String> { use v1_interpreter::Value; - let comparison = floor_diff_comparison_readout()?; + let base = base_tree.commit()?; let roots = default_source_roots(); let entry = "src/v2/workflow/floor_diff_observe.dag"; let (graph, indices) = resolve_entry_graph_shared(&roots, entry) .map_err(|e| format!("floor_diff_observe resolve: {e}"))?; let ctx = make_eval_context(&graph, indices, v1_interpreter::ExecutionMode::Wet); let args = [ - (Some("base".to_string()), str_value(comparison.base())), + (Some("base".to_string()), str_value(base)), (Some("path".to_string()), str_value(path)), ]; let result = @@ -942,13 +998,14 @@ pub(crate) fn floor_base_file_read(path: &str) -> Result, String> /// Authority: `v2.workflow.floor_diff_observe` `floor_run_base_test_decl_census`. A refused /// census is an observation failure and never becomes an empty map. pub(crate) fn floor_base_test_decl_census( + base_tree: &FloorBaseTree, paths: &[String], ) -> Result>, String> { use v1_interpreter::Value; if paths.is_empty() { return Ok(std::collections::HashMap::new()); } - let comparison = floor_diff_comparison_readout()?; + let base = base_tree.commit()?; let roots = default_source_roots(); let entry = "src/v2/workflow/floor_diff_observe.dag"; let (graph, indices) = resolve_entry_graph_shared(&roots, entry) @@ -956,7 +1013,7 @@ pub(crate) fn floor_base_test_decl_census( let ctx = make_eval_context(&graph, indices, v1_interpreter::ExecutionMode::Wet); let path_values: Vec = paths.iter().map(|p| str_value(p.clone())).collect(); let args = [ - (Some("base".to_string()), str_value(comparison.base())), + (Some("base".to_string()), str_value(base)), (Some("paths".to_string()), list_value_from_vec(path_values)), ]; let result = v1_interpreter::run_in_context_with_args( @@ -1497,6 +1554,7 @@ pub(crate) struct ChangedWitnessProjectionRow { /// the `changed_witness_identities` wrapper above it (already callerless on `main`) are deleted /// here rather than left for that later consolidation. fn changed_and_enrolled_witness_identities_with_index( + base_tree: &FloorBaseTree, index: &MultiEntryIndex, source_roots: &[String], planning_index: Option<&crate::cli_run::declaration_index::DeclarationIndex>, @@ -1512,6 +1570,7 @@ fn changed_and_enrolled_witness_identities_with_index( // THE SAME DIFF every projection below reads, so the rule and the planned subject cannot // describe different changes. unimported_bare_provider_gate( + base_tree, index, source_roots, &changed_paths, @@ -1530,7 +1589,7 @@ fn changed_and_enrolled_witness_identities_with_index( } let dag_path_list: Vec = dag_paths.into_iter().collect(); floor_seam("diff-base-decl-census"); - let base_test_decl_names = floor_base_test_decl_census(&dag_path_list)?; + let base_test_decl_names = floor_base_test_decl_census(base_tree, &dag_path_list)?; floor_seam("diff-edits"); let edits = floor_diff_edits_from_line_ranges( index, @@ -1587,7 +1646,8 @@ fn changed_and_enrolled_witness_identities_with_index( floor_seam("interface-consumer-planning"); let touched_declarations = declaration_seeds_from_touched_declarations(&root, &edits.touched_declarations)?; - let interface_consumers = interface_consumer_planning(planning_index, &touched_declarations)?; + let interface_consumers = + interface_consumer_planning(base_tree, planning_index, &touched_declarations)?; // THE FIFTH PROJECTION IS THE DIFF'S PATHS THEMSELVES, for the checker-input rule // (`v2.workflow.floor_subject_seed` `checker_subject_rule`): a changed non-.dag path is // structural-empty for the .dag frontier above and still an input to every Strict verdict @@ -1703,6 +1763,7 @@ pub(crate) fn declaration_seeds_from_touched_declarations( /// rebuilt. A floor invoked without it on a CI commit is refused below rather than planned /// blind; a local run without a diff baseline never reaches here. fn interface_consumer_planning( + base_tree: &FloorBaseTree, planning_index: Option<&crate::cli_run::declaration_index::DeclarationIndex>, touched_declarations: &std::collections::BTreeSet<(String, String)>, ) -> Result { @@ -1721,15 +1782,8 @@ fn interface_consumer_planning( // direct comparison at the base ref itself -- the same relation the affected-set diff was // taken under, so the interface delta and the line-range attribution describe one change. let workspace = process_workspace_root(); - let (base_commit, head_commit) = match floor_diff_comparison_readout()? { - FreezeBaselineComparison::Direct { base, head, .. } => (base, head), - FreezeBaselineComparison::MergeBase { base, head, .. } => { - let merge_base = git_stdout(&workspace, &["merge-base", &base, &head])?; - (merge_base, head) - } - }; - let base_commit = git_stdout(&workspace, &["rev-parse", &base_commit])?; - let head_commit = git_stdout(&workspace, &["rev-parse", &head_commit])?; + let base_commit = base_tree.commit()?.to_string(); + let head_commit = base_tree.head()?.to_string(); match reconstruct_base_index(&workspace, &base_commit, &head_commit, head_index)? { BaselineReconstruction::NoSubject { head } => { Ok(InterfaceConsumerPlanning::NoSubject { head }) @@ -2567,6 +2621,7 @@ fn unimported_bare_provider_standing_refusals( /// that is decided from the diff's own added paths, never from a failed base read. Every refusal is /// reported before the line stops. pub(crate) fn unimported_bare_provider_gate( + base_tree: &FloorBaseTree, index: &MultiEntryIndex, source_roots: &[String], changed_paths: &[String], @@ -2597,7 +2652,8 @@ pub(crate) fn unimported_bare_provider_gate( head.rows.len() ); } else { - let base = floor_diff_comparison_readout()?.base().to_string(); + let base = base_tree.commit()?.to_string(); + let located = base_tree.located()?; let base_source = unimported_bare_provider_roster_source_at_base(source_roots, &base)?; let base_reading = unimported_bare_provider_base_reading(&base_source)?; let base_coherence = head.refusals( @@ -2609,7 +2665,7 @@ pub(crate) fn unimported_bare_provider_gate( .chain(head.judge_edit(&base_reading, &head)?) { refusals.push(format!( - "{ROUTE} cause=UnimportedBareProvider {r} base={base}" + "{ROUTE} cause=UnimportedBareProvider {r} {located}" )); } } @@ -6872,6 +6928,8 @@ pub fn run_required_floor( // that ancestor. The entry index one line above was already shared for exactly this reason -- // the corpus read simply never was. let floor_corpus = crate::cli_run::read_source_corpus_once(source_roots); + // ONE BASE TREE FOR THE RUN (`FloorBaseTree`): every base-side read below takes this commit. + let floor_base_tree = FloorBaseTree::unresolved(); // ONE DERIVATION, CONSUMED FOUR WAYS. The same diff observation supplies changed-witness // identities, newly enrolled identities, the compile-subject modules of // `touched_entry_files`, and the match-bearing consumers of every coproduct whose arm set @@ -6879,6 +6937,7 @@ pub fn run_required_floor( // over which identities this run promised to execute. let (changed_witnesses, newly_enrolled_witnesses, compile_subject, diff_paths) = match changed_and_enrolled_witness_identities_with_index( + &floor_base_tree, &gate_entry_index, source_roots, planning_index.as_ref(), @@ -7132,9 +7191,9 @@ pub fn run_required_floor( .iter() .any(|m| m == "gunbc.non_fold_residue") => { - crate::cli_run::non_fold_residue_changed_row_subjects().map_err(|e| { - format!("REQUIRED-FLOOR REFUSAL cause=NonFoldResidueRosterBaseUnreadable {e}") - })? + crate::cli_run::non_fold_residue_changed_row_subjects(&floor_base_tree).map_err( + |e| format!("REQUIRED-FLOOR REFUSAL cause=NonFoldResidueRosterBaseUnreadable {e}"), + )? } _ => Vec::new(), }; @@ -8610,7 +8669,7 @@ pub fn run_required_floor( } else if grandfathered_roster.contains(&identity) { grandfathered_eval_step_budget } else if cost_debt_roster.contains(&identity) { - let base = floor_diff_comparison_readout()?.base().to_string(); + let base = floor_base_tree.commit()?.to_string(); let rel_path = normalize_repo_path(&workspace_relative_repo_path(&file.path)); if file_cost_debt_ceilings.is_none() { let head_source = std::fs::read_to_string(&file.path).map_err(|e| { @@ -12011,6 +12070,141 @@ mod pure_producer_share_tests { }); } + /// THE BASE TREE IS THE DEPARTURE POINT, for the roster edit judgment + /// (`gunbc.recurring_failure_mode` `a_roster_edit_judged_against_the_base_tip_not_the_merge_base`). + /// A real history: the branch departs at P, main then moves a retirement the branch never + /// touched. The merge-base window reads P and the branch's untouched row is not this change's + /// edit; the TIP, which is what the gate read before `FloorBaseTree`, attributes main's own + /// later edit to the branch -- asserted as the executed contrast. The positive control is a + /// branch that really rewrites the retirement: it still refuses through the same window. + #[test] + fn roster_edit_is_judged_at_the_merge_base_not_the_base_tip() { + crate::cli_run::on_live_pool_thread(|| { + let root = process_workspace_root(); + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| root.join(r).to_string_lossy().to_string()) + .collect(); + let judge = UnimportedBareProviderRosterReading::head(&roots).expect("head roster"); + let roster_head = std::fs::read_to_string(root.join(UNIMPORTED_BARE_PROVIDER_ROSTER)) + .expect("roster source") + .split("data unimported_bare_provider_dispositions:") + .next() + .expect("roster head") + .to_string(); + let roster = |standing: &str| { + format!( + "{roster_head}data unimported_bare_provider_dispositions: \ + List = [\n UnimportedBareProviderDisposition \ + {{ file: \"dag/a.dag\", name: \"f\", standing: {standing} }},\n]\n" + ) + }; + let fixed = "Retired { cause: ImportsFixed }"; + let deleted = "Retired { cause: FileDeleted }"; + + let dir = std::env::temp_dir().join(format!( + "gunbc-floor-base-tree-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock") + .as_nanos() + )); + let path = dir.join(UNIMPORTED_BARE_PROVIDER_ROSTER); + std::fs::create_dir_all(path.parent().expect("roster parent")).expect("mkdir"); + let git = |args: &[&str]| -> String { + let out = std::process::Command::new("git") + .args(args) + .current_dir(&dir) + .output() + .unwrap_or_else(|e| panic!("git {args:?}: {e}")); + assert!( + out.status.success(), + "git {args:?}: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8_lossy(&out.stdout).trim().to_string() + }; + let commit = |standing: &str, msg: &str| { + std::fs::write(&path, roster(standing)).expect("write roster"); + git(&["add", "-A"]); + git(&["commit", "--quiet", "-m", msg]); + git(&["rev-parse", "HEAD"]) + }; + git(&["init", "--quiet", "--initial-branch", "main", "."]); + git(&["config", "user.email", "fixture@gunbc.invalid"]); + git(&["config", "user.name", "fixture"]); + let departure = commit(fixed, "P: the row is retired ImportsFixed"); + git(&["checkout", "--quiet", "-b", "subject"]); + std::fs::write(dir.join("unrelated.txt"), "the branch's own change\n").expect("write"); + git(&["add", "-A"]); + git(&[ + "commit", + "--quiet", + "-m", + "H: touches nothing in the roster", + ]); + let untouched_head = git(&["rev-parse", "HEAD"]); + git(&["checkout", "--quiet", "main"]); + let tip = commit( + deleted, + "C: main moves the retirement after the branch point", + ); + git(&["checkout", "--quiet", "subject"]); + + let read_at = |rev: &str| { + let src = git(&["show", &format!("{rev}:{UNIMPORTED_BARE_PROVIDER_ROSTER}")]); + unimported_bare_provider_base_reading(&(src + "\n")).expect("lone roster") + }; + let window = |head: &str| { + FreezeBaselineComparison::MergeBase { + base: "main".to_string(), + head: head.to_string(), + kind: "MergeTargetBaseline".to_string(), + } + .resolve_window(&dir) + .expect("merge-base window") + }; + + // RED (before the window): the tip reads main's later edit as this change's. + let head_reading = read_at(&untouched_head); + assert_eq!( + judge.judge_edit(&read_at(&tip), &head_reading).expect("verdict"), + vec![ + "RosterRetirementChanged dag/a.dag#f (Retired FileDeleted -> Retired ImportsFixed)" + .to_string() + ], + "the base tip attributes main's own later retirement edit to the branch" + ); + // THE WALL: the window's base tree is the departure point, and nothing refuses. + let resolved = window(&untouched_head); + assert_eq!(resolved.base_tree, departure); + assert_eq!(resolved.head, untouched_head); + assert_eq!( + judge + .judge_edit(&read_at(&resolved.base_tree), &head_reading) + .expect("verdict"), + Vec::::new(), + "a row main retired after the branch point is not this change's edit" + ); + + // POSITIVE CONTROL: the branch really rewrites the retirement, and still refuses. + let edited_head = commit("ActiveDebt", "H2: the branch un-retires the row"); + let resolved = window(&edited_head); + assert_eq!(resolved.base_tree, departure); + assert_eq!( + judge + .judge_edit(&read_at(&resolved.base_tree), &read_at(&edited_head)) + .expect("verdict"), + vec![ + "RosterRetirementChanged dag/a.dag#f (Retired ImportsFixed -> ActiveDebt)" + .to_string() + ] + ); + std::fs::remove_dir_all(&dir).ok(); + }); + } + /// THE REAL BASE READ, END TO END: the roster at `HEAD` is read through the `.dag` /// `unimported_bare_provider_roster_at_base` (a real `git show`), decoded, evaluated alone and /// judged against a head that retires its first active row -- which admits -- and against a head From 9d1198266299723637ba89ef3ad1a46b52af9799 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 03:57:48 +0000 Subject: [PATCH 2/8] row: take #13344's corrected mechanism receipt; discharge names both inputs Co-Authored-By: Claude Opus 5.5 (1M context) --- ...er_edit_judged_against_the_base_tip_not_the_merge_base.dag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag index 012a8731c4a..b4bce76a581 100644 --- a/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag +++ b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag @@ -11,7 +11,7 @@ data a_roster_edit_judged_against_the_base_tip_not_the_merge_base: RecurringFail "OCCURRENCES, 2026-10-05, two PRs from one parent lane in one hour. gunbc#13333 (clever-raven-680) and gunbc#13341 (sunny-ant-198) each edited v2.workflow.floor_unimported_bare_provider_debt_roster. Main then retired roadmap_launch_deployment_receipt rows. Both floors went red with RosterRetirementChanged (Retired -> ActiveDebt) on rows neither diff touched, and both went green after merging origin/main with no change to the fix. Each lane spent a diagnosis round on a red its diff did not cause.", - "THE MECHANISM, read from the code, not inferred from the symptom. v1_compiler.cli_run required_floor_runner interface_consumer_planning matches floor_diff_comparison_readout and, on the FreezeBaselineComparison MergeBase arm, resolves `git merge-base base head` before reading the base tree; its comment states the floor's window is the relation the affected-set diff was taken under. unimported_bare_provider_gate and the cost-debt edit-budget arm of the claim planner instead call floor_diff_comparison_readout()?.base() and read the roster at that ref through v2.workflow.floor_unimported_bare_provider_debt unimported_bare_provider_roster_at_base (git.Core.Show at the tip). The same readout therefore yields two different base trees in one run, depending on which consumer asks.", + "THE MECHANISM, read from the code, not inferred from the symptom. v1_compiler.cli_run required_floor_runner interface_consumer_planning matches floor_diff_comparison_readout and, on the FreezeBaselineComparison MergeBase arm, resolves `git merge-base base head` before reading the base tree; its comment states the floor's window is the relation the affected-set diff was taken under. Two other readers take floor_diff_comparison_readout()?.base(), the tip, and they read DIFFERENT inputs there. (a) unimported_bare_provider_gate reads the UNIMPORTED-BARE-PROVIDER ROSTER at the tip, through v2.workflow.floor_unimported_bare_provider_debt unimported_bare_provider_roster_at_base (git.Core.Show), and judges the roster edit against it; this is the reader the two occurrences above hit. (b) The cost-debt edit-budget arm of the claim planner passes the tip to cost_debt_changed_witness_ceilings, which runs v2.workflow.floor_cost_debt_edit cost_debt_changed_witness_ceilings_at_base over the changed WITNESS FILE'S SOURCE at that ref, not a roster, to classify the edit and so pick the grandfathered or new-witness budget. Its stale-base consequence is a different one: a witness file main edited after the branch point is classified as if this change made the reverse edit. That is unobserved so far, and reachable by the same mechanism. So one readout yields two different base commits in one run, depending on which consumer asks.", "HARM AND RUNG. As observed, it is a LOUD false red: the line stops, typed and located, on a fact the diff did not author. That is §5-shaped but mis-aimed, and the cost is real: diagnosis rounds, and a standing incentive to merge main reflexively instead of reading the refusal. The SILENT arm is reachable in the same class through an admission reading. A roster whose base-tip membership decides what a change ADMITS (gunbc#13332's cost_debt_admitted_identities, at its head 103a8ea847, reads floor_diff_comparison_readout()?.base() the same way) charges a behind branch with admitting every row main retired since the branch point. That is spurious work at best, and a refusal attributed to the wrong change at worst. Found at rung 1, mitigated only by authors merging main.", @@ -23,7 +23,7 @@ data a_roster_edit_judged_against_the_base_tip_not_the_merge_base: RecurringFail "RECOGNITION RULE AND REVIEW TELL: a refusal naming rows the diff did not touch, which clears after merging main with no change to the diff, is this class until shown otherwise. In review, grep `.base()` on the comparison readout: every caller that reads a tree with it, rather than printing it, is a member.", - "DISCHARGED, 2026-10-05 (crisp-raven-17), by the trigger above and at the grain it names. v1_compiler.cli_run FreezeBaselineComparison resolve_window is now the one place the relation is read for a tree: it returns ResolvedComparisonWindow { base_tree, head }, the base ref under two-dot and the departure point under merge-base, refusing FreezeBaselineUnobservable or FreezeBaselineUnrelatedHistory rather than degrading to a ref. v1_compiler.cli_run required_floor_runner FloorBaseTree resolves that window at most once per floor run and is lent to every base-tree reader: interface_consumer_planning, unimported_bare_provider_gate, the cost-debt edit budget in run_required_floor, floor_base_test_decl_census, floor_base_file_read and through it non_fold_residue_changed_row_subjects. The frozen path-deferral gate collect_frozen_path_deferral_additions_for takes the same resolve_window instead of its own copy of the arm. The bare base() accessor is deleted; base_ref() remains for printing and its doc says it is not a tree, so a reader re-adding a tip read now has to spell a ref it was told not to use. gunbc#13332's cost_debt_admitted_identities read the tip the same way and had not landed when this did; it no longer compiles against base(), so whichever of the two lands second must take FloorBaseTree, loudly rather than silently. EVIDENCE AND ITS RUNG, stated no higher than it executes: required_floor_runner pure_producer_share_tests roster_edit_is_judged_at_the_merge_base_not_the_base_tip builds a real history (branch departs at P, main then moves a retirement the branch never touched), asserts the tip reading refuses RosterRetirementChanged as the executed contrast, asserts the window's base tree is P and nothing refuses, and keeps a positive control where the branch really un-retires the row and still refuses. That test is a Rust unit test, which runs on no CI path while gunbc.rung_drop rust_unit_tests_off_the_merge_path stands, so the regression evidence is mechanically preventable only once that drop is restored; the construction itself (one window, no tree accessor) is what this row's ceiling asked for.", + "DISCHARGED, 2026-10-05 (crisp-raven-17), by the trigger above and at the grain it names. v1_compiler.cli_run FreezeBaselineComparison resolve_window is now the one place the relation is read for a tree: it returns ResolvedComparisonWindow { base_tree, head }, the base ref under two-dot and the departure point under merge-base, refusing FreezeBaselineUnobservable or FreezeBaselineUnrelatedHistory rather than degrading to a ref. v1_compiler.cli_run required_floor_runner FloorBaseTree resolves that window at most once per floor run and is lent to every base-tree reader: interface_consumer_planning, unimported_bare_provider_gate (input (a), the unimported-bare-provider roster, read at the base tree through unimported_bare_provider_roster_at_base), the cost-debt edit budget in run_required_floor (input (b), the changed witness file's source, passed to cost_debt_changed_witness_ceilings at the base tree, so a witness main edited after the branch point is no longer classified as this change's reverse edit), floor_base_test_decl_census, floor_base_file_read and through it non_fold_residue_changed_row_subjects. The frozen path-deferral gate collect_frozen_path_deferral_additions_for takes the same resolve_window instead of its own copy of the arm. The bare base() accessor is deleted; base_ref() remains for printing and its doc says it is not a tree, so a reader re-adding a tip read now has to spell a ref it was told not to use. gunbc#13332's cost_debt_admitted_identities read the tip the same way and had not landed when this did; it no longer compiles against base(), so whichever of the two lands second must take FloorBaseTree, loudly rather than silently. EVIDENCE AND ITS RUNG, stated no higher than it executes: required_floor_runner pure_producer_share_tests roster_edit_is_judged_at_the_merge_base_not_the_base_tip builds a real history (branch departs at P, main then moves a retirement the branch never touched) for input (a); input (b) takes the same FloorBaseTree commit and has no executed control of its own, so its repair is by construction of the shared commit, not by a discriminating red. The test asserts the tip reading refuses RosterRetirementChanged as the executed contrast, asserts the window's base tree is P and nothing refuses, and keeps a positive control where the branch really un-retires the row and still refuses. That test is a Rust unit test, which runs on no CI path while gunbc.rung_drop rust_unit_tests_off_the_merge_path stands, so the regression evidence is mechanically preventable only once that drop is restored; the construction itself (one window, no tree accessor) is what this row's ceiling asked for.", ], evidence: [], } From 05459b682d23a3ec4a48ce1b4d0f6cdc6d83aa56 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 03:58:27 +0000 Subject: [PATCH 3/8] row: discharge cites the structural wall first; states why no .dag control reaches the window Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ster_edit_judged_against_the_base_tip_not_the_merge_base.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag index b4bce76a581..4d0ff81141f 100644 --- a/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag +++ b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag @@ -23,7 +23,7 @@ data a_roster_edit_judged_against_the_base_tip_not_the_merge_base: RecurringFail "RECOGNITION RULE AND REVIEW TELL: a refusal naming rows the diff did not touch, which clears after merging main with no change to the diff, is this class until shown otherwise. In review, grep `.base()` on the comparison readout: every caller that reads a tree with it, rather than printing it, is a member.", - "DISCHARGED, 2026-10-05 (crisp-raven-17), by the trigger above and at the grain it names. v1_compiler.cli_run FreezeBaselineComparison resolve_window is now the one place the relation is read for a tree: it returns ResolvedComparisonWindow { base_tree, head }, the base ref under two-dot and the departure point under merge-base, refusing FreezeBaselineUnobservable or FreezeBaselineUnrelatedHistory rather than degrading to a ref. v1_compiler.cli_run required_floor_runner FloorBaseTree resolves that window at most once per floor run and is lent to every base-tree reader: interface_consumer_planning, unimported_bare_provider_gate (input (a), the unimported-bare-provider roster, read at the base tree through unimported_bare_provider_roster_at_base), the cost-debt edit budget in run_required_floor (input (b), the changed witness file's source, passed to cost_debt_changed_witness_ceilings at the base tree, so a witness main edited after the branch point is no longer classified as this change's reverse edit), floor_base_test_decl_census, floor_base_file_read and through it non_fold_residue_changed_row_subjects. The frozen path-deferral gate collect_frozen_path_deferral_additions_for takes the same resolve_window instead of its own copy of the arm. The bare base() accessor is deleted; base_ref() remains for printing and its doc says it is not a tree, so a reader re-adding a tip read now has to spell a ref it was told not to use. gunbc#13332's cost_debt_admitted_identities read the tip the same way and had not landed when this did; it no longer compiles against base(), so whichever of the two lands second must take FloorBaseTree, loudly rather than silently. EVIDENCE AND ITS RUNG, stated no higher than it executes: required_floor_runner pure_producer_share_tests roster_edit_is_judged_at_the_merge_base_not_the_base_tip builds a real history (branch departs at P, main then moves a retirement the branch never touched) for input (a); input (b) takes the same FloorBaseTree commit and has no executed control of its own, so its repair is by construction of the shared commit, not by a discriminating red. The test asserts the tip reading refuses RosterRetirementChanged as the executed contrast, asserts the window's base tree is P and nothing refuses, and keeps a positive control where the branch really un-retires the row and still refuses. That test is a Rust unit test, which runs on no CI path while gunbc.rung_drop rust_unit_tests_off_the_merge_path stands, so the regression evidence is mechanically preventable only once that drop is restored; the construction itself (one window, no tree accessor) is what this row's ceiling asked for.", + "DISCHARGED, 2026-10-05 (crisp-raven-17, gunbc#13347), by the trigger above and at the grain it names. THE CONSTRUCTION: v1_compiler.cli_run FreezeBaselineComparison resolve_window is the one place the relation is read for a tree. It returns ResolvedComparisonWindow { base_tree, head }: the base ref under two-dot, the departure point under merge-base. When it cannot resolve it refuses FreezeBaselineUnobservable or FreezeBaselineUnrelatedHistory and never degrades to a ref. v1_compiler.cli_run required_floor_runner FloorBaseTree resolves that window at most once per floor run and lends it to every base-tree reader: interface_consumer_planning; unimported_bare_provider_gate, for input (a), the unimported-bare-provider roster, read through unimported_bare_provider_roster_at_base; the cost-debt edit budget in run_required_floor, for input (b), the changed witness file's source passed to cost_debt_changed_witness_ceilings; and two tip readers this row had not named, floor_base_test_decl_census and floor_base_file_read, the second reached through non_fold_residue_changed_row_subjects. collect_frozen_path_deferral_additions_for uses the same resolve_window instead of its own copy of the arm. gunbc#13332's cost_debt_admitted_identities had not landed; it read base() the same way and must take FloorBaseTree. EVIDENCE, IN ORDER OF STRENGTH. (1) THE STRUCTURAL WALL: the bare base() accessor is deleted, and only base_ref() remains, documented for printing, so a reader written to read a tree at the tip through the readout's accessor does not compile; this is what made gunbc#13332 fail loudly rather than silently. Its limit is precise and is why this sits at ceiling 3, not 4: re-adding a tree accessor, or passing base_ref() to git, reopens the hole; nothing stops a consumer naming any ref to git. (2) THE EXECUTED CONTROL: required_floor_runner pure_producer_share_tests roster_edit_is_judged_at_the_merge_base_not_the_base_tip builds a real history in which the branch departs at P and main then moves a retirement the branch never touched. At the tip it observes RosterRetirementChanged (Retired FileDeleted -> Retired ImportsFixed), the executed contrast; at the window's base tree, which it asserts is P, nothing refuses. A positive control, where the branch really un-retires the row, still refuses. It is a Rust unit test, which runs on no CI path while gunbc.rung_drop rust_unit_tests_off_the_merge_path stands. Input (b) shares the commit and has no discriminating control of its own. (3) NO .dag CONTROL IS REACHABLE for the defect itself, said here rather than left implied: the window resolution, meaning which commit is the base side, is host-side in FloorBaseTree and resolve_window, and a .dag claim receives a base roster already read. A claim over v2.workflow.floor_unimported_bare_provider_debt's edit judgment fed the tip roster or the departure roster only re-checks the judgment, which v2.test.claim.floor_unimported_bare_provider_debt_test edit_refuses_a_rewritten_cause_and_a_reverted_retirement already executes on the floor for both strings above. It cannot fail if the host chose the wrong commit, so it would be a decoration. Its next-rung trigger is the window resolution moving into v2.workflow.floor_diff_observe, at which point the merge-base choice becomes a .dag fact a floor claim can discriminate.", ], evidence: [], } From 7628274e106bc248f3b62b029b260d10738edf47 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 03:59:25 +0000 Subject: [PATCH 4/8] row: correct the #13332 note; its reader now carries a per-site arm, to be converted by whichever lands second Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ster_edit_judged_against_the_base_tip_not_the_merge_base.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag index 4d0ff81141f..a9d2d216a83 100644 --- a/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag +++ b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag @@ -23,7 +23,7 @@ data a_roster_edit_judged_against_the_base_tip_not_the_merge_base: RecurringFail "RECOGNITION RULE AND REVIEW TELL: a refusal naming rows the diff did not touch, which clears after merging main with no change to the diff, is this class until shown otherwise. In review, grep `.base()` on the comparison readout: every caller that reads a tree with it, rather than printing it, is a member.", - "DISCHARGED, 2026-10-05 (crisp-raven-17, gunbc#13347), by the trigger above and at the grain it names. THE CONSTRUCTION: v1_compiler.cli_run FreezeBaselineComparison resolve_window is the one place the relation is read for a tree. It returns ResolvedComparisonWindow { base_tree, head }: the base ref under two-dot, the departure point under merge-base. When it cannot resolve it refuses FreezeBaselineUnobservable or FreezeBaselineUnrelatedHistory and never degrades to a ref. v1_compiler.cli_run required_floor_runner FloorBaseTree resolves that window at most once per floor run and lends it to every base-tree reader: interface_consumer_planning; unimported_bare_provider_gate, for input (a), the unimported-bare-provider roster, read through unimported_bare_provider_roster_at_base; the cost-debt edit budget in run_required_floor, for input (b), the changed witness file's source passed to cost_debt_changed_witness_ceilings; and two tip readers this row had not named, floor_base_test_decl_census and floor_base_file_read, the second reached through non_fold_residue_changed_row_subjects. collect_frozen_path_deferral_additions_for uses the same resolve_window instead of its own copy of the arm. gunbc#13332's cost_debt_admitted_identities had not landed; it read base() the same way and must take FloorBaseTree. EVIDENCE, IN ORDER OF STRENGTH. (1) THE STRUCTURAL WALL: the bare base() accessor is deleted, and only base_ref() remains, documented for printing, so a reader written to read a tree at the tip through the readout's accessor does not compile; this is what made gunbc#13332 fail loudly rather than silently. Its limit is precise and is why this sits at ceiling 3, not 4: re-adding a tree accessor, or passing base_ref() to git, reopens the hole; nothing stops a consumer naming any ref to git. (2) THE EXECUTED CONTROL: required_floor_runner pure_producer_share_tests roster_edit_is_judged_at_the_merge_base_not_the_base_tip builds a real history in which the branch departs at P and main then moves a retirement the branch never touched. At the tip it observes RosterRetirementChanged (Retired FileDeleted -> Retired ImportsFixed), the executed contrast; at the window's base tree, which it asserts is P, nothing refuses. A positive control, where the branch really un-retires the row, still refuses. It is a Rust unit test, which runs on no CI path while gunbc.rung_drop rust_unit_tests_off_the_merge_path stands. Input (b) shares the commit and has no discriminating control of its own. (3) NO .dag CONTROL IS REACHABLE for the defect itself, said here rather than left implied: the window resolution, meaning which commit is the base side, is host-side in FloorBaseTree and resolve_window, and a .dag claim receives a base roster already read. A claim over v2.workflow.floor_unimported_bare_provider_debt's edit judgment fed the tip roster or the departure roster only re-checks the judgment, which v2.test.claim.floor_unimported_bare_provider_debt_test edit_refuses_a_rewritten_cause_and_a_reverted_retirement already executes on the floor for both strings above. It cannot fail if the host chose the wrong commit, so it would be a decoration. Its next-rung trigger is the window resolution moving into v2.workflow.floor_diff_observe, at which point the merge-base choice becomes a .dag fact a floor claim can discriminate.", + "DISCHARGED, 2026-10-05 (crisp-raven-17, gunbc#13347), by the trigger above and at the grain it names. THE CONSTRUCTION: v1_compiler.cli_run FreezeBaselineComparison resolve_window is the one place the relation is read for a tree. It returns ResolvedComparisonWindow { base_tree, head }: the base ref under two-dot, the departure point under merge-base. When it cannot resolve it refuses FreezeBaselineUnobservable or FreezeBaselineUnrelatedHistory and never degrades to a ref. v1_compiler.cli_run required_floor_runner FloorBaseTree resolves that window at most once per floor run and lends it to every base-tree reader: interface_consumer_planning; unimported_bare_provider_gate, for input (a), the unimported-bare-provider roster, read through unimported_bare_provider_roster_at_base; the cost-debt edit budget in run_required_floor, for input (b), the changed witness file's source passed to cost_debt_changed_witness_ceilings; and two tip readers this row had not named, floor_base_test_decl_census and floor_base_file_read, the second reached through non_fold_residue_changed_row_subjects. collect_frozen_path_deferral_additions_for uses the same resolve_window instead of its own copy of the arm. gunbc#13332's cost_debt_admitted_identities had not landed. At its head 8e0aa21f9e it no longer reads base(); it carries its own per-site copy of the arm, cost_debt_comparison_base_commit, which is exactly what this row's trigger says does not discharge it. Agreed with its author (crisp-ram-667): whichever PR lands second replaces that helper with FloorBaseTree commit() and keeps its RED cost_debt_row_retired_at_the_tip_but_carried_at_the_merge_base_is_not_admitted pointed at the shared commit. Until that conversion lands, this row's discharge does not cover that reader. EVIDENCE, IN ORDER OF STRENGTH. (1) THE STRUCTURAL WALL: the bare base() accessor is deleted, and only base_ref() remains, documented for printing, so a reader written to read a tree at the tip through the readout's accessor does not compile; it does not catch a reader that runs its own merge-base, which is how gunbc#13332's helper got past it. Its limit is precise and is why this sits at ceiling 3, not 4: re-adding a tree accessor, or passing base_ref() to git, reopens the hole; nothing stops a consumer naming any ref to git. (2) THE EXECUTED CONTROL: required_floor_runner pure_producer_share_tests roster_edit_is_judged_at_the_merge_base_not_the_base_tip builds a real history in which the branch departs at P and main then moves a retirement the branch never touched. At the tip it observes RosterRetirementChanged (Retired FileDeleted -> Retired ImportsFixed), the executed contrast; at the window's base tree, which it asserts is P, nothing refuses. A positive control, where the branch really un-retires the row, still refuses. It is a Rust unit test, which runs on no CI path while gunbc.rung_drop rust_unit_tests_off_the_merge_path stands. Input (b) shares the commit and has no discriminating control of its own. (3) NO .dag CONTROL IS REACHABLE for the defect itself, said here rather than left implied: the window resolution, meaning which commit is the base side, is host-side in FloorBaseTree and resolve_window, and a .dag claim receives a base roster already read. A claim over v2.workflow.floor_unimported_bare_provider_debt's edit judgment fed the tip roster or the departure roster only re-checks the judgment, which v2.test.claim.floor_unimported_bare_provider_debt_test edit_refuses_a_rewritten_cause_and_a_reverted_retirement already executes on the floor for both strings above. It cannot fail if the host chose the wrong commit, so it would be a decoration. Its next-rung trigger is the window resolution moving into v2.workflow.floor_diff_observe, at which point the merge-base choice becomes a .dag fact a floor claim can discriminate.", ], evidence: [], } From 63a76fe170ff08e0415701a041c08dd1854b2844 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 05:16:33 +0000 Subject: [PATCH 5/8] floor: base-side readers take a BaseTreeCommit only the resolver can build; real-history control drives FloorBaseTree and the gate's judgment; row names three inputs Co-Authored-By: Claude Opus 5.5 (1M context) --- ...gainst_the_base_tip_not_the_merge_base.dag | 4 +- src/v1/stage0/src/cli_run.rs | 143 +++++++++----- .../src/cli_run/required_floor_runner.rs | 177 ++++++++++++------ 3 files changed, 216 insertions(+), 108 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag index a9d2d216a83..08c256755d9 100644 --- a/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag +++ b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag @@ -11,7 +11,7 @@ data a_roster_edit_judged_against_the_base_tip_not_the_merge_base: RecurringFail "OCCURRENCES, 2026-10-05, two PRs from one parent lane in one hour. gunbc#13333 (clever-raven-680) and gunbc#13341 (sunny-ant-198) each edited v2.workflow.floor_unimported_bare_provider_debt_roster. Main then retired roadmap_launch_deployment_receipt rows. Both floors went red with RosterRetirementChanged (Retired -> ActiveDebt) on rows neither diff touched, and both went green after merging origin/main with no change to the fix. Each lane spent a diagnosis round on a red its diff did not cause.", - "THE MECHANISM, read from the code, not inferred from the symptom. v1_compiler.cli_run required_floor_runner interface_consumer_planning matches floor_diff_comparison_readout and, on the FreezeBaselineComparison MergeBase arm, resolves `git merge-base base head` before reading the base tree; its comment states the floor's window is the relation the affected-set diff was taken under. Two other readers take floor_diff_comparison_readout()?.base(), the tip, and they read DIFFERENT inputs there. (a) unimported_bare_provider_gate reads the UNIMPORTED-BARE-PROVIDER ROSTER at the tip, through v2.workflow.floor_unimported_bare_provider_debt unimported_bare_provider_roster_at_base (git.Core.Show), and judges the roster edit against it; this is the reader the two occurrences above hit. (b) The cost-debt edit-budget arm of the claim planner passes the tip to cost_debt_changed_witness_ceilings, which runs v2.workflow.floor_cost_debt_edit cost_debt_changed_witness_ceilings_at_base over the changed WITNESS FILE'S SOURCE at that ref, not a roster, to classify the edit and so pick the grandfathered or new-witness budget. Its stale-base consequence is a different one: a witness file main edited after the branch point is classified as if this change made the reverse edit. That is unobserved so far, and reachable by the same mechanism. So one readout yields two different base commits in one run, depending on which consumer asks.", + "THE MECHANISM, read from the code, not inferred from the symptom. v1_compiler.cli_run required_floor_runner interface_consumer_planning matches floor_diff_comparison_readout and, on the FreezeBaselineComparison MergeBase arm, resolves `git merge-base base head` before reading the base tree; its comment states the floor's window is the relation the affected-set diff was taken under. Two other readers take floor_diff_comparison_readout()?.base(), the tip, and they read DIFFERENT inputs there. (a) unimported_bare_provider_gate reads the UNIMPORTED-BARE-PROVIDER ROSTER at the tip, through v2.workflow.floor_unimported_bare_provider_debt unimported_bare_provider_roster_at_base (git.Core.Show), and judges the roster edit against it; this is the reader the two occurrences above hit. (b) The cost-debt edit-budget arm of the claim planner passes the tip to cost_debt_changed_witness_ceilings, which runs v2.workflow.floor_cost_debt_edit cost_debt_changed_witness_ceilings_at_base over the changed WITNESS FILE'S SOURCE at that ref, not a roster, to classify the edit and so pick the grandfathered or new-witness budget. Its stale-base consequence is a different one: a witness file main edited after the branch point is classified as if this change made the reverse edit. That is unobserved so far, and reachable by the same mechanism. (c) A third reader, of a third input: gunbc#13332's cost_debt_admitted_identities reads COST-ROSTER MEMBERSHIP at the base side to decide what a change admits; at its head 103a8ea847 it read the tip through base() as well (see HARM AND RUNG). So one readout yields two different base commits in one run, depending on which consumer asks.", "HARM AND RUNG. As observed, it is a LOUD false red: the line stops, typed and located, on a fact the diff did not author. That is §5-shaped but mis-aimed, and the cost is real: diagnosis rounds, and a standing incentive to merge main reflexively instead of reading the refusal. The SILENT arm is reachable in the same class through an admission reading. A roster whose base-tip membership decides what a change ADMITS (gunbc#13332's cost_debt_admitted_identities, at its head 103a8ea847, reads floor_diff_comparison_readout()?.base() the same way) charges a behind branch with admitting every row main retired since the branch point. That is spurious work at best, and a refusal attributed to the wrong change at worst. Found at rung 1, mitigated only by authors merging main.", @@ -23,7 +23,7 @@ data a_roster_edit_judged_against_the_base_tip_not_the_merge_base: RecurringFail "RECOGNITION RULE AND REVIEW TELL: a refusal naming rows the diff did not touch, which clears after merging main with no change to the diff, is this class until shown otherwise. In review, grep `.base()` on the comparison readout: every caller that reads a tree with it, rather than printing it, is a member.", - "DISCHARGED, 2026-10-05 (crisp-raven-17, gunbc#13347), by the trigger above and at the grain it names. THE CONSTRUCTION: v1_compiler.cli_run FreezeBaselineComparison resolve_window is the one place the relation is read for a tree. It returns ResolvedComparisonWindow { base_tree, head }: the base ref under two-dot, the departure point under merge-base. When it cannot resolve it refuses FreezeBaselineUnobservable or FreezeBaselineUnrelatedHistory and never degrades to a ref. v1_compiler.cli_run required_floor_runner FloorBaseTree resolves that window at most once per floor run and lends it to every base-tree reader: interface_consumer_planning; unimported_bare_provider_gate, for input (a), the unimported-bare-provider roster, read through unimported_bare_provider_roster_at_base; the cost-debt edit budget in run_required_floor, for input (b), the changed witness file's source passed to cost_debt_changed_witness_ceilings; and two tip readers this row had not named, floor_base_test_decl_census and floor_base_file_read, the second reached through non_fold_residue_changed_row_subjects. collect_frozen_path_deferral_additions_for uses the same resolve_window instead of its own copy of the arm. gunbc#13332's cost_debt_admitted_identities had not landed. At its head 8e0aa21f9e it no longer reads base(); it carries its own per-site copy of the arm, cost_debt_comparison_base_commit, which is exactly what this row's trigger says does not discharge it. Agreed with its author (crisp-ram-667): whichever PR lands second replaces that helper with FloorBaseTree commit() and keeps its RED cost_debt_row_retired_at_the_tip_but_carried_at_the_merge_base_is_not_admitted pointed at the shared commit. Until that conversion lands, this row's discharge does not cover that reader. EVIDENCE, IN ORDER OF STRENGTH. (1) THE STRUCTURAL WALL: the bare base() accessor is deleted, and only base_ref() remains, documented for printing, so a reader written to read a tree at the tip through the readout's accessor does not compile; it does not catch a reader that runs its own merge-base, which is how gunbc#13332's helper got past it. Its limit is precise and is why this sits at ceiling 3, not 4: re-adding a tree accessor, or passing base_ref() to git, reopens the hole; nothing stops a consumer naming any ref to git. (2) THE EXECUTED CONTROL: required_floor_runner pure_producer_share_tests roster_edit_is_judged_at_the_merge_base_not_the_base_tip builds a real history in which the branch departs at P and main then moves a retirement the branch never touched. At the tip it observes RosterRetirementChanged (Retired FileDeleted -> Retired ImportsFixed), the executed contrast; at the window's base tree, which it asserts is P, nothing refuses. A positive control, where the branch really un-retires the row, still refuses. It is a Rust unit test, which runs on no CI path while gunbc.rung_drop rust_unit_tests_off_the_merge_path stands. Input (b) shares the commit and has no discriminating control of its own. (3) NO .dag CONTROL IS REACHABLE for the defect itself, said here rather than left implied: the window resolution, meaning which commit is the base side, is host-side in FloorBaseTree and resolve_window, and a .dag claim receives a base roster already read. A claim over v2.workflow.floor_unimported_bare_provider_debt's edit judgment fed the tip roster or the departure roster only re-checks the judgment, which v2.test.claim.floor_unimported_bare_provider_debt_test edit_refuses_a_rewritten_cause_and_a_reverted_retirement already executes on the floor for both strings above. It cannot fail if the host chose the wrong commit, so it would be a decoration. Its next-rung trigger is the window resolution moving into v2.workflow.floor_diff_observe, at which point the merge-base choice becomes a .dag fact a floor claim can discriminate.", + "DISCHARGED, 2026-10-05 (crisp-raven-17, gunbc#13347), by the trigger above, for inputs (a) and (b); (c) is not yet covered, see below. THE CONSTRUCTION. v1_compiler.cli_run comparison_window resolve is the one place the relation is read for a tree: the base ref under two-dot, the departure point under merge-base. When it cannot resolve it refuses FreezeBaselineUnobservable or FreezeBaselineUnrelatedHistory and never degrades to a ref. It returns ResolvedComparisonWindow, whose base side is a BaseTreeCommit. That is a newtype whose field is private to that module, so resolve is its only constructor; ResolvedComparisonWindow's fields are private too. v1_compiler.cli_run required_floor_runner FloorBaseTree resolves the window at most once per floor run and lends it to every base-tree reader: interface_consumer_planning; unimported_bare_provider_gate, for input (a), the roster, through unimported_bare_provider_edit_refusals and unimported_bare_provider_roster_source_at_base; the cost-debt edit budget in run_required_floor, for input (b), the changed witness file's source, through cost_debt_changed_witness_ceilings; and two tip readers this row had not named, floor_base_test_decl_census and floor_base_file_read, the second reached through non_fold_residue_changed_row_subjects. collect_frozen_path_deferral_additions_for uses the same resolver. FreezeBaselineComparison base() is deleted, and base_ref() remains for printing. EVIDENCE, IN ORDER OF STRENGTH. (1) THE READER INTERFACE, rung 3, structurally guaranteed at that interface. unimported_bare_provider_roster_source_at_base, unimported_bare_provider_edit_refusals and cost_debt_changed_witness_ceilings take a BaseTreeCommit, not a string, so the substitution a review raised does not compile: passing the readout's base_ref() as a string into the roster read. floor_base_file_read and floor_base_test_decl_census take FloorBaseTree. The limit is precise, and it is why this is ceiling 3, not 4: a consumer can still hand any ref to git outside these readers, or add a new reader that takes a string. namespace_baseline reconstruct_base_index still takes a string, because it is a general utility with other callers; the floor's only call passes FloorBaseTree's commit. That substitution was executed once as a one-off probe on this branch, and rustc refused it with E0308 (expected `&BaseTreeCommit`, found `&String`). The probe is not enrolled anywhere, because the crate has no compile-fail harness, so after this change the wall rests on the signatures. (2) THE EXECUTED CONTROL, through the production selection and judgment. required_floor_runner pure_producer_share_tests roster_edit_is_judged_at_the_merge_base_not_the_base_tip builds a real history: the branch departs at P, then main moves a retirement the branch never touched. It drives FloorBaseTree for_comparison and unimported_bare_provider_edit_refusals, the gate's own judgment step. A tip base tree (two-dot against main) refuses RosterRetirementChanged (Retired FileDeleted -> Retired ImportsFixed), the executed contrast. The merge-base tree resolves to P, and nothing refuses. A positive control, where the branch really un-retires the row, still refuses. The fixture supplies one seam, the git show inside that repository, because the .dag read runs in the process workspace; that .dag read is executed by the end-to-end test beside it, which now also takes a resolved BaseTreeCommit. Both are Rust unit tests, which run on no CI path while gunbc.rung_drop rust_unit_tests_off_the_merge_path stands, so CI evidence is still owed until that drop is restored. Input (b) shares the commit and type, but has no discriminating control of its own. (3) NO .dag CONTROL IS REACHABLE for the defect itself. Which commit is the base side is decided on the host, and a .dag claim receives a roster already read. A claim over v2.workflow.floor_unimported_bare_provider_debt's edit judgment would only re-check the judgment, which v2.test.claim.floor_unimported_bare_provider_debt_test edit_refuses_a_rewritten_cause_and_a_reverted_retirement already executes on the floor. Its next-rung trigger is the window resolution moving into v2.workflow.floor_diff_observe. INPUT (c), NOT COVERED. gunbc#13332 had not landed. At its head 8e0aa21f9e it carries its own per-site arm, cost_debt_comparison_base_commit, which this row's trigger says does not discharge it. Agreed with its author (crisp-ram-667): whichever PR lands second replaces that helper with FloorBaseTree commit(), and points its RED cost_debt_row_retired_at_the_tip_but_carried_at_the_merge_base_is_not_admitted at the shared commit. Until that conversion lands, this row's discharge does not cover input (c).", ], evidence: [], } diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 0d145687665..f94f618e84d 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -25005,58 +25005,53 @@ pub fn collect_frozen_path_deferral_additions() -> Result, String> { collect_frozen_path_deferral_additions_for(&workspace_root(), &comparison) } -/// THE COMPARISON WINDOW, as the seed sees it. Mirrors `FloorDiffComparisonReadout` arm for arm; -/// the mode is a variant rather than a flag because it selects which commit the base side is read -/// at, and a bool would let a caller forget to ask. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum FreezeBaselineComparison { - /// Two-dot: the endpoints are compared directly. `PushBefore` and `ExactReplayBoundary` are - /// this, and for them the base is EXACT — imposing a merge base here is what passes real - /// growth on a rewritten push. - Direct { - base: String, - head: String, - kind: String, - }, - /// Three-dot: the comparison is against the point the head departed the base from, so a base - /// that moves after the subject is fixed does not change the verdict. - MergeBase { - base: String, +/// THE BASE-TREE COMMIT AS A TYPE ONLY THE RESOLVER CAN BUILD +/// (`gunbc.recurring_failure_mode` `a_roster_edit_judged_against_the_base_tip_not_the_merge_base`). +/// A base-side reader takes `&BaseTreeCommit`, never a `&str`, and the field is private to this +/// module, so the only constructor is `resolve`, which applies the comparison's relation: the base +/// ref under two-dot, the departure point under merge-base. The tip string `base_ref()` returns does +/// not type-check at a reader. The limit is explicit: a consumer may still hand any ref to git +/// directly, outside these readers, which is why this is a ceiling-3 guarantee and not ceiling 4. +pub mod comparison_window { + use super::FreezeBaselineComparison; + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct BaseTreeCommit(String); + + impl BaseTreeCommit { + pub fn as_str(&self) -> &str { + &self.0 + } + } + + impl std::fmt::Display for BaseTreeCommit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ResolvedComparisonWindow { + base_tree: BaseTreeCommit, head: String, - kind: String, - }, -} + } -/// THE TWO COMMITS A BASE-TREE READER MAY TAKE, resolved from a `FreezeBaselineComparison` by -/// `resolve_window` and by nothing else. `base_tree` is the commit whose TREE is the base side of -/// the comparison: the base ref itself under two-dot, the departure point under merge-base. Every -/// reader of a base-side file takes this commit, so no reader can choose a relation for itself -- -/// the defect `gunbc.recurring_failure_mode` `a_roster_edit_judged_against_the_base_tip_not_the_merge_base` -/// records, where two readers took the base TIP while a third applied the merge-base arm and a -/// roster row main retired after the branch point read as this change's edit. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ResolvedComparisonWindow { - pub base_tree: String, - pub head: String, -} + impl ResolvedComparisonWindow { + pub fn base_tree(&self) -> &BaseTreeCommit { + &self.base_tree + } -impl FreezeBaselineComparison { - /// The base REF as the authority named it. For printing and locating only: it is not a tree. - /// A reader that needs base-side content takes `resolve_window`'s `base_tree`, because under - /// merge-base mode this ref's tree carries every change the base made after the head departed. - pub(crate) fn base_ref(&self) -> &str { - match self { - Self::Direct { base, .. } | Self::MergeBase { base, .. } => base, + pub fn head(&self) -> &str { + &self.head } } - /// THE BASE COMMIT, with the relation applied -- the one place the arm is read for a tree. /// Two-dot compares the exact base; merge-base compares the commit the head departed from. /// The match may never grow a default: imposing merge-base on a two-dot arm passes growth on a /// rewritten push, and reading the tip on a merge-base arm attributes the base's own later /// edits to the change. Each refusal carries a typed cause and never degrades to a ref. - pub(crate) fn resolve_window( - &self, + pub(crate) fn resolve( + comparison: &FreezeBaselineComparison, root: &std::path::Path, ) -> Result { let run = |args: &[&str]| -> Result { @@ -25084,12 +25079,12 @@ impl FreezeBaselineComparison { } Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) }; - let base = self.base_ref(); + let base = comparison.base_ref(); let base_commit = resolve(base, "base")?; - let head = resolve(self.head(), "head")?; - let base_tree = match self { - Self::Direct { .. } => base_commit, - Self::MergeBase { .. } => { + let head = resolve(comparison.head(), "head")?; + let base_tree = match comparison { + FreezeBaselineComparison::Direct { .. } => base_commit, + FreezeBaselineComparison::MergeBase { .. } => { let merge_base = run(&["merge-base", &base_commit, &head])?; let fork_point = String::from_utf8_lossy(&merge_base.stdout) .trim() @@ -25107,7 +25102,52 @@ impl FreezeBaselineComparison { fork_point } }; - Ok(ResolvedComparisonWindow { base_tree, head }) + Ok(ResolvedComparisonWindow { + base_tree: BaseTreeCommit(base_tree), + head, + }) + } +} + +/// THE COMPARISON WINDOW, as the seed sees it. Mirrors `FloorDiffComparisonReadout` arm for arm; +/// the mode is a variant rather than a flag because it selects which commit the base side is read +/// at, and a bool would let a caller forget to ask. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FreezeBaselineComparison { + /// Two-dot: the endpoints are compared directly. `PushBefore` and `ExactReplayBoundary` are + /// this, and for them the base is EXACT — imposing a merge base here is what passes real + /// growth on a rewritten push. + Direct { + base: String, + head: String, + kind: String, + }, + /// Three-dot: the comparison is against the point the head departed the base from, so a base + /// that moves after the subject is fixed does not change the verdict. + MergeBase { + base: String, + head: String, + kind: String, + }, +} + +impl FreezeBaselineComparison { + /// The base REF as the authority named it. For printing and locating only: it is not a tree. + /// A reader that needs base-side content takes `resolve_window`'s `base_tree`, because under + /// merge-base mode this ref's tree carries every change the base made after the head departed. + pub(crate) fn base_ref(&self) -> &str { + match self { + Self::Direct { base, .. } | Self::MergeBase { base, .. } => base, + } + } + + /// THE BASE COMMIT, with the relation applied: the one place the arm is read for a tree + /// (`comparison_window::resolve`). + pub(crate) fn resolve_window( + &self, + root: &std::path::Path, + ) -> Result { + comparison_window::resolve(self, root) } fn head(&self) -> &str { @@ -25152,12 +25192,11 @@ pub fn collect_frozen_path_deferral_additions_for( let base = comparison.base_ref(); // THE BASELINE COMMIT is the window's base tree: the relation is applied once, by the // comparison itself, for this gate and every floor base-tree reader alike. - let ResolvedComparisonWindow { - base_tree: baseline_commit, - head: head_commit, - } = comparison + let window = comparison .resolve_window(root) .map_err(|cause| located(format!("WITNESS ADMISSION REFUSAL {cause} (frozen path-deferral roster: a monotone debt contract, so growth cannot be ruled out)")))?; + let baseline_commit = window.base_tree().as_str().to_string(); + let head_commit = window.head().to_string(); // THE CURRENT SIDE IS THE SELECTED HEAD, not an ambient one, and this runs BEFORE any arm that // can permit. Reading the live filesystem keeps an uncommitted local roster edit in scope (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 3045553403c..89e7b928285 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -723,22 +723,52 @@ pub(crate) fn floor_diff_comparison_readout() -> Result>, + /// `None` reads the run's own comparison (`floor_diff_comparison_readout`) at the process + /// workspace; `Some` is a supplied comparison over a supplied repository, the same resolver. + supplied: Option<(FreezeBaselineComparison, PathBuf)>, + window: std::cell::OnceCell< + Result< + ( + FreezeBaselineComparison, + comparison_window::ResolvedComparisonWindow, + ), + String, + >, + >, } impl FloorBaseTree { pub(crate) fn unresolved() -> Self { Self { + supplied: None, + window: std::cell::OnceCell::new(), + } + } + + /// A supplied comparison over a supplied repository, resolved by the same `resolve_window`. + pub(crate) fn for_comparison(comparison: FreezeBaselineComparison, root: PathBuf) -> Self { + Self { + supplied: Some((comparison, root)), window: std::cell::OnceCell::new(), } } - fn window(&self) -> Result<&(FreezeBaselineComparison, ResolvedComparisonWindow), String> { + fn window( + &self, + ) -> Result< + &( + FreezeBaselineComparison, + comparison_window::ResolvedComparisonWindow, + ), + String, + > { self.window .get_or_init(|| { - let comparison = floor_diff_comparison_readout()?; - let window = comparison.resolve_window(&process_workspace_root())?; + let (comparison, root) = match &self.supplied { + Some((comparison, root)) => (comparison.clone(), root.clone()), + None => (floor_diff_comparison_readout()?, process_workspace_root()), + }; + let window = comparison.resolve_window(&root)?; Ok((comparison, window)) }) .as_ref() @@ -746,13 +776,13 @@ impl FloorBaseTree { } /// The commit whose tree is the base side of this run's comparison. - pub(crate) fn commit(&self) -> Result<&str, String> { - Ok(self.window()?.1.base_tree.as_str()) + pub(crate) fn commit(&self) -> Result<&comparison_window::BaseTreeCommit, String> { + Ok(self.window()?.1.base_tree()) } /// The resolved head commit of this run's comparison. pub(crate) fn head(&self) -> Result<&str, String> { - Ok(self.window()?.1.head.as_str()) + Ok(self.window()?.1.head()) } /// The base REF and the base-tree commit, for a located line. Never a tree route. @@ -761,7 +791,7 @@ impl FloorBaseTree { Ok(format!( "base_ref={} base_tree={}", comparison.base_ref(), - window.base_tree + window.base_tree() )) } } @@ -861,7 +891,7 @@ pub(crate) fn floor_git_diff_name_status_range() -> Result<(Vec, HashSet /// resolves a removed call against. Returns an exact identity map of the model's edit labels and budgets. /// Tokenization is shared only within this observed file pair, never across revisions. pub(crate) fn cost_debt_changed_witness_ceilings( - base: &str, + base_tree: &comparison_window::BaseTreeCommit, rel_path: &str, functions: &[String], head_source: &str, @@ -874,7 +904,7 @@ pub(crate) fn cost_debt_changed_witness_ceilings( .map_err(|e| format!("floor_cost_debt_edit resolve: {e}"))?; let ctx = make_eval_context(&graph, indices, v1_interpreter::ExecutionMode::Wet); let args = [ - (Some("base".to_string()), str_value(base)), + (Some("base".to_string()), str_value(base_tree.as_str())), (Some("path".to_string()), str_value(rel_path)), ( Some("functions".to_string()), @@ -952,7 +982,7 @@ pub(crate) fn floor_base_file_read( path: &str, ) -> Result, String> { use v1_interpreter::Value; - let base = base_tree.commit()?; + let base = base_tree.commit()?.as_str(); let roots = default_source_roots(); let entry = "src/v2/workflow/floor_diff_observe.dag"; let (graph, indices) = resolve_entry_graph_shared(&roots, entry) @@ -1005,7 +1035,7 @@ pub(crate) fn floor_base_test_decl_census( if paths.is_empty() { return Ok(std::collections::HashMap::new()); } - let base = base_tree.commit()?; + let base = base_tree.commit()?.as_str(); let roots = default_source_roots(); let entry = "src/v2/workflow/floor_diff_observe.dag"; let (graph, indices) = resolve_entry_graph_shared(&roots, entry) @@ -1782,7 +1812,7 @@ fn interface_consumer_planning( // direct comparison at the base ref itself -- the same relation the affected-set diff was // taken under, so the interface delta and the line-range attribution describe one change. let workspace = process_workspace_root(); - let base_commit = base_tree.commit()?.to_string(); + let base_commit = base_tree.commit()?.as_str().to_string(); let head_commit = base_tree.head()?.to_string(); match reconstruct_base_index(&workspace, &base_commit, &head_commit, head_index)? { BaselineReconstruction::NoSubject { head } => { @@ -2652,18 +2682,10 @@ pub(crate) fn unimported_bare_provider_gate( head.rows.len() ); } else { - let base = base_tree.commit()?.to_string(); let located = base_tree.located()?; - let base_source = unimported_bare_provider_roster_source_at_base(source_roots, &base)?; - let base_reading = unimported_bare_provider_base_reading(&base_source)?; - let base_coherence = head.refusals( - "unimported_bare_provider_roster_coherence", - &[(Some("rows".to_string()), base_reading.rows_value.clone())], - )?; - for r in base_coherence - .into_iter() - .chain(head.judge_edit(&base_reading, &head)?) - { + for r in unimported_bare_provider_edit_refusals(base_tree, &head, &head, |base| { + unimported_bare_provider_roster_source_at_base(source_roots, base) + })? { refusals.push(format!( "{ROUTE} cause=UnimportedBareProvider {r} {located}" )); @@ -2718,10 +2740,31 @@ pub fn unimported_bare_provider_entry_refusals( /// The roster's bytes at the diff base, read through the `.dag` (`unimported_bare_provider_roster_at_base`), /// whose unreadable arm refuses here rather than reading as an empty roster. +/// THE ROSTER EDIT JUDGMENT AT THE RUN'S BASE TREE: the base side is read at `base_tree.commit()` +/// and nowhere else, then evaluated for coherence and judged against the head. The reader is the +/// one seam: the gate supplies the `.dag` read (`unimported_bare_provider_roster_source_at_base`), +/// and it can only be handed a `BaseTreeCommit`, so a tip ref does not type-check here. +fn unimported_bare_provider_edit_refusals( + base_tree: &FloorBaseTree, + judge: &UnimportedBareProviderRosterReading, + head: &UnimportedBareProviderRosterReading, + read_base: impl FnOnce(&comparison_window::BaseTreeCommit) -> Result, +) -> Result, String> { + let base_source = read_base(base_tree.commit()?)?; + let base_reading = unimported_bare_provider_base_reading(&base_source)?; + let mut refusals = judge.refusals( + "unimported_bare_provider_roster_coherence", + &[(Some("rows".to_string()), base_reading.rows_value.clone())], + )?; + refusals.extend(judge.judge_edit(&base_reading, head)?); + Ok(refusals) +} + fn unimported_bare_provider_roster_source_at_base( source_roots: &[String], - base: &str, + base_tree: &comparison_window::BaseTreeCommit, ) -> Result { + let base = base_tree.as_str(); let (graph, indices) = resolve_entry_graph_shared( source_roots, &unimported_bare_provider_authority(UNIMPORTED_BARE_PROVIDER_VERDICT), @@ -8669,7 +8712,7 @@ pub fn run_required_floor( } else if grandfathered_roster.contains(&identity) { grandfathered_eval_step_budget } else if cost_debt_roster.contains(&identity) { - let base = floor_base_tree.commit()?.to_string(); + let base = floor_base_tree.commit()?; let rel_path = normalize_repo_path(&workspace_relative_repo_path(&file.path)); if file_cost_debt_ceilings.is_none() { let head_source = std::fs::read_to_string(&file.path).map_err(|e| { @@ -8696,7 +8739,7 @@ pub fn run_required_floor( functions.len() ); file_cost_debt_ceilings = Some(cost_debt_changed_witness_ceilings( - &base, &rel_path, &functions, &head_source, + base, &rel_path, &functions, &head_source, &discovered_test_fn_identities, ).map_err(|e| format!( "REQUIRED-FLOOR REFUSAL cause=CostDebtEditUnobserved identity={identity} — {e}" @@ -12152,50 +12195,65 @@ mod pure_producer_share_tests { ); git(&["checkout", "--quiet", "subject"]); - let read_at = |rev: &str| { + // THE PRODUCTION SELECTION AND JUDGMENT: `FloorBaseTree` resolves the window and + // `unimported_bare_provider_edit_refusals` reads at its commit. The read seam is the + // fixture's `git show` in this repository, since the `.dag` read runs in the process + // workspace; that read itself is the end-to-end test below. + let reader = |base: &comparison_window::BaseTreeCommit| -> Result { + Ok(git(&["show", &format!("{base}:{UNIMPORTED_BARE_PROVIDER_ROSTER}")]) + "\n") + }; + let head_at = |rev: &str| { let src = git(&["show", &format!("{rev}:{UNIMPORTED_BARE_PROVIDER_ROSTER}")]); unimported_bare_provider_base_reading(&(src + "\n")).expect("lone roster") }; - let window = |head: &str| { - FreezeBaselineComparison::MergeBase { - base: "main".to_string(), - head: head.to_string(), - kind: "MergeTargetBaseline".to_string(), - } - .resolve_window(&dir) - .expect("merge-base window") + let tree = |merge_base: bool, head: &str| { + let (base, head, kind) = ( + "main".to_string(), + head.to_string(), + "MergeTargetBaseline".to_string(), + ); + FloorBaseTree::for_comparison( + if merge_base { + FreezeBaselineComparison::MergeBase { base, head, kind } + } else { + FreezeBaselineComparison::Direct { base, head, kind } + }, + dir.clone(), + ) + }; + let judged = |tree: &FloorBaseTree, head: &str| { + unimported_bare_provider_edit_refusals(tree, &judge, &head_at(head), reader) + .expect("verdict") }; - // RED (before the window): the tip reads main's later edit as this change's. - let head_reading = read_at(&untouched_head); + // THE CONTRAST: a base tree at the tip (what the gate read before) attributes main's + // own later retirement edit to the branch. + let tip_tree = tree(false, &untouched_head); + assert_eq!(tip_tree.commit().expect("tip").as_str(), tip); assert_eq!( - judge.judge_edit(&read_at(&tip), &head_reading).expect("verdict"), + judged(&tip_tree, &untouched_head), vec![ "RosterRetirementChanged dag/a.dag#f (Retired FileDeleted -> Retired ImportsFixed)" .to_string() ], "the base tip attributes main's own later retirement edit to the branch" ); - // THE WALL: the window's base tree is the departure point, and nothing refuses. - let resolved = window(&untouched_head); - assert_eq!(resolved.base_tree, departure); - assert_eq!(resolved.head, untouched_head); + // THE WALL: under the merge-base relation the base tree is the departure point. + let floor_tree = tree(true, &untouched_head); + assert_eq!(floor_tree.commit().expect("window").as_str(), departure); + assert_eq!(floor_tree.head().expect("head"), untouched_head); assert_eq!( - judge - .judge_edit(&read_at(&resolved.base_tree), &head_reading) - .expect("verdict"), + judged(&floor_tree, &untouched_head), Vec::::new(), "a row main retired after the branch point is not this change's edit" ); // POSITIVE CONTROL: the branch really rewrites the retirement, and still refuses. let edited_head = commit("ActiveDebt", "H2: the branch un-retires the row"); - let resolved = window(&edited_head); - assert_eq!(resolved.base_tree, departure); + let floor_tree = tree(true, &edited_head); + assert_eq!(floor_tree.commit().expect("window").as_str(), departure); assert_eq!( - judge - .judge_edit(&read_at(&resolved.base_tree), &read_at(&edited_head)) - .expect("verdict"), + judged(&floor_tree, &edited_head), vec![ "RosterRetirementChanged dag/a.dag#f (Retired ImportsFixed -> ActiveDebt)" .to_string() @@ -12219,8 +12277,19 @@ mod pure_producer_share_tests { .map(|r| root.join(r).to_string_lossy().to_string()) .collect(); let judge = UnimportedBareProviderRosterReading::head(&roots).expect("head roster"); - let base_source = unimported_bare_provider_roster_source_at_base(&roots, "HEAD") - .expect("the roster exists at HEAD and decodes as BaseRosterShown"); + let head_tree = FloorBaseTree::for_comparison( + FreezeBaselineComparison::Direct { + base: "HEAD".to_string(), + head: "HEAD".to_string(), + kind: "OperatorOverrideBaseline".to_string(), + }, + root.clone(), + ); + let base_source = unimported_bare_provider_roster_source_at_base( + &roots, + head_tree.commit().expect("HEAD resolves"), + ) + .expect("the roster exists at HEAD and decodes as BaseRosterShown"); let read = |src: &str| unimported_bare_provider_base_reading(src).expect("lone roster"); let base = read(&base_source); let active = "standing: ActiveDebt }"; From 401bd7249aed8667a5a40466b65359b493260ced Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 11:33:09 +0000 Subject: [PATCH 6/8] Fast stale-base RED for cost-debt admission through FloorBaseTree (supplied values, within the unit budget); merge of main incl #13452 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/cli_run/required_floor_runner.rs | 114 ++++++++++++++++-- 1 file changed, 107 insertions(+), 7 deletions(-) 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 049bfb12102..adaff4c89c9 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -2745,13 +2745,31 @@ const FLOOR_COST_DEBT_VERDICT: &str = "src/v2/workflow/floor_cost_debt_verdict.d /// cannot change the base population (review on gunbc#13332). A roster this change modifies must /// exist and evaluate at the base; anything else refuses rather than reading as carried. fn cost_debt_admitted_identities(base_tree: &FloorBaseTree) -> Result, String> { - let workspace = process_workspace_root(); - let base_commit = base_tree.commit()?; - let tree = cost_debt_scratch_dir("base")?; - let read = cost_debt_base_tree_extract(&workspace, base_commit, &tree) - .and_then(|()| cost_debt_roster_in_tree(&tree)); - std::fs::remove_dir_all(&tree).ok(); - cost_debt_admitted_by_fold(&read?, None) + cost_debt_admitted_at_base_tree( + base_tree, + |base_commit| { + let workspace = process_workspace_root(); + let tree = cost_debt_scratch_dir("base")?; + let read = cost_debt_base_tree_extract(&workspace, base_commit, &tree) + .and_then(|()| cost_debt_roster_in_tree(&tree)); + std::fs::remove_dir_all(&tree).ok(); + read + }, + |base_rows| cost_debt_admitted_by_fold(base_rows, None), + ) +} + +/// THE BASE SIDE OF ADMISSION: the base roster is read at the run's base-tree commit and nowhere +/// else, then folded against head. The read and the fold are parameters so the stale-base RED +/// drives THIS selection with supplied values inside the unit budget (gunbc#13452); production +/// supplies the base-tree extraction and the `.dag` fold above. +fn cost_debt_admitted_at_base_tree( + base_tree: &FloorBaseTree, + read_base_rows: impl FnOnce(&comparison_window::BaseTreeCommit) -> Result, String>, + fold: impl FnOnce(&[String]) -> Result, String>, +) -> Result, String> { + let base_rows = read_base_rows(base_tree.commit()?)?; + fold(&base_rows) } /// A fresh scratch directory under the ignored build tree, one per read. @@ -18562,4 +18580,86 @@ mod floor_base_tree_tests { ); std::fs::remove_dir_all(&dir).ok(); } + + /// RED (gunbc#13344, input (c)): a cost-debt row main retired after the branch point is NOT + /// admitted as this change's. Drives `cost_debt_admitted_at_base_tree` through `FloorBaseTree` + /// over a real history, with the roster read by `git show` and the fold supplied as the set + /// difference the `.dag` fold computes (`cost_debt_admitted_identities`), so no corpus is + /// evaluated. POSITIVE CONTROL: a row the branch really adds is admitted. + #[test] + fn cost_debt_row_retired_at_the_tip_but_carried_at_the_merge_base_is_not_admitted() { + let dir = std::env::temp_dir().join(format!( + "gunbc-cost-debt-base-tree-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock") + .as_nanos() + )); + std::fs::create_dir_all(&dir).expect("mkdir"); + let git = |args: &[&str]| -> String { + let out = std::process::Command::new("git") + .args(args) + .current_dir(&dir) + .output() + .unwrap_or_else(|e| panic!("git {args:?}: {e}")); + assert!( + out.status.success(), + "git {args:?}: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8_lossy(&out.stdout).trim().to_string() + }; + let commit = |rows: &str, msg: &str| { + std::fs::write(dir.join("roster.txt"), rows).expect("write"); + git(&["add", "-A"]); + git(&["commit", "--quiet", "--allow-empty", "-m", msg]); + git(&["rev-parse", "HEAD"]) + }; + git(&["init", "--quiet", "--initial-branch", "main", "."]); + git(&["config", "user.email", "fixture@gunbc.invalid"]); + git(&["config", "user.name", "fixture"]); + commit("t.old t.retired", "P"); + git(&["checkout", "--quiet", "-b", "subject"]); + let carried = commit("t.old t.retired", "H: carries the roster unchanged"); + let added = commit("t.old t.retired t.new", "H2: adds t.new"); + git(&["checkout", "--quiet", "main"]); + commit("t.old", "C: main retires t.retired"); + let rows_at = |rev: &str| -> Vec { + git(&["show", &format!("{rev}:roster.txt")]) + .split_whitespace() + .map(str::to_string) + .collect() + }; + let admitted = |head: &str| { + let base_tree = FloorBaseTree::for_comparison( + FreezeBaselineComparison::MergeBase { + base: "main".to_string(), + head: head.to_string(), + kind: "MergeTargetBaseline".to_string(), + }, + dir.clone(), + ); + let head_rows = rows_at(head); + cost_debt_admitted_at_base_tree( + &base_tree, + |base| Ok(rows_at(base.as_str())), + |base_rows| { + Ok(head_rows + .iter() + .filter(|r| !base_rows.contains(r)) + .cloned() + .collect()) + }, + ) + .expect("admission") + }; + assert_eq!( + admitted(&carried), + Vec::::new(), + "t.retired was retired on main after the branch point; it is not this change's" + ); + assert_eq!(admitted(&added), vec!["t.new".to_string()]); + std::fs::remove_dir_all(&dir).ok(); + } } From a94c6ad025a97f4decec5023a4f790b19af234c4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 11:42:19 +0000 Subject: [PATCH 7/8] row: cite the re-established stale-base RED and its executed mutant Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ster_edit_judged_against_the_base_tip_not_the_merge_base.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag index f7a0cb00a15..c0097258896 100644 --- a/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag +++ b/dag/gunbc/recurring_failure_mode/a_roster_edit_judged_against_the_base_tip_not_the_merge_base.dag @@ -23,7 +23,7 @@ data a_roster_edit_judged_against_the_base_tip_not_the_merge_base: RecurringFail "RECOGNITION RULE AND REVIEW TELL: a refusal naming rows the diff did not touch, which clears after merging main with no change to the diff, is this class until shown otherwise. In review, grep `.base()` on the comparison readout: every caller that reads a tree with it, rather than printing it, is a member.", - "DISCHARGED, 2026-10-05 (crisp-raven-17, gunbc#13347), by the trigger above, for all three inputs. THE CONSTRUCTION. v1_compiler.cli_run comparison_window resolve is the one place the relation is read for a tree: the base ref under two-dot, the departure point under merge-base. When it cannot resolve it refuses FreezeBaselineUnobservable or FreezeBaselineUnrelatedHistory and never degrades to a ref. It returns ResolvedComparisonWindow, whose base side is a BaseTreeCommit. That is a newtype whose field is private to that module, so resolve is its only constructor; ResolvedComparisonWindow's fields are private too. v1_compiler.cli_run required_floor_runner FloorBaseTree resolves the window at most once per floor run and lends it to every base-tree reader: interface_consumer_planning; unimported_bare_provider_gate, for input (a), the roster, through unimported_bare_provider_edit_refusals and unimported_bare_provider_roster_source_at_base; the cost-debt edit budget in run_required_floor, for input (b), the changed witness file's source, through cost_debt_changed_witness_ceilings; cost-debt admission, for input (c), cost-roster membership, through cost_debt_admitted_identities and cost_debt_base_tree_extract, the git archive of the base tree the base roster is evaluated over; and two tip readers this row had not named, floor_base_test_decl_census and floor_base_file_read, the second reached through non_fold_residue_changed_row_subjects. collect_frozen_path_deferral_additions_for uses the same resolver. FreezeBaselineComparison base() is deleted, and base_ref() remains for printing. EVIDENCE, IN ORDER OF STRENGTH. (1) THE READER INTERFACE, rung 3, structurally guaranteed at that interface. unimported_bare_provider_roster_source_at_base, unimported_bare_provider_edit_refusals, cost_debt_changed_witness_ceilings and cost_debt_base_tree_extract take a BaseTreeCommit, not a string, so the substitution a review raised does not compile: passing the readout's base_ref() as a string into the roster read. floor_base_file_read and floor_base_test_decl_census take FloorBaseTree. The limit is precise, and it is why this is ceiling 3, not 4: a consumer can still hand any ref to git outside these readers, or add a new reader that takes a string. namespace_baseline reconstruct_base_index still takes a string, because it is a general utility with other callers; the floor's only call passes FloorBaseTree's commit. That substitution was executed once as a one-off probe on this branch, and rustc refused it with E0308 (expected `&BaseTreeCommit`, found `&String`). The probe is not enrolled anywhere, because the crate has no compile-fail harness, so after this change the wall rests on the signatures. (2) THE EXECUTED CONTROL, at the selection boundary. required_floor_runner floor_base_tree_tests floor_base_tree_is_the_departure_point_under_merge_base_and_the_base_under_two_dot builds a real history (the branch departs at P, then main moves to C) and drives FloorBaseTree. Under merge-base every base-tree reader is handed P; before this change, the readers read C. Under two-dot the base tree is C, and an unrelated history refuses FreezeBaselineUnrelatedHistory under merge-base. It runs git and nothing else, so it fits the v1 unit lane's per-test budget. At this head that lane is the rust-unit-tests job of .github/workflows/witnesses.yml, which the aggregate job needs; that job, not this sentence, is the standing of the control. WHAT IT REPLACED, recorded rather than silently dropped: this PR first carried two real-history controls that drove the selection through the judgment: roster_edit_is_judged_at_the_merge_base_not_the_base_tip, through unimported_bare_provider_edit_refusals, and gunbc#13332's cost_debt_row_retired_at_the_tip_but_carried_at_the_merge_base_is_not_admitted, through cost_debt_admitted_by_fold. Both passed locally at head 165da6683e. gunbc#13452 then deleted every v1 Rust test over 100ms that evaluates the .dag corpus, the second of those among them, so this change follows that ruling rather than re-adding them. The judgments are the .dag's own and are claimed there. The join, meaning that a reader receives the selected commit, rests on the reader signatures in (1), which take BaseTreeCommit or FloorBaseTree, and has no executed control. Input (b) shares the commit and type, and has no discriminating control of its own. (3) NO .dag CONTROL IS REACHABLE for the defect itself. Which commit is the base side is decided on the host, and a .dag claim receives a roster already read. A claim over v2.workflow.floor_unimported_bare_provider_debt's edit judgment would only re-check the judgment, which v2.test.claim.floor_unimported_bare_provider_debt_test edit_refuses_a_rewritten_cause_and_a_reverted_retirement already executes on the floor. Its next-rung trigger is the window resolution moving into v2.workflow.floor_diff_observe. INPUT (c), COVERED AFTER gunbc#13332 LANDED. That PR carried its own per-site arm, cost_debt_comparison_base_commit, which this row's trigger says does not discharge it. As agreed with its author (crisp-ram-667), the PR that landed second converted it: the helper is deleted, cost_debt_admitted_identities takes FloorBaseTree, and the base-tree archive takes the BaseTreeCommit. cost_debt_roster_head_closure reads only the head tree and is not a base reader. That PR's RED, cost_debt_row_retired_at_the_tip_but_carried_at_the_merge_base_is_not_admitted, was converted to draw its commit from FloorBaseTree, and was later deleted with the slow v1 tests by gunbc#13452 (see (2)). No floor helper resolves its own merge base any more.", + "DISCHARGED, 2026-10-05 (crisp-raven-17, gunbc#13347), by the trigger above, for all three inputs. THE CONSTRUCTION. v1_compiler.cli_run comparison_window resolve is the one place the relation is read for a tree: the base ref under two-dot, the departure point under merge-base. When it cannot resolve it refuses FreezeBaselineUnobservable or FreezeBaselineUnrelatedHistory and never degrades to a ref. It returns ResolvedComparisonWindow, whose base side is a BaseTreeCommit. That is a newtype whose field is private to that module, so resolve is its only constructor; ResolvedComparisonWindow's fields are private too. v1_compiler.cli_run required_floor_runner FloorBaseTree resolves the window at most once per floor run and lends it to every base-tree reader: interface_consumer_planning; unimported_bare_provider_gate, for input (a), the roster, through unimported_bare_provider_edit_refusals and unimported_bare_provider_roster_source_at_base; the cost-debt edit budget in run_required_floor, for input (b), the changed witness file's source, through cost_debt_changed_witness_ceilings; cost-debt admission, for input (c), cost-roster membership, through cost_debt_admitted_identities and cost_debt_base_tree_extract, the git archive of the base tree the base roster is evaluated over; and two tip readers this row had not named, floor_base_test_decl_census and floor_base_file_read, the second reached through non_fold_residue_changed_row_subjects. collect_frozen_path_deferral_additions_for uses the same resolver. FreezeBaselineComparison base() is deleted, and base_ref() remains for printing. EVIDENCE, IN ORDER OF STRENGTH. (1) THE READER INTERFACE, rung 3, structurally guaranteed at that interface. unimported_bare_provider_roster_source_at_base, unimported_bare_provider_edit_refusals, cost_debt_changed_witness_ceilings and cost_debt_base_tree_extract take a BaseTreeCommit, not a string, so the substitution a review raised does not compile: passing the readout's base_ref() as a string into the roster read. floor_base_file_read and floor_base_test_decl_census take FloorBaseTree. The limit is precise, and it is why this is ceiling 3, not 4: a consumer can still hand any ref to git outside these readers, or add a new reader that takes a string. namespace_baseline reconstruct_base_index still takes a string, because it is a general utility with other callers; the floor's only call passes FloorBaseTree's commit. That substitution was executed once as a one-off probe on this branch, and rustc refused it with E0308 (expected `&BaseTreeCommit`, found `&String`). The probe is not enrolled anywhere, because the crate has no compile-fail harness, so after this change the wall rests on the signatures. (2) THE EXECUTED CONTROL, at the selection boundary. required_floor_runner floor_base_tree_tests floor_base_tree_is_the_departure_point_under_merge_base_and_the_base_under_two_dot builds a real history (the branch departs at P, then main moves to C) and drives FloorBaseTree. Under merge-base every base-tree reader is handed P; before this change, the readers read C. Under two-dot the base tree is C, and an unrelated history refuses FreezeBaselineUnrelatedHistory under merge-base. It runs git and nothing else, so it fits the v1 unit lane's per-test budget. At this head that lane is the rust-unit-tests job of .github/workflows/witnesses.yml, which the aggregate job needs; that job, not this sentence, is the standing of the control. WHAT IT REPLACED, recorded rather than silently dropped: this PR first carried two real-history controls that drove the selection through the judgment: roster_edit_is_judged_at_the_merge_base_not_the_base_tip, through unimported_bare_provider_edit_refusals, and gunbc#13332's cost_debt_row_retired_at_the_tip_but_carried_at_the_merge_base_is_not_admitted, through cost_debt_admitted_by_fold. Both passed locally at head 165da6683e. gunbc#13452 then deleted every v1 Rust test over 100ms that evaluates the .dag corpus, the second of those among them, so this change follows that ruling rather than re-adding them. The judgments are the .dag's own and are claimed there. For input (c) the join, meaning that the reader receives the selected commit, is executed by the cost-debt RED below. For (a) and (b) it rests on the reader signatures in (1), which take BaseTreeCommit or FloorBaseTree. Input (b) shares the commit and type, and has no discriminating control of its own. (3) NO .dag CONTROL IS REACHABLE for the defect itself. Which commit is the base side is decided on the host, and a .dag claim receives a roster already read. A claim over v2.workflow.floor_unimported_bare_provider_debt's edit judgment would only re-check the judgment, which v2.test.claim.floor_unimported_bare_provider_debt_test edit_refuses_a_rewritten_cause_and_a_reverted_retirement already executes on the floor. Its next-rung trigger is the window resolution moving into v2.workflow.floor_diff_observe. INPUT (c), COVERED AFTER gunbc#13332 LANDED. That PR carried its own per-site arm, cost_debt_comparison_base_commit, which this row's trigger says does not discharge it. As agreed with its author (crisp-ram-667), the PR that landed second converted it: the helper is deleted, cost_debt_admitted_identities takes FloorBaseTree, and the base-tree archive takes the BaseTreeCommit. cost_debt_roster_head_closure reads only the head tree and is not a base reader. That PR's RED, cost_debt_row_retired_at_the_tip_but_carried_at_the_merge_base_is_not_admitted, was deleted with the slow v1 tests by gunbc#13452. A fix may not land after its discriminating RED is gone, so the RED is re-established at a grain that runs, required_floor_runner floor_base_tree_tests cost_debt_row_retired_at_the_tip_but_carried_at_the_merge_base_is_not_admitted. It drives cost_debt_admitted_at_base_tree, the selection cost_debt_admitted_identities runs, through FloorBaseTree over a real history, with the roster read by git show and the .dag fold's set difference supplied, so no corpus is evaluated. A row main retired after the branch point is not admitted; the POSITIVE CONTROL, a row the branch really adds, is admitted. MUTANT, executed remotely at gunbc#13347 head 401bd7249a: FloorBaseTree resolving a two-dot window (the tip) instead of the comparison's own makes this RED fail, admitting [t.retired], and makes the selection control fail too, base tree = tip rather than departure point. Unmutated, both pass in 0.05s. No floor helper resolves its own merge base any more.", ], evidence: [], } From eb2ef6d6a9df7c678ad6c0f396f7ff57a042771c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 11:54:03 +0000 Subject: [PATCH 8/8] Restore comparison_operands_never_judged_in_v2_infer.dag, dropped by the hand-built main merge (review 77008) Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ison_operands_never_judged_in_v2_infer.dag | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 dag/gunbc/recurring_failure_mode/comparison_operands_never_judged_in_v2_infer.dag diff --git a/dag/gunbc/recurring_failure_mode/comparison_operands_never_judged_in_v2_infer.dag b/dag/gunbc/recurring_failure_mode/comparison_operands_never_judged_in_v2_infer.dag new file mode 100644 index 00000000000..e8b98b3b142 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/comparison_operands_never_judged_in_v2_infer.dag @@ -0,0 +1,24 @@ +module gunbc.recurring_failure_mode.comparison_operands_never_judged_in_v2_infer + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data comparison_operands_never_judged_in_v2_infer: RecurringFailureMode = RecurringFailureMode { + identity: "comparison_operands_never_judged_in_v2_infer" as NonEmptyStr, + + receipts: [ + "**In v2 infer, the operands of an equality or ordering comparison are never judged against each other.** Measured on session/stern-swift-290-literal (XL-2 literal elaboration), with fixtures assembled through the production route and inferred: `fn k(n: Nat) -> Bool { n == (0 + 1) }`, where n is std.nat Nat and the right operand is an Int, is ACCEPTED. A Nat compared with an Int is a type error the compiler floor owes (DESIGN section 4b, values inhabit declared types), so this is below the floor and silently admitted.", + + "DISTINGUISHING FACTS. (1) v2.compiler.infer infer_binary_algebra_field reads the operator's canonical-operation wire and answers a field only for AlgebraPrimitive (`+` ^ring_field_add, `||` ^lattice_field_join, `&&` ^lattice_field_meet). For EqualityComparison and OrderingComparison (`==`, `!=`, `<`, `<=`, `>`, `>=`) it answers Absent. (2) A binary Transform whose operator is not an algebra primitive stays GroundingNotDerived, so infer_unify_transform_operand_types is never reached for it and nothing compares the two operand types. (3) This is not a literal problem: any two operands of different types compare without a diagnostic. Found while measuring the reach of a literal-elaboration change, where a control expected `n == 0` to exercise the operand route and it did not.", + + "RUNG FOUND AT: below the floor (silently admitted). CEILING: structurally guaranteed. Whether two operand types can be compared is decidable over modeled types: through the equality and ordering structures (Equivalence, ordered structures) the operand type's AlgebraInhabitanceDecl rows declare. NEXT-RUNG TRIGGER, NAMED AS A CAPABILITY: an infer arm that judges comparison operands -- and the inverse-compose operators (`-`) of the receipt below -- through the equality/ordering structure via the same inhabitance rows the algebra-operator arm reads (gunbc#13060's route extended to EqualityComparison and OrderingComparison), sufficient for every comparison whose operand types are derived to be judged rather than left GroundingNotDerived. Owner, per the N7 manager: the #13060 operator route.", + "SECOND OPERATOR POPULATION, SAME CLASS (calm-boar-904): subtraction. The `-` token canonicalizes to canonical_operation_op_sub, an AlgebraInverseCompose (add composed with the group inverse), and infer_canonical_operation_field answers Absent for AlgebraInverseCompose exactly as for the comparisons, so a binary `-` also stays GroundingNotDerived and its operands are never judged. Specimen: v2.std.node's `count(children) - positional_child_count(children: children)`, where count returns std.nat Nat and positional_child_count returns Int, passes with no diagnostic. The repair the trigger names covers both populations together: comparisons through the equality/ordering structure, and inverse-compose operators through their binary field plus the inverse the structure declares.", + ], + + evidence: [ + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_binary_algebra_field", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_unify_transform_operand_types", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.std.node", decl_name: "positional_child_count", field: WholeDeclaration }, + ], +}