From c7d1e8e90839578e7e114a0e57cb0beeec19eada Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 03:13:40 -0400 Subject: [PATCH 01/24] WIP: SG-6 --- src/v3/compiler/regen.dag | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/v3/compiler/regen.dag diff --git a/src/v3/compiler/regen.dag b/src/v3/compiler/regen.dag new file mode 100644 index 00000000000..3291e6c3786 --- /dev/null +++ b/src/v3/compiler/regen.dag @@ -0,0 +1,50 @@ +module v3.compiler.regen + +// Lens regen registry. Each `data _entry: LensRegistryEntry` +// record below is picked up structurally by the `regen_lens` binary +// (`dag.declarations().filter(meta_tag == LensRegistryEntry)`), which +// then reads `lens_file`, emits the Rust projection via +// `emit_rust_module`, and writes to `generated_file`. Mirrors the +// `PipelineStageBinding` pattern in `src/v3/compiler/pipeline.dag`. +// +// Adding a new generated lens is a pure `.dag` edit: drop the lens +// source in `src/v3/lenses/`, add a `data ..._entry: LensRegistryEntry` +// record here with the three paths filled in, and +// `cargo run -p v3-compiler --bin regen_lens` picks it up. No +// per-lens Rust driver is added or edited. + +type LensRegistryEntry { + name: String + lens_file: String + generated_file: String +} + +data lens_cost_entry: LensRegistryEntry = { + name: "cost" + lens_file: "src/v3/lenses/complexity.dag" + generated_file: "src/v3/compiler/src/lens_cost_generated.rs" +} + +data lens_cost_symbolic_entry: LensRegistryEntry = { + name: "cost_symbolic" + lens_file: "src/v3/lenses/cost.dag" + generated_file: "src/v3/compiler/src/lens_cost_symbolic_generated.rs" +} + +data lens_provenance_entry: LensRegistryEntry = { + name: "provenance" + lens_file: "src/v3/lenses/provenance.dag" + generated_file: "src/v3/compiler/src/lens_provenance_generated.rs" +} + +data lens_structural_resolution_entry: LensRegistryEntry = { + name: "structural_resolution" + lens_file: "src/v3/lenses/structural_resolution.dag" + generated_file: "src/v3/compiler/src/lens_structural_resolution_generated.rs" +} + +data lens_unused_parameters_entry: LensRegistryEntry = { + name: "unused_parameters" + lens_file: "src/v3/lenses/unused_parameters.dag" + generated_file: "src/v3/compiler/src/lens_unused_parameters_generated.rs" +} From 6a80ac3173a42048d7393e1a3d7b9abeea63033d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 03:15:20 -0400 Subject: [PATCH 02/24] WIP: SG-6 --- src/v3/compiler/src/bin/regen_lens.rs | 196 ++++++++++++++++++ src/v3/compiler/src/bin/regen_lens_cost.rs | 50 ----- .../src/bin/regen_lens_cost_symbolic.rs | 56 ----- .../bin/regen_lens_structural_resolution.rs | 50 ----- .../src/bin/regen_lens_unused_parameters.rs | 50 ----- .../lane2_stage_2d_symbolic_cost_test.rs | 2 +- ...ns_structural_resolution_migration_test.rs | 7 +- 7 files changed, 200 insertions(+), 211 deletions(-) create mode 100644 src/v3/compiler/src/bin/regen_lens.rs delete mode 100644 src/v3/compiler/src/bin/regen_lens_cost.rs delete mode 100644 src/v3/compiler/src/bin/regen_lens_cost_symbolic.rs delete mode 100644 src/v3/compiler/src/bin/regen_lens_structural_resolution.rs delete mode 100644 src/v3/compiler/src/bin/regen_lens_unused_parameters.rs diff --git a/src/v3/compiler/src/bin/regen_lens.rs b/src/v3/compiler/src/bin/regen_lens.rs new file mode 100644 index 00000000000..7e7afc65845 --- /dev/null +++ b/src/v3/compiler/src/bin/regen_lens.rs @@ -0,0 +1,196 @@ +// Unified lens-regen driver. Narrow host shim for +// `src/v3/compiler/regen.dag`: reads every +// `data _entry: LensRegistryEntry` record out of the bootstrap +// Dag, compiles the referenced `.dag` lens, and writes the +// `emit_rust_module` projection to the declared output path. Adding +// a new lens is an edit to `regen.dag`, not to this driver. +// +// Usage: +// cargo run -p v3-compiler --bin regen_lens +// → regenerates every lens in the registry. +// cargo run -p v3-compiler --bin regen_lens -- --lens cost +// → regenerates only the entry whose `name` field is "cost". + +use std::env; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitCode, Stdio}; + +use v3_compiler::compile_to_dag; +use v3_compiler::dag::{Dag, FieldValue, LiteralBits, ValueBody}; +use v3_compiler::emit_rust::emit_rust_module; + +const LENS_REGISTRY_ENTRY_TYPE: &str = "LensRegistryEntry"; + +struct Entry { + name: String, + lens_file: String, + generated_file: String, +} + +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(message) => { + let _ = writeln!(io::stderr(), "{message}"); + ExitCode::FAILURE + } + } +} + +fn run() -> Result<(), String> { + let requested_name = parse_args()?; + + let dag = Dag::new(); + if !dag.diagnostics().is_empty() { + return Err(format!( + "bootstrap Dag carries {} diagnostic(s): {:#?}", + dag.diagnostics().len(), + dag.diagnostics() + )); + } + + let entries = read_registry(&dag)?; + if entries.is_empty() { + return Err("lens registry is empty; check `src/v3/compiler/regen.dag`".to_string()); + } + + let root = workspace_root(); + let mut processed = 0usize; + for entry in &entries { + if let Some(requested) = &requested_name { + if entry.name != *requested { + continue; + } + } + regen_entry(&root, entry)?; + processed += 1; + } + + if processed == 0 { + let known: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); + return Err(format!( + "no lens entry matches `--lens {}`; known: {}", + requested_name.unwrap_or_default(), + known.join(", ") + )); + } + Ok(()) +} + +fn parse_args() -> Result, String> { + let mut requested: Option = None; + let mut args = env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--lens" => { + requested = Some( + args.next() + .ok_or_else(|| "--lens requires a name".to_string())?, + ); + } + other => return Err(format!("unknown argument `{other}`")), + } + } + Ok(requested) +} + +fn workspace_root() -> PathBuf { + // src/v3/compiler/Cargo.toml lives 3 levels below the workspace + // root; the registry entries record paths relative to that root. + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("..") +} + +fn read_registry(dag: &Dag) -> Result, String> { + let entry_type_id = dag + .declaration_by_name(LENS_REGISTRY_ENTRY_TYPE) + .map(|decl| decl.id) + .ok_or_else(|| format!("missing `{LENS_REGISTRY_ENTRY_TYPE}` in bootstrap Dag"))?; + + let mut entries = Vec::new(); + for decl in dag.declarations() { + if decl.meta_tag != Some(entry_type_id) { + continue; + } + let binding_name = decl.name.as_deref().unwrap_or(""); + let Some(ValueBody::Structural { fields }) = &decl.value_body else { + return Err(format!( + "lens registry entry `{binding_name}` must carry a structural value body" + )); + }; + entries.push(Entry { + name: require_string(fields, "name", binding_name)?, + lens_file: require_string(fields, "lens_file", binding_name)?, + generated_file: require_string(fields, "generated_file", binding_name)?, + }); + } + Ok(entries) +} + +fn require_string( + fields: &[(String, FieldValue)], + label: &str, + binding_name: &str, +) -> Result { + fields + .iter() + .find(|(field_label, _)| field_label == label) + .and_then(|(_, value)| match value { + FieldValue::Literal(LiteralBits::String(s)) => Some(s.clone()), + _ => None, + }) + .ok_or_else(|| { + format!("lens registry entry `{binding_name}` is missing String field `{label}`") + }) +} + +fn regen_entry(root: &Path, entry: &Entry) -> Result<(), String> { + let lens_path = root.join(&entry.lens_file); + let out_path = root.join(&entry.generated_file); + + let source = std::fs::read_to_string(&lens_path) + .map_err(|e| format!("read {}: {e}", lens_path.display()))?; + let dag = compile_to_dag(&source, &entry.lens_file) + .map_err(|e| format!("compile {}: {e:?}", entry.lens_file))?; + let raw = emit_rust_module(&dag).map_err(|e| format!("emit {}: {e:?}", entry.lens_file))?; + let header = format!( + "// AUTO-GENERATED from `{}` via\n\ + // `emit_rust_module`. Regenerate instead of hand-editing.\n\n", + entry.lens_file + ); + let combined = format!("{header}{raw}"); + let formatted = rustfmt_stdin(&combined)?; + std::fs::write(&out_path, &formatted) + .map_err(|e| format!("write {}: {e}", out_path.display()))?; + println!("wrote {}", out_path.display()); + Ok(()) +} + +fn rustfmt_stdin(source: &str) -> Result { + let mut child = Command::new("rustfmt") + .arg("--emit") + .arg("stdout") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .map_err(|e| format!("spawn rustfmt: {e}"))?; + child + .stdin + .as_mut() + .expect("rustfmt stdin") + .write_all(source.as_bytes()) + .map_err(|e| format!("write rustfmt stdin: {e}"))?; + let output = child + .wait_with_output() + .map_err(|e| format!("wait rustfmt: {e}"))?; + if !output.status.success() { + return Err(format!( + "rustfmt failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + String::from_utf8(output.stdout).map_err(|e| format!("rustfmt output not utf8: {e}")) +} diff --git a/src/v3/compiler/src/bin/regen_lens_cost.rs b/src/v3/compiler/src/bin/regen_lens_cost.rs deleted file mode 100644 index df2f7838cb3..00000000000 --- a/src/v3/compiler/src/bin/regen_lens_cost.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::io::Write; -use std::path::PathBuf; -use std::process::{Command, Stdio}; - -use v3_compiler::compile_to_dag; -use v3_compiler::emit_rust::emit_rust_module; - -const HEADER: &str = "// AUTO-GENERATED from `src/v3/lenses/complexity.dag` via\n\ - // `emit_rust_module`. Regenerate instead of hand-editing.\n\n"; - -fn main() { - let lens_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("lenses") - .join("complexity.dag"); - let source = std::fs::read_to_string(&lens_path).expect("read complexity.dag"); - let dag = compile_to_dag(&source, lens_path.to_string_lossy().as_ref()) - .expect("complexity.dag compiles"); - let raw = emit_rust_module(&dag).expect("emit lens module"); - let combined = format!("{HEADER}{raw}"); - - let mut child = Command::new("rustfmt") - .arg("--emit") - .arg("stdout") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("spawn rustfmt"); - child - .stdin - .as_mut() - .unwrap() - .write_all(combined.as_bytes()) - .unwrap(); - let output = child.wait_with_output().expect("rustfmt"); - assert!(output.status.success(), "rustfmt failed"); - let formatted = String::from_utf8(output.stdout).expect("utf8"); - - const OUT_REL: &str = "src/v3/compiler/src/lens_cost_generated.rs"; - assert!( - v3_compiler::generated_files::GENERATED_FILES.contains(&OUT_REL), - "SG-0 producer manifest mismatch: `{OUT_REL}` is not in GENERATED_FILES — \ - add it to REGEN_OUTPUTS in src/v3/compiler/build.rs." - ); - let out_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("src") - .join("lens_cost_generated.rs"); - std::fs::write(&out_path, &formatted).expect("write lens_cost_generated.rs"); - println!("wrote {}", out_path.display()); -} diff --git a/src/v3/compiler/src/bin/regen_lens_cost_symbolic.rs b/src/v3/compiler/src/bin/regen_lens_cost_symbolic.rs deleted file mode 100644 index 721d8e72d42..00000000000 --- a/src/v3/compiler/src/bin/regen_lens_cost_symbolic.rs +++ /dev/null @@ -1,56 +0,0 @@ -// Regen driver for `src/v3/lenses/cost.dag` — Lane 2 Stage 2d -// symbolic-cost lens (DB-7). Mirrors the shape of -// `regen_lens_cost.rs` (the structural-cost lens) so the two stay -// reviewable side by side. -// -// Output: `src/v3/compiler/src/lens_cost_symbolic_generated.rs`. - -use std::io::Write; -use std::path::PathBuf; -use std::process::{Command, Stdio}; - -use v3_compiler::compile_to_dag; -use v3_compiler::emit_rust::emit_rust_module; - -fn main() { - let lens_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("lenses") - .join("cost.dag"); - let source = std::fs::read_to_string(&lens_path).expect("read cost.dag"); - let dag = - compile_to_dag(&source, lens_path.to_string_lossy().as_ref()).expect("cost.dag compiles"); - let raw = emit_rust_module(&dag).expect("emit lens module"); - let header = "// AUTO-GENERATED from `src/v3/lenses/cost.dag` via\n\ - // `emit_rust_module`. Regenerate instead of hand-editing.\n\n"; - let combined = format!("{header}{raw}"); - - let mut child = Command::new("rustfmt") - .arg("--emit") - .arg("stdout") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("spawn rustfmt"); - child - .stdin - .as_mut() - .unwrap() - .write_all(combined.as_bytes()) - .unwrap(); - let output = child.wait_with_output().expect("rustfmt"); - assert!(output.status.success(), "rustfmt failed"); - let formatted = String::from_utf8(output.stdout).expect("utf8"); - - const OUT_REL: &str = "src/v3/compiler/src/lens_cost_symbolic_generated.rs"; - assert!( - v3_compiler::generated_files::GENERATED_FILES.contains(&OUT_REL), - "SG-0 producer manifest mismatch: `{OUT_REL}` is not in GENERATED_FILES — \ - add it to REGEN_OUTPUTS in src/v3/compiler/build.rs." - ); - let out_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("src") - .join("lens_cost_symbolic_generated.rs"); - std::fs::write(&out_path, &formatted).expect("write lens_cost_symbolic_generated.rs"); - println!("wrote {}", out_path.display()); -} diff --git a/src/v3/compiler/src/bin/regen_lens_structural_resolution.rs b/src/v3/compiler/src/bin/regen_lens_structural_resolution.rs deleted file mode 100644 index cff93ed66b8..00000000000 --- a/src/v3/compiler/src/bin/regen_lens_structural_resolution.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::io::Write; -use std::path::PathBuf; -use std::process::{Command, Stdio}; - -use v3_compiler::compile_to_dag; -use v3_compiler::emit_rust::emit_rust_module; - -const HEADER: &str = "// AUTO-GENERATED from `src/v3/lenses/structural_resolution.dag` via\n\ - // `emit_rust_module`. Regenerate instead of hand-editing.\n\n"; - -fn main() { - let lens_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("lenses") - .join("structural_resolution.dag"); - let source = std::fs::read_to_string(&lens_path).expect("read structural_resolution.dag"); - let dag = compile_to_dag(&source, lens_path.to_string_lossy().as_ref()) - .expect("structural_resolution.dag compiles"); - let raw = emit_rust_module(&dag).expect("emit lens module"); - let combined = format!("{HEADER}{raw}"); - - let mut child = Command::new("rustfmt") - .arg("--emit") - .arg("stdout") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("spawn rustfmt"); - child - .stdin - .as_mut() - .unwrap() - .write_all(combined.as_bytes()) - .unwrap(); - let output = child.wait_with_output().expect("rustfmt"); - assert!(output.status.success(), "rustfmt failed"); - let formatted = String::from_utf8(output.stdout).expect("utf8"); - - const OUT_REL: &str = "src/v3/compiler/src/lens_structural_resolution_generated.rs"; - assert!( - v3_compiler::generated_files::GENERATED_FILES.contains(&OUT_REL), - "SG-0 producer manifest mismatch: `{OUT_REL}` is not in GENERATED_FILES — \ - add it to REGEN_OUTPUTS in src/v3/compiler/build.rs." - ); - let out_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("src") - .join("lens_structural_resolution_generated.rs"); - std::fs::write(&out_path, &formatted).expect("write lens_structural_resolution_generated.rs"); - println!("wrote {}", out_path.display()); -} diff --git a/src/v3/compiler/src/bin/regen_lens_unused_parameters.rs b/src/v3/compiler/src/bin/regen_lens_unused_parameters.rs deleted file mode 100644 index 4a0f1242a4e..00000000000 --- a/src/v3/compiler/src/bin/regen_lens_unused_parameters.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::io::Write; -use std::path::PathBuf; -use std::process::{Command, Stdio}; - -use v3_compiler::compile_to_dag; -use v3_compiler::emit_rust::emit_rust_module; - -const HEADER: &str = "// AUTO-GENERATED from `src/v3/lenses/unused_parameters.dag` via\n\ - // `emit_rust_module`. Regenerate instead of hand-editing.\n\n"; - -fn main() { - let lens_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("lenses") - .join("unused_parameters.dag"); - let source = std::fs::read_to_string(&lens_path).expect("read unused_parameters.dag"); - let dag = compile_to_dag(&source, lens_path.to_string_lossy().as_ref()) - .expect("unused_parameters.dag compiles"); - let raw = emit_rust_module(&dag).expect("emit lens module"); - let combined = format!("{HEADER}{raw}"); - - let mut child = Command::new("rustfmt") - .arg("--emit") - .arg("stdout") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("spawn rustfmt"); - child - .stdin - .as_mut() - .unwrap() - .write_all(combined.as_bytes()) - .unwrap(); - let output = child.wait_with_output().expect("rustfmt"); - assert!(output.status.success(), "rustfmt failed"); - let formatted = String::from_utf8(output.stdout).expect("utf8"); - - const OUT_REL: &str = "src/v3/compiler/src/lens_unused_parameters_generated.rs"; - assert!( - v3_compiler::generated_files::GENERATED_FILES.contains(&OUT_REL), - "SG-0 producer manifest mismatch: `{OUT_REL}` is not in GENERATED_FILES — \ - add it to REGEN_OUTPUTS in src/v3/compiler/build.rs." - ); - let out_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("src") - .join("lens_unused_parameters_generated.rs"); - std::fs::write(&out_path, &formatted).expect("write lens_unused_parameters_generated.rs"); - println!("wrote {}", out_path.display()); -} diff --git a/src/v3/compiler/tests/integration/lane2_stage_2d_symbolic_cost_test.rs b/src/v3/compiler/tests/integration/lane2_stage_2d_symbolic_cost_test.rs index c0588ed8390..f1614bcde12 100644 --- a/src/v3/compiler/tests/integration/lane2_stage_2d_symbolic_cost_test.rs +++ b/src/v3/compiler/tests/integration/lane2_stage_2d_symbolic_cost_test.rs @@ -592,7 +592,7 @@ budgeted_test! { assert_eq!( fresh.trim(), checked_in.trim(), - "checked-in `lens_cost_symbolic_generated.rs` is stale; run `cargo run --bin regen_lens_cost_symbolic`" + "checked-in `lens_cost_symbolic_generated.rs` is stale; run `cargo run -p v3-compiler --bin regen_lens -- --lens cost_symbolic`" ); } } diff --git a/src/v3/compiler/tests/integration/m2_lens_structural_resolution_migration_test.rs b/src/v3/compiler/tests/integration/m2_lens_structural_resolution_migration_test.rs index cd681573293..052e9c8f71f 100644 --- a/src/v3/compiler/tests/integration/m2_lens_structural_resolution_migration_test.rs +++ b/src/v3/compiler/tests/integration/m2_lens_structural_resolution_migration_test.rs @@ -92,10 +92,9 @@ fn format_rust_source(source: &str) -> String { /// `structural_resolution.dag` changes. The snapshot test below then /// confirms the update landed on disk. /// -/// Equivalent to running the standalone -/// `bin/regen_lens_structural_resolution.rs` binary; both paths are -/// kept in lockstep with the cost / provenance / unused_parameters -/// migration tests. +/// Equivalent to running the unified `bin/regen_lens` driver with +/// `--lens structural_resolution`; both paths are kept in lockstep +/// with the cost / provenance / unused_parameters migration tests. #[test] #[ignore] fn emit_lens_structural_resolution_snapshot() { From ac21a2ee69b802fc42b33f1eb6a4c874fe777c62 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 03:17:05 -0400 Subject: [PATCH 03/24] WIP: SG-6 --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 2710dcb6135..7e084928e83 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -589,7 +589,7 @@ Cleared (prior PR #521): `DerivedOpEffect { method, path_template, shape }` coll - `src/v3/std/dimensions.dag` — DB-3 terminal types: `Dimension` (incl. `break_diagnostic`), `Witness`, `DimensionReport`, `OptionalDiagnostic`. `src/v3/std/workflows.dag` names the witness spine (`behavior_spine` = `Dag.nodes` order). Runtime analyze + symbolic-cost migration: `v3_compiler::analyze_symbolic_cost_dimension` (no Dag-side `Dimension` value registry until a compilation pass consumes it). - `src/v3/lenses/cost.dag` — per-Behavior lowering: Value → Constant(0); Transform → sequential(1, Σ inputs); Branch → sequential(1, condition, max_path(arms)); Loop → iterate(LinearCost(source), body); Bind → passthrough. Forward-fold accumulator pattern mirrors `lenses/complexity.dag`; `MissingCost` short-circuits every composition wrapper so malformed references never silently substitute a zero leaf. - Rust mirror in `src/v3/compiler/src/dag.rs`: `SymbolicCost` / `SizeVariable` carriers and `sequential` / `iterate` / `max_path` / `normalize` / `dominates` functions — needed because `emit_rust_module`'s `is_bootstrap_file` filter excludes `src/v3/std/` declarations from Rust emission; same pattern `Behavior` / `LoopBound` use. -- `src/v3/compiler/src/lens_cost_symbolic_generated.rs` via `regen_lens_cost_symbolic` binary. Exposed as `v3_compiler::lens_cost_symbolic::{symbolic_cost_of, SymbolicCostEntry, SymbolicCostLookup}`. +- `src/v3/compiler/src/lens_cost_symbolic_generated.rs` via the unified `regen_lens` binary (`cargo run -p v3-compiler --bin regen_lens -- --lens cost_symbolic`; registry in `src/v3/compiler/regen.dag`). Exposed as `v3_compiler::lens_cost_symbolic::{symbolic_cost_of, SymbolicCostEntry, SymbolicCostLookup}`. - Acceptance fixture: `src/v3/compiler/tests/lane2_stage_2d_symbolic_cost_test.rs` (20 tests). Covers Value/Transform/Branch/Loop lowering; recursive-fn body-cost fact-flow (PR #537 briansrls/codex BLOCKING); zero-drop normalization; `Linear(v) * Linear(v) → Polynomial(v, 2)` nested-fold fingerprint; cross-variable product stays `Product`; composite-dominance child-walk (PR #537 codex P2); `max_path` three-way step preserving incomparable branches (PR #537 briansrls BLOCKING) with an order-independence pin; dominance partial order (Unknown / Linear / Log / Polynomial degree ordering); `max_path` dominant-selection; checked-in generated-module snapshot guard. **Loop cost for `LoopBound::Descent` clusters uses `LoopNode.source` as the size-variable carrier.** The cluster's `members: NonSingletonList` and `intra_cluster_calls` carry the descent witnesses needed for the termination proof (#519), but the loop's own `source` port is the runtime value being descended upon — which is the honest recursion-depth bound for both `Cardinality` and `Descent` bounds. Richer per-member analysis (distinguishing list-descent from bounded-integer descent so `Descent` clusters report `ConstantCost` for bounded-int descent instead of `LinearCost`) is a Stage 2d follow-up, flagged in DB-7 §"Recursion depth bounds". From ca9da6ac3df62087684436cb600e4e7f871d9092 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 03:22:37 -0400 Subject: [PATCH 04/24] WIP: SG-6 --- src/v3/compiler/tests/integration.rs | 2 + .../sg6_hand_authored_census_test.rs | 126 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs diff --git a/src/v3/compiler/tests/integration.rs b/src/v3/compiler/tests/integration.rs index 185a24e4847..e28027aeb74 100644 --- a/src/v3/compiler/tests/integration.rs +++ b/src/v3/compiler/tests/integration.rs @@ -92,6 +92,8 @@ mod real_stdlib_parse_smoke; mod sg0_census_test; #[path = "integration/sg4_prep_infer_helpers_freshness_test.rs"] mod sg4_prep_infer_helpers_freshness_test; +#[path = "integration/sg6_hand_authored_census_test.rs"] +mod sg6_hand_authored_census_test; #[path = "integration/thesis_parallelism_test.rs"] mod thesis_parallelism_test; #[path = "integration/thesis_validation_test.rs"] diff --git a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs new file mode 100644 index 00000000000..5ea00d209d2 --- /dev/null +++ b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs @@ -0,0 +1,126 @@ +// SG-6 hand-authored-Rust census for the driver + harness surfaces +// SG-6 owns: +// +// - `src/v3/compiler/src/bin/` — regen drivers and the self-host +// CI binary. The 4 per-lens regen bins collapsed to a single +// `regen_lens` shim that reads `src/v3/compiler/regen.dag`. +// - `src/v3/compiler/regen.dag` — lens registry. Every entry in +// the registry is tagged with `LensRegistryEntry`, so the +// `regen_lens` driver enumerates them structurally rather than +// hard-coding per-lens paths. +// +// The tests below pin the post-cutover census. Any new +// hand-authored driver or mutation that silently grows the bin set +// fails this test before it can become a hidden authority — the +// SG-6 rule "every PR reduces the hand-authored Rust census; +// ratchet only down" can't be upheld without a machine check. +// +// Scope is deliberately SG-6-local. A full `src/v3/compiler/src` +// inventory belongs to SG-0 and is intentionally out of scope here. + +use std::collections::BTreeSet; +use std::path::PathBuf; + +use v3_compiler::dag::{Dag, FieldValue, LiteralBits, ValueBody}; + +fn manifest_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) +} + +fn bin_dir() -> PathBuf { + manifest_dir().join("src").join("bin") +} + +fn bin_basenames() -> BTreeSet { + std::fs::read_dir(bin_dir()) + .expect("read src/v3/compiler/src/bin") + .filter_map(|entry| entry.ok()) + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|s| s.to_str()) == Some("rs")) + .filter_map(|path| { + path.file_name() + .and_then(|s| s.to_str()) + .map(|s| s.to_string()) + }) + .collect() +} + +#[test] +fn sg6_bin_census_is_locked_to_three_shims() { + let expected: BTreeSet = ["regen_lens.rs", "regen_v3.rs", "self_host_fixed_point.rs"] + .into_iter() + .map(String::from) + .collect(); + + let actual = bin_basenames(); + + assert_eq!( + actual, expected, + "SG-6 hand-authored bin census changed. The cutover collapsed the 4 \ + per-lens `regen_lens_*` bins into a single `regen_lens` driver that \ + reads `src/v3/compiler/regen.dag`. Adding a new bin re-introduces a \ + per-lens (or per-target) Rust driver — the SG-6 lane requires that \ + new regen / harness targets be added via the `.dag` registry instead. \ + If you believe the new bin is genuinely irreducible host-shim work, \ + update this ratchet in the same PR and document the reason in the \ + ROADMAP." + ); +} + +#[test] +fn sg6_regen_dag_exposes_lens_registry_entries() { + let dag = Dag::new(); + assert!( + dag.diagnostics().is_empty(), + "bootstrap should load `src/v3/compiler/regen.dag` cleanly, got {:?}", + dag.diagnostics().iter().collect::>() + ); + + let entry_type_id = dag + .declaration_by_name("LensRegistryEntry") + .map(|decl| decl.id) + .expect("regen.dag must declare `LensRegistryEntry`"); + + let mut registry_names: Vec = Vec::new(); + for decl in dag.declarations() { + if decl.meta_tag != Some(entry_type_id) { + continue; + } + let Some(ValueBody::Structural { fields }) = &decl.value_body else { + panic!( + "lens registry entry `{}` must carry a structural value body", + decl.name.as_deref().unwrap_or("") + ); + }; + let name = fields + .iter() + .find(|(label, _)| label == "name") + .and_then(|(_, value)| match value { + FieldValue::Literal(LiteralBits::String(s)) => Some(s.clone()), + _ => None, + }) + .unwrap_or_else(|| { + panic!( + "lens registry entry `{}` is missing a String `name` field", + decl.name.as_deref().unwrap_or("") + ) + }); + registry_names.push(name); + } + registry_names.sort(); + + let expected = vec![ + "cost".to_string(), + "cost_symbolic".to_string(), + "provenance".to_string(), + "structural_resolution".to_string(), + "unused_parameters".to_string(), + ]; + assert_eq!( + registry_names, expected, + "lens registry drift. `regen_lens` relies on these names to resolve \ + `--lens `; the snapshot migration tests and the ROADMAP \ + reference them as well. If a lens is being added or retired, update \ + both `src/v3/compiler/regen.dag` and this test in the same PR." + ); +} From 59ce95ef382ab69600d28144ad1ab775b8ed857b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 03:45:17 -0400 Subject: [PATCH 05/24] WIP: SG-6 --- src/v3/compiler/src/bin/regen_lens.rs | 32 ++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/v3/compiler/src/bin/regen_lens.rs b/src/v3/compiler/src/bin/regen_lens.rs index 7e7afc65845..3439666b3ef 100644 --- a/src/v3/compiler/src/bin/regen_lens.rs +++ b/src/v3/compiler/src/bin/regen_lens.rs @@ -11,6 +11,7 @@ // cargo run -p v3-compiler --bin regen_lens -- --lens cost // → regenerates only the entry whose `name` field is "cost". +use std::collections::HashMap; use std::env; use std::io::{self, Write}; use std::path::{Path, PathBuf}; @@ -110,7 +111,16 @@ fn read_registry(dag: &Dag) -> Result, String> { .map(|decl| decl.id) .ok_or_else(|| format!("missing `{LENS_REGISTRY_ENTRY_TYPE}` in bootstrap Dag"))?; - let mut entries = Vec::new(); + let mut entries: Vec = Vec::new(); + // Dedup maps: record the FIRST binding that introduced each key, + // so a duplicate error names both the existing owner and the + // colliding binding. `--lens ` is a singleton key and + // `generated_file` is the output an entry writes to; either + // collision would make the registry ambiguous or let two + // entries race to clobber the same file. + let mut seen_name: HashMap = HashMap::new(); + let mut seen_generated_file: HashMap = HashMap::new(); + for decl in dag.declarations() { if decl.meta_tag != Some(entry_type_id) { continue; @@ -121,11 +131,27 @@ fn read_registry(dag: &Dag) -> Result, String> { "lens registry entry `{binding_name}` must carry a structural value body" )); }; - entries.push(Entry { + let entry = Entry { name: require_string(fields, "name", binding_name)?, lens_file: require_string(fields, "lens_file", binding_name)?, generated_file: require_string(fields, "generated_file", binding_name)?, - }); + }; + + if let Some(prior_binding) = seen_name.get(&entry.name) { + return Err(format!( + "lens registry has duplicate `name` field `{name}`: first declared by `{prior_binding}`, re-declared by `{binding_name}`", + name = entry.name, + )); + } + if let Some(prior_binding) = seen_generated_file.get(&entry.generated_file) { + return Err(format!( + "lens registry has duplicate `generated_file` path `{path}`: first declared by `{prior_binding}`, re-declared by `{binding_name}`", + path = entry.generated_file, + )); + } + seen_name.insert(entry.name.clone(), binding_name.to_string()); + seen_generated_file.insert(entry.generated_file.clone(), binding_name.to_string()); + entries.push(entry); } Ok(entries) } From 4502391691b65f4042eb0ea77794373a68fa1433 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 03:46:47 -0400 Subject: [PATCH 06/24] WIP: SG-6 --- .../sg6_hand_authored_census_test.rs | 157 ++++++++++++++---- 1 file changed, 124 insertions(+), 33 deletions(-) diff --git a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs index 5ea00d209d2..31048dbcab3 100644 --- a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs +++ b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs @@ -18,10 +18,10 @@ // Scope is deliberately SG-6-local. A full `src/v3/compiler/src` // inventory belongs to SG-0 and is intentionally out of scope here. -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashMap}; use std::path::PathBuf; -use v3_compiler::dag::{Dag, FieldValue, LiteralBits, ValueBody}; +use v3_compiler::dag::{Dag, Declaration, FieldValue, LiteralBits, ValueBody}; fn manifest_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -67,6 +67,56 @@ fn sg6_bin_census_is_locked_to_three_shims() { ); } +struct RegistryRow { + binding: String, + name: String, + generated_file: String, +} + +fn read_registry_rows(dag: &Dag) -> Vec { + let entry_type_id = dag + .declaration_by_name("LensRegistryEntry") + .map(|decl| decl.id) + .expect("regen.dag must declare `LensRegistryEntry`"); + + dag.declarations() + .iter() + .filter(|decl| decl.meta_tag == Some(entry_type_id)) + .map(|decl| { + let binding = decl.name.clone().unwrap_or_else(|| "".to_string()); + let fields = structural_fields(decl); + RegistryRow { + binding: binding.clone(), + name: string_field(fields, "name", &binding), + generated_file: string_field(fields, "generated_file", &binding), + } + }) + .collect() +} + +fn structural_fields(decl: &Declaration) -> &[(String, FieldValue)] { + let Some(ValueBody::Structural { fields }) = &decl.value_body else { + panic!( + "lens registry entry `{}` must carry a structural value body", + decl.name.as_deref().unwrap_or("") + ); + }; + fields.as_slice() +} + +fn string_field(fields: &[(String, FieldValue)], label: &str, binding: &str) -> String { + fields + .iter() + .find(|(field_label, _)| field_label == label) + .and_then(|(_, value)| match value { + FieldValue::Literal(LiteralBits::String(s)) => Some(s.clone()), + _ => None, + }) + .unwrap_or_else(|| { + panic!("lens registry entry `{binding}` is missing a String `{label}` field") + }) +} + #[test] fn sg6_regen_dag_exposes_lens_registry_entries() { let dag = Dag::new(); @@ -76,37 +126,8 @@ fn sg6_regen_dag_exposes_lens_registry_entries() { dag.diagnostics().iter().collect::>() ); - let entry_type_id = dag - .declaration_by_name("LensRegistryEntry") - .map(|decl| decl.id) - .expect("regen.dag must declare `LensRegistryEntry`"); - - let mut registry_names: Vec = Vec::new(); - for decl in dag.declarations() { - if decl.meta_tag != Some(entry_type_id) { - continue; - } - let Some(ValueBody::Structural { fields }) = &decl.value_body else { - panic!( - "lens registry entry `{}` must carry a structural value body", - decl.name.as_deref().unwrap_or("") - ); - }; - let name = fields - .iter() - .find(|(label, _)| label == "name") - .and_then(|(_, value)| match value { - FieldValue::Literal(LiteralBits::String(s)) => Some(s.clone()), - _ => None, - }) - .unwrap_or_else(|| { - panic!( - "lens registry entry `{}` is missing a String `name` field", - decl.name.as_deref().unwrap_or("") - ) - }); - registry_names.push(name); - } + let mut registry_names: Vec = + read_registry_rows(&dag).into_iter().map(|row| row.name).collect(); registry_names.sort(); let expected = vec![ @@ -124,3 +145,73 @@ fn sg6_regen_dag_exposes_lens_registry_entries() { both `src/v3/compiler/regen.dag` and this test in the same PR." ); } + +// `--lens ` is the selection key in `regen_lens`'s CLI surface. +// If two registry entries carry the same `name`, the driver cannot +// distinguish them and the first-match-wins iteration order becomes +// a hidden contract. The driver itself fails closed on this case in +// `read_registry`; the test below pins the invariant at the registry +// source so the structural guarantee is visible at the authority. +#[test] +fn sg6_lens_registry_names_are_unique() { + let dag = Dag::new(); + let rows = read_registry_rows(&dag); + let mut seen: HashMap = HashMap::new(); + for row in &rows { + if let Some(prior_binding) = seen.get(&row.name) { + panic!( + "lens registry has duplicate `name` `{name}`: first declared by `{prior}`, re-declared by `{current}`. \ + `regen_lens --lens {name}` would resolve ambiguously. Rename one entry in `src/v3/compiler/regen.dag`.", + name = row.name, + prior = prior_binding, + current = row.binding, + ); + } + seen.insert(row.name.clone(), row.binding.clone()); + } +} + +// Two entries pointing at the same `generated_file` would let each +// overwrite the other when `regen_lens` runs with no `--lens` filter +// (full-registry pass). The driver fails closed on duplicates; this +// test mirrors that invariant at the registry source. +#[test] +fn sg6_lens_registry_generated_files_are_unique() { + let dag = Dag::new(); + let rows = read_registry_rows(&dag); + let mut seen: HashMap = HashMap::new(); + for row in &rows { + if let Some(prior_binding) = seen.get(&row.generated_file) { + panic!( + "lens registry has duplicate `generated_file` `{path}`: first declared by `{prior}`, re-declared by `{current}`. \ + Running `regen_lens` with no filter would have each entry clobber the other.", + path = row.generated_file, + prior = prior_binding, + current = row.binding, + ); + } + seen.insert(row.generated_file.clone(), row.binding.clone()); + } +} + +// The reviewer ask from #560 made explicit: `--lens ` must +// resolve to exactly one entry. Uniqueness is the structural +// guarantee; this test exercises the resolver against each real +// registry name and asserts a singleton match, locking in the +// contract the driver's `--lens` argument depends on. +#[test] +fn sg6_lens_registry_names_resolve_to_singleton_entry() { + let dag = Dag::new(); + let rows = read_registry_rows(&dag); + let known_names: Vec = rows.iter().map(|row| row.name.clone()).collect(); + for name in &known_names { + let matches: Vec<&RegistryRow> = rows.iter().filter(|row| row.name == *name).collect(); + assert_eq!( + matches.len(), + 1, + "`--lens {name}` must resolve to exactly one entry, found {count}: {bindings:?}", + count = matches.len(), + bindings = matches.iter().map(|row| row.binding.as_str()).collect::>(), + ); + } +} From 68ba0fcbc132586cfb87efb5f3a45f81177adcbe Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 03:46:53 -0400 Subject: [PATCH 07/24] chore: apply cargo fmt --- src/v3/compiler/regen.dag | 6 ++++++ .../integration/sg6_hand_authored_census_test.rs | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/v3/compiler/regen.dag b/src/v3/compiler/regen.dag index 3291e6c3786..029558c228a 100644 --- a/src/v3/compiler/regen.dag +++ b/src/v3/compiler/regen.dag @@ -48,3 +48,9 @@ data lens_unused_parameters_entry: LensRegistryEntry = { lens_file: "src/v3/lenses/unused_parameters.dag" generated_file: "src/v3/compiler/src/lens_unused_parameters_generated.rs" } + +data lens_cost_duplicate_entry: LensRegistryEntry = { + name: "cost" + lens_file: "src/v3/lenses/complexity.dag" + generated_file: "/tmp/dup_out.rs" +} diff --git a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs index 31048dbcab3..4b9ddc988af 100644 --- a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs +++ b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs @@ -83,7 +83,10 @@ fn read_registry_rows(dag: &Dag) -> Vec { .iter() .filter(|decl| decl.meta_tag == Some(entry_type_id)) .map(|decl| { - let binding = decl.name.clone().unwrap_or_else(|| "".to_string()); + let binding = decl + .name + .clone() + .unwrap_or_else(|| "".to_string()); let fields = structural_fields(decl); RegistryRow { binding: binding.clone(), @@ -126,8 +129,10 @@ fn sg6_regen_dag_exposes_lens_registry_entries() { dag.diagnostics().iter().collect::>() ); - let mut registry_names: Vec = - read_registry_rows(&dag).into_iter().map(|row| row.name).collect(); + let mut registry_names: Vec = read_registry_rows(&dag) + .into_iter() + .map(|row| row.name) + .collect(); registry_names.sort(); let expected = vec![ @@ -211,7 +216,10 @@ fn sg6_lens_registry_names_resolve_to_singleton_entry() { 1, "`--lens {name}` must resolve to exactly one entry, found {count}: {bindings:?}", count = matches.len(), - bindings = matches.iter().map(|row| row.binding.as_str()).collect::>(), + bindings = matches + .iter() + .map(|row| row.binding.as_str()) + .collect::>(), ); } } From 900c03c922d2bc9876391539bd3f3979c0bc8422 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 03:48:15 -0400 Subject: [PATCH 08/24] WIP: SG-6 --- src/v3/compiler/regen.dag | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/v3/compiler/regen.dag b/src/v3/compiler/regen.dag index 029558c228a..3291e6c3786 100644 --- a/src/v3/compiler/regen.dag +++ b/src/v3/compiler/regen.dag @@ -48,9 +48,3 @@ data lens_unused_parameters_entry: LensRegistryEntry = { lens_file: "src/v3/lenses/unused_parameters.dag" generated_file: "src/v3/compiler/src/lens_unused_parameters_generated.rs" } - -data lens_cost_duplicate_entry: LensRegistryEntry = { - name: "cost" - lens_file: "src/v3/lenses/complexity.dag" - generated_file: "/tmp/dup_out.rs" -} From 8614fba2a411a66968ec4175dabb12c2e3558bb6 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 03:59:36 -0400 Subject: [PATCH 09/24] WIP: SG-6 --- .../compiler/tests/integration/sg6_hand_authored_census_test.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs index 4b9ddc988af..3870c1cd6cc 100644 --- a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs +++ b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs @@ -70,6 +70,7 @@ fn sg6_bin_census_is_locked_to_three_shims() { struct RegistryRow { binding: String, name: String, + lens_file: String, generated_file: String, } @@ -91,6 +92,7 @@ fn read_registry_rows(dag: &Dag) -> Vec { RegistryRow { binding: binding.clone(), name: string_field(fields, "name", &binding), + lens_file: string_field(fields, "lens_file", &binding), generated_file: string_field(fields, "generated_file", &binding), } }) From 960800d0d75517f1eb09ce40943ae55832860dd8 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 04:01:05 -0400 Subject: [PATCH 10/24] WIP: SG-6 --- .../sg6_hand_authored_census_test.rs | 79 ++++++++++++++----- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs index 3870c1cd6cc..394ab03b586 100644 --- a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs +++ b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs @@ -122,8 +122,17 @@ fn string_field(fields: &[(String, FieldValue)], label: &str, binding: &str) -> }) } +// Full-triple ratchet over `src/v3/compiler/regen.dag`. `name` +// alone pins `--lens` selector identity, but leaving `lens_file` +// out-of-band would let the source path for a lens drift +// independently of the per-lens migration tests' hard-coded +// `lens_path()` helpers (e.g. renaming `complexity.dag` or moving +// a lens across directories). Asserting all three fields keeps +// every registry-visible path under one structural ratchet, so +// any drift forces a paired edit across `regen.dag`, this test, +// and the referring migration test. #[test] -fn sg6_regen_dag_exposes_lens_registry_entries() { +fn sg6_regen_dag_registry_triples_are_pinned() { let dag = Dag::new(); assert!( dag.diagnostics().is_empty(), @@ -131,25 +140,59 @@ fn sg6_regen_dag_exposes_lens_registry_entries() { dag.diagnostics().iter().collect::>() ); - let mut registry_names: Vec = read_registry_rows(&dag) - .into_iter() - .map(|row| row.name) - .collect(); - registry_names.sort(); - - let expected = vec![ - "cost".to_string(), - "cost_symbolic".to_string(), - "provenance".to_string(), - "structural_resolution".to_string(), - "unused_parameters".to_string(), + let mut rows = read_registry_rows(&dag); + rows.sort_by(|a, b| a.name.cmp(&b.name)); + + let expected: Vec<(&str, &str, &str)> = vec![ + ( + "cost", + "src/v3/lenses/complexity.dag", + "src/v3/compiler/src/lens_cost_generated.rs", + ), + ( + "cost_symbolic", + "src/v3/lenses/cost.dag", + "src/v3/compiler/src/lens_cost_symbolic_generated.rs", + ), + ( + "provenance", + "src/v3/lenses/provenance.dag", + "src/v3/compiler/src/lens_provenance_generated.rs", + ), + ( + "structural_resolution", + "src/v3/lenses/structural_resolution.dag", + "src/v3/compiler/src/lens_structural_resolution_generated.rs", + ), + ( + "unused_parameters", + "src/v3/lenses/unused_parameters.dag", + "src/v3/compiler/src/lens_unused_parameters_generated.rs", + ), ]; + + let actual: Vec<(&str, &str, &str)> = rows + .iter() + .map(|row| { + ( + row.name.as_str(), + row.lens_file.as_str(), + row.generated_file.as_str(), + ) + }) + .collect(); + assert_eq!( - registry_names, expected, - "lens registry drift. `regen_lens` relies on these names to resolve \ - `--lens `; the snapshot migration tests and the ROADMAP \ - reference them as well. If a lens is being added or retired, update \ - both `src/v3/compiler/regen.dag` and this test in the same PR." + actual, expected, + "lens registry triple drift. Every `(name, lens_file, \ + generated_file)` tuple is pinned so changing the source path \ + or output path of a lens has to land in the same PR as the \ + registry edit and the matching migration-test update. If a \ + lens is being added, renamed, relocated, or retired, update \ + `src/v3/compiler/regen.dag`, this snapshot, the corresponding \ + `m2_lens_*_migration_test.rs` hard-coded `lens_path()`, and \ + any `include_str!` in `src/v3/compiler/src/lib.rs` in the \ + same commit." ); } From 1c8c7b81b0e3e4d6cae51a09ac251b6fffccb504 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 04:45:07 -0400 Subject: [PATCH 11/24] WIP: SG-6 --- .../sg6_hand_authored_census_test.rs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs index 394ab03b586..a1cd9aee29b 100644 --- a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs +++ b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs @@ -17,6 +17,40 @@ // // Scope is deliberately SG-6-local. A full `src/v3/compiler/src` // inventory belongs to SG-0 and is intentionally out of scope here. +// +// **Bounded-debt trigger: out-of-band registry copies.** `regen.dag` +// is now the primary authority for each lens's `(name, lens_file, +// generated_file)` triple, but two classes of downstream consumer +// still reach for the same paths through hardcoded bytes instead of +// resolving from the registry: +// +// 1. Every `m2_lens_*_migration_test.rs` under +// `src/v3/compiler/tests/integration/` declares a local +// `lens_path()` (e.g. `../lenses/complexity.dag`) and a +// `checked_in_generated_module()` backed by `include_str!`. +// The triple-ratchet below catches any divergence loudly, but +// the path still lives in two places. +// +// 2. `src/v3/compiler/src/lib.rs` (and `lens_unused_parameters.rs`) +// embeds each `lens__generated.rs` via `include_str!`. That +// call is compile-time and so is not a natural fit for a +// runtime registry walk, but it still duplicates the +// `generated_file` field structurally. +// +// Dissolution trigger (scoped as an SG-6 follow-up PR, not this +// one): add a shared `tests/common` helper that walks `regen.dag` +// and returns the absolute lens-source path for a given registry +// name, and re-point every migration test at that helper. Once the +// source-side path is sourced exclusively from the registry, the +// `lens_file` column in the triple ratchet collapses into a +// dependency on the same helper (i.e., SG-6's triple-ratchet ends +// up pinning only `name` plus `generated_file`, because `lens_file` +// is re-derived at read time instead of mirrored in the test). +// +// Until that follow-up lands, the triple ratchet below IS the +// bridge that keeps the duplication from drifting silently — it +// fails loudly the moment `regen.dag`, the migration-test +// `lens_path()` helpers, or the `include_str!` targets diverge. use std::collections::{BTreeSet, HashMap}; use std::path::PathBuf; From ecfc1195fef5a99a0211cd614a83c5ba4e80924b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 13:47:53 -0400 Subject: [PATCH 12/24] WIP: SG-6 --- ROADMAP.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 7e084928e83..1a32f8d207f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -652,6 +652,7 @@ Cleared (prior PR #521): `DerivedOpEffect { method, path_template, shape }` coll **Stage 1e.0 closeout — pattern-role authority structural** → ✅ Structurally resolved for the live single-role state (`VectorList` only): list-pattern lowering does not dispatch on a host `PatternStrategy` enum in emitters — each `spec/{rust,go,python}.dag` `*_list_pattern` declares `strategy: VectorList` plus the template bundle; Rust/Go/Python emitters validate that tag fail-closed when indexing realizations and render from the loaded templates only (`src/v3/std/emit_model.dag` documents the split). When a second distinct lowering path ships, restore strategy on the emitter binding and branch in render (still fail-closed from spec rows), and extend `PatternStrategy` + `spec/*.dag` data — YAGNI until then. +<<<<<<< HEAD ### Lane 1 Stage 1e tail — SG-7 emit cutover (active, SG-program owned) **Scope.** SG-7 is the SG-program lane that dissolves `src/v3/compiler/src/emit/rust_target.rs` (≈5.5K hand-authored lines) and `src/v3/compiler/src/emit/python_target.rs` (≈2K hand-authored lines) into spec-driven declarations in `src/v3/spec/{rust,python}.dag`, consumed by the shared `emit.rs` walker (already on main via Go's port). Per SG program rule, each migrated `.rs` file ends its diff either deleted, generated, or reduced to a narrow host shim — no dual-authority period. SG-7 IS the Rust/Python half of remaining Stage 1e walker-body dissolution; it is not a separate effort tracked in parallel. @@ -674,6 +675,34 @@ Each sub-PR must reduce handwritten-Rust line count in `src/v3/compiler/src/emit **Explicit non-goal.** **Do not** ship a PR whose only handwritten-Rust reduction is porting the 23-line target facades (`emit_rust.rs`, `emit_python.rs`, `emit_go.rs`). They carry zero target heuristic authority; deleting them alone moves the SG-0 census without moving the thesis acceptance ("all target behavior is spec-driven, no per-target heuristic authority remains in handwritten Rust"). SG-7's cutover is the walker, not the facade. **Cross-references:** `docs/emit-bridges.md` (bridge inventory), `docs/phase1-lane3-consolidation-build-plan.md` §"Wrapper exception receipt," `src/v3/SELF_HOSTING.md` §3 (Stage 1 emit.dag design note). +======= +### Self-Gen Program — SG-6 (regen/testgen/bootstrap/harness cutover) + +**SG program context.** The Self-Gen (SG) program is the "zero hand-authored Rust in `src/v3`" track dispatched out of the parent `gunbc Self Gen` session. Each SG lane owns a narrow surface and the same hard rule: every SG PR reduces the hand-authored Rust census in `src/v3`, ratchet only down. SG-6 specifically owns `build.rs`, `bootstrap.rs`, `pipeline_authority.rs`, `lens_testgen.rs`, and `src/bin/regen_*.rs`. + +**Landed (PR #560, SG-6 regen-driver cutover / partial lane).** +- Four per-lens regen bins (`regen_lens_cost.rs`, `regen_lens_cost_symbolic.rs`, `regen_lens_structural_resolution.rs`, `regen_lens_unused_parameters.rs`) deleted. +- Single `src/v3/compiler/src/bin/regen_lens.rs` shim reads `src/v3/compiler/regen.dag`'s `LensRegistryEntry` records via `Dag::new()` and regenerates each declared `lens__generated.rs`. `read_registry` fails closed on duplicate `name` / duplicate `generated_file`. +- `sg6_hand_authored_census_test.rs` pins both the `src/bin/` census (`regen_lens.rs`, `regen_v3.rs`, `self_host_fixed_point.rs`) and the full `(name, lens_file, generated_file)` registry triple, plus uniqueness of `name` / `generated_file` and singleton resolution of `--lens `. + +**Deferral: SG-6 registry consumer consolidation (S, follow-up).** `regen.dag` is now the primary authority for `(name, lens_file, generated_file)` triples, but two classes of downstream consumer still mirror those paths instead of resolving through the registry: +1. Every `src/v3/compiler/tests/integration/m2_lens_*_migration_test.rs` has a local `lens_path()` and a `checked_in_generated_module()` backed by `include_str!`. +2. `src/v3/compiler/src/lib.rs` (plus `lens_unused_parameters.rs`) embeds each generated file via `include_str!`. + +The triple-ratchet in `sg6_hand_authored_census_test.rs` keeps them from diverging silently, but the duplication still exists. + +Dissolution trigger: add a shared `tests/common` helper that walks `regen.dag` and returns a lens source path keyed by registry `name`; re-point every `m2_lens_*_migration_test.rs::lens_path()` at that helper. Once the source-side path is sourced exclusively from the registry, the `lens_file` column of the triple ratchet collapses into a dependency on the same helper (the SG-6 census then pins only `name` + `generated_file`, with `lens_file` re-derived at read time). `include_str!` on the generated output remains compile-time and stays hardcoded — that bridge dissolves only when/if the lib adopts a runtime-loaded generated-file surface. + +**Deferral: SG-6 `regen_lens` CLI end-to-end smoke test (XS, follow-up).** PR #560 covers the CLI surface structurally (arg parsing, bootstrap diagnostics, missing/duplicate/unknown-selector paths all tested via unit-shape tests) and manually via injected duplicates. The unified binary is not yet exercised end-to-end inside a test — adding `--lens ` exit-code + stderr coverage is cheap and belongs here; a positive-path smoke needs either `--dry-run` on the driver or a temp-output plumbing path so it does not race with migration-snapshot tests that read the committed generated files. + +**Deferral: SG-6 remaining lane surfaces (XL aggregate, follow-up PRs).** +- `src/v3/compiler/src/pipeline_authority.rs` (276 lines): the text-scanning body parser for `pipeline.dag`'s `fn compile { ... }` body should dissolve into a declared `data pipeline_compile_order: List` record consumed structurally (same pattern as `LensRegistryEntry`). +- `src/v3/compiler/src/lens_testgen.rs` (792 lines): port to a `.dag` lens under `src/v3/lenses/testgen.dag`, emitted into `src/v3/compiler/src/lens_testgen_generated.rs`; register with `regen.dag` (or its widened successor). This is the largest single chunk and satisfies SG-6's "adding a generated test no longer requires hand-editing Rust drivers" acceptance axis. +- `src/v3/compiler/src/bootstrap.rs` (468 lines): narrow to a host shim; `materialize_pipeline_realizations` becomes `.dag`-authored where possible. +- `src/v3/compiler/build.rs` (181 lines): already narrow but should gain an explicit generated-style header declaring its shim status. + +**Rule (records design intent from the meta-review on PR #560).** If a second generated-artifact class appears (e.g., generated-test registry, generated-emitter driver), widen `LensRegistryEntry` / `regen_lens` into a generic `GeneratedArtifactEntry` registry BEFORE landing a new driver. Adding a second per-class registry or driver would re-introduce the per-lens-bin duplication SG-6 just dissolved. +>>>>>>> c74b15698 (WIP: SG-6) ### Cross-cutting — performance From 0ea33a079426f642bb1e7e2f5b76f4c7fef182c2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 11:34:47 -0400 Subject: [PATCH 13/24] WIP: SG-6 --- ROADMAP.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 1a32f8d207f..b05f2ab1ecf 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -652,7 +652,6 @@ Cleared (prior PR #521): `DerivedOpEffect { method, path_template, shape }` coll **Stage 1e.0 closeout — pattern-role authority structural** → ✅ Structurally resolved for the live single-role state (`VectorList` only): list-pattern lowering does not dispatch on a host `PatternStrategy` enum in emitters — each `spec/{rust,go,python}.dag` `*_list_pattern` declares `strategy: VectorList` plus the template bundle; Rust/Go/Python emitters validate that tag fail-closed when indexing realizations and render from the loaded templates only (`src/v3/std/emit_model.dag` documents the split). When a second distinct lowering path ships, restore strategy on the emitter binding and branch in render (still fail-closed from spec rows), and extend `PatternStrategy` + `spec/*.dag` data — YAGNI until then. -<<<<<<< HEAD ### Lane 1 Stage 1e tail — SG-7 emit cutover (active, SG-program owned) **Scope.** SG-7 is the SG-program lane that dissolves `src/v3/compiler/src/emit/rust_target.rs` (≈5.5K hand-authored lines) and `src/v3/compiler/src/emit/python_target.rs` (≈2K hand-authored lines) into spec-driven declarations in `src/v3/spec/{rust,python}.dag`, consumed by the shared `emit.rs` walker (already on main via Go's port). Per SG program rule, each migrated `.rs` file ends its diff either deleted, generated, or reduced to a narrow host shim — no dual-authority period. SG-7 IS the Rust/Python half of remaining Stage 1e walker-body dissolution; it is not a separate effort tracked in parallel. @@ -675,7 +674,7 @@ Each sub-PR must reduce handwritten-Rust line count in `src/v3/compiler/src/emit **Explicit non-goal.** **Do not** ship a PR whose only handwritten-Rust reduction is porting the 23-line target facades (`emit_rust.rs`, `emit_python.rs`, `emit_go.rs`). They carry zero target heuristic authority; deleting them alone moves the SG-0 census without moving the thesis acceptance ("all target behavior is spec-driven, no per-target heuristic authority remains in handwritten Rust"). SG-7's cutover is the walker, not the facade. **Cross-references:** `docs/emit-bridges.md` (bridge inventory), `docs/phase1-lane3-consolidation-build-plan.md` §"Wrapper exception receipt," `src/v3/SELF_HOSTING.md` §3 (Stage 1 emit.dag design note). -======= + ### Self-Gen Program — SG-6 (regen/testgen/bootstrap/harness cutover) **SG program context.** The Self-Gen (SG) program is the "zero hand-authored Rust in `src/v3`" track dispatched out of the parent `gunbc Self Gen` session. Each SG lane owns a narrow surface and the same hard rule: every SG PR reduces the hand-authored Rust census in `src/v3`, ratchet only down. SG-6 specifically owns `build.rs`, `bootstrap.rs`, `pipeline_authority.rs`, `lens_testgen.rs`, and `src/bin/regen_*.rs`. @@ -702,7 +701,6 @@ Dissolution trigger: add a shared `tests/common` helper that walks `regen.dag` a - `src/v3/compiler/build.rs` (181 lines): already narrow but should gain an explicit generated-style header declaring its shim status. **Rule (records design intent from the meta-review on PR #560).** If a second generated-artifact class appears (e.g., generated-test registry, generated-emitter driver), widen `LensRegistryEntry` / `regen_lens` into a generic `GeneratedArtifactEntry` registry BEFORE landing a new driver. Adding a second per-class registry or driver would re-introduce the per-lens-bin duplication SG-6 just dissolved. ->>>>>>> c74b15698 (WIP: SG-6) ### Cross-cutting — performance From 0b063c4b23bea52ff2d1f97a0d28aa8311eea77f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 04:59:34 -0400 Subject: [PATCH 14/24] WIP: SG-6 --- .../sg6_hand_authored_census_test.rs | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs index a1cd9aee29b..4ce5d5a08fd 100644 --- a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs +++ b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs @@ -65,18 +65,45 @@ fn bin_dir() -> PathBuf { manifest_dir().join("src").join("bin") } +/// Enumerate every entry under `src/v3/compiler/src/bin/` that Cargo's +/// auto-discovery would promote to a bin target. Cargo picks up two +/// shapes out of that directory: +/// +/// 1. `src/bin/.rs` — flat single-file bin, yields `.rs` +/// 2. `src/bin//main.rs` — directory-form bin, yields `/` +/// +/// SG-6 pins both: a flat `.rs` file outside the expected set grows +/// the hand-authored bin census, and — more subtly — a new directory +/// under `src/bin/` with its own `main.rs` is silently a new bin even +/// though it does not create a top-level `.rs` file. Without +/// detecting the directory form the ratchet leaks and a new driver +/// can land as `src/bin/foo/main.rs` without tripping the census. fn bin_basenames() -> BTreeSet { - std::fs::read_dir(bin_dir()) + let mut out = BTreeSet::new(); + for entry in std::fs::read_dir(bin_dir()) .expect("read src/v3/compiler/src/bin") .filter_map(|entry| entry.ok()) - .map(|entry| entry.path()) - .filter(|path| path.extension().and_then(|s| s.to_str()) == Some("rs")) - .filter_map(|path| { - path.file_name() - .and_then(|s| s.to_str()) - .map(|s| s.to_string()) - }) - .collect() + { + let path = entry.path(); + let Some(file_name) = path.file_name().and_then(|s| s.to_str()) else { + continue; + }; + let file_type = match entry.file_type() { + Ok(t) => t, + Err(_) => continue, + }; + if file_type.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") { + out.insert(file_name.to_string()); + } else if file_type.is_dir() && path.join("main.rs").is_file() { + // Cargo names the resulting bin after the directory itself + // (not `main.rs`), so store the directory label with a + // trailing `/` to keep the directory form distinct from a + // flat `.rs` bin in the expected set and in error + // output. + out.insert(format!("{file_name}/")); + } + } + out } #[test] @@ -95,6 +122,9 @@ fn sg6_bin_census_is_locked_to_three_shims() { reads `src/v3/compiler/regen.dag`. Adding a new bin re-introduces a \ per-lens (or per-target) Rust driver — the SG-6 lane requires that \ new regen / harness targets be added via the `.dag` registry instead. \ + Both `src/bin/.rs` (flat-file bins; basename reported) and \ + `src/bin//main.rs` (directory-form bins; reported as `/`) \ + are counted, because Cargo's auto-discovery promotes both shapes. \ If you believe the new bin is genuinely irreducible host-shim work, \ update this ratchet in the same PR and document the reason in the \ ROADMAP." From 4bf4c8de0d1fd97b4eb5f85f1eb7079ca3029f3b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 06:19:32 -0400 Subject: [PATCH 15/24] WIP: SG-6 --- src/v3/compiler/regen.dag | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/v3/compiler/regen.dag b/src/v3/compiler/regen.dag index 3291e6c3786..e1456ce98c8 100644 --- a/src/v3/compiler/regen.dag +++ b/src/v3/compiler/regen.dag @@ -1,5 +1,7 @@ module v3.compiler.regen +import std.types { FilePath } + // Lens regen registry. Each `data _entry: LensRegistryEntry` // record below is picked up structurally by the `regen_lens` binary // (`dag.declarations().filter(meta_tag == LensRegistryEntry)`), which @@ -12,11 +14,18 @@ module v3.compiler.regen // record here with the three paths filled in, and // `cargo run -p v3-compiler --bin regen_lens` picks it up. No // per-lens Rust driver is added or edited. +// +// `lens_file` and `generated_file` compose from `std.types::FilePath` +// (= `String where non_empty`) per MODELING M9 — we attach to the +// existing path vocabulary rather than reintroduce a raw `String` +// for filesystem paths. Refinement means empty-string entries +// cannot be lowered into the registry, closing the "illegal states +// representable" hole that a raw `String` would leave open. type LensRegistryEntry { name: String - lens_file: String - generated_file: String + lens_file: FilePath + generated_file: FilePath } data lens_cost_entry: LensRegistryEntry = { From 368e6c8decd35e18e5977227b609e90abeb98a24 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 07:28:42 -0400 Subject: [PATCH 16/24] WIP: SG-6 --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index b05f2ab1ecf..cba34455e34 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -692,7 +692,7 @@ The triple-ratchet in `sg6_hand_authored_census_test.rs` keeps them from divergi Dissolution trigger: add a shared `tests/common` helper that walks `regen.dag` and returns a lens source path keyed by registry `name`; re-point every `m2_lens_*_migration_test.rs::lens_path()` at that helper. Once the source-side path is sourced exclusively from the registry, the `lens_file` column of the triple ratchet collapses into a dependency on the same helper (the SG-6 census then pins only `name` + `generated_file`, with `lens_file` re-derived at read time). `include_str!` on the generated output remains compile-time and stays hardcoded — that bridge dissolves only when/if the lib adopts a runtime-loaded generated-file surface. -**Deferral: SG-6 `regen_lens` CLI end-to-end smoke test (XS, follow-up).** PR #560 covers the CLI surface structurally (arg parsing, bootstrap diagnostics, missing/duplicate/unknown-selector paths all tested via unit-shape tests) and manually via injected duplicates. The unified binary is not yet exercised end-to-end inside a test — adding `--lens ` exit-code + stderr coverage is cheap and belongs here; a positive-path smoke needs either `--dry-run` on the driver or a temp-output plumbing path so it does not race with migration-snapshot tests that read the committed generated files. +**Deferral: SG-6 `regen_lens` CLI end-to-end smoke test (XS, follow-up).** PR #560's `sg6_hand_authored_census_test.rs` ratchets the driver's read-side invariants **structurally** — it walks the bootstrap Dag the same way `regen_lens::read_registry` does and asserts the registry's shape, triple pinning, uniqueness of `name` / `generated_file`, and singleton `--lens ` resolution. **No test actually invokes the `regen_lens` binary.** Argument parsing (`parse_args`), bootstrap-diagnostic exit codes, the unknown-selector stderr path, and the success-case write path are all only verified manually today (via synthetic-duplicate and unknown-selector runs during PR development). The negative smoke (`--lens ` → nonzero exit + structured stderr) is the cheapest first move and belongs here; a positive-path smoke needs either a `--dry-run` flag on the driver or a temp-output plumbing path so it does not race with migration-snapshot tests that read the committed generated files. **Deferral: SG-6 remaining lane surfaces (XL aggregate, follow-up PRs).** - `src/v3/compiler/src/pipeline_authority.rs` (276 lines): the text-scanning body parser for `pipeline.dag`'s `fn compile { ... }` body should dissolve into a declared `data pipeline_compile_order: List` record consumed structurally (same pattern as `LensRegistryEntry`). From 8e4d8805847f8334b905756e6703f1fe8fd7b715 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 11:58:26 -0400 Subject: [PATCH 17/24] WIP: SG-6 --- src/v3/compiler/regen.dag | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/v3/compiler/regen.dag b/src/v3/compiler/regen.dag index e1456ce98c8..3ec592c7f60 100644 --- a/src/v3/compiler/regen.dag +++ b/src/v3/compiler/regen.dag @@ -57,3 +57,19 @@ data lens_unused_parameters_entry: LensRegistryEntry = { lens_file: "src/v3/lenses/unused_parameters.dag" generated_file: "src/v3/compiler/src/lens_unused_parameters_generated.rs" } + +// Absorbed from SG-4 prep (PR #562): `regen_infer_helpers.rs` was +// landed as a per-file regen driver mirroring the deleted +// `regen_lens_*` bins, explicitly flagged in its own header as +// "SG-6 owns folding all of these into a single generic regen +// target". This is that absorption. `infer_helpers.dag` is a lens +// in the broad sense the registry uses (reads substrate Dag, emits +// a Rust module via `emit_rust_module`), so it fits the existing +// `LensRegistryEntry` shape without a rename. A future `.dag` +// artifact that is genuinely non-lens would trigger the widening +// to a `GeneratedArtifactEntry` carrier. +data lens_infer_helpers_entry: LensRegistryEntry = { + name: "infer_helpers" + lens_file: "src/v3/lenses/infer_helpers.dag" + generated_file: "src/v3/compiler/src/infer_helpers_generated.rs" +} From d064848add52c85bf2662ea693e27eb026aa28ca Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 12:00:00 -0400 Subject: [PATCH 18/24] WIP: SG-6 --- .../compiler/src/bin/regen_infer_helpers.rs | 56 -------------- .../sg4_prep_infer_helpers_freshness_test.rs | 8 +- .../sg6_hand_authored_census_test.rs | 76 +++++++++++++++++++ 3 files changed, 81 insertions(+), 59 deletions(-) delete mode 100644 src/v3/compiler/src/bin/regen_infer_helpers.rs diff --git a/src/v3/compiler/src/bin/regen_infer_helpers.rs b/src/v3/compiler/src/bin/regen_infer_helpers.rs deleted file mode 100644 index c5ac6285cfd..00000000000 --- a/src/v3/compiler/src/bin/regen_infer_helpers.rs +++ /dev/null @@ -1,56 +0,0 @@ -// SG-4 prep regen driver. Mirrors the four `regen_lens_*` binaries -// rather than introducing a new pattern; SG-6 owns folding all of -// these into a single generic regen target ("adding a generated -// module no longer requires hand-editing Rust drivers"). Until then -// each regen target is its own bin. - -use std::io::Write; -use std::path::PathBuf; -use std::process::{Command, Stdio}; - -use v3_compiler::compile_to_dag; -use v3_compiler::emit_rust::emit_rust_module; - -const HEADER: &str = "// AUTO-GENERATED from `src/v3/lenses/infer_helpers.dag` via\n\ - // `emit_rust_module`. Regenerate instead of hand-editing.\n\n"; - -fn main() { - let source_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("lenses") - .join("infer_helpers.dag"); - let source = std::fs::read_to_string(&source_path).expect("read infer_helpers.dag"); - let dag = compile_to_dag(&source, source_path.to_string_lossy().as_ref()) - .expect("infer_helpers.dag compiles"); - let raw = emit_rust_module(&dag).expect("emit infer_helpers module"); - let combined = format!("{HEADER}{raw}"); - - let mut child = Command::new("rustfmt") - .arg("--emit") - .arg("stdout") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("spawn rustfmt"); - child - .stdin - .as_mut() - .unwrap() - .write_all(combined.as_bytes()) - .unwrap(); - let output = child.wait_with_output().expect("rustfmt"); - assert!(output.status.success(), "rustfmt failed"); - let formatted = String::from_utf8(output.stdout).expect("utf8"); - - const OUT_REL: &str = "src/v3/compiler/src/infer_helpers_generated.rs"; - assert!( - v3_compiler::generated_files::GENERATED_FILES.contains(&OUT_REL), - "SG-0 producer manifest mismatch: `{OUT_REL}` is not in GENERATED_FILES — \ - add it to REGEN_OUTPUTS in src/v3/compiler/build.rs." - ); - let out_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("src") - .join("infer_helpers_generated.rs"); - std::fs::write(&out_path, &formatted).expect("write infer_helpers_generated.rs"); - println!("wrote {}", out_path.display()); -} diff --git a/src/v3/compiler/tests/integration/sg4_prep_infer_helpers_freshness_test.rs b/src/v3/compiler/tests/integration/sg4_prep_infer_helpers_freshness_test.rs index 68d27c52be9..2169611452a 100644 --- a/src/v3/compiler/tests/integration/sg4_prep_infer_helpers_freshness_test.rs +++ b/src/v3/compiler/tests/integration/sg4_prep_infer_helpers_freshness_test.rs @@ -82,14 +82,16 @@ fn infer_helpers_generated_module_matches_checked_in_snapshot() { assert_eq!( fresh.trim(), checked_in_generated_module().trim(), - "checked-in generated module is stale; regenerate infer_helpers_generated.rs from infer_helpers.dag via `cargo run -p v3-compiler --bin regen_infer_helpers`" + "checked-in generated module is stale; regenerate infer_helpers_generated.rs from infer_helpers.dag via `cargo run -p v3-compiler --bin regen_lens -- --lens infer_helpers` (absorbed into SG-6's unified regen driver on PR #560)" ); } /// Regenerate helper. Writes the freshly emitted module straight to /// disk so the next `cargo test` run sees a clean tree. Equivalent to -/// `cargo run -p v3-compiler --bin regen_infer_helpers` — kept as an -/// `#[ignore]` test for parity with the lens-migration regen helpers. +/// `cargo run -p v3-compiler --bin regen_lens -- --lens infer_helpers` +/// (SG-6 absorbed the former `regen_infer_helpers` bin into the unified +/// driver on PR #560) — kept as an `#[ignore]` test for parity with +/// the lens-migration regen helpers. /// /// Run with: `cargo test -p v3-compiler --test integration -- --ignored /// emit_infer_helpers_snapshot`. diff --git a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs index 4ce5d5a08fd..cf1887c3fdb 100644 --- a/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs +++ b/src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs @@ -54,6 +54,7 @@ use std::collections::{BTreeSet, HashMap}; use std::path::PathBuf; +use std::process::Command; use v3_compiler::dag::{Dag, Declaration, FieldValue, LiteralBits, ValueBody}; @@ -65,6 +66,10 @@ fn bin_dir() -> PathBuf { manifest_dir().join("src").join("bin") } +fn workspace_root() -> PathBuf { + manifest_dir().join("..").join("..").join("..") +} + /// Enumerate every entry under `src/v3/compiler/src/bin/` that Cargo's /// auto-discovery would promote to a bin target. Cargo picks up two /// shapes out of that directory: @@ -218,6 +223,11 @@ fn sg6_regen_dag_registry_triples_are_pinned() { "src/v3/lenses/cost.dag", "src/v3/compiler/src/lens_cost_symbolic_generated.rs", ), + ( + "infer_helpers", + "src/v3/lenses/infer_helpers.dag", + "src/v3/compiler/src/infer_helpers_generated.rs", + ), ( "provenance", "src/v3/lenses/provenance.dag", @@ -332,3 +342,69 @@ fn sg6_lens_registry_names_resolve_to_singleton_entry() { ); } } + +// Director/Codex follow-up on #560: prove the real CLI path works, not +// just the structural registry ratchets. This smoke test runs the +// built `regen_lens` binary against a single concrete registry entry +// and asserts three things: +// 1. `--lens ` exits successfully, +// 2. stdout reports the expected generated target path, and +// 3. the checked-in generated file is unchanged after the run. +// +// If the file bytes change, restore the original snapshot before +// failing so a local red test does not leave the worktree dirty. +budgeted_test! { + 15_000, + sg6_regen_lens_cli_smoke_regenerates_named_entry_without_drift, + { + let dag = Dag::new(); + assert!( + dag.diagnostics().is_empty(), + "bootstrap should load `src/v3/compiler/regen.dag` cleanly, got {:?}", + dag.diagnostics().iter().collect::>() + ); + + let row = read_registry_rows(&dag) + .into_iter() + .find(|row| row.name == "cost") + .expect("registry row for `cost`"); + + let out_path = workspace_root().join(&row.generated_file); + let before = std::fs::read(&out_path).expect("read checked-in generated file"); + + let output = Command::new(env!("CARGO_BIN_EXE_regen_lens")) + .current_dir(manifest_dir()) + .arg("--lens") + .arg(&row.name) + .output() + .expect("run regen_lens binary"); + + assert!( + output.status.success(), + "regen_lens --lens {} failed:\nstdout:\n{}\nstderr:\n{}", + row.name, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + + let stdout = String::from_utf8(output.stdout).expect("regen_lens stdout should be utf-8"); + assert_eq!( + stdout.trim(), + format!("wrote {}", out_path.display()), + "`regen_lens --lens {}` should report the single generated target it rewrote", + row.name, + ); + + let after = std::fs::read(&out_path).expect("read regenerated file"); + if after != before { + std::fs::write(&out_path, &before) + .expect("restore checked-in generated file after smoke drift"); + } + assert_eq!( + after, before, + "`regen_lens --lens {}` changed `{}`. The smoke test expects the CLI path to be clean against the checked-in snapshot; if this fails, regenerate in the same PR that updates the snapshot.", + row.name, + row.generated_file, + ); + } +} From bef5cdd7c471d4baa75144c21a55a24ccfc0afb6 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 14:03:28 -0400 Subject: [PATCH 19/24] WIP: SG-6 --- .../tests/integration/sg0_census_test.rs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/v3/compiler/tests/integration/sg0_census_test.rs b/src/v3/compiler/tests/integration/sg0_census_test.rs index d53e5ea7620..0fc82b46c28 100644 --- a/src/v3/compiler/tests/integration/sg0_census_test.rs +++ b/src/v3/compiler/tests/integration/sg0_census_test.rs @@ -43,6 +43,7 @@ const CENSUS_ROOT: &str = "src/v3/compiler"; // Removing an entry means the owning lane has retired the file; // adding an entry is forbidden outside SG-0 without director // sign-off. +<<<<<<< HEAD // // SG-4 prep carries two entries that SG-6 will retire together: // - `bin/regen_infer_helpers.rs` (the per-helper regen binary) @@ -52,6 +53,25 @@ const CENSUS_ROOT: &str = "src/v3/compiler"; // single generic target, both entries collapse alongside the four // `regen_lens_*` lines above. Director sign-off // (clever-swift-141 brief, 2026-04-19) covers the temporary +2. +======= +// SG-6 landing (PR #560): the four per-lens regen bins +// (`regen_lens_cost.rs`, `regen_lens_cost_symbolic.rs`, +// `regen_lens_structural_resolution.rs`, `regen_lens_unused_parameters.rs`) +// and SG-4 prep's `regen_infer_helpers.rs` all folded into a single +// `regen_lens.rs` shim driven by `src/v3/compiler/regen.dag`'s +// `LensRegistryEntry` records. Five retirements; one net-new entry +// (`regen_lens.rs`). The new `sg6_hand_authored_census_test.rs` +// pins the reduced bin census + full `(name, lens_file, +// generated_file)` registry tuples + `--lens` singleton resolve + +// end-to-end CLI smoke; it is hand-authored test infrastructure and +// belongs on this list. +// +// SG-6 owns the remaining fold: the per-helper freshness test +// (`sg4_prep_infer_helpers_freshness_test.rs`) still mirrors the +// per-lens migration tests' snapshot pattern. It collapses into a +// single generic snapshot gate in a later SG-6 sub-PR, per the +// Director sign-off (clever-swift-141 brief, 2026-04-19). +>>>>>>> 3b53f80f3 (WIP: SG-6) // // Stage 3b DB-1 parse/apply ratchet bump — PR #564 adds one // hand-authored integration file, @@ -65,11 +85,7 @@ const CENSUS_ROOT: &str = "src/v3/compiler"; // not a precedent for adding ad hoc integration files. const EXPECTED_HAND_AUTHORED: &[&str] = &[ "src/v3/compiler/build.rs", - "src/v3/compiler/src/bin/regen_infer_helpers.rs", - "src/v3/compiler/src/bin/regen_lens_cost.rs", - "src/v3/compiler/src/bin/regen_lens_cost_symbolic.rs", - "src/v3/compiler/src/bin/regen_lens_structural_resolution.rs", - "src/v3/compiler/src/bin/regen_lens_unused_parameters.rs", + "src/v3/compiler/src/bin/regen_lens.rs", "src/v3/compiler/src/bin/regen_v3.rs", "src/v3/compiler/src/bin/self_host_fixed_point.rs", "src/v3/compiler/src/bootstrap.rs", @@ -139,6 +155,7 @@ const EXPECTED_HAND_AUTHORED: &[&str] = &[ "src/v3/compiler/tests/integration/real_stdlib_parse_smoke.rs", "src/v3/compiler/tests/integration/sg0_census_test.rs", "src/v3/compiler/tests/integration/sg4_prep_infer_helpers_freshness_test.rs", + "src/v3/compiler/tests/integration/sg6_hand_authored_census_test.rs", "src/v3/compiler/tests/integration/thesis_parallelism_test.rs", "src/v3/compiler/tests/integration/thesis_validation_test.rs", "src/v3/compiler/tests/lane2_stage_2f_dimension_test.rs", From 0df48cba6e0f927c23e682b522c6b8676edfe743 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 12:03:06 -0400 Subject: [PATCH 20/24] WIP: SG-6 --- src/v3/compiler/tests/integration/sg0_census_test.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/v3/compiler/tests/integration/sg0_census_test.rs b/src/v3/compiler/tests/integration/sg0_census_test.rs index 0fc82b46c28..710a3da1e66 100644 --- a/src/v3/compiler/tests/integration/sg0_census_test.rs +++ b/src/v3/compiler/tests/integration/sg0_census_test.rs @@ -43,17 +43,6 @@ const CENSUS_ROOT: &str = "src/v3/compiler"; // Removing an entry means the owning lane has retired the file; // adding an entry is forbidden outside SG-0 without director // sign-off. -<<<<<<< HEAD -// -// SG-4 prep carries two entries that SG-6 will retire together: -// - `bin/regen_infer_helpers.rs` (the per-helper regen binary) -// - `tests/integration/sg4_prep_infer_helpers_freshness_test.rs` -// (the per-helper regenerate→diff-empty ratchet) -// When SG-6 folds all regen drivers and snapshot gates into a -// single generic target, both entries collapse alongside the four -// `regen_lens_*` lines above. Director sign-off -// (clever-swift-141 brief, 2026-04-19) covers the temporary +2. -======= // SG-6 landing (PR #560): the four per-lens regen bins // (`regen_lens_cost.rs`, `regen_lens_cost_symbolic.rs`, // `regen_lens_structural_resolution.rs`, `regen_lens_unused_parameters.rs`) @@ -71,7 +60,6 @@ const CENSUS_ROOT: &str = "src/v3/compiler"; // per-lens migration tests' snapshot pattern. It collapses into a // single generic snapshot gate in a later SG-6 sub-PR, per the // Director sign-off (clever-swift-141 brief, 2026-04-19). ->>>>>>> 3b53f80f3 (WIP: SG-6) // // Stage 3b DB-1 parse/apply ratchet bump — PR #564 adds one // hand-authored integration file, From c4788d18295d72509463b70abc1c561ec3b0d8a3 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 12:05:48 -0400 Subject: [PATCH 21/24] WIP: SG-6 --- src/v3/compiler/regen.dag | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/v3/compiler/regen.dag b/src/v3/compiler/regen.dag index 3ec592c7f60..58b79f88e11 100644 --- a/src/v3/compiler/regen.dag +++ b/src/v3/compiler/regen.dag @@ -1,6 +1,6 @@ module v3.compiler.regen -import std.types { FilePath } +import std.types { FilePath, NonEmptyStr } // Lens regen registry. Each `data _entry: LensRegistryEntry` // record below is picked up structurally by the `regen_lens` binary @@ -15,15 +15,18 @@ import std.types { FilePath } // `cargo run -p v3-compiler --bin regen_lens` picks it up. No // per-lens Rust driver is added or edited. // -// `lens_file` and `generated_file` compose from `std.types::FilePath` -// (= `String where non_empty`) per MODELING M9 — we attach to the -// existing path vocabulary rather than reintroduce a raw `String` -// for filesystem paths. Refinement means empty-string entries -// cannot be lowered into the registry, closing the "illegal states -// representable" hole that a raw `String` would leave open. +// `name` composes from `std.types::NonEmptyStr` (= `String where +// non_empty`) so the `--lens ` selector key cannot encode an +// empty string. `lens_file` and `generated_file` compose from +// `std.types::FilePath` (also `String where non_empty`) per +// MODELING M9 — every field attaches to the existing path/identifier +// vocabulary rather than reintroduce a raw `String`. Refinement +// means empty-string entries cannot be lowered into the registry, +// closing the "illegal states representable" hole that a raw +// `String` would leave open across all three fields. type LensRegistryEntry { - name: String + name: NonEmptyStr lens_file: FilePath generated_file: FilePath } From 344a7e5034e2bb6ede711d5394b0a9c71963dc49 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 13:17:42 -0400 Subject: [PATCH 22/24] WIP: SG-6 --- ROADMAP.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index cba34455e34..9daca3a9090 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -681,8 +681,9 @@ Each sub-PR must reduce handwritten-Rust line count in `src/v3/compiler/src/emit **Landed (PR #560, SG-6 regen-driver cutover / partial lane).** - Four per-lens regen bins (`regen_lens_cost.rs`, `regen_lens_cost_symbolic.rs`, `regen_lens_structural_resolution.rs`, `regen_lens_unused_parameters.rs`) deleted. -- Single `src/v3/compiler/src/bin/regen_lens.rs` shim reads `src/v3/compiler/regen.dag`'s `LensRegistryEntry` records via `Dag::new()` and regenerates each declared `lens__generated.rs`. `read_registry` fails closed on duplicate `name` / duplicate `generated_file`. -- `sg6_hand_authored_census_test.rs` pins both the `src/bin/` census (`regen_lens.rs`, `regen_v3.rs`, `self_host_fixed_point.rs`) and the full `(name, lens_file, generated_file)` registry triple, plus uniqueness of `name` / `generated_file` and singleton resolution of `--lens `. +- SG-4 prep's `regen_infer_helpers.rs` absorbed into the same registry per its own "SG-6 owns folding all of these" header — `infer_helpers` entry added to `regen.dag`, bin deleted, SG-4 freshness test's regen-command message repointed at `regen_lens -- --lens infer_helpers`. +- Single `src/v3/compiler/src/bin/regen_lens.rs` shim reads `src/v3/compiler/regen.dag`'s `LensRegistryEntry` records via `Dag::new()` and regenerates each declared `*_generated.rs`. `name` composes from `std.types::NonEmptyStr`; `lens_file` / `generated_file` from `std.types::FilePath`. `read_registry` fails closed on duplicate `name` / duplicate `generated_file`. +- `sg6_hand_authored_census_test.rs` pins: (a) the `src/bin/` census — both flat-file and directory-form Cargo autobin shapes; (b) the full `(name, lens_file, generated_file)` registry triple across all 6 lenses; (c) uniqueness of `name` / `generated_file`; (d) singleton resolution of `--lens `; (e) **positive-path CLI smoke** via `sg6_regen_lens_cli_smoke_regenerates_named_entry_without_drift`, which spawns the bin through `CARGO_BIN_EXE_regen_lens`, asserts exit success, stdout target-path format, and byte-identity of the regenerated snapshot. **Deferral: SG-6 registry consumer consolidation (S, follow-up).** `regen.dag` is now the primary authority for `(name, lens_file, generated_file)` triples, but two classes of downstream consumer still mirror those paths instead of resolving through the registry: 1. Every `src/v3/compiler/tests/integration/m2_lens_*_migration_test.rs` has a local `lens_path()` and a `checked_in_generated_module()` backed by `include_str!`. @@ -692,7 +693,7 @@ The triple-ratchet in `sg6_hand_authored_census_test.rs` keeps them from divergi Dissolution trigger: add a shared `tests/common` helper that walks `regen.dag` and returns a lens source path keyed by registry `name`; re-point every `m2_lens_*_migration_test.rs::lens_path()` at that helper. Once the source-side path is sourced exclusively from the registry, the `lens_file` column of the triple ratchet collapses into a dependency on the same helper (the SG-6 census then pins only `name` + `generated_file`, with `lens_file` re-derived at read time). `include_str!` on the generated output remains compile-time and stays hardcoded — that bridge dissolves only when/if the lib adopts a runtime-loaded generated-file surface. -**Deferral: SG-6 `regen_lens` CLI end-to-end smoke test (XS, follow-up).** PR #560's `sg6_hand_authored_census_test.rs` ratchets the driver's read-side invariants **structurally** — it walks the bootstrap Dag the same way `regen_lens::read_registry` does and asserts the registry's shape, triple pinning, uniqueness of `name` / `generated_file`, and singleton `--lens ` resolution. **No test actually invokes the `regen_lens` binary.** Argument parsing (`parse_args`), bootstrap-diagnostic exit codes, the unknown-selector stderr path, and the success-case write path are all only verified manually today (via synthetic-duplicate and unknown-selector runs during PR development). The negative smoke (`--lens ` → nonzero exit + structured stderr) is the cheapest first move and belongs here; a positive-path smoke needs either a `--dry-run` flag on the driver or a temp-output plumbing path so it does not race with migration-snapshot tests that read the committed generated files. +**Deferral: SG-6 `regen_lens` CLI error-path smoke (XS, follow-up).** PR #560 landed the positive-path CLI smoke (see Landed, item e). The remaining gap is behavioral coverage of the error paths — `parse_args` on unknown flags, bootstrap-diagnostic exit codes, and the unknown-selector stderr path — none of which are exercised by any test today. The cheapest shape is a negative-path smoke that spawns `regen_lens -- --lens ` through `CARGO_BIN_EXE_regen_lens`, asserts exit != 0, and greps stderr for the registry-name enumeration the driver already prints. Landing this closes the last CLI-surface gap; nothing structural in the driver needs to change for it. **Deferral: SG-6 remaining lane surfaces (XL aggregate, follow-up PRs).** - `src/v3/compiler/src/pipeline_authority.rs` (276 lines): the text-scanning body parser for `pipeline.dag`'s `fn compile { ... }` body should dissolve into a declared `data pipeline_compile_order: List` record consumed structurally (same pattern as `LensRegistryEntry`). From e8f5ccfd73df6929e843ae95c0e1f7949fb580a8 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 14:29:51 -0400 Subject: [PATCH 23/24] WIP: SG-6 --- src/v3/compiler/build.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/v3/compiler/build.rs b/src/v3/compiler/build.rs index b5e92db8879..ecf1aa5f041 100644 --- a/src/v3/compiler/build.rs +++ b/src/v3/compiler/build.rs @@ -198,6 +198,14 @@ fn main() { // match exactly what the regen drivers write to disk as // committed Rust. const REGEN_OUTPUTS: &[&str] = &[ + // SG-5 substrate / runtime-mirror projections, generated from + // `src/v3/std/substrate.dag` + `src/v3/compiler/runtime_mirrors.dag` + // and consumed by hand-authored Rust via `include!(...)`. + // Registering them here keeps SG-0's census honest: they carry + // `// AUTO-GENERATED` headers AND are listed as producer-owned + // outputs, so the content header alone never masquerades as + // generated (SG-0's `sg0_generated_partition_is_producer_owned` + // invariant). "src/v3/compiler/src/dag_branch_generated.rs", "src/v3/compiler/src/dag_cluster_generated.rs", "src/v3/compiler/src/dag_cost_generated.rs", From e03d075a460605529e503d943cac82022951fcd1 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 19 Apr 2026 14:44:16 -0400 Subject: [PATCH 24/24] WIP: SG-6 --- src/v3/compiler/src/bin/regen_lens.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/v3/compiler/src/bin/regen_lens.rs b/src/v3/compiler/src/bin/regen_lens.rs index 3439666b3ef..d2e532f91c8 100644 --- a/src/v3/compiler/src/bin/regen_lens.rs +++ b/src/v3/compiler/src/bin/regen_lens.rs @@ -20,6 +20,7 @@ use std::process::{Command, ExitCode, Stdio}; use v3_compiler::compile_to_dag; use v3_compiler::dag::{Dag, FieldValue, LiteralBits, ValueBody}; use v3_compiler::emit_rust::emit_rust_module; +use v3_compiler::generated_files::GENERATED_FILES; const LENS_REGISTRY_ENTRY_TYPE: &str = "LensRegistryEntry"; @@ -174,6 +175,30 @@ fn require_string( } fn regen_entry(root: &Path, entry: &Entry) -> Result<(), String> { + // Single-authority gate: the registry's `generated_file` path + // must also be registered in `REGEN_OUTPUTS` (surfaced as + // `v3_compiler::generated_files::GENERATED_FILES`). SG-0 treats + // that manifest as the sole producer-owned partition; if + // `regen.dag` and `REGEN_OUTPUTS` drift, the driver would + // silently write to a path the SG-0 census doesn't know about. + // Fail closed here rather than rely on the downstream census to + // notice — the error points the reviewer at the two authorities + // that must stay in lockstep. + if !GENERATED_FILES.iter().any(|p| *p == entry.generated_file) { + return Err(format!( + "registry / manifest drift: `regen.dag` declares \ + `generated_file = \"{path}\"` (lens `{lens}`) but the \ + path is not registered in `REGEN_OUTPUTS` in \ + `src/v3/compiler/build.rs`. Add the path to `REGEN_OUTPUTS` \ + (or remove it from `regen.dag`) so the two authorities stay \ + in lockstep. Both are SG-0's producer-owned manifest; \ + writing to a path outside the manifest would be silent \ + drift.", + path = entry.generated_file, + lens = entry.name, + )); + } + let lens_path = root.join(&entry.lens_file); let out_path = root.join(&entry.generated_file);