From 607b23ffcf5f00aa37e3a800a48faac1d83efcbd Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 02:55:49 +0000 Subject: [PATCH 1/9] dependency_resolution_facts unions reference edges for every importer (drops ImportBearing exemption) Co-Authored-By: Claude Sonnet 5.5 --- src/v1/stage0/src/cli_run.rs | 84 ++++++++++++++++++++-- src/v1/stage0/src/cli_run/entry_resolve.rs | 14 ++-- 2 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 728d0d0d83a..22ad4741e04 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -30145,8 +30145,8 @@ pub fn dependency_resolution_facts( /// /// WHAT IT DOES NOT DEMAND is the corpus. The declared-module index is the process-cached /// `build_module_path_index` the population read also consults; the reference half's pool name -/// index is built only when the importer carries no `import` line, which is the one case whose -/// edges depend on other files' names. An importer the population would not walk -- outside every +/// index is built for every readable importer, since a reference edge depends on other files' +/// names whether or not the importer carries `import` lines. An importer the population would not walk -- outside every /// pool root, or matched by an exclusion -- answers the empty list, as the population carries no /// row for it; so does an unreadable one, whose import half the population walk also skips. pub fn dependency_resolution_facts_at( @@ -30179,8 +30179,7 @@ pub fn dependency_resolution_facts_at( entry_resolve::FileReferenceEdges::Edges(edges) => { reference_edges_as_import_facts(&edges, /* strict */ true) } - entry_resolve::FileReferenceEdges::ImportBearing - | entry_resolve::FileReferenceEdges::Unaccounted(_) => Vec::new(), + entry_resolve::FileReferenceEdges::Unaccounted(_) => Vec::new(), }; union_dedup_import_facts_reference_first(reference_edges, import_edges) } @@ -37310,6 +37309,83 @@ mod output_policy_decode_tests { } } +#[cfg(test)] +mod import_bearing_reference_edges { + //! An `import` line does not own a module's edge set: the producer unions reference edges for + //! every importer. Supplied fixture (pool names handed in), no corpus resolve. + + use super::*; + + fn names() -> entry_resolve::ReferencePoolNames { + entry_resolve::ReferencePoolNames { + decl_index: Default::default(), + module_names: [ + "v2.std.artifact", + "v2.std.refinement", + "v2.std.node", + "v2.std.layer", + ] + .iter() + .map(|s| s.to_string()) + .collect(), + } + } + + const IMPORTING: &str = + "module v2.std.artifact\nimport v2.std.node\nfn g() -> Int { v2.std.refinement.k }\n"; + const IMPORTLESS: &str = "module v2.std.artifact\nfn g() -> Int { v2.std.refinement.k }\n"; + + fn edge_targets(src: &str, imports_only_mutant: bool) -> Vec { + if imports_only_mutant && !extract_import_paths(src).is_empty() { + return Vec::new(); + } + let n = names(); + let entry_resolve::FileReferenceEdges::Edges(edges) = + entry_resolve::reference_edges_for_file("v2/std/artifact.dag", Some(src), &n) + else { + panic!("a parsed file has edges"); + }; + let import_edges = entry_resolve::import_facts_for_file("v2/std/artifact.dag", src, |m| { + n.module_names.contains(m) + }); + union_dedup_import_facts_reference_first( + entry_resolve::reference_edges_as_import_facts(&edges, true), + import_edges, + ) + .into_iter() + .map(|f| f.import_module) + .collect() + } + + /// RED: the qualified reference to a module the file does not import is an edge. + #[test] + fn an_import_bearing_file_carries_its_unimported_reference_edge() { + let t = edge_targets(IMPORTING, false); + assert!(t.contains(&"v2.std.refinement".to_string()), "{t:?}"); + assert!(t.contains(&"v2.std.node".to_string()), "{t:?}"); + } + + /// The imports-only mutant is the pre-fix producer; the RED must reject it. + #[test] + fn the_imports_only_mutant_fails_the_red() { + let t = edge_targets(IMPORTING, true); + assert!(!t.contains(&"v2.std.refinement".to_string())); + } + + /// Control: an import-less file's reference edges are the same under both producers. + #[test] + fn an_importless_file_is_unchanged() { + assert_eq!( + edge_targets(IMPORTLESS, false), + edge_targets(IMPORTLESS, true) + ); + assert_eq!( + edge_targets(IMPORTLESS, false), + vec!["v2.std.refinement".to_string()] + ); + } +} + #[cfg(test)] mod union_dedup_import_facts_law { //! The dedup that carries the edge population's identity and order law, tested on synthetic diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 9aa4c5988d2..cf76f6bb909 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -3155,9 +3155,6 @@ impl ReferenceSelectionTier { /// One file's answer from the reference-edge producer. pub(crate) enum FileReferenceEdges { - /// The file carries `import` lines: its edges are owned EXACTLY by `import_resolution_facts`, - /// and emitting reference edges for it would only over-connect. - ImportBearing, /// The file's reference edges, every tier (`reference_edges_as_import_facts` filters). Edges(Vec), /// An import-less file the producer could not answer for, with the located cause. @@ -3338,9 +3335,10 @@ pub(crate) fn reference_edges_for_file( } /// The same per-file answer with the pool name index DEMANDED rather than supplied. An unreadable -/// or import-bearing file is decided from its own bytes, so a caller asking about one such file -/// never builds the whole-pool heads index; only an import-less file, whose references must be -/// resolved against pool names, forces it. +/// file is decided from its own bytes and never builds the whole-pool heads index. EVERY readable +/// file, import-bearing or not, answers its reference edges: an `import` line declares some of a +/// module's dependencies, not all of them (`v2.std.artifact` imports three modules and reaches +/// `v2.std.refinement` only by qualified reference), so imports never own a file's edge set. pub(crate) fn reference_edges_for_file_on_demand< R: std::ops::Deref, >( @@ -3351,9 +3349,6 @@ pub(crate) fn reference_edges_for_file_on_demand< let Some(content) = content else { return FileReferenceEdges::Unaccounted("unreadable"); }; - if !extract_import_paths(content).is_empty() { - return FileReferenceEdges::ImportBearing; - } let names = names(); let names: &ReferencePoolNames = &names; let Some(self_module) = extract_module_path(content) else { @@ -3490,7 +3485,6 @@ pub fn reference_resolution_facts( } let content = std::fs::read_to_string(&file).ok(); match reference_edges_for_file(&rel, content.as_deref(), &names) { - FileReferenceEdges::ImportBearing => {} FileReferenceEdges::Edges(file_edges) => edges.extend(file_edges), FileReferenceEdges::Unaccounted(cause) => { unaccounted.push(ReferenceAccountingRefusal { From 78956b9908d0aeffbd533f5e04f2273a9cd72e96 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 03:21:12 +0000 Subject: [PATCH 2/9] Update seed-growth current_boundary for the all-importer reference half Co-Authored-By: Claude Sonnet 5.5 --- dag/gunbc/keyed_dependency_edge_read_seed_growth.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag b/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag index cd740274d85..c3eea6e62f4 100644 --- a/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag +++ b/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag @@ -54,5 +54,5 @@ data keyed_dependency_edge_read_seed_growth_justification: SeedGrowthJustificati reason: "A keyed read of a host-produced edge census is a property of the host that holds it: the edges are produced by the seed's own source walk and parse, the population read that produces them is already native (dependency_resolution_facts, moved to the host because its interpreted union cost 104,943ms), and grouping them on the side of the seam where they still exist as host rows is the only place the grouping costs nothing. Running the grouping in the interpreter is precisely the corpus-wide fold being removed.\n\nTHE RETURN SHAPE IS THE POPULATION READ'S. It returns the same ModuleDependencyEdge rows the population read returns for that importer, including target_declared: false rows, so the demand walk applies the same edge law the adjacency fold did: an undeclared target contributes no path.\n\nA FILE PATH AS AN IMPORTER ROOT NOW REFUSES. Before this change a file passed to the population read's importer_roots reached cli_run anchor_source_root and aborted the process with a panic. Both arms now classify their directory roots with coproduct_reflection pool_root_defects first and refuse with InterpError PoolRootContributesNothing, naming the builtin and each offending root with its PoolRootDefect.", owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, trigger: "Delete when a SELF-EMITTED reader reaches the per-file dependency edges of a module: that is, when the edge census is produced by an emitted v2 fold over the source roots rather than by the seed's host walk, so the self-emitted closure walk can ask one importer's edges of that fold directly. Sufficient means the emitted reader answers dependency_closure_live_excluding for the approval broker entry and one compiler-reaching entry with the same closures this host read produces; an emitted population read that the walk would still have to fold whole does not satisfy it.", - current_boundary: "dependency_resolution_facts_at reads one importer file and composes its two per-file halves: import_facts_for_file against the process-cached build_module_path_index, and reference_edges_for_file_on_demand, which decides an unreadable or import-bearing file from its own bytes and builds the process-cached pool name index (reference_pool_names) only for an import-less one. An importer outside every pool root, matched by an exclusion, or unreadable answers the empty list, as the population read carries no row for it. It adds no walk, no row, no field and no decision, and holds no state of its own: the two indexes it consults are the ones the population read already caches." + current_boundary: "dependency_resolution_facts_at reads one importer file and composes its two per-file halves: import_facts_for_file against the process-cached build_module_path_index, and reference_edges_for_file_on_demand, which decides an unreadable file from its own bytes and builds the process-cached pool name index (reference_pool_names) for every readable importer, import-bearing or not. An importer outside every pool root, matched by an exclusion, or unreadable answers the empty list, as the population read carries no row for it. It adds no walk, no row, no field and no decision, and holds no state of its own: the two indexes it consults are the ones the population read already caches." } From 1d72abda9e4ac0d460b5f8bc199d7068006f2d98 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 03:26:36 +0000 Subject: [PATCH 3/9] Declare dry_run as an emit-only basename at the workspace closure check; re-ground stale import-bearing prose Co-Authored-By: Claude Sonnet 5.5 --- .../keyed_dependency_edge_read_seed_growth.dag | 4 ++-- .../module_graph_edge_source_witness_test.dag | 2 +- src/v1/stage0/src/cli_run.rs | 7 +++---- src/v1/stage0/src/cli_run/entry_resolve.rs | 6 ++++-- src/v2/lens/manufactured_dependency_census.dag | 7 +++---- .../emitted_crate_workspace_witness_test.dag | 15 +++++++++++++++ src/v2/workflow/emitted_crate_workspace.dag | 10 +++++++++- 7 files changed, 37 insertions(+), 14 deletions(-) diff --git a/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag b/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag index c3eea6e62f4..99013da74fb 100644 --- a/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag +++ b/dag/gunbc/keyed_dependency_edge_read_seed_growth.dag @@ -24,8 +24,8 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } // any row it returns is the row the population read carries for that path, in the same order. // Its first form grouped the whole population union by importer, which made the first keyed read // in a process pay the whole-pool reference parse (the floor's reference_edges shared fill, -// 15.7s charged to one claim on gunbc#12523); the per-file form demands the pool name index only -// for an import-less importer, whose edges genuinely depend on other files' names. +// 15.7s charged to one claim on gunbc#12523); the per-file form demands the pool name index for +// every readable importer, import-bearing or not, since a reference edge depends on other files' names. // // A FOURTH ITEM, with_module_path_index, is the borrowing reader of the SAME MODULE_PATH_INDEX_CACHE // build_module_path_index fills: the keyed reads above asked one entry of it per visited module and diff --git a/dag/test/claim/module_graph_edge_source_witness_test.dag b/dag/test/claim/module_graph_edge_source_witness_test.dag index 92cf9bb171b..e7848c86274 100644 --- a/dag/test/claim/module_graph_edge_source_witness_test.dag +++ b/dag/test/claim/module_graph_edge_source_witness_test.dag @@ -12,7 +12,7 @@ data witness_subject_note: String = "THE EDGE SOURCE IS A UNION OF TWO PRODUCERS WHY A FIXTURE TREE RATHER THAN HAND-BUILT LISTS. A unit test of union_import_resolution_fact_lists proves the fold and leaves the defect untouched, because the defect is in WHICH PRODUCERS the production function calls — the fold receives whatever it is handed. So this points the production function itself at a four-module fixture: consumer imports provider_imported and provider_shared explicitly, and reaches provider_referenced ONLY through a qualified container.member reference with no import. The union is then asserted at identity grain in both directions — every expected edge present, and no surplus target — so a missing arm reds on the reference-only edge and a doubled arm reds on the surplus. -WHY THERE IS NO SHARED-PROVIDER DEDUP CONTROL HERE, stated rather than quietly omitted: the two producers are DISJOINT BY FILE. reference_resolution_facts emits nothing for a file that still carries import lines, precisely so the reference producer cannot over-connect the un-stripped tree, so one path cannot appear in both producers and a same-path duplicate is unconstructible through this seam. Asserting a dedup that cannot arise would be a tautological control. What IS asserted instead is the union at identity grain in both directions - every producer row reaches the union, and the union carries no row neither producer produced - which is the property the dedup exists to preserve." +WHY THERE IS NO SHARED-PROVIDER DEDUP CONTROL HERE, stated rather than quietly omitted: the two producers are NO LONGER disjoint by file: reference_resolution_facts answers import-bearing files too, so one path can appear in both and a dual-backed edge is constructible; witness_union_has_no_duplicate_target is the dedup control. What IS asserted instead is the union at identity grain in both directions - every producer row reaches the union, and the union carries no row neither producer produced - which is the property the dedup exists to preserve." data fixture_roots: List = ["dag/test/fixture/module_graph_edge_source"] diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 22ad4741e04..d10bddfdd77 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -6337,10 +6337,9 @@ impl ModuleGraphFactsLive { /// reference edge (`selection_adjacency` minus `adjacency`) — i.e. the direct-import term /// a stripped (no `import` line) module is otherwise missing from its typed-module content /// key (DESIGN §3: consumes the same `selection_adjacency` authority affected-set selection - /// already reads; no second reference-edge producer). An import-bearing file's declared - /// imports are already covered by `resolved.resolved_imports`, so this returns empty for it - /// (`adjacency` and `selection_adjacency` agree on such a file — see - /// `reference_resolution_facts` pass 2). + /// already reads; no second reference-edge producer). For an import-bearing file it + /// returns the modules it reaches by qualified or unique-bare reference WITHOUT importing them; + /// its declared imports are already covered by `resolved.resolved_imports`. /// Workspace-relative repo paths `importer_repo_path` depends on ONLY through a strict-tier /// reference edge (`selection_adjacency` minus `adjacency`). The path-grain authority /// `selection_adjacency` already carries; module names are derived only for diagnostics. diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index cf76f6bb909..e89bef3c60c 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -521,8 +521,10 @@ pub(crate) fn build_module_graph_facts_live_uncached( // modules), while for SELECTION it is precisely the thing that destroys the answer. The loader // (`extend_with_bare_reference_closure`) is deliberately left alone. // - // Import-bearing files emit no reference edges at all (see `reference_resolution_facts` pass 2), - // so on an un-stripped file the union is a no-op and the graph is byte-identical to before. + // (SUPERSEDED: this measurement predates the all-importer producer. Import-bearing files now + // also emit strict-tier reference edges for modules they reach without importing, so the union + // is no longer a no-op on an un-stripped file. The selection-tier effect of that widening is + // measured by the floor, not asserted here.) // FIVE ROWS, EACH NET OF THE OTHERS. The import-edge facts are the module path index's first // demander, and that index is the first demander of every pool file's lexing, newline index // and heads parse (`pool_acquire`, which records those three as `pool_source_tokenize`, diff --git a/src/v2/lens/manufactured_dependency_census.dag b/src/v2/lens/manufactured_dependency_census.dag index 2f33272b25d..839c2166b54 100644 --- a/src/v2/lens/manufactured_dependency_census.dag +++ b/src/v2/lens/manufactured_dependency_census.dag @@ -28,10 +28,9 @@ import v2.lens.module_graph { // forbids one and is right to; this is a pure reader over the two that module already owns, in the // idiom DESIGN section 6 gives a lens. // -// WHAT THE COUNT IS AND IS NOT, because the honest bound matters more than the number. The two -// producers agree on an import-BEARING file (`ModuleGraphFactsLive::reference_only_direct_import_paths` -// returns empty for one), so this census sees the manufactured edges of import-LESS modules and -// NOT the bare cross-module references of modules that carry an import line. Those exist -- +// WHAT THE COUNT IS AND IS NOT, because the honest bound matters more than the number. The reference +// producer now answers every importer, so this census sees the unimported references of +// import-BEARING modules too; the paragraph below describes the earlier import-less-only reading. Those exist -- // `src/v2/lens/grounding.dag` has three import lines and calls functions it never imports, each one // named by that file's own entry-scoped compile refusal -- and they are invisible here BY // CONSTRUCTION, not by measuring zero. So this figure is a LOWER BOUND on diff --git a/src/v2/test/claim/emitted_crate_workspace_witness_test.dag b/src/v2/test/claim/emitted_crate_workspace_witness_test.dag index 99dd93ec995..029b6ae0533 100644 --- a/src/v2/test/claim/emitted_crate_workspace_witness_test.dag +++ b/src/v2/test/claim/emitted_crate_workspace_witness_test.dag @@ -117,6 +117,21 @@ test fn a_dag_module_the_emission_did_not_produce_refuses_by_name() -> Bool { } } +// dry_run is a declared emit-only basename: emitted with no DAG module, it is admitted beside v1_rt. +test fn a_declared_emit_only_basename_is_not_outside_the_dag() -> Bool { + match emitted_workspace_plan_over( + module_dag: chain_dag(), + emitted_basenames: ["fx_a", "fx_b", "fx_c", "v1_rt", "dry_run"], + edge_froms: chain_edge_froms(), + edge_tos: chain_edge_tos(), + edge_provenances: chain_edge_provenances(), + target_crate_count: 3 + ) { + EmittedWorkspacePlanned { plan: _ } => true + EmittedWorkspaceRefused { cause: _ } => false + } +} + test fn an_emitted_module_outside_the_dag_refuses_by_name() -> Bool { match emitted_workspace_plan_over( module_dag: chain_dag(), diff --git a/src/v2/workflow/emitted_crate_workspace.dag b/src/v2/workflow/emitted_crate_workspace.dag index f090819e8a1..2562bd465c6 100644 --- a/src/v2/workflow/emitted_crate_workspace.dag +++ b/src/v2/workflow/emitted_crate_workspace.dag @@ -204,6 +204,13 @@ fn emitted_workspace_edges(froms: List, tos: List, provenances: } } +// EMIT-ONLY BASENAMES: files the emitter writes with no .dag module behind them, beside the runtime +// prelude. v1.compiler.emit_rust emit_dry_run_module writes dry_run.rs (the DryRunMode wrapper) +// whenever the closure declares services, so it is never a module of the derived DAG. It is declared +// here, at the authority that owns the closure-agreement exception, rather than filtered out of the +// report; a basename absent from this list and from the DAG still refuses by name. +data emitted_workspace_emit_only_basenames: List = ["dry_run"] + // The derived DAG in BASENAME space, with the runtime module as a node, because that is the space // the emitter's edges and the rendered includes speak. type BasenameDagBuild @@ -216,6 +223,7 @@ fn emitted_workspace_basename_dag(module_dag: ModuleDag, module_by_basename: Map match emitted_workspace_lookup(m: emitted_set, key: rust_module_emit_filename(module_name: m as String)) { Present { value: _ } => false Absent => true }) let outside = emitted_basenames |> filter(b => b != rust_runtime_prelude_module + && !(emitted_workspace_emit_only_basenames |> any(e => e == b)) && match emitted_workspace_lookup(m: module_by_basename, key: b) { Present { value: _ } => false Absent => true }) if ((not_emitted |> count) + (outside |> count)) > 0 { BasenameDagRefused { @@ -289,7 +297,7 @@ fn emitted_workspace_runtime_row() -> GeneratedPartitionCrateRow { package_name: emitted_workspace_runtime_package, crate_dir: emitted_workspace_crate_dir(package: emitted_workspace_runtime_package), kind: GeneratedFoundationCrate, - modules: [rust_runtime_prelude_module], + modules: concat([rust_runtime_prelude_module], emitted_workspace_emit_only_basenames), reexport_packages: [], carries_non_empty_wrappers: true } From fc0c88b3475726bd0e39f7c01fa811548d707386 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 03:42:32 +0000 Subject: [PATCH 4/9] Runtime crate lists dry_run only when emitted; census bound states the all-importer reading once Co-Authored-By: Claude Sonnet 5.5 --- .../lens/manufactured_dependency_census.dag | 20 +++++++++---------- .../emitted_crate_workspace_witness_test.dag | 19 +++++++++++++++++- src/v2/workflow/emitted_crate_workspace.dag | 10 +++++----- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/v2/lens/manufactured_dependency_census.dag b/src/v2/lens/manufactured_dependency_census.dag index 839c2166b54..3587eb83123 100644 --- a/src/v2/lens/manufactured_dependency_census.dag +++ b/src/v2/lens/manufactured_dependency_census.dag @@ -28,17 +28,15 @@ import v2.lens.module_graph { // forbids one and is right to; this is a pure reader over the two that module already owns, in the // idiom DESIGN section 6 gives a lens. // -// WHAT THE COUNT IS AND IS NOT, because the honest bound matters more than the number. The reference -// producer now answers every importer, so this census sees the unimported references of -// import-BEARING modules too; the paragraph below describes the earlier import-less-only reading. Those exist -- -// `src/v2/lens/grounding.dag` has three import lines and calls functions it never imports, each one -// named by that file's own entry-scoped compile refusal -- and they are invisible here BY -// CONSTRUCTION, not by measuring zero. So this figure is a LOWER BOUND on -// the population that binds without an authored dependency, and quoting it as the population is the -// denominator error. The upper half needs a per-REFERENCE census on the value channel, which does -// not exist: the mask check that emits `UnlistedImportUse` lives in the TYPE resolver -// (`v1.compiler.infer_resolve` `resolve_node`, keyed on `type_name`), so a bare cross-module CALL -// produces neither a refusal nor an advisory and no census can currently see it. +// WHAT THE COUNT IS AND IS NOT, because the honest bound matters more than the number. The census +// counts every module's manufactured edges, import-bearing or not: `src/v2/lens/grounding.dag` has +// three import lines and calls functions it never imports, and those references are edges here. It +// is still a LOWER BOUND on the population that binds without an authored dependency, because it +// counts only what the reference reader collects and resolves to a declared module, and quoting it +// as the population is the denominator error. The upper half needs a per-REFERENCE census on the +// value channel, which does not exist: the mask check that emits `UnlistedImportUse` lives in the +// TYPE resolver (`v1.compiler.infer_resolve` `resolve_node`, keyed on `type_name`), so a bare +// cross-module CALL the reader does not collect produces neither a refusal nor an advisory. // // SUBJECT: `gunbc.recurring_failure_mode` // `binding_chosen_by_pool_membership_rather_than_by_the_declared_rule`, whose next-rung trigger diff --git a/src/v2/test/claim/emitted_crate_workspace_witness_test.dag b/src/v2/test/claim/emitted_crate_workspace_witness_test.dag index 029b6ae0533..65e52531931 100644 --- a/src/v2/test/claim/emitted_crate_workspace_witness_test.dag +++ b/src/v2/test/claim/emitted_crate_workspace_witness_test.dag @@ -127,7 +127,24 @@ test fn a_declared_emit_only_basename_is_not_outside_the_dag() -> Bool { edge_provenances: chain_edge_provenances(), target_crate_count: 3 ) { - EmittedWorkspacePlanned { plan: _ } => true + EmittedWorkspacePlanned { plan: plan } => + plan.rows |> any(r => r.package_name == emitted_workspace_runtime_package && r.modules |> any(m => m == "dry_run")) + EmittedWorkspaceRefused { cause: _ } => false + } +} + +// A closure the emitter wrote no dry_run for (no services) plans a runtime crate that names none. +test fn the_runtime_crate_names_dry_run_only_when_the_emission_wrote_it() -> Bool { + match emitted_workspace_plan_over( + module_dag: chain_dag(), + emitted_basenames: chain_emitted_basenames(), + edge_froms: chain_edge_froms(), + edge_tos: chain_edge_tos(), + edge_provenances: chain_edge_provenances(), + target_crate_count: 3 + ) { + EmittedWorkspacePlanned { plan: plan } => + plan.rows |> any(r => r.package_name == emitted_workspace_runtime_package && !(r.modules |> any(m => m == "dry_run"))) EmittedWorkspaceRefused { cause: _ } => false } } diff --git a/src/v2/workflow/emitted_crate_workspace.dag b/src/v2/workflow/emitted_crate_workspace.dag index 2562bd465c6..de14c8e2579 100644 --- a/src/v2/workflow/emitted_crate_workspace.dag +++ b/src/v2/workflow/emitted_crate_workspace.dag @@ -292,12 +292,12 @@ fn emitted_workspace_generated_row(row: Stage0PartitionCrateRow, reexports: List } } -fn emitted_workspace_runtime_row() -> GeneratedPartitionCrateRow { +fn emitted_workspace_runtime_row(emitted_basenames: List) -> GeneratedPartitionCrateRow { GeneratedPartitionCrateRow { package_name: emitted_workspace_runtime_package, crate_dir: emitted_workspace_crate_dir(package: emitted_workspace_runtime_package), kind: GeneratedFoundationCrate, - modules: concat([rust_runtime_prelude_module], emitted_workspace_emit_only_basenames), + modules: concat([rust_runtime_prelude_module], emitted_workspace_emit_only_basenames |> filter(e => emitted_basenames |> any(b => b == e))), reexport_packages: [], carries_non_empty_wrappers: true } @@ -307,8 +307,8 @@ type DerivedRowsBuild = DerivedRowsOk { rows: List } | DerivedRowsRefused { cause: EmittedWorkspaceRefusal } -fn emitted_workspace_derived_rows(unit_rows: List, unit_edges: List) -> DerivedRowsBuild { - unit_rows |> fold(init: DerivedRowsOk { rows: [emitted_workspace_runtime_row()] }, f: fn(acc, row) { +fn emitted_workspace_derived_rows(unit_rows: List, unit_edges: List, emitted_basenames: List) -> DerivedRowsBuild { + unit_rows |> fold(init: DerivedRowsOk { rows: [emitted_workspace_runtime_row(emitted_basenames: emitted_basenames)] }, f: fn(acc, row) { match acc { DerivedRowsRefused { cause: c } => DerivedRowsRefused { cause: c } DerivedRowsOk { rows: rows } => @@ -430,7 +430,7 @@ fn emitted_workspace_plan_over( ResolvePartitionOk { assignment: assignment, provenance: _ } => { let unit_ids = list_unique_sorted_ids(items: assignment |> map_values) let unit_edges = assignment_unit_dep_edges(module_dag: closure_dag, assignment: assignment) - match emitted_workspace_derived_rows(unit_rows: emitted_workspace_unit_rows(modules: closure_dag.modules, assignment: assignment, unit_ids: unit_ids), unit_edges: unit_edges) { + match emitted_workspace_derived_rows(unit_rows: emitted_workspace_unit_rows(modules: closure_dag.modules, assignment: assignment, unit_ids: unit_ids), unit_edges: unit_edges, emitted_basenames: emitted_basenames) { DerivedRowsRefused { cause: c } => EmittedWorkspaceRefused { cause: c } DerivedRowsOk { rows: rows } => match emitted_workspace_red(edges: edges, assignment: assignment, unit_edges: unit_edges) { From ad37f6fd2e3d7a8ceadf99906cdb4c949c47d215 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 04:19:32 +0000 Subject: [PATCH 5/9] Reference reader: an importing file's bare names are lexically bound, only qualified references become edges; note producers overlap in the edge-source witness Co-Authored-By: Claude Sonnet 5.5 --- .../module_graph_edge_source_witness_test.dag | 2 +- src/v1/stage0/src/cli_run.rs | 52 +++++++++++++++++++ src/v1/stage0/src/cli_run/entry_resolve.rs | 16 +++++- 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/dag/test/claim/module_graph_edge_source_witness_test.dag b/dag/test/claim/module_graph_edge_source_witness_test.dag index e7848c86274..5357446b1ee 100644 --- a/dag/test/claim/module_graph_edge_source_witness_test.dag +++ b/dag/test/claim/module_graph_edge_source_witness_test.dag @@ -12,7 +12,7 @@ data witness_subject_note: String = "THE EDGE SOURCE IS A UNION OF TWO PRODUCERS WHY A FIXTURE TREE RATHER THAN HAND-BUILT LISTS. A unit test of union_import_resolution_fact_lists proves the fold and leaves the defect untouched, because the defect is in WHICH PRODUCERS the production function calls — the fold receives whatever it is handed. So this points the production function itself at a four-module fixture: consumer imports provider_imported and provider_shared explicitly, and reaches provider_referenced ONLY through a qualified container.member reference with no import. The union is then asserted at identity grain in both directions — every expected edge present, and no surplus target — so a missing arm reds on the reference-only edge and a doubled arm reds on the surplus. -WHY THERE IS NO SHARED-PROVIDER DEDUP CONTROL HERE, stated rather than quietly omitted: the two producers are NO LONGER disjoint by file: reference_resolution_facts answers import-bearing files too, so one path can appear in both and a dual-backed edge is constructible; witness_union_has_no_duplicate_target is the dedup control. What IS asserted instead is the union at identity grain in both directions - every producer row reaches the union, and the union carries no row neither producer produced - which is the property the dedup exists to preserve." +THE PRODUCERS OVERLAP, SO THE DEDUP HAS ITS OWN CONTROL. reference_resolution_facts answers import-bearing files too, so one path can appear in both producers and a dual-backed edge is constructible; witness_union_has_no_duplicate_target is the control that such an edge appears once. Beside it, the union is asserted at identity grain in both directions - every producer row reaches the union, and the union carries no row neither producer produced - which is the property the dedup exists to preserve." data fixture_roots: List = ["dag/test/fixture/module_graph_edge_source"] diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index d10bddfdd77..b5a547b7f80 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -37383,6 +37383,58 @@ mod import_bearing_reference_edges { vec!["v2.std.refinement".to_string()] ); } + + fn homonym_names() -> entry_resolve::ReferencePoolNames { + let mut decl_index = HashMap::new(); + decl_index.insert( + "Present".to_string(), + ["std.optional", "test.fixture.planted"] + .iter() + .map(|s| s.to_string()) + .collect(), + ); + entry_resolve::ReferencePoolNames { + decl_index, + module_names: ["std.optional", "test.fixture.planted"] + .iter() + .map(|s| s.to_string()) + .collect(), + } + } + + fn homonym_edges(src: &str) -> Vec { + let entry_resolve::FileReferenceEdges::Edges(edges) = + entry_resolve::reference_edges_for_file( + "test/claim/a.dag", + Some(src), + &homonym_names(), + ) + else { + panic!("a parsed file has edges"); + }; + edges.into_iter().map(|e| e.target_module).collect() + } + + /// RED (reader defect): `Present` is bound by the file's own `import std.optional { Present }`. + /// A fixture module declaring the same spelling and nearer in the containment tree must not + /// become a phantom dependency of the importer; in a file that imports, a bare name is + /// lexically bound, never resolved by proximity. + #[test] + fn a_bare_name_in_an_import_bearing_file_is_not_resolved_to_a_pool_homonym() { + let src = + "module test.claim.a\nimport std.optional { Present }\nfn g() -> Int { Present }\n"; + let t = homonym_edges(src); + assert!(!t.contains(&"test.fixture.planted".to_string()), "{t:?}"); + } + + /// Control: the same bare name in an IMPORT-LESS file still resolves by proximity (unchanged), + /// and a qualified reference in an import-bearing file is still an edge (the other test). + #[test] + fn the_same_bare_name_without_the_import_still_resolves() { + let src = "module test.claim.a\nfn g() -> Int { Present }\n"; + let t = homonym_edges(src); + assert!(t.contains(&"test.fixture.planted".to_string()), "{t:?}"); + } } #[cfg(test)] diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index e89bef3c60c..035271bbb88 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -3360,7 +3360,12 @@ pub(crate) fn reference_edges_for_file_on_demand< Ok(refs) => refs, Err(cause) => return FileReferenceEdges::Unaccounted(cause), }; - let ParsedFileReferences { bare, chains, .. } = refs; + let ParsedFileReferences { + bare, + chains, + imports, + .. + } = refs; // Resolve to per-file (target_module → strongest confidence). let mut file_edges: std::collections::BTreeMap = std::collections::BTreeMap::new(); @@ -3384,6 +3389,15 @@ pub(crate) fn reference_edges_for_file_on_demand< if super::is_substrate_vocabulary(name) { continue; } + // LEXICAL BINDING, NOT PROXIMITY, IN A FILE THAT IMPORTS: a bare name there is a local + // declaration or a name the file imports (the import edge already carries that), so + // guessing a pool declarer for it is a heuristic the closed substrate never needs. It + // manufactured a phantom edge from `std.optional { Present }` importers to a fixture that + // merely declares the same spelling. The proximity tier below stays for import-less files, + // where it already applied. + if !imports.is_empty() { + continue; + } if let Some(mods) = names.decl_index.get(name) { // Same-module declaration wins by lexical scope (namespace-only): a bare name the // referencing file itself declares resolves LOCALLY — no cross-module edge. This From e78c3b834943c54b1a422804c646ae27cd68ccb5 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 04:32:44 +0000 Subject: [PATCH 6/9] Edge-source witness: construct a dual-backed edge (import plus qualified reference) so the dedup control can red Co-Authored-By: Claude Sonnet 5.5 --- dag/test/claim/module_graph_edge_source_witness_test.dag | 7 ++++++- .../fixture/module_graph_edge_source/consumer_imported.dag | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/dag/test/claim/module_graph_edge_source_witness_test.dag b/dag/test/claim/module_graph_edge_source_witness_test.dag index 5357446b1ee..17f17003a3f 100644 --- a/dag/test/claim/module_graph_edge_source_witness_test.dag +++ b/dag/test/claim/module_graph_edge_source_witness_test.dag @@ -89,9 +89,14 @@ test fn witness_every_producer_row_reaches_the_union() -> Bool { fold(produced, init: true, f: fn(acc, t) { acc && contains_str(xs: edge_targets, want: t) }) } +// consumer_imported imports provider_imported AND reaches it by a qualified reference, so BOTH +// producers carry that edge: the overlap is constructed, not asserted in prose. Deleting the dedup +// makes the union carry it twice and this reds. test fn witness_union_has_no_duplicate_target() -> Bool { let edge_targets = targets_of(edges: fixture_edges()) - count_of(xs: edge_targets, want: imported_provider) == 1 + contains_str(xs: fact_targets(facts: fixture_import_facts()), want: imported_provider) + && contains_str(xs: fact_targets(facts: fixture_reference_facts()), want: imported_provider) + && count_of(xs: edge_targets, want: imported_provider) == 1 && count_of(xs: edge_targets, want: referenced_provider) == 1 } diff --git a/dag/test/fixture/module_graph_edge_source/consumer_imported.dag b/dag/test/fixture/module_graph_edge_source/consumer_imported.dag index 71d17ba1c40..f30558bc599 100644 --- a/dag/test/fixture/module_graph_edge_source/consumer_imported.dag +++ b/dag/test/fixture/module_graph_edge_source/consumer_imported.dag @@ -4,5 +4,5 @@ import std.types { Int } import test.fixture.module_graph_edge_source.provider_imported { edge_source_only_imported_value } fn edge_source_imported_consumer_total() -> Int { - edge_source_only_imported_value() + edge_source_only_imported_value() + test.fixture.module_graph_edge_source.provider_imported.edge_source_only_imported_value() } From e5a16eeaf0f50838ec9d5b8855cd447c4d8c6517 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 04:42:51 +0000 Subject: [PATCH 7/9] Own the emit-only basename beside the runtime prelude; census bound states what an importing module's references are Co-Authored-By: Claude Sonnet 5.5 --- dag/gunbc/rust_emitted_edge.dag | 8 ++++++++ .../lens/manufactured_dependency_census.dag | 20 ++++++++++--------- src/v2/workflow/emitted_crate_workspace.dag | 12 +++-------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/dag/gunbc/rust_emitted_edge.dag b/dag/gunbc/rust_emitted_edge.dag index 69d413308de..7e6cafeaabc 100644 --- a/dag/gunbc/rust_emitted_edge.dag +++ b/dag/gunbc/rust_emitted_edge.dag @@ -52,6 +52,14 @@ fn rust_module_emit_filename(module_name: String) -> String { // the same value. data rust_runtime_prelude_module: String = "v1_rt" +// Files the emitter writes with no .dag module behind them, beside the runtime prelude: +// v1.compiler.emit_rust emit_dry_run_module writes dry_run.rs (the DryRunMode wrapper) whenever the +// closure declares services. The emitter still spells that path itself, so the two are not one +// value; the pairing is the workspace closure check, which refuses an emitted basename that is in +// neither the DAG nor this list, so an emitter that renames or adds such a file is a located +// refusal rather than a silent drift. +data rust_emit_only_basenames: List = ["dry_run"] + fn rust_prelude_emitted_edges(module: String) -> List { if module == rust_runtime_prelude_module { [ diff --git a/src/v2/lens/manufactured_dependency_census.dag b/src/v2/lens/manufactured_dependency_census.dag index 3587eb83123..050063031e3 100644 --- a/src/v2/lens/manufactured_dependency_census.dag +++ b/src/v2/lens/manufactured_dependency_census.dag @@ -28,15 +28,17 @@ import v2.lens.module_graph { // forbids one and is right to; this is a pure reader over the two that module already owns, in the // idiom DESIGN section 6 gives a lens. // -// WHAT THE COUNT IS AND IS NOT, because the honest bound matters more than the number. The census -// counts every module's manufactured edges, import-bearing or not: `src/v2/lens/grounding.dag` has -// three import lines and calls functions it never imports, and those references are edges here. It -// is still a LOWER BOUND on the population that binds without an authored dependency, because it -// counts only what the reference reader collects and resolves to a declared module, and quoting it -// as the population is the denominator error. The upper half needs a per-REFERENCE census on the -// value channel, which does not exist: the mask check that emits `UnlistedImportUse` lives in the -// TYPE resolver (`v1.compiler.infer_resolve` `resolve_node`, keyed on `type_name`), so a bare -// cross-module CALL the reader does not collect produces neither a refusal nor an advisory. +// WHAT THE COUNT IS AND IS NOT, because the honest bound matters more than the number. What it +// counts depends on whether a module imports. An import-LESS module's manufactured edges are every +// reference the reader resolves, qualified or bare. An import-BEARING module's are its qualified +// references only: a bare name there is lexically bound (a local, or an import), so the reader +// resolves no pool declarer for it, and an unimported bare cross-module call in such a module is +// not an edge here. The count is therefore a LOWER BOUND on the population that binds without an +// authored dependency, and quoting it as the population is the denominator error. The upper half +// needs a per-REFERENCE census on the value channel, which does not exist: the mask check that +// emits `UnlistedImportUse` lives in the TYPE resolver (`v1.compiler.infer_resolve` `resolve_node`, +// keyed on `type_name`), so a bare cross-module CALL the reader does not collect produces neither a +// refusal nor an advisory. // // SUBJECT: `gunbc.recurring_failure_mode` // `binding_chosen_by_pool_membership_rather_than_by_the_declared_rule`, whose next-rung trigger diff --git a/src/v2/workflow/emitted_crate_workspace.dag b/src/v2/workflow/emitted_crate_workspace.dag index de14c8e2579..171f33e1c45 100644 --- a/src/v2/workflow/emitted_crate_workspace.dag +++ b/src/v2/workflow/emitted_crate_workspace.dag @@ -20,6 +20,7 @@ import gunbc.rust_emitted_edge { ReexportFacade, SemanticSourceReference, rust_module_emit_filename, + rust_emit_only_basenames, rust_runtime_prelude_module } import gunbc.stage0_crate_partition_generated { @@ -204,13 +205,6 @@ fn emitted_workspace_edges(froms: List, tos: List, provenances: } } -// EMIT-ONLY BASENAMES: files the emitter writes with no .dag module behind them, beside the runtime -// prelude. v1.compiler.emit_rust emit_dry_run_module writes dry_run.rs (the DryRunMode wrapper) -// whenever the closure declares services, so it is never a module of the derived DAG. It is declared -// here, at the authority that owns the closure-agreement exception, rather than filtered out of the -// report; a basename absent from this list and from the DAG still refuses by name. -data emitted_workspace_emit_only_basenames: List = ["dry_run"] - // The derived DAG in BASENAME space, with the runtime module as a node, because that is the space // the emitter's edges and the rendered includes speak. type BasenameDagBuild @@ -223,7 +217,7 @@ fn emitted_workspace_basename_dag(module_dag: ModuleDag, module_by_basename: Map match emitted_workspace_lookup(m: emitted_set, key: rust_module_emit_filename(module_name: m as String)) { Present { value: _ } => false Absent => true }) let outside = emitted_basenames |> filter(b => b != rust_runtime_prelude_module - && !(emitted_workspace_emit_only_basenames |> any(e => e == b)) + && !(rust_emit_only_basenames |> any(e => e == b)) && match emitted_workspace_lookup(m: module_by_basename, key: b) { Present { value: _ } => false Absent => true }) if ((not_emitted |> count) + (outside |> count)) > 0 { BasenameDagRefused { @@ -297,7 +291,7 @@ fn emitted_workspace_runtime_row(emitted_basenames: List) -> GeneratedPa package_name: emitted_workspace_runtime_package, crate_dir: emitted_workspace_crate_dir(package: emitted_workspace_runtime_package), kind: GeneratedFoundationCrate, - modules: concat([rust_runtime_prelude_module], emitted_workspace_emit_only_basenames |> filter(e => emitted_basenames |> any(b => b == e))), + modules: concat([rust_runtime_prelude_module], rust_emit_only_basenames |> filter(e => emitted_basenames |> any(b => b == e))), reexport_packages: [], carries_non_empty_wrappers: true } From a79628262ccf89a92f33c6cb2ac23f579c46e84d Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 04:58:02 +0000 Subject: [PATCH 8/9] Docstring: an import-bearing file's reference-only paths are qualified references only Co-Authored-By: Claude Sonnet 5.5 --- src/v1/stage0/src/cli_run.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index b5a547b7f80..a9bab411344 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -6338,7 +6338,7 @@ impl ModuleGraphFactsLive { /// a stripped (no `import` line) module is otherwise missing from its typed-module content /// key (DESIGN §3: consumes the same `selection_adjacency` authority affected-set selection /// already reads; no second reference-edge producer). For an import-bearing file it - /// returns the modules it reaches by qualified or unique-bare reference WITHOUT importing them; + /// returns the modules it reaches by qualified reference (bare names there are lexically bound) WITHOUT importing them; /// its declared imports are already covered by `resolved.resolved_imports`. /// Workspace-relative repo paths `importer_repo_path` depends on ONLY through a strict-tier /// reference edge (`selection_adjacency` minus `adjacency`). The path-grain authority From 355fdae8eeb2ac8a251705f838da4bf3355d5041 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 07:15:03 +0000 Subject: [PATCH 9/9] Regenerate gunbc_rust_emitted_edge.rs mirror for rust_emit_only_basenames Co-Authored-By: Claude Sonnet 5.5 --- src/v1/stage0/src/gunbc_rust_emitted_edge.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/v1/stage0/src/gunbc_rust_emitted_edge.rs b/src/v1/stage0/src/gunbc_rust_emitted_edge.rs index fcc446709ba..aea43fe5e24 100644 --- a/src/v1/stage0/src/gunbc_rust_emitted_edge.rs +++ b/src/v1/stage0/src/gunbc_rust_emitted_edge.rs @@ -73,6 +73,15 @@ pub fn rust_runtime_prelude_module() -> String { CACHED.with(|c: &String| c.clone()) } +pub fn rust_emit_only_basenames() -> Rc> { + thread_local! { + static CACHED: Rc> = { + Rc::new(vec!["dry_run".to_string()]) + }; + } + CACHED.with(|c: &Rc>| c.clone()) +} + pub fn rust_prelude_emitted_edges(module: String) -> Rc>> { if (module.clone() == rust_runtime_prelude_module()) { Rc::new(vec![Rc::new(EmittedEdge {