From 495b1f357bd60e1eb1f1ca66d67bb314704d11a1 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:15:30 +0300 Subject: [PATCH 01/18] test(context): add CLI fixture dependencies --- crates/commandf-cli/Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/commandf-cli/Cargo.toml b/crates/commandf-cli/Cargo.toml index 98f2f8d0..f26b4705 100644 --- a/crates/commandf-cli/Cargo.toml +++ b/crates/commandf-cli/Cargo.toml @@ -8,3 +8,8 @@ publish.workspace = true [dependencies] clap.workspace = true commandf-pkg = { path = "../commandf-pkg" } + +[dev-dependencies] +flate2.workspace = true +tar.workspace = true +tempfile = "3.12" From fdc034a9a8ef3abaa848d30cf400d731de105cb8 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:16:16 +0300 Subject: [PATCH 02/18] feat(context): add offline context CLI command --- crates/commandf-cli/src/main.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/commandf-cli/src/main.rs b/crates/commandf-cli/src/main.rs index daa8244f..eba618ae 100644 --- a/crates/commandf-cli/src/main.rs +++ b/crates/commandf-cli/src/main.rs @@ -8,7 +8,7 @@ use std::process::{self, ExitCode}; use clap::{Parser, Subcommand, ValueEnum}; use commandf_pkg::{ - build_source_mapped_check_report, build_terminology_diff_report, + build_context_graph, build_source_mapped_check_report, build_terminology_diff_report, check_report_to_github_annotations_bytes, check_report_to_sarif_bytes, classify_structural_diff, diff_package_archives, evaluate_compatibility_policy, inspect_package, source_mapped_check_report_to_github_annotations_bytes, CheckDirection, @@ -48,6 +48,14 @@ enum Command { #[arg(long, value_enum, default_value = "json")] format: OutputFormat, }, + Context { + #[arg(long, default_value = "commandf.lock")] + lock: PathBuf, + #[arg(long, default_value = ".commandf/cache")] + cache: PathBuf, + #[arg(long, value_enum, default_value = "json")] + format: OutputFormat, + }, Diff { package: String, #[arg(long)] @@ -319,6 +327,18 @@ fn run(cli: Cli) -> Result> { OutputFormat::Json => io::stdout().write_all(&inspection.to_json_bytes()?)?, } } + Command::Context { + lock, + cache, + format, + } => { + let lockfile = Lockfile::from_slice(&fs::read(&lock)?)?; + let cache = PackageCache::new(cache); + let report = build_context_graph(&lockfile, &cache)?; + match format { + OutputFormat::Json => io::stdout().write_all(&report.to_json_bytes()?)?, + } + } Command::Diff { package, before_lock, From 0ba0dcb467378dfd9883e0a12831f4a79bd5749c Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:16:41 +0300 Subject: [PATCH 03/18] test(context): parse CLI graph evidence --- crates/commandf-cli/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/commandf-cli/Cargo.toml b/crates/commandf-cli/Cargo.toml index f26b4705..33fb4642 100644 --- a/crates/commandf-cli/Cargo.toml +++ b/crates/commandf-cli/Cargo.toml @@ -11,5 +11,6 @@ commandf-pkg = { path = "../commandf-pkg" } [dev-dependencies] flate2.workspace = true +serde_json.workspace = true tar.workspace = true tempfile = "3.12" From fb49508c1ac25727c83c8224580981411ef569e2 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:17:29 +0300 Subject: [PATCH 04/18] test(context): add deterministic CLI graph fixtures --- crates/commandf-cli/tests/context_behavior.rs | 464 ++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 crates/commandf-cli/tests/context_behavior.rs diff --git a/crates/commandf-cli/tests/context_behavior.rs b/crates/commandf-cli/tests/context_behavior.rs new file mode 100644 index 00000000..c47e59b3 --- /dev/null +++ b/crates/commandf-cli/tests/context_behavior.rs @@ -0,0 +1,464 @@ +use std::collections::BTreeMap; +use std::fs; +use std::io::Cursor; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +use commandf_pkg::{LockedPackage, Lockfile, PackageCache, ResolvedDependency}; +use flate2::write::GzEncoder; +use flate2::Compression; +use serde_json::Value; +use tar::{Builder, Header}; +use tempfile::{tempdir, TempDir}; + +fn commandf() -> Command { + Command::new(env!("CARGO_BIN_EXE_commandf")) +} + +fn run_context(lock: &Path, cache: &Path) -> Output { + commandf() + .args([ + "context", + "--lock", + lock.to_str().expect("UTF-8 lock path"), + "--cache", + cache.to_str().expect("UTF-8 cache path"), + "--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 context must execute") +} + +#[test] +fn context_help_exposes_offline_inputs() { + let output = commandf() + .args(["context", "--help"]) + .output() + .expect("commandf context help must execute"); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("UTF-8 help"); + for flag in ["--lock", "--cache", "--format"] { + assert!(stdout.contains(flag), "missing {flag}"); + } +} + +#[test] +fn context_emits_byte_identical_multi_version_graph_evidence() { + let state = write_context_state(); + + let first = run_context(&state.lock, &state.cache); + let second = run_context(&state.lock, &state.cache); + assert_success(&first); + assert_success(&second); + assert_eq!(first.stdout, second.stdout); + + let report: Value = serde_json::from_slice(&first.stdout).expect("context JSON"); + assert_eq!(report["schema"], 1); + assert_eq!(report["lock_schema"], 2); + assert_eq!(report["packages"].as_array().unwrap().len(), 4); + + let package_edges = report["package_dependency_edges"].as_array().unwrap(); + assert_eq!(package_edges.len(), 2); + let selected_shared_versions = package_edges + .iter() + .map(|edge| edge["to"]["version"].as_str().unwrap()) + .collect::>(); + assert!(selected_shared_versions.contains(&"1.0.0")); + assert!(selected_shared_versions.contains(&"2.0.0")); + + let artifacts = report["artifacts"].as_array().unwrap(); + assert!(artifacts.iter().any(|artifact| { + artifact["resource_type"] == "StructureDefinition" && artifact["id"] == "extension" + })); + assert!(artifacts + .iter() + .any(|artifact| artifact["resource_type"] == "Patient")); + + let reference_edges = report["canonical_reference_edges"].as_array().unwrap(); + let resolutions = reference_edges + .iter() + .map(|edge| edge["resolution"].as_str().unwrap()) + .collect::>(); + assert!(resolutions.contains(&"resolved")); + assert!(resolutions.contains(&"external")); + assert!(resolutions.contains(&"ambiguous")); + + for relation in [ + "structure_base_definition", + "structure_type_profile", + "structure_type_target_profile", + "structure_binding_value_set", + "value_set_include_system", + "value_set_include_value_set", + "value_set_exclude_system", + "code_system_supplements", + ] { + assert!(reference_edges + .iter() + .any(|edge| edge["relation"].as_str() == Some(relation)), + "missing relation {relation}"); + } + + assert_eq!( + report["coverage"]["unsupported_source_resource_types"], + serde_json::json!(["Patient"]) + ); +} + +#[test] +fn context_rejects_schema_v1_with_stable_migration_diagnostic() { + let dir = tempdir().unwrap(); + let lock = dir.path().join("commandf.lock"); + let cache = dir.path().join("cache"); + fs::create_dir_all(&cache).unwrap(); + fs::write(&lock, Lockfile::new(Vec::new(), Vec::new()).to_bytes().unwrap()).unwrap(); + + let output = run_context(&lock, &cache); + assert_eq!(output.status.code(), Some(1)); + assert!(output.stdout.is_empty()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("commandf context requires commandf.lock schema 2; found schema 1")); +} + +#[test] +fn context_fails_closed_on_missing_or_corrupted_cache() { + let missing = tempdir().unwrap(); + let missing_lock = missing.path().join("commandf.lock"); + let missing_cache = missing.path().join("cache"); + let lock = Lockfile::new_v2( + vec!["acme.root@1.0.0".to_owned()], + vec![locked_package( + "acme.root", + "1.0.0", + &"a".repeat(64), + BTreeMap::new(), + )], + vec![], + ); + fs::write(&missing_lock, lock.to_bytes().unwrap()).unwrap(); + let missing_output = run_context(&missing_lock, &missing_cache); + assert_eq!(missing_output.status.code(), Some(1)); + assert!(missing_output.stdout.is_empty()); + + let state = write_context_state(); + let lockfile = Lockfile::from_slice(&fs::read(&state.lock).unwrap()).unwrap(); + let digest = &lockfile.packages[0].sha256; + fs::write( + state.cache.join("sha256").join(format!("{digest}.tgz")), + b"corrupted", + ) + .unwrap(); + let corrupted_output = run_context(&state.lock, &state.cache); + assert_eq!(corrupted_output.status.code(), Some(1)); + assert!(corrupted_output.stdout.is_empty()); +} + +#[test] +fn context_rejects_malformed_supported_reference_shape() { + let dir = tempdir().unwrap(); + let cache_path = dir.path().join("cache"); + let lock_path = dir.path().join("commandf.lock"); + let cache = PackageCache::new(&cache_path); + let archive = package_archive( + "acme.bad", + "1.0.0", + &[( + "package/StructureDefinition-bad.json", + br#"{ + "resourceType":"StructureDefinition", + "id":"bad", + "url":"https://example.org/StructureDefinition/bad", + "version":"1.0.0", + "differential":{"element":[{ + "id":"Observation.subject", + "type":[{"code":"Reference","profile":"not-an-array"}] + }]} + }"#, + )], + ); + let digest = cache.put(&archive).unwrap(); + let lock = Lockfile::new_v2( + vec!["acme.bad@1.0.0".to_owned()], + vec![locked_package( + "acme.bad", + "1.0.0", + &digest, + BTreeMap::new(), + )], + vec![], + ); + fs::write(&lock_path, lock.to_bytes().unwrap()).unwrap(); + + let output = run_context(&lock_path, &cache_path); + assert_eq!(output.status.code(), Some(1)); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains("must be an array")); +} + +struct ContextState { + _dir: TempDir, + lock: PathBuf, + cache: PathBuf, +} + +fn write_context_state() -> ContextState { + let dir = tempdir().unwrap(); + let cache_path = dir.path().join("cache"); + let lock_path = dir.path().join("commandf.lock"); + let cache = PackageCache::new(&cache_path); + + let parent_a_archive = package_archive( + "acme.parenta", + "1.0.0", + &[ + ( + "package/StructureDefinition-profile.json", + br#"{ + "resourceType":"StructureDefinition", + "id":"profile", + "url":"https://example.org/StructureDefinition/profile", + "version":"1.0.0", + "baseDefinition":"https://example.org/StructureDefinition/base|1.0.0", + "differential":{"element":[{ + "id":"Observation.subject", + "type":[{ + "code":"Reference", + "profile":["https://example.org/StructureDefinition/shared"], + "targetProfile":["https://external.example/StructureDefinition/missing"] + }], + "binding":{"valueSet":"https://example.org/ValueSet/binding|1.0.0"} + }]} + }"#, + ), + ( + "package/ValueSet-refs.json", + br#"{ + "resourceType":"ValueSet", + "id":"refs", + "url":"https://example.org/ValueSet/refs", + "version":"1.0.0", + "compose":{ + "include":[{ + "system":"https://example.org/CodeSystem/system|1.0.0", + "valueSet":["https://example.org/ValueSet/imported|1.0.0"] + }], + "exclude":[{"system":"https://external.example/CodeSystem/missing"}] + } + }"#, + ), + ( + "package/Patient-unsupported.json", + br#"{"resourceType":"Patient","id":"unsupported"}"#, + ), + ], + ); + let parent_b_archive = package_archive( + "acme.parentb", + "1.0.0", + &[ + ( + "package/StructureDefinition-extension.json", + br#"{ + "resourceType":"StructureDefinition", + "id":"extension", + "url":"https://example.org/StructureDefinition/extension", + "version":"1.0.0", + "type":"Extension", + "baseDefinition":"https://example.org/StructureDefinition/profile|1.0.0", + "differential":{"element":[{ + "id":"Extension.value[x]", + "type":[{"code":"Reference","profile":["https://example.org/StructureDefinition/shared|2.0.0"]}] + }]} + }"#, + ), + canonical_resource( + "StructureDefinition", + "base", + "https://example.org/StructureDefinition/base", + "1.0.0", + ), + canonical_resource( + "ValueSet", + "binding", + "https://example.org/ValueSet/binding", + "1.0.0", + ), + canonical_resource( + "ValueSet", + "imported", + "https://example.org/ValueSet/imported", + "1.0.0", + ), + canonical_resource( + "CodeSystem", + "system", + "https://example.org/CodeSystem/system", + "1.0.0", + ), + ( + "package/CodeSystem-supplement.json", + br#"{ + "resourceType":"CodeSystem", + "id":"supplement", + "url":"https://example.org/CodeSystem/supplement", + "version":"1.0.0", + "supplements":"https://example.org/CodeSystem/system|1.0.0" + }"#, + ), + ], + ); + let shared_v1_archive = package_archive( + "acme.shared", + "1.0.0", + &[canonical_resource( + "StructureDefinition", + "shared-v1", + "https://example.org/StructureDefinition/shared", + "1.0.0", + )], + ); + let shared_v2_archive = package_archive( + "acme.shared", + "2.0.0", + &[canonical_resource( + "StructureDefinition", + "shared-v2", + "https://example.org/StructureDefinition/shared", + "2.0.0", + )], + ); + + let parent_a_sha = cache.put(&parent_a_archive).unwrap(); + let parent_b_sha = cache.put(&parent_b_archive).unwrap(); + let shared_v1_sha = cache.put(&shared_v1_archive).unwrap(); + let shared_v2_sha = cache.put(&shared_v2_archive).unwrap(); + + let mut parent_a_dependencies = BTreeMap::new(); + parent_a_dependencies.insert("acme.shared".to_owned(), "1.0.0".to_owned()); + let mut parent_b_dependencies = BTreeMap::new(); + parent_b_dependencies.insert("acme.shared".to_owned(), "2.0.0".to_owned()); + + let lock = Lockfile::new_v2( + vec![ + "acme.parentb@1.0.0".to_owned(), + "acme.parenta@1.0.0".to_owned(), + ], + vec![ + locked_package( + "acme.parenta", + "1.0.0", + &parent_a_sha, + parent_a_dependencies, + ), + locked_package( + "acme.parentb", + "1.0.0", + &parent_b_sha, + parent_b_dependencies, + ), + locked_package( + "acme.shared", + "1.0.0", + &shared_v1_sha, + BTreeMap::new(), + ), + locked_package( + "acme.shared", + "2.0.0", + &shared_v2_sha, + BTreeMap::new(), + ), + ], + vec![ + ResolvedDependency { + from_name: "acme.parenta".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(), + }, + ResolvedDependency { + from_name: "acme.parentb".to_owned(), + from_version: "1.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(&lock_path, lock.to_bytes().unwrap()).unwrap(); + + ContextState { + _dir: dir, + lock: lock_path, + cache: cache_path, + } +} + +fn assert_success(output: &Output) { + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.stderr.is_empty()); +} + +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-context-test".to_owned(), + dependencies, + } +} + +fn canonical_resource( + resource_type: &'static str, + id: &'static str, + url: &'static str, + version: &'static str, +) -> (&'static str, &'static [u8]) { + let filename = Box::leak(format!("package/{resource_type}-{id}.json").into_boxed_str()); + let body = Box::leak( + format!( + "{{\"resourceType\":\"{resource_type}\",\"id\":\"{id}\",\"url\":\"{url}\",\"version\":\"{version}\"}}" + ) + .into_bytes() + .into_boxed_slice(), + ); + (filename, body) +} + +fn package_archive(name: &str, version: &str, resources: &[(&str, &[u8])]) -> Vec { + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + { + let mut builder = Builder::new(&mut encoder); + let manifest = format!("{{\"name\":\"{name}\",\"version\":\"{version}\"}}"); + append_entry(&mut builder, "package/package.json", manifest.as_bytes()); + for (path, body) in resources { + append_entry(&mut builder, path, body); + } + builder.finish().unwrap(); + } + encoder.finish().unwrap() +} + +fn append_entry(builder: &mut Builder<&mut GzEncoder>>, path: &str, body: &[u8]) { + let mut header = Header::new_gnu(); + header.set_path(path).unwrap(); + header.set_size(body.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + builder.append(&header, Cursor::new(body)).unwrap(); +} From 84f4c135c88bcf976e8719a372a22f5e4e121645 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:18:06 +0300 Subject: [PATCH 05/18] test(context): add exact-byte CLI determinism proof --- .../tests/context_determinism_proof.rs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 crates/commandf-cli/tests/context_determinism_proof.rs diff --git a/crates/commandf-cli/tests/context_determinism_proof.rs b/crates/commandf-cli/tests/context_determinism_proof.rs new file mode 100644 index 00000000..bff26924 --- /dev/null +++ b/crates/commandf-cli/tests/context_determinism_proof.rs @@ -0,0 +1,92 @@ +use std::collections::BTreeMap; +use std::fs; +use std::io::Cursor; +use std::process::Command; + +use commandf_pkg::{LockedPackage, Lockfile, PackageCache}; +use flate2::write::GzEncoder; +use flate2::Compression; +use tar::{Builder, Header}; +use tempfile::tempdir; + +#[test] +fn context_cli_output_is_byte_identical_and_reports_sha256() { + let dir = tempdir().unwrap(); + let cache_path = dir.path().join("cache"); + let lock_path = dir.path().join("commandf.lock"); + let cache = PackageCache::new(&cache_path); + let archive = package_archive(&[( + "package/StructureDefinition-proof.json", + br#"{ + "resourceType":"StructureDefinition", + "id":"proof", + "url":"https://example.org/StructureDefinition/proof", + "version":"1.0.0", + "baseDefinition":"https://external.example/StructureDefinition/base" + }"#, + )]); + let digest = cache.put(&archive).unwrap(); + let lock = Lockfile::new_v2( + vec!["acme.proof@1.0.0".to_owned()], + vec![LockedPackage { + name: "acme.proof".to_owned(), + version: "1.0.0".to_owned(), + sha256: digest, + source: "synthetic-context-proof".to_owned(), + dependencies: BTreeMap::new(), + }], + vec![], + ); + fs::write(&lock_path, lock.to_bytes().unwrap()).unwrap(); + + let first = run_context(&lock_path, &cache_path); + let second = run_context(&lock_path, &cache_path); + 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!("CF11G_CONTEXT_SHA256={}", PackageCache::digest(&first.stdout)); +} + +fn run_context(lock: &std::path::Path, cache: &std::path::Path) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_commandf")) + .args([ + "context", + "--lock", + lock.to_str().unwrap(), + "--cache", + 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 package_archive(resources: &[(&str, &[u8])]) -> Vec { + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + { + let mut builder = Builder::new(&mut encoder); + append_entry( + &mut builder, + "package/package.json", + br#"{"name":"acme.proof","version":"1.0.0"}"#, + ); + for (path, body) in resources { + append_entry(&mut builder, path, body); + } + builder.finish().unwrap(); + } + encoder.finish().unwrap() +} + +fn append_entry(builder: &mut Builder<&mut GzEncoder>>, path: &str, body: &[u8]) { + let mut header = Header::new_gnu(); + header.set_path(path).unwrap(); + header.set_size(body.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + builder.append(&header, Cursor::new(body)).unwrap(); +} From 5b71ad6ae0f1bf6106196e3de53b97946de5efe0 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:18:26 +0300 Subject: [PATCH 06/18] ci(context): add deterministic CLI proof --- .github/workflows/cf11g-context-proof.yml | 68 +++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/cf11g-context-proof.yml diff --git a/.github/workflows/cf11g-context-proof.yml b/.github/workflows/cf11g-context-proof.yml new file mode 100644 index 00000000..72999668 --- /dev/null +++ b/.github/workflows/cf11g-context-proof.yml @@ -0,0 +1,68 @@ +name: cf11g-context-proof + +on: + pull_request: + paths: + - .github/workflows/cf11g-context-proof.yml + - Cargo.toml + - Cargo.lock + - crates/commandf-pkg/** + - crates/commandf-cli/** + - specs/012-cf-11g-ecosystem-context-graph/** + push: + branches: + - impl/cf11g-context-cli + paths: + - .github/workflows/cf11g-context-proof.yml + - Cargo.toml + - Cargo.lock + - crates/commandf-pkg/** + - crates/commandf-cli/** + - specs/012-cf-11g-ecosystem-context-graph/** + workflow_dispatch: + +permissions: + contents: read + +env: + CF11G_PROOF_CONTAINER: docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f + +jobs: + deterministic-context-cli: + runs-on: ubuntu-24.04 + container: + # Docker Official Image rust:1.97.1-trixie, pinned to the linux/amd64 manifest. + image: rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f + timeout-minutes: 15 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 + 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 context output + run: | + set -euo pipefail + cargo test --locked -p commandf --test context_determinism_proof -- --nocapture \ + | tee /tmp/cf11g-context-proof.log + grep -oE 'CF11G_CONTEXT_SHA256=[0-9a-f]{64}' /tmp/cf11g-context-proof.log \ + | tail -n 1 \ + | tee /tmp/cf11g-context.sha256 + test -s /tmp/cf11g-context.sha256 + + - name: Assert repository remains clean + run: test -z "$(git status --porcelain)" + + - name: Upload context determinism evidence + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: cf11g-context-proof + path: /tmp/cf11g-context.sha256 + if-no-files-found: error + retention-days: 3 From 296888827f53a3f607a37494659cd36243d55bd8 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:22:30 +0300 Subject: [PATCH 07/18] test(context): keep CLI proof dependency-neutral --- crates/commandf-cli/Cargo.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/commandf-cli/Cargo.toml b/crates/commandf-cli/Cargo.toml index 33fb4642..98f2f8d0 100644 --- a/crates/commandf-cli/Cargo.toml +++ b/crates/commandf-cli/Cargo.toml @@ -8,9 +8,3 @@ publish.workspace = true [dependencies] clap.workspace = true commandf-pkg = { path = "../commandf-pkg" } - -[dev-dependencies] -flate2.workspace = true -serde_json.workspace = true -tar.workspace = true -tempfile = "3.12" From 61cd2368039f21f216f5a9a00b74bca6d0fe4469 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:23:43 +0300 Subject: [PATCH 08/18] test(context): make CLI fixtures lockfile-neutral --- crates/commandf-cli/tests/context_behavior.rs | 408 +++++------------- 1 file changed, 98 insertions(+), 310 deletions(-) diff --git a/crates/commandf-cli/tests/context_behavior.rs b/crates/commandf-cli/tests/context_behavior.rs index c47e59b3..23335c78 100644 --- a/crates/commandf-cli/tests/context_behavior.rs +++ b/crates/commandf-cli/tests/context_behavior.rs @@ -1,20 +1,23 @@ use std::collections::BTreeMap; use std::fs; -use std::io::Cursor; use std::path::{Path, PathBuf}; use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; use commandf_pkg::{LockedPackage, Lockfile, PackageCache, ResolvedDependency}; -use flate2::write::GzEncoder; -use flate2::Compression; -use serde_json::Value; -use tar::{Builder, Header}; -use tempfile::{tempdir, TempDir}; fn commandf() -> Command { Command::new(env!("CARGO_BIN_EXE_commandf")) } +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-context-{label}-{}-{nonce}", std::process::id())) +} + fn run_context(lock: &Path, cache: &Path) -> Output { commandf() .args([ @@ -48,88 +51,65 @@ fn context_help_exposes_offline_inputs() { #[test] fn context_emits_byte_identical_multi_version_graph_evidence() { - let state = write_context_state(); + let root = unique_temp_dir("success"); + let (lock, cache) = write_context_state(&root); - let first = run_context(&state.lock, &state.cache); - let second = run_context(&state.lock, &state.cache); + let first = run_context(&lock, &cache); + let second = run_context(&lock, &cache); assert_success(&first); assert_success(&second); assert_eq!(first.stdout, second.stdout); - let report: Value = serde_json::from_slice(&first.stdout).expect("context JSON"); - assert_eq!(report["schema"], 1); - assert_eq!(report["lock_schema"], 2); - assert_eq!(report["packages"].as_array().unwrap().len(), 4); - - let package_edges = report["package_dependency_edges"].as_array().unwrap(); - assert_eq!(package_edges.len(), 2); - let selected_shared_versions = package_edges - .iter() - .map(|edge| edge["to"]["version"].as_str().unwrap()) - .collect::>(); - assert!(selected_shared_versions.contains(&"1.0.0")); - assert!(selected_shared_versions.contains(&"2.0.0")); - - let artifacts = report["artifacts"].as_array().unwrap(); - assert!(artifacts.iter().any(|artifact| { - artifact["resource_type"] == "StructureDefinition" && artifact["id"] == "extension" - })); - assert!(artifacts - .iter() - .any(|artifact| artifact["resource_type"] == "Patient")); - - let reference_edges = report["canonical_reference_edges"].as_array().unwrap(); - let resolutions = reference_edges - .iter() - .map(|edge| edge["resolution"].as_str().unwrap()) - .collect::>(); - assert!(resolutions.contains(&"resolved")); - assert!(resolutions.contains(&"external")); - assert!(resolutions.contains(&"ambiguous")); - - for relation in [ - "structure_base_definition", - "structure_type_profile", - "structure_type_target_profile", - "structure_binding_value_set", - "value_set_include_system", - "value_set_include_value_set", - "value_set_exclude_system", - "code_system_supplements", + let json = String::from_utf8(first.stdout).expect("UTF-8 context JSON"); + for evidence in [ + "\"lock_schema\": 2", + "\"name\": \"acme.shared\"", + "\"version\": \"1.0.0\"", + "\"version\": \"2.0.0\"", + "\"id\": \"extension\"", + "\"resource_type\": \"Patient\"", + "\"resolution\": \"resolved\"", + "\"resolution\": \"external\"", + "\"resolution\": \"ambiguous\"", + "\"relation\": \"structure_base_definition\"", + "\"relation\": \"structure_type_profile\"", + "\"relation\": \"structure_type_target_profile\"", + "\"relation\": \"structure_binding_value_set\"", + "\"relation\": \"value_set_include_system\"", + "\"relation\": \"value_set_include_value_set\"", + "\"relation\": \"value_set_exclude_system\"", + "\"relation\": \"code_system_supplements\"", + "\"unsupported_source_resource_types\": [\n \"Patient\"\n ]", ] { - assert!(reference_edges - .iter() - .any(|edge| edge["relation"].as_str() == Some(relation)), - "missing relation {relation}"); + assert!(json.contains(evidence), "missing evidence: {evidence}"); } - assert_eq!( - report["coverage"]["unsupported_source_resource_types"], - serde_json::json!(["Patient"]) - ); + let _ = fs::remove_dir_all(root); } #[test] fn context_rejects_schema_v1_with_stable_migration_diagnostic() { - let dir = tempdir().unwrap(); - let lock = dir.path().join("commandf.lock"); - let cache = dir.path().join("cache"); + let root = unique_temp_dir("schema-v1"); + let lock = root.join("commandf.lock"); + let cache = root.join("cache"); fs::create_dir_all(&cache).unwrap(); fs::write(&lock, Lockfile::new(Vec::new(), Vec::new()).to_bytes().unwrap()).unwrap(); let output = run_context(&lock, &cache); assert_eq!(output.status.code(), Some(1)); assert!(output.stdout.is_empty()); - let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("commandf context requires commandf.lock schema 2; found schema 1")); + assert!(String::from_utf8_lossy(&output.stderr) + .contains("commandf context requires commandf.lock schema 2; found schema 1")); + let _ = fs::remove_dir_all(root); } #[test] -fn context_fails_closed_on_missing_or_corrupted_cache() { - let missing = tempdir().unwrap(); - let missing_lock = missing.path().join("commandf.lock"); - let missing_cache = missing.path().join("cache"); - let lock = Lockfile::new_v2( +fn context_fails_closed_on_missing_corrupt_and_malformed_inputs() { + let missing_root = unique_temp_dir("missing"); + fs::create_dir_all(&missing_root).unwrap(); + let missing_lock = missing_root.join("commandf.lock"); + let missing_cache = missing_root.join("cache"); + let missing = Lockfile::new_v2( vec!["acme.root@1.0.0".to_owned()], vec![locked_package( "acme.root", @@ -139,48 +119,32 @@ fn context_fails_closed_on_missing_or_corrupted_cache() { )], vec![], ); - fs::write(&missing_lock, lock.to_bytes().unwrap()).unwrap(); - let missing_output = run_context(&missing_lock, &missing_cache); - assert_eq!(missing_output.status.code(), Some(1)); - assert!(missing_output.stdout.is_empty()); + fs::write(&missing_lock, missing.to_bytes().unwrap()).unwrap(); + let output = run_context(&missing_lock, &missing_cache); + assert_eq!(output.status.code(), Some(1)); + assert!(output.stdout.is_empty()); + let _ = fs::remove_dir_all(missing_root); - let state = write_context_state(); - let lockfile = Lockfile::from_slice(&fs::read(&state.lock).unwrap()).unwrap(); + let corrupt_root = unique_temp_dir("corrupt"); + let (corrupt_lock, corrupt_cache) = write_context_state(&corrupt_root); + let lockfile = Lockfile::from_slice(&fs::read(&corrupt_lock).unwrap()).unwrap(); let digest = &lockfile.packages[0].sha256; fs::write( - state.cache.join("sha256").join(format!("{digest}.tgz")), + corrupt_cache.join("sha256").join(format!("{digest}.tgz")), b"corrupted", ) .unwrap(); - let corrupted_output = run_context(&state.lock, &state.cache); - assert_eq!(corrupted_output.status.code(), Some(1)); - assert!(corrupted_output.stdout.is_empty()); -} - -#[test] -fn context_rejects_malformed_supported_reference_shape() { - let dir = tempdir().unwrap(); - let cache_path = dir.path().join("cache"); - let lock_path = dir.path().join("commandf.lock"); - let cache = PackageCache::new(&cache_path); - let archive = package_archive( - "acme.bad", - "1.0.0", - &[( - "package/StructureDefinition-bad.json", - br#"{ - "resourceType":"StructureDefinition", - "id":"bad", - "url":"https://example.org/StructureDefinition/bad", - "version":"1.0.0", - "differential":{"element":[{ - "id":"Observation.subject", - "type":[{"code":"Reference","profile":"not-an-array"}] - }]} - }"#, - )], - ); - let digest = cache.put(&archive).unwrap(); + let output = run_context(&corrupt_lock, &corrupt_cache); + assert_eq!(output.status.code(), Some(1)); + assert!(output.stdout.is_empty()); + let _ = fs::remove_dir_all(corrupt_root); + + let malformed_root = unique_temp_dir("malformed"); + let malformed_cache = malformed_root.join("cache"); + let malformed_lock = malformed_root.join("commandf.lock"); + fs::create_dir_all(&malformed_root).unwrap(); + let cache = PackageCache::new(&malformed_cache); + let digest = cache.put(MALFORMED_ARCHIVE).unwrap(); let lock = Lockfile::new_v2( vec!["acme.bad@1.0.0".to_owned()], vec![locked_package( @@ -191,151 +155,24 @@ fn context_rejects_malformed_supported_reference_shape() { )], vec![], ); - fs::write(&lock_path, lock.to_bytes().unwrap()).unwrap(); - - let output = run_context(&lock_path, &cache_path); + fs::write(&malformed_lock, lock.to_bytes().unwrap()).unwrap(); + let output = run_context(&malformed_lock, &malformed_cache); assert_eq!(output.status.code(), Some(1)); assert!(output.stdout.is_empty()); assert!(String::from_utf8_lossy(&output.stderr).contains("must be an array")); + let _ = fs::remove_dir_all(malformed_root); } -struct ContextState { - _dir: TempDir, - lock: PathBuf, - cache: PathBuf, -} - -fn write_context_state() -> ContextState { - let dir = tempdir().unwrap(); - let cache_path = dir.path().join("cache"); - let lock_path = dir.path().join("commandf.lock"); +fn write_context_state(root: &Path) -> (PathBuf, PathBuf) { + fs::create_dir_all(root).unwrap(); + let cache_path = root.join("cache"); + let lock_path = root.join("commandf.lock"); let cache = PackageCache::new(&cache_path); - let parent_a_archive = package_archive( - "acme.parenta", - "1.0.0", - &[ - ( - "package/StructureDefinition-profile.json", - br#"{ - "resourceType":"StructureDefinition", - "id":"profile", - "url":"https://example.org/StructureDefinition/profile", - "version":"1.0.0", - "baseDefinition":"https://example.org/StructureDefinition/base|1.0.0", - "differential":{"element":[{ - "id":"Observation.subject", - "type":[{ - "code":"Reference", - "profile":["https://example.org/StructureDefinition/shared"], - "targetProfile":["https://external.example/StructureDefinition/missing"] - }], - "binding":{"valueSet":"https://example.org/ValueSet/binding|1.0.0"} - }]} - }"#, - ), - ( - "package/ValueSet-refs.json", - br#"{ - "resourceType":"ValueSet", - "id":"refs", - "url":"https://example.org/ValueSet/refs", - "version":"1.0.0", - "compose":{ - "include":[{ - "system":"https://example.org/CodeSystem/system|1.0.0", - "valueSet":["https://example.org/ValueSet/imported|1.0.0"] - }], - "exclude":[{"system":"https://external.example/CodeSystem/missing"}] - } - }"#, - ), - ( - "package/Patient-unsupported.json", - br#"{"resourceType":"Patient","id":"unsupported"}"#, - ), - ], - ); - let parent_b_archive = package_archive( - "acme.parentb", - "1.0.0", - &[ - ( - "package/StructureDefinition-extension.json", - br#"{ - "resourceType":"StructureDefinition", - "id":"extension", - "url":"https://example.org/StructureDefinition/extension", - "version":"1.0.0", - "type":"Extension", - "baseDefinition":"https://example.org/StructureDefinition/profile|1.0.0", - "differential":{"element":[{ - "id":"Extension.value[x]", - "type":[{"code":"Reference","profile":["https://example.org/StructureDefinition/shared|2.0.0"]}] - }]} - }"#, - ), - canonical_resource( - "StructureDefinition", - "base", - "https://example.org/StructureDefinition/base", - "1.0.0", - ), - canonical_resource( - "ValueSet", - "binding", - "https://example.org/ValueSet/binding", - "1.0.0", - ), - canonical_resource( - "ValueSet", - "imported", - "https://example.org/ValueSet/imported", - "1.0.0", - ), - canonical_resource( - "CodeSystem", - "system", - "https://example.org/CodeSystem/system", - "1.0.0", - ), - ( - "package/CodeSystem-supplement.json", - br#"{ - "resourceType":"CodeSystem", - "id":"supplement", - "url":"https://example.org/CodeSystem/supplement", - "version":"1.0.0", - "supplements":"https://example.org/CodeSystem/system|1.0.0" - }"#, - ), - ], - ); - let shared_v1_archive = package_archive( - "acme.shared", - "1.0.0", - &[canonical_resource( - "StructureDefinition", - "shared-v1", - "https://example.org/StructureDefinition/shared", - "1.0.0", - )], - ); - let shared_v2_archive = package_archive( - "acme.shared", - "2.0.0", - &[canonical_resource( - "StructureDefinition", - "shared-v2", - "https://example.org/StructureDefinition/shared", - "2.0.0", - )], - ); - - let parent_a_sha = cache.put(&parent_a_archive).unwrap(); - let parent_b_sha = cache.put(&parent_b_archive).unwrap(); - let shared_v1_sha = cache.put(&shared_v1_archive).unwrap(); - let shared_v2_sha = cache.put(&shared_v2_archive).unwrap(); + let parent_a_sha = cache.put(PARENT_A_ARCHIVE).unwrap(); + let parent_b_sha = cache.put(PARENT_B_ARCHIVE).unwrap(); + let shared_v1_sha = cache.put(SHARED_V1_ARCHIVE).unwrap(); + let shared_v2_sha = cache.put(SHARED_V2_ARCHIVE).unwrap(); let mut parent_a_dependencies = BTreeMap::new(); parent_a_dependencies.insert("acme.shared".to_owned(), "1.0.0".to_owned()); @@ -348,30 +185,10 @@ fn write_context_state() -> ContextState { "acme.parenta@1.0.0".to_owned(), ], vec![ - locked_package( - "acme.parenta", - "1.0.0", - &parent_a_sha, - parent_a_dependencies, - ), - locked_package( - "acme.parentb", - "1.0.0", - &parent_b_sha, - parent_b_dependencies, - ), - locked_package( - "acme.shared", - "1.0.0", - &shared_v1_sha, - BTreeMap::new(), - ), - locked_package( - "acme.shared", - "2.0.0", - &shared_v2_sha, - BTreeMap::new(), - ), + locked_package("acme.parenta", "1.0.0", &parent_a_sha, parent_a_dependencies), + locked_package("acme.parentb", "1.0.0", &parent_b_sha, parent_b_dependencies), + locked_package("acme.shared", "1.0.0", &shared_v1_sha, BTreeMap::new()), + locked_package("acme.shared", "2.0.0", &shared_v2_sha, BTreeMap::new()), ], vec![ ResolvedDependency { @@ -391,12 +208,7 @@ fn write_context_state() -> ContextState { ], ); fs::write(&lock_path, lock.to_bytes().unwrap()).unwrap(); - - ContextState { - _dir: dir, - lock: lock_path, - cache: cache_path, - } + (lock_path, cache_path) } fn assert_success(output: &Output) { @@ -423,42 +235,18 @@ fn locked_package( } } -fn canonical_resource( - resource_type: &'static str, - id: &'static str, - url: &'static str, - version: &'static str, -) -> (&'static str, &'static [u8]) { - let filename = Box::leak(format!("package/{resource_type}-{id}.json").into_boxed_str()); - let body = Box::leak( - format!( - "{{\"resourceType\":\"{resource_type}\",\"id\":\"{id}\",\"url\":\"{url}\",\"version\":\"{version}\"}}" - ) - .into_bytes() - .into_boxed_slice(), - ); - (filename, body) -} - -fn package_archive(name: &str, version: &str, resources: &[(&str, &[u8])]) -> Vec { - let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); - { - let mut builder = Builder::new(&mut encoder); - let manifest = format!("{{\"name\":\"{name}\",\"version\":\"{version}\"}}"); - append_entry(&mut builder, "package/package.json", manifest.as_bytes()); - for (path, body) in resources { - append_entry(&mut builder, path, body); - } - builder.finish().unwrap(); - } - encoder.finish().unwrap() -} - -fn append_entry(builder: &mut Builder<&mut GzEncoder>>, path: &str, body: &[u8]) { - let mut header = Header::new_gnu(); - header.set_path(path).unwrap(); - header.set_size(body.len() as u64); - header.set_mode(0o644); - header.set_cksum(); - builder.append(&header, Cursor::new(body)).unwrap(); -} +const PARENT_A_ARCHIVE: &[u8] = &[ + 31,139,8,0,0,0,0,0,0,255,237,151,75,111,155,64,16,128,115,246,175,168,246,28,3,166,96,75,190,182,247,70,117,213,75,228,195,26,6,103,83,94,218,135,149,136,242,223,59,203,195,177,8,137,35,133,58,173,52,223,5,175,217,157,29,96,190,17,148,60,250,197,247,224,150,237,209,185,87,69,126,53,49,30,178,12,130,230,136,12,143,158,23,46,158,126,219,255,23,139,165,239,95,125,242,166,78,100,12,163,52,151,184,253,37,246,250,7,169,88,206,51,96,107,198,163,12,156,146,75,200,53,103,215,236,0,82,137,34,199,19,11,199,115,60,86,127,116,162,196,95,161,243,222,221,104,105,34,109,36,124,133,68,228,66,227,179,159,151,178,72,68,250,254,158,112,206,255,85,248,121,224,127,184,242,86,228,255,37,168,102,76,130,42,140,140,224,199,99,105,27,193,72,37,96,63,16,49,158,234,10,2,135,70,166,56,190,211,186,84,107,215,133,7,158,149,88,40,133,220,143,21,146,251,180,110,216,86,174,103,108,199,213,233,86,111,143,106,23,254,62,134,137,69,146,128,237,94,130,99,106,21,131,20,50,28,177,245,109,213,38,255,109,167,64,30,184,93,234,40,179,187,135,72,99,66,186,185,104,156,19,21,177,189,250,239,208,68,137,108,178,125,218,235,219,55,231,164,238,176,131,198,108,139,129,185,220,131,190,25,11,161,65,230,60,117,186,88,163,113,50,161,148,200,247,108,91,99,168,157,200,99,59,192,171,58,240,212,192,6,244,11,183,233,103,119,218,237,150,116,183,167,174,183,117,61,27,125,254,189,255,253,202,185,132,68,77,252,22,112,206,255,96,25,12,252,247,87,126,64,254,95,130,106,168,127,95,8,189,243,182,30,94,21,254,88,115,221,204,103,138,163,90,89,89,40,176,245,43,242,40,53,113,107,156,122,84,26,178,23,162,126,65,29,55,205,4,183,157,215,155,126,162,192,184,150,199,124,4,238,42,53,196,221,202,70,37,120,120,117,255,129,153,39,73,244,66,90,147,62,250,153,77,73,239,255,13,54,70,108,151,115,147,43,83,182,247,109,178,46,112,254,253,63,28,248,31,248,126,72,254,95,130,103,254,119,133,208,235,127,82,15,244,13,64,16,4,65,16,4,65,16,4,65,16,4,65,16,4,65,16,4,241,31,240,7,94,0,20,74,0,40,0,0, +]; +const PARENT_B_ARCHIVE: &[u8] = &[ + 31,139,8,0,0,0,0,0,0,255,237,153,203,110,163,48,20,134,251,40,35,175,27,174,129,72,217,118,230,5,38,163,217,84,93,16,56,73,221,225,98,217,38,74,149,244,221,199,132,112,81,72,3,153,161,132,72,231,207,194,128,109,124,34,206,247,219,6,230,249,127,188,53,232,44,47,181,55,145,196,15,61,203,80,114,167,211,67,169,116,90,26,134,99,86,199,217,117,211,116,45,235,225,155,209,119,32,231,148,10,233,113,53,252,16,99,141,80,59,18,123,17,144,57,241,252,8,52,230,113,136,229,146,60,146,13,112,65,147,88,85,152,154,161,25,228,227,214,129,162,190,68,71,238,245,133,228,169,47,83,14,223,97,69,99,42,213,179,159,192,86,66,156,101,193,127,186,66,27,255,206,236,148,127,215,114,77,228,127,8,237,8,7,145,164,220,135,95,239,44,243,129,51,137,160,236,128,6,170,170,204,7,117,33,229,161,186,242,42,37,19,115,93,135,173,23,177,16,180,132,175,207,101,146,94,239,121,234,44,143,68,230,35,255,168,53,90,122,162,30,64,247,145,24,79,86,52,132,125,113,239,128,174,86,144,153,26,245,84,192,59,2,33,68,234,140,204,159,119,249,159,42,71,213,54,94,152,194,243,246,165,140,72,53,241,147,32,11,237,39,28,110,226,131,170,59,142,160,170,59,7,37,94,149,175,6,123,235,16,211,203,71,246,27,139,159,94,226,63,123,8,125,44,8,218,248,55,27,252,59,166,99,35,255,67,232,26,254,179,124,184,26,253,99,39,92,79,140,83,151,248,207,109,107,178,49,191,120,254,183,234,123,129,124,254,55,172,41,242,63,132,174,225,191,204,135,171,77,32,239,137,54,48,62,117,225,223,186,5,255,14,242,63,132,254,133,127,171,15,254,45,228,127,12,42,248,255,157,109,126,22,32,39,75,26,7,52,94,247,249,34,176,117,253,223,120,255,103,219,51,3,249,31,66,13,254,139,68,40,23,253,121,62,92,68,190,232,163,87,141,113,162,191,15,53,248,167,17,75,184,132,160,71,3,104,231,223,62,229,223,53,112,255,63,136,90,249,47,242,161,155,1,212,90,163,3,220,131,10,254,159,146,0,22,239,66,66,52,17,135,226,198,252,219,200,255,32,106,240,95,37,66,185,236,47,206,62,231,191,234,164,151,173,145,255,123,208,57,254,83,198,242,175,36,61,121,64,235,254,223,153,157,240,63,117,76,92,255,15,162,46,252,151,249,208,217,3,234,61,154,223,251,170,106,209,213,79,246,232,33,40,20,10,213,175,254,2,236,72,28,58,0,40,0,0, +]; +const SHARED_V1_ARCHIVE: &[u8] = &[ + 31,139,8,0,0,0,0,0,0,255,237,206,59,14,194,48,16,132,225,28,5,109,141,204,26,133,20,220,102,21,44,94,138,19,217,64,131,184,59,6,26,64,148,17,80,252,95,51,171,105,102,7,107,247,182,14,179,225,145,110,151,251,88,141,76,139,166,174,239,89,188,167,234,226,233,190,245,222,55,115,95,77,116,236,71,62,57,230,131,165,50,255,141,173,63,116,150,104,93,144,165,88,219,5,151,55,150,194,74,166,114,10,41,111,251,88,122,239,212,169,92,126,253,39,0,0,0,0,0,0,0,0,0,0,0,0,0,224,213,21,117,232,185,119,0,40,0,0, +]; +const SHARED_V2_ARCHIVE: &[u8] = &[ + 31,139,8,0,0,0,0,0,0,255,237,206,59,14,194,48,16,132,225,28,5,109,141,204,58,10,41,184,205,42,88,188,20,39,138,129,38,226,238,24,104,0,81,70,64,241,127,205,172,166,153,237,173,57,216,38,44,250,71,186,125,234,98,49,49,205,234,170,186,103,246,158,170,203,167,251,214,123,95,151,190,152,233,212,143,124,114,74,71,27,242,252,55,182,254,208,40,209,218,32,43,177,166,13,46,109,109,8,107,153,203,57,12,105,215,197,220,151,78,157,202,229,215,127,2,0,0,0,0,0,0,0,0,0,0,0,0,0,94,93,1,133,109,57,185,0,40,0,0, +]; +const MALFORMED_ARCHIVE: &[u8] = &[ + 31,139,8,0,0,0,0,0,0,255,237,212,65,75,195,48,20,7,240,125,20,201,121,109,51,237,42,236,236,93,80,111,226,33,75,95,103,102,155,148,36,29,142,178,239,238,107,39,8,163,224,101,78,145,255,175,135,164,73,154,60,104,222,107,149,126,83,27,202,218,99,155,110,131,179,179,51,147,172,200,243,177,101,167,173,148,249,242,171,63,140,47,22,197,245,114,118,37,207,29,200,148,46,68,229,249,248,75,156,245,7,245,194,170,134,196,74,40,221,80,186,86,165,152,139,29,249,96,156,229,193,69,42,83,41,14,191,29,36,252,152,207,188,207,30,163,239,116,236,60,221,81,101,172,137,252,255,19,190,13,103,169,7,223,229,255,205,82,158,228,127,126,91,20,200,255,75,232,133,167,224,58,175,233,105,223,14,117,96,226,34,112,73,48,37,79,29,171,67,231,107,238,191,198,216,134,85,150,209,187,106,218,154,82,231,55,83,119,40,155,174,40,115,81,154,170,34,79,54,26,197,219,245,130,106,106,248,77,172,158,251,227,97,247,235,64,126,167,134,77,210,208,173,183,164,35,127,22,199,32,121,141,118,229,16,237,3,141,187,104,226,185,214,187,202,212,195,168,117,49,81,54,81,222,171,189,56,188,240,131,18,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,214,7,111,234,157,86,0,40,0,0, +]; From 66cb11c9a7ecdbcec98705f3916e5735dd76346b Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:24:04 +0300 Subject: [PATCH 09/18] test(context): make determinism proof dependency-neutral --- .../tests/context_determinism_proof.rs | 80 ++++++++----------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/crates/commandf-cli/tests/context_determinism_proof.rs b/crates/commandf-cli/tests/context_determinism_proof.rs index bff26924..3ad879ff 100644 --- a/crates/commandf-cli/tests/context_determinism_proof.rs +++ b/crates/commandf-cli/tests/context_determinism_proof.rs @@ -1,31 +1,19 @@ use std::collections::BTreeMap; use std::fs; -use std::io::Cursor; +use std::path::{Path, PathBuf}; use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; use commandf_pkg::{LockedPackage, Lockfile, PackageCache}; -use flate2::write::GzEncoder; -use flate2::Compression; -use tar::{Builder, Header}; -use tempfile::tempdir; #[test] fn context_cli_output_is_byte_identical_and_reports_sha256() { - let dir = tempdir().unwrap(); - let cache_path = dir.path().join("cache"); - let lock_path = dir.path().join("commandf.lock"); + let root = unique_temp_dir(); + fs::create_dir_all(&root).unwrap(); + let cache_path = root.join("cache"); + let lock_path = root.join("commandf.lock"); let cache = PackageCache::new(&cache_path); - let archive = package_archive(&[( - "package/StructureDefinition-proof.json", - br#"{ - "resourceType":"StructureDefinition", - "id":"proof", - "url":"https://example.org/StructureDefinition/proof", - "version":"1.0.0", - "baseDefinition":"https://external.example/StructureDefinition/base" - }"#, - )]); - let digest = cache.put(&archive).unwrap(); + let digest = cache.put(PROOF_ARCHIVE).unwrap(); let lock = Lockfile::new_v2( vec!["acme.proof@1.0.0".to_owned()], vec![LockedPackage { @@ -41,13 +29,33 @@ fn context_cli_output_is_byte_identical_and_reports_sha256() { let first = run_context(&lock_path, &cache_path); let second = run_context(&lock_path, &cache_path); - assert!(first.status.success(), "{}", String::from_utf8_lossy(&first.stderr)); - assert!(second.status.success(), "{}", String::from_utf8_lossy(&second.stderr)); + 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!("CF11G_CONTEXT_SHA256={}", PackageCache::digest(&first.stdout)); + let _ = fs::remove_dir_all(root); +} + +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-context-proof-{}-{nonce}", + std::process::id() + )) } -fn run_context(lock: &std::path::Path, cache: &std::path::Path) -> std::process::Output { +fn run_context(lock: &Path, cache: &Path) -> std::process::Output { Command::new(env!("CARGO_BIN_EXE_commandf")) .args([ "context", @@ -65,28 +73,6 @@ fn run_context(lock: &std::path::Path, cache: &std::path::Path) -> std::process: .unwrap() } -fn package_archive(resources: &[(&str, &[u8])]) -> Vec { - let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); - { - let mut builder = Builder::new(&mut encoder); - append_entry( - &mut builder, - "package/package.json", - br#"{"name":"acme.proof","version":"1.0.0"}"#, - ); - for (path, body) in resources { - append_entry(&mut builder, path, body); - } - builder.finish().unwrap(); - } - encoder.finish().unwrap() -} - -fn append_entry(builder: &mut Builder<&mut GzEncoder>>, path: &str, body: &[u8]) { - let mut header = Header::new_gnu(); - header.set_path(path).unwrap(); - header.set_size(body.len() as u64); - header.set_mode(0o644); - header.set_cksum(); - builder.append(&header, Cursor::new(body)).unwrap(); -} +const PROOF_ARCHIVE: &[u8] = &[ + 31,139,8,0,0,0,0,0,0,255,237,212,209,78,195,32,20,6,224,61,138,225,122,2,117,181,75,118,237,27,232,11,96,61,155,213,22,200,1,204,150,197,119,151,214,25,27,83,227,77,157,94,252,223,13,133,166,7,146,114,126,111,234,103,179,35,229,223,71,249,20,156,93,204,76,103,85,89,14,99,246,117,212,186,92,127,62,247,235,69,81,93,173,23,23,122,238,131,76,73,33,26,206,219,159,99,175,127,232,40,172,233,72,108,132,169,59,146,158,157,219,138,165,120,33,14,141,179,121,185,144,90,106,241,250,215,199,132,95,114,234,123,117,27,57,213,49,49,221,208,182,177,77,204,127,255,114,184,13,51,36,194,79,253,191,26,103,193,208,255,215,171,74,163,255,207,225,40,152,130,75,92,211,221,193,247,57,48,113,17,114,32,52,15,249,213,71,58,36,110,243,236,49,70,31,54,74,209,222,116,190,37,233,120,55,117,139,212,119,153,178,20,247,38,140,119,25,151,140,196,214,180,242,84,123,178,110,255,53,130,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,224,13,155,19,112,211,0,40,0,0, +]; From 036fdb274a00a3fbe9130981152823bf07c7bc84 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:26:14 +0300 Subject: [PATCH 10/18] test(context): store pinned archives as fixtures --- crates/commandf-cli/tests/context_behavior.rs | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/crates/commandf-cli/tests/context_behavior.rs b/crates/commandf-cli/tests/context_behavior.rs index 23335c78..d9b22090 100644 --- a/crates/commandf-cli/tests/context_behavior.rs +++ b/crates/commandf-cli/tests/context_behavior.rs @@ -6,6 +6,12 @@ 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"); +const MALFORMED_ARCHIVE: &[u8] = include_bytes!("fixtures/malformed.tgz"); + fn commandf() -> Command { Command::new(env!("CARGO_BIN_EXE_commandf")) } @@ -15,7 +21,10 @@ fn unique_temp_dir(label: &str) -> PathBuf { .duration_since(UNIX_EPOCH) .expect("system clock after epoch") .as_nanos(); - std::env::temp_dir().join(format!("commandf-context-{label}-{}-{nonce}", std::process::id())) + std::env::temp_dir().join(format!( + "commandf-context-{label}-{}-{nonce}", + std::process::id() + )) } fn run_context(lock: &Path, cache: &Path) -> Output { @@ -93,7 +102,11 @@ fn context_rejects_schema_v1_with_stable_migration_diagnostic() { let lock = root.join("commandf.lock"); let cache = root.join("cache"); fs::create_dir_all(&cache).unwrap(); - fs::write(&lock, Lockfile::new(Vec::new(), Vec::new()).to_bytes().unwrap()).unwrap(); + fs::write( + &lock, + Lockfile::new(Vec::new(), Vec::new()).to_bytes().unwrap(), + ) + .unwrap(); let output = run_context(&lock, &cache); assert_eq!(output.status.code(), Some(1)); @@ -185,8 +198,18 @@ fn write_context_state(root: &Path) -> (PathBuf, PathBuf) { "acme.parenta@1.0.0".to_owned(), ], vec![ - locked_package("acme.parenta", "1.0.0", &parent_a_sha, parent_a_dependencies), - locked_package("acme.parentb", "1.0.0", &parent_b_sha, parent_b_dependencies), + locked_package( + "acme.parenta", + "1.0.0", + &parent_a_sha, + parent_a_dependencies, + ), + locked_package( + "acme.parentb", + "1.0.0", + &parent_b_sha, + parent_b_dependencies, + ), locked_package("acme.shared", "1.0.0", &shared_v1_sha, BTreeMap::new()), locked_package("acme.shared", "2.0.0", &shared_v2_sha, BTreeMap::new()), ], @@ -234,19 +257,3 @@ fn locked_package( dependencies, } } - -const PARENT_A_ARCHIVE: &[u8] = &[ - 31,139,8,0,0,0,0,0,0,255,237,151,75,111,155,64,16,128,115,246,175,168,246,28,3,166,96,75,190,182,247,70,117,213,75,228,195,26,6,103,83,94,218,135,149,136,242,223,59,203,195,177,8,137,35,133,58,173,52,223,5,175,217,157,29,96,190,17,148,60,250,197,247,224,150,237,209,185,87,69,126,53,49,30,178,12,130,230,136,12,143,158,23,46,158,126,219,255,23,139,165,239,95,125,242,166,78,100,12,163,52,151,184,253,37,246,250,7,169,88,206,51,96,107,198,163,12,156,146,75,200,53,103,215,236,0,82,137,34,199,19,11,199,115,60,86,127,116,162,196,95,161,243,222,221,104,105,34,109,36,124,133,68,228,66,227,179,159,151,178,72,68,250,254,158,112,206,255,85,248,121,224,127,184,242,86,228,255,37,168,102,76,130,42,140,140,224,199,99,105,27,193,72,37,96,63,16,49,158,234,10,2,135,70,166,56,190,211,186,84,107,215,133,7,158,149,88,40,133,220,143,21,146,251,180,110,216,86,174,103,108,199,213,233,86,111,143,106,23,254,62,134,137,69,146,128,237,94,130,99,106,21,131,20,50,28,177,245,109,213,38,255,109,167,64,30,184,93,234,40,179,187,135,72,99,66,186,185,104,156,19,21,177,189,250,239,208,68,137,108,178,125,218,235,219,55,231,164,238,176,131,198,108,139,129,185,220,131,190,25,11,161,65,230,60,117,186,88,163,113,50,161,148,200,247,108,91,99,168,157,200,99,59,192,171,58,240,212,192,6,244,11,183,233,103,119,218,237,150,116,183,167,174,183,117,61,27,125,254,189,255,253,202,185,132,68,77,252,22,112,206,255,96,25,12,252,247,87,126,64,254,95,130,106,168,127,95,8,189,243,182,30,94,21,254,88,115,221,204,103,138,163,90,89,89,40,176,245,43,242,40,53,113,107,156,122,84,26,178,23,162,126,65,29,55,205,4,183,157,215,155,126,162,192,184,150,199,124,4,238,42,53,196,221,202,70,37,120,120,117,255,129,153,39,73,244,66,90,147,62,250,153,77,73,239,255,13,54,70,108,151,115,147,43,83,182,247,109,178,46,112,254,253,63,28,248,31,248,126,72,254,95,130,103,254,119,133,208,235,127,82,15,244,13,64,16,4,65,16,4,65,16,4,65,16,4,65,16,4,65,16,4,241,31,240,7,94,0,20,74,0,40,0,0, -]; -const PARENT_B_ARCHIVE: &[u8] = &[ - 31,139,8,0,0,0,0,0,0,255,237,153,203,110,163,48,20,134,251,40,35,175,27,174,129,72,217,118,230,5,38,163,217,84,93,16,56,73,221,225,98,217,38,74,149,244,221,199,132,112,81,72,3,153,161,132,72,231,207,194,128,109,124,34,206,247,219,6,230,249,127,188,53,232,44,47,181,55,145,196,15,61,203,80,114,167,211,67,169,116,90,26,134,99,86,199,217,117,211,116,45,235,225,155,209,119,32,231,148,10,233,113,53,252,16,99,141,80,59,18,123,17,144,57,241,252,8,52,230,113,136,229,146,60,146,13,112,65,147,88,85,152,154,161,25,228,227,214,129,162,190,68,71,238,245,133,228,169,47,83,14,223,97,69,99,42,213,179,159,192,86,66,156,101,193,127,186,66,27,255,206,236,148,127,215,114,77,228,127,8,237,8,7,145,164,220,135,95,239,44,243,129,51,137,160,236,128,6,170,170,204,7,117,33,229,161,186,242,42,37,19,115,93,135,173,23,177,16,180,132,175,207,101,146,94,239,121,234,44,143,68,230,35,255,168,53,90,122,162,30,64,247,145,24,79,86,52,132,125,113,239,128,174,86,144,153,26,245,84,192,59,2,33,68,234,140,204,159,119,249,159,42,71,213,54,94,152,194,243,246,165,140,72,53,241,147,32,11,237,39,28,110,226,131,170,59,142,160,170,59,7,37,94,149,175,6,123,235,16,211,203,71,246,27,139,159,94,226,63,123,8,125,44,8,218,248,55,27,252,59,166,99,35,255,67,232,26,254,179,124,184,26,253,99,39,92,79,140,83,151,248,207,109,107,178,49,191,120,254,183,234,123,129,124,254,55,172,41,242,63,132,174,225,191,204,135,171,77,32,239,137,54,48,62,117,225,223,186,5,255,14,242,63,132,254,133,127,171,15,254,45,228,127,12,42,248,255,157,109,126,22,32,39,75,26,7,52,94,247,249,34,176,117,253,223,120,255,103,219,51,3,249,31,66,13,254,139,68,40,23,253,121,62,92,68,190,232,163,87,141,113,162,191,15,53,248,167,17,75,184,132,160,71,3,104,231,223,62,229,223,53,112,255,63,136,90,249,47,242,161,155,1,212,90,163,3,220,131,10,254,159,146,0,22,239,66,66,52,17,135,226,198,252,219,200,255,32,106,240,95,37,66,185,236,47,206,62,231,191,234,164,151,173,145,255,123,208,57,254,83,198,242,175,36,61,121,64,235,254,223,153,157,240,63,117,76,92,255,15,162,46,252,151,249,208,217,3,234,61,154,223,251,170,106,209,213,79,246,232,33,40,20,10,213,175,254,2,236,72,28,58,0,40,0,0, -]; -const SHARED_V1_ARCHIVE: &[u8] = &[ - 31,139,8,0,0,0,0,0,0,255,237,206,59,14,194,48,16,132,225,28,5,109,141,204,26,133,20,220,102,21,44,94,138,19,217,64,131,184,59,6,26,64,148,17,80,252,95,51,171,105,102,7,107,247,182,14,179,225,145,110,151,251,88,141,76,139,166,174,239,89,188,167,234,226,233,190,245,222,55,115,95,77,116,236,71,62,57,230,131,165,50,255,141,173,63,116,150,104,93,144,165,88,219,5,151,55,150,194,74,166,114,10,41,111,251,88,122,239,212,169,92,126,253,39,0,0,0,0,0,0,0,0,0,0,0,0,0,224,213,21,117,232,185,119,0,40,0,0, -]; -const SHARED_V2_ARCHIVE: &[u8] = &[ - 31,139,8,0,0,0,0,0,0,255,237,206,59,14,194,48,16,132,225,28,5,109,141,204,58,10,41,184,205,42,88,188,20,39,138,129,38,226,238,24,104,0,81,70,64,241,127,205,172,166,153,237,173,57,216,38,44,250,71,186,125,234,98,49,49,205,234,170,186,103,246,158,170,203,167,251,214,123,95,151,190,152,233,212,143,124,114,74,71,27,242,252,55,182,254,208,40,209,218,32,43,177,166,13,46,109,109,8,107,153,203,57,12,105,215,197,220,151,78,157,202,229,215,127,2,0,0,0,0,0,0,0,0,0,0,0,0,0,94,93,1,133,109,57,185,0,40,0,0, -]; -const MALFORMED_ARCHIVE: &[u8] = &[ - 31,139,8,0,0,0,0,0,0,255,237,212,65,75,195,48,20,7,240,125,20,201,121,109,51,237,42,236,236,93,80,111,226,33,75,95,103,102,155,148,36,29,142,178,239,238,107,39,8,163,224,101,78,145,255,175,135,164,73,154,60,104,222,107,149,126,83,27,202,218,99,155,110,131,179,179,51,147,172,200,243,177,101,167,173,148,249,242,171,63,140,47,22,197,245,114,118,37,207,29,200,148,46,68,229,249,248,75,156,245,7,245,194,170,134,196,74,40,221,80,186,86,165,152,139,29,249,96,156,229,193,69,42,83,41,14,191,29,36,252,152,207,188,207,30,163,239,116,236,60,221,81,101,172,137,252,255,19,190,13,103,169,7,223,229,255,205,82,158,228,127,126,91,20,200,255,75,232,133,167,224,58,175,233,105,223,14,117,96,226,34,112,73,48,37,79,29,171,67,231,107,238,191,198,216,134,85,150,209,187,106,218,154,82,231,55,83,119,40,155,174,40,115,81,154,170,34,79,54,26,197,219,245,130,106,106,248,77,172,158,251,227,97,247,235,64,126,167,134,77,210,208,173,183,164,35,127,22,199,32,121,141,118,229,16,237,3,141,187,104,226,185,214,187,202,212,195,168,117,49,81,54,81,222,171,189,56,188,240,131,18,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,214,7,111,234,157,86,0,40,0,0, -]; From 01fb00a472da6b2450eae4e82ccbd09e964d6492 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:26:25 +0300 Subject: [PATCH 11/18] test(context): use pinned proof archive fixture --- .../commandf-cli/tests/context_determinism_proof.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/commandf-cli/tests/context_determinism_proof.rs b/crates/commandf-cli/tests/context_determinism_proof.rs index 3ad879ff..089a5f7d 100644 --- a/crates/commandf-cli/tests/context_determinism_proof.rs +++ b/crates/commandf-cli/tests/context_determinism_proof.rs @@ -6,6 +6,8 @@ use std::time::{SystemTime, UNIX_EPOCH}; use commandf_pkg::{LockedPackage, Lockfile, PackageCache}; +const PROOF_ARCHIVE: &[u8] = include_bytes!("fixtures/proof.tgz"); + #[test] fn context_cli_output_is_byte_identical_and_reports_sha256() { let root = unique_temp_dir(); @@ -40,7 +42,10 @@ fn context_cli_output_is_byte_identical_and_reports_sha256() { String::from_utf8_lossy(&second.stderr) ); assert_eq!(first.stdout, second.stdout); - println!("CF11G_CONTEXT_SHA256={}", PackageCache::digest(&first.stdout)); + println!( + "CF11G_CONTEXT_SHA256={}", + PackageCache::digest(&first.stdout) + ); let _ = fs::remove_dir_all(root); } @@ -72,7 +77,3 @@ fn run_context(lock: &Path, cache: &Path) -> std::process::Output { .output() .unwrap() } - -const PROOF_ARCHIVE: &[u8] = &[ - 31,139,8,0,0,0,0,0,0,255,237,212,209,78,195,32,20,6,224,61,138,225,122,2,117,181,75,118,237,27,232,11,96,61,155,213,22,200,1,204,150,197,119,151,214,25,27,83,227,77,157,94,252,223,13,133,166,7,146,114,126,111,234,103,179,35,229,223,71,249,20,156,93,204,76,103,85,89,14,99,246,117,212,186,92,127,62,247,235,69,81,93,173,23,23,122,238,131,76,73,33,26,206,219,159,99,175,127,232,40,172,233,72,108,132,169,59,146,158,157,219,138,165,120,33,14,141,179,121,185,144,90,106,241,250,215,199,132,95,114,234,123,117,27,57,213,49,49,221,208,182,177,77,204,127,255,114,184,13,51,36,194,79,253,191,26,103,193,208,255,215,171,74,163,255,207,225,40,152,130,75,92,211,221,193,247,57,48,113,17,114,32,52,15,249,213,71,58,36,110,243,236,49,70,31,54,74,209,222,116,190,37,233,120,55,117,139,212,119,153,178,20,247,38,140,119,25,151,140,196,214,180,242,84,123,178,110,255,53,130,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,224,13,155,19,112,211,0,40,0,0, -]; From b7784bd70d37945bbebbf0a2cce77cfcfff483eb Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 13:27:51 +0300 Subject: [PATCH 12/18] test(context): add pinned archive fixtures --- crates/commandf-cli/tests/fixtures/malformed.tgz | Bin 0 -> 318 bytes crates/commandf-cli/tests/fixtures/parent-a.tgz | Bin 0 -> 544 bytes crates/commandf-cli/tests/fixtures/parent-b.tgz | Bin 0 -> 595 bytes crates/commandf-cli/tests/fixtures/proof.tgz | Bin 0 -> 266 bytes crates/commandf-cli/tests/fixtures/shared-v1.tgz | Bin 0 -> 148 bytes crates/commandf-cli/tests/fixtures/shared-v2.tgz | Bin 0 -> 149 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 crates/commandf-cli/tests/fixtures/malformed.tgz create mode 100644 crates/commandf-cli/tests/fixtures/parent-a.tgz create mode 100644 crates/commandf-cli/tests/fixtures/parent-b.tgz create mode 100644 crates/commandf-cli/tests/fixtures/proof.tgz create mode 100644 crates/commandf-cli/tests/fixtures/shared-v1.tgz create mode 100644 crates/commandf-cli/tests/fixtures/shared-v2.tgz diff --git a/crates/commandf-cli/tests/fixtures/malformed.tgz b/crates/commandf-cli/tests/fixtures/malformed.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5401b70c3618c18ea761b968696a6c7030badf5c GIT binary patch literal 318 zcmV-E0m1$siwFP!00000|LxR4OT#b}2k?Cq$$4!v?JDf-T~Kf0AxmFpW}B2G9gec^ z?rSFqqu^yuk^irUq)D1QXx?j;ep4ID+GCq;gR`?UldQ<|v1O;Nl=p6TS^+sR!TX|IJdKHc{TIy*Rw^gAxV+000000000000000 Q0095i2XE?~Rsbjf0N~`F9smFU literal 0 HcmV?d00001 diff --git a/crates/commandf-cli/tests/fixtures/parent-a.tgz b/crates/commandf-cli/tests/fixtures/parent-a.tgz new file mode 100644 index 0000000000000000000000000000000000000000..cf6d972c75a399e62ee8dd18a6408e2ba3607a72 GIT binary patch literal 544 zcmV+*0^j`~iwFP!00000|LvDcZ<|06fOGb*sP-HKreI6Hw)aML)l1~V8U|-mUfPG1 zi1Ob%%fqnIwpfMy5Ev)4Eh^*M$e3l~^c++>aHK`?PM@R<37k$JOapZ;xsh{yv6@ zMUsH+UV>w46@wHq9I^Fn)h7RKr$8RKUFs;ayN5_)Lb|zVoD&tXz54IaM2T#&ecJ2W zH|M18u!F{Ii-Eb^gT5IHp+V+6b-GxiaWbKl$oFhpW2l|TV>`gBI`GuM2J{QJ>1TJ^ z?Ur=6r>?hkJsW-gz5o5nxr9Vb{1$M||6mym{P$OWK>lBXYN&r-2)*;R9$ppxSaaRX zXNsd*Sy?Es^(*oyHF0a4dQ=*+7ovVa9XHJcx1HCUexks*mdAVq?kY9J-O5HKczAXH zfte>s^g>#bKKhwWN$>v+Hb!ihbCWAmw)btaE^z++KOFcU_*1TxcOd7K3h8k*{19V}P&*QP5s*3Y{0KDWafs!TJdzD?L6cZjn3|y(Wx;>CLL2|i?391ka!ura2<-?5k)+&*U+*mQ zfisDq?0^QUs>}yMV~$<|F^8HClS29zgez z7*AF-gne=EfUZ`MnHu#}z&ipVMCy#ppLhA6Do52eUYNr3_N9zSHSv=m3+*QyZsLQg zJC2~LI|n6Rm9GYS>k!k+NA??wpI+iWdkB3j2-^5J8~i(_V?};`rK6T;Wx&{9Z@;`+Bg@3CL z{w?Hx3@Z5loo#*=ASX*22Q*&y`694&{oi>1XWKIa`5!_J{)pKIPvvaQ{DuTKO;Xp_>8JTB8Hpg9`qik^mO(LP9hVhvLTk z+sOYQYVcnrLb>cO&OYbA>ZF&gk^g(pIsQ||@~tPjB=~nNux6&`T6KrQ+6+6LrX4=v6>DMHsgCG0O zjr((-w{;o&q@ud~SLvITpWb)>DKaPaj8A%KBwzBk(kr`S>g~S2b`6YOD=uF3uGz;^ zQR>|7`N`|+Uudj(>53Jh5y%AdoBKd{-MT?mEN3L!86jHq@@4smiC&agK@&WH` L;eyKy8Vn2oCzyOy literal 0 HcmV?d00001 diff --git a/crates/commandf-cli/tests/fixtures/shared-v1.tgz b/crates/commandf-cli/tests/fixtures/shared-v1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..8b0cf12d572c885abf6031fb117cce560bbe567d GIT binary patch literal 148 zcmb2|=3oGW|8LJ(^Bpn}Xn82Zn%jFus#WApny5}(m+(!8<{j2-QVvrD1OCJtug*+k z&;GuRZ}Y>6dDDMK^!ju!TlYS4&+=D~UheyP&%8L^x8#kxo#nIUrAGgI*V>m%%ZQz@ sG~zbvbn|J4yp|PlY3BcqsCs{8WlY^)b%@1K^1)Tn(ic0+88jFe0B%M;yZ`_I literal 0 HcmV?d00001 diff --git a/crates/commandf-cli/tests/fixtures/shared-v2.tgz b/crates/commandf-cli/tests/fixtures/shared-v2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..c9a567e518c35f101914c2fd436bf3f682adf623 GIT binary patch literal 149 zcmb2|=3oGW|8LJ(^Bpn}Xn82Zn%jHEic53HS*?gYBI;d@YLDJYWH1D}Iee@?yJp$U zw`(nLsOkK2-&Ol6$^KJhwXk5Id upuKS!uU>90NA}FqmOPo)kKUQ?H}}-j>-9_!o1tV}EMse~ Date: Tue, 25 Aug 2026 13:30:34 +0300 Subject: [PATCH 13/18] ci(context): fail closed on repository cleanliness --- .github/workflows/cf11g-context-proof.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cf11g-context-proof.yml b/.github/workflows/cf11g-context-proof.yml index 72999668..b2612568 100644 --- a/.github/workflows/cf11g-context-proof.yml +++ b/.github/workflows/cf11g-context-proof.yml @@ -57,7 +57,10 @@ jobs: test -s /tmp/cf11g-context.sha256 - name: Assert repository remains clean - run: test -z "$(git status --porcelain)" + run: | + set -euo pipefail + status="$(git -c safe.directory="$GITHUB_WORKSPACE" status --porcelain)" + test -z "$status" - name: Upload context determinism evidence uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 From 85558bdfa9609bd839dc9a8e67b5a934ff6a3480 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 14:27:48 +0300 Subject: [PATCH 14/18] ci(context): make proof container provenance explicit --- .github/workflows/cf11g-context-proof.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cf11g-context-proof.yml b/.github/workflows/cf11g-context-proof.yml index b2612568..457d2cc1 100644 --- a/.github/workflows/cf11g-context-proof.yml +++ b/.github/workflows/cf11g-context-proof.yml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-24.04 container: # Docker Official Image rust:1.97.1-trixie, pinned to the linux/amd64 manifest. - image: rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f + image: docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f timeout-minutes: 15 steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / Node 24 From a9d8de879eb598089082040c3ce7a132c035e4a5 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 14:42:48 +0300 Subject: [PATCH 15/18] fix(pkg): propagate v1 resolved-edge refusal --- crates/commandf-pkg/src/lock.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/commandf-pkg/src/lock.rs b/crates/commandf-pkg/src/lock.rs index 0e0b4631..9c56e679 100644 --- a/crates/commandf-pkg/src/lock.rs +++ b/crates/commandf-pkg/src/lock.rs @@ -87,11 +87,18 @@ impl Lockfile { pub fn to_bytes(&self) -> Result, PackageError> { let mut bytes = match self.schema { - Self::SCHEMA_V1 => serde_json::to_vec_pretty(&LockfileV1 { - schema: self.schema, - roots: &self.roots, - packages: &self.packages, - })?, + Self::SCHEMA_V1 => { + if !self.resolved_dependencies.is_empty() { + return Err(PackageError::InvalidLockfile( + "schema v1 must not contain resolved_dependencies".to_owned(), + )); + } + serde_json::to_vec_pretty(&LockfileV1 { + schema: self.schema, + roots: &self.roots, + packages: &self.packages, + })? + } Self::SCHEMA_V2 => { self.validate_v2()?; serde_json::to_vec_pretty(&LockfileV2 { From fc1fd27e17720d449e7d2358acb620be7e301d47 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 14:43:12 +0300 Subject: [PATCH 16/18] test(pkg): propagate v1 resolved-edge refusal coverage --- crates/commandf-pkg/tests/lock_schema.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/commandf-pkg/tests/lock_schema.rs b/crates/commandf-pkg/tests/lock_schema.rs index 776b0b43..ff29cb17 100644 --- a/crates/commandf-pkg/tests/lock_schema.rs +++ b/crates/commandf-pkg/tests/lock_schema.rs @@ -22,6 +22,21 @@ fn schema_v1_rejects_resolved_dependency_evidence() { assert!(matches!(error, PackageError::InvalidLockfile(_))); } +#[test] +fn schema_v1_refuses_to_serialize_resolved_dependency_evidence() { + let mut lock = Lockfile::new(Vec::new(), Vec::new()); + lock.resolved_dependencies.push(ResolvedDependency { + from_name: "acme.parent".to_owned(), + from_version: "1.0.0".to_owned(), + to_name: "acme.child".to_owned(), + to_version: "1.0.0".to_owned(), + declared_constraint: "1.0.0".to_owned(), + }); + + let error = lock.to_bytes().unwrap_err(); + assert!(matches!(error, PackageError::InvalidLockfile(_))); +} + #[test] fn schema_v2_requires_resolved_dependency_field() { let bytes = br#"{"schema":2,"roots":[],"packages":[]}"#; From 4d5ba5afc92a3b579008e72e28ca87482860502f Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 14:51:31 +0300 Subject: [PATCH 17/18] docs(cf11g): record convergence evidence candidate --- .../convergence.md | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 specs/012-cf-11g-ecosystem-context-graph/convergence.md diff --git a/specs/012-cf-11g-ecosystem-context-graph/convergence.md b/specs/012-cf-11g-ecosystem-context-graph/convergence.md new file mode 100644 index 00000000..d2f22541 --- /dev/null +++ b/specs/012-cf-11g-ecosystem-context-graph/convergence.md @@ -0,0 +1,249 @@ +# CF-11G Convergence Evidence — Ecosystem Context Graph + +Status: convergence candidate; final exact-head regression and independent-review gate still open + +Decision: `CF11G_IMPLEMENTATION_PROVEN_PENDING_FINAL_CONVERGENCE_GATES` + +This document records the evidence accumulated for CF-11G before the final convergence-head qualification. It does not make the branch canonical, does not authorize merge by itself, and does not authorize CF-12 implementation. + +## 1. Authority boundary + +CF-11G remains an evidence-only, offline dependency graph slice. + +It does not: + +- classify compatibility, safety, clinical meaning, or semantic equivalence; +- perform package acquisition or network canonical resolution during `commandf context`; +- change CF-03/04/05 compatibility semantics; +- change the CF-06 production HL7 pin or oracle failure behavior; +- modify the frozen CF-10 corpus; +- introduce a graph database, vector store, RAG system, model authority, or agent authority; +- start CF-12 `commandf impact`. + +CF-12 remains blocked until T043 is closed on a final exact head. + +## 2. Canonical base and stacked implementation identity + +Canonical `main` read during convergence preparation: + +```text +main commit = 5bafce4f63537e0507e9b0708e1ebd8e22e3c463 +main tree = f0f009fe8abcab99b3a1d339d400b63af4a7b486 +``` + +Stack snapshot before this convergence-document commit: + +| Stack | PR | Head | Tree | Purpose | +|---|---:|---|---|---| +| Planning | #20 | `190945b1b7665e8b01a2c3ce9f93cf2c5e23dd45` | `f9e274e743964ca4ad0b92e622fce6c901078fb2` | restore CF-11G prerequisite and freeze Spec 012 | +| Stack A | #21 | `40983be3bbd6aed098c18cae8f381cab0ed1826e` | `b3d9cac69f74a6f41ed77250aad3006aab98b035` | lock schema v2 and exact resolved dependency edges | +| Stack B | #22 | `1dce479f7e2b548109e8d2b99a928353639be4b6` | `67fb1cb71e9259d34ceaa989c798f9c239560f3f` | deterministic Context Graph library | +| Stack C | #23 | `fc1fd27e17720d449e7d2358acb620be7e301d47` | `1bb0198f32ecff9d404d8536a2e595e3e596f76a` | shipped offline `commandf context` and deterministic proof | + +The stacks were advanced without force-push or rebase. A reviewer-discovered fail-closed repair was propagated to the downstream stack so the current library and CLI heads contain the same invariant. + +## 3. Lock schema migration evidence + +Stack A establishes schema v2 as the resolver output contract while retaining existing schema-v1 read compatibility for existing commands. + +Proven properties include: + +- schema v2 carries exact parent identity, child identity, and declared dependency constraint evidence; +- exact dependency edges are sorted and deduplicated deterministically; +- shared child identities retain multiple parent edges; +- cycle-closing edges are retained while exact-identity expansion remains bounded; +- malformed or unsupported lock schema states fail closed; +- schema-v1 locks remain readable by existing commands; +- `commandf context` rejects schema v1 because exact resolved-edge evidence is unavailable; +- schema-v1 serialization now fails closed if a caller constructs a v1 `Lockfile` with non-empty `resolved_dependencies`, preventing silent evidence loss. + +The final point was added after independent review. Regression coverage is named: + +```text +schema_v1_refuses_to_serialize_resolved_dependency_evidence +``` + +## 4. Context Graph evidence + +Stack B implements deterministic graph evidence through the existing bounded package-inspection and package-cache trust boundaries. + +The report contains deterministic evidence for: + +- exact package nodes and exact resolved package dependency edges; +- artifact nodes tied to exact owning package identity, archive digest, filename, resource type, canonical URL/version when present, and resource SHA-256; +- StructureDefinition `baseDefinition`, profile, targetProfile, and binding ValueSet references; +- ValueSet include/exclude system and imported ValueSet references; +- CodeSystem `supplements` references; +- explicit extraction coverage for supported and present-but-unsupported resource types; +- canonical target states frozen by Spec 012: `resolved`, `external`, and `ambiguous`. + +For canonical matching, `url|version` uses exact in-closure URL + version eligibility. Zero eligible matches are intentionally `external` / unresolved-in-closure under Spec 012 §6; no fourth version-mismatch state is introduced. + +## 5. Shipped CLI evidence + +Stack C ships: + +```text +commandf context --lock commandf.lock --cache .commandf/cache --format json +``` + +The command: + +- reads explicit lock/cache inputs; +- builds the library-owned deterministic Context Graph; +- writes canonical JSON to stdout; +- performs no package acquisition or registry lookup; +- fails closed for schema-v1 context requests, missing archives, corrupt archive digests, and malformed graph-required supported inputs. + +End-to-end fixtures cover multi-version package edges, profile and Extension references, terminology references, all three frozen target states, and unsupported resource coverage. + +## 6. Determinism proof + +Exact Stack C pre-convergence head `fc1fd27e17720d449e7d2358acb620be7e301d47` passed `cf11g-context-proof` run `32843775848`. + +The proof environment used: + +```text +Rust = 1.97.1 +container = docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f +checkout = actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 +``` + +The proof ran `commandf context` twice from identical pinned fixture inputs and compared stdout bytes exactly. + +Observed output identity: + +```text +CF11G_CONTEXT_SHA256=cbc08088a858ca12af0a2a773be5f4b02a03bc099442e59f7300f5edaca069c0 +repeat equality = PASS +repository-clean assertion = PASS +``` + +The proof also uploaded the retained checksum evidence artifact successfully. + +## 7. Pre-convergence regression evidence + +These are the exact-head runs that were green immediately before this convergence-document commit. Because this document changes the candidate head, T040/T041 remain open until the final convergence head reruns the applicable gates. + +### Planning head — PR #20 + +```text +head = 190945b1b7665e8b01a2c3ce9f93cf2c5e23dd45 +ci = 32832888034 / SUCCESS +cf06-oracle = 32832888010 / SUCCESS +``` + +### Stack A — PR #21 + +```text +head = 40983be3bbd6aed098c18cae8f381cab0ed1826e +ci = 32843594591 / SUCCESS +cf06-oracle = 32843594634 / SUCCESS +cf11-multi-version-proof = 32843594609 / SUCCESS +``` + +The `ci` job includes successful format, clippy, workspace tests, CF-08/CF-09 security regressions, real FHIR smoke, terminology smoke, and local GitHub Action smoke. + +### Stack B — PR #22 + +```text +head = 1dce479f7e2b548109e8d2b99a928353639be4b6 +ci = 32843690946 / SUCCESS +cf06-oracle = 32843690704 / SUCCESS +cf11-multi-version-proof = 32843690836 / SUCCESS +``` + +### Stack C — PR #23 + +```text +head = fc1fd27e17720d449e7d2358acb620be7e301d47 +ci = 32843775881 / SUCCESS +cf06-oracle = 32843775904 / SUCCESS +cf11-multi-version-proof = 32843775886 / SUCCESS +cf11g-context-proof = 32843775848 / SUCCESS +``` + +## 8. Independent-review truth + +Independent review has produced substantive findings, and each known substantive finding has an explicit disposition. + +### PR #21 — schema-v1 serialization evidence loss + +Finding: a public schema-v1 `Lockfile` state with populated `resolved_dependencies` could serialize through the v1 shape and silently drop edge evidence. + +Disposition: **FIXED**. + +The write path now rejects the state with the same fail-closed diagnostic used by the read path, and a regression test covers it. CodeRabbit confirmed the repair in-thread and the thread is resolved. The same invariant was propagated to #22 and #23. + +### PR #22 — proposed fourth canonical-resolution state + +Finding: distinguish explicit-version mismatch from `External` with a fourth target status. + +Disposition: **NOT ADOPTED — conflicts with frozen Spec 012 V1 contract**. + +Spec 012 §6 defines exact `url|version` eligibility and zero matches as `external` / unresolved-in-closure. Acceptance criterion H freezes the three V1 states `resolved`, `external`, and `ambiguous`. The exact source canonical string retains the explicit version evidence. The review thread is resolved with this rationale. + +### PR #23 — proof-container provenance + +Finding: the job container used an implicit Docker Hub registry/namespace even though its digest was pinned. + +Disposition: **FIXED**. + +The workflow now uses the fully qualified digest reference: + +```text +docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f +``` + +CodeRabbit confirmed the repair and resolved the thread. + +### Review availability that remains open + +T042 is not yet closed in this convergence candidate. + +- PR #20 CodeRabbit review was triggered for the exact planning range and remained in processing at the last live read. +- Incremental review attempts after the propagated #21 repair on #22 and #23 hit CodeRabbit's included-review rate limit. These attempts must not be represented as exact-head independent-review PASS. +- Qodo was not observed as connected/available on these PRs during this convergence pass and no Qodo PASS is claimed. +- CodeRabbit's docstring-coverage notices are reviewer advisories, not repository acceptance gates for this slice; no behavior is weakened to satisfy them. + +## 9. Remaining gaps / explicit deferrals + +### Blocking convergence gap — T042-R1 + +Complete or explicitly disposition the still-running PR #20 independent review, and obtain the final available independent review coverage required by T042. Any new substantive finding reopens the affected implementation task and must be repaired or rejected against the frozen contract with evidence. + +### Final-head proof gap — T043-R1 + +After the convergence documentation/task-state commit, rerun and inspect on the new exact Stack C head: + +```text +ci +cf06-oracle +cf11-multi-version-proof +cf11g-context-proof +``` + +Record the final exact head/tree/run identities in PR metadata after the runs settle. A failing or missing required gate prevents T043 closure. + +### Reviewer availability deferral — T042-D1 + +If CodeRabbit remains rate-limited for an incremental re-review, record the limitation exactly rather than converting it into PASS. Previously completed findings and their dispositions remain evidence, but reviewer unavailability is not itself a positive review result. + +Qodo remains an explicit availability-based deferral unless a connected review becomes observable before closure. + +## 10. CF-12 eligibility + +Current decision: + +```text +CF-12 = BLOCKED_PENDING_T042_T043 +``` + +CF-12 `commandf impact` MUST NOT begin implementation until: + +1. T042 has a defensible independent-review disposition with every substantive finding closed; +2. the final convergence head passes the required exact-head workflows; +3. T043 is marked complete without unresolved CF-11G implementation gaps; +4. the stacked CF-11G changes are merged through the repository's normal PR lifecycle. + +No statement in this document grants compatibility, safety, model, runtime, source, dependency, or clinical authority beyond the deterministic evidence recorded above. From 1475a9d117f11dd5af3de6b118cd72fc8ccdfebd Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Tue, 25 Aug 2026 14:51:52 +0300 Subject: [PATCH 18/18] docs(cf11g): mark implementation tasks proven --- .../tasks.md | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/specs/012-cf-11g-ecosystem-context-graph/tasks.md b/specs/012-cf-11g-ecosystem-context-graph/tasks.md index 6bb07570..96e4a6cb 100644 --- a/specs/012-cf-11g-ecosystem-context-graph/tasks.md +++ b/specs/012-cf-11g-ecosystem-context-graph/tasks.md @@ -1,6 +1,6 @@ # CF-11G Tasks — Ecosystem Context Graph -Status: planning candidate +Status: implementation proven; final convergence gates pending Tasks are dependency ordered. A task is complete only with executable evidence on the exact candidate state. @@ -23,20 +23,20 @@ Tasks are dependency ordered. A task is complete only with executable evidence o ## Stack A — explicit resolved package-edge evidence -- [ ] T010 — Introduce explicit lock schema v2 model and version-aware decoding. +- [x] T010 — Introduce explicit lock schema v2 model and version-aware decoding. - Preserve roots, packages, digests, source provenance, and declared manifest dependency constraints. - Add deterministic exact resolved dependency edge relation. - Existing commands continue accepting valid schema-v1 locks. - New resolver output writes schema v2. - Malformed or unsupported schema states fail closed. -- [ ] T011 — Capture exact parent→child dependency edges during resolver traversal. +- [x] T011 — Capture exact parent→child dependency edges during resolver traversal. - Record edge after concrete child selection and before expansion dedup short-circuit. - Preserve declared constraint on edge evidence. - Shared exact child identities remain one node with multiple parent edges. - Cycle closing edges are retained while exact-identity expansion remains bounded. -- [ ] T012 — Prove lock v2 determinism and v1 compatibility. +- [x] T012 — Prove lock v2 determinism and v1 compatibility. - Multi-version branch-local edge fixture. - Shared-child fixture. - Cycle fixture. @@ -46,7 +46,7 @@ Tasks are dependency ordered. A task is complete only with executable evidence o ## Stack B — deterministic Context Graph library -- [ ] T020 — Add library-owned Context Graph schema v1. +- [x] T020 — Add library-owned Context Graph schema v1. - Deterministic package nodes. - Deterministic artifact nodes. - Package dependency edges. @@ -55,23 +55,23 @@ Tasks are dependency ordered. A task is complete only with executable evidence o - Explicit extraction coverage metadata. - Stable pretty-JSON bytes with trailing newline. -- [ ] T021 — Build artifact nodes through the existing bounded CF-02 inspection boundary. +- [x] T021 — Build artifact nodes through the existing bounded CF-02 inspection boundary. - Verify each lock digest before reading archive bytes. - Preserve exact owner package identity, archive digest, filename, resource type, canonical URL/version, resource SHA. - No second unbounded archive path. -- [ ] T022 — Implement StructureDefinition V1 reference extraction. +- [x] T022 — Implement StructureDefinition V1 reference extraction. - top-level `baseDefinition`; - differential `element[].type[].profile[]`; - differential `element[].type[].targetProfile[]`; - differential `element[].binding.valueSet`; - cover both profile and extension StructureDefinitions. -- [ ] T023 — Implement ValueSet and CodeSystem V1 reference extraction. +- [x] T023 — Implement ValueSet and CodeSystem V1 reference extraction. - ValueSet include/exclude `system` and imported `valueSet[]`; - CodeSystem `supplements`. -- [ ] T024 — Implement deterministic in-closure canonical target resolution. +- [x] T024 — Implement deterministic in-closure canonical target resolution. - exact versioned unique target → `resolved`; - unique unversioned target → `resolved`; - no target → `external`; @@ -79,12 +79,12 @@ Tasks are dependency ordered. A task is complete only with executable evidence o - source canonical string retained exactly; - no network lookup or preferred-candidate heuristic. -- [ ] T025 — Expose explicit extraction coverage. +- [x] T025 — Expose explicit extraction coverage. - Supported source resource types/extractor version. - Present-but-unsupported resource types sorted deterministically. - Unsupported types remain artifact nodes. -- [ ] T026 — Prove Context Graph byte determinism and graph invariants. +- [x] T026 — Prove Context Graph byte determinism and graph invariants. - Repeat build on identical lock/cache bytes is byte-identical. - Package/artifact/edge input-order permutations do not affect output. - Duplicate identical edges deduplicate deterministically. @@ -92,28 +92,28 @@ Tasks are dependency ordered. A task is complete only with executable evidence o ## Stack C — shipped `commandf context` -- [ ] T030 — Add `commandf context` CLI command. +- [x] T030 — Add `commandf context` CLI command. - `--lock` path. - `--cache` path. - JSON-only format in V1. - Canonical JSON to stdout. - No package acquisition or registry access. -- [ ] T031 — Enforce lock/cache fail-closed behavior at CLI boundary. +- [x] T031 — Enforce lock/cache fail-closed behavior at CLI boundary. - schema-v1 context request rejects with stable migration diagnostic; - missing archive rejects; - corrupted archive rejects; - malformed graph-required resource input rejects according to existing bounded parser policy; - runtime diagnostic sanitization remains intact. -- [ ] T032 — Add end-to-end graph fixtures. +- [x] T032 — Add end-to-end graph fixtures. - exact multi-version package edges; - StructureDefinition profile + extension edges; - ValueSet/CodeSystem edges; - resolved/external/ambiguous canonical states; - unsupported resource type coverage. -- [ ] T033 — Add exact-head deterministic CLI proof. +- [x] T033 — Add exact-head deterministic CLI proof. - run `commandf context` twice from identical pinned fixture inputs; - compare output bytes exactly; - retain SHA-256 evidence in CI logs or artifact metadata.