From a6805a6b4039e1d92176ac209b08d641aea940d3 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 10:01:16 +0300 Subject: [PATCH 1/9] feat(impact): add CLI impact adapter --- crates/commandf-cli/src/impact.rs | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 crates/commandf-cli/src/impact.rs diff --git a/crates/commandf-cli/src/impact.rs b/crates/commandf-cli/src/impact.rs new file mode 100644 index 00000000..786b8d10 --- /dev/null +++ b/crates/commandf-cli/src/impact.rs @@ -0,0 +1,75 @@ +use std::fs; +use std::io; +use std::path::PathBuf; + +use commandf_pkg::{ + build_context_graph, build_impact_report, diff_package_archives, LockedPackage, Lockfile, + PackageCache, PackageName, +}; + +pub fn run( + package: String, + before_lock: PathBuf, + before_cache: PathBuf, + after_lock: PathBuf, + after_cache: PathBuf, +) -> Result, Box> { + let package_name = PackageName::parse(package)?; + let before_lockfile = Lockfile::from_slice(&fs::read(before_lock)?)?; + let after_lockfile = Lockfile::from_slice(&fs::read(after_lock)?)?; + let before_locked = select_locked_package(&before_lockfile, package_name.as_str())?; + let after_locked = select_locked_package(&after_lockfile, package_name.as_str())?; + + let before_cache = PackageCache::new(before_cache); + let after_cache = PackageCache::new(after_cache); + before_cache.verify(&before_locked.sha256)?; + after_cache.verify(&after_locked.sha256)?; + + let before_bytes = read_locked_archive(&before_cache, before_locked)?; + let after_bytes = read_locked_archive(&after_cache, after_locked)?; + let diff = diff_package_archives( + package_name.to_string(), + &before_locked.version, + &before_locked.sha256, + &before_bytes, + &after_locked.version, + &after_locked.sha256, + &after_bytes, + )?; + let before_graph = build_context_graph(&before_lockfile, &before_cache)?; + let after_graph = build_context_graph(&after_lockfile, &after_cache)?; + let report = build_impact_report(&diff, &before_graph, &after_graph)?; + Ok(report.to_json_bytes()?) +} + +fn read_locked_archive(cache: &PackageCache, locked: &LockedPackage) -> io::Result> { + fs::read( + cache + .root() + .join("sha256") + .join(format!("{}.tgz", locked.sha256)), + ) +} + +fn select_locked_package<'a>( + lockfile: &'a Lockfile, + package_name: &str, +) -> Result<&'a LockedPackage, io::Error> { + let mut matches = lockfile + .packages + .iter() + .filter(|candidate| candidate.name == package_name); + let selected = matches.next().ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + format!("package {package_name} is not present in the lockfile"), + ) + })?; + if matches.next().is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("package {package_name} appears more than once in the lockfile"), + )); + } + Ok(selected) +} From ca25db3a5944ea3159e49d53f619dd0a4ba68071 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 10:03:23 +0300 Subject: [PATCH 2/9] feat(impact): expose commandf impact --- crates/commandf-cli/src/main.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/crates/commandf-cli/src/main.rs b/crates/commandf-cli/src/main.rs index eba618ae..966ef9a7 100644 --- a/crates/commandf-cli/src/main.rs +++ b/crates/commandf-cli/src/main.rs @@ -1,3 +1,4 @@ +mod impact; mod oracle; use std::ffi::OsStr; @@ -69,6 +70,19 @@ enum Command { #[arg(long, value_enum, default_value = "json")] format: OutputFormat, }, + Impact { + package: String, + #[arg(long)] + before_lock: PathBuf, + #[arg(long)] + before_cache: PathBuf, + #[arg(long)] + after_lock: PathBuf, + #[arg(long)] + after_cache: PathBuf, + #[arg(long, value_enum, default_value = "json")] + format: OutputFormat, + }, Classify { package: String, #[arg(long)] @@ -353,6 +367,25 @@ fn run(cli: Cli) -> Result> { OutputFormat::Json => io::stdout().write_all(&report.to_json_bytes()?)?, } } + Command::Impact { + package, + before_lock, + before_cache, + after_lock, + after_cache, + format, + } => { + let bytes = impact::run( + package, + before_lock, + before_cache, + after_lock, + after_cache, + )?; + match format { + OutputFormat::Json => io::stdout().write_all(&bytes)?, + } + } Command::Classify { package, before_lock, From 6431dfde1a77a2e241f6d88dd245462ff6c6cc92 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 10:04:39 +0300 Subject: [PATCH 3/9] test(impact): cover CLI fail-closed behavior --- crates/commandf-cli/tests/impact_behavior.rs | 245 +++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 crates/commandf-cli/tests/impact_behavior.rs diff --git a/crates/commandf-cli/tests/impact_behavior.rs b/crates/commandf-cli/tests/impact_behavior.rs new file mode 100644 index 00000000..f48f8056 --- /dev/null +++ b/crates/commandf-cli/tests/impact_behavior.rs @@ -0,0 +1,245 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use commandf_pkg::{LockedPackage, Lockfile, PackageCache, ResolvedDependency}; + +const PARENT_A_ARCHIVE: &[u8] = include_bytes!("fixtures/parent-a.tgz"); +const PARENT_B_ARCHIVE: &[u8] = include_bytes!("fixtures/parent-b.tgz"); +const SHARED_V1_ARCHIVE: &[u8] = include_bytes!("fixtures/shared-v1.tgz"); +const SHARED_V2_ARCHIVE: &[u8] = include_bytes!("fixtures/shared-v2.tgz"); + +fn commandf() -> Command { + Command::new(env!("CARGO_BIN_EXE_commandf")) +} + +#[test] +fn impact_help_exposes_pinned_before_after_inputs() { + let output = commandf() + .args(["impact", "--help"]) + .output() + .expect("commandf impact help must execute"); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("UTF-8 help"); + for expected in [ + "", + "--before-lock", + "--before-cache", + "--after-lock", + "--after-cache", + "--format", + ] { + assert!(stdout.contains(expected), "missing {expected}"); + } +} + +#[test] +fn impact_is_byte_identical_and_reports_dependency_evidence_without_severity() { + let root = unique_temp_dir("success"); + let (before_lock, before_cache, after_lock, after_cache) = write_impact_state(&root); + + let first = run_impact(&before_lock, &before_cache, &after_lock, &after_cache); + let second = run_impact(&before_lock, &before_cache, &after_lock, &after_cache); + assert_success(&first); + assert_success(&second); + assert_eq!(first.stdout, second.stdout); + + let json = String::from_utf8(first.stdout).expect("UTF-8 impact JSON"); + for expected in [ + "\"schema\": 1", + "\"package_name\": \"acme.subject\"", + "\"before_evidence\"", + "\"after_evidence\"", + "\"seeds\"", + "\"artifact_impacts\"", + "\"package_impacts\"", + "\"unresolved_boundaries\"", + "\"coverage\"", + ] { + assert!(json.contains(expected), "missing {expected}"); + } + for forbidden in ["\"breaking\"", "\"risky\"", "\"additive\""] { + assert!(!json.contains(forbidden), "impact invented severity: {forbidden}"); + } + + let _ = fs::remove_dir_all(root); +} + +#[test] +fn impact_rejects_schema_v1_and_corrupt_cache_without_stdout() { + let schema_root = unique_temp_dir("schema-v1"); + let (before_lock, before_cache, after_lock, after_cache) = write_impact_state(&schema_root); + fs::write( + &before_lock, + Lockfile::new(Vec::new(), Vec::new()).to_bytes().unwrap(), + ) + .unwrap(); + let output = run_impact(&before_lock, &before_cache, &after_lock, &after_cache); + assert_eq!(output.status.code(), Some(1)); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains("requires commandf.lock schema 2")); + let _ = fs::remove_dir_all(schema_root); + + let corrupt_root = unique_temp_dir("corrupt"); + let (before_lock, before_cache, after_lock, after_cache) = write_impact_state(&corrupt_root); + let lock = Lockfile::from_slice(&fs::read(&before_lock).unwrap()).unwrap(); + let subject = lock + .packages + .iter() + .find(|package| package.name == "acme.subject") + .unwrap(); + fs::write( + before_cache + .join("sha256") + .join(format!("{}.tgz", subject.sha256)), + b"corrupted", + ) + .unwrap(); + let output = run_impact(&before_lock, &before_cache, &after_lock, &after_cache); + assert_eq!(output.status.code(), Some(1)); + assert!(output.stdout.is_empty()); + let _ = fs::remove_dir_all(corrupt_root); +} + +fn write_impact_state(root: &Path) -> (PathBuf, PathBuf, PathBuf, PathBuf) { + fs::create_dir_all(root).unwrap(); + let before_cache_path = root.join("before-cache"); + let after_cache_path = root.join("after-cache"); + let before_lock_path = root.join("before.lock"); + let after_lock_path = root.join("after.lock"); + let before_cache = PackageCache::new(&before_cache_path); + let after_cache = PackageCache::new(&after_cache_path); + + let before_subject_sha = before_cache.put(PARENT_A_ARCHIVE).unwrap(); + let before_shared_sha = before_cache.put(SHARED_V1_ARCHIVE).unwrap(); + let after_subject_sha = after_cache.put(PARENT_B_ARCHIVE).unwrap(); + let after_shared_sha = after_cache.put(SHARED_V2_ARCHIVE).unwrap(); + + let mut before_dependencies = BTreeMap::new(); + before_dependencies.insert("acme.shared".to_owned(), "1.0.0".to_owned()); + let mut after_dependencies = BTreeMap::new(); + after_dependencies.insert("acme.shared".to_owned(), "2.0.0".to_owned()); + + let before = Lockfile::new_v2( + vec!["acme.subject@1.0.0".to_owned()], + vec![ + locked_package( + "acme.subject", + "1.0.0", + &before_subject_sha, + before_dependencies, + ), + locked_package( + "acme.shared", + "1.0.0", + &before_shared_sha, + BTreeMap::new(), + ), + ], + vec![ResolvedDependency { + from_name: "acme.subject".to_owned(), + from_version: "1.0.0".to_owned(), + to_name: "acme.shared".to_owned(), + to_version: "1.0.0".to_owned(), + declared_constraint: "1.0.0".to_owned(), + }], + ); + let after = Lockfile::new_v2( + vec!["acme.subject@2.0.0".to_owned()], + vec![ + locked_package( + "acme.subject", + "2.0.0", + &after_subject_sha, + after_dependencies, + ), + locked_package( + "acme.shared", + "2.0.0", + &after_shared_sha, + BTreeMap::new(), + ), + ], + vec![ResolvedDependency { + from_name: "acme.subject".to_owned(), + from_version: "2.0.0".to_owned(), + to_name: "acme.shared".to_owned(), + to_version: "2.0.0".to_owned(), + declared_constraint: "2.0.0".to_owned(), + }], + ); + fs::write(&before_lock_path, before.to_bytes().unwrap()).unwrap(); + fs::write(&after_lock_path, after.to_bytes().unwrap()).unwrap(); + + ( + before_lock_path, + before_cache_path, + after_lock_path, + after_cache_path, + ) +} + +fn run_impact( + before_lock: &Path, + before_cache: &Path, + after_lock: &Path, + after_cache: &Path, +) -> Output { + commandf() + .args([ + "impact", + "acme.subject", + "--before-lock", + before_lock.to_str().unwrap(), + "--before-cache", + before_cache.to_str().unwrap(), + "--after-lock", + after_lock.to_str().unwrap(), + "--after-cache", + after_cache.to_str().unwrap(), + "--format", + "json", + ]) + .env("HTTP_PROXY", "http://127.0.0.1:9") + .env("HTTPS_PROXY", "http://127.0.0.1:9") + .env("NO_PROXY", "") + .output() + .expect("commandf impact must execute") +} + +fn assert_success(output: &Output) { + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.stderr.is_empty()); +} + +fn unique_temp_dir(label: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock after epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "commandf-impact-{label}-{}-{nonce}", + std::process::id() + )) +} + +fn locked_package( + name: &str, + version: &str, + sha256: &str, + dependencies: BTreeMap, +) -> LockedPackage { + LockedPackage { + name: name.to_owned(), + version: version.to_owned(), + sha256: sha256.to_owned(), + source: "synthetic-impact-test".to_owned(), + dependencies, + } +} From f4311ff5ca61a18c843c147617bcf88e5c587f2f Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 10:04:59 +0300 Subject: [PATCH 4/9] test(impact): add deterministic CLI proof --- .../tests/impact_determinism_proof.rs | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 crates/commandf-cli/tests/impact_determinism_proof.rs diff --git a/crates/commandf-cli/tests/impact_determinism_proof.rs b/crates/commandf-cli/tests/impact_determinism_proof.rs new file mode 100644 index 00000000..1c65cda3 --- /dev/null +++ b/crates/commandf-cli/tests/impact_determinism_proof.rs @@ -0,0 +1,166 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use commandf_pkg::{LockedPackage, Lockfile, PackageCache, ResolvedDependency}; + +const PARENT_A_ARCHIVE: &[u8] = include_bytes!("fixtures/parent-a.tgz"); +const PARENT_B_ARCHIVE: &[u8] = include_bytes!("fixtures/parent-b.tgz"); +const SHARED_V1_ARCHIVE: &[u8] = include_bytes!("fixtures/shared-v1.tgz"); +const SHARED_V2_ARCHIVE: &[u8] = include_bytes!("fixtures/shared-v2.tgz"); + +#[test] +fn impact_cli_output_is_byte_identical_and_reports_sha256() { + let root = unique_temp_dir(); + fs::create_dir_all(&root).unwrap(); + let (before_lock, before_cache, after_lock, after_cache) = write_state(&root); + + let first = run_impact(&before_lock, &before_cache, &after_lock, &after_cache); + let second = run_impact(&before_lock, &before_cache, &after_lock, &after_cache); + assert!( + first.status.success(), + "{}", + String::from_utf8_lossy(&first.stderr) + ); + assert!( + second.status.success(), + "{}", + String::from_utf8_lossy(&second.stderr) + ); + assert_eq!(first.stdout, second.stdout); + println!("CF12_IMPACT_SHA256={}", PackageCache::digest(&first.stdout)); + let _ = fs::remove_dir_all(root); +} + +fn write_state(root: &Path) -> (PathBuf, PathBuf, PathBuf, PathBuf) { + let before_cache_path = root.join("before-cache"); + let after_cache_path = root.join("after-cache"); + let before_lock_path = root.join("before.lock"); + let after_lock_path = root.join("after.lock"); + let before_cache = PackageCache::new(&before_cache_path); + let after_cache = PackageCache::new(&after_cache_path); + + let before_subject_sha = before_cache.put(PARENT_A_ARCHIVE).unwrap(); + let before_shared_sha = before_cache.put(SHARED_V1_ARCHIVE).unwrap(); + let after_subject_sha = after_cache.put(PARENT_B_ARCHIVE).unwrap(); + let after_shared_sha = after_cache.put(SHARED_V2_ARCHIVE).unwrap(); + + let mut before_dependencies = BTreeMap::new(); + before_dependencies.insert("acme.shared".to_owned(), "1.0.0".to_owned()); + let mut after_dependencies = BTreeMap::new(); + after_dependencies.insert("acme.shared".to_owned(), "2.0.0".to_owned()); + + let before = Lockfile::new_v2( + vec!["acme.subject@1.0.0".to_owned()], + vec![ + locked_package( + "acme.subject", + "1.0.0", + &before_subject_sha, + before_dependencies, + ), + locked_package( + "acme.shared", + "1.0.0", + &before_shared_sha, + BTreeMap::new(), + ), + ], + vec![ResolvedDependency { + from_name: "acme.subject".to_owned(), + from_version: "1.0.0".to_owned(), + to_name: "acme.shared".to_owned(), + to_version: "1.0.0".to_owned(), + declared_constraint: "1.0.0".to_owned(), + }], + ); + let after = Lockfile::new_v2( + vec!["acme.subject@2.0.0".to_owned()], + vec![ + locked_package( + "acme.subject", + "2.0.0", + &after_subject_sha, + after_dependencies, + ), + locked_package( + "acme.shared", + "2.0.0", + &after_shared_sha, + BTreeMap::new(), + ), + ], + vec![ResolvedDependency { + from_name: "acme.subject".to_owned(), + from_version: "2.0.0".to_owned(), + to_name: "acme.shared".to_owned(), + to_version: "2.0.0".to_owned(), + declared_constraint: "2.0.0".to_owned(), + }], + ); + fs::write(&before_lock_path, before.to_bytes().unwrap()).unwrap(); + fs::write(&after_lock_path, after.to_bytes().unwrap()).unwrap(); + + ( + before_lock_path, + before_cache_path, + after_lock_path, + after_cache_path, + ) +} + +fn run_impact( + before_lock: &Path, + before_cache: &Path, + after_lock: &Path, + after_cache: &Path, +) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_commandf")) + .args([ + "impact", + "acme.subject", + "--before-lock", + before_lock.to_str().unwrap(), + "--before-cache", + before_cache.to_str().unwrap(), + "--after-lock", + after_lock.to_str().unwrap(), + "--after-cache", + after_cache.to_str().unwrap(), + "--format", + "json", + ]) + .env("HTTP_PROXY", "http://127.0.0.1:9") + .env("HTTPS_PROXY", "http://127.0.0.1:9") + .env("NO_PROXY", "") + .output() + .unwrap() +} + +fn unique_temp_dir() -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock after epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "commandf-impact-proof-{}-{nonce}", + std::process::id() + )) +} + +fn locked_package( + name: &str, + version: &str, + sha256: &str, + dependencies: BTreeMap, +) -> LockedPackage { + LockedPackage { + name: name.to_owned(), + version: version.to_owned(), + sha256: sha256.to_owned(), + source: "synthetic-impact-proof".to_owned(), + dependencies, + } +} From b350c732f515dd62f3189fca0a74862b44525ad4 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 10:05:10 +0300 Subject: [PATCH 5/9] ci(impact): add deterministic CF-12 proof --- .github/workflows/cf12-impact-proof.yml | 70 +++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/cf12-impact-proof.yml diff --git a/.github/workflows/cf12-impact-proof.yml b/.github/workflows/cf12-impact-proof.yml new file mode 100644 index 00000000..aac7e1e4 --- /dev/null +++ b/.github/workflows/cf12-impact-proof.yml @@ -0,0 +1,70 @@ +name: cf12-impact-proof + +on: + pull_request: + paths: + - .github/workflows/cf12-impact-proof.yml + - Cargo.toml + - Cargo.lock + - crates/commandf-pkg/** + - crates/commandf-cli/** + - specs/013-cf-12-impact/** + push: + branches: + - impl/cf12-impact-cli + paths: + - .github/workflows/cf12-impact-proof.yml + - Cargo.toml + - Cargo.lock + - crates/commandf-pkg/** + - crates/commandf-cli/** + - specs/013-cf-12-impact/** + workflow_dispatch: + +permissions: + contents: read + +env: + CF12_PROOF_CONTAINER: docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f + +jobs: + deterministic-impact-cli: + runs-on: ubuntu-24.04 + container: + image: docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f + timeout-minutes: 15 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 + with: + persist-credentials: false + + - name: Assert pinned execution toolchain + run: | + set -euo pipefail + rustc --version --verbose + cargo --version + test "$(rustc --version | awk '{print $2}')" = "1.97.1" + + - name: Prove byte-identical commandf impact output + run: | + set -euo pipefail + cargo test --locked -p commandf --test impact_determinism_proof -- --nocapture \ + | tee /tmp/cf12-impact-proof.log + grep -oE 'CF12_IMPACT_SHA256=[0-9a-f]{64}' /tmp/cf12-impact-proof.log \ + | tail -n 1 \ + | tee /tmp/cf12-impact.sha256 + test -s /tmp/cf12-impact.sha256 + + - name: Assert repository remains clean + run: | + set -euo pipefail + status="$(git -c safe.directory="$GITHUB_WORKSPACE" status --porcelain)" + test -z "$status" + + - name: Upload impact determinism evidence + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: cf12-impact-proof + path: /tmp/cf12-impact.sha256 + if-no-files-found: error + retention-days: 3 From e6b79729b65360544f07a9294944172dbd975a79 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 10:06:58 +0300 Subject: [PATCH 6/9] style(impact): apply rustfmt to CLI behavior tests --- crates/commandf-cli/tests/impact_behavior.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/crates/commandf-cli/tests/impact_behavior.rs b/crates/commandf-cli/tests/impact_behavior.rs index f48f8056..1c73ff12 100644 --- a/crates/commandf-cli/tests/impact_behavior.rs +++ b/crates/commandf-cli/tests/impact_behavior.rs @@ -61,7 +61,10 @@ fn impact_is_byte_identical_and_reports_dependency_evidence_without_severity() { assert!(json.contains(expected), "missing {expected}"); } for forbidden in ["\"breaking\"", "\"risky\"", "\"additive\""] { - assert!(!json.contains(forbidden), "impact invented severity: {forbidden}"); + assert!( + !json.contains(forbidden), + "impact invented severity: {forbidden}" + ); } let _ = fs::remove_dir_all(root); @@ -131,12 +134,7 @@ fn write_impact_state(root: &Path) -> (PathBuf, PathBuf, PathBuf, PathBuf) { &before_subject_sha, before_dependencies, ), - locked_package( - "acme.shared", - "1.0.0", - &before_shared_sha, - BTreeMap::new(), - ), + locked_package("acme.shared", "1.0.0", &before_shared_sha, BTreeMap::new()), ], vec![ResolvedDependency { from_name: "acme.subject".to_owned(), @@ -155,12 +153,7 @@ fn write_impact_state(root: &Path) -> (PathBuf, PathBuf, PathBuf, PathBuf) { &after_subject_sha, after_dependencies, ), - locked_package( - "acme.shared", - "2.0.0", - &after_shared_sha, - BTreeMap::new(), - ), + locked_package("acme.shared", "2.0.0", &after_shared_sha, BTreeMap::new()), ], vec![ResolvedDependency { from_name: "acme.subject".to_owned(), From d3b8ca418cd2f780e3d751c3117a3a438252d827 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 10:07:25 +0300 Subject: [PATCH 7/9] style(impact): apply rustfmt to deterministic proof --- .../commandf-cli/tests/impact_determinism_proof.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/crates/commandf-cli/tests/impact_determinism_proof.rs b/crates/commandf-cli/tests/impact_determinism_proof.rs index 1c65cda3..a86eae88 100644 --- a/crates/commandf-cli/tests/impact_determinism_proof.rs +++ b/crates/commandf-cli/tests/impact_determinism_proof.rs @@ -61,12 +61,7 @@ fn write_state(root: &Path) -> (PathBuf, PathBuf, PathBuf, PathBuf) { &before_subject_sha, before_dependencies, ), - locked_package( - "acme.shared", - "1.0.0", - &before_shared_sha, - BTreeMap::new(), - ), + locked_package("acme.shared", "1.0.0", &before_shared_sha, BTreeMap::new()), ], vec![ResolvedDependency { from_name: "acme.subject".to_owned(), @@ -85,12 +80,7 @@ fn write_state(root: &Path) -> (PathBuf, PathBuf, PathBuf, PathBuf) { &after_subject_sha, after_dependencies, ), - locked_package( - "acme.shared", - "2.0.0", - &after_shared_sha, - BTreeMap::new(), - ), + locked_package("acme.shared", "2.0.0", &after_shared_sha, BTreeMap::new()), ], vec![ResolvedDependency { from_name: "acme.subject".to_owned(), From 989e3870ef465f4ff8da0967c94bbd064ac0e556 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 10:08:41 +0300 Subject: [PATCH 8/9] style(impact): apply rustfmt to CLI dispatch --- crates/commandf-cli/src/main.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/commandf-cli/src/main.rs b/crates/commandf-cli/src/main.rs index 966ef9a7..fc68b037 100644 --- a/crates/commandf-cli/src/main.rs +++ b/crates/commandf-cli/src/main.rs @@ -375,13 +375,7 @@ fn run(cli: Cli) -> Result> { after_cache, format, } => { - let bytes = impact::run( - package, - before_lock, - before_cache, - after_lock, - after_cache, - )?; + let bytes = impact::run(package, before_lock, before_cache, after_lock, after_cache)?; match format { OutputFormat::Json => io::stdout().write_all(&bytes)?, } From 6d8e22b1d8c999256692052d473ba3c27effc972 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 10:10:39 +0300 Subject: [PATCH 9/9] fix(impact): reject schema v1 before package selection --- crates/commandf-cli/src/impact.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/commandf-cli/src/impact.rs b/crates/commandf-cli/src/impact.rs index 786b8d10..c0a6d2b2 100644 --- a/crates/commandf-cli/src/impact.rs +++ b/crates/commandf-cli/src/impact.rs @@ -17,6 +17,8 @@ pub fn run( let package_name = PackageName::parse(package)?; let before_lockfile = Lockfile::from_slice(&fs::read(before_lock)?)?; let after_lockfile = Lockfile::from_slice(&fs::read(after_lock)?)?; + require_lock_v2(&before_lockfile, "before")?; + require_lock_v2(&after_lockfile, "after")?; let before_locked = select_locked_package(&before_lockfile, package_name.as_str())?; let after_locked = select_locked_package(&after_lockfile, package_name.as_str())?; @@ -42,6 +44,19 @@ pub fn run( Ok(report.to_json_bytes()?) } +fn require_lock_v2(lockfile: &Lockfile, side: &'static str) -> io::Result<()> { + if lockfile.schema == Lockfile::SCHEMA_V2 { + return Ok(()); + } + Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "impact requires commandf.lock schema 2 on {side}; found schema {}", + lockfile.schema + ), + )) +} + fn read_locked_archive(cache: &PackageCache, locked: &LockedPackage) -> io::Result> { fs::read( cache