diff --git a/.github/workflows/cf11-multi-version-proof.yml b/.github/workflows/cf11-multi-version-proof.yml new file mode 100644 index 00000000..cff02b5f --- /dev/null +++ b/.github/workflows/cf11-multi-version-proof.yml @@ -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 + 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') + 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 diff --git a/crates/commandf-pkg/src/resolver.rs b/crates/commandf-pkg/src/resolver.rs index 1a313fca..0671e755 100644 --- a/crates/commandf-pkg/src/resolver.rs +++ b/crates/commandf-pkg/src/resolver.rs @@ -19,20 +19,14 @@ impl<'a, S: PackageSource> Resolver<'a, S> { pub fn resolve(&self, roots: Vec) -> Result { let root_labels = roots.iter().map(PackageRequest::display).collect(); let mut queue: VecDeque = roots.into(); - let mut selected: BTreeMap = 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)?; @@ -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(), diff --git a/crates/commandf-pkg/tests/resolution.rs b/crates/commandf-pkg/tests/resolution.rs index cdf5910c..f06790d0 100644 --- a/crates/commandf-pkg/tests/resolution.rs +++ b/crates/commandf-pkg/tests/resolution.rs @@ -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")]); @@ -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::>(); + 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![("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] diff --git a/donors/cf-11-multi-version-package-graph.yaml b/donors/cf-11-multi-version-package-graph.yaml new file mode 100644 index 00000000..d39d84ad --- /dev/null +++ b/donors/cf-11-multi-version-package-graph.yaml @@ -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 diff --git a/specs/011-cf-11-multi-version-package-graph/convergence.md b/specs/011-cf-11-multi-version-package-graph/convergence.md new file mode 100644 index 00000000..608028bb --- /dev/null +++ b/specs/011-cf-11-multi-version-package-graph/convergence.md @@ -0,0 +1,141 @@ +# CF-11 Convergence — Multi-Version Package Graph + +Status: foundation behavior proven; reviewer findings reconciled; final convergence-head gates pending + +## Decision + +```text +CF-11_FOUNDATION_BEHAVIOR_PROVEN_PENDING_FINAL_EXACT_HEAD_GATES +``` + +CF-11 corrects only the package-closure identity model. It does not reinterpret compatibility, policy, terminology, oracle, source attribution, or CF-10 corpus semantics. + +## Canonical base and implementation identity + +```text +repository: TheHalfMoon/commandF +PR: #13 +base: main +canonical base: 4c72f4a21aca757fbdadd2fe34384b8d0c746b85 +branch: fix/cf-11-multi-version-package-graph +resolver implementation evidence head: 7411cebaa3052ccd71e83a916eb8d02e8269912c +proof/reviewer-hardening evidence head: 744a64c7fcd84961aed9ce0417d443129f230541 +``` + +The resolver changes the selected-closure key from package name alone to exact `(package name, concrete version)` identity. Same exact identities deduplicate; different concrete versions of the same name remain distinct closure nodes. Lock schema v1 is unchanged and retains manifest-declared dependency constraints rather than claiming explicit resolved edges. + +## Synthetic regression evidence + +The workspace regression suite proves: + +- two branches may retain `acme.dep@1.0.0` and `acme.dep@2.0.0` simultaneously; +- repeated requests resolving to the same exact identity produce one locked package; +- exact and patch-wildcard requests for the same package name can resolve to distinct versions; +- equivalent root-order permutations produce byte-identical synthetic lockfiles; +- exact-identity cycles terminate through deduplication; +- existing stable-patch selection and cache digest verification remain intact. + +The byte-identical statement above is intentionally limited to deterministic synthetic sources. It is not imposed on independent real-registry acquisitions because real lock provenance records the actual validated transport URL. + +## Real frozen-state foundation proof + +CF-10 remains frozen. CF-11 reused one of its previously ineligible states without replacing or cherry-picking the case: + +```text +state: C002-ips-after +package: hl7.fhir.uv.ips@2.0.1 +prior CF-10 failure: hl7.terminology.r4 selected 7.2.0, requested 7.1.0 +``` + +Final proof/reviewer-hardening evidence on head `744a64c7fcd84961aed9ce0417d443129f230541`: + +```text +workflow: cf11-multi-version-proof +run: 31858847516 +result: SUCCESS +artifact id: 9239883413 +artifact digest: sha256:7770c8f6e1f78b37279efaf69a1e938596516c9310df6583860108d2c85be21c +``` + +The workflow performed two independent clean `pkg resolve` + `pkg verify` runs and required identical deterministic semantic lock identity across the two resolutions: + +```text +roots +(name, version, sha256, declared dependencies) +``` + +Transport provenance is kept explicit for every locked package in both resolutions but is not used as a cross-run equality requirement. `LockedPackage.source` records the actual validated acquisition URL, which may legitimately differ if registry fallback or an accepted redirect is used. The final evidence artifact records both resolution package sets with exact `name`, `version`, `source`, `sha256`, and declared dependencies. In the observed final evidence run, `transport_provenance_identical` was also `true`, but that observation is not elevated into a convergence requirement. + +The same-name multi-version closure contained: + +```text +hl7.fhir.uv.extensions.r4: 5.2.0, 5.3.0 +hl7.terminology.r4: 6.2.0, 7.1.0, 7.2.0 +``` + +This proves the old name-level flattening was insufficient for this frozen real graph. It does not claim every FHIR package graph is supported. + +## Reproducible proof environment + +The real proof executes inside an immutable digest-pinned Rust container rather than relying on the mutable GitHub runner image as the execution environment: + +```text +container: docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f +machine: x86_64 +rustc: rustc 1.97.1 (8bab26f4f 2026-07-14) +cargo: cargo 1.97.1 (c980f4866 2026-06-30) +``` + +The workflow also uses immutable action SHAs, `persist-credentials: false`, and path coverage for resolver, lock, cache, registry/source, CLI, Cargo inputs, CF-11 specs, donor metadata, and the proof workflow itself. + +## Exact proof/reviewer-hardening regression gates + +Head `744a64c7fcd84961aed9ce0417d443129f230541` passed: + +```text +ci 31858847463 SUCCESS +cf06-oracle 31858846042 SUCCESS +cf11-multi-version-proof 31858847516 SUCCESS +``` + +The mainline workflow passed Format, locked Clippy with `-D warnings`, full workspace tests, CF-08 and CF-09 security regressions, real FHIR resolve/verify + inspect/diff/classify/check, terminology smoke, CF-09 fixture preparation, local composite Action source-map self-check, and output verification. + +The dedicated oracle workflow passed the pinned HL7 adapter build, real R4 context, self-equivalence, `commandf oracle` self-diff, invalid-snapshot and corrupted-cache fail-closed gates, changed-profile determinism, and end-to-end reconciliation. + +## Downstream ambiguity boundary + +CF-11 does not add a name-only version choice rule. Existing commands that request one locked package by name and encounter multiple locked versions MUST fail closed. Exact `inspect name@version` selection remains exact. + +Terminology canonical ambiguity and duplicate protections are unchanged. + +## CF-10 boundary + +CF-10 / PR #11 remains frozen and `BLOCKED_BY_FOUNDATION` until CF-11 is canonical. No CF-10 case may be replaced merely because the previous resolver rejected it. + +After CF-11 becomes canonical, the first CF-10 action must be to reconcile onto the new main and rerun the exact same six frozen package states. Semantic diff/classify/check/terminology/oracle execution remains blocked until that eligibility rerun establishes the new foundation state. + +## Reviewer truth and dispositions + +- **Codex Code Review:** reviewed resolver implementation head `7411cebaa3052ccd71e83a916eb8d02e8269912c` and reported: `Didn't find any major issues.` This is positive implementation-head review evidence; it is not represented as a separate approval state. +- **Qodo:** returned one substantive Medium finding: the original real proof compared complete lockfile bytes even though `LockedPackage.source` can legitimately vary with validated registry fallback/redirect transport. This finding was accepted. The proof was changed to compare deterministic semantic lock identity while preserving complete per-resolution source and digest provenance. Qodo marked the original thread resolved/outdated after the correction. +- **CodeRabbit:** returned actionable findings on proof path coverage, immutable execution environment, explicit source/digest evidence, mandatory downstream ambiguity wording, actual proof-state documentation, and full-SHA documentation hygiene. The valid findings were implemented. A later suggestion to fail whenever transport source URLs differ was intentionally not adopted because it would recreate the Qodo-identified flakiness and incorrectly turn transport-route equality into package-identity semantics. That thread was answered with exact run/artifact evidence and resolved explicitly. +- **Greptile:** exact-head review was requested; no returned substantive result was observed. No PASS is claimed. +- **Cubic:** generated PR summaries only; not treated as correctness certification. + +Reviewer absence is recorded rather than replaced with invented approval. A new substantive reviewer finding after this convergence update reopens the merge gate. + +## Research inputs explicitly outside CF-11 + +Recent research/donor inputs — CPGPrompt, PathWISE, and `reason-healthcare/rh-skills` — are not dependencies of this foundation correction and do not modify its acceptance criteria. They belong to later research/benchmark/clinical-knowledge roadmap work after the package/corpus foundation is stable. + +## Final convergence-head rule + +This final convergence reconciliation changes documentation only. Its resulting repository head MUST pass all three configured CF-11 gates again: + +```text +ci +cf06-oracle +cf11-multi-version-proof +``` + +The final convergence-head SHA and final run ids are recorded in PR #13 metadata/body after those workflows settle. A failure or new substantive unresolved review thread reopens convergence. This document intentionally does not self-reference its own future commit SHA/run ids. diff --git a/specs/011-cf-11-multi-version-package-graph/plan.md b/specs/011-cf-11-multi-version-package-graph/plan.md new file mode 100644 index 00000000..1c9c72cf --- /dev/null +++ b/specs/011-cf-11-multi-version-package-graph/plan.md @@ -0,0 +1,101 @@ +# CF-11 Plan — Multi-Version Package Graph + +Status: implementation authorized by CF-10 foundation evidence + +## Base + +```text +repository: TheHalfMoon/commandF +canonical base: 4c72f4a21aca757fbdadd2fe34384b8d0c746b85 +branch: fix/cf-11-multi-version-package-graph +blocked consumer: CF-10 / PR #11 +``` + +## Evidence trigger + +CF-10 run `31856586654` attempted two clean resolutions for every frozen package state and proved 5/6 states fail repeatably under CF-01's name-keyed selected map. The evidence artifact was uploaded before the workflow's final eligibility assertion failed. + +This plan corrects that foundation model without using any semantic benchmark result to guide implementation. + +## Implementation shape + +### 1. Resolver identity + +Replace the selected closure key: + +```text +package name +``` + +with: + +```text +(package name, selected concrete version) +``` + +The selected version is still computed before deduplication using the request's existing exact/patch-wildcard rules. + +### 2. Expansion rule + +For each queued request: + +1. select the request-local concrete version deterministically; +2. form exact identity `(name, version)`; +3. if that identity is already selected, skip re-expansion; +4. otherwise download, verify manifest identity, cache by digest, lock the concrete package, and enqueue its declared dependency requests. + +Different concrete versions of the same name therefore coexist rather than conflict. + +### 3. Lock schema + +Keep `commandf.lock` schema v1 unchanged for this slice. Its `packages: Vec` already carries name, concrete version, digest, source, and declared dependency constraints and already sorts by `(name, version)`. + +CF-11 does not claim schema-v1 records resolved dependency edges. A future explicit graph schema may add those only if a shipped command requires them. + +### 4. Downstream guard + +Do not weaken name-only ambiguity checks in diff/check/terminology/oracle. These commands MUST reject a lock where the requested package name maps to more than one version unless the caller already supplies an exact identity (`inspect`). + +### 5. Tests + +Update the former `incompatible_versions_fail_closed` regression into a positive multi-version graph test and add: + +- same concrete identity dedup across branches; +- wildcard + exact request producing two concrete identities; +- equivalent root-order byte stability with multi-version closure; +- cycle terminating on exact-identity revisit. + +Preserve all previous exact identity/manifest/cache tests. + +### 6. Real proof + +Add a bounded real-network workflow dedicated to this correction. It must: + +- use immutable action SHAs and `persist-credentials: false`; +- run the proof inside a digest-pinned execution container and record that identity in evidence; +- resolve + verify one previously failing frozen CF-10 state from clean cache; +- assert the lock contains at least one package name at multiple concrete versions; +- assert the exact root package/version is present; +- keep package name, exact version, acquisition provenance, and content digest explicit in uploaded evidence; +- compare deterministic semantic lock identity independently of transport URL variance; +- remain evidence-only for CF-11 and not execute CF-10 semantic diff/classification. + +Preferred proof state: `hl7.fhir.us.core@8.0.1`, because the prior frozen sweep recorded its exact old-resolver conflict in both A/B runs. + +Actual proof state used by the convergence workflow: `hl7.fhir.uv.ips@2.0.1` (`C002-ips-after`). It was selected from the same frozen CF-10 set and had a repeatable old-resolver same-name version conflict; no case was changed based on semantic benchmark output. + +## Review focus + +Reviewers should prioritize: + +1. traversal/order dependence; +2. duplicate exact identity or repeated archive expansion; +3. silent global version coercion; +4. lock nondeterminism; +5. cycles / unbounded re-expansion; +6. downstream accidental first-match behavior; +7. regression of archive/registry/cache trust boundaries. + +## Exit + +CF-11 may converge only after exact-head CI, oracle, real multi-version proof, and returned reviewer findings are dispositioned. CF-10 stays Draft and frozen until CF-11 is merged to canonical main. diff --git a/specs/011-cf-11-multi-version-package-graph/spec.md b/specs/011-cf-11-multi-version-package-graph/spec.md new file mode 100644 index 00000000..97de350c --- /dev/null +++ b/specs/011-cf-11-multi-version-package-graph/spec.md @@ -0,0 +1,83 @@ +# CF-11 Specification — Multi-Version Package Graph + +Status: foundation correction candidate + +## Problem + +CF-01 currently flattens the selected dependency closure by package **name**. A second request for the same name at a different concrete version fails with `VersionConflict`, even when the requests originate from different dependency branches. + +CF-10's frozen real-IG eligibility sweep (`31856586654`) proved this model cannot represent 5 of 6 selected public package states. No semantic CF-10 result was executed before this foundation gap was identified. + +## Goal + +Represent the resolved package closure using exact package identities: + +```text +(package name, concrete version) +``` + +while preserving deterministic selection, provenance, content digests, bounded acquisition, and fail-closed downstream ambiguity handling. + +## Normative behavior + +1. Every dependency request is resolved independently under its declared constraint. +2. Exact constraints select exactly that version. +3. Patch wildcards retain CF-01 behavior: select the highest stable matching patch from the source metadata. +4. The selected concrete identity is `(name, version)`. +5. A concrete identity already present in the closure is deduplicated and is not downloaded/expanded again. +6. A different concrete version of the same package name is a distinct closure node and MUST NOT be rejected solely because another version of that name is already present. +7. Resolution MUST NOT silently replace one selected version with another, apply last-writer-wins, or globally coerce two branch-local requests to one version. +8. Lockfile package ordering remains deterministic by `(name, version)`. +9. Lock schema v1 remains unchanged in CF-11. `LockedPackage.dependencies` continues to record the package manifest's declared dependency constraints; CF-11 does not claim that schema v1 is an explicit resolved-edge graph. +10. Cache verification remains digest-based for every locked concrete package identity. +11. Existing commands that select a locked package by name only MUST remain fail-closed if multiple locked versions make that selection ambiguous. CF-11 does not invent a semantic/root-selection rule for those commands. +12. `inspect name@exact-version` MUST continue to select one exact locked identity. +13. Existing terminology canonical ambiguity/duplicate protections remain unchanged. + +## Explicit non-goals + +CF-11 does not: + +- change CF-03/04/05 compatibility semantics; +- choose benchmark cases or alter CF-10's frozen corpus; +- add npm-style hoisting or dedupe optimization; +- solve peer/optional dependencies; +- add arbitrary semver ranges beyond existing exact + patch wildcard support; +- change registry trust, archive bounds, cache layout, or package identity validation; +- change terminology canonical resolution; +- change CLI diff/check/oracle package-selection syntax; +- introduce AI authority. + +## Acceptance criteria + +### A. Multi-version graph + +A synthetic graph where one branch requires `acme.dep@1.0.0` and another requires `acme.dep@2.0.0` MUST resolve successfully and lock both exact identities. + +### B. Same identity deduplication + +If multiple branches resolve to the same concrete `(name, version)`, that package appears exactly once in the lock. + +### C. Request-local wildcard determinism + +An exact request and a patch-wildcard request for the same name may resolve to different concrete versions; both identities are retained. Equivalent root-order permutations produce byte-identical lockfiles. + +### D. Cycles + +A dependency cycle that returns to an already-selected exact identity terminates by identity deduplication rather than repeatedly expanding the package. + +### E. Downstream ambiguity remains fail-closed + +A name-only locked-package selector encountering multiple versions MUST error rather than choose one silently. + +### F. Regression + +All existing workspace tests, CF-08/CF-09 security regressions, real FHIR smoke, and CF-06 oracle gates remain green. + +### G. Real foundation proof + +Before CF-11 convergence, at least one package state that failed CF-10 solely due to the old name-level `VersionConflict` must complete `pkg resolve` + `pkg verify` with multiple same-name concrete versions visible in the lock. CF-10 itself remains frozen until CF-11 is canonical. + +## Authority boundary + +A green CF-11 proves only that commandF can represent a multi-version transitive package closure deterministically. It does not prove semantic compatibility, terminology correctness, or that every real FHIR package graph is supported. diff --git a/specs/011-cf-11-multi-version-package-graph/tasks.md b/specs/011-cf-11-multi-version-package-graph/tasks.md new file mode 100644 index 00000000..5002a78f --- /dev/null +++ b/specs/011-cf-11-multi-version-package-graph/tasks.md @@ -0,0 +1,16 @@ +# CF-11 Tasks — Multi-Version Package Graph + +Status: implementation, reviewer reconciliation, and implementation-head evidence complete; final documentation-head gates pending + +- [x] T001 — Record CF-10 comprehensive eligibility evidence and freeze CF-10 as `BLOCKED_BY_FOUNDATION` without changing cases. +- [x] T002 — Audit current resolver, lock schema, CLI locked-package selection, and terminology ambiguity boundaries. +- [x] T003 — Specify exact package identity `(name, concrete version)` and explicit non-goals. +- [x] T004 — Change resolver selected closure to exact identities while preserving request-local version selection. +- [x] T005 — Replace the old same-name/different-version failure regression with positive multi-version graph coverage. +- [x] T006 — Add same-identity dedup, wildcard/exact coexistence, root-order determinism, and cycle regressions. +- [x] T007 — Preserve schema-v1 lock ordering and cache verification; do not add implicit edge semantics. +- [x] T008 — Add bounded real-network proof for a previously failing CF-10 state. +- [x] T009 — Run Format, locked Clippy, full workspace tests, CF-08/CF-09 security gates, real FHIR smoke, and CF-06 oracle gates. +- [x] T010 — Reconcile Codex, Qodo, CodeRabbit, and Greptile review truth on the implementation candidate; no substantive returned finding remains unresolved and unavailable/rate-limited reviewers are recorded without invented PASS. +- [x] T011 — Write convergence truth with exact implementation head/run identities and real multi-version lock evidence; final documentation head must rerun all configured CF-11 gates. +- [ ] T012 — Merge only after final exact-head gates; then reconcile CF-10 and rerun the same frozen six-state eligibility sweep before semantic execution.