Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions .github/workflows/cf11-multi-version-proof.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
name: cf11-multi-version-proof

on:
pull_request:
paths:
- .github/workflows/cf11-multi-version-proof.yml
- Cargo.toml
- Cargo.lock
- crates/commandf-pkg/**
- crates/commandf-cli/**
- specs/011-cf-11-multi-version-package-graph/**
- donors/cf-11-multi-version-package-graph.yaml
push:
branches:
- fix/cf-11-multi-version-package-graph
paths:
- .github/workflows/cf11-multi-version-proof.yml
- Cargo.toml
- Cargo.lock
- crates/commandf-pkg/**
- crates/commandf-cli/**
- specs/011-cf-11-multi-version-package-graph/**
- donors/cf-11-multi-version-package-graph.yaml
Comment thread
coderabbitai[bot] marked this conversation as resolved.
workflow_dispatch:

permissions:
contents: read

env:
CF11_PROOF_CONTAINER: docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f

jobs:
real-package-graph:
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: 20
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: Build commandF resolver
run: cargo build --locked -p commandf

- name: Resolve previously blocked frozen IPS state twice
run: |
set -euo pipefail
rm -rf /tmp/cf11-multiversion
mkdir -p /tmp/cf11-multiversion

for pass in a b; do
root="/tmp/cf11-multiversion/$pass"
mkdir -p "$root"
cargo run --locked --quiet -p commandf -- \
pkg resolve hl7.fhir.uv.ips@2.0.1 \
--cache "$root/cache" \
--lock "$root/commandf.lock"
cargo run --locked --quiet -p commandf -- \
pkg verify \
--cache "$root/cache" \
--lock "$root/commandf.lock"
done

- name: Prove multi-version identity and deterministic semantic lock identity
run: |
set -euo pipefail

python3 - <<'PY'
import json
import os
import platform
import subprocess
from pathlib import Path

first = json.loads(Path('/tmp/cf11-multiversion/a/commandf.lock').read_text())
second = json.loads(Path('/tmp/cf11-multiversion/b/commandf.lock').read_text())

def package_evidence(package):
source = package.get('source')
assert isinstance(source, str) and source, package
return {
'name': package['name'],
'version': package['version'],
'source': source,
'sha256': package['sha256'],
'dependencies': dict(sorted(package.get('dependencies', {}).items())),
}

def semantic_identity(lock):
packages = []
for package in lock['packages']:
item = package_evidence(package)
item.pop('source')
packages.append(item)
return {
'schema': lock['schema'],
'roots': sorted(lock['roots']),
'packages': sorted(packages, key=lambda package: (package['name'], package['version'])),
}

first_semantic = semantic_identity(first)
second_semantic = semantic_identity(second)
assert first_semantic == second_semantic, (
'independent resolutions produced different package identities, digests, or declared dependencies'
)

roots = [
package for package in first['packages']
if package['name'] == 'hl7.fhir.uv.ips' and package['version'] == '2.0.1'
]
assert len(roots) == 1, roots

by_name = {}
for package in first['packages']:
by_name.setdefault(package['name'], set()).add(package['version'])

multi = {
name: sorted(versions)
for name, versions in by_name.items()
if len(versions) > 1
}
assert multi, 'expected at least one same-name multi-version dependency'

terminology = by_name.get('hl7.terminology.r4', set())
assert {'7.1.0', '7.2.0'} <= terminology, terminology

first_packages = sorted(
(package_evidence(package) for package in first['packages']),
key=lambda package: (package['name'], package['version']),
)
second_packages = sorted(
(package_evidence(package) for package in second['packages']),
key=lambda package: (package['name'], package['version']),
)
first_sources = [(p['name'], p['version'], p['source']) for p in first_packages]
second_sources = [(p['name'], p['version'], p['source']) for p in second_packages]

evidence = {
'schema': 3,
'frozen_state': 'C002-ips-after',
'package': 'hl7.fhir.uv.ips',
'version': '2.0.1',
'independent_resolutions': 2,
'semantic_lock_identity_identical': True,
'transport_provenance_identical': first_sources == second_sources,
'execution_environment': {
'container': os.environ['CF11_PROOF_CONTAINER'],
'machine': platform.machine(),
'rustc': subprocess.check_output(['rustc', '--version'], text=True).strip(),
'cargo': subprocess.check_output(['cargo', '--version'], text=True).strip(),
},
'multi_version_packages': multi,
'resolution_a': {
'roots': sorted(first['roots']),
'packages': first_packages,
},
'resolution_b': {
'roots': sorted(second['roots']),
'packages': second_packages,
},
}
out = Path('/tmp/cf11-multiversion/evidence.json')
out.write_text(json.dumps(evidence, indent=2, sort_keys=True) + '\n')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
print(out.read_text())
PY

- name: Assert repository remains clean
run: test -z "$(git status --porcelain)"

- name: Upload foundation evidence
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: cf11-multi-version-proof
path: /tmp/cf11-multiversion/evidence.json
if-no-files-found: error
retention-days: 3
16 changes: 5 additions & 11 deletions crates/commandf-pkg/src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,14 @@ impl<'a, S: PackageSource> Resolver<'a, S> {
pub fn resolve(&self, roots: Vec<PackageRequest>) -> Result<Lockfile, PackageError> {
let root_labels = roots.iter().map(PackageRequest::display).collect();
let mut queue: VecDeque<PackageRequest> = roots.into();
let mut selected: BTreeMap<String, LockedPackage> = BTreeMap::new();
let mut selected: BTreeMap<(String, String), LockedPackage> = BTreeMap::new();

while let Some(request) = queue.pop_front() {
let version = self.select_version(&request)?;
let identity = (request.name.to_string(), version.to_string());

if let Some(existing) = selected.get(request.name.as_str()) {
if existing.version == version.to_string() {
continue;
}
return Err(PackageError::VersionConflict {
name: request.name.to_string(),
selected: existing.version.clone(),
requested: request.constraint.to_string(),
});
if selected.contains_key(&identity) {
continue;
}

let archive = self.source.archive_with_source(&request.name, &version)?;
Expand All @@ -47,7 +41,7 @@ impl<'a, S: PackageSource> Resolver<'a, S> {
let digest = self.cache.put(&archive.bytes)?;
let dependencies = manifest.dependencies;
selected.insert(
request.name.to_string(),
identity,
LockedPackage {
name: request.name.to_string(),
version: version.to_string(),
Expand Down
90 changes: 86 additions & 4 deletions crates/commandf-pkg/tests/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ fn resolves_transitive_dependency_and_highest_stable_patch() {
}

#[test]
fn incompatible_versions_fail_closed() {
fn resolves_branch_local_concrete_versions_of_same_package() {
let mut source = MemorySource::default();
source.add("acme.left", "1.0.0", &[("acme.dep", "1.0.0")]);
source.add("acme.right", "1.0.0", &[("acme.dep", "2.0.0")]);
Expand All @@ -113,14 +113,96 @@ fn incompatible_versions_fail_closed() {
let dir = tempdir().unwrap();
let cache = PackageCache::new(dir.path());

let error = Resolver::new(&source, &cache)
let lock = Resolver::new(&source, &cache)
.resolve(vec![
PackageRequest::parse("acme.left@1.0.0").unwrap(),
PackageRequest::parse("acme.right@1.0.0").unwrap(),
])
.unwrap_err();
.unwrap();

let versions = lock
.packages
.iter()
.filter(|package| package.name == "acme.dep")
.map(|package| package.version.as_str())
.collect::<Vec<_>>();
assert_eq!(versions, vec!["1.0.0", "2.0.0"]);
lock.verify_cache(&cache).unwrap();
}

assert!(matches!(error, PackageError::VersionConflict { .. }));
#[test]
fn deduplicates_the_same_concrete_identity_across_branches() {
let mut source = MemorySource::default();
source.add("acme.left", "1.0.0", &[("acme.dep", "1.0.0")]);
source.add("acme.right", "1.0.0", &[("acme.dep", "1.0.0")]);
source.add("acme.dep", "1.0.0", &[]);
let dir = tempdir().unwrap();

let lock = Resolver::new(&source, &PackageCache::new(dir.path()))
.resolve(vec![
PackageRequest::parse("acme.left@1.0.0").unwrap(),
PackageRequest::parse("acme.right@1.0.0").unwrap(),
])
.unwrap();

assert_eq!(
lock.packages
.iter()
.filter(|package| package.name == "acme.dep" && package.version == "1.0.0")
.count(),
1
);
}

#[test]
fn exact_and_patch_wildcard_requests_can_resolve_to_distinct_versions_deterministically() {
let mut source = MemorySource::default();
source.add("acme.dep", "1.2.0", &[]);
source.add("acme.dep", "1.2.3", &[]);
source.add("acme.dep", "1.2.4-beta.1", &[]);
let first_dir = tempdir().unwrap();
let second_dir = tempdir().unwrap();

let first = Resolver::new(&source, &PackageCache::new(first_dir.path()))
.resolve(vec![
PackageRequest::parse("acme.dep@1.2.0").unwrap(),
PackageRequest::parse("acme.dep@1.2.x").unwrap(),
])
.unwrap();
let second = Resolver::new(&source, &PackageCache::new(second_dir.path()))
.resolve(vec![
PackageRequest::parse("acme.dep@1.2.x").unwrap(),
PackageRequest::parse("acme.dep@1.2.0").unwrap(),
])
.unwrap();

assert_eq!(
first
.packages
.iter()
.map(|package| (package.name.as_str(), package.version.as_str()))
.collect::<Vec<_>>(),
vec![("acme.dep", "1.2.0"), ("acme.dep", "1.2.3")]
);
assert_eq!(first.to_bytes().unwrap(), second.to_bytes().unwrap());
}

#[test]
fn exact_identity_cycle_terminates_by_deduplication() {
let mut source = MemorySource::default();
source.add("acme.a", "1.0.0", &[("acme.b", "1.0.0")]);
source.add("acme.b", "1.0.0", &[("acme.a", "1.0.0")]);
let dir = tempdir().unwrap();
let cache = PackageCache::new(dir.path());

let lock = Resolver::new(&source, &cache)
.resolve(vec![PackageRequest::parse("acme.a@1.0.0").unwrap()])
.unwrap();

assert_eq!(lock.packages.len(), 2);
assert_eq!(lock.packages[0].name, "acme.a");
assert_eq!(lock.packages[1].name, "acme.b");
lock.verify_cache(&cache).unwrap();
}

#[test]
Expand Down
34 changes: 34 additions & 0 deletions donors/cf-11-multi-version-package-graph.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
schema: commandf.donor-manifest/v1
updated: 2026-08-15

standards:
- id: hl7-fhir-npm-packages
reference: https://hl7.org/fhir/packages.html
mode: PROTOCOL_STANDARD
adopted_patterns:
- FHIR implementation-guide dependencies are NPM package dependencies
- dependency identity includes package id and version constraint
- production package dependencies should use explicit versions or supported patch wildcards

- id: npm-dependency-tree
reference: https://docs.npmjs.com/cli/v11/commands/npm-dedupe
mode: STUDY
adopted_patterns:
- a dependency tree can legitimately contain multiple concrete versions of the same package name
- deduplication is valid only when one concrete version satisfies the relevant dependency requirements
exclusions:
- no npm hoisting algorithm copied
- no npm source code copied
- no arbitrary npm semver/range semantics imported into CF-11

commandf_constraints:
- resolver identity is exact package name plus concrete version
- exact identity dedup only
- request-local exact and patch-wildcard selection remain deterministic
- no last-writer-wins or silent global version coercion
- schema-v1 lockfile remains an ordered package closure, not an explicit resolved-edge graph
- downstream name-only ambiguity remains fail-closed

rules:
- standards and public behavior are STUDY/PROTOCOL inputs, not code-copy authority
- CF-10 frozen corpus is not modified to fit the pre-CF-11 resolver
Loading
Loading