Skip to content
80 changes: 80 additions & 0 deletions dag/gunbc/commit_workflow.dag
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,86 @@ data commit_gate_roster: List<CommitCheckEnrollment> = [
},
surfaces: [GithubActionsCiJob]
},
CommitCheckEnrollment {
check: CommitWitnessClaim {
entry: "src/v2/test/claim/loop_multiplicity_measure_test.dag",
check_fns: [
"loop_strict_measure_infers_proven_termination",
"loop_perturbed_measure_is_refused",
"loop_lone_non_increasing_measure_is_refused",
"loop_lexicographic_measure_proves_with_strict_dimension",
"loop_unknown_dimension_poisons_lexicographic_measure",
"loop_missing_measure_is_refused",
"live_registry_proves_loop_measure",
"nested_unproven_loop_still_poisons_the_fold"
],
kind: CorpusWitnessKind,
span: SpanUndeclared
},
surfaces: [GithubActionsCiJob]
},
CommitCheckEnrollment {
check: CommitWitnessClaim {
entry: "src/v2/test/claim/complexity/fold_lowering_test.dag",
check_fns: [
"fold_probe_ingest_is_accepted",
"fold_callee_name_survives_parse",
"fold_collection_name_survives_parse",
"fold_call_lowers_to_terminating_loop",
"non_fold_call_refuses",
"bodyless_fold_call_refuses_no_loop_fabricated"
],
kind: CorpusWitnessKind,
span: SpanUndeclared
},
surfaces: [GithubActionsCiJob]
},
CommitCheckEnrollment {
check: CommitWitnessClaim {
entry: "src/v2/test/claim/complexity/r1_fold_analysis_test.dag",
check_fns: [
"quadratic_fold_has_suspect",
"linear_fold_has_no_suspect",
"identity_fold_has_no_finding",
"real_branch_body_quadratic_is_caught"
],
kind: CorpusWitnessKind,
span: SpanUndeclared
},
surfaces: [GithubActionsCiJob]
},
CommitCheckEnrollment {
check: CommitWitnessClaim {
entry: "src/v2/lens/complexity_r1_accumulator_copy_test.dag",
check_fns: [
"red_list_append_left_is_suspect",
"red_list_snoc_left_is_suspect",
"red_map_merge_overlay_is_suspect",
"green_map_merge_base_is_clean",
"green_list_append_acc_in_noncopied_port_is_clean",
"green_unregistered_combiner_is_clean",
"opaque_copied_port_is_analysis_opaque"
],
kind: CorpusWitnessKind,
span: SpanUndeclared
},
surfaces: [GithubActionsCiJob]
},
CommitCheckEnrollment {
check: CommitWitnessClaim {
entry: "src/v2/test/claim/loop_infer_iteration_test.dag",
check_fns: [
"loop_infer_invariant_body_accepts_with_carrier_type",
"loop_infer_divergent_body_is_refused",
"loop_infer_refinement_shaped_body_is_refused",
"loop_infer_unregistered_measure_is_refused",
"loop_infer_missing_measure_is_refused"
],
kind: CorpusWitnessKind,
span: SpanUndeclared
},
surfaces: [GithubActionsCiJob]
},
CommitCheckEnrollment {
check: CommitWitnessClaim {
entry: "dag/test/claim/parse_test.dag",
Expand Down
89 changes: 86 additions & 3 deletions src/v1/stage0/src/bin/claim_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,20 @@ fn require_value(args: &[String], idx: usize, flag: &str) -> Result<String, Exit
}
}

/// Path-valued arguments resolve against the PROCESS CWD at the CLI boundary, refusing
/// on a nonexistent path — never falling back to the compile-time-baked workspace root
/// (`v1_compiler::cli_run::resolve_cli_path_arg`; DESIGN §5 fail-open closed there).
fn require_path_value(args: &[String], idx: usize, flag: &str) -> Result<String, ExitCode> {
let given = require_value(args, idx, flag)?;
match v1_compiler::cli_run::resolve_cli_path_arg("claim_batch", flag, &given) {
Ok(resolved) => Ok(resolved),
Err(msg) => {
eprintln!("{msg}");
Err(ExitCode::from(2))
}
}
}

struct EntryGroup {
entry: String,
functions: Vec<String>,
Expand Down Expand Up @@ -183,11 +197,11 @@ fn parse_args(args: &[String]) -> Result<ParsedArgs, ExitCode> {
match args[i].as_str() {
"--source-root" => {
i += 1;
source_roots.push(require_value(args, i, "--source-root")?);
source_roots.push(require_path_value(args, i, "--source-root")?);
}
"--entry" => {
i += 1;
let entry = require_value(args, i, "--entry")?;
let entry = require_path_value(args, i, "--entry")?;
entry_groups.push(EntryGroup {
entry,
functions: Vec::new(),
Expand Down Expand Up @@ -223,7 +237,7 @@ fn parse_args(args: &[String]) -> Result<ParsedArgs, ExitCode> {
"--roster-from-discovery" => roster_from_discovery = true,
"--scan-dir" => {
i += 1;
scan_dirs.push(require_value(args, i, "--scan-dir")?);
scan_dirs.push(require_path_value(args, i, "--scan-dir")?);
}
"--notice-title" => {
i += 1;
Expand Down Expand Up @@ -639,6 +653,75 @@ fn main() -> ExitCode {
}
}

#[cfg(test)]
mod cli_path_resolution_wiring_tests {
use super::parse_args;
use v1_compiler::cli_run::workspace_root;

fn args(v: &[&str]) -> Vec<String> {
std::iter::once("claim_batch")
.chain(v.iter().copied())
.map(str::to_string)
.collect()
}

/// Absolute existing paths pass parse_args unchanged (resolution applied, no rewrite).
#[test]
fn absolute_existing_args_parse_and_survive_resolution() {
let ws = workspace_root();
let root = ws.join("dag").to_string_lossy().into_owned();
let entry = ws
.join("dag/test/claim/commit_workflow_witness_test.dag")
.to_string_lossy()
.into_owned();
let parsed = parse_args(&args(&[
"--source-root",
&root,
"--entry",
&entry,
"--function",
"commit_workflow_witnesses",
]))
.unwrap_or_else(|_| panic!("absolute existing paths must parse"));
assert_eq!(parsed.source_roots, vec![root]);
assert_eq!(parsed.entry_groups.len(), 1);
assert_eq!(parsed.entry_groups[0].entry, entry);
}

/// A nonexistent --source-root refuses at the CLI boundary (exit-code error), never
/// proceeding to a partial run.
#[test]
fn nonexistent_source_root_refuses_at_parse() {
assert!(parse_args(&args(&["--source-root", "/no/such/tree"])).is_err());
}

/// Relative args resolve against the PROCESS CWD, not the baked workspace root.
/// `cargo test` runs bin unit tests with cwd == the package dir (src/v1/stage0),
/// where `dag` does not exist — while it DOES exist under the baked workspace root.
/// parse_args must therefore agree with the cwd, whichever it is: refuse when
/// cwd/dag is absent (a baked-root fallback would accept), accept when present.
#[test]
fn relative_source_root_follows_process_cwd_not_baked_root() {
let cwd = std::env::current_dir().expect("test cwd");
let cwd_has_dag = cwd.join("dag").is_dir();
let result = parse_args(&args(&[
"--source-root",
"dag",
"--entry",
"dag/test/claim/commit_workflow_witness_test.dag",
"--function",
"commit_workflow_witnesses",
]));
assert_eq!(
result.is_ok(),
cwd_has_dag,
"relative --source-root must resolve against the process cwd {} (dag present: {cwd_has_dag}), never the baked workspace root {}",
cwd.display(),
workspace_root().display()
);
}
}

#[cfg(test)]
mod discovery_exclude_tests {
use v1_compiler::cli_run::floor_discovery_path_excluded;
Expand Down
18 changes: 16 additions & 2 deletions src/v1/stage0/src/bin/claim_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ fn require_value(args: &[String], idx: usize, flag: &str) -> Result<String, Exit
}
}

/// Path-valued arguments resolve against the PROCESS CWD at the CLI boundary, refusing
/// on a nonexistent path — never falling back to the compile-time-baked workspace root
/// (`v1_compiler::cli_run::resolve_cli_path_arg`; DESIGN §5 fail-open closed there).
fn require_path_value(args: &[String], idx: usize, flag: &str) -> Result<String, ExitCode> {
let given = require_value(args, idx, flag)?;
match v1_compiler::cli_run::resolve_cli_path_arg("claim_executor", flag, &given) {
Ok(resolved) => Ok(resolved),
Err(msg) => {
eprintln!("{msg}");
Err(ExitCode::from(2))
}
}
}

fn free_monoid_elems<'a>(value: &'a Value, ctx: &InterpContext) -> Result<Vec<&'a Value>, String> {
let mut out = Vec::new();
let mut cur = value;
Expand Down Expand Up @@ -1701,11 +1715,11 @@ fn run() -> Result<ExitCode, ExitCode> {
}
"--source-root" => {
i += 1;
source_roots.push(require_value(&args, i, "--source-root")?);
source_roots.push(require_path_value(&args, i, "--source-root")?);
}
"--plan-entry" => {
i += 1;
plan_entry = Some(require_value(&args, i, "--plan-entry")?);
plan_entry = Some(require_path_value(&args, i, "--plan-entry")?);
}
"--plan-function" => {
i += 1;
Expand Down
149 changes: 149 additions & 0 deletions src/v1/stage0/src/cli_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,155 @@ pub fn workspace_root() -> PathBuf {
.to_path_buf()
}

/// CLI-boundary path resolution for the claim bins (`claim_batch` / `claim_executor`).
///
/// `workspace_root()` above is baked from `env!("CARGO_MANIFEST_DIR")` at COMPILE time,
/// and part of the shared resolution pipeline (`pool_roots_abs`) anchors RELATIVE source
/// roots to that baked path while the module-content index reads them relative to the
/// process cwd. For the claim bins that meant a run from any other cwd (e.g. a git
/// worktree) silently mixed two trees — module contents from the cwd, import-graph facts
/// from the baked root — wrong answers with zero diagnostic (DESIGN §5 fail-open).
///
/// The bins therefore resolve their path-valued arguments HERE, at the CLI boundary,
/// with standard CLI semantics: a relative path resolves against the PROCESS CWD, and a
/// resolved path that does not exist is a refusal naming the argument, the given value,
/// and the resolution base — never a fallback to the baked root, never a partial run.
/// When the cwd IS the baked workspace root, the relative spelling and the absolutized
/// spelling denote the same file for every downstream consumer (cwd-anchored reads and
/// baked-root-anchored reads agree), so the given spelling passes through unchanged and
/// the normal case (and CI) stays byte-identical. Any other cwd absolutizes, so the
/// baked-root anchoring in the shared pipeline can never re-route the read.
pub fn resolve_cli_path_arg(bin: &str, flag: &str, given: &str) -> Result<String, String> {
if Path::new(given).is_absolute() {
return resolve_cli_path_arg_against(bin, flag, given, Path::new("/"));
}
let cwd = std::env::current_dir().map_err(|e| {
format!(
"{bin}: {flag} {given}: cannot resolve a relative CLI path — the process \
working directory is unavailable: {e}"
)
})?;
resolve_cli_path_arg_against(bin, flag, given, &cwd)
}

/// Core of [`resolve_cli_path_arg`] with an explicit resolution base (testable without
/// mutating the process-global cwd).
fn resolve_cli_path_arg_against(
bin: &str,
flag: &str,
given: &str,
base: &Path,
) -> Result<String, String> {
let given_path = Path::new(given);
if given_path.is_absolute() {
if !given_path.exists() {
return Err(format!(
"{bin}: {flag} {given}: absolute path does not exist; refusing (no \
fallback to the compiled-in workspace root {})",
workspace_root().display()
));
}
return Ok(given.to_string());
}
let resolved = base.join(given_path);
if !resolved.exists() {
return Err(format!(
"{bin}: {flag} {given}: resolved against the process working directory {} \
to {}, which does not exist; refusing — relative CLI paths resolve against \
the process cwd, never the compiled-in workspace root {} — run from the \
tree you meant or pass an absolute path",
base.display(),
resolved.display(),
workspace_root().display()
));
}
let ws = workspace_root();
if same_canonical_file(&base.to_string_lossy(), &ws.to_string_lossy()) {
return Ok(given.to_string());
}
Ok(resolved.to_string_lossy().into_owned())
}

#[cfg(test)]
mod cli_path_arg_resolution_tests {
use super::{resolve_cli_path_arg_against, workspace_root};

fn fixture_base(tag: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!("gunbc-cli-path-arg-{tag}-{}", std::process::id()))
}

/// (a) Relative arg with base == workspace root: byte-identical pass-through —
/// the exact string the bins forwarded before this boundary existed.
#[test]
fn relative_arg_at_workspace_root_passes_through_unchanged() {
let ws = workspace_root();
let resolved = resolve_cli_path_arg_against("claim_batch", "--source-root", "dag", &ws)
.expect("`dag` exists under the workspace root");
assert_eq!(resolved, "dag");
}

/// (b) Relative arg from a different cwd resolves against THAT cwd — the result is
/// the absolutized fixture path, never the bare relative spelling (which the shared
/// pipeline would re-anchor to the baked workspace root) and never a baked-root path.
#[test]
fn relative_arg_from_other_cwd_resolves_against_that_cwd() {
let base = fixture_base("other-cwd");
std::fs::create_dir_all(base.join("dag")).expect("create fixture tree");
std::fs::write(base.join("dag/mini.dag"), "module mini\n").expect("write fixture module");

let root = resolve_cli_path_arg_against("claim_batch", "--source-root", "dag", &base)
.expect("fixture tree exists under the fixture cwd");
assert_eq!(root, base.join("dag").to_string_lossy());
assert_ne!(
root, "dag",
"must absolutize away from the baked-root anchor"
);

let entry = resolve_cli_path_arg_against("claim_batch", "--entry", "dag/mini.dag", &base)
.expect("fixture entry exists under the fixture cwd");
assert_eq!(entry, base.join("dag/mini.dag").to_string_lossy());

let _ = std::fs::remove_dir_all(&base);
}

/// (c) Nonexistent relative path: refusal names the argument, the given value, and
/// the resolution base — and does NOT resolve against the baked workspace root even
/// though the path exists there (the discriminating red control for the fail-open).
#[test]
fn nonexistent_relative_arg_refuses_naming_flag_value_and_base() {
let base = fixture_base("refusal");
std::fs::create_dir_all(&base).expect("create empty fixture cwd");
// `dag` exists under the baked workspace root but NOT under `base`; a baked-root
// fallback would return Ok here — the refusal is the proof there is none.
let err = resolve_cli_path_arg_against("claim_batch", "--source-root", "dag", &base)
.expect_err("missing path must refuse, never fall back to the baked root");
assert!(err.contains("claim_batch"), "names the binary: {err}");
assert!(err.contains("--source-root"), "names the argument: {err}");
assert!(err.contains("dag"), "names the given value: {err}");
assert!(
err.contains(&base.display().to_string()),
"names the resolution base: {err}"
);
let _ = std::fs::remove_dir_all(&base);
}

/// Nonexistent absolute path refuses too — never proceeds partially.
#[test]
fn nonexistent_absolute_arg_refuses() {
let missing = fixture_base("absolute-missing").join("no/such/entry.dag");
let missing_str = missing.to_string_lossy().into_owned();
let err = resolve_cli_path_arg_against(
"claim_executor",
"--plan-entry",
&missing_str,
std::path::Path::new("/"),
)
.expect_err("missing absolute path must refuse");
assert!(err.contains("--plan-entry"), "names the argument: {err}");
assert!(err.contains(&missing_str), "names the given value: {err}");
}
}

/// Empty ingest-manifest placeholder excluded from the module index when a later
/// source root carries the host-emitted manifest (source-root ingest / closure gates).
const SOURCE_ROOT_INGEST_MANIFEST_STUB_REL: &str =
Expand Down
Loading
Loading