From f9847c2b0b001b2108934258207673ab16f68917 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 20 Jun 2026 16:28:28 +0000 Subject: [PATCH 1/8] WIP: Widen dsl_compile_clean to dual-root --- .../claim/dsl_compile_clean_witness_test.dag | 12 ++- .../dsl_compile_clean_cross_tree_probe.dag | 10 ++ dsl/tools/dsl_compile_clean_gate.dag | 5 +- dsl/tools/dsl_compile_clean_transport.dag | 100 +++++++++++++----- 4 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 dsl/test/fixture/dsl_compile_clean_cross_tree_probe.dag diff --git a/dsl/test/claim/dsl_compile_clean_witness_test.dag b/dsl/test/claim/dsl_compile_clean_witness_test.dag index e4c88739182..02811fe77c2 100644 --- a/dsl/test/claim/dsl_compile_clean_witness_test.dag +++ b/dsl/test/claim/dsl_compile_clean_witness_test.dag @@ -1,6 +1,7 @@ // dsl/test/claim/dsl_compile_clean_witness.dag — by-execution +/- witness for dsl compile-clean. // -// GREEN: run_clean_tree_compile() — gunbc compile over live dsl/ exits 0. +// GREEN: run_clean_tree_compile() — gunbc compile over live dsl/ (+ src/v2 pools) exits 0. +// GREEN: run_cross_tree_import_green_receipt() — planted dsl→v2 import resolves and compiles. // RED: run_perturb_optional_skew_red_receipt() — planted Some/None skew fails compile. // // Run: gunbc run --source-root dsl --entry --function dsl_compile_clean_witnesses --claim-run @@ -9,6 +10,7 @@ module test.claim.dsl_compile_clean_witness import tools.dsl_compile_clean_transport { run_clean_tree_compile, + run_cross_tree_import_green_receipt, run_perturb_optional_skew_red_receipt } @@ -16,10 +18,16 @@ fn witness_clean_tree_compile_holds() -> Bool { run_clean_tree_compile() } +fn witness_cross_tree_import_compiles_green() -> Bool { + run_cross_tree_import_green_receipt() +} + fn witness_perturb_optional_skew_goes_red() -> Bool { run_perturb_optional_skew_red_receipt() } test fn dsl_compile_clean_witnesses() -> Bool { - witness_clean_tree_compile_holds() && witness_perturb_optional_skew_goes_red() + witness_clean_tree_compile_holds() + && witness_cross_tree_import_compiles_green() + && witness_perturb_optional_skew_goes_red() } diff --git a/dsl/test/fixture/dsl_compile_clean_cross_tree_probe.dag b/dsl/test/fixture/dsl_compile_clean_cross_tree_probe.dag new file mode 100644 index 00000000000..f3208dee57e --- /dev/null +++ b/dsl/test/fixture/dsl_compile_clean_cross_tree_probe.dag @@ -0,0 +1,10 @@ +// dsl/test/fixture/dsl_compile_clean_cross_tree_probe.dag — minimal dsl-rooted module +// importing v2.*; enrolled in the live dsl/ compile-clean tree when dual-root pools are on. + +module test.fixture.dsl_compile_clean_cross_tree_probe + +import v2.std.logic { Bool } + +fn probe_cross_tree_import() -> Bool { + True +} diff --git a/dsl/tools/dsl_compile_clean_gate.dag b/dsl/tools/dsl_compile_clean_gate.dag index 7610f161735..8115de0a802 100644 --- a/dsl/tools/dsl_compile_clean_gate.dag +++ b/dsl/tools/dsl_compile_clean_gate.dag @@ -1,9 +1,12 @@ -// tools/dsl_compile_clean_gate.dag — whole-tree dsl/ compile-clean gate, run by `gunbc`. +// tools/dsl_compile_clean_gate.dag — whole-tree dsl/ compile-clean gate (dual-root pools), run by `gunbc`. // // Run in CI: // cargo run -p v1-compiler --release --bin gunbc -- run \ // --source-root dsl --entry dsl/tools/dsl_compile_clean_gate.dag --function main // +// Transport compiles every .dag under dsl/ with witness_layer_roots (dsl + src/v2) as import +// pools — see tools.dsl_compile_clean_transport header for indexing semantics. +// // SCAFFOLD — uses `--target rust` pending v1.compiler.dag_collect identity-key fix (180 // collisions on `--target dag` emit); flip to `--target dag` when that backend bug lands. // Parse→resolve→typecheck front-end is identical; rust emit proves the tree is well-typed. diff --git a/dsl/tools/dsl_compile_clean_transport.dag b/dsl/tools/dsl_compile_clean_transport.dag index b39df24c54b..cd65af3d32d 100644 --- a/dsl/tools/dsl_compile_clean_transport.dag +++ b/dsl/tools/dsl_compile_clean_transport.dag @@ -1,8 +1,14 @@ // tools/dsl_compile_clean_transport.dag — shell transport for whole-tree dsl/ compile-clean. // -// Host runs `gunbc compile --source-root --target rust` over the live tree (GREEN) and -// a perturb receipt (RED: planted fresh-name unimported module resolved against the live -// dsl/ index as dependency pool — no cp -r + cold whole-tree rebuild). +// Host runs `gunbc compile` over the live dsl/ tree (GREEN) with witness_layer_roots as the +// dependency pool (§3 single authority: dsl first, src/v2 second). gunbc indexes every .dag +// under all --source-root flags into the module pool, but only compiles entry modules from +// the FIRST root (load_sources / Compile in cli_run.rs); later roots are import pools only — +// adding src/v2 does NOT cold-compile the whole v2 tree, only modules pulled by dsl imports. +// +// Also runs: +// * perturb RED receipt — planted bad module fails typecheck against the live pools; +// * cross-tree GREEN receipt — planted dsl module importing v2.* resolves and compiles. // // Both transports are modeled off hand-written `let script = "..."` blobs onto structured // `ShellProgram`s rendered by `serialize_bash` (extdeps.languages.bash.program). The shared @@ -12,6 +18,7 @@ module tools.dsl_compile_clean_transport import extdeps.shell +import gunbc.ci_layer_roots { witness_layer_roots } import extdeps.languages.bash.program { ShellProgram, ShellStmt, ShellWord, Command, Assign, If, Subshell, AndOr, WithRedir, Heredoc, Exit, @@ -21,7 +28,12 @@ import extdeps.languages.bash.program { } import tools.host_prelude { ensure_gunbc_built } -// set -e; ; OUT=$(mktemp -d); "$GUNBC" compile --source-root "$ROOT"/dsl ... ; rm -rf "$OUT" +// --source-root for every witness_layer_root (§3 — same roots as the rest of CI). +fn dcc_source_root_flags() -> List { + fold(witness_layer_roots, init: [], f: (acc, r) => concat(acc, [lit(text: "--source-root"), lit(text: r)])) +} + +// set -e; ; cd "$ROOT"; OUT=$(mktemp -d); "$GUNBC" compile ... ; rm -rf "$OUT" fn clean_tree_compile_program() -> ShellProgram { let root = var_ref(name: "ROOT") let gunbc = var_ref(name: "GUNBC") @@ -29,13 +41,15 @@ fn clean_tree_compile_program() -> ShellProgram { ShellProgram { set_e: true, statements: concat(ensure_gunbc_built(), [ + command(words: [lit(text: "cd"), root]), assign(name: "OUT", value: CmdSubst { body: command(words: [lit(text: "mktemp"), lit(text: "-d")]) }), - command(words: [ - gunbc, lit(text: "compile"), - lit(text: "--source-root"), Concat { parts: [root, lit(text: "/dsl")] }, - lit(text: "--output-dir"), out, - lit(text: "--target"), lit(text: "rust") - ]), + command(words: concat( + [gunbc, lit(text: "compile")], + concat(dcc_source_root_flags(), [ + lit(text: "--output-dir"), out, + lit(text: "--target"), lit(text: "rust") + ]) + )), command(words: [lit(text: "rm"), lit(text: "-rf"), out]) ]) } @@ -49,15 +63,12 @@ fn run_clean_tree_compile() -> Bool { // The planted module whose legacy Some/None on a builtin T? must fail typecheck. data perturb_module_source: String = "module test.perturb_compile_clean_red\nfn probe() -> String? {\n match Present { value: \"x\" } {\n Some { value: s } => Some { value: s }\n None => none\n }\n}" -// RED receipt: plant the fresh-name unimported bad module; resolve against the live dsl/ -// index (second --source-root) as dependency pool — no cp -r + cold whole-tree rebuild. -// Compile MUST fail (if it succeeds → exit 1). -// set -e; ; TMP=$(mktemp -d) -// cat > "$TMP"/_perturb_compile_clean_red.dag <<'PERTURB' ... PERTURB -// if "$GUNBC" compile --source-root "$TMP" --source-root "$ROOT"/dsl \ -// --output-dir "$TMP"/out --target rust >/dev/null 2>&1 -// then rm -rf "$TMP"; exit 1; fi -// rm -rf "$TMP"; exit 0 +// Planted dsl module that imports v2.* — must compile when pools include src/v2. +data cross_tree_green_module_source: String = "module test.perturb_compile_clean_cross_tree_green\nimport v2.std.logic { Bool }\nfn probe() -> Bool { True }" + +// RED receipt: plant the fresh-name unimported bad module; resolve against witness_layer_roots +// as dependency pools — no cp -r + cold whole-tree rebuild. Compile MUST fail (if it succeeds +// → exit 1). fn compile_clean_perturb_program() -> ShellProgram { let root = var_ref(name: "ROOT") let gunbc = var_ref(name: "GUNBC") @@ -65,6 +76,7 @@ fn compile_clean_perturb_program() -> ShellProgram { ShellProgram { set_e: true, statements: concat(ensure_gunbc_built(), [ + command(words: [lit(text: "cd"), root]), assign(name: "TMP", value: CmdSubst { body: command(words: [lit(text: "mktemp"), lit(text: "-d")]) }), Heredoc { path: Concat { parts: [tmp, lit(text: "/_perturb_compile_clean_red.dag")] }, @@ -73,13 +85,13 @@ fn compile_clean_perturb_program() -> ShellProgram { }, If { cond: WithRedir { - stmt: command(words: [ - gunbc, lit(text: "compile"), - lit(text: "--source-root"), tmp, - lit(text: "--source-root"), Concat { parts: [root, lit(text: "/dsl")] }, - lit(text: "--output-dir"), Concat { parts: [tmp, lit(text: "/out")] }, - lit(text: "--target"), lit(text: "rust") - ]), + stmt: command(words: concat( + [gunbc, lit(text: "compile"), lit(text: "--source-root"), tmp], + concat(dcc_source_root_flags(), [ + lit(text: "--output-dir"), Concat { parts: [tmp, lit(text: "/out")] }, + lit(text: "--target"), lit(text: "rust") + ]) + )), redir: StdoutAndStderrToNull }, then: [ @@ -97,3 +109,39 @@ fn run_perturb_optional_skew_red_receipt() -> Bool { let result = shell.Exec.Run(script: serialize_bash(p: compile_clean_perturb_program())) result.success } + +// GREEN receipt: planted dsl module imports v2.std.logic; first --source-root is TMP (entry +// tree), witness_layer_roots are dependency pools — compile must succeed (set -e). +fn compile_cross_tree_green_program() -> ShellProgram { + let root = var_ref(name: "ROOT") + let gunbc = var_ref(name: "GUNBC") + let tmp = var_ref(name: "TMP") + let out = var_ref(name: "OUT") + ShellProgram { + set_e: true, + statements: concat(ensure_gunbc_built(), [ + command(words: [lit(text: "cd"), root]), + assign(name: "TMP", value: CmdSubst { body: command(words: [lit(text: "mktemp"), lit(text: "-d")]) }), + Heredoc { + path: Concat { parts: [tmp, lit(text: "/_perturb_compile_clean_cross_tree_green.dag")] }, + delimiter: "CROSS_TREE_GREEN", + body: cross_tree_green_module_source + }, + assign(name: "OUT", value: CmdSubst { body: command(words: [lit(text: "mktemp"), lit(text: "-d")]) }), + command(words: concat( + [gunbc, lit(text: "compile"), lit(text: "--source-root"), tmp], + concat(dcc_source_root_flags(), [ + lit(text: "--output-dir"), out, + lit(text: "--target"), lit(text: "rust") + ]) + )), + command(words: [lit(text: "rm"), lit(text: "-rf"), out]), + command(words: [lit(text: "rm"), lit(text: "-rf"), tmp]) + ]) + } +} + +fn run_cross_tree_import_green_receipt() -> Bool { + let result = shell.Exec.Run(script: serialize_bash(p: compile_cross_tree_green_program())) + result.success +} From ee9f7534f637c903078d8c1904a2604c13e10445 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 20 Jun 2026 16:32:56 +0000 Subject: [PATCH 2/8] fix(compile): co-root overlay last-wins in gunbc compile index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gunbc compile must accept witness_layer_roots (dsl + src/v2) without panicking on duplicate module paths — matches cli_run overlay policy so the widened dsl_compile_clean gate can run. Co-authored-by: Cursor --- src/v1/stage0/src/main.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/v1/stage0/src/main.rs b/src/v1/stage0/src/main.rs index 5817bef1c05..55888d77bf4 100644 --- a/src/v1/stage0/src/main.rs +++ b/src/v1/stage0/src/main.rs @@ -113,7 +113,8 @@ fn extract_import_paths(content: &str) -> Vec { } /// Build module index: scan all source roots, map module_path -> file_path. -/// Fail-closed: panics on missing roots, unreadable files, and duplicate module paths. +/// Fail-closed: panics on missing roots and unreadable files. Duplicate module paths +/// across roots use co-root overlay — later `source_roots` win (matches cli_run). fn build_module_index(source_roots: &[String]) -> HashMap { let mut index = HashMap::new(); for root in source_roots { @@ -127,12 +128,6 @@ fn build_module_index(source_roots: &[String]) -> HashMap Date: Sat, 20 Jun 2026 16:47:48 +0000 Subject: [PATCH 3/8] WIP: Widen dsl_compile_clean to dual-root --- .../fixture/dsl_compile_clean_cross_tree_probe.dag | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 dsl/test/fixture/dsl_compile_clean_cross_tree_probe.dag diff --git a/dsl/test/fixture/dsl_compile_clean_cross_tree_probe.dag b/dsl/test/fixture/dsl_compile_clean_cross_tree_probe.dag deleted file mode 100644 index f3208dee57e..00000000000 --- a/dsl/test/fixture/dsl_compile_clean_cross_tree_probe.dag +++ /dev/null @@ -1,10 +0,0 @@ -// dsl/test/fixture/dsl_compile_clean_cross_tree_probe.dag — minimal dsl-rooted module -// importing v2.*; enrolled in the live dsl/ compile-clean tree when dual-root pools are on. - -module test.fixture.dsl_compile_clean_cross_tree_probe - -import v2.std.logic { Bool } - -fn probe_cross_tree_import() -> Bool { - True -} From e9ce5bc4867834a5d3fb807ba27a25c60305a73e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 20 Jun 2026 16:48:06 +0000 Subject: [PATCH 4/8] fix(ci): drop cross-tree fixture that breaks single-root mock corpus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The heredoc cross-tree GREEN receipt in dsl_compile_clean_transport already proves dsl→v2 imports under dual-root pools. A committed dsl/test/fixture module importing v2.std.logic breaks batch-2 discovery-corpus precompute (single-root dsl closure). Co-authored-by: Cursor --- dsl/tools/dsl_compile_clean_transport.dag | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dsl/tools/dsl_compile_clean_transport.dag b/dsl/tools/dsl_compile_clean_transport.dag index cd65af3d32d..02579b71bc8 100644 --- a/dsl/tools/dsl_compile_clean_transport.dag +++ b/dsl/tools/dsl_compile_clean_transport.dag @@ -8,7 +8,8 @@ // // Also runs: // * perturb RED receipt — planted bad module fails typecheck against the live pools; -// * cross-tree GREEN receipt — planted dsl module importing v2.* resolves and compiles. +// * cross-tree GREEN receipt — heredoc-planted dsl module importing v2.* (not a committed +// dsl/ fixture: single-root mock-corpus precompute would fail on v2 imports). // // Both transports are modeled off hand-written `let script = "..."` blobs onto structured // `ShellProgram`s rendered by `serialize_bash` (extdeps.languages.bash.program). The shared From 10674a0bdeb0d96d6ca88bd57828b82947d822aa Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 20 Jun 2026 16:48:22 +0000 Subject: [PATCH 5/8] fix(ci): drop cross-tree fixture that breaks dsl-only mock precompute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The committed probe imported v2.std.logic but discovery-corpus mock precompute indexes dsl/ only — unresolved import + circular dep in batch 2. Cross-tree GREEN proof stays on the transport heredoc receipt + witness. Co-authored-by: Cursor --- dsl/tools/dsl_compile_clean_transport.dag | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dsl/tools/dsl_compile_clean_transport.dag b/dsl/tools/dsl_compile_clean_transport.dag index 02579b71bc8..cd65af3d32d 100644 --- a/dsl/tools/dsl_compile_clean_transport.dag +++ b/dsl/tools/dsl_compile_clean_transport.dag @@ -8,8 +8,7 @@ // // Also runs: // * perturb RED receipt — planted bad module fails typecheck against the live pools; -// * cross-tree GREEN receipt — heredoc-planted dsl module importing v2.* (not a committed -// dsl/ fixture: single-root mock-corpus precompute would fail on v2 imports). +// * cross-tree GREEN receipt — planted dsl module importing v2.* resolves and compiles. // // Both transports are modeled off hand-written `let script = "..."` blobs onto structured // `ShellProgram`s rendered by `serialize_bash` (extdeps.languages.bash.program). The shared From a2065192c87b4f12dcfba895e62a7d24fb15ad86 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 20 Jun 2026 17:06:24 +0000 Subject: [PATCH 6/8] WIP: Widen dsl_compile_clean to dual-root --- dsl/tools/dsl_compile_clean_transport.dag | 16 +-- src/v1/stage0/src/main.rs | 113 +++++++++++++++++++--- 2 files changed, 107 insertions(+), 22 deletions(-) diff --git a/dsl/tools/dsl_compile_clean_transport.dag b/dsl/tools/dsl_compile_clean_transport.dag index cd65af3d32d..6fea27e5dc6 100644 --- a/dsl/tools/dsl_compile_clean_transport.dag +++ b/dsl/tools/dsl_compile_clean_transport.dag @@ -33,6 +33,10 @@ fn dcc_source_root_flags() -> List { fold(witness_layer_roots, init: [], f: (acc, r) => concat(acc, [lit(text: "--source-root"), lit(text: r)])) } +fn dcc_pool_index_flags() -> List { + [lit(text: "--dependency-pool-index"), lit(text: "primary-precedence")] +} + // set -e; ; cd "$ROOT"; OUT=$(mktemp -d); "$GUNBC" compile ... ; rm -rf "$OUT" fn clean_tree_compile_program() -> ShellProgram { let root = var_ref(name: "ROOT") @@ -45,10 +49,10 @@ fn clean_tree_compile_program() -> ShellProgram { assign(name: "OUT", value: CmdSubst { body: command(words: [lit(text: "mktemp"), lit(text: "-d")]) }), command(words: concat( [gunbc, lit(text: "compile")], - concat(dcc_source_root_flags(), [ + concat(dcc_source_root_flags(), concat(dcc_pool_index_flags(), [ lit(text: "--output-dir"), out, lit(text: "--target"), lit(text: "rust") - ]) + ])) )), command(words: [lit(text: "rm"), lit(text: "-rf"), out]) ]) @@ -87,10 +91,10 @@ fn compile_clean_perturb_program() -> ShellProgram { cond: WithRedir { stmt: command(words: concat( [gunbc, lit(text: "compile"), lit(text: "--source-root"), tmp], - concat(dcc_source_root_flags(), [ + concat(dcc_source_root_flags(), concat(dcc_pool_index_flags(), [ lit(text: "--output-dir"), Concat { parts: [tmp, lit(text: "/out")] }, lit(text: "--target"), lit(text: "rust") - ]) + ])) )), redir: StdoutAndStderrToNull }, @@ -130,10 +134,10 @@ fn compile_cross_tree_green_program() -> ShellProgram { assign(name: "OUT", value: CmdSubst { body: command(words: [lit(text: "mktemp"), lit(text: "-d")]) }), command(words: concat( [gunbc, lit(text: "compile"), lit(text: "--source-root"), tmp], - concat(dcc_source_root_flags(), [ + concat(dcc_source_root_flags(), concat(dcc_pool_index_flags(), [ lit(text: "--output-dir"), out, lit(text: "--target"), lit(text: "rust") - ]) + ])) )), command(words: [lit(text: "rm"), lit(text: "-rf"), out]), command(words: [lit(text: "rm"), lit(text: "-rf"), tmp]) diff --git a/src/v1/stage0/src/main.rs b/src/v1/stage0/src/main.rs index 55888d77bf4..ad3d1d496db 100644 --- a/src/v1/stage0/src/main.rs +++ b/src/v1/stage0/src/main.rs @@ -43,6 +43,11 @@ enum Commands { /// Target language: rust, python, go, dag, or + separated set such as rust+dag #[arg(long, default_value = "rust")] target: String, + /// Multi-root index policy: `strict` (default) panics on any cross-root duplicate; + /// `primary-precedence` keeps the first root authoritative and fills only absent paths + /// from later dependency-pool roots (opt-in for dual-root dsl compile-clean gate). + #[arg(long = "dependency-pool-index", default_value = "strict")] + dependency_pool_index: String, }, /// Run repo CI from CiSpec (delegates to dsl/tools/gunbc_ci.dag) Ci, @@ -112,23 +117,97 @@ fn extract_import_paths(content: &str) -> Vec { imports } -/// Build module index: scan all source roots, map module_path -> file_path. -/// Fail-closed: panics on missing roots and unreadable files. Duplicate module paths -/// across roots use co-root overlay — later `source_roots` win (matches cli_run). -fn build_module_index(source_roots: &[String]) -> HashMap { +#[derive(Clone, Copy, PartialEq, Eq)] +enum DependencyPoolIndex { + Strict, + PrimaryPrecedence, +} + +fn parse_dependency_pool_index(value: &str) -> DependencyPoolIndex { + match value { + "strict" => DependencyPoolIndex::Strict, + "primary-precedence" => DependencyPoolIndex::PrimaryPrecedence, + other => { + eprintln!( + "unknown --dependency-pool-index: {} (expected strict | primary-precedence)", + other + ); + std::process::exit(1); + } + } +} + +fn insert_module_path( + index: &mut HashMap, + module_path: String, + path: std::path::PathBuf, + within_root: &mut HashMap, +) { + if let Some(existing) = within_root.get(&module_path) { + panic!( + "duplicate module path '{}' within source root: declared in both {:?} and {:?}", + module_path, existing, path + ); + } + within_root.insert(module_path.clone(), path.clone()); + index.insert(module_path, path); +} + +/// Index one source root. `pool_fill_only`: only insert paths absent from `index` (pool roots). +fn index_source_root( + root: &str, + index: &mut HashMap, + pool_fill_only: bool, +) { + let root_path = std::path::Path::new(root); + if !root_path.exists() { + panic!("source root does not exist: {}", root); + } + let mut dag_files = Vec::new(); + collect_dag_files(root_path, &mut dag_files); + let mut within_root = HashMap::new(); + for path in dag_files { + let content = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("failed to read {:?}: {}", path, e)); + if let Some(module_path) = extract_module_path(&content) { + if pool_fill_only { + if index.contains_key(&module_path) { + continue; + } + insert_module_path(index, module_path, path, &mut within_root); + } else if let Some(existing) = index.get(&module_path) { + panic!( + "duplicate module path '{}': declared in both {:?} and {:?}", + module_path, existing, path + ); + } else { + insert_module_path(index, module_path, path, &mut within_root); + } + } + } +} + +/// Build module index: scan source roots, map module_path -> file_path. +/// Fail-closed: panics on missing roots, unreadable files, and within-root duplicates. +/// Cross-root policy is opt-in via `pool_index` (default `Strict` preserves legacy behavior). +fn build_module_index( + source_roots: &[String], + pool_index: DependencyPoolIndex, +) -> HashMap { let mut index = HashMap::new(); - for root in source_roots { - let root_path = std::path::Path::new(root); - if !root_path.exists() { - panic!("source root does not exist: {}", root); + if source_roots.is_empty() { + return index; + } + match pool_index { + DependencyPoolIndex::Strict => { + for root in source_roots { + index_source_root(root, &mut index, false); + } } - let mut dag_files = Vec::new(); - collect_dag_files(root_path, &mut dag_files); - for path in dag_files { - let content = std::fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("failed to read {:?}: {}", path, e)); - if let Some(module_path) = extract_module_path(&content) { - index.insert(module_path, path); + DependencyPoolIndex::PrimaryPrecedence => { + index_source_root(&source_roots[0], &mut index, false); + for root in &source_roots[1..] { + index_source_root(root, &mut index, true); } } } @@ -244,12 +323,14 @@ fn main() { source_dir, output_dir, target, + dependency_pool_index, } => { let render_targets = parse_render_targets(&target); + let pool_index = parse_dependency_pool_index(&dependency_pool_index); let sources = if !source_roots.is_empty() { // FF-9: Import-driven resolution from source roots - let index = build_module_index(&source_roots); + let index = build_module_index(&source_roots, pool_index); eprintln!( "indexed {} modules from {} source roots", index.len(), From 71baa4ca14c3b431e1c38d5f3a9a56811445ae75 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 20 Jun 2026 17:41:25 +0000 Subject: [PATCH 7/8] WIP: Widen dsl_compile_clean to dual-root --- .../claim/dsl_compile_clean_witness_test.dag | 7 + dsl/tools/dsl_compile_clean_transport.dag | 44 +++++- .../src/dependency_pool_index_compile_test.rs | 134 ++++++++++++++++++ src/v1/tests/src/lib.rs | 2 + 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/v1/tests/src/dependency_pool_index_compile_test.rs diff --git a/dsl/test/claim/dsl_compile_clean_witness_test.dag b/dsl/test/claim/dsl_compile_clean_witness_test.dag index 02811fe77c2..11ef2de410e 100644 --- a/dsl/test/claim/dsl_compile_clean_witness_test.dag +++ b/dsl/test/claim/dsl_compile_clean_witness_test.dag @@ -2,6 +2,7 @@ // // GREEN: run_clean_tree_compile() — gunbc compile over live dsl/ (+ src/v2 pools) exits 0. // GREEN: run_cross_tree_import_green_receipt() — planted dsl→v2 import resolves and compiles. +// GREEN: run_dsl_shell_primary_precedence_receipt() — dsl extdeps.shell (shell.Env) wins over v2 overlay. // RED: run_perturb_optional_skew_red_receipt() — planted Some/None skew fails compile. // // Run: gunbc run --source-root dsl --entry --function dsl_compile_clean_witnesses --claim-run @@ -11,6 +12,7 @@ module test.claim.dsl_compile_clean_witness import tools.dsl_compile_clean_transport { run_clean_tree_compile, run_cross_tree_import_green_receipt, + run_dsl_shell_primary_precedence_receipt, run_perturb_optional_skew_red_receipt } @@ -22,6 +24,10 @@ fn witness_cross_tree_import_compiles_green() -> Bool { run_cross_tree_import_green_receipt() } +fn witness_dsl_shell_primary_precedence_holds() -> Bool { + run_dsl_shell_primary_precedence_receipt() +} + fn witness_perturb_optional_skew_goes_red() -> Bool { run_perturb_optional_skew_red_receipt() } @@ -29,5 +35,6 @@ fn witness_perturb_optional_skew_goes_red() -> Bool { test fn dsl_compile_clean_witnesses() -> Bool { witness_clean_tree_compile_holds() && witness_cross_tree_import_compiles_green() + && witness_dsl_shell_primary_precedence_holds() && witness_perturb_optional_skew_goes_red() } diff --git a/dsl/tools/dsl_compile_clean_transport.dag b/dsl/tools/dsl_compile_clean_transport.dag index 6fea27e5dc6..07a77c14504 100644 --- a/dsl/tools/dsl_compile_clean_transport.dag +++ b/dsl/tools/dsl_compile_clean_transport.dag @@ -1,7 +1,9 @@ // tools/dsl_compile_clean_transport.dag — shell transport for whole-tree dsl/ compile-clean. // // Host runs `gunbc compile` over the live dsl/ tree (GREEN) with witness_layer_roots as the -// dependency pool (§3 single authority: dsl first, src/v2 second). gunbc indexes every .dag +// dependency pool (§3 single authority: dsl first, src/v2 second) and +// `--dependency-pool-index primary-precedence` (primary root authoritative; pool fills absent +// paths only — NOT global last-wins). gunbc indexes every .dag under all --source-root flags // under all --source-root flags into the module pool, but only compiles entry modules from // the FIRST root (load_sources / Compile in cli_run.rs); later roots are import pools only — // adding src/v2 does NOT cold-compile the whole v2 tree, only modules pulled by dsl imports. @@ -70,6 +72,10 @@ data perturb_module_source: String = "module test.perturb_compile_clean_red\nfn // Planted dsl module that imports v2.* — must compile when pools include src/v2. data cross_tree_green_module_source: String = "module test.perturb_compile_clean_cross_tree_green\nimport v2.std.logic { Bool }\nfn probe() -> Bool { True }" +// Discriminating primary-precedence receipt: shell.Env exists only in dsl extdeps.shell (~101 lines); +// v2 overlay (~34 lines) has shell.Exec only — last-wins would misresolve and fail typecheck. +data dsl_shell_primary_module_source: String = "module test.perturb_compile_clean_dsl_shell_primary\nimport extdeps.shell\nfn probe() -> String? {\n shell.Env.Get(name: \"PATH\")\n}" + // RED receipt: plant the fresh-name unimported bad module; resolve against witness_layer_roots // as dependency pools — no cp -r + cold whole-tree rebuild. Compile MUST fail (if it succeeds // → exit 1). @@ -149,3 +155,39 @@ fn run_cross_tree_import_green_receipt() -> Bool { let result = shell.Exec.Run(script: serialize_bash(p: compile_cross_tree_green_program())) result.success } + +// GREEN receipt: primary root entry calls shell.Env (dsl-only service); pools supply v2 paths +// absent from dsl — extdeps.shell must stay on the dsl row, not the v2 overlay. +fn compile_dsl_shell_primary_precedence_program() -> ShellProgram { + let root = var_ref(name: "ROOT") + let gunbc = var_ref(name: "GUNBC") + let tmp = var_ref(name: "TMP") + let out = var_ref(name: "OUT") + ShellProgram { + set_e: true, + statements: concat(ensure_gunbc_built(), [ + command(words: [lit(text: "cd"), root]), + assign(name: "TMP", value: CmdSubst { body: command(words: [lit(text: "mktemp"), lit(text: "-d")]) }), + Heredoc { + path: Concat { parts: [tmp, lit(text: "/_perturb_compile_clean_dsl_shell_primary.dag")] }, + delimiter: "DSL_SHELL_PRIMARY", + body: dsl_shell_primary_module_source + }, + assign(name: "OUT", value: CmdSubst { body: command(words: [lit(text: "mktemp"), lit(text: "-d")]) }), + command(words: concat( + [gunbc, lit(text: "compile"), lit(text: "--source-root"), tmp], + concat(dcc_source_root_flags(), concat(dcc_pool_index_flags(), [ + lit(text: "--output-dir"), out, + lit(text: "--target"), lit(text: "rust") + ])) + )), + command(words: [lit(text: "rm"), lit(text: "-rf"), out]), + command(words: [lit(text: "rm"), lit(text: "-rf"), tmp]) + ]) + } +} + +fn run_dsl_shell_primary_precedence_receipt() -> Bool { + let result = shell.Exec.Run(script: serialize_bash(p: compile_dsl_shell_primary_precedence_program())) + result.success +} diff --git a/src/v1/tests/src/dependency_pool_index_compile_test.rs b/src/v1/tests/src/dependency_pool_index_compile_test.rs new file mode 100644 index 00000000000..991998c8d3a --- /dev/null +++ b/src/v1/tests/src/dependency_pool_index_compile_test.rs @@ -0,0 +1,134 @@ +//! Execution receipts for `gunbc compile --dependency-pool-index` (primary-precedence gate path). + +use crate::helpers::workspace_root; +use std::fs; +use std::process::Command; + +fn gunbc_bin() -> Option { + let gunbc = workspace_root().join("target/release/gunbc"); + gunbc.is_file().then_some(gunbc) +} + +fn compile_with_roots( + gunbc: &std::path::Path, + ws: &std::path::Path, + source_roots: &[&std::path::Path], + pool_index: &str, + output_dir: &std::path::Path, +) -> std::process::Output { + let mut cmd = Command::new(gunbc); + cmd.arg("compile") + .arg("--dependency-pool-index") + .arg(pool_index) + .arg("--output-dir") + .arg(output_dir) + .arg("--target") + .arg("rust") + .current_dir(ws); + for root in source_roots { + cmd.arg("--source-root").arg(root); + } + cmd.output().expect("spawn gunbc compile") +} + +#[test] +fn primary_precedence_within_root_duplicate_module_path_panics() { + let Some(gunbc) = gunbc_bin() else { + eprintln!("skipping: release gunbc binary not found"); + return; + }; + let ws = workspace_root(); + let tmp = tempfile::tempdir().expect("tempdir"); + let root = tmp.path(); + fs::write( + root.join("a.dag"), + "module duplicate.within.root\nfn a() -> Int { 1 }\n", + ) + .expect("write a.dag"); + fs::write( + root.join("b.dag"), + "module duplicate.within.root\nfn b() -> Int { 2 }\n", + ) + .expect("write b.dag"); + let out = tempfile::tempdir().expect("out dir"); + let output = compile_with_roots(&gunbc, &ws, &[root], "primary-precedence", out.path()); + let combined = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + !output.status.success(), + "expected within-root duplicate to fail closed; got success" + ); + assert!( + combined.contains("duplicate module path") && combined.contains("within source root"), + "expected within-root duplicate panic message; got:\n{combined}" + ); +} + +#[test] +fn strict_dependency_pool_index_panics_on_cross_root_extdeps_shell_collision() { + let Some(gunbc) = gunbc_bin() else { + eprintln!("skipping: release gunbc binary not found"); + return; + }; + let ws = workspace_root(); + let out = tempfile::tempdir().expect("out dir"); + let output = compile_with_roots( + &gunbc, + &ws, + &[&ws.join("dsl"), &ws.join("src/v2")], + "strict", + out.path(), + ); + let combined = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + !output.status.success(), + "strict mode must panic on cross-root extdeps.shell collision" + ); + assert!( + combined.contains("extdeps.shell"), + "expected extdeps.shell collision diagnostic; got:\n{combined}" + ); +} + +#[test] +fn primary_precedence_keeps_dsl_extdeps_shell_for_dsl_only_services() { + let Some(gunbc) = gunbc_bin() else { + eprintln!("skipping: release gunbc binary not found"); + return; + }; + let ws = workspace_root(); + let tmp = tempfile::tempdir().expect("tempdir"); + let entry_root = tmp.path(); + fs::write( + entry_root.join("probe.dag"), + "module test.probe.dsl_shell_env_primary\n\ + import extdeps.shell\n\ + fn probe() -> String? {\n\ + shell.Env.Get(name: \"PATH\")\n\ + }\n", + ) + .expect("write probe.dag"); + let out = tempfile::tempdir().expect("out dir"); + let output = compile_with_roots( + &gunbc, + &ws, + &[entry_root, &ws.join("dsl"), &ws.join("src/v2")], + "primary-precedence", + out.path(), + ); + if !output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + panic!( + "shell.Env.Get probe must compile under primary-precedence (dsl extdeps.shell wins over v2 overlay); exit {:?}\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}", + output.status.code() + ); + } +} diff --git a/src/v1/tests/src/lib.rs b/src/v1/tests/src/lib.rs index b6f99a0b3c2..80626ef7957 100644 --- a/src/v1/tests/src/lib.rs +++ b/src/v1/tests/src/lib.rs @@ -30,6 +30,8 @@ mod cron_tag_test; #[cfg(test)] mod data_cache_scoping_test; #[cfg(test)] +mod dependency_pool_index_compile_test; +#[cfg(test)] mod derive_bound_fail_closed_test; #[cfg(test)] mod diagnostics; From 14eb5bed0e3bb7b28ee40952eaa9361b3aca367f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 20 Jun 2026 17:46:26 +0000 Subject: [PATCH 8/8] fix(compile): primary-precedence pool within-root dup panics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pool-root indexing must panic on duplicate module paths within the same root before skipping paths already owned by the primary root — silent continue masked within-pool dups. Add execution receipts: Rust tests for strict vs primary-precedence and pool within-root panic; dsl witness arm for shell.Env (dsl-only extdeps.shell surface). Co-authored-by: Cursor --- src/v1/stage0/src/main.rs | 6 ++ .../src/dependency_pool_index_compile_test.rs | 91 ++++++++++++++++--- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/v1/stage0/src/main.rs b/src/v1/stage0/src/main.rs index ad3d1d496db..96d03d5c8f5 100644 --- a/src/v1/stage0/src/main.rs +++ b/src/v1/stage0/src/main.rs @@ -171,6 +171,12 @@ fn index_source_root( .unwrap_or_else(|e| panic!("failed to read {:?}: {}", path, e)); if let Some(module_path) = extract_module_path(&content) { if pool_fill_only { + if let Some(existing) = within_root.get(&module_path) { + panic!( + "duplicate module path '{}' within source root: declared in both {:?} and {:?}", + module_path, existing, path + ); + } if index.contains_key(&module_path) { continue; } diff --git a/src/v1/tests/src/dependency_pool_index_compile_test.rs b/src/v1/tests/src/dependency_pool_index_compile_test.rs index 991998c8d3a..c6c6c678fdf 100644 --- a/src/v1/tests/src/dependency_pool_index_compile_test.rs +++ b/src/v1/tests/src/dependency_pool_index_compile_test.rs @@ -3,12 +3,29 @@ use crate::helpers::workspace_root; use std::fs; use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; fn gunbc_bin() -> Option { let gunbc = workspace_root().join("target/release/gunbc"); gunbc.is_file().then_some(gunbc) } +fn temp_dir(label: &str) -> std::path::PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock") + .as_nanos(); + let path = std::env::temp_dir().join(format!("gunbc-{label}-{stamp}")); + fs::create_dir_all(&path).expect("create temp dir"); + path +} + +fn rm_rf(path: &std::path::Path) { + if path.is_dir() { + fs::remove_dir_all(path).expect("remove temp dir"); + } +} + fn compile_with_roots( gunbc: &std::path::Path, ws: &std::path::Path, @@ -38,8 +55,7 @@ fn primary_precedence_within_root_duplicate_module_path_panics() { return; }; let ws = workspace_root(); - let tmp = tempfile::tempdir().expect("tempdir"); - let root = tmp.path(); + let root = temp_dir("within-root-dup"); fs::write( root.join("a.dag"), "module duplicate.within.root\nfn a() -> Int { 1 }\n", @@ -50,8 +66,8 @@ fn primary_precedence_within_root_duplicate_module_path_panics() { "module duplicate.within.root\nfn b() -> Int { 2 }\n", ) .expect("write b.dag"); - let out = tempfile::tempdir().expect("out dir"); - let output = compile_with_roots(&gunbc, &ws, &[root], "primary-precedence", out.path()); + let out = temp_dir("within-root-dup-out"); + let output = compile_with_roots(&gunbc, &ws, &[&root], "primary-precedence", &out); let combined = format!( "{}{}", String::from_utf8_lossy(&output.stdout), @@ -61,10 +77,59 @@ fn primary_precedence_within_root_duplicate_module_path_panics() { !output.status.success(), "expected within-root duplicate to fail closed; got success" ); + assert!( + combined.contains("duplicate module path"), + "expected duplicate module path panic; got:\n{combined}" + ); + rm_rf(&root); + rm_rf(&out); +} + +#[test] +fn primary_precedence_pool_root_within_root_duplicate_module_path_panics() { + let Some(gunbc) = gunbc_bin() else { + eprintln!("skipping: release gunbc binary not found"); + return; + }; + let ws = workspace_root(); + let primary = temp_dir("pool-dup-primary"); + fs::write( + primary.join("primary.dag"), + "module primary.only\nfn ok() -> Int { 1 }\n", + ) + .expect("write primary.dag"); + let pool = temp_dir("pool-dup-pool"); + fs::write( + pool.join("a.dag"), + "module duplicate.pool.root\nfn a() -> Int { 1 }\n", + ) + .expect("write pool a.dag"); + fs::write( + pool.join("b.dag"), + "module duplicate.pool.root\nfn b() -> Int { 2 }\n", + ) + .expect("write pool b.dag"); + let out = temp_dir("pool-dup-out"); + let output = compile_with_roots( + &gunbc, + &ws, + &[&primary, &pool], + "primary-precedence", + &out, + ); + let combined = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(!output.status.success()); assert!( combined.contains("duplicate module path") && combined.contains("within source root"), - "expected within-root duplicate panic message; got:\n{combined}" + "expected pool-root within-root duplicate panic; got:\n{combined}" ); + rm_rf(&primary); + rm_rf(&pool); + rm_rf(&out); } #[test] @@ -74,13 +139,13 @@ fn strict_dependency_pool_index_panics_on_cross_root_extdeps_shell_collision() { return; }; let ws = workspace_root(); - let out = tempfile::tempdir().expect("out dir"); + let out = temp_dir("strict-cross-root-out"); let output = compile_with_roots( &gunbc, &ws, &[&ws.join("dsl"), &ws.join("src/v2")], "strict", - out.path(), + &out, ); let combined = format!( "{}{}", @@ -95,6 +160,7 @@ fn strict_dependency_pool_index_panics_on_cross_root_extdeps_shell_collision() { combined.contains("extdeps.shell"), "expected extdeps.shell collision diagnostic; got:\n{combined}" ); + rm_rf(&out); } #[test] @@ -104,8 +170,7 @@ fn primary_precedence_keeps_dsl_extdeps_shell_for_dsl_only_services() { return; }; let ws = workspace_root(); - let tmp = tempfile::tempdir().expect("tempdir"); - let entry_root = tmp.path(); + let entry_root = temp_dir("dsl-shell-primary-entry"); fs::write( entry_root.join("probe.dag"), "module test.probe.dsl_shell_env_primary\n\ @@ -115,13 +180,13 @@ fn primary_precedence_keeps_dsl_extdeps_shell_for_dsl_only_services() { }\n", ) .expect("write probe.dag"); - let out = tempfile::tempdir().expect("out dir"); + let out = temp_dir("dsl-shell-primary-out"); let output = compile_with_roots( &gunbc, &ws, - &[entry_root, &ws.join("dsl"), &ws.join("src/v2")], + &[&entry_root, &ws.join("dsl"), &ws.join("src/v2")], "primary-precedence", - out.path(), + &out, ); if !output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); @@ -131,4 +196,6 @@ fn primary_precedence_keeps_dsl_extdeps_shell_for_dsl_only_services() { output.status.code() ); } + rm_rf(&entry_root); + rm_rf(&out); }