diff --git a/.github/workflows/cf11g-context-proof.yml b/.github/workflows/cf11g-context-proof.yml new file mode 100644 index 00000000..457d2cc1 --- /dev/null +++ b/.github/workflows/cf11g-context-proof.yml @@ -0,0 +1,71 @@ +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: docker.io/library/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: | + 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 + with: + name: cf11g-context-proof + path: /tmp/cf11g-context.sha256 + if-no-files-found: error + retention-days: 3 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, diff --git a/crates/commandf-cli/tests/context_behavior.rs b/crates/commandf-cli/tests/context_behavior.rs new file mode 100644 index 00000000..d9b22090 --- /dev/null +++ b/crates/commandf-cli/tests/context_behavior.rs @@ -0,0 +1,259 @@ +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"); +const MALFORMED_ARCHIVE: &[u8] = include_bytes!("fixtures/malformed.tgz"); + +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([ + "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 root = unique_temp_dir("success"); + let (lock, cache) = write_context_state(&root); + + 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 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!(json.contains(evidence), "missing evidence: {evidence}"); + } + + let _ = fs::remove_dir_all(root); +} + +#[test] +fn context_rejects_schema_v1_with_stable_migration_diagnostic() { + 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()); + 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_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", + "1.0.0", + &"a".repeat(64), + BTreeMap::new(), + )], + vec![], + ); + 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 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( + corrupt_cache.join("sha256").join(format!("{digest}.tgz")), + b"corrupted", + ) + .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( + "acme.bad", + "1.0.0", + &digest, + BTreeMap::new(), + )], + vec![], + ); + 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); +} + +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_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(); + (lock_path, 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, + } +} 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..089a5f7d --- /dev/null +++ b/crates/commandf-cli/tests/context_determinism_proof.rs @@ -0,0 +1,79 @@ +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}; + +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(); + 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 digest = cache.put(PROOF_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) + ); + 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: &Path, cache: &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() +} diff --git a/crates/commandf-cli/tests/fixtures/malformed.tgz b/crates/commandf-cli/tests/fixtures/malformed.tgz new file mode 100644 index 00000000..5401b70c Binary files /dev/null and b/crates/commandf-cli/tests/fixtures/malformed.tgz differ 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 00000000..cf6d972c Binary files /dev/null and b/crates/commandf-cli/tests/fixtures/parent-a.tgz differ diff --git a/crates/commandf-cli/tests/fixtures/parent-b.tgz b/crates/commandf-cli/tests/fixtures/parent-b.tgz new file mode 100644 index 00000000..372f809f Binary files /dev/null and b/crates/commandf-cli/tests/fixtures/parent-b.tgz differ diff --git a/crates/commandf-cli/tests/fixtures/proof.tgz b/crates/commandf-cli/tests/fixtures/proof.tgz new file mode 100644 index 00000000..8f6c9ed4 Binary files /dev/null and b/crates/commandf-cli/tests/fixtures/proof.tgz differ 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 00000000..8b0cf12d Binary files /dev/null and b/crates/commandf-cli/tests/fixtures/shared-v1.tgz differ 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 00000000..c9a567e5 Binary files /dev/null and b/crates/commandf-cli/tests/fixtures/shared-v2.tgz differ 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 { 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":[]}"#; 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. 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.