From 5ef9707930f7095a2f57bc3e38e53bfeac06aaf2 Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 11:30:59 +0200 Subject: [PATCH 01/17] docs: design clean architecture capability packs --- ...ability-packs-clean-architecture-design.md | 547 ++++++++++++++++++ 1 file changed, 547 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md diff --git a/docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md b/docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md new file mode 100644 index 0000000..2619513 --- /dev/null +++ b/docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md @@ -0,0 +1,547 @@ +--- +type: execution-specification +title: "Clean Architecture and Advanced Linux Capability Packs" +description: "Evolve CCP into a Mac-first local Linux qualification platform without coupling its evidence core to individual analysis tools." +status: approved-direction +last_verified: 2026-08-30 +--- + +# Clean Architecture and Advanced Linux Capability Packs + +This file is the canonical execution contract for the current programme. +Conversation history, research notes, and issues are supporting context, not +competing plans. + +## Outcome + +Commit CI Preflight remains a small, language-agnostic qualification and +evidence engine while becoming substantially easier to use for advanced Linux +checks from an Apple Silicon Mac. + +The programme delivers: + +1. an internal Clean Architecture refactor with no observable compatibility + change; +2. a versioned, inspectable Capability Pack contract outside the evidence + domain; +3. one dogfooded Rust deep-analysis reference pack; +4. one language-agnostic repository-security reference pack; +5. a separately versioned design for normalized machine-readable findings; +6. a stable release checkpoint before additional product expansion. + +The result is falsified if CCP becomes a new build language, package manager, +GitHub Actions emulator, cloud runner service, or tool-specific monolith. + +## Authoritative anchors + +| Item | Verified state | Evidence | +|---|---|---| +| Repository | `MarcoPorcellato/commit-ci-preflight` | local `origin` remote | +| Delivery worktree | `/Users/marco1/Documents/CODICE con VS CODE/ccp-worktrees/capability-packs-clean-architecture-v1` | `git worktree list --porcelain` | +| Base revision | `5fed7c443504969e62980141048f9279f9fa1dfe` | fetched `origin/main`, 2026-08-30 | +| Delivery branch | `codex/capability-packs-clean-architecture-v1` | live branch | +| Compatibility envelope | strict preservation | owner approval, 2026-08-30 | +| Product direction | Capability Packs plus later generic evidence adapters | owner approval, 2026-08-30 | +| Current milestone | M0 specification and implementation plan | this document | + +Reverify drift-prone anchors before every external mutation, qualification, or +release operation. + +## Status vocabulary + +- **Verified:** terminal authoritative evidence exists for the exact revision. +- **In delivery:** recoverable committed work exists but its milestone gate is + incomplete. +- **Blocked:** a named external, technical, or human dependency prevents the + path. +- **Planned:** dependency-ordered work with an explicit exit gate. +- **Deferred:** deliberately excluded until a predecessor justifies it. +- **Qualified:** the exact candidate passed every platform and evidence gate + claimed for it. + +## Product decision + +CCP's durable moat is not executing more commands than existing CI systems. It +is the combination of: + +- host resource admission and coordination; +- an exact clean source binding; +- explicit shell-free execution in pinned Linux runtimes; +- bounded caches, artifacts, timeouts, and cleanup; +- canonical receipts and independent policy verification. + +Advanced tools therefore integrate through declarative Capability Packs. The +domain core must not contain branches for Semgrep, Trivy, Miri, Valgrind, +CodeQL, or any other individual analyzer. + +### Rejected alternatives + +1. **Tool-specific core integrations.** Rejected because licensing, release + cadence, report formats, image support, and databases would force external + volatility into the evidence domain. +2. **Executor-only documentation.** Rejected because hand-written argv and + image wiring do not provide a compelling or safe onboarding experience. +3. **A new workflow or build DSL.** Rejected because act, Dagger, Bazel, Nix, + Task, Just, Make, and pre-commit already own those concerns. + +## Clean Architecture boundaries + +Dependency direction is inward: + +```text +CLI / GitHub / pack tooling + | +Application use cases +plan / inspect / execute / verify + | +Domain +check plan / policy / evidence / receipt + ^ +Ports +runtime / storage / process / clock / admission + ^ +Adapters +Docker-compatible runtime / filesystem / GitHub / capability packs +``` + +### Domain + +The domain owns canonical check plans, policy decisions, evidence status, +receipt identity, and compatibility rules. It contains no Clap parsing, +filesystem discovery, Docker argv construction, GitHub API behavior, analyzer +catalog, or human output formatting. + +### Application + +Application services express bounded use cases such as plan, inspect, execute, +and verify. They receive request objects and ports rather than accumulating +telescoping function parameters. + +### Ports + +Ports describe effects required by the application layer: runtime execution, +process supervision, storage, filesystem operations, time, admission, and +publication boundaries. Test doubles implement the same narrow contracts. + +### Adapters + +Docker-compatible execution, local filesystems, the CLI, GitHub integration, +and Capability Pack loading are replaceable adapters. They may depend inward; +the domain never depends outward on them. + +### Compatibility facade + +Existing public Rust modules and functions remain available. Internal request +objects may replace parameter chains, but existing public functions delegate to +the new use cases. This preserves downstream compilation while allowing the +implementation to become coherent. + +## Capability Pack contract + +A pack is inert, declarative input. Installing or inspecting a pack never runs +project code. Execution remains an ordinary reviewed CCP plan. + +Every pack records: + +- stable pack ID, schema version, pack version, license, and upstream sources; +- supported host/runtime architectures and required runtime features; +- pinned OCI image references and tool versions; +- explicit shell-free check argv, working directories, dependencies, resource + profiles, and timeouts; +- declared caches and bounded output artifacts; +- rule, type-stub, corpus, advisory, or vulnerability-database provenance and + digest where applicable; +- network policy and offline preparation requirements; +- deterministic, schedule-sensitive, or bounded-nondeterministic evidence + classification; +- documented PASS/FAIL semantics and known blind spots. + +Packs expand into existing configuration primitives. They do not silently +override repository policy, broaden network access, install host software, +pull mutable images, execute a shell, or publish evidence. + +### Trust model + +- A pack is untrusted data until its schema, bounds, source, and digests pass. +- A pack version is immutable. +- Tool and database freshness is distinct from integrity. +- A report digest proves exact bytes, not the truth or completeness of a tool. +- Findings remain local unless a separately authorized publication surface is + designed and reviewed. +- CodeQL is not an official default pack until licensing, private-repository + eligibility, Linux ABI, and Apple Silicon execution are explicitly resolved. + +## Reference packs + +### `rust-deep` + +The first pack is dogfooded on CCP. Its stable-release minimum is limited to +four independently selectable profiles: strict Clippy, cargo-deny, bounded +Miri, and one Linux AddressSanitizer fixture. Each profile may be omitted for a +consumer repository that lacks its explicit prerequisites; omission is +reported and is never a PASS. + +Loom, cargo-fuzz corpus replay, open-ended fuzzing, mutation testing, other +sanitizers, and reproducible-build comparison are deferred experiments. They +do not belong to the first stable pack exit gate. + +Open-ended fuzzing, timing-sensitive concurrency claims, and mutation testing +must not be represented as ordinary deterministic checks. Every such profile +records seed, budget, corpus digest, schedule bounds, timeout, architecture, +and residual uncertainty. + +### `secure-repository` + +The second pack targets common repository and supply-chain risks. Its +stable-release minimum is limited to actionlint, zizmor, Trivy filesystem or +configuration scanning, and OSV-Scanner lockfile scanning. + +Syft/Grype, ShellCheck, Hadolint, and Semgrep Community Edition with reviewed +local rules remain deferred candidates until the minimum pack is qualified. + +Vulnerability or advisory results bind the exact database snapshot and its +freshness metadata. SBOM generation is evidence inventory, not a vulnerability +PASS. Workflow linting is not proof that GitHub will execute the workflow +identically. + +## Findings and report boundary + +Current receipts prove command identity, status, output digest, and bounded +artifact evidence. They do not interpret analyzer findings. + +The first Capability Pack release uses those existing guarantees and a local +bounded report validator external to the receipt schema. A future findings +contract may normalize SARIF or versioned JSON only after the packs establish +real requirements. + +Any future receipt extension must: + +- use a new explicit schema version; +- retain verification of all historical receipts; +- bind the raw report digest and parser identity; +- impose byte, record, nesting, string, and path limits; +- distinguish tool execution failure from policy findings; +- preserve rule/database provenance and freshness separately; +- avoid raw source, secrets, local paths, and unbounded messages; +- support independent verification without executing the analyzer. + +## Scope + +- Record and progressively enforce the Clean Architecture boundaries. +- Reduce `main.rs` dispatch responsibility and `run.rs` parameter telescoping + through compatibility-preserving seams. +- Define and validate an inert Capability Pack manifest. +- Provide deterministic expansion or generation into existing CCP contracts. +- Deliver and dogfood `rust-deep`. +- Deliver `secure-repository` after the first pack proves the contract. +- Document tool licenses, architectures, offline/freshness behavior, evidence + class, resource cost, and limitations. +- Preserve a durable checkpoint and milestone ledger throughout delivery. + +## Non-goals + +- Reimplement GitHub Actions semantics or marketplace actions. +- Create a new task language, build graph, package manager, container runtime, + hypervisor, remote cache, hosted runner fleet, IDE, or cloud dashboard. +- Automatically install tools on macOS. +- Automatically grant Docker socket, privileged mode, host networking, secrets, + or mutable network access. +- Claim native Linux hardware equivalence for emulation or a Mac-hosted VM. +- Claim monetary savings without repository-specific billing evidence. +- Implement a new receipt schema before pack evidence demonstrates the need. + +## Invariants and boundaries + +- Existing CLI syntax, exit codes, valid configuration bytes and digests, + receipt bytes and IDs, policy schemas, JSON shapes, and public Rust facade + remain compatible. +- M0 records a compatibility corpus under + `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/compatibility/`. + Its manifest names and hashes the current CLI help surfaces, exit-code + cases, canonical v1/v2/matrix plan and dry-run fixtures, receipt and policy + fixtures, verification decisions, and a downstream compile fixture that + imports the supported public Rust facade. M1 must compare the candidate + against these exact files and produce no unexplained byte or compile delta. +- No analyzer-specific dependency enters the evidence domain. +- All new parsing is bounded, deny-unknown where appropriate, deterministic, + and fail closed. +- Source remains read-only during qualification; writable outputs are explicit. +- Network is disabled by default. Preparation and offline execution are + distinct operations and evidence classes. +- The public repository uses hosted CI by default under the economic policy. + CCP heavy work requires an explicit non-economic exception and exact + authorization. +- Commit, push, PR creation, ready transition, merge, tag, release, package + publication, stable installation, and external evidence publication remain + separate gates. +- Architecture, security, integration, release, and final evidence decisions + stay with the primary agent. + +## Ordered milestones + +### M0 — Durable design and TDD execution plan + +**Outcome** + +- Canonical specification, persistent goal, gap inventory, and dependency- + ordered TDD plan are committed locally. +- A named and hash-manifested compatibility corpus makes the strict envelope + executable rather than aspirational. + +**Dependencies** + +- Approved product direction and exact isolated base. + +**Exit evidence** + +- Markdown contract checks and independent documentation review. +- Compatibility inventory with complete file hashes and commands that can + regenerate or compare every item without network or project execution. +- Local commit on the delivery branch. + +**Impact** + +- The programme can survive compaction, restart, or delegation. + +**Residual risk** + +- No runtime behavior or product capability has changed. + +### M1 — Compatibility-preserving application seam + +**Outcome** + +- One vertical execution path uses a coherent request/dependency object and + narrow application service while existing public entry points delegate to it. +- CLI parsing/formatting is separated from the selected use case. + +**Dependencies** + +- M0. + +**Exit evidence** + +- RED/GREEN unit and integration tests. +- Byte-for-byte comparison against the M0 CLI, exit-code, plan, dry-run, + receipt, policy, verification, and JSON corpus. +- Compile success of the M0 downstream public-facade fixture without source + changes. +- Hosted CI terminal on the exact commit before integration. + +**Impact** + +- Establishes an architectural seam without a broad rewrite. + +**Residual risk** + +- Other command families remain on their current internal structure. + +### M2 — Capability Pack schema and inert inspection + +**Outcome** + +- A bounded versioned manifest, validator, canonical representation, and inert + inspection/expansion surface exist without executing project code. + +**Dependencies** + +- M1 seam or an independently reviewed adapter boundary. + +**Exit evidence** + +- Parser abuse/boundary tests, golden canonicalization fixtures, unknown-field + rejection, path/image/license/provenance validation, and no-execution tests. +- Compatibility suite remains byte-stable. + +**Impact** + +- Tool integrations become data-driven rather than core branches. + +**Residual risk** + +- No reference pack is yet qualified. + +### M3 — `rust-deep` reference pack + +**Outcome** + +- A documented subset of Rust deep checks runs through ordinary CCP primitives + in a Docker-compatible Linux VM hosted on Apple Silicon macOS and produces + bounded local artifacts. + +**Dependencies** + +- M2 and reviewed tool/image/license matrix. + +**Exit evidence** + +- Synthetic positive and negative fixtures. +- The four stable profiles are inspected and fixture-qualified; strict Clippy, + cargo-deny, and bounded Miri are dogfooded where their declared prerequisites + exist, and the AddressSanitizer profile executes a dedicated Linux fixture. +- Every omitted profile is recorded as NOT_RUN with its missing prerequisite. +- Deferred or nondeterministic profiles cannot satisfy this milestone. +- Exact architecture, image, toolchain, corpus/rules, and artifact hashes. + +**Impact** + +- Demonstrates high-value Linux analysis unavailable or awkward in ordinary + macOS-only workflows. + +**Residual risk** + +- Proves only the exact Mac host, Docker-compatible runtime, Linux guest + architecture (initially `linux/arm64`), image, and selected tools. It does not + prove a native Linux host, `linux/amd64`, or macOS-native execution. + +### M4 — `secure-repository` reference pack + +**Outcome** + +- Repository, workflow, dependency, SBOM, vulnerability, and rule-based checks + are available through reviewed bounded profiles. + +**Dependencies** + +- M2 and lessons from M3. + +**Exit evidence** + +- For each of the four stable tools: exact version, upstream source commit or + release, authoritative URL, license and redistribution mode, OCI image + digest, `linux/arm64` support evidence, rules or database snapshot digest and + freshness metadata where applicable. +- DB/rules provenance fixtures, stale/offline failure tests, and + positive/negative repositories. +- Deferred candidates cannot satisfy this milestone. + +**Impact** + +- Extends CCP beyond language-specific tests without polluting its core. + +**Residual risk** + +- Scanner blind spots and database freshness remain explicit. + +### M5 — Generic findings contract decision + +**Outcome** + +- Evidence from M3/M4 produces either an approved versioned report design or a + documented decision that artifact digests remain sufficient. +- The decision is recorded in + `docs/superpowers/specs/2026-08-30-findings-contract-decision.md`, owned by + the primary agent, with exactly one terminal verdict: `GO_VERSIONED_SCHEMA` + or `NO_GO_KEEP_ARTIFACT_DIGESTS`. + +**Dependencies** + +- Real report formats and policy needs observed in M3/M4. + +**Exit evidence** + +- Threat model, parser limits, compatibility analysis, independent verifier + impact, and migration plan. +- A per-tool table covering report format/version stability, maximum observed + bytes and records, policy semantics, parser ownership, privacy fields, and + provenance needs. +- Every decision checklist item is resolved; missing evidence forces + `NO_GO_KEEP_ARTIFACT_DIGESTS` rather than an ambiguous defer. + +**Impact** + +- Prevents speculative receipt/schema complexity. + +**Residual risk** + +- Implementation, if approved, is a separately gated programme. + +### M6 — Stable release checkpoint + +**Outcome** + +- The compatible architectural seam, pack contract, qualified reference packs, + installation guidance, changelog, release notes, and rollback procedure are + ready as one stable candidate. + +**Dependencies** + +- M1-M4 terminal; M5 decision terminal. + +**Exit evidence** + +- Exact-head hosted CI and platform matrix appropriate to the claims. +- Release artifact hashes, SBOM, independent verifier result, install/rollback + proof, and owner-authorized tag/release publication. + +**Impact** + +- Establishes a stable usable version before additional functionality. + +**Residual risk** + +- Unqualified platforms and optional tools remain explicitly unsupported. + +## Delegation and cost policy + +1. Use deterministic tools before LLMs. +2. Delegate research, inventory, documentation drafts, mechanical isolated + edits, test execution, and bounded reviews to Luna or Spark. +3. Assign one owner per file group; no overlapping writes. +4. Keep architecture, security, integration, release, and final judgment with + the primary agent. +5. Run narrow deterministic checks before broader validation. +6. Stop a cheap worker after one clear failure and one focused correction. + +## Validation and publication gates + +- Every production change follows RED/GREEN TDD. +- Every milestone includes focused tests and compatibility fixtures. +- Full hosted CI is the authoritative public-repository integration gate. +- A CCP heavy run is not the default for this public repository and requires a + separately documented non-economic exception. +- No generated pack may execute during inspection or configuration generation. +- Tool licenses and distribution rights are verified before an official pack + embeds or redistributes anything. +- Push requires the exact local head and non-force destination. +- PR creation, ready transition, merge, tag, release, package publication, and + stable installation are separate exact-state gates. + +## Interruption and recovery + +Maintain `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/` +with a concise persistent goal, current progress, exact branch/HEAD/base, +completed validations, active worker state, unproven gates, and next action. + +Before a restart or handoff: + +1. stop or record active workers and processes; +2. record worktree, branch, exact HEAD, base, dirty state, and remotes; +3. save in-scope work in a recoverable local commit; +4. record local, hosted, and release evidence separately; +5. preserve exact resume commands and authorization boundaries. + +## Milestone report format + +- Result obtained +- Terminal validation evidence +- Behavior or claims changed +- Residual risks +- Next dependency and approval gate + +## Completion checklist + +- [ ] M0 canonical specification, persistent goal, and TDD plan are committed. +- [ ] M1 creates a tested compatibility-preserving architectural seam. +- [ ] Existing CLI, exit codes, plans, receipts, schemas, and public facade are + proven compatible. +- [ ] M2 validates inert, bounded, versioned Capability Packs. +- [ ] `rust-deep` has documented deterministic and nondeterministic profiles. +- [ ] `secure-repository` binds tool, rule, and database provenance. +- [ ] M5 reaches an evidence-backed findings-contract decision. +- [ ] Documentation and onboarding match observed behavior. +- [ ] A fresh Mac user can inspect and run a supported pack through the + documented entry point. +- [ ] Stable release evidence, rollback, tag, and publication are terminal. + +Completion is unproven until every applicable item has authoritative evidence. From 6bb5b313b1e4a46fe46b9ab4b1fd55a3f9960c6a Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 12:36:38 +0200 Subject: [PATCH 02/17] docs: plan compatibility baseline and run seam --- .../2026-08-30-m0-compatibility-baseline.md | 771 ++++++++++++++++++ .../2026-08-30-m1-run-application-seam.md | 300 +++++++ ...ability-packs-clean-architecture-design.md | 18 +- 3 files changed, 1083 insertions(+), 6 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-30-m0-compatibility-baseline.md create mode 100644 docs/superpowers/plans/2026-08-30-m1-run-application-seam.md diff --git a/docs/superpowers/plans/2026-08-30-m0-compatibility-baseline.md b/docs/superpowers/plans/2026-08-30-m0-compatibility-baseline.md new file mode 100644 index 0000000..d0c0c70 --- /dev/null +++ b/docs/superpowers/plans/2026-08-30-m0-compatibility-baseline.md @@ -0,0 +1,771 @@ +# M0 Compatibility Baseline Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Create a durable, executable baseline that makes CCP's strict CLI, exit-code, canonical-byte, verification, and public Rust API compatibility contract falsifiable before internal refactoring begins. + +**Architecture:** The baseline is test-only evidence. Existing golden fixtures remain authoritative; a small integration contract binds them, exact CLI help bytes, deterministic read-only command outputs, and a downstream compile fixture to base commit `5fed7c443504969e62980141048f9279f9fa1dfe`. No production module, schema, receipt, runtime, or CLI behavior changes. + +**Tech Stack:** Rust 2024 integration tests, `std::process::Command`, existing `sha2` and `serde_json`, Markdown/JSON fixtures, Cargo offline mode. + +**Spec:** `docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md` + +## Global Constraints + +- Base is exact commit `5fed7c443504969e62980141048f9279f9fa1dfe`; design-only commit is `5ef9707930f7095a2f57bc3e38e53bfeac06aaf2`. +- Every shell command begins with `rtk`. +- Preserve existing CLI syntax, exit codes, valid configuration bytes and digests, receipt bytes and IDs, policy schemas, JSON shapes, and public Rust facade. +- This milestone executes no Docker workload, CCP heavy command, network operation, publication, or external mutation. +- Fixture generation may execute only read-only CCP commands whose current integration tests already prove they do not execute project checks: help, `plan`, and `verify` against checked-in fixtures. +- Store no usernames, absolute local paths, environment dumps, source contents, secrets, container IDs, or raw project logs. +- Use RED/GREEN TDD for every executable contract. + +--- + +### Task 1: Durable goal and progress ledger + +**Files:** +- Create: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/goal.txt` +- Create: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/progress.md` + +**Interfaces:** +- Consumes: the canonical specification and exact design commit. +- Produces: the restart-safe execution pointer used by every later task. + +- [ ] **Step 1: Create the concise persistent goal** + +Write exactly: + +```text +Complete the Clean Architecture and Advanced Linux Capability Packs programme +according to `docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md`. + +First verify the live worktree, exact branch/HEAD/base, active operator policy, +and the progress ledger. Preserve strict CLI, exit-code, plan, receipt, policy, +JSON, and public Rust API compatibility. Use deterministic tools first and +delegate bounded inventory, mechanical edits, tests, and review to Luna or +Spark; retain architecture, security, integration, qualification, release, and +merge judgment centrally. + +Proceed through dependency-ordered milestones until the canonical checklist is +proved. Do not run CCP heavy work for this public repository without an exact +approved non-economic exception. Treat push, PR, ready transition, merge, tag, +release, stable installation, and external publication as separate exact-state +gates. +``` + +- [ ] **Step 2: Create the progress ledger** + +Write: + +```markdown +# Capability Packs and Clean Architecture progress + +- Base: `5fed7c443504969e62980141048f9279f9fa1dfe` +- Branch: `codex/capability-packs-clean-architecture-v1` +- Specification commit: `5ef9707930f7095a2f57bc3e38e53bfeac06aaf2` +- Current milestone: M0 compatibility baseline +- Completed evidence: design review READY; `git diff --check` PASS +- Unproven: baseline tests, application seam, pack contract, reference packs, + hosted CI, publication, release +- Heavy processes: none +- External mutations: none +- Next action: execute Task 2 of the M0 plan with RED/GREEN TDD +``` + +- [ ] **Step 3: Verify the files are bounded and path-free** + +Run: + +```console +rtk rg -n "(/Users/|TODO|TBD|FIXME|container ID|secret)" .superpowers/sdd/2026-08-30-capability-packs-clean-architecture +``` + +Expected: no match. The relative specification path is permitted; no local +absolute path is stored. + +- [ ] **Step 4: Commit Task 1** + +```console +rtk git add .superpowers/sdd/2026-08-30-capability-packs-clean-architecture/goal.txt .superpowers/sdd/2026-08-30-capability-packs-clean-architecture/progress.md +rtk git commit -m "docs: add capability programme checkpoint" +``` + +### Task 2: Pin CLI and canonical evidence bytes + +**Files:** +- Create: `tests/compatibility_baseline.rs` +- Create: `tests/fixtures/compatibility/root-help.stdout.txt` +- Create: `tests/fixtures/compatibility/plan-help.stdout.txt` +- Create: `tests/fixtures/compatibility/verify-help.stdout.txt` +- Create: `tests/fixtures/compatibility/plan-v1.stdout.json` +- Create: `tests/fixtures/compatibility/plan-v2-legacy.stdout.json` +- Create: `tests/fixtures/compatibility/dry-run-v1.normalized.json` +- Create: `tests/fixtures/compatibility/dry-run-v2-current.normalized.json` +- Create: `tests/fixtures/compatibility/dry-run-v2-legacy.normalized.json` +- Create: `tests/fixtures/compatibility/verify-v1-pass.stdout.json` +- Create: `tests/fixtures/compatibility/verify-v1-fail.stdout.json` +- Reuse: `tests/fixtures/plan-v2-current-default.stdout.json` +- Reuse: `tests/fixtures/receipt-v1-pass.json` +- Reuse: `tests/fixtures/receipt-v2-pass.json` +- Reuse: `tests/fixtures/matrix-v2-legacy-plan-044697.json` +- Reuse: `tests/fixtures/policy-v1.toml` +- Reuse: `tests/fixtures/policy-v1_1-trusted-plan.toml` +- Reuse: `tests/fixtures/policy-v2-legacy-compatible.toml` + +**Interfaces:** +- Consumes: `CARGO_BIN_EXE_commit-ci-preflight` and existing read-only fixtures. +- Produces: exact byte comparisons for public CLI/canonical evidence plus + privacy-normalized dry-run projections used by M1. + +- [ ] **Step 1: Write a deliberately incomplete failing contract** + +Create `tests/compatibility_baseline.rs` with imports, constants, and the first +test. Do not create the snapshot files yet: + +```rust +use std::process::{Command, Output}; + +const COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; +const EVALUATED_AT: &str = "2026-08-08T12:30:00Z"; + +fn ccp(args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_commit-ci-preflight")) + .args(args) + .output() + .expect("execute compatibility command") +} + +#[test] +fn root_help_bytes_match_the_baseline() { + let output = ccp(&["--help"]); + assert_eq!(output.status.code(), Some(0)); + assert!(output.stderr.is_empty()); + assert_eq!( + output.stdout, + include_bytes!("fixtures/compatibility/root-help.stdout.txt") + ); +} +``` + +- [ ] **Step 2: Run the focused test and verify RED** + +Run: + +```console +rtk cargo test --locked --offline --test compatibility_baseline root_help_bytes_match_the_baseline +``` + +Expected: compile failure because +`tests/fixtures/compatibility/root-help.stdout.txt` does not exist. + +- [ ] **Step 3: Capture exact read-only baseline bytes** + +Build the test binary once with the focused test command, then run only these +read-only commands from the repository root and save stdout byte-for-byte under +the named fixture paths: + +```console +rtk cargo run --locked --offline -- --help +rtk cargo run --locked --offline -- plan --help +rtk cargo run --locked --offline -- verify --help +rtk cargo run --locked --offline -- plan --config tests/fixtures/config-v1-read-only.toml --json +rtk cargo run --locked --offline -- plan --config tests/fixtures/config-v2-matrix.toml --json +rtk cargo run --locked --offline -- plan --config tests/fixtures/config-v2-legacy-compatible.toml --matrix-plan-profile matrix-v2-legacy-v1 --json +rtk cargo run --locked --offline -- verify --receipt tests/fixtures/receipt-v1-pass.json --policy tests/fixtures/policy-v1.toml --expected-commit 0123456789abcdef0123456789abcdef01234567 --evaluated-at-utc 2026-08-08T12:30:00Z --json +``` + +For the failing verification snapshot, use the same command with expected +commit `bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb`; require exit `3` and save +stdout only. Use `apply_patch` for every fixture write; do not use shell +redirection. + +- [ ] **Step 4: Complete the byte and exit-code contract** + +Add tests that: + +```rust +#[test] +fn command_help_bytes_match_the_baseline() { + for (command, expected) in [ + ("plan", include_bytes!("fixtures/compatibility/plan-help.stdout.txt").as_slice()), + ("verify", include_bytes!("fixtures/compatibility/verify-help.stdout.txt").as_slice()), + ] { + let output = ccp(&[command, "--help"]); + assert_eq!(output.status.code(), Some(0)); + assert!(output.stderr.is_empty()); + assert_eq!(output.stdout, expected, "{command} help drifted"); + } +} + +#[test] +fn plan_and_verification_bytes_and_exit_codes_match_the_baseline() { + let plan = ccp(&[ + "plan", "--config", "tests/fixtures/config-v1-read-only.toml", "--json", + ]); + assert_eq!(plan.status.code(), Some(0)); + assert!(plan.stderr.is_empty()); + assert_eq!(plan.stdout, include_bytes!("fixtures/compatibility/plan-v1.stdout.json")); + + let pass = ccp(&[ + "verify", "--receipt", "tests/fixtures/receipt-v1-pass.json", + "--policy", "tests/fixtures/policy-v1.toml", "--expected-commit", COMMIT, + "--evaluated-at-utc", EVALUATED_AT, "--json", + ]); + assert_eq!(pass.status.code(), Some(0)); + assert!(pass.stderr.is_empty()); + assert_eq!(pass.stdout, include_bytes!("fixtures/compatibility/verify-v1-pass.stdout.json")); + + let fail = ccp(&[ + "verify", "--receipt", "tests/fixtures/receipt-v1-pass.json", + "--policy", "tests/fixtures/policy-v1.toml", "--expected-commit", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "--evaluated-at-utc", + EVALUATED_AT, "--json", + ]); + assert_eq!(fail.status.code(), Some(3)); + assert_eq!(fail.stdout, include_bytes!("fixtures/compatibility/verify-v1-fail.stdout.json")); +} + +#[test] +fn matrix_plan_profiles_match_the_baseline() { + let current = ccp(&[ + "plan", "--config", "tests/fixtures/config-v2-matrix.toml", "--json", + ]); + assert_eq!(current.status.code(), Some(0)); + assert!(current.stderr.is_empty()); + assert_eq!(current.stdout, include_bytes!("fixtures/plan-v2-current-default.stdout.json")); + + let legacy = ccp(&[ + "plan", "--config", "tests/fixtures/config-v2-legacy-compatible.toml", + "--matrix-plan-profile", "matrix-v2-legacy-v1", "--json", + ]); + assert_eq!(legacy.status.code(), Some(0)); + assert!(legacy.stderr.is_empty()); + assert_eq!(legacy.stdout, include_bytes!("fixtures/compatibility/plan-v2-legacy.stdout.json")); +} + +fn normalize_mount_argv(argument: &str, sources: &std::collections::BTreeMap) -> String { + let Some(rest) = argument.strip_prefix("type=bind,src=") else { + return argument.to_owned(); + }; + let (source, suffix) = rest.split_once(",dst=").expect("bind destination"); + let token = sources.get(source).expect("declared mount source"); + format!("type=bind,src={token},dst={suffix}") +} + +fn normalize_one_dry_run(mut value: serde_json::Value) -> (serde_json::Value, Vec) { + let workspace = value["workspace"].as_object().expect("workspace object"); + let repository = workspace["repository"].as_str().expect("repository path").to_owned(); + let run_root = workspace["run_root"].as_str().expect("run root").to_owned(); + let mut sources = std::collections::BTreeMap::new(); + sources.insert(repository.clone(), "$REPOSITORY".to_owned()); + for mount in workspace["mounts"].as_array().expect("mounts") { + let source = mount["source"].as_str().expect("mount source").to_owned(); + let purpose = mount["purpose"].as_str().expect("mount purpose"); + let logical_id = mount.get("logical_id").and_then(|item| item.as_str()); + let token = match (purpose, logical_id) { + ("repository", _) => "$REPOSITORY".to_owned(), + ("cache", Some(id)) => format!("$CACHE:{id}"), + ("artifact", Some(id)) => format!("$ARTIFACT:{id}"), + _ => panic!("unsupported mount identity"), + }; + sources.insert(source, token); + } + + value["workspace"]["repository"] = serde_json::json!("$REPOSITORY"); + value["workspace"]["run_root"] = serde_json::json!("$RUN_ROOT"); + for mount in value["workspace"]["mounts"].as_array_mut().expect("mounts") { + let source = mount["source"].as_str().expect("mount source"); + mount["source"] = serde_json::json!(sources.get(source).expect("mount token")); + } + for check in value["checks"].as_array_mut().expect("checks") { + for argument in check["argv"].as_array_mut().expect("argv") { + let raw = argument.as_str().expect("argv string"); + *argument = serde_json::json!(normalize_mount_argv(raw, &sources)); + } + } + + let mut host_paths = sources.into_keys().collect::>(); + host_paths.push(run_root); + (value, host_paths) +} + +fn normalize_dry_run(mut value: serde_json::Value) -> (serde_json::Value, Vec) { + let mut host_paths = Vec::new(); + if let Some(runtimes) = value.get_mut("runtimes").and_then(|value| value.as_array_mut()) { + for runtime in runtimes { + let dry_run = runtime.get_mut("dry_run").expect("matrix dry-run").take(); + let (normalized, paths) = normalize_one_dry_run(dry_run); + runtime["dry_run"] = normalized; + host_paths.extend(paths); + } + (value, host_paths) + } else { + normalize_one_dry_run(value) + } +} + +#[test] +fn dry_run_profiles_match_the_baseline_without_execution() { + for (args, expected) in [ + ( + vec!["dry-run", "--config", "tests/fixtures/config-v1-read-only.toml", "--json"], + include_bytes!("fixtures/compatibility/dry-run-v1.normalized.json").as_slice(), + ), + ( + vec!["dry-run", "--config", "tests/fixtures/config-v2-matrix.toml", "--json"], + include_bytes!("fixtures/compatibility/dry-run-v2-current.normalized.json").as_slice(), + ), + ( + vec![ + "dry-run", "--config", "tests/fixtures/config-v2-legacy-compatible.toml", + "--matrix-plan-profile", "matrix-v2-legacy-v1", "--json", + ], + include_bytes!("fixtures/compatibility/dry-run-v2-legacy.normalized.json").as_slice(), + ), + ] { + let output = ccp(&args); + assert_eq!(output.status.code(), Some(0)); + assert!(output.stderr.is_empty()); + let value: serde_json::Value = serde_json::from_slice(&output.stdout).expect("dry-run JSON"); + let original = value.clone(); + let (normalized, host_paths) = normalize_dry_run(value); + assert_dry_run_normalization_preserves_contract(&original, &normalized); + let serialized = serde_json::to_vec(&normalized).expect("normalized JSON"); + for path in host_paths { + assert!(!serialized.windows(path.len()).any(|window| window == path.as_bytes())); + } + assert_eq!(serialized, expected); + if let Some(executed) = value.get("executed") { + assert_eq!(executed, false); + } else { + for runtime in value["runtimes"].as_array().expect("matrix runtimes") { + assert_eq!(runtime["dry_run"]["executed"], false); + } + } + } +} + +#[test] +fn usage_error_exit_code_remains_two() { + let output = ccp(&[ + "plan", "--config", "tests/fixtures/does-not-exist.toml", + ]); + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); +} +``` + +Implement `assert_dry_run_normalization_preserves_contract` alongside the +normalizer. For each single or matrix runtime it must assert identical +top-level key sets, check count, check IDs, programs, dependencies, argv count, +workspace key sets, mount count, `target`, `access`, `purpose`, and +`logical_id`. Every argv element must be byte-identical unless the original +starts with `type=bind,src=`; for those elements, split both values at `,dst=` +and require identical suffixes plus a normalized prefix beginning +`type=bind,src=$`. This assertion runs before every snapshot comparison. + +```rust +fn object_keys(value: &serde_json::Value) -> std::collections::BTreeSet<&str> { + value + .as_object() + .expect("object") + .keys() + .map(String::as_str) + .collect() +} + +fn assert_one_dry_run_contract(original: &serde_json::Value, normalized: &serde_json::Value) { + assert_eq!(object_keys(original), object_keys(normalized)); + for field in ["schema_version", "plan_digest", "runtime", "program", "workspace_mount_policy", "executed"] { + assert_eq!(original[field], normalized[field], "{field}"); + } + let original_checks = original["checks"].as_array().expect("original checks"); + let normalized_checks = normalized["checks"].as_array().expect("normalized checks"); + assert_eq!(original_checks.len(), normalized_checks.len()); + for (before, after) in original_checks.iter().zip(normalized_checks) { + assert_eq!(object_keys(before), object_keys(after)); + for field in ["id", "program", "depends_on"] { + assert_eq!(before[field], after[field], "{field}"); + } + let before_argv = before["argv"].as_array().expect("original argv"); + let after_argv = after["argv"].as_array().expect("normalized argv"); + assert_eq!(before_argv.len(), after_argv.len()); + for (raw, scrubbed) in before_argv.iter().zip(after_argv) { + let raw = raw.as_str().expect("raw argv"); + let scrubbed = scrubbed.as_str().expect("scrubbed argv"); + if let Some(raw_mount) = raw.strip_prefix("type=bind,src=") { + let (_, raw_suffix) = raw_mount.split_once(",dst=").expect("raw destination"); + let scrubbed_mount = scrubbed.strip_prefix("type=bind,src=$").expect("tokenized source"); + let (_, scrubbed_suffix) = scrubbed_mount.split_once(",dst=").expect("scrubbed destination"); + assert_eq!(raw_suffix, scrubbed_suffix); + } else { + assert_eq!(raw, scrubbed); + } + } + } + + let before_workspace = &original["workspace"]; + let after_workspace = &normalized["workspace"]; + assert_eq!(object_keys(before_workspace), object_keys(after_workspace)); + assert_eq!(before_workspace["schema_version"], after_workspace["schema_version"]); + assert_eq!(before_workspace["source_snapshot_digest"], after_workspace["source_snapshot_digest"]); + let before_mounts = before_workspace["mounts"].as_array().expect("original mounts"); + let after_mounts = after_workspace["mounts"].as_array().expect("normalized mounts"); + assert_eq!(before_mounts.len(), after_mounts.len()); + for (before, after) in before_mounts.iter().zip(after_mounts) { + assert_eq!(object_keys(before), object_keys(after)); + for field in ["target", "access", "purpose", "logical_id"] { + assert_eq!(before.get(field), after.get(field), "{field}"); + } + } +} + +fn assert_dry_run_normalization_preserves_contract( + original: &serde_json::Value, + normalized: &serde_json::Value, +) { + assert_eq!(object_keys(original), object_keys(normalized)); + match (original.get("runtimes"), normalized.get("runtimes")) { + (Some(before), Some(after)) => { + let before = before.as_array().expect("original runtimes"); + let after = after.as_array().expect("normalized runtimes"); + assert_eq!(before.len(), after.len()); + for (before, after) in before.iter().zip(after) { + assert_eq!(object_keys(before), object_keys(after)); + assert_eq!(before["runtime_id"], after["runtime_id"]); + assert_eq!(before["configuration_digest"], after["configuration_digest"]); + assert_one_dry_run_contract(&before["dry_run"], &after["dry_run"]); + } + } + (None, None) => assert_one_dry_run_contract(original, normalized), + _ => panic!("matrix shape changed"), + } +} +``` + +Add one ignored test that runs the same three command arrays, calls +`normalize_dry_run`, and prints `serde_json::to_string` for each named fixture. +Run it once with: + +```console +rtk cargo test --locked --offline --test compatibility_baseline print_normalized_dry_run_baselines -- --ignored --nocapture +``` + +Use `apply_patch` to save only the normalized one-line JSON values. The test's +exact host-path absence assertions are authoritative across `/Users`, `/tmp`, +`/private`, `/var/folders`, `/Volumes`, or any other absolute host root. Raw +dry-run JSON is never committed. + +- [ ] **Step 5: Verify GREEN and existing neighboring contracts** + +```console +rtk cargo test --locked --offline --test compatibility_baseline +rtk cargo test --locked --offline --test plan_cli +rtk cargo test --locked --offline --test verify_cli +rtk cargo test --locked --offline --test receipt_contract +rtk cargo test --locked --offline --test matrix_contract +rtk cargo test --locked --offline --test verification_contract +``` + +Expected: every named test target passes without Docker or network. + +- [ ] **Step 6: Commit Task 2** + +```console +rtk git add tests/compatibility_baseline.rs tests/fixtures/compatibility +rtk git commit -m "test: pin compatibility baseline bytes" +``` + +### Task 3: Compile the supported public Rust facade downstream + +**Files:** +- Create: `tests/fixtures/public-api-compat/Cargo.toml` +- Create: `tests/fixtures/public-api-compat/src/main.rs` +- Modify: `tests/compatibility_baseline.rs` + +**Interfaces:** +- Consumes: modules and public symbols exported through `src/lib.rs`. +- Produces: a downstream compile gate that later internal refactors must preserve. + +- [ ] **Step 1: Write the failing downstream check** + +Add: + +```rust +#[test] +fn supported_public_facade_compiles_downstream() { + let status = Command::new(env!("CARGO")) + .args([ + "check", "--locked", "--offline", "--quiet", + "--manifest-path", "tests/fixtures/public-api-compat/Cargo.toml", + ]) + .status() + .expect("check downstream facade fixture"); + assert!(status.success()); +} +``` + +- [ ] **Step 2: Run the focused test and verify RED** + +```console +rtk cargo test --locked --offline --test compatibility_baseline supported_public_facade_compiles_downstream +``` + +Expected: FAIL because the downstream manifest does not exist. + +- [ ] **Step 3: Add the downstream fixture manifest** + +```toml +[package] +name = "ccp-public-api-compat" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +commit-ci-preflight = { path = "../../.." } + +[workspace] +``` + +- [ ] **Step 4: Add the compile-only facade consumer** + +The fixture imports the supported modules and takes function pointers without +performing I/O: + +```rust +use commit_ci_preflight::{config, matrix, receipt, run, runtime, verify}; + +fn main() { + let _schema: fn() -> Result = config::config_schema_json; + let _canonical = canonical_u8; +} + +fn canonical_u8(value: &u8) -> Result, receipt::ReceiptError> { + receipt::canonical_json(value) +} + +fn verify_v1( + bytes: &[u8], + policy: &verify::VerificationPolicyV1, + commit: &str, + evaluated_at: &str, +) -> Result { + verify::verify_receipt_document(bytes, policy, commit, evaluated_at) +} + +fn verify_v2( + bytes: &[u8], + policy: &matrix::MatrixVerificationPolicyV2, + commit: &str, + evaluated_at: &str, +) -> Result { + matrix::verify_matrix_receipt_document(bytes, policy, commit, evaluated_at) +} + +fn execute<'a>( + request: &run::RunRequest<'a>, + runtime_port: &dyn runtime::RuntimePort, + supervisor: &dyn commit_ci_preflight::process::SupervisorPort, + cancellation: &commit_ci_preflight::process::CancellationToken, + clock: &dyn run::Clock, +) -> Result { + run::execute_local_run(request, runtime_port, supervisor, cancellation, clock) +} + +fn select_runtime( + kind: config::RuntimeKind, +) -> Result, runtime::RuntimeError> { + runtime::runtime_for(kind) +} +``` + +The unused helper functions are intentional compile-only bindings. Add +`#![allow(dead_code)]` at the top; do not execute them. + +- [ ] **Step 5: Lock and verify the downstream fixture offline** + +Generate its lockfile from already cached dependencies, then verify: + +```console +rtk cargo generate-lockfile --offline --manifest-path tests/fixtures/public-api-compat/Cargo.toml +rtk cargo test --locked --offline --test compatibility_baseline supported_public_facade_compiles_downstream +``` + +Expected: PASS and no network access. + +- [ ] **Step 6: Commit Task 3** + +```console +rtk git add tests/compatibility_baseline.rs tests/fixtures/public-api-compat +rtk git commit -m "test: compile supported public facade downstream" +``` + +### Task 4: Hash manifest and M0 closure + +**Files:** +- Create: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json` +- Create: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/compatibility/README.md` +- Modify: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/progress.md` + +**Interfaces:** +- Consumes: all M0 fixtures and tests. +- Produces: stable file inventory and regeneration commands for M1 comparison. + +- [ ] **Step 1: Write the failing manifest validation test** + +Extend the integration test imports with `std::fs`, `std::path::{Component, +Path}`, and `sha2::{Digest, Sha256}`. Add: + +```rust +#[test] +fn manifest_paths_and_hashes_match() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let manifest_path = root.join( + ".superpowers/sdd/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json", + ); + let manifest: serde_json::Value = serde_json::from_slice( + &fs::read(&manifest_path).expect("read compatibility manifest"), + ) + .expect("parse compatibility manifest"); + assert_eq!(manifest["schema_version"], "1.0"); + assert_eq!( + manifest["base_commit"], + "5fed7c443504969e62980141048f9279f9fa1dfe" + ); + assert_eq!(manifest["hash_algorithm"], "sha256"); + + let object = manifest.as_object().expect("manifest object"); + let keys = object + .keys() + .map(String::as_str) + .collect::>(); + assert_eq!( + keys, + std::collections::BTreeSet::from([ + "base_commit", + "files", + "hash_algorithm", + "non_executed_surfaces", + "schema_version", + ]) + ); + assert_eq!( + manifest["non_executed_surfaces"], + serde_json::json!([ + "admission and resource decisions", + "benchmark", + "doctor runtime probe", + "dry-run runtime rendering", + "guard exec", + "project run", + "receipt publication" + ]) + ); + + let entries = manifest["files"].as_array().expect("file entries"); + let mut previous = None::; + for entry in entries { + let entry_object = entry.as_object().expect("file entry object"); + let entry_keys = entry_object + .keys() + .map(String::as_str) + .collect::>(); + assert_eq!( + entry_keys, + std::collections::BTreeSet::from(["digest", "path"]) + ); + let relative = entry["path"].as_str().expect("relative path"); + let path = Path::new(relative); + assert!(!path.is_absolute()); + assert!(path.components().all(|component| matches!( + component, + Component::Normal(_) | Component::CurDir + ))); + if let Some(previous) = &previous { + assert!(previous.as_str() < relative, "paths must be unique and sorted"); + } + previous = Some(relative.to_owned()); + + let bytes = fs::read(root.join(path)).expect("read manifested file"); + let digest = format!("sha256:{:x}", Sha256::digest(bytes)); + assert_eq!(entry["digest"].as_str(), Some(digest.as_str()), "{relative}"); + } +} +``` + +- [ ] **Step 2: Run the focused test and verify RED** + +```console +rtk cargo test --locked --offline --test compatibility_baseline manifest_paths_and_hashes_match +``` + +Expected: FAIL because `manifest.json` does not exist. + +- [ ] **Step 3: Generate complete SHA-256 evidence** + +Run `rtk sha256sum` over every file explicitly listed in Tasks 2 and 3 plus: + +```text +schema/config-v1.schema.json +schema/config-v2.schema.json +schema/receipt-v1.schema.json +schema/receipt-v2.schema.json +schema/policy-v1.schema.json +schema/policy-v1_1.schema.json +schema/policy-v2.schema.json +schema/benchmark-v1.schema.json +schema/verification-report-v1.schema.json +``` + +Include the now-final `tests/compatibility_baseline.rs`. Exclude +`manifest.json` itself to avoid a recursive hash. Require `rtk rg --files +schema` to return exactly this set before hashing. A +missing or additional schema stops the task and requires an explicit plan +review rather than silent omission. + +- [ ] **Step 4: Write the manifest** + +Write one valid JSON object with exactly five top-level members: +`schema_version`, `base_commit`, `hash_algorithm`, `files`, and +`non_executed_surfaces`. Their first three string values are respectively +`1.0`, `5fed7c443504969e62980141048f9279f9fa1dfe`, and `sha256`. `files` is a +lexicographically ordered array of objects containing exactly `path` and +`digest`; every digest is the string `sha256:` followed by the complete +lowercase value measured in Step 1. + +The seven strings are `admission and resource decisions`, `benchmark`, +`doctor runtime probe`, `dry-run runtime rendering`, `guard exec`, `project +run`, and `receipt publication`. The completed manifest contains only hashes +actually emitted in Step 1; no sentinel, ellipsis, or template token is valid. + +- [ ] **Step 5: Document regeneration and evidence limits** + +The README records every capture command from Task 2, every focused test target +from Task 2, the downstream check from Task 3, the exact base, and these limits: +no Docker/runtime/admission or project-run behavior is proved; fixture hashes +prove byte stability only. + +- [ ] **Step 6: Validate manifest paths and hashes** + +Run: + +```console +rtk cargo test --locked --offline --test compatibility_baseline manifest_paths_and_hashes_match +rtk git diff --check +``` + +Expected: PASS. + +- [ ] **Step 7: Update progress and commit M0** + +Record terminal commands and exact commit predecessor in `progress.md`, then: + +```console +rtk git add .superpowers/sdd/2026-08-30-capability-packs-clean-architecture tests/compatibility_baseline.rs +rtk git commit -m "docs: close compatibility baseline milestone" +``` + +M0 is complete only after an independent review confirms every manifest path, +hash, command, and non-executed boundary. diff --git a/docs/superpowers/plans/2026-08-30-m1-run-application-seam.md b/docs/superpowers/plans/2026-08-30-m1-run-application-seam.md new file mode 100644 index 0000000..60fb3f7 --- /dev/null +++ b/docs/superpowers/plans/2026-08-30-m1-run-application-seam.md @@ -0,0 +1,300 @@ +# M1 Run Application Seam Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the private telescoping run orchestration boundary with one coherent dependency object while retaining every existing public wrapper and byte-visible behavior. + +**Architecture:** `RunRequest` remains the immutable use-case input. A private `RunDependencies` object groups existing effect ports and travels through the run and receipt orchestration paths; no new trait or public API is introduced. `main.rs`, `matrix.rs`, `runtime.rs`, and `process.rs` remain unchanged composition roots or adapters. + +**Tech Stack:** Rust 2024, existing `RuntimePort`, `SupervisorPort`, `StorageProbe`, `RuntimeCapabilityProbe`, `CompletionBarrier`, `RunLifecycleObserver`, unit fakes, and the M0 compatibility corpus. + +**Spec:** `docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md` + +## Global Constraints + +- M0 must be terminal before this plan starts. +- Every shell command begins with `rtk`. +- Preserve all public functions at `src/run.rs:139-301` and `src/run.rs:378-402` with exact signatures and semantics. +- Do not modify `src/main.rs`, `src/matrix.rs`, `src/runtime.rs`, `src/process.rs`, schemas, fixtures, CLI text, receipt formats, or policy formats. +- `RunDependencies` remains private to `run.rs`; no new analyzer or pack concept enters this milestone. +- Preserve run ID inputs/order, timestamps, receipt sealing, atomic no-overwrite publication, capability evidence, artifact observation, cache promotion, cancellation, lifecycle, and barrier ordering. +- Strict RED/GREEN TDD; one production file owner; no Docker, network, CCP heavy command, publication, or external mutation. + +--- + +### Task 1: Introduce the private dependency object at the outer run seam + +**Files:** +- Modify: `src/run.rs:55-424` +- Test: `src/run.rs` test module + +**Interfaces:** +- Consumes: existing `RunRequest<'a>` and all existing run effect ports. +- Produces: private `RunDependencies<'a>` and `execute_local_run_with_dependencies`. +- Preserves: every existing public run wrapper. + +- [ ] **Step 1: Write the failing seam equivalence test** + +Reuse the smallest existing passing run fixture in the `run.rs` test module. +Add a test that constructs the same request, runtime, supervisor, cancellation, +fixed clock, storage probe, capability probe, noop barrier, and noop lifecycle +as that fixture, then calls this not-yet-existing function: + +```rust +let mut barrier = NoopCompletionBarrier; +let mut lifecycle = NoopRunLifecycleObserver; +let mut dependencies = RunDependencies { + runtime: &runtime, + supervisor: &supervisor, + cancellation: &cancellation, + clock: &clock, + barrier: &mut barrier, + lifecycle: &mut lifecycle, + storage_probe: &storage_probe, + capability_probe: &capability_probe, + runtime_preflight: None, +}; +let outcome = execute_local_run_with_dependencies(&request, &mut dependencies) + .expect("execute through dependency seam"); +assert_eq!(outcome.exit_code(), 0); +assert_eq!( + outcome.published_canonical_bytes().expect("canonical bytes"), + fs::read(&outcome.receipt_path).expect("published receipt") +); +``` + +- [ ] **Step 2: Run the focused test and verify RED** + +```console +rtk cargo test --locked --offline --lib run_dependencies_execute_the_existing_run_path +``` + +Expected: compile failure because `RunDependencies` and +`execute_local_run_with_dependencies` do not exist. + +- [ ] **Step 3: Add the minimal private dependency object** + +Add immediately after `RunRequest`: + +```rust +struct RunDependencies<'a> { + runtime: &'a dyn RuntimePort, + supervisor: &'a dyn SupervisorPort, + cancellation: &'a CancellationToken, + clock: &'a dyn Clock, + barrier: &'a mut dyn CompletionBarrier, + lifecycle: &'a mut dyn RunLifecycleObserver, + storage_probe: &'a dyn StorageProbe, + capability_probe: &'a dyn RuntimeCapabilityProbe, + runtime_preflight: Option, +} +``` + +Replace the private +`execute_local_run_with_barrier_and_lifecycle_and_storage_probe_and_capability_probe` +signature with: + +```rust +fn execute_local_run_with_dependencies( + request: &RunRequest<'_>, + dependencies: &mut RunDependencies<'_>, +) -> Result +``` + +Keep its receipt-v2 sealing, canonical byte selection, atomic write, and outcome +construction byte-for-byte. Pass `dependencies` to the receipt/artifact core; +do not reorder any operation. + +- [ ] **Step 4: Verify focused GREEN** + +```console +rtk cargo test --locked --offline --lib run_dependencies_execute_the_existing_run_path +``` + +Expected: PASS. + +- [ ] **Step 5: Commit Task 1** + +```console +rtk git add src/run.rs +rtk git commit -m "refactor: add private run dependency seam" +``` + +### Task 2: Route every compatibility wrapper through the seam + +**Files:** +- Modify: `src/run.rs:139-424` +- Test: `src/run.rs` test module + +**Interfaces:** +- Consumes: `RunDependencies` and `execute_local_run_with_dependencies` from Task 1. +- Produces: thin adapters for every existing public wrapper. +- Preserves: wrapper names, visibility, argument order, return types, defaults, and fault injection. + +- [ ] **Step 1: Write the wrapper equivalence test before migration** + +Using two independently created copies of the same fixture and fixed clock, +characterize the default `execute_local_run` wrapper only: execute one copy +through that public wrapper and one through a direct `RunDependencies` call. +Compare: + +```rust +assert_eq!(public.exit_code(), direct.exit_code()); +assert_eq!(public.receipt.receipt, direct.receipt.receipt); +assert_eq!(public.receipt_v2, direct.receipt_v2); +assert_eq!( + public.published_canonical_bytes().expect("public bytes"), + direct.published_canonical_bytes().expect("direct bytes") +); +``` + +Give each fixture an isolated repository/cache root so atomic receipt output and +cache generations cannot collide. + +- [ ] **Step 2: Establish the pre-migration characterization baseline** + +Run the equivalence test before migrating the remaining wrappers: + +```console +rtk cargo test --locked --offline --lib public_run_wrapper_matches_dependency_seam_bytes +``` + +Expected: PASS because Task 1's seam still delegates to the unchanged receipt +core. This passing characterization test is the safety net for the mechanical +adapter migration; Task 1 already supplied the compile-RED production seam. + +- [ ] **Step 3: Migrate public wrappers mechanically** + +Each existing wrapper creates only its current defaults plus a +`RunDependencies` value, then calls `execute_local_run_with_dependencies`. +For example, the storage-probe wrapper becomes structurally: + +```rust +let mut barrier = NoopCompletionBarrier; +let mut lifecycle = NoopRunLifecycleObserver; +let capability_probe = DockerRuntimeCapabilityProbe; +let mut dependencies = RunDependencies { + runtime, + supervisor, + cancellation, + clock, + barrier: &mut barrier, + lifecycle: &mut lifecycle, + storage_probe, + capability_probe: &capability_probe, + runtime_preflight: None, +}; +execute_local_run_with_dependencies(request, &mut dependencies) +``` + +The runtime-preflight wrapper sets `runtime_preflight: Some(runtime_preflight)`. +The injected capability-probe wrapper uses the caller's probe. Do not change +which wrappers choose `SystemStorageProbe`, `DockerRuntimeCapabilityProbe`, or +noop barrier/lifecycle values. The remaining wrappers are covered by the named +runtime-preflight, resource-pressure, storage-preflight, and full `run::tests` +targets in Step 5; do not generalize the default-wrapper byte comparison beyond +that one adapter. + +- [ ] **Step 4: Route receipt-only execution through the same dependencies** + +Rename the private receipt/artifact function to: + +```rust +fn execute_local_receipt_and_artifacts_with_dependencies( + request: &RunRequest<'_>, + dependencies: &mut RunDependencies<'_>, +) -> Result<( + ReceiptEnvelopeV1, + Vec, + Option, +), RunError> +``` + +Inside it, replace only parameter references with the corresponding dependency +field. Consume preflight exactly once with +`dependencies.runtime_preflight.take()`. The public receipt-only wrapper builds +the same default dependencies as before and returns `.map(|(receipt, _, _)| +receipt)`. It must still avoid receipt publication. + +- [ ] **Step 5: Verify wrapper and fault-path GREEN** + +```console +rtk cargo test --locked --offline --lib public_run_wrapper_matches_dependency_seam_bytes +rtk cargo test --locked --offline --lib runtime_preflight +rtk cargo test --locked --offline --lib resource_pressure +rtk cargo test --locked --offline --lib storage_preflight +rtk cargo test --locked --offline --lib run::tests +``` + +Expected: all selected tests pass; no Docker or network. + +- [ ] **Step 6: Commit Task 2** + +```console +rtk git add src/run.rs +rtk git commit -m "refactor: route run wrappers through dependencies" +``` + +### Task 3: Prove strict compatibility and close M1 + +**Files:** +- Modify: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/progress.md` + +**Interfaces:** +- Consumes: M0 compatibility manifest and all M1 commits. +- Produces: terminal local evidence that internal composition changed but public bytes and facade did not. + +- [ ] **Step 1: Run formatting and lint for the changed production module** + +```console +rtk cargo fmt --check +rtk cargo clippy --locked --offline --all-targets --all-features -- -D warnings +``` + +Expected: PASS. Clippy may compile but may not use network; missing cached +dependencies stop the task rather than authorizing network. + +- [ ] **Step 2: Run the M0 compatibility contract** + +```console +rtk cargo test --locked --offline --test compatibility_baseline +rtk cargo test --locked --offline --test plan_cli +rtk cargo test --locked --offline --test verify_cli +rtk cargo test --locked --offline --test receipt_contract +rtk cargo test --locked --offline --test matrix_contract +rtk cargo test --locked --offline --test verification_contract +``` + +Expected: all tests pass with no fixture update. Any golden change is a product +regression, not an automatic baseline refresh. A suspected false positive +stops M1 and requires an explicit amendment to the M0 plan and manifest before +any fixture can change. + +- [ ] **Step 3: Run the complete offline local suite** + +```console +rtk cargo test --locked --offline --workspace --all-targets --all-features +``` + +Expected: PASS. This is local deterministic evidence, not hosted/platform or +release qualification. + +- [ ] **Step 4: Independent reviews** + +Request one specification-compliance review and one code-quality review. Both +must inspect the exact diff from the M0 terminal commit to the current HEAD. +Critical or Important findings block closure; Minor findings are either fixed +with focused tests or recorded with owner-approved rationale. + +- [ ] **Step 5: Update progress and commit closure** + +Record exact HEAD, commands, counts, review verdicts, unchanged fixtures, and +remaining hosted-CI gate. Then: + +```console +rtk git add .superpowers/sdd/2026-08-30-capability-packs-clean-architecture/progress.md +rtk git commit -m "docs: close run application seam milestone" +``` + +M1 is locally complete only after the worktree is clean and every M0 manifest +hash still matches. Push and hosted CI remain external exact-head gates. diff --git a/docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md b/docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md index 2619513..ec315dc 100644 --- a/docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md +++ b/docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md @@ -258,10 +258,15 @@ Any future receipt extension must: - M0 records a compatibility corpus under `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/compatibility/`. Its manifest names and hashes the current CLI help surfaces, exit-code - cases, canonical v1/v2/matrix plan and dry-run fixtures, receipt and policy - fixtures, verification decisions, and a downstream compile fixture that - imports the supported public Rust facade. M1 must compare the candidate - against these exact files and produce no unexplained byte or compile delta. + cases, canonical v1/v2/matrix plan fixtures, privacy-normalized dry-run + fixtures, receipt and policy fixtures, verification decisions, and a + downstream compile fixture that imports the supported public Rust facade. + Dry-run normalization replaces only explicit workspace host-path fields and + the `src=` segment of CCP-generated `type=bind,src=...,dst=...` argv with + typed tokens. It preserves JSON structure, every non-mount argv byte, + container destinations, access, purpose, and logical IDs. M1 must compare the + candidate against these files and produce no unexplained byte, + normalized-structure, or compile delta. - No analyzer-specific dependency enters the evidence domain. - All new parsing is bounded, deny-unknown where appropriate, deterministic, and fail closed. @@ -322,8 +327,9 @@ Any future receipt extension must: **Exit evidence** - RED/GREEN unit and integration tests. -- Byte-for-byte comparison against the M0 CLI, exit-code, plan, dry-run, - receipt, policy, verification, and JSON corpus. +- Byte-for-byte comparison against the M0 CLI, exit-code, plan, receipt, + policy, verification, and JSON corpus, plus exact comparison of the + privacy-normalized dry-run projections. - Compile success of the M0 downstream public-facade fixture without source changes. - Hosted CI terminal on the exact commit before integration. From d6e26a5d8fac678faa761563f4e5799c7524a78e Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 12:54:55 +0200 Subject: [PATCH 03/17] docs: separate programme checkpoint from sdd ledger --- .../2026-08-30-m0-compatibility-baseline.md | 20 +++++++++---------- .../2026-08-30-m1-run-application-seam.md | 4 ++-- ...ability-packs-clean-architecture-design.md | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/superpowers/plans/2026-08-30-m0-compatibility-baseline.md b/docs/superpowers/plans/2026-08-30-m0-compatibility-baseline.md index d0c0c70..9bc0a44 100644 --- a/docs/superpowers/plans/2026-08-30-m0-compatibility-baseline.md +++ b/docs/superpowers/plans/2026-08-30-m0-compatibility-baseline.md @@ -25,8 +25,8 @@ ### Task 1: Durable goal and progress ledger **Files:** -- Create: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/goal.txt` -- Create: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/progress.md` +- Create: `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/goal.txt` +- Create: `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md` **Interfaces:** - Consumes: the canonical specification and exact design commit. @@ -62,7 +62,7 @@ Write: # Capability Packs and Clean Architecture progress - Base: `5fed7c443504969e62980141048f9279f9fa1dfe` -- Branch: `codex/capability-packs-clean-architecture-v1` +- Branch: `codex/capability-packs-clean-architecture-delivery-v1` - Specification commit: `5ef9707930f7095a2f57bc3e38e53bfeac06aaf2` - Current milestone: M0 compatibility baseline - Completed evidence: design review READY; `git diff --check` PASS @@ -78,7 +78,7 @@ Write: Run: ```console -rtk rg -n "(/Users/|TODO|TBD|FIXME|container ID|secret)" .superpowers/sdd/2026-08-30-capability-packs-clean-architecture +rtk rg -n "(/Users/|TODO|TBD|FIXME|container ID|secret)" docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture ``` Expected: no match. The relative specification path is permitted; no local @@ -87,7 +87,7 @@ absolute path is stored. - [ ] **Step 4: Commit Task 1** ```console -rtk git add .superpowers/sdd/2026-08-30-capability-packs-clean-architecture/goal.txt .superpowers/sdd/2026-08-30-capability-packs-clean-architecture/progress.md +rtk git add docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/goal.txt docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md rtk git commit -m "docs: add capability programme checkpoint" ``` @@ -605,9 +605,9 @@ rtk git commit -m "test: compile supported public facade downstream" ### Task 4: Hash manifest and M0 closure **Files:** -- Create: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json` -- Create: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/compatibility/README.md` -- Modify: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/progress.md` +- Create: `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json` +- Create: `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/README.md` +- Modify: `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md` **Interfaces:** - Consumes: all M0 fixtures and tests. @@ -623,7 +623,7 @@ Path}`, and `sha2::{Digest, Sha256}`. Add: fn manifest_paths_and_hashes_match() { let root = Path::new(env!("CARGO_MANIFEST_DIR")); let manifest_path = root.join( - ".superpowers/sdd/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json", + "docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json", ); let manifest: serde_json::Value = serde_json::from_slice( &fs::read(&manifest_path).expect("read compatibility manifest"), @@ -763,7 +763,7 @@ Expected: PASS. Record terminal commands and exact commit predecessor in `progress.md`, then: ```console -rtk git add .superpowers/sdd/2026-08-30-capability-packs-clean-architecture tests/compatibility_baseline.rs +rtk git add docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture tests/compatibility_baseline.rs rtk git commit -m "docs: close compatibility baseline milestone" ``` diff --git a/docs/superpowers/plans/2026-08-30-m1-run-application-seam.md b/docs/superpowers/plans/2026-08-30-m1-run-application-seam.md index 60fb3f7..fa1030d 100644 --- a/docs/superpowers/plans/2026-08-30-m1-run-application-seam.md +++ b/docs/superpowers/plans/2026-08-30-m1-run-application-seam.md @@ -238,7 +238,7 @@ rtk git commit -m "refactor: route run wrappers through dependencies" ### Task 3: Prove strict compatibility and close M1 **Files:** -- Modify: `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/progress.md` +- Modify: `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md` **Interfaces:** - Consumes: M0 compatibility manifest and all M1 commits. @@ -292,7 +292,7 @@ Record exact HEAD, commands, counts, review verdicts, unchanged fixtures, and remaining hosted-CI gate. Then: ```console -rtk git add .superpowers/sdd/2026-08-30-capability-packs-clean-architecture/progress.md +rtk git add docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md rtk git commit -m "docs: close run application seam milestone" ``` diff --git a/docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md b/docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md index ec315dc..91a4a63 100644 --- a/docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md +++ b/docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md @@ -37,9 +37,9 @@ GitHub Actions emulator, cloud runner service, or tool-specific monolith. | Item | Verified state | Evidence | |---|---|---| | Repository | `MarcoPorcellato/commit-ci-preflight` | local `origin` remote | -| Delivery worktree | `/Users/marco1/Documents/CODICE con VS CODE/ccp-worktrees/capability-packs-clean-architecture-v1` | `git worktree list --porcelain` | +| Delivery worktree | isolated worktree; live absolute path remains private | `git worktree list --porcelain` | | Base revision | `5fed7c443504969e62980141048f9279f9fa1dfe` | fetched `origin/main`, 2026-08-30 | -| Delivery branch | `codex/capability-packs-clean-architecture-v1` | live branch | +| Delivery branch | `codex/capability-packs-clean-architecture-delivery-v1` | live branch | | Compatibility envelope | strict preservation | owner approval, 2026-08-30 | | Product direction | Capability Packs plus later generic evidence adapters | owner approval, 2026-08-30 | | Current milestone | M0 specification and implementation plan | this document | @@ -256,7 +256,7 @@ Any future receipt extension must: receipt bytes and IDs, policy schemas, JSON shapes, and public Rust facade remain compatible. - M0 records a compatibility corpus under - `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/compatibility/`. + `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/`. Its manifest names and hashes the current CLI help surfaces, exit-code cases, canonical v1/v2/matrix plan fixtures, privacy-normalized dry-run fixtures, receipt and policy fixtures, verification decisions, and a @@ -515,7 +515,7 @@ Any future receipt extension must: ## Interruption and recovery -Maintain `.superpowers/sdd/2026-08-30-capability-packs-clean-architecture/` +Maintain `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/` with a concise persistent goal, current progress, exact branch/HEAD/base, completed validations, active worker state, unproven gates, and next action. From 5acd01327d1c33c51e54540694ab91737e5ef723 Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 12:57:11 +0200 Subject: [PATCH 04/17] docs: add capability programme checkpoint --- .../goal.txt | 15 +++++++++++++++ .../progress.md | 12 ++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/goal.txt create mode 100644 docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/goal.txt b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/goal.txt new file mode 100644 index 0000000..50bf350 --- /dev/null +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/goal.txt @@ -0,0 +1,15 @@ +Complete the Clean Architecture and Advanced Linux Capability Packs programme +according to `docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md`. + +First verify the live worktree, exact branch/HEAD/base, active operator policy, +and the progress ledger. Preserve strict CLI, exit-code, plan, receipt, policy, +JSON, and public Rust API compatibility. Use deterministic tools first and +delegate bounded inventory, mechanical edits, tests, and review to Luna or +Spark; retain architecture, security, integration, qualification, release, +and merge judgment centrally. + +Proceed through dependency-ordered milestones until the canonical checklist is +proved. Do not run CCP heavy work for this public repository without an exact +approved non-economic exception. Treat push, PR, ready transition, merge, tag, +release, stable installation, and external publication as separate exact-state +gates. diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md new file mode 100644 index 0000000..f04f6ae --- /dev/null +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md @@ -0,0 +1,12 @@ +# Capability Packs and Clean Architecture progress + +- Base: `5fed7c443504969e62980141048f9279f9fa1dfe` +- Branch: `codex/capability-packs-clean-architecture-delivery-v1` +- Specification commit: `5ef9707930f7095a2f57bc3e38e53bfeac06aaf2` +- Current milestone: M0 compatibility baseline +- Completed evidence: design review READY; `git diff --check` PASS +- Unproven: baseline tests, application seam, pack contract, reference packs, + hosted CI, publication, release +- Heavy processes: none +- External mutations: none +- Next action: execute Task 2 of the M0 plan with RED/GREEN TDD From b4fcb6ef1fe67d9ae8a31f3a29d6d25eaf456704 Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 13:16:39 +0200 Subject: [PATCH 05/17] test: pin public compatibility baseline --- tests/compatibility_baseline.rs | 361 ++++++++++++++++++ .../compatibility/dry-run-v1.normalized.json | 1 + .../dry-run-v2-current.normalized.json | 1 + .../dry-run-v2-legacy.normalized.json | 1 + .../compatibility/plan-help.stdout.txt | 13 + .../compatibility/plan-v1.stdout.json | 1 + .../compatibility/plan-v2-legacy.stdout.json | 1 + .../compatibility/root-help.stdout.txt | 23 ++ .../compatibility/verify-help.stdout.txt | 17 + .../compatibility/verify-v1-fail.stdout.json | 1 + .../compatibility/verify-v1-pass.stdout.json | 1 + 11 files changed, 421 insertions(+) create mode 100644 tests/compatibility_baseline.rs create mode 100644 tests/fixtures/compatibility/dry-run-v1.normalized.json create mode 100644 tests/fixtures/compatibility/dry-run-v2-current.normalized.json create mode 100644 tests/fixtures/compatibility/dry-run-v2-legacy.normalized.json create mode 100644 tests/fixtures/compatibility/plan-help.stdout.txt create mode 100644 tests/fixtures/compatibility/plan-v1.stdout.json create mode 100644 tests/fixtures/compatibility/plan-v2-legacy.stdout.json create mode 100644 tests/fixtures/compatibility/root-help.stdout.txt create mode 100644 tests/fixtures/compatibility/verify-help.stdout.txt create mode 100644 tests/fixtures/compatibility/verify-v1-fail.stdout.json create mode 100644 tests/fixtures/compatibility/verify-v1-pass.stdout.json diff --git a/tests/compatibility_baseline.rs b/tests/compatibility_baseline.rs new file mode 100644 index 0000000..df29ff9 --- /dev/null +++ b/tests/compatibility_baseline.rs @@ -0,0 +1,361 @@ +use std::process::{Command, Output}; + +const COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; +const EVALUATED_AT: &str = "2026-08-08T12:30:00Z"; + +fn ccp(args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_commit-ci-preflight")) + .args(args) + .output() + .expect("execute compatibility command") +} + +#[test] +fn root_help_bytes_match_the_baseline() { + let output = ccp(&["--help"]); + assert_eq!(output.status.code(), Some(0)); + assert!(output.stderr.is_empty()); + assert_eq!( + output.stdout, + include_bytes!("fixtures/compatibility/root-help.stdout.txt") + ); +} + +#[test] +fn command_help_bytes_match_the_baseline() { + for (command, expected) in [ + ( + "plan", + include_bytes!("fixtures/compatibility/plan-help.stdout.txt").as_slice(), + ), + ( + "verify", + include_bytes!("fixtures/compatibility/verify-help.stdout.txt").as_slice(), + ), + ] { + let o = ccp(&[command, "--help"]); + assert_eq!(o.status.code(), Some(0)); + assert!(o.stderr.is_empty()); + assert_eq!(o.stdout, expected, "{command} help drifted"); + } +} + +#[test] +fn plan_and_verification_bytes_and_exit_codes_match_the_baseline() { + let p = ccp(&[ + "plan", + "--config", + "tests/fixtures/config-v1-read-only.toml", + "--json", + ]); + assert_eq!(p.status.code(), Some(0)); + assert!(p.stderr.is_empty()); + assert_eq!( + p.stdout, + include_bytes!("fixtures/compatibility/plan-v1.stdout.json") + ); + let a = [ + "verify", + "--receipt", + "tests/fixtures/receipt-v1-pass.json", + "--policy", + "tests/fixtures/policy-v1.toml", + "--expected-commit", + COMMIT, + "--evaluated-at-utc", + EVALUATED_AT, + "--json", + ]; + let v = ccp(&a); + assert_eq!(v.status.code(), Some(0)); + assert!(v.stderr.is_empty()); + assert_eq!( + v.stdout, + include_bytes!("fixtures/compatibility/verify-v1-pass.stdout.json") + ); + let f = ccp(&[ + "verify", + "--receipt", + "tests/fixtures/receipt-v1-pass.json", + "--policy", + "tests/fixtures/policy-v1.toml", + "--expected-commit", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "--evaluated-at-utc", + EVALUATED_AT, + "--json", + ]); + assert_eq!(f.status.code(), Some(3)); + assert_eq!( + f.stdout, + include_bytes!("fixtures/compatibility/verify-v1-fail.stdout.json") + ); +} + +#[test] +fn matrix_plan_profiles_match_the_baseline() { + let c = ccp(&[ + "plan", + "--config", + "tests/fixtures/config-v2-matrix.toml", + "--json", + ]); + assert_eq!(c.status.code(), Some(0)); + assert!(c.stderr.is_empty()); + assert_eq!( + c.stdout, + include_bytes!("fixtures/plan-v2-current-default.stdout.json") + ); + let l = ccp(&[ + "plan", + "--config", + "tests/fixtures/config-v2-legacy-compatible.toml", + "--matrix-plan-profile", + "matrix-v2-legacy-v1", + "--json", + ]); + assert_eq!(l.status.code(), Some(0)); + assert!(l.stderr.is_empty()); + assert_eq!( + l.stdout, + include_bytes!("fixtures/compatibility/plan-v2-legacy.stdout.json") + ); +} + +fn normalize_dry_run(mut v: serde_json::Value) -> (serde_json::Value, Vec) { + let mut paths = Vec::new(); + fn collect_one(value: &serde_json::Value, paths: &mut Vec) { + let workspace = &value["workspace"]; + paths.push(workspace["repository"].as_str().unwrap().to_owned()); + paths.push(workspace["run_root"].as_str().unwrap().to_owned()); + for mount in workspace["mounts"].as_array().unwrap() { + paths.push(mount["source"].as_str().unwrap().to_owned()); + } + } + if let Some(runtimes) = v.get("runtimes").and_then(|x| x.as_array()) { + for runtime in runtimes { + collect_one(&runtime["dry_run"], &mut paths); + } + } else { + collect_one(&v, &mut paths); + } + fn one(v: &mut serde_json::Value) { + v["workspace"]["repository"] = serde_json::json!("$REPOSITORY"); + v["workspace"]["run_root"] = serde_json::json!("$RUN_ROOT"); + let mut m = std::collections::BTreeMap::new(); + m.insert("$REPOSITORY".to_string(), "$REPOSITORY".to_string()); + for x in v["workspace"]["mounts"].as_array_mut().unwrap() { + let s = x["source"].as_str().unwrap().to_string(); + let t = match ( + x["purpose"].as_str().unwrap(), + x.get("logical_id").and_then(|x| x.as_str()), + ) { + ("repository", _) => "$REPOSITORY".into(), + ("cache", Some(i)) => format!("$CACHE:{i}"), + ("artifact", Some(i)) => format!("$ARTIFACT:{i}"), + _ => panic!(), + }; + m.insert(s, t.clone()); + x["source"] = serde_json::json!(t); + } + for c in v["checks"].as_array_mut().unwrap() { + for a in c["argv"].as_array_mut().unwrap() { + let s = a.as_str().unwrap(); + if let Some(r) = s.strip_prefix("type=bind,src=") { + let (_, d) = r.split_once(",dst=").unwrap(); + let src = m + .iter() + .find(|(k, _)| r.starts_with(*k)) + .map(|(_, v)| v) + .unwrap(); + *a = serde_json::json!(format!("type=bind,src={src},dst={d}")); + } + } + } + } + if let Some(rs) = v.get_mut("runtimes").and_then(|x| x.as_array_mut()) { + for r in rs { + one(&mut r["dry_run"]); + } + } else { + one(&mut v) + } + (v, paths) +} + +fn object_keys(value: &serde_json::Value) -> std::collections::BTreeSet<&str> { + value + .as_object() + .expect("object") + .keys() + .map(String::as_str) + .collect() +} + +fn assert_one_dry_run_contract(original: &serde_json::Value, normalized: &serde_json::Value) { + assert_eq!(object_keys(original), object_keys(normalized)); + for field in [ + "schema_version", + "plan_digest", + "runtime", + "program", + "workspace_mount_policy", + "executed", + ] { + assert_eq!(original[field], normalized[field], "{field}"); + } + assert_eq!(original["executed"], false); + let before = original["checks"].as_array().expect("checks"); + let after = normalized["checks"].as_array().expect("checks"); + assert_eq!(before.len(), after.len()); + for (before, after) in before.iter().zip(after) { + assert_eq!(object_keys(before), object_keys(after)); + for field in ["id", "program", "depends_on"] { + assert_eq!(before[field], after[field], "{field}"); + } + let ba = before["argv"].as_array().unwrap(); + let aa = after["argv"].as_array().unwrap(); + assert_eq!(ba.len(), aa.len()); + for (raw, scrubbed) in ba.iter().zip(aa) { + let raw = raw.as_str().unwrap(); + let scrubbed = scrubbed.as_str().unwrap(); + if let Some(raw_mount) = raw.strip_prefix("type=bind,src=") { + let (_, suffix) = raw_mount.split_once(",dst=").unwrap(); + let normalized_mount = scrubbed.strip_prefix("type=bind,src=$").unwrap(); + let (_, normalized_suffix) = normalized_mount.split_once(",dst=").unwrap(); + assert_eq!(suffix, normalized_suffix); + } else { + assert_eq!(raw, scrubbed); + } + } + } + let bw = &original["workspace"]; + let aw = &normalized["workspace"]; + assert_eq!(object_keys(bw), object_keys(aw)); + assert_eq!(bw["schema_version"], aw["schema_version"]); + assert_eq!(bw["source_snapshot_digest"], aw["source_snapshot_digest"]); + let bm = bw["mounts"].as_array().unwrap(); + let am = aw["mounts"].as_array().unwrap(); + assert_eq!(bm.len(), am.len()); + for (before, after) in bm.iter().zip(am) { + assert_eq!(object_keys(before), object_keys(after)); + for field in ["target", "access", "purpose", "logical_id"] { + assert_eq!(before.get(field), after.get(field), "{field}"); + } + } +} + +fn assert_dry_run_normalization_preserves_contract( + original: &serde_json::Value, + normalized: &serde_json::Value, +) { + assert_eq!(object_keys(original), object_keys(normalized)); + match (original.get("runtimes"), normalized.get("runtimes")) { + (Some(before), Some(after)) => { + let before = before.as_array().unwrap(); + let after = after.as_array().unwrap(); + assert_eq!(before.len(), after.len()); + for (before, after) in before.iter().zip(after) { + assert_eq!(object_keys(before), object_keys(after)); + assert_eq!(before["runtime_id"], after["runtime_id"]); + assert_eq!( + before["configuration_digest"], + after["configuration_digest"] + ); + assert_one_dry_run_contract(&before["dry_run"], &after["dry_run"]); + } + } + (None, None) => assert_one_dry_run_contract(original, normalized), + _ => panic!("matrix shape changed"), + } +} + +#[test] +fn dry_run_profiles_match_the_baseline_without_execution() { + for (a, e) in [ + ( + vec![ + "dry-run", + "--config", + "tests/fixtures/config-v1-read-only.toml", + "--json", + ], + include_bytes!("fixtures/compatibility/dry-run-v1.normalized.json").as_slice(), + ), + ( + vec![ + "dry-run", + "--config", + "tests/fixtures/config-v2-matrix.toml", + "--json", + ], + include_bytes!("fixtures/compatibility/dry-run-v2-current.normalized.json").as_slice(), + ), + ( + vec![ + "dry-run", + "--config", + "tests/fixtures/config-v2-legacy-compatible.toml", + "--matrix-plan-profile", + "matrix-v2-legacy-v1", + "--json", + ], + include_bytes!("fixtures/compatibility/dry-run-v2-legacy.normalized.json").as_slice(), + ), + ] as [(Vec<&str>, &[u8]); 3] + { + let o = ccp(&a); + assert_eq!(o.status.code(), Some(0)); + assert!(o.stderr.is_empty()); + let v: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap(); + let original = v.clone(); + let (n, host_paths) = normalize_dry_run(v); + assert_dry_run_normalization_preserves_contract(&original, &n); + let serialized = serde_json::to_vec(&n).unwrap(); + for path in host_paths { + assert!(!serialized.windows(path.len()).any(|w| w == path.as_bytes())); + } + assert_eq!(serialized, e.strip_suffix(b"\n").unwrap_or(e)); + } +} + +#[test] +fn usage_error_exit_code_remains_two() { + let o = ccp(&["plan", "--config", "tests/fixtures/does-not-exist.toml"]); + assert_eq!(o.status.code(), Some(2)); + assert!(o.stdout.is_empty()); +} + +#[test] +#[ignore] +fn print_normalized_dry_run_baselines() { + for a in [ + vec![ + "dry-run", + "--config", + "tests/fixtures/config-v1-read-only.toml", + "--json", + ], + vec![ + "dry-run", + "--config", + "tests/fixtures/config-v2-matrix.toml", + "--json", + ], + vec![ + "dry-run", + "--config", + "tests/fixtures/config-v2-legacy-compatible.toml", + "--matrix-plan-profile", + "matrix-v2-legacy-v1", + "--json", + ], + ] { + let o = ccp(&a); + let v: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap(); + println!( + "{}", + serde_json::to_string(&normalize_dry_run(v).0).unwrap() + ); + } +} diff --git a/tests/fixtures/compatibility/dry-run-v1.normalized.json b/tests/fixtures/compatibility/dry-run-v1.normalized.json new file mode 100644 index 0000000..63921c7 --- /dev/null +++ b/tests/fixtures/compatibility/dry-run-v1.normalized.json @@ -0,0 +1 @@ +{"checks":[{"argv":["run","--rm","--init","--read-only","--network","none","--cpus","1","--memory","128m","--pids-limit","16","--tmpfs","/tmp:rw,noexec,nosuid,nodev,size=64m","--env","TMPDIR=/tmp","--mount","type=bind,src=$REPOSITORY,dst=/workspace,readonly","--workdir","/workspace","example.invalid/ci@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","this-command-does-not-exist-and-must-not-run"],"depends_on":[],"id":"must-not-execute","program":"docker"}],"executed":false,"plan_digest":"sha256:14d55568b35ca83abf2722b00ee16d8029de769cdf4acec6111789f067532fb1","program":"docker","runtime":"docker_compatible","schema_version":"1.0","workspace":{"mounts":[{"access":"read_only","purpose":"repository","source":"$REPOSITORY","target":"/workspace"}],"repository":"$REPOSITORY","run_root":"$RUN_ROOT","schema_version":"1.0"},"workspace_mount_policy":"explicit_bindings"} diff --git a/tests/fixtures/compatibility/dry-run-v2-current.normalized.json b/tests/fixtures/compatibility/dry-run-v2-current.normalized.json new file mode 100644 index 0000000..66c1143 --- /dev/null +++ b/tests/fixtures/compatibility/dry-run-v2-current.normalized.json @@ -0,0 +1 @@ +{"plan_digest":"sha256:a190ff8f9f55d0f09ab312d9d5386c284e6abcdb8bde0764840641b638e6c385","runtimes":[{"configuration_digest":"sha256:29e542a68a29235ff98fb7afab5f093c8212b1748ab8296987336ca1ffa40ea3","dry_run":{"checks":[{"argv":["run","--rm","--init","--read-only","--network","none","--cpus","1","--memory","256m","--pids-limit","64","--tmpfs","/tmp:rw,noexec,nosuid,nodev,size=64m","--env","TMPDIR=/tmp","--mount","type=bind,src=$REPOSITORY,dst=/workspace,readonly","--workdir","/workspace","example.invalid/python311@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","python","-V"],"depends_on":[],"id":"compat-py311","program":"docker"}],"executed":false,"plan_digest":"sha256:29e542a68a29235ff98fb7afab5f093c8212b1748ab8296987336ca1ffa40ea3","program":"docker","runtime":"docker_compatible","schema_version":"1.0","workspace":{"mounts":[{"access":"read_only","purpose":"repository","source":"$REPOSITORY","target":"/workspace"}],"repository":"$REPOSITORY","run_root":"$RUN_ROOT","schema_version":"1.0"},"workspace_mount_policy":"explicit_bindings"},"runtime_id":"python311"},{"configuration_digest":"sha256:505d34cc1722e82b3633df6e3c92534a3b0836d4083cd32354eb1acde28511ec","dry_run":{"checks":[{"argv":["run","--rm","--init","--read-only","--network","none","--cpus","1","--memory","256m","--pids-limit","64","--tmpfs","/tmp:rw,noexec,nosuid,nodev,size=64m","--env","TMPDIR=/tmp","--mount","type=bind,src=$REPOSITORY,dst=/workspace,readonly","--workdir","/workspace","example.invalid/python312@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","python","-V"],"depends_on":[],"id":"repository-check","program":"docker"}],"executed":false,"plan_digest":"sha256:505d34cc1722e82b3633df6e3c92534a3b0836d4083cd32354eb1acde28511ec","program":"docker","runtime":"docker_compatible","schema_version":"1.0","workspace":{"mounts":[{"access":"read_only","purpose":"repository","source":"$REPOSITORY","target":"/workspace"}],"repository":"$REPOSITORY","run_root":"$RUN_ROOT","schema_version":"1.0"},"workspace_mount_policy":"explicit_bindings"},"runtime_id":"python312"}],"schema_version":"2.0"} diff --git a/tests/fixtures/compatibility/dry-run-v2-legacy.normalized.json b/tests/fixtures/compatibility/dry-run-v2-legacy.normalized.json new file mode 100644 index 0000000..18592fb --- /dev/null +++ b/tests/fixtures/compatibility/dry-run-v2-legacy.normalized.json @@ -0,0 +1 @@ +{"plan_digest":"sha256:3248c763ccc37fecac1e29727007232d274f561e0943fc2e5a1996a38526fe13","runtimes":[{"configuration_digest":"sha256:755f77f6815b1ed7b4415b3312c48a6528e2d752270775916efa6c10f1ffe192","dry_run":{"checks":[{"argv":["run","--rm","--init","--read-only","--network","none","--cpus","1","--memory","256m","--pids-limit","64","--tmpfs","/tmp:rw,noexec,nosuid,nodev,size=64m","--env","SOURCE_DATE_EPOCH","--env","TMPDIR=/tmp","--mount","type=bind,src=$REPOSITORY,dst=/workspace,readonly","--mount","type=bind,src=$CACHE:cargo,dst=/workspace/.cache/cargo","--workdir","/workspace","example.invalid/python311@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","python","-V"],"depends_on":[],"id":"python311-version","program":"docker"}],"executed":false,"plan_digest":"sha256:755f77f6815b1ed7b4415b3312c48a6528e2d752270775916efa6c10f1ffe192","program":"docker","runtime":"docker_compatible","schema_version":"1.0","workspace":{"mounts":[{"access":"read_only","purpose":"repository","source":"$REPOSITORY","target":"/workspace"},{"access":"read_write","logical_id":"cargo","purpose":"cache","source":"$CACHE:cargo","target":"/workspace/.cache/cargo"}],"repository":"$REPOSITORY","run_root":"$RUN_ROOT","schema_version":"1.0"},"workspace_mount_policy":"explicit_bindings"},"runtime_id":"python311"},{"configuration_digest":"sha256:be2eb7d200946e9f1dc84cebd8c0cca8739e424163f91c86063bbe4caed936f9","dry_run":{"checks":[{"argv":["run","--rm","--init","--read-only","--network","none","--cpus","1","--memory","256m","--pids-limit","64","--tmpfs","/tmp:rw,noexec,nosuid,nodev,size=64m","--env","SOURCE_DATE_EPOCH","--env","TMPDIR=/tmp","--mount","type=bind,src=$REPOSITORY,dst=/workspace,readonly","--mount","type=bind,src=$CACHE:cargo,dst=/workspace/.cache/cargo","--workdir","/workspace","example.invalid/python312@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","python","-V"],"depends_on":[],"id":"python312-version","program":"docker"}],"executed":false,"plan_digest":"sha256:be2eb7d200946e9f1dc84cebd8c0cca8739e424163f91c86063bbe4caed936f9","program":"docker","runtime":"docker_compatible","schema_version":"1.0","workspace":{"mounts":[{"access":"read_only","purpose":"repository","source":"$REPOSITORY","target":"/workspace"},{"access":"read_write","logical_id":"cargo","purpose":"cache","source":"$CACHE:cargo","target":"/workspace/.cache/cargo"}],"repository":"$REPOSITORY","run_root":"$RUN_ROOT","schema_version":"1.0"},"workspace_mount_policy":"explicit_bindings"},"runtime_id":"python312"}],"schema_version":"2.0"} diff --git a/tests/fixtures/compatibility/plan-help.stdout.txt b/tests/fixtures/compatibility/plan-help.stdout.txt new file mode 100644 index 0000000..f74df51 --- /dev/null +++ b/tests/fixtures/compatibility/plan-help.stdout.txt @@ -0,0 +1,13 @@ +Validate configuration and print the normalized read-only execution plan + +Usage: commit-ci-preflight plan [OPTIONS] + +Options: + --config + Configuration file to validate [default: .commit-ci-preflight.toml] + --json + Emit canonical machine-readable JSON + --matrix-plan-profile + Compatibility digest profile for Matrix V2 configuration [default: current-v2] + -h, --help + Print help diff --git a/tests/fixtures/compatibility/plan-v1.stdout.json b/tests/fixtures/compatibility/plan-v1.stdout.json new file mode 100644 index 0000000..f9e5562 --- /dev/null +++ b/tests/fixtures/compatibility/plan-v1.stdout.json @@ -0,0 +1 @@ +{"plan":{"caches":[],"checks":[{"argv":["this-command-does-not-exist-and-must-not-run"],"artifacts":[],"depends_on":[],"id":"must-not-execute","required":true,"timeout_seconds":1,"working_directory":"."}],"environment":{"fixed":[],"inherit":[],"remote_secret_only":[],"runtime_internal":[]},"project":"example/read-only-proof","receipt":{"freshness_seconds":86400,"output":".ccp/receipt.json"},"runtime":{"cpu_count":1,"image":"example.invalid/ci@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","kind":"docker_compatible","memory_mib":128,"network":false,"pids_limit":16},"schema_version":"1.0"},"plan_digest":"sha256:14d55568b35ca83abf2722b00ee16d8029de769cdf4acec6111789f067532fb1"} diff --git a/tests/fixtures/compatibility/plan-v2-legacy.stdout.json b/tests/fixtures/compatibility/plan-v2-legacy.stdout.json new file mode 100644 index 0000000..febef68 --- /dev/null +++ b/tests/fixtures/compatibility/plan-v2-legacy.stdout.json @@ -0,0 +1 @@ +{"matrix_plan_profile":"matrix-v2-legacy-v1","plan_digest":"sha256:3248c763ccc37fecac1e29727007232d274f561e0943fc2e5a1996a38526fe13","plan":{"schema_version":"2.0","project":"example/legacy-matrix","receipt":{"output":".ccp/receipt.json","freshness_seconds":300},"environment":{"inherit":["SOURCE_DATE_EPOCH"],"fixed":[],"runtime_internal":[],"remote_secret_only":[]},"caches":[{"id":"cargo","mount_path":".cache/cargo"}],"runtimes":[{"id":"python311","configuration_digest":"sha256:755f77f6815b1ed7b4415b3312c48a6528e2d752270775916efa6c10f1ffe192","runtime":{"kind":"docker_compatible","image":"example.invalid/python311@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","cpu_count":1,"memory_mib":256,"pids_limit":64,"network":false},"checks":[{"id":"python311-version","required":true,"argv":["python","-V"],"working_directory":".","timeout_seconds":30,"depends_on":[],"artifacts":[]}]},{"id":"python312","configuration_digest":"sha256:be2eb7d200946e9f1dc84cebd8c0cca8739e424163f91c86063bbe4caed936f9","runtime":{"kind":"docker_compatible","image":"example.invalid/python312@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","cpu_count":1,"memory_mib":256,"pids_limit":64,"network":false},"checks":[{"id":"python312-version","required":true,"argv":["python","-V"],"working_directory":".","timeout_seconds":30,"depends_on":[],"artifacts":[]}]}]},"legacy_digest_basis":{"caches":[{"id":"cargo","mount_path":".cache/cargo"}],"environment_allow":["SOURCE_DATE_EPOCH"],"project":"example/legacy-matrix","receipt":{"freshness_seconds":300,"output":".ccp/receipt.json"},"runtimes":[{"checks":[{"argv":["python","-V"],"artifacts":[],"depends_on":[],"id":"python311-version","required":true,"timeout_seconds":30,"working_directory":"."}],"configuration_digest":"sha256:755f77f6815b1ed7b4415b3312c48a6528e2d752270775916efa6c10f1ffe192","id":"python311","runtime":{"cpu_count":1,"image":"example.invalid/python311@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","kind":"docker_compatible","memory_mib":256,"network":false,"pids_limit":64}},{"checks":[{"argv":["python","-V"],"artifacts":[],"depends_on":[],"id":"python312-version","required":true,"timeout_seconds":30,"working_directory":"."}],"configuration_digest":"sha256:be2eb7d200946e9f1dc84cebd8c0cca8739e424163f91c86063bbe4caed936f9","id":"python312","runtime":{"cpu_count":1,"image":"example.invalid/python312@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","kind":"docker_compatible","memory_mib":256,"network":false,"pids_limit":64}}],"schema_version":"2.0"}} diff --git a/tests/fixtures/compatibility/root-help.stdout.txt b/tests/fixtures/compatibility/root-help.stdout.txt new file mode 100644 index 0000000..83d18c8 --- /dev/null +++ b/tests/fixtures/compatibility/root-help.stdout.txt @@ -0,0 +1,23 @@ +Run reproducible local CI preflights and emit verifiable evidence before remote CI. + +Usage: commit-ci-preflight [COMMAND] + +Commands: + admission Inspect the host-wide heavy-command admission queue + resource Inspect the host-memory resource guard without changing host state + guard Serialize and supervise one explicit external program + plan Validate configuration and print the normalized read-only execution plan + doctor Probe the configured runtime without running project checks + dry-run Render runtime argv without spawning a process + run Execute the validated checks locally and write a canonical receipt + verify Independently verify receipt integrity and repository policy + migrate-github-actions Analyze a GitHub Actions workflow as data and report migration compatibility + benchmark Run the fixed deterministic native benchmark and emit a verifiable receipt + verify-benchmark Independently verify a benchmark receipt and its expected native platform + cache Inspect or initialize the persistent managed cache + recover Inspect or quarantine unfinished CCP-owned run journal state + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + -V, --version Print version diff --git a/tests/fixtures/compatibility/verify-help.stdout.txt b/tests/fixtures/compatibility/verify-help.stdout.txt new file mode 100644 index 0000000..ed37f38 --- /dev/null +++ b/tests/fixtures/compatibility/verify-help.stdout.txt @@ -0,0 +1,17 @@ +Independently verify receipt integrity and repository policy + +Usage: commit-ci-preflight verify [OPTIONS] --expected-commit + +Options: + --receipt + Canonical receipt JSON to verify [default: .ccp/receipt.json] + --policy + Strict repository verification policy [default: .commit-ci-policy.toml] + --expected-commit + Exact lowercase Git SHA supplied by the calling trust boundary + --evaluated-at-utc + Explicit strict UTC evaluation instant; defaults to the local system clock + --json + Emit the canonical machine-readable verification report + -h, --help + Print help diff --git a/tests/fixtures/compatibility/verify-v1-fail.stdout.json b/tests/fixtures/compatibility/verify-v1-fail.stdout.json new file mode 100644 index 0000000..8331e16 --- /dev/null +++ b/tests/fixtures/compatibility/verify-v1-fail.stdout.json @@ -0,0 +1 @@ +{"assurance_scope":"integrity_and_repository_policy_only","decision":"FAIL","evaluated_at_utc":"2026-08-08T12:30:00Z","expected_commit":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","findings":[{"code":"policy.commit","field":"repository.commit_sha","message":"receipt commit does not match the externally supplied commit"}],"integrity_status":"PASS","policy_status":"FAIL","receipt_id":"sha256:76479a6c9aba612e97066c9e631ec50fee8bcc460b8dcca25d1875a451939baa","schema_version":"1.0"} diff --git a/tests/fixtures/compatibility/verify-v1-pass.stdout.json b/tests/fixtures/compatibility/verify-v1-pass.stdout.json new file mode 100644 index 0000000..c7e1449 --- /dev/null +++ b/tests/fixtures/compatibility/verify-v1-pass.stdout.json @@ -0,0 +1 @@ +{"assurance_scope":"integrity_and_repository_policy_only","decision":"PASS","evaluated_at_utc":"2026-08-08T12:30:00Z","expected_commit":"0123456789abcdef0123456789abcdef01234567","findings":[],"integrity_status":"PASS","policy_status":"PASS","receipt_id":"sha256:76479a6c9aba612e97066c9e631ec50fee8bcc460b8dcca25d1875a451939baa","schema_version":"1.0"} From 3080c5892e3ae9e0ebba13d0a88901ea36de0dcc Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 13:20:26 +0200 Subject: [PATCH 06/17] test: require exact dry-run mount normalization --- tests/compatibility_baseline.rs | 34 ++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/compatibility_baseline.rs b/tests/compatibility_baseline.rs index df29ff9..bea632a 100644 --- a/tests/compatibility_baseline.rs +++ b/tests/compatibility_baseline.rs @@ -122,6 +122,18 @@ fn matrix_plan_profiles_match_the_baseline() { ); } +fn normalize_mount_argv( + argument: &str, + sources: &std::collections::BTreeMap, +) -> String { + let Some(rest) = argument.strip_prefix("type=bind,src=") else { + return argument.to_owned(); + }; + let (source, suffix) = rest.split_once(",dst=").expect("bind destination"); + let token = sources.get(source).expect("declared mount source"); + format!("type=bind,src={token},dst={suffix}") +} + fn normalize_dry_run(mut v: serde_json::Value) -> (serde_json::Value, Vec) { let mut paths = Vec::new(); fn collect_one(value: &serde_json::Value, paths: &mut Vec) { @@ -162,13 +174,7 @@ fn normalize_dry_run(mut v: serde_json::Value) -> (serde_json::Value, Vec (serde_json::Value, Vec std::collections::BTreeSet<&str> { value .as_object() From 17e069a7eb3bcc6596c93bb6432984eba8472208 Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 13:27:32 +0200 Subject: [PATCH 07/17] test: compile supported public facade downstream --- tests/compatibility_baseline.rs | 16 + tests/fixtures/public-api-compat/Cargo.lock | 707 +++++++++++++++++++ tests/fixtures/public-api-compat/Cargo.toml | 10 + tests/fixtures/public-api-compat/src/main.rs | 46 ++ 4 files changed, 779 insertions(+) create mode 100644 tests/fixtures/public-api-compat/Cargo.lock create mode 100644 tests/fixtures/public-api-compat/Cargo.toml create mode 100644 tests/fixtures/public-api-compat/src/main.rs diff --git a/tests/compatibility_baseline.rs b/tests/compatibility_baseline.rs index bea632a..05b1617 100644 --- a/tests/compatibility_baseline.rs +++ b/tests/compatibility_baseline.rs @@ -10,6 +10,22 @@ fn ccp(args: &[&str]) -> Output { .expect("execute compatibility command") } +#[test] +fn supported_public_facade_compiles_downstream() { + let status = Command::new(env!("CARGO")) + .args([ + "check", + "--locked", + "--offline", + "--quiet", + "--manifest-path", + "tests/fixtures/public-api-compat/Cargo.toml", + ]) + .status() + .expect("check downstream facade fixture"); + assert!(status.success()); +} + #[test] fn root_help_bytes_match_the_baseline() { let output = ccp(&["--help"]); diff --git a/tests/fixtures/public-api-compat/Cargo.lock b/tests/fixtures/public-api-compat/Cargo.lock new file mode 100644 index 0000000..c1ad38a --- /dev/null +++ b/tests/fixtures/public-api-compat/Cargo.lock @@ -0,0 +1,707 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "ccp-public-api-compat" +version = "0.0.0" +dependencies = [ + "commit-ci-preflight", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "commit-ci-preflight" +version = "0.1.0" +dependencies = [ + "clap", + "ctrlc", + "fs2", + "nix", + "process-wrap", + "saphyr", + "saphyr-parser", + "schemars", + "serde", + "serde_json", + "sha2", + "toml", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nix" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "process-wrap" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" +dependencies = [ + "indexmap", + "nix", + "windows", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "saphyr" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3767dfe8889ebb55a21409df2b6f36e66abfbe1eb92d64ff76ae799d3f91016" +dependencies = [ + "arraydeque", + "hashlink", + "ordered-float", + "saphyr-parser", +] + +[[package]] +name = "saphyr-parser" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb771b59f6b1985d1406325ec28f97cfb14256abcec4fdfb37b36a1766d6af7" +dependencies = [ + "arraydeque", + "hashlink", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.3", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tests/fixtures/public-api-compat/Cargo.toml b/tests/fixtures/public-api-compat/Cargo.toml new file mode 100644 index 0000000..11f00ac --- /dev/null +++ b/tests/fixtures/public-api-compat/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ccp-public-api-compat" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +commit-ci-preflight = { path = "../../.." } + +[workspace] diff --git a/tests/fixtures/public-api-compat/src/main.rs b/tests/fixtures/public-api-compat/src/main.rs new file mode 100644 index 0000000..b0d7f87 --- /dev/null +++ b/tests/fixtures/public-api-compat/src/main.rs @@ -0,0 +1,46 @@ +#![allow(dead_code)] + +use commit_ci_preflight::{config, matrix, receipt, run, runtime, verify}; + +fn main() { + let _schema: fn() -> Result = config::config_schema_json; + let _canonical = canonical_u8; +} + +fn canonical_u8(value: &u8) -> Result, receipt::ReceiptError> { + receipt::canonical_json(value) +} + +fn verify_v1( + bytes: &[u8], + policy: &verify::VerificationPolicyV1, + commit: &str, + evaluated_at: &str, +) -> Result { + verify::verify_receipt_document(bytes, policy, commit, evaluated_at) +} + +fn verify_v2( + bytes: &[u8], + policy: &matrix::MatrixVerificationPolicyV2, + commit: &str, + evaluated_at: &str, +) -> Result { + matrix::verify_matrix_receipt_document(bytes, policy, commit, evaluated_at) +} + +fn execute<'a>( + request: &run::RunRequest<'a>, + runtime_port: &dyn runtime::RuntimePort, + supervisor: &dyn commit_ci_preflight::process::SupervisorPort, + cancellation: &commit_ci_preflight::process::CancellationToken, + clock: &dyn run::Clock, +) -> Result { + run::execute_local_run(request, runtime_port, supervisor, cancellation, clock) +} + +fn select_runtime( + kind: config::RuntimeKind, +) -> Result, runtime::RuntimeError> { + runtime::runtime_for(kind) +} From 9e689c7f04e3d8c0479c74d91669c170a4c66e52 Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 13:36:33 +0200 Subject: [PATCH 08/17] docs: close compatibility baseline milestone --- .../compatibility/README.md | 54 ++++++++++++ .../compatibility/manifest.json | 38 ++++++++ .../progress.md | 9 +- tests/compatibility_baseline.rs | 86 +++++++++++++++++++ 4 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/README.md create mode 100644 docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/README.md b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/README.md new file mode 100644 index 0000000..3dbf60c --- /dev/null +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/README.md @@ -0,0 +1,54 @@ +# M0 compatibility manifest + +This manifest is bound to base commit `5fed7c443504969e62980141048f9279f9fa1dfe`. +It records SHA-256 digests for the Task 2 compatibility captures, Task 3 +downstream fixture, final compatibility test, and all nine schemas. + +Task 2 capture commands: + +```text +rtk cargo run --locked --offline -- --help +rtk cargo run --locked --offline -- plan --help +rtk cargo run --locked --offline -- verify --help +rtk cargo run --locked --offline -- plan --config tests/fixtures/config-v1-read-only.toml --json +rtk cargo run --locked --offline -- plan --config tests/fixtures/config-v2-matrix.toml --json +rtk cargo run --locked --offline -- plan --config tests/fixtures/config-v2-legacy-compatible.toml --matrix-plan-profile matrix-v2-legacy-v1 --json +rtk cargo run --locked --offline -- verify --receipt tests/fixtures/receipt-v1-pass.json --policy tests/fixtures/policy-v1.toml --expected-commit 0123456789abcdef0123456789abcdef01234567 --evaluated-at-utc 2026-08-08T12:30:00Z --json +rtk cargo run --locked --offline -- verify --receipt tests/fixtures/receipt-v1-pass.json --policy tests/fixtures/policy-v1.toml --expected-commit bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb --evaluated-at-utc 2026-08-08T12:30:00Z --json # exits 3; capture stdout only +rtk cargo test --locked --offline --test compatibility_baseline print_normalized_dry_run_baselines -- --ignored --nocapture +``` + +Task 2 focused GREEN checks: + +```text +rtk cargo test --locked --offline --test compatibility_baseline root_help_bytes_match_the_baseline +rtk cargo test --locked --offline --test compatibility_baseline command_help_bytes_match_the_baseline +rtk cargo test --locked --offline --test compatibility_baseline plan_and_verification_bytes_and_exit_codes_match_the_baseline +rtk cargo test --locked --offline --test compatibility_baseline matrix_plan_profiles_match_the_baseline +rtk cargo test --locked --offline --test compatibility_baseline dry_run_profiles_match_the_baseline_without_execution +rtk cargo test --locked --offline --test compatibility_baseline usage_error_exit_code_remains_two +``` + +Task 3 commands: + +```text +rtk cargo generate-lockfile --offline --manifest-path tests/fixtures/public-api-compat/Cargo.toml +rtk cargo test --locked --offline --test compatibility_baseline supported_public_facade_compiles_downstream +``` + +Task 2 terminal test targets: + +```text +rtk cargo test --locked --offline --test compatibility_baseline +rtk cargo test --locked --offline --test plan_cli +rtk cargo test --locked --offline --test verify_cli +rtk cargo test --locked --offline --test receipt_contract +rtk cargo test --locked --offline --test matrix_contract +rtk cargo test --locked --offline --test verification_contract +``` + +Regenerate captures with the commands above, update files using reviewed +patches, then measure every listed path with `sha256sum` and update this +manifest. Fixture hashes prove byte stability only; they do not prove Docker, +runtime, admission, or project-run behavior. No receipt publication or heavy +surface is exercised by this baseline. diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json new file mode 100644 index 0000000..e681c2e --- /dev/null +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json @@ -0,0 +1,38 @@ +{ + "schema_version": "1.0", + "base_commit": "5fed7c443504969e62980141048f9279f9fa1dfe", + "hash_algorithm": "sha256", + "files": [ + {"path":"schema/benchmark-v1.schema.json","digest":"sha256:98e42ebe83dface724fe53195ad468b2da4c88b1bcd6072b65b3661bbdd1f644"}, + {"path":"schema/config-v1.schema.json","digest":"sha256:4cd85f6b0fb81a4c1ea095293d0cc0a36cae4bc4affeb0e3412301404979870f"}, + {"path":"schema/config-v2.schema.json","digest":"sha256:2474b202839ea63585f880c100747ddbd189d9bdda20d0f565bbc979c01c8a63"}, + {"path":"schema/policy-v1.schema.json","digest":"sha256:e76d92f8a328c714ace3aab8c7c88e1ef58c4092ca48961fd2e57ca347b53d79"}, + {"path":"schema/policy-v1_1.schema.json","digest":"sha256:b06ee6753cab21db0a4cca5bfbdbd90ec2cdeccd2a7d2eaaed68b826d0944b00"}, + {"path":"schema/policy-v2.schema.json","digest":"sha256:2cc87c4bbfdbe787144a4ff96ff2a20dd004525f704f9f1cbd80117225426316"}, + {"path":"schema/receipt-v1.schema.json","digest":"sha256:684b81b685a6013181e3fdd4bcf29573c01959da5e510bc158d5f94b8aeb8abc"}, + {"path":"schema/receipt-v2.schema.json","digest":"sha256:bf315a2609366adc94520f5e8966d3048529e86759e66701a544be10eca9adbf"}, + {"path":"schema/verification-report-v1.schema.json","digest":"sha256:67ec9524749d471d6a23e22ba0f9d52377273448f5c25ef318fbbd1a9f8f9b23"}, + {"path":"tests/compatibility_baseline.rs","digest":"sha256:cba5318f6e7265250127562a01e12675c6dd99ab69c159aa6de2e355882f9751"}, + {"path":"tests/fixtures/compatibility/dry-run-v1.normalized.json","digest":"sha256:a04d86e0c9fb24b703731df16b3d1f61ac9ec8e9ec76a23d59642c7003745079"}, + {"path":"tests/fixtures/compatibility/dry-run-v2-current.normalized.json","digest":"sha256:ee98f6460e3f12d6bf758413d8f8fc3c0ef4db61b6c80bac5fc7d9459aa7a261"}, + {"path":"tests/fixtures/compatibility/dry-run-v2-legacy.normalized.json","digest":"sha256:21f35e6457671c233b3863e70cc9a7b3aece11f3f42c735a9293866b5b18283d"}, + {"path":"tests/fixtures/compatibility/plan-help.stdout.txt","digest":"sha256:0b3db7c5f505d5cd1b4a7d49af29ef5e51de423cfaa99e421019bd778f8b1615"}, + {"path":"tests/fixtures/compatibility/plan-v1.stdout.json","digest":"sha256:4e61d35fbf0adf47b15c464ad29eccccff2150c60da64c0f4b5cf42d41a8d946"}, + {"path":"tests/fixtures/compatibility/plan-v2-legacy.stdout.json","digest":"sha256:d8708b604f4574994466fa278853fa068d797c3c5ce342bbaf813f4b84e5989f"}, + {"path":"tests/fixtures/compatibility/root-help.stdout.txt","digest":"sha256:66771cc38667e518c09a93aab5ecaab800b8269bfe6ed20155e5b7ad47c67d5a"}, + {"path":"tests/fixtures/compatibility/verify-help.stdout.txt","digest":"sha256:f4c2e4662dfbc7675180042acf98579ab76391057d4e7e63c313a110f20f7744"}, + {"path":"tests/fixtures/compatibility/verify-v1-fail.stdout.json","digest":"sha256:343c3a89dfb5812c1f8eea6460693b227232243f8d6fb9c1a3fbb8eace061d23"}, + {"path":"tests/fixtures/compatibility/verify-v1-pass.stdout.json","digest":"sha256:d94625d1f9771095ac2a5f3bbc5e163d9267d4db9eeccf12f44e7500bd304527"}, + {"path":"tests/fixtures/matrix-v2-legacy-plan-044697.json","digest":"sha256:224cc7b31c1541d4270960980412058f23e4ed396d63c79a4454c3c1bd25653d"}, + {"path":"tests/fixtures/plan-v2-current-default.stdout.json","digest":"sha256:6c86ecad1b7c213945656282aa1f2844909302a13e1a585c3a2c93be6081458a"}, + {"path":"tests/fixtures/policy-v1.toml","digest":"sha256:21d0f45765ba8bc4ea1f443ad0fa76093cf5edeb7b98f181e50e34052ca8bac3"}, + {"path":"tests/fixtures/policy-v1_1-trusted-plan.toml","digest":"sha256:f9c3293ba7ddeb100a85680d1b931b2246621fcc62314dffd06c4a4e16d8ca79"}, + {"path":"tests/fixtures/policy-v2-legacy-compatible.toml","digest":"sha256:846a94bd6b298ca1a35a4c0f39bed31ae08d1b94e448fa82fc058ea16550afec"}, + {"path":"tests/fixtures/public-api-compat/Cargo.lock","digest":"sha256:3003f77d7bb2572af781b843ea122dacc04e4e5fb3eae548e2529cc964789eca"}, + {"path":"tests/fixtures/public-api-compat/Cargo.toml","digest":"sha256:efb71edcdd8bceb190a9ba3cd4e2b86b3337f19b22a56702ee89d26389e7e18a"}, + {"path":"tests/fixtures/public-api-compat/src/main.rs","digest":"sha256:3175134f79b22feb3720215457d954d43fea419a30221f4cc375ec3e92b01d07"}, + {"path":"tests/fixtures/receipt-v1-pass.json","digest":"sha256:f6824b28ded398d26620f5cc53c8e891997f909ff83b6ff7e17eef2ca821b017"}, + {"path":"tests/fixtures/receipt-v2-pass.json","digest":"sha256:35d15b81c281f13fe07a49cf20a1aa942ab07f3e8b2ddbd400d9747e37eba8fe"} + ], + "non_executed_surfaces": ["admission and resource decisions", "benchmark", "doctor runtime probe", "dry-run runtime rendering", "guard exec", "project run", "receipt publication"] +} diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md index f04f6ae..71b05de 100644 --- a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md @@ -1,12 +1,15 @@ # Capability Packs and Clean Architecture progress - Base: `5fed7c443504969e62980141048f9279f9fa1dfe` +- Predecessor: `17e069a7eb3bcc6596c93bb6432984eba8472208` - Branch: `codex/capability-packs-clean-architecture-delivery-v1` - Specification commit: `5ef9707930f7095a2f57bc3e38e53bfeac06aaf2` - Current milestone: M0 compatibility baseline -- Completed evidence: design review READY; `git diff --check` PASS -- Unproven: baseline tests, application seam, pack contract, reference packs, +- Completed evidence: design review READY; `git diff --check` PASS; M0 compatibility manifest and downstream facade checks prepared +- Terminal M0 verification: `rtk cargo fmt --check` PASS; focused manifest test PASS; full compatibility baseline PASS (9 passed, 1 ignored); `rtk git diff --check` PASS; bounded privacy scan PASS (zero matches) +- M0 status: awaits independent review; not terminally closed +- Unproven: application seam, pack contract, reference packs, hosted CI, publication, release - Heavy processes: none - External mutations: none -- Next action: execute Task 2 of the M0 plan with RED/GREEN TDD +- Next action: independent review of M0 closure evidence, then proceed to the application seam diff --git a/tests/compatibility_baseline.rs b/tests/compatibility_baseline.rs index 05b1617..5ea029f 100644 --- a/tests/compatibility_baseline.rs +++ b/tests/compatibility_baseline.rs @@ -1,5 +1,9 @@ +use std::fs; +use std::path::{Component, Path}; use std::process::{Command, Output}; +use sha2::{Digest, Sha256}; + const COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; const EVALUATED_AT: &str = "2026-08-08T12:30:00Z"; @@ -10,6 +14,88 @@ fn ccp(args: &[&str]) -> Output { .expect("execute compatibility command") } +#[test] +fn manifest_paths_and_hashes_match() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let manifest_path = root.join( + "docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json", + ); + let manifest: serde_json::Value = + serde_json::from_slice(&fs::read(&manifest_path).expect("read compatibility manifest")) + .expect("parse compatibility manifest"); + assert_eq!(manifest["schema_version"], "1.0"); + assert_eq!( + manifest["base_commit"], + "5fed7c443504969e62980141048f9279f9fa1dfe" + ); + assert_eq!(manifest["hash_algorithm"], "sha256"); + let object = manifest.as_object().expect("manifest object"); + let keys = object + .keys() + .map(String::as_str) + .collect::>(); + assert_eq!( + keys, + std::collections::BTreeSet::from([ + "base_commit", + "files", + "hash_algorithm", + "non_executed_surfaces", + "schema_version" + ]) + ); + assert_eq!( + manifest["non_executed_surfaces"], + serde_json::json!([ + "admission and resource decisions", + "benchmark", + "doctor runtime probe", + "dry-run runtime rendering", + "guard exec", + "project run", + "receipt publication" + ]) + ); + let entries = manifest["files"].as_array().expect("file entries"); + let mut previous = None::; + for entry in entries { + let entry_object = entry.as_object().expect("file entry object"); + let entry_keys = entry_object + .keys() + .map(String::as_str) + .collect::>(); + assert_eq!( + entry_keys, + std::collections::BTreeSet::from(["digest", "path"]) + ); + let relative = entry["path"].as_str().expect("relative path"); + let path = Path::new(relative); + assert!(!path.is_absolute()); + assert!( + path.components() + .all(|component| matches!(component, Component::Normal(_) | Component::CurDir)) + ); + if let Some(previous) = &previous { + assert!( + previous.as_str() < relative, + "paths must be unique and sorted" + ); + } + previous = Some(relative.to_owned()); + let bytes = fs::read(root.join(path)).expect("read manifested file"); + let digest = Sha256::digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + let digest = format!("sha256:{digest}"); + assert_eq!( + entry["digest"].as_str(), + Some(digest.as_str()), + "{relative}" + ); + } +} + #[test] fn supported_public_facade_compiles_downstream() { let status = Command::new(env!("CARGO")) From d6e4307b701e7a19c18066831a3ddfa63d9920e5 Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 13:40:15 +0200 Subject: [PATCH 09/17] docs: record M0 review closure --- .../progress.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md index 71b05de..2b9c307 100644 --- a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md @@ -5,11 +5,11 @@ - Branch: `codex/capability-packs-clean-architecture-delivery-v1` - Specification commit: `5ef9707930f7095a2f57bc3e38e53bfeac06aaf2` - Current milestone: M0 compatibility baseline -- Completed evidence: design review READY; `git diff --check` PASS; M0 compatibility manifest and downstream facade checks prepared +- Completed evidence: design review READY; `git diff --check` PASS; M0 compatibility manifest and downstream facade checks independently approved at `9e689c7f04e3d8c0479c74d91669c170a4c66e52` - Terminal M0 verification: `rtk cargo fmt --check` PASS; focused manifest test PASS; full compatibility baseline PASS (9 passed, 1 ignored); `rtk git diff --check` PASS; bounded privacy scan PASS (zero matches) -- M0 status: awaits independent review; not terminally closed +- M0 status: terminally closed; Spec PASS and Task quality Approved - Unproven: application seam, pack contract, reference packs, hosted CI, publication, release - Heavy processes: none - External mutations: none -- Next action: independent review of M0 closure evidence, then proceed to the application seam +- Next action: execute the M1 private run application seam with RED/GREEN TDD From 0907ef57106555db552e164282063ea52233520c Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 13:46:22 +0200 Subject: [PATCH 10/17] refactor: add private run dependency seam --- src/run.rs | 111 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 20 deletions(-) diff --git a/src/run.rs b/src/run.rs index 159cbac..8f18c29 100644 --- a/src/run.rs +++ b/src/run.rs @@ -61,6 +61,18 @@ pub struct RunRequest<'a> { pub source_snapshot: Option<&'a SourceSnapshot>, } +struct RunDependencies<'a> { + runtime: &'a dyn RuntimePort, + supervisor: &'a dyn SupervisorPort, + cancellation: &'a CancellationToken, + clock: &'a dyn Clock, + barrier: &'a mut dyn CompletionBarrier, + lifecycle: &'a mut dyn RunLifecycleObserver, + storage_probe: &'a dyn StorageProbe, + capability_probe: &'a dyn RuntimeCapabilityProbe, + runtime_preflight: Option, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct RunOutcome { pub receipt: ReceiptEnvelopeV1, @@ -301,31 +313,22 @@ pub fn execute_local_run_with_barrier_and_lifecycle_and_runtime_preflight( } // This private composition boundary wires independently injected run ports. -#[allow(clippy::too_many_arguments)] -fn execute_local_run_with_barrier_and_lifecycle_and_storage_probe_and_capability_probe( +fn execute_local_run_with_dependencies( request: &RunRequest<'_>, - runtime: &dyn RuntimePort, - supervisor: &dyn SupervisorPort, - cancellation: &CancellationToken, - clock: &dyn Clock, - barrier: &mut dyn CompletionBarrier, - lifecycle: &mut dyn RunLifecycleObserver, - storage_probe: &dyn StorageProbe, - capability_probe: &dyn RuntimeCapabilityProbe, - runtime_preflight: Option, + dependencies: &mut RunDependencies<'_>, ) -> Result { let (receipt, artifact_manifest, runtime_capability_evidence) = execute_local_receipt_and_artifacts_with_barrier_and_lifecycle_and_storage_probe( request, - runtime, - supervisor, - cancellation, - clock, - barrier, - lifecycle, - storage_probe, - capability_probe, - runtime_preflight, + dependencies.runtime, + dependencies.supervisor, + dependencies.cancellation, + dependencies.clock, + dependencies.barrier, + dependencies.lifecycle, + dependencies.storage_probe, + dependencies.capability_probe, + dependencies.runtime_preflight.take(), )?; let receipt_v2 = request .source_snapshot @@ -371,6 +374,33 @@ fn execute_local_run_with_barrier_and_lifecycle_and_storage_probe_and_capability }) } +#[allow(clippy::too_many_arguments)] +fn execute_local_run_with_barrier_and_lifecycle_and_storage_probe_and_capability_probe( + request: &RunRequest<'_>, + runtime: &dyn RuntimePort, + supervisor: &dyn SupervisorPort, + cancellation: &CancellationToken, + clock: &dyn Clock, + barrier: &mut dyn CompletionBarrier, + lifecycle: &mut dyn RunLifecycleObserver, + storage_probe: &dyn StorageProbe, + capability_probe: &dyn RuntimeCapabilityProbe, + runtime_preflight: Option, +) -> Result { + let mut dependencies = RunDependencies { + runtime, + supervisor, + cancellation, + clock, + barrier, + lifecycle, + storage_probe, + capability_probe, + runtime_preflight, + }; + execute_local_run_with_dependencies(request, &mut dependencies) +} + /// Execute one already-validated v1 plan and return sealed evidence without /// writing a receipt into the source checkout. Matrix orchestration uses this /// primitive so every runtime observes the same clean exact head before the @@ -1169,6 +1199,47 @@ mod tests { DockerCompatibleRuntime, RuntimeCapabilityEvidenceV1, RuntimeFlavor, RuntimeProbe, }; + #[test] + fn run_dependencies_execute_the_existing_run_path() { + let fixture = RunFixture::new("dependency-seam"); + let runtime = DockerCompatibleRuntime; + let supervisor = FakeSupervisor::new(ExecutionMode::Pass); + let cancellation = CancellationToken::default(); + let clock = FixedClock::new(); + let storage_probe = SystemStorageProbe; + let capability_probe = DockerRuntimeCapabilityProbe; + let mut barrier = NoopCompletionBarrier; + let mut lifecycle = NoopRunLifecycleObserver; + let request = RunRequest { + envelope: &fixture.envelope, + repository: &fixture.repository, + cache: &fixture.cache, + producer_version: env!("CARGO_PKG_VERSION"), + generation: 7, + source_snapshot: None, + }; + let mut dependencies = RunDependencies { + runtime: &runtime, + supervisor: &supervisor, + cancellation: &cancellation, + clock: &clock, + barrier: &mut barrier, + lifecycle: &mut lifecycle, + storage_probe: &storage_probe, + capability_probe: &capability_probe, + runtime_preflight: None, + }; + let outcome = execute_local_run_with_dependencies(&request, &mut dependencies) + .expect("execute through dependency seam"); + assert_eq!(outcome.exit_code(), 0); + assert_eq!( + outcome + .published_canonical_bytes() + .expect("canonical bytes"), + fs::read(&outcome.receipt_path).expect("published receipt") + ); + } + #[test] fn utc_formatter_covers_epoch_leap_day_and_current_range() { assert_eq!(format_unix_utc(0).expect("epoch"), "1970-01-01T00:00:00Z"); From f880c27730793322cae4886a25ab9687967e33b0 Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 13:53:30 +0200 Subject: [PATCH 11/17] refactor: route run wrappers through dependencies --- src/run.rs | 226 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 131 insertions(+), 95 deletions(-) diff --git a/src/run.rs b/src/run.rs index 8f18c29..5580eaa 100644 --- a/src/run.rs +++ b/src/run.rs @@ -156,14 +156,21 @@ pub fn execute_local_run( clock: &dyn Clock, ) -> Result { let storage_probe = SystemStorageProbe; - execute_local_run_with_storage_probe( - request, + let mut barrier = NoopCompletionBarrier; + let mut lifecycle = NoopRunLifecycleObserver; + let capability_probe = DockerRuntimeCapabilityProbe; + let mut dependencies = RunDependencies { runtime, supervisor, cancellation, clock, - &storage_probe, - ) + barrier: &mut barrier, + lifecycle: &mut lifecycle, + storage_probe: &storage_probe, + capability_probe: &capability_probe, + runtime_preflight: None, + }; + execute_local_run_with_dependencies(request, &mut dependencies) } pub fn execute_local_run_with_storage_probe( @@ -176,16 +183,19 @@ pub fn execute_local_run_with_storage_probe( ) -> Result { let mut barrier = NoopCompletionBarrier; let mut lifecycle = NoopRunLifecycleObserver; - execute_local_run_with_barrier_and_lifecycle_and_storage_probe( - request, + let capability_probe = DockerRuntimeCapabilityProbe; + let mut dependencies = RunDependencies { runtime, supervisor, cancellation, clock, - &mut barrier, - &mut lifecycle, + barrier: &mut barrier, + lifecycle: &mut lifecycle, storage_probe, - ) + capability_probe: &capability_probe, + runtime_preflight: None, + }; + execute_local_run_with_dependencies(request, &mut dependencies) } pub fn execute_local_run_with_runtime_capability_probe( @@ -199,18 +209,18 @@ pub fn execute_local_run_with_runtime_capability_probe( let storage_probe = SystemStorageProbe; let mut barrier = NoopCompletionBarrier; let mut lifecycle = NoopRunLifecycleObserver; - execute_local_run_with_barrier_and_lifecycle_and_storage_probe_and_capability_probe( - request, + let mut dependencies = RunDependencies { runtime, supervisor, cancellation, clock, - &mut barrier, - &mut lifecycle, - &storage_probe, + barrier: &mut barrier, + lifecycle: &mut lifecycle, + storage_probe: &storage_probe, capability_probe, - None, - ) + runtime_preflight: None, + }; + execute_local_run_with_dependencies(request, &mut dependencies) } pub fn execute_local_run_with_barrier( @@ -223,16 +233,19 @@ pub fn execute_local_run_with_barrier( ) -> Result { let mut lifecycle = NoopRunLifecycleObserver; let storage_probe = SystemStorageProbe; - execute_local_run_with_barrier_and_lifecycle_and_storage_probe( - request, + let capability_probe = DockerRuntimeCapabilityProbe; + let mut dependencies = RunDependencies { runtime, supervisor, cancellation, clock, barrier, - &mut lifecycle, - &storage_probe, - ) + lifecycle: &mut lifecycle, + storage_probe: &storage_probe, + capability_probe: &capability_probe, + runtime_preflight: None, + }; + execute_local_run_with_dependencies(request, &mut dependencies) } pub fn execute_local_run_with_barrier_and_lifecycle( @@ -245,16 +258,19 @@ pub fn execute_local_run_with_barrier_and_lifecycle( lifecycle: &mut dyn RunLifecycleObserver, ) -> Result { let storage_probe = SystemStorageProbe; - execute_local_run_with_barrier_and_lifecycle_and_storage_probe( - request, + let capability_probe = DockerRuntimeCapabilityProbe; + let mut dependencies = RunDependencies { runtime, supervisor, cancellation, clock, barrier, lifecycle, - &storage_probe, - ) + storage_probe: &storage_probe, + capability_probe: &capability_probe, + runtime_preflight: None, + }; + execute_local_run_with_dependencies(request, &mut dependencies) } // The explicit ports keep storage and lifecycle fault paths independently injectable. @@ -270,8 +286,7 @@ pub fn execute_local_run_with_barrier_and_lifecycle_and_storage_probe( storage_probe: &dyn StorageProbe, ) -> Result { let capability_probe = DockerRuntimeCapabilityProbe; - execute_local_run_with_barrier_and_lifecycle_and_storage_probe_and_capability_probe( - request, + let mut dependencies = RunDependencies { runtime, supervisor, cancellation, @@ -279,9 +294,10 @@ pub fn execute_local_run_with_barrier_and_lifecycle_and_storage_probe( barrier, lifecycle, storage_probe, - &capability_probe, - None, - ) + capability_probe: &capability_probe, + runtime_preflight: None, + }; + execute_local_run_with_dependencies(request, &mut dependencies) } // The explicit ports preserve the public runtime-preflight test seam. @@ -298,18 +314,18 @@ pub fn execute_local_run_with_barrier_and_lifecycle_and_runtime_preflight( ) -> Result { let storage_probe = SystemStorageProbe; let capability_probe = DockerRuntimeCapabilityProbe; - execute_local_run_with_barrier_and_lifecycle_and_storage_probe_and_capability_probe( - request, + let mut dependencies = RunDependencies { runtime, supervisor, cancellation, clock, barrier, lifecycle, - &storage_probe, - &capability_probe, - Some(runtime_preflight), - ) + storage_probe: &storage_probe, + capability_probe: &capability_probe, + runtime_preflight: Some(runtime_preflight), + }; + execute_local_run_with_dependencies(request, &mut dependencies) } // This private composition boundary wires independently injected run ports. @@ -318,18 +334,7 @@ fn execute_local_run_with_dependencies( dependencies: &mut RunDependencies<'_>, ) -> Result { let (receipt, artifact_manifest, runtime_capability_evidence) = - execute_local_receipt_and_artifacts_with_barrier_and_lifecycle_and_storage_probe( - request, - dependencies.runtime, - dependencies.supervisor, - dependencies.cancellation, - dependencies.clock, - dependencies.barrier, - dependencies.lifecycle, - dependencies.storage_probe, - dependencies.capability_probe, - dependencies.runtime_preflight.take(), - )?; + execute_local_receipt_and_artifacts_with_dependencies(request, dependencies)?; let receipt_v2 = request .source_snapshot .map(|snapshot| { @@ -374,33 +379,6 @@ fn execute_local_run_with_dependencies( }) } -#[allow(clippy::too_many_arguments)] -fn execute_local_run_with_barrier_and_lifecycle_and_storage_probe_and_capability_probe( - request: &RunRequest<'_>, - runtime: &dyn RuntimePort, - supervisor: &dyn SupervisorPort, - cancellation: &CancellationToken, - clock: &dyn Clock, - barrier: &mut dyn CompletionBarrier, - lifecycle: &mut dyn RunLifecycleObserver, - storage_probe: &dyn StorageProbe, - capability_probe: &dyn RuntimeCapabilityProbe, - runtime_preflight: Option, -) -> Result { - let mut dependencies = RunDependencies { - runtime, - supervisor, - cancellation, - clock, - barrier, - lifecycle, - storage_probe, - capability_probe, - runtime_preflight, - }; - execute_local_run_with_dependencies(request, &mut dependencies) -} - /// Execute one already-validated v1 plan and return sealed evidence without /// writing a receipt into the source checkout. Matrix orchestration uses this /// primitive so every runtime observes the same clean exact head before the @@ -416,34 +394,25 @@ pub fn execute_local_receipt_with_barrier_and_lifecycle( ) -> Result { let storage_probe = SystemStorageProbe; let capability_probe = DockerRuntimeCapabilityProbe; - execute_local_receipt_and_artifacts_with_barrier_and_lifecycle_and_storage_probe( - request, + let mut dependencies = RunDependencies { runtime, supervisor, cancellation, clock, barrier, lifecycle, - &storage_probe, - &capability_probe, - None, - ) - .map(|(receipt, _, _)| receipt) + storage_probe: &storage_probe, + capability_probe: &capability_probe, + runtime_preflight: None, + }; + execute_local_receipt_and_artifacts_with_dependencies(request, &mut dependencies) + .map(|(receipt, _, _)| receipt) } -// Receipt finalization shares the same explicit fault-injection ports as execution. -#[allow(clippy::too_many_arguments)] -fn execute_local_receipt_and_artifacts_with_barrier_and_lifecycle_and_storage_probe( +// Receipt finalization shares the same explicit fault-injection ports through the dependency seam. +fn execute_local_receipt_and_artifacts_with_dependencies( request: &RunRequest<'_>, - runtime: &dyn RuntimePort, - supervisor: &dyn SupervisorPort, - cancellation: &CancellationToken, - clock: &dyn Clock, - barrier: &mut dyn CompletionBarrier, - lifecycle: &mut dyn RunLifecycleObserver, - storage_probe: &dyn StorageProbe, - capability_probe: &dyn RuntimeCapabilityProbe, - runtime_preflight: Option, + dependencies: &mut RunDependencies<'_>, ) -> Result< ( ReceiptEnvelopeV1, @@ -452,6 +421,14 @@ fn execute_local_receipt_and_artifacts_with_barrier_and_lifecycle_and_storage_pr ), RunError, > { + let cancellation = dependencies.cancellation; + let runtime = dependencies.runtime; + let supervisor = dependencies.supervisor; + let clock = dependencies.clock; + let barrier = &mut *dependencies.barrier; + let lifecycle = &mut *dependencies.lifecycle; + let storage_probe = dependencies.storage_probe; + let capability_probe = dependencies.capability_probe; if cancellation.reason() == Some(CancellationReason::ResourcePressure) { return Err(RunError::ResourcePressure); } @@ -464,7 +441,7 @@ fn execute_local_receipt_and_artifacts_with_barrier_and_lifecycle_and_storage_pr preflight_storage(storage, &request.cache.root().path, storage_probe) .map_err(RunError::Storage)?; } - let runtime_preflight = match runtime_preflight { + let runtime_preflight = match dependencies.runtime_preflight.take() { Some(preflight) => preflight, None => preflight_runtime_capabilities( request.envelope, @@ -1240,6 +1217,65 @@ mod tests { ); } + #[test] + fn public_run_wrapper_matches_dependency_seam_bytes() { + let public_fixture = RunFixture::new("wrapper-public"); + let direct_fixture = RunFixture::new("wrapper-direct"); + let public_request = RunRequest { + envelope: &public_fixture.envelope, + repository: &public_fixture.repository, + cache: &public_fixture.cache, + producer_version: env!("CARGO_PKG_VERSION"), + generation: 7, + source_snapshot: None, + }; + let direct_request = RunRequest { + envelope: &direct_fixture.envelope, + repository: &direct_fixture.repository, + cache: &direct_fixture.cache, + producer_version: env!("CARGO_PKG_VERSION"), + generation: 7, + source_snapshot: None, + }; + let runtime = DockerCompatibleRuntime; + let public_supervisor = FakeSupervisor::new(ExecutionMode::Pass); + let direct_supervisor = FakeSupervisor::new(ExecutionMode::Pass); + let public = execute_local_run( + &public_request, + &runtime, + &public_supervisor, + &CancellationToken::default(), + &FixedClock::new(), + ) + .expect("public wrapper"); + let cancellation = CancellationToken::default(); + let clock = FixedClock::new(); + let storage_probe = SystemStorageProbe; + let capability_probe = DockerRuntimeCapabilityProbe; + let mut barrier = NoopCompletionBarrier; + let mut lifecycle = NoopRunLifecycleObserver; + let mut dependencies = RunDependencies { + runtime: &runtime, + supervisor: &direct_supervisor, + cancellation: &cancellation, + clock: &clock, + barrier: &mut barrier, + lifecycle: &mut lifecycle, + storage_probe: &storage_probe, + capability_probe: &capability_probe, + runtime_preflight: None, + }; + let direct = execute_local_run_with_dependencies(&direct_request, &mut dependencies) + .expect("dependency seam"); + assert_eq!(public.exit_code(), direct.exit_code()); + assert_eq!(public.receipt.receipt, direct.receipt.receipt); + assert_eq!(public.receipt_v2, direct.receipt_v2); + assert_eq!( + public.published_canonical_bytes().expect("public bytes"), + direct.published_canonical_bytes().expect("direct bytes") + ); + } + #[test] fn utc_formatter_covers_epoch_leap_day_and_current_range() { assert_eq!(format_unix_utc(0).expect("epoch"), "1970-01-01T00:00:00Z"); From 69851f688590a121b980eeeff48b3fb48d74877e Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 13:58:44 +0200 Subject: [PATCH 12/17] chore: ignore downstream fixture target --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4c02b4c..6fe092f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/tests/fixtures/public-api-compat/target/ /.ccp /.ccp-cache /.ccp-mounts/cargo-home/* From 12d85c475b63427ad18de8630ffeb6d5ac07000f Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 14:07:59 +0200 Subject: [PATCH 13/17] test: keep compatibility baseline lint-clean --- .../compatibility/manifest.json | 2 +- tests/compatibility_baseline.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json index e681c2e..383855f 100644 --- a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/compatibility/manifest.json @@ -12,7 +12,7 @@ {"path":"schema/receipt-v1.schema.json","digest":"sha256:684b81b685a6013181e3fdd4bcf29573c01959da5e510bc158d5f94b8aeb8abc"}, {"path":"schema/receipt-v2.schema.json","digest":"sha256:bf315a2609366adc94520f5e8966d3048529e86759e66701a544be10eca9adbf"}, {"path":"schema/verification-report-v1.schema.json","digest":"sha256:67ec9524749d471d6a23e22ba0f9d52377273448f5c25ef318fbbd1a9f8f9b23"}, - {"path":"tests/compatibility_baseline.rs","digest":"sha256:cba5318f6e7265250127562a01e12675c6dd99ab69c159aa6de2e355882f9751"}, + {"path":"tests/compatibility_baseline.rs","digest":"sha256:032ae7f3636016dc616af402a3bd6d30dd17df1d444b4df3a9e6e98f6a0ef577"}, {"path":"tests/fixtures/compatibility/dry-run-v1.normalized.json","digest":"sha256:a04d86e0c9fb24b703731df16b3d1f61ac9ec8e9ec76a23d59642c7003745079"}, {"path":"tests/fixtures/compatibility/dry-run-v2-current.normalized.json","digest":"sha256:ee98f6460e3f12d6bf758413d8f8fc3c0ef4db61b6c80bac5fc7d9459aa7a261"}, {"path":"tests/fixtures/compatibility/dry-run-v2-legacy.normalized.json","digest":"sha256:21f35e6457671c233b3863e70cc9a7b3aece11f3f42c735a9293866b5b18283d"}, diff --git a/tests/compatibility_baseline.rs b/tests/compatibility_baseline.rs index 5ea029f..a0463e9 100644 --- a/tests/compatibility_baseline.rs +++ b/tests/compatibility_baseline.rs @@ -275,7 +275,7 @@ fn normalize_dry_run(mut v: serde_json::Value) -> (serde_json::Value, Vec Date: Sun, 30 Aug 2026 14:16:09 +0200 Subject: [PATCH 14/17] docs: close run application seam milestone --- .../progress.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md index 2b9c307..6e6bd82 100644 --- a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md @@ -4,12 +4,15 @@ - Predecessor: `17e069a7eb3bcc6596c93bb6432984eba8472208` - Branch: `codex/capability-packs-clean-architecture-delivery-v1` - Specification commit: `5ef9707930f7095a2f57bc3e38e53bfeac06aaf2` -- Current milestone: M0 compatibility baseline +- Current milestone: M1 private run application seam (closed) - Completed evidence: design review READY; `git diff --check` PASS; M0 compatibility manifest and downstream facade checks independently approved at `9e689c7f04e3d8c0479c74d91669c170a4c66e52` - Terminal M0 verification: `rtk cargo fmt --check` PASS; focused manifest test PASS; full compatibility baseline PASS (9 passed, 1 ignored); `rtk git diff --check` PASS; bounded privacy scan PASS (zero matches) - M0 status: terminally closed; Spec PASS and Task quality Approved -- Unproven: application seam, pack contract, reference packs, - hosted CI, publication, release +- M1 implementation HEAD: `12d85c475b63427ad18de8630ffeb6d5ac07000f` +- M1 verification: `rtk cargo fmt --check` PASS; strict Clippy PASS; compatibility_baseline 9 passed/1 ignored; plan_cli 11 passed; verify_cli 6 passed; receipt_contract 10 passed; matrix_contract 16 passed; verification_contract 20 passed/1 ignored; host full suite 467 passed/5 ignored/27 suites after the sandbox permission failure was repeated with narrow host permission; manifest test 1 passed/9 filtered; privacy scan zero matches; clean status and diff check PASS +- M1 reviews: specification compliance PASS; code quality/architecture APPROVED; no Critical, Important, or Minor findings +- M0 fixtures unchanged except the reviewed one-line Clippy-equivalent test and its single digest update +- Unproven: remaining hosted CI, push, PR, publication, and release gates; M2 pack contract and reference packs - Heavy processes: none - External mutations: none -- Next action: execute the M1 private run application seam with RED/GREEN TDD +- Next action: M2 planning/execution From a1c922d4f47fd06e74a336b89a19aecc703908bd Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 14:36:45 +0200 Subject: [PATCH 15/17] docs: plan capability pack contract --- .../2026-08-30-m2-capability-pack-contract.md | 1021 +++++++++++++++++ 1 file changed, 1021 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-30-m2-capability-pack-contract.md diff --git a/docs/superpowers/plans/2026-08-30-m2-capability-pack-contract.md b/docs/superpowers/plans/2026-08-30-m2-capability-pack-contract.md new file mode 100644 index 0000000..91aaa72 --- /dev/null +++ b/docs/superpowers/plans/2026-08-30-m2-capability-pack-contract.md @@ -0,0 +1,1021 @@ +# M2 Capability Pack Contract Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a bounded, versioned, inert Capability Pack library contract that validates untrusted TOML and expands one explicitly selected profile into CCP's existing `ExecutionPlanEnvelopeV1` without executing project code or changing existing CLI, receipt, policy, or plan bytes. + +**Architecture:** A new public `capability_pack` adapter parses a strict TOML manifest, validates pack-specific trust metadata, and delegates all executable configuration validation to an internally constructed schema-`1.3` `ConfigV1`. Validation produces a canonical, privacy-bounded pack envelope plus private validated profile material; expansion requires an explicit repository binding and returns a pack-bound wrapper around the existing plan envelope. M2 is library-first: it does not add a CLI command or any execution path, so the M0 compatibility corpus remains byte-stable. + +**Tech Stack:** Rust 1.87+, Serde, TOML, Schemars, serde_json, existing CCP configuration normalization and canonical JSON/SHA-256 helpers, Cargo offline tests. + +**Spec:** `docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md` + +## Global Constraints + +- Existing CLI syntax/help, exit codes, valid configuration bytes and digests, receipt bytes and IDs, policy schemas, JSON shapes, and public Rust facade remain compatible. +- M2 adds no analyzer-specific dependency, receipt field, policy field, configuration schema version, runtime execution, subprocess, shell invocation, Docker probe, network access, host installation, evidence publication, or mutable-image pull. +- The only accepted manifest serialization in schema `1.0` is UTF-8 TOML; JSON input and dual-format autodetection are out of scope. +- Manifest input is at most `1_048_576` bytes and uses `deny_unknown_fields` at every object boundary. +- A profile expands through schema `1.3` `ConfigV1`; it must use `docker_compatible`, a lowercase `sha256:`-pinned OCI image, `network = false`, `pull_policy = "never"`, `swap_mode = "disabled"`, and an explicit storage policy. +- A profile is selected explicitly by ID. One expansion produces exactly one existing `ExecutionPlanEnvelopeV1`; M2 does not infer a matrix or combine profiles. +- Pack identity is `(pack_id, pack_version, pack_digest)`. Pack-version text is strict `MAJOR.MINOR.PATCH` with three unsigned decimal components and no prefix, whitespace, prerelease, or build suffix. Tool versions are bounded control-free strings because real pinned tools may use date/nightly or vendor version syntax; their source digest remains mandatory. +- License fields accept one bounded SPDX-style identifier, not an expression: 1-128 ASCII alphanumeric, `.`, `-`, or `+`; `NOASSERTION` and `NONE` are rejected. M2 validates syntax, not legal compatibility. +- Every source, tool, rule, type-stub, corpus, advisory, or vulnerability database record uses an `https://` URL without credentials, fragments, controls, or whitespace and a lowercase `sha256:` digest followed by exactly 64 hexadecimal characters. +- Integrity and freshness remain distinct. `advisory-database` and `vulnerability-database` inputs require both `snapshot_created_at_utc` in strict `YYYY-MM-DDTHH:MM:SSZ` form and `max_age_seconds` in `1..=31_536_000`; rules, type stubs, and corpora require neither. +- Pack canonicalization uses the existing `receipt::canonical_json` and `canonical_digest` over the normalized pack only. The pack digest never replaces or enters the execution-plan digest or a receipt. +- Normalized collections are duplicate-free and deterministically sorted by stable ID or enum value before canonicalization. +- Official pack execution remains out of scope until M3. Inspection, validation, schema generation, canonical bytes, and expansion must be demonstrably inert. +- The public repository uses hosted CI as its integration gate. Do not begin Task 1 production implementation until the exact M0-M1-plus-plan head has terminal hosted CI success. +- All production behavior follows RED/GREEN TDD. Every accepted task receives an independent spec-and-quality review before the next task. +- No CCP heavy command, Docker workload, network access, push, PR mutation, merge, stable installation, tag, release, package publication, or evidence publication is authorized by this plan. + +--- + +## File Structure + +| Path | Responsibility | +|---|---| +| `src/capability_pack.rs` | Manifest input types, bounds, validation, normalized envelope, canonical digest, inspection, explicit profile binding, inert expansion, typed errors, and focused unit helpers. | +| `src/lib.rs` | Additive public module export only. | +| `tests/capability_pack_contract.rs` | Black-box parser, validator, canonicalization, schema, expansion, and no-execution contract tests. | +| `tests/fixtures/capability-pack-v1/*.toml` | Valid and adversarial manifest inputs. | +| `tests/fixtures/capability-pack-v1/*.json` | Exact canonical inspection and expansion golden bytes. | +| `schema/capability-pack-v1.schema.json` | Pinned generated JSON Schema for the TOML data model. | +| `docs/CAPABILITY_PACKS.md` | Trust boundary, format, inspection/expansion API, evidence classes, limitations, and M3 handoff. | +| `CHANGELOG.md` | Pre-1.0 additive library-contract entry; no claim of executable official packs. | +| `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md` | Exact M2 commits, validation evidence, residual risks, and next gate. | + +The existing `src/config.rs`, `src/main.rs`, receipt/policy modules, and pinned compatibility fixtures are not modified by M2. Pack validation reuses their public types and `ConfigV1::into_plan`; it does not extract or duplicate execution semantics. + +### Task 1: Strict bounded manifest parser and identity contract + +**Files:** +- Create: `src/capability_pack.rs` +- Modify: `src/lib.rs` +- Create: `tests/capability_pack_contract.rs` +- Create: `tests/fixtures/capability-pack-v1/valid-minimal.toml` +- Create: `tests/fixtures/capability-pack-v1/unknown-field.toml` +- Create: `tests/fixtures/capability-pack-v1/unknown-version.toml` + +**Interfaces:** +- Consumes: existing `RuntimeConfig`, `EnvironmentConfig`, `CacheConfig`, `StorageConfig`, `CheckConfig`, `ReceiptConfig`, `ExecutionPlanEnvelopeV1`, `canonical_json`, and `canonical_digest`. +- Produces: `CAPABILITY_PACK_SCHEMA_VERSION`, `MAX_CAPABILITY_PACK_BYTES`, `CapabilityPackManifestV1::parse`, `CapabilityPackManifestV1::load`, and `CapabilityPackError`. + +- [ ] **Step 1: Add the strict manifest fixtures** + +Create `valid-minimal.toml` with this complete shape; all SHA-256 values contain exactly 64 lowercase hex characters: + +```toml +schema_version = "1.0" +pack_id = "ccp.rust-minimal" +pack_version = "1.0.0" +license = "Apache-2.0" +description = "One inert profile used to prove the Capability Pack contract." + +[[upstream_sources]] +id = "pack-source" +url = "https://github.com/MarcoPorcellato/commit-ci-preflight" +digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + +[[profiles]] +id = "strict-clippy" +description = "Compile all targets with warnings denied." +evidence_class = "deterministic" +pass_semantics = "The exact pinned command exits zero." +known_blind_spots = ["Does not prove dynamic behavior."] +supported_hosts = ["macos-aarch64"] +target_platforms = ["linux-arm64"] +required_runtime_features = ["linux-userland", "no-network", "read-only-source"] +offline_preparation = "none" + +[[profiles.tools]] +id = "clippy" +version = "1.87.0" +license = "Apache-2.0" +url = "https://github.com/rust-lang/rust" +digest = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + +[[profiles.inputs]] +id = "rustsec-db" +kind = "advisory-database" +url = "https://github.com/RustSec/advisory-db" +digest = "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" +snapshot_created_at_utc = "2026-08-30T00:00:00Z" +max_age_seconds = 604800 + +[profiles.runtime] +kind = "docker_compatible" +image = "ghcr.io/example/rust@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" +cpu_count = 2 +memory_mib = 2048 +pids_limit = 256 +network = false +pull_policy = "never" +swap_mode = "disabled" + +[profiles.environment] +fixed = { CARGO_NET_OFFLINE = "true" } + +[profiles.storage] +min_free_bytes = 1048576 +receipt_journal_reserve_bytes = 4096 +max_cache_growth_bytes = 1048576 + +[[profiles.caches]] +id = "cargo" +mount_path = ".cache/cargo" + +[[profiles.checks]] +id = "clippy" +required = true +argv = ["cargo", "clippy", "--locked", "--offline", "--all-targets", "--all-features", "--", "-D", "warnings"] +working_directory = "." +timeout_seconds = 1800 +``` + +Create `unknown-field.toml` by adding `unexpected = true` at the root, and `unknown-version.toml` by changing only `schema_version` to `"2.0"`. + +- [ ] **Step 2: Write failing black-box parser tests** + +Add tests with these exact names and assertions: + +```rust +const VALID: &str = include_str!("fixtures/capability-pack-v1/valid-minimal.toml"); +const UNKNOWN_FIELD: &str = include_str!("fixtures/capability-pack-v1/unknown-field.toml"); +const UNKNOWN_VERSION: &str = include_str!("fixtures/capability-pack-v1/unknown-version.toml"); + +#[test] +fn strict_manifest_parser_accepts_only_schema_1_0_toml() { + let manifest = CapabilityPackManifestV1::parse(VALID).expect("valid manifest"); + assert_eq!(manifest.schema_version, CAPABILITY_PACK_SCHEMA_VERSION); + assert_eq!(manifest.pack_id, "ccp.rust-minimal"); + assert!(CapabilityPackManifestV1::parse(UNKNOWN_FIELD).is_err()); + assert!(matches!( + CapabilityPackManifestV1::parse(UNKNOWN_VERSION) + .and_then(CapabilityPackManifestV1::validate), + Err(CapabilityPackError::UnsupportedSchemaVersion(version)) if version == "2.0" + )); +} + +#[test] +fn manifest_parser_rejects_more_than_one_mebibyte_before_toml_decode() { + let oversized = "x".repeat(MAX_CAPABILITY_PACK_BYTES + 1); + assert!(matches!( + CapabilityPackManifestV1::parse(&oversized), + Err(CapabilityPackError::ManifestTooLarge { actual, maximum }) + if actual == MAX_CAPABILITY_PACK_BYTES + 1 && maximum == MAX_CAPABILITY_PACK_BYTES + )); +} +``` + +- [ ] **Step 3: Run the focused test and capture RED** + +Run: + +```bash +rtk cargo test --locked --offline --test capability_pack_contract strict_manifest_parser -- --exact +rtk cargo test --locked --offline --test capability_pack_contract manifest_parser_rejects_more_than_one_mebibyte_before_toml_decode -- --exact +``` + +Expected: compilation fails because the module and types do not exist. + +- [ ] **Step 4: Add the minimal strict input model** + +Create the module with these public top-level interfaces and private nested input types: + +```rust +pub const CAPABILITY_PACK_SCHEMA_VERSION: &str = "1.0"; +pub const MAX_CAPABILITY_PACK_BYTES: usize = 1_048_576; +const MAX_PACK_PROFILES: usize = 32; +const MAX_PACK_SOURCES: usize = 16; +const MAX_PROFILE_TOOLS: usize = 32; +const MAX_PROFILE_INPUTS: usize = 64; +const MAX_PROFILE_HOSTS: usize = 8; +const MAX_PROFILE_TARGETS: usize = 8; +const MAX_PROFILE_FEATURES: usize = 16; +const MAX_BLIND_SPOTS: usize = 32; +const MAX_LICENSE_BYTES: usize = 128; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CapabilityPackManifestV1 { + pub schema_version: String, + pub pack_id: String, + pub pack_version: String, + pub license: String, + pub description: String, + pub upstream_sources: Vec, + pub profiles: Vec, +} + +impl CapabilityPackManifestV1 { + pub fn parse(input: &str) -> Result; + pub fn load(path: &Path) -> Result; + pub fn validate(self) -> Result; +} +``` + +Every nested manifest object derives `Deserialize` and `JsonSchema` and uses `#[serde(deny_unknown_fields)]`. Task 1 may leave `validate` returning `UnsupportedSchemaVersion` first and a typed `InvalidField("profiles")` for unimplemented semantic validation; Task 2 replaces that temporary branch. `load` checks metadata size before `read_to_string` and records the path in `CapabilityPackError::Io`. + +Use exactly these nested input types so later tasks do not invent fields or enum spelling: + +```rust +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CapabilitySourceV1 { + pub id: String, + pub url: String, + pub digest: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CapabilityProfileConfigV1 { + pub id: String, + pub description: String, + pub evidence_class: CapabilityEvidenceClassV1, + pub pass_semantics: String, + pub known_blind_spots: Vec, + pub supported_hosts: Vec, + pub target_platforms: Vec, + pub required_runtime_features: Vec, + pub offline_preparation: OfflinePreparationV1, + pub tools: Vec, + #[serde(default)] + pub inputs: Vec, + pub runtime: RuntimeConfig, + #[serde(default)] + pub environment: EnvironmentConfig, + #[serde(default)] + pub caches: Vec, + pub storage: StorageConfig, + pub checks: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CapabilityToolV1 { + pub id: String, + pub version: String, + pub license: String, + pub url: String, + pub digest: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CapabilityInputProvenanceV1 { + pub id: String, + pub kind: CapabilityInputKindV1, + pub url: String, + pub digest: String, + #[serde(default)] + pub snapshot_created_at_utc: Option, + #[serde(default)] + pub max_age_seconds: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum CapabilityEvidenceClassV1 { Deterministic, ScheduleSensitive, BoundedNondeterministic } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum CapabilityInputKindV1 { Rules, TypeStubs, Corpus, AdvisoryDatabase, VulnerabilityDatabase } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum CapabilityHostPlatformV1 { MacosAarch64, LinuxArm64, LinuxAmd64, WindowsAmd64 } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum CapabilityTargetPlatformV1 { LinuxArm64, LinuxAmd64 } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum CapabilityRuntimeFeatureV1 { + LinuxUserland, + NoNetwork, + ReadOnlySource, + WritableCaches, + BoundedArtifacts, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum OfflinePreparationV1 { None, RequiredExternal } +``` + +The fixture value is `offline_preparation = "none"`; future manifests that require a separately reviewed preparation step use `"required-external"`. + +Define the complete stable error surface now: + +```rust +#[derive(Debug)] +pub enum CapabilityPackError { + Io { path: PathBuf, source: std::io::Error }, + Parse(toml::de::Error), + Json(serde_json::Error), + Receipt(ReceiptError), + Config(ConfigError), + UnsupportedSchemaVersion(String), + ManifestTooLarge { actual: usize, maximum: usize }, + InvalidField(&'static str), + TooManyItems { field: &'static str, actual: usize, maximum: usize }, + DuplicateId { field: &'static str, id: String }, + DuplicateValue(&'static str), + ShellEntrypoint(String), + UnknownProfile(String), + PackDigestMismatch, +} +``` + +Implement `Display`, `Error::source`, `From`, and `From` with deterministic text. `canonical_json` and `canonical_digest` failures map to `CapabilityPackError::Receipt`; `serde_json::to_string_pretty` schema-generation failures map to `CapabilityPackError::Json`. Do not include raw manifest contents or fixed environment values in an error. + +Add `pub mod capability_pack;` to `src/lib.rs`. Do not modify `src/main.rs`. + +- [ ] **Step 5: Run focused GREEN checks** + +Run: + +```bash +rtk cargo fmt --check +rtk cargo test --locked --offline --test capability_pack_contract strict_manifest_parser_accepts_only_schema_1_0_toml -- --exact +rtk cargo test --locked --offline --test capability_pack_contract manifest_parser_rejects_more_than_one_mebibyte_before_toml_decode -- --exact +rtk cargo clippy --locked --offline --lib --test capability_pack_contract -- -D warnings +``` + +Expected: all commands PASS; no subprocess, Docker, network, or project command is invoked. + +- [ ] **Step 6: Commit the reviewed parser slice** + +```bash +rtk git add src/capability_pack.rs src/lib.rs tests/capability_pack_contract.rs tests/fixtures/capability-pack-v1/valid-minimal.toml tests/fixtures/capability-pack-v1/unknown-field.toml tests/fixtures/capability-pack-v1/unknown-version.toml +rtk git commit -m "feat: add strict capability pack parser" +``` + +### Task 2: Pack-specific trust validation and canonical envelope + +**Files:** +- Modify: `src/capability_pack.rs` +- Modify: `tests/capability_pack_contract.rs` +- Create: `tests/fixtures/capability-pack-v1/invalid-image.toml` +- Create: `tests/fixtures/capability-pack-v1/invalid-license.toml` +- Create: `tests/fixtures/capability-pack-v1/invalid-provenance.toml` +- Create: `tests/fixtures/capability-pack-v1/invalid-path.toml` +- Create: `tests/fixtures/capability-pack-v1/shell-entrypoint.toml` +- Create: `tests/fixtures/capability-pack-v1/dependency-cycle.toml` +- Create: `tests/fixtures/capability-pack-v1/valid-minimal-reordered.toml` +- Create: `tests/fixtures/capability-pack-v1/valid-minimal.canonical.json` + +**Interfaces:** +- Consumes: Task 1 manifest types; `ConfigV1::into_plan`; `receipt::canonical_json`; `receipt::canonical_digest`; the already-`pub(crate)` `verify::parse_utc_seconds` helper. +- Produces: `CapabilityPackEnvelopeV1`, `NormalizedCapabilityPackV1`, normalized profile/provenance metadata, `canonical_bytes`, `inspection`, and stable typed validation errors. + +- [ ] **Step 1: Write the semantic rejection fixtures** + +Derive each fixture from `valid-minimal.toml`, changing only the named field: + +| Fixture | Exact mutation | Expected field/error | +|---|---|---| +| `invalid-image.toml` | remove `@sha256:<64 hex>` from `profiles.runtime.image` | wrapped `ConfigError::InvalidField("runtime.image")` | +| `invalid-license.toml` | set root license to `"Apache-2.0 OR MIT"` | `InvalidField("license")` | +| `invalid-provenance.toml` | uppercase one hex character in the tool digest | `InvalidField("profiles.tools.digest")` | +| `invalid-path.toml` | set cache mount to `"../cargo"` | wrapped `ConfigError::InvalidField("cache.mount_path")` | +| `shell-entrypoint.toml` | set check argv to `["/bin/sh", "-c", "cargo clippy"]` | `ShellEntrypoint("clippy")` | +| `dependency-cycle.toml` | add a second required check and make both checks depend on each other | wrapped `ConfigError::DependencyCycle` | + +- [ ] **Step 2: Write failing semantic, bound, and canonicalization tests** + +Add exact tests for: + +```rust +const INVALID_IMAGE: &str = include_str!("fixtures/capability-pack-v1/invalid-image.toml"); +const INVALID_LICENSE: &str = include_str!("fixtures/capability-pack-v1/invalid-license.toml"); +const INVALID_PROVENANCE: &str = include_str!("fixtures/capability-pack-v1/invalid-provenance.toml"); +const INVALID_PATH: &str = include_str!("fixtures/capability-pack-v1/invalid-path.toml"); +const SHELL_ENTRYPOINT: &str = include_str!("fixtures/capability-pack-v1/shell-entrypoint.toml"); +const DEPENDENCY_CYCLE: &str = include_str!("fixtures/capability-pack-v1/dependency-cycle.toml"); +const REORDERED_VALID: &str = include_str!("fixtures/capability-pack-v1/valid-minimal-reordered.toml"); +const PINNED_CANONICAL: &[u8] = include_bytes!("fixtures/capability-pack-v1/valid-minimal.canonical.json"); + +fn validate_fixture(source: &str) -> Result { + CapabilityPackManifestV1::parse(source)?.validate() +} + +#[test] +fn validator_rejects_untrusted_metadata_and_unsafe_execution_shape() { + assert!(matches!(validate_fixture(INVALID_LICENSE), Err(CapabilityPackError::InvalidField("license")))); + assert!(matches!(validate_fixture(INVALID_PROVENANCE), Err(CapabilityPackError::InvalidField("profiles.tools.digest")))); + assert!(matches!(validate_fixture(SHELL_ENTRYPOINT), Err(CapabilityPackError::ShellEntrypoint(id)) if id == "clippy")); + assert!(matches!(validate_fixture(INVALID_IMAGE), Err(CapabilityPackError::Config(ConfigError::InvalidField("runtime.image"))))); + assert!(matches!(validate_fixture(INVALID_PATH), Err(CapabilityPackError::Config(ConfigError::InvalidField("cache.mount_path"))))); + assert!(matches!(validate_fixture(DEPENDENCY_CYCLE), Err(CapabilityPackError::Config(ConfigError::DependencyCycle(_))))); +} + +#[test] +fn normalized_pack_is_order_independent_and_matches_pinned_canonical_bytes() { + let first = validate_fixture(VALID).expect("first pack"); + let reordered = validate_fixture(REORDERED_VALID).expect("reordered pack"); + assert_eq!(first.pack_digest, reordered.pack_digest); + assert_eq!(first.canonical_bytes().expect("canonical bytes"), PINNED_CANONICAL); +} + +#[test] +fn freshness_metadata_is_required_as_an_atomic_pair() { + assert_invalid_freshness(None, Some(86_400)); + assert_invalid_freshness(Some("2026-08-30T00:00:00Z"), None); + assert_invalid_freshness(Some("2026-02-30T00:00:00Z"), Some(86_400)); +} +``` + +Use these helpers for all in-memory semantic mutations: + +```rust +fn valid_manifest() -> CapabilityPackManifestV1 { + CapabilityPackManifestV1::parse(VALID).expect("valid input model") +} + +fn assert_invalid_freshness(created: Option<&str>, max_age_seconds: Option) { + let mut manifest = valid_manifest(); + let input = manifest.profiles[0] + .inputs + .first_mut() + .expect("valid fixture database input"); + input.snapshot_created_at_utc = created.map(str::to_owned); + input.max_age_seconds = max_age_seconds; + let expected = if created.is_some() && max_age_seconds.is_some() { + "profiles.inputs.snapshot_created_at_utc" + } else { + "profiles.inputs.freshness" + }; + assert!(matches!( + manifest.validate(), + Err(CapabilityPackError::InvalidField(field)) if field == expected + )); +} + +fn assert_invalid_field( + mutate: impl FnOnce(&mut CapabilityPackManifestV1), + expected: &'static str, +) { + let mut manifest = valid_manifest(); + mutate(&mut manifest); + assert!(matches!( + manifest.validate(), + Err(CapabilityPackError::InvalidField(field)) if field == expected + )); +} +``` + +The fixture already contains one `advisory-database` input with a valid pair: ID `rustsec-db`, URL `https://github.com/RustSec/advisory-db`, digest with 64 lowercase `d` characters, timestamp `2026-08-30T00:00:00Z`, and maximum age `604800`. + +Add table-driven mutations with these exact expected variants: + +| Mutation | Expected error | +|---|---| +| zero profiles or zero upstream sources | `InvalidField("profiles")` or `InvalidField("upstream_sources")` | +| zero tools, hosts, targets, runtime features, or blind spots in one profile | `InvalidField("profiles.tools")`, `InvalidField("profiles.supported_hosts")`, `InvalidField("profiles.target_platforms")`, `InvalidField("profiles.required_runtime_features")`, or `InvalidField("profiles.known_blind_spots")` | +| zero checks or no required check | wrapped `ConfigError::NoChecks` or `ConfigError::NoRequiredChecks` | +| 33 profiles | `TooManyItems { field: "profiles", actual: 33, maximum: 32 }` | +| 17 upstream sources | `TooManyItems { field: "upstream_sources", actual: 17, maximum: 16 }` | +| 33 tools | `TooManyItems { field: "profiles.tools", actual: 33, maximum: 32 }` | +| 65 inputs | `TooManyItems { field: "profiles.inputs", actual: 65, maximum: 64 }` | +| 9 hosts | `TooManyItems { field: "profiles.supported_hosts", actual: 9, maximum: 8 }` | +| 9 targets | `TooManyItems { field: "profiles.target_platforms", actual: 9, maximum: 8 }` | +| 17 runtime features | `TooManyItems { field: "profiles.required_runtime_features", actual: 17, maximum: 16 }` | +| 33 blind spots | `TooManyItems { field: "profiles.known_blind_spots", actual: 33, maximum: 32 }` | +| 129 checks | `Config(ConfigError::TooManyItems { field: "checks", actual: 129, maximum: 128 })` | +| 33 caches | `Config(ConfigError::TooManyItems { field: "caches", actual: 33, maximum: 32 })` | +| 65 argv parts | `Config(ConfigError::InvalidField("check.argv"))` | +| 4,097-byte root description | `InvalidField("description")` | +| invalid pack/profile/source/tool/input stable ID | `InvalidField("pack_id")`, `InvalidField("profiles.id")`, `InvalidField("upstream_sources.id")`, `InvalidField("profiles.tools.id")`, or `InvalidField("profiles.inputs.id")` | +| duplicate profile/source/tool/input IDs | `DuplicateId` with field `profiles.id`, `upstream_sources.id`, `profiles.tools.id`, or `profiles.inputs.id` and the duplicated ID | +| duplicate check/cache IDs | wrapped `ConfigError::DuplicateId` with field `check.id` or `cache.id` | +| duplicate hosts/targets/features/blind spots | `DuplicateValue` with field `profiles.supported_hosts`, `profiles.target_platforms`, `profiles.required_runtime_features`, or `profiles.known_blind_spots` | +| duplicate check dependency/artifact | wrapped `ConfigError::DuplicateValue("check.depends_on")` or `ConfigError::DuplicateValue("check.artifacts")` | +| pack versions `v1.0.0`, `1.0`, `01.0.0`, `1.0.0-alpha` | `InvalidField("pack_version")` | +| empty, control-containing, or 4,097-byte tool version | `InvalidField("profiles.tools.version")` | +| source URL with `http://`, credentials, fragment, whitespace, or control | `InvalidField("upstream_sources.url")` | +| equivalent invalid tool/input URL | `InvalidField("profiles.tools.url")` or `InvalidField("profiles.inputs.url")` | +| root license `NOASSERTION`, `NONE`, `Apache-2.0 OR MIT`, or 129 bytes | `InvalidField("license")` | +| invalid tool license | `InvalidField("profiles.tools.license")` | +| empty root/profile description or pass semantics | `InvalidField("description")`, `InvalidField("profiles.description")`, or `InvalidField("profiles.pass_semantics")` | +| `RuntimeKind::Host` | `InvalidField("profiles.runtime.kind")` | +| `network = true` | `InvalidField("profiles.runtime.network")` | +| absent pull policy or absent swap mode | `Config(ConfigError::MissingRuntimeCapabilityPolicy)` | +| TOML with the complete `[profiles.storage]` table removed | `CapabilityPackError::Parse(_)` | + +For list-size cases that require repeated stable IDs, generate unique IDs with `format!("item-{index}")` so the size error is observed before any duplicate error. For the missing-storage case, remove the exact storage table and its three key lines from `VALID` before calling `parse`; do not attempt an impossible in-memory `None` mutation because the input field is non-optional. + +`valid-minimal-reordered.toml` contains the same values as `valid-minimal.toml` but places root scalar keys, profile scalar keys, and the `supported_hosts`, `target_platforms`, `required_runtime_features`, and `known_blind_spots` arrays in a different TOML order. It must validate to the same normalized object and pack digest; it does not add or remove a semantic value. + +- [ ] **Step 3: Run the semantic suite and capture RED** + +Run: + +```bash +rtk cargo test --locked --offline --test capability_pack_contract validator_ -- --nocapture +rtk cargo test --locked --offline --test capability_pack_contract normalized_pack_is_order_independent_and_matches_pinned_canonical_bytes -- --exact +rtk cargo test --locked --offline --test capability_pack_contract freshness_metadata_is_required_as_an_atomic_pair -- --exact +``` + +Expected: FAIL because semantic normalization, envelope types, and canonical methods are absent. + +- [ ] **Step 4: Implement pack-specific validation and reuse ConfigV1 normalization** + +Use these public normalized interfaces: + +```rust +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct CapabilityPackEnvelopeV1 { + pub pack_digest: String, + pub pack: NormalizedCapabilityPackV1, + #[serde(skip)] + profile_configs: BTreeMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct NormalizedCapabilityPackV1 { + pub schema_version: String, + pub pack_id: String, + pub pack_version: String, + pub license: String, + pub description: String, + pub upstream_sources: Vec, + pub profiles: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct NormalizedCapabilityProfileV1 { + pub id: String, + pub description: String, + pub evidence_class: CapabilityEvidenceClassV1, + pub pass_semantics: String, + pub known_blind_spots: Vec, + pub supported_hosts: Vec, + pub target_platforms: Vec, + pub required_runtime_features: Vec, + pub offline_preparation: OfflinePreparationV1, + pub tools: Vec, + pub inputs: Vec, + pub runtime: NormalizedRuntime, + pub environment: NormalizedEnvironment, + pub caches: Vec, + pub storage: NormalizedStorage, + pub checks: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ValidatedProfileConfigV1 { + evidence_class: CapabilityEvidenceClassV1, + runtime: RuntimeConfig, + environment: EnvironmentConfig, + caches: Vec, + storage: StorageConfig, + checks: Vec, +} + +impl CapabilityPackEnvelopeV1 { + pub fn canonical_bytes(&self) -> Result, CapabilityPackError>; + pub fn inspection(&self) -> &NormalizedCapabilityPackV1; +} +``` + +For each profile, first validate all pack-specific fields, collections, and cross-field rules, then construct a private schema-`1.3` `ConfigV1` using: + +```rust +ConfigV1 { + schema_version: "1.3".to_owned(), + project: "capability-pack/validation".to_owned(), + runtime: profile.runtime.clone(), + receipt: ReceiptConfig { + output: ".ccp/capability-pack-validation.json".to_owned(), + freshness_seconds: 86_400, + }, + environment: profile.environment.clone(), + caches: profile.caches.clone(), + storage: Some(profile.storage.clone()), + checks: profile.checks.clone(), +} +.into_plan() +``` + +Extract the normalized runtime, environment, caches, storage, and checks into `NormalizedCapabilityProfileV1`; do not include the sentinel project or receipt in the pack digest. Retain the validated raw profile inputs privately for later binding. Sort profiles, sources, tools, input provenance, host platforms, target platforms, runtime features, and blind spots deterministically. Reject duplicates before sorting. + +The pack-specific validators use these exact rules: + +- stable IDs reuse `crate::config::validate_identifier`; +- `description`, `pass_semantics`, and blind spots use the existing 4,096-byte/control-free text bound; +- pack semantic-version components contain ASCII decimal digits, have no leading zero unless the component is exactly `0`, and parse as `u64`; tool versions use only the bounded text rule; +- HTTPS URLs start with `https://`, have a non-empty authority, contain no `@` in the authority, `#`, whitespace, or control character, and are at most 4,096 bytes; +- digests match lowercase `sha256:` plus exactly 64 lowercase hexadecimal characters; +- license identifiers use the Global Constraints rule and reject the two reserved values; +- database kinds require the complete valid freshness pair; all other kinds reject either freshness field; call existing `crate::verify::parse_utc_seconds` and map `None` to `InvalidField("profiles.inputs.snapshot_created_at_utc")`; +- the pack has at least one upstream source and one profile; every profile has at least one tool, one supported host, one target, one runtime feature, one blind spot, one check, and one required check; +- `NoNetwork`, `ReadOnlySource`, and `LinuxUserland` are mandatory runtime features; `WritableCaches` is present if and only if caches are non-empty; `BoundedArtifacts` is present if and only if checks declare artifacts; +- only `DockerCompatible` is accepted; runtime network must be false; pull policy and swap mode are validated by schema `1.3` conversion; +- `RequiredExternal` is metadata only and never performs preparation during validation or expansion. + +Reject shell entrypoints by lowercase basename for `sh`, `bash`, `dash`, `zsh`, `ksh`, `fish`, `csh`, `tcsh`, `cmd`, `cmd.exe`, `powershell`, `powershell.exe`, `pwsh`, and `pwsh.exe`; also reject `/usr/bin/env` or `env` when its next non-option token is one of those names. This is a fail-closed entrypoint rule, not a claim to inspect the behavior of arbitrary binaries. + +Compute `pack_digest = canonical_digest(&normalized_pack)`. `canonical_bytes` first recomputes that digest and returns `PackDigestMismatch` on mismatch, then canonicalizes the public envelope. Do not include private raw fixed-environment values; the normalized environment already binds their canonical digests. + +- [ ] **Step 5: Run GREEN semantic and canonical tests** + +Run: + +```bash +rtk cargo fmt --check +rtk cargo test --locked --offline --test capability_pack_contract +rtk cargo clippy --locked --offline --lib --test capability_pack_contract -- -D warnings +``` + +Expected: all Task 1-2 tests PASS and the golden canonical bytes match exactly. + +- [ ] **Step 6: Commit the reviewed trust contract** + +```bash +rtk git add src/capability_pack.rs tests/capability_pack_contract.rs tests/fixtures/capability-pack-v1 +rtk git commit -m "feat: validate capability pack trust metadata" +``` + +### Task 3: Explicit inert profile expansion + +**Files:** +- Modify: `src/capability_pack.rs` +- Modify: `tests/capability_pack_contract.rs` +- Create: `tests/fixtures/capability-pack-v1/valid-minimal.strict-clippy.expansion.json` + +**Interfaces:** +- Consumes: Task 2 validated private profile material and existing `ConfigV1::into_plan`. +- Produces: `CapabilityPackBindingV1`, `CapabilityPackExpansionV1`, `CapabilityPackEnvelopeV1::expand`, `CapabilityPackExpansionV1::canonical_bytes`, and explicit unknown-profile failure. + +- [ ] **Step 1: Write failing binding, expansion, and inertness tests** + +Use these exact public signatures in the tests: + +```rust +use std::path::PathBuf; +use std::sync::atomic::{AtomicU64, Ordering}; + +const PINNED_EXPANSION: &[u8] = include_bytes!( + "fixtures/capability-pack-v1/valid-minimal.strict-clippy.expansion.json" +); +static TEST_SEQUENCE: AtomicU64 = AtomicU64::new(0); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CapabilityPackBindingV1 { + pub project: String, + pub profile_id: String, + pub receipt: ReceiptConfig, +} + +fn valid_binding() -> CapabilityPackBindingV1 { + CapabilityPackBindingV1 { + project: "example/project".to_owned(), + profile_id: "strict-clippy".to_owned(), + receipt: ReceiptConfig { + output: ".ccp/receipt.json".to_owned(), + freshness_seconds: 300, + }, + } +} + +fn binding_for(profile_id: &str) -> CapabilityPackBindingV1 { + CapabilityPackBindingV1 { profile_id: profile_id.to_owned(), ..valid_binding() } +} + +fn binding_for_project(project: &str) -> CapabilityPackBindingV1 { + CapabilityPackBindingV1 { project: project.to_owned(), ..valid_binding() } +} + +fn unique_test_root(label: &str) -> PathBuf { + std::env::temp_dir().join(format!( + "ccp-{label}-{}-{}", + std::process::id(), + TEST_SEQUENCE.fetch_add(1, Ordering::Relaxed), + )) +} + +fn valid_manifest_with_argv(argv: Vec) -> String { + let original = "argv = [\"cargo\", \"clippy\", \"--locked\", \"--offline\", \"--all-targets\", \"--all-features\", \"--\", \"-D\", \"warnings\"]"; + let replacement = format!("argv = {}", serde_json::to_string(&argv).expect("argv JSON")); + assert!(VALID.contains(original)); + VALID.replacen(original, &replacement, 1) +} + +#[test] +fn one_explicit_profile_expands_to_one_existing_plan_envelope() { + let pack = validate_fixture(VALID).expect("pack"); + let expansion = pack.expand(CapabilityPackBindingV1 { + project: "example/project".to_owned(), + profile_id: "strict-clippy".to_owned(), + receipt: ReceiptConfig { + output: ".ccp/receipt.json".to_owned(), + freshness_seconds: 300, + }, + }).expect("expansion"); + assert_eq!(expansion.pack_digest, pack.pack_digest); + assert_eq!(expansion.profile_id, "strict-clippy"); + assert_eq!(expansion.execution_plan.plan.project, "example/project"); + assert_eq!(expansion.execution_plan.plan.schema_version, "1.3"); + assert_eq!( + expansion.canonical_bytes().expect("canonical expansion"), + PINNED_EXPANSION + ); +} + +#[test] +fn inspection_and_expansion_never_execute_declared_argv() { + let root = unique_test_root("pack-inertness"); + std::fs::create_dir_all(&root).expect("create owned test root"); + let marker = root.join("must-not-exist"); + let source = valid_manifest_with_argv(vec![ + "/usr/bin/touch".to_owned(), + marker.display().to_string(), + ]); + let pack = CapabilityPackManifestV1::parse(&source) + .and_then(CapabilityPackManifestV1::validate) + .expect("inert pack"); + let _inspection = pack.inspection(); + let _expansion = pack.expand(valid_binding()).expect("inert expansion"); + assert!(!marker.exists()); + std::fs::remove_dir(&root).expect("remove empty owned test root"); +} + +#[test] +fn expansion_rejects_unknown_profile_and_invalid_repository_binding() { + let pack = validate_fixture(VALID).expect("pack"); + assert!(matches!(pack.expand(binding_for("missing")), Err(CapabilityPackError::UnknownProfile(id)) if id == "missing")); + assert!(matches!(pack.expand(binding_for_project("not-a-repository")), Err(CapabilityPackError::Config(ConfigError::InvalidField("project"))))); +} +``` + +- [ ] **Step 2: Run expansion tests and capture RED** + +Run: + +```bash +rtk cargo test --locked --offline --test capability_pack_contract one_explicit_profile_expands_to_one_existing_plan_envelope -- --exact +rtk cargo test --locked --offline --test capability_pack_contract inspection_and_expansion_never_execute_declared_argv -- --exact +rtk cargo test --locked --offline --test capability_pack_contract expansion_rejects_unknown_profile_and_invalid_repository_binding -- --exact +``` + +Expected: compilation fails because binding and expansion interfaces do not exist. + +- [ ] **Step 3: Implement the minimal explicit expansion adapter** + +Add: + +```rust +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct CapabilityPackExpansionV1 { + pub schema_version: String, + pub pack_id: String, + pub pack_version: String, + pub pack_digest: String, + pub profile_id: String, + pub evidence_class: CapabilityEvidenceClassV1, + pub execution_plan: ExecutionPlanEnvelopeV1, +} + +impl CapabilityPackEnvelopeV1 { + pub fn expand( + &self, + binding: CapabilityPackBindingV1, + ) -> Result; +} + +impl CapabilityPackExpansionV1 { + pub fn canonical_bytes(&self) -> Result, CapabilityPackError>; +} +``` + +`expand` must select exactly one validated profile by exact ID and reconstruct schema-`1.3` `ConfigV1` with only the caller's `project` and `receipt` substituted. It then calls `ConfigV1::into_plan`; it does not construct `ExecutionPlanV1` directly. This preserves all current configuration validation and plan-digest semantics. The expansion wrapper binds the pack identity separately; it does not modify the nested execution plan or any receipt contract. + +`canonical_bytes` verifies both the pack digest format and `execution_plan.canonical_bytes()` before returning canonical JSON for the wrapper. It must not execute, probe, inspect the filesystem beyond an explicit prior `load`, resolve images, contact a registry, or write output. + +- [ ] **Step 4: Run GREEN expansion and compatibility checks** + +Run: + +```bash +rtk cargo fmt --check +rtk cargo test --locked --offline --test capability_pack_contract +rtk cargo test --locked --offline --test compatibility_baseline +rtk cargo test --locked --offline --test plan_cli +rtk cargo test --locked --offline --test receipt_contract +rtk cargo test --locked --offline --test verification_contract +``` + +Expected: all tests PASS; compatibility fixtures remain unchanged; the marker does not exist. + +- [ ] **Step 5: Commit the reviewed expansion slice** + +```bash +rtk git add src/capability_pack.rs tests/capability_pack_contract.rs tests/fixtures/capability-pack-v1/valid-minimal.strict-clippy.expansion.json +rtk git commit -m "feat: expand inert capability pack profiles" +``` + +### Task 4: Pinned schema and operator documentation + +**Files:** +- Modify: `src/capability_pack.rs` +- Modify: `tests/capability_pack_contract.rs` +- Create: `schema/capability-pack-v1.schema.json` +- Create: `docs/CAPABILITY_PACKS.md` +- Modify: `CHANGELOG.md` + +**Interfaces:** +- Consumes: stable Task 1-3 manifest and expansion types. +- Produces: `capability_pack_schema_json()` and documentation that distinguishes inspection, expansion, execution, evidence integrity, freshness, and qualification. + +- [ ] **Step 1: Write the failing pinned-schema test** + +```rust +const PINNED_SCHEMA: &str = include_str!("../schema/capability-pack-v1.schema.json"); + +#[test] +fn generated_capability_pack_schema_matches_pinned_bytes() { + assert_eq!( + capability_pack_schema_json().expect("capability pack schema"), + PINNED_SCHEMA + ); +} +``` + +Run: + +```bash +rtk cargo test --locked --offline --test capability_pack_contract generated_capability_pack_schema_matches_pinned_bytes -- --exact +``` + +Expected: FAIL because the function and pinned schema are absent. + +- [ ] **Step 2: Add schema generation and pin exact bytes** + +Implement: + +```rust +pub fn capability_pack_schema_json() -> Result { + let schema = schema_for!(CapabilityPackManifestV1); + serde_json::to_string_pretty(&schema).map_err(CapabilityPackError::Json) +} +``` + +Generate the schema from the exact function output, not by hand, and add it at `schema/capability-pack-v1.schema.json`. The test is the drift gate. + +- [ ] **Step 3: Document the bounded public contract** + +`docs/CAPABILITY_PACKS.md` must contain these explicit sections and claims: + +1. `Status: schema and inert library inspection/expansion only; no official pack execution in M2`. +2. TOML schema `1.0` fields and exact bounds. +3. Pack identity tuple and immutable-version expectation. +4. Digest-pinned image and provenance requirements. +5. SPDX-style identifier syntax versus legal review. +6. Integrity versus database/rules freshness. +7. Explicit profile binding and one-profile-to-one-plan expansion. +8. Why expansion is not execution and why no CLI command exists yet. +9. Network disabled, preparation external, source read-only, outputs explicit. +10. Deterministic, schedule-sensitive, and bounded-nondeterministic evidence classes. +11. Known non-goals: no workflow DSL, package manager, tool installer, report interpretation, receipt extension, publication, or policy override. +12. M3 entry criteria for the `rust-deep` reference pack. + +Add an `Unreleased` changelog bullet stating that this is a pre-1.0 additive Rust library contract and does not change existing CLI or receipt schemas. Do not claim that users can execute an official pack yet. + +- [ ] **Step 4: Run schema, docs, and compatibility checks** + +Run: + +```bash +rtk cargo fmt --check +rtk cargo test --locked --offline --test capability_pack_contract +rtk cargo test --locked --offline --test compatibility_baseline +rtk git diff --check +rtk rg -n "(TBD|TODO|FIXME|/Users/|container ID|secret value)" docs/CAPABILITY_PACKS.md schema/capability-pack-v1.schema.json +``` + +Expected: tests and diff check PASS; the bounded documentation scan returns no matches. + +- [ ] **Step 5: Commit the reviewed schema and docs** + +```bash +rtk git add src/capability_pack.rs tests/capability_pack_contract.rs schema/capability-pack-v1.schema.json docs/CAPABILITY_PACKS.md CHANGELOG.md +rtk git commit -m "docs: define capability pack contract" +``` + +### Task 5: M2 exact-head qualification and durable closure + +**Files:** +- Modify: `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md` +- Create: `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/m2-manifest.json` + +**Interfaces:** +- Consumes: accepted Task 1-4 commits and their review reports. +- Produces: terminal local M2 evidence, a deterministic file-hash manifest, explicit residual boundaries, and the exact M3 gate. + +- [ ] **Step 1: Run narrow-to-broad offline qualification** + +Run in this order: + +```bash +rtk cargo fmt --check +rtk cargo clippy --locked --offline --workspace --all-targets --all-features -- -D warnings +rtk cargo test --locked --offline --test capability_pack_contract +rtk cargo test --locked --offline --test compatibility_baseline +rtk cargo test --locked --offline --test plan_cli +rtk cargo test --locked --offline --test matrix_contract +rtk cargo test --locked --offline --test receipt_contract +rtk cargo test --locked --offline --test verification_contract +rtk cargo test --locked --offline --workspace --all-targets --all-features +``` + +If the sandbox denies an otherwise ordinary filesystem operation, record the exact denial and repeat only the same command with the narrow host permission allowed by the operator contract. Do not reinterpret a sandbox denial as a product failure or PASS. + +- [ ] **Step 2: Prove no compatibility fixture or execution surface changed** + +Run: + +```bash +rtk cargo test --locked --offline --test compatibility_baseline manifest_matches_the_exact_compatibility_corpus -- --exact +rtk git diff 5fed7c443504969e62980141048f9279f9fa1dfe -- src/main.rs src/config.rs src/matrix.rs src/receipt.rs src/verify.rs schema/config-v1.schema.json schema/config-v2.schema.json schema/receipt-v1.schema.json schema/receipt-v2.schema.json schema/policy-v1.schema.json schema/policy-v1_1.schema.json schema/policy-v2.schema.json +``` + +Expected: manifest test PASS and the scoped diff is empty. Any non-empty scoped diff blocks M2 closure. + +- [ ] **Step 3: Write the failing deterministic M2 manifest test** + +Add `m2_manifest_matches_exact_file_bytes` to `tests/capability_pack_contract.rs`. The test parses `m2-manifest.json`, requires schema version `1.0`, requires the exact sorted file set below, rejects duplicates or additional entries, and recomputes each file's byte length and lowercase SHA-256 with the existing `sha2` dev-visible dependency. + +Run: + +```bash +rtk cargo test --locked --offline --test capability_pack_contract m2_manifest_matches_exact_file_bytes -- --exact +``` + +Expected: FAIL because `m2-manifest.json` does not exist yet. + +- [ ] **Step 4: Create the deterministic M2 hash manifest** + +`m2-manifest.json` uses schema version `1.0`, the exact branch HEAD before the closure commit, and sorted entries for: + +```text +src/capability_pack.rs +src/lib.rs +tests/capability_pack_contract.rs +tests/fixtures/capability-pack-v1/dependency-cycle.toml +tests/fixtures/capability-pack-v1/invalid-image.toml +tests/fixtures/capability-pack-v1/invalid-license.toml +tests/fixtures/capability-pack-v1/invalid-path.toml +tests/fixtures/capability-pack-v1/invalid-provenance.toml +tests/fixtures/capability-pack-v1/shell-entrypoint.toml +tests/fixtures/capability-pack-v1/unknown-field.toml +tests/fixtures/capability-pack-v1/unknown-version.toml +tests/fixtures/capability-pack-v1/valid-minimal.canonical.json +tests/fixtures/capability-pack-v1/valid-minimal-reordered.toml +tests/fixtures/capability-pack-v1/valid-minimal.strict-clippy.expansion.json +tests/fixtures/capability-pack-v1/valid-minimal.toml +schema/capability-pack-v1.schema.json +docs/CAPABILITY_PACKS.md +CHANGELOG.md +``` + +Each entry contains `path`, `bytes`, and `sha256:<64 lowercase hex>`. A focused test in `tests/capability_pack_contract.rs` reads the manifest, rejects missing/extra/duplicate/unsorted entries, recomputes byte lengths and hashes, and verifies the exact file set above. + +Run the same focused test again. Expected: PASS. + +- [ ] **Step 5: Update the durable checkpoint without overstating evidence** + +Record exact commits, local test counts, review verdicts, current HEAD, clean/dirty state, and these residual facts: + +- M2 provides inert library validation and expansion only. +- No official pack, CLI entry point, tool/image qualification, Docker execution, hosted exact-head result, push, PR update, merge, stable installation, tag, or release is implied. +- M3 must first review the `rust-deep` tool/image/license matrix and design the smallest user entry point without weakening M0 compatibility guarantees. + +- [ ] **Step 6: Run independent dual review** + +Dispatch one spec-compliance review and one code-quality/security review over the complete M2 range. Both must inspect the task reports and review package. Critical or Important findings enter the SDD fix loop; Minor findings are recorded for final branch review. + +- [ ] **Step 7: Commit the closure evidence** + +```bash +rtk git add tests/capability_pack_contract.rs docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/m2-manifest.json docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md +rtk git commit -m "docs: close capability pack contract milestone" +``` + +- [ ] **Step 8: Stop at the next exact external gate** + +Report the final local HEAD, worktree status, complete verification evidence, all reviewer findings/rulings, and the exact non-force push/hosted-CI authorization needed. Do not push, mutate a PR, run CCP, merge, install, tag, or release without that exact authorization. + +## Plan Self-Review Checklist + +- [ ] Every M2 outcome and exit-evidence requirement in the canonical spec maps to a task above. +- [ ] No task changes existing CLI, receipt, policy, configuration, matrix, or verification bytes. +- [ ] Parser, semantic validation, canonicalization, expansion, schema, inertness, docs, compatibility, and durable closure each have an explicit RED/GREEN or verification step. +- [ ] Every type and method consumed by a later task is produced by an earlier task with the same name and signature. +- [ ] No step contains a placeholder, open-ended implementation instruction, or unbounded research task. +- [ ] M3/M4 tool execution, report interpretation, and release work remain outside this plan. From 1da293a7a1f0e78440f70afe7e2356c56a608535 Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 14:37:46 +0200 Subject: [PATCH 16/17] docs: checkpoint capability pack planning --- .../progress.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md index 6e6bd82..7e7aef5 100644 --- a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md @@ -4,7 +4,7 @@ - Predecessor: `17e069a7eb3bcc6596c93bb6432984eba8472208` - Branch: `codex/capability-packs-clean-architecture-delivery-v1` - Specification commit: `5ef9707930f7095a2f57bc3e38e53bfeac06aaf2` -- Current milestone: M1 private run application seam (closed) +- Current milestone: M2 Capability Pack contract plan (ready; implementation gated by hosted CI) - Completed evidence: design review READY; `git diff --check` PASS; M0 compatibility manifest and downstream facade checks independently approved at `9e689c7f04e3d8c0479c74d91669c170a4c66e52` - Terminal M0 verification: `rtk cargo fmt --check` PASS; focused manifest test PASS; full compatibility baseline PASS (9 passed, 1 ignored); `rtk git diff --check` PASS; bounded privacy scan PASS (zero matches) - M0 status: terminally closed; Spec PASS and Task quality Approved @@ -12,7 +12,12 @@ - M1 verification: `rtk cargo fmt --check` PASS; strict Clippy PASS; compatibility_baseline 9 passed/1 ignored; plan_cli 11 passed; verify_cli 6 passed; receipt_contract 10 passed; matrix_contract 16 passed; verification_contract 20 passed/1 ignored; host full suite 467 passed/5 ignored/27 suites after the sandbox permission failure was repeated with narrow host permission; manifest test 1 passed/9 filtered; privacy scan zero matches; clean status and diff check PASS - M1 reviews: specification compliance PASS; code quality/architecture APPROVED; no Critical, Important, or Minor findings - M0 fixtures unchanged except the reviewed one-line Clippy-equivalent test and its single digest update -- Unproven: remaining hosted CI, push, PR, publication, and release gates; M2 pack contract and reference packs +- M1 closure commit: `b6318ea9837b76a6594258b0dc9f5f66967bd688` +- M2 implementation plan: `docs/superpowers/plans/2026-08-30-m2-capability-pack-contract.md` +- M2 plan commit: `a1c922d4f47fd06e74a336b89a19aecc703908bd` +- M2 plan decision: TOML-only schema `1.0`; library-first inert validation/inspection; explicit one-profile binding into the existing schema-`1.3` plan; separate pack and plan digests; no CLI, receipt, policy, execution, or publication change in M2 +- M2 plan review: initial NOT_READY findings for implicit types/helpers and missing error mappings were corrected; final Luna re-review READY with no Critical or Important findings +- Unproven: hosted CI for the exact M0-M1-plus-plan head, push, draft PR, publication, and release gates; all M2 implementation; reference packs - Heavy processes: none - External mutations: none -- Next action: M2 planning/execution +- Next action: preserve this checkpoint in a local commit, then obtain an exact bounded authorization to fetch `origin`, push the unchanged branch non-forced, open a draft PR, and require terminal hosted CI before dispatching M2 Task 1 From a1bae40c4536c3b4051e712b267dd8a28c06d87f Mon Sep 17 00:00:00 2001 From: Marco Porcellato Date: Sun, 30 Aug 2026 14:45:08 +0200 Subject: [PATCH 17/17] docs: preserve capability packs restart checkpoint --- .../progress.md | 3 +- .../CCP_CAPABILITY_PACKS_RESTART_HANDOFF.md | 174 ++++++++++++++++++ .../CCP_CAPABILITY_PACKS_RESUME_PROMPT.md | 38 ++++ 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/restart/2026-08-30/CCP_CAPABILITY_PACKS_RESTART_HANDOFF.md create mode 100644 docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/restart/2026-08-30/CCP_CAPABILITY_PACKS_RESUME_PROMPT.md diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md index 7e7aef5..8a6c882 100644 --- a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md @@ -20,4 +20,5 @@ - Unproven: hosted CI for the exact M0-M1-plus-plan head, push, draft PR, publication, and release gates; all M2 implementation; reference packs - Heavy processes: none - External mutations: none -- Next action: preserve this checkpoint in a local commit, then obtain an exact bounded authorization to fetch `origin`, push the unchanged branch non-forced, open a draft PR, and require terminal hosted CI before dispatching M2 Task 1 +- Restart checkpoint: requested 2026-08-30; tracked handoff and resume prompt added under `restart/2026-08-30/`; the final exact branch and external archive hashes are recorded in the persistent `RECOVERY_MANIFEST.md` +- Next action after restart: complete the handoff's read-only audit, then obtain an exact bounded authorization to fetch `origin`, push the unchanged branch non-forced, open a draft PR, and require terminal hosted CI before dispatching M2 Task 1 diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/restart/2026-08-30/CCP_CAPABILITY_PACKS_RESTART_HANDOFF.md b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/restart/2026-08-30/CCP_CAPABILITY_PACKS_RESTART_HANDOFF.md new file mode 100644 index 0000000..491afe2 --- /dev/null +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/restart/2026-08-30/CCP_CAPABILITY_PACKS_RESTART_HANDOFF.md @@ -0,0 +1,174 @@ +# Capability Packs restart handoff — 2026-08-30 + +This handoff preserves the local Clean Architecture and Capability Packs +programme before a macOS restart. It is a checkpoint, not a qualification +receipt and not authorization to publish or execute heavy work. + +## Safe resume point + +- Persistent repository: + `/Users/marco1/Documents/CODICE con VS CODE/commit-ci-preflight` +- Disposable delivery worktree at checkpoint: + `/private/tmp/ccp-capability-packs-clean-architecture-delivery-v1` +- Durable local branch: + `codex/capability-packs-clean-architecture-delivery-v1` +- Pre-handoff programme HEAD: + `1da293a7a1f0e78440f70afe7e2356c56a608535` +- Pre-handoff tree: + `54368e0190214f00304f3040004842af6931b9c6` +- Base and last fetched `origin/main`: + `5fed7c443504969e62980141048f9279f9fa1dfe` +- Remote programme branch: absent in the last local ref inventory; not fetched + or reverified during this restart checkpoint. +- Pull request: none created for this programme. +- Working tree before adding this handoff: clean. + +The final restart-checkpoint commit and bundle SHA-256 are recorded in the +persistent external `RECOVERY_MANIFEST.md` next to the bundle. The branch must +contain the pre-handoff programme HEAD above as an ancestor. + +## Canonical tracked sources + +Read these completely before deciding or implementing anything: + +1. `/Users/marco1/.codex/AGENTS.md` +2. `/Users/marco1/.codex/CCP_USAGE.md` +3. `docs/superpowers/specs/2026-08-30-capability-packs-clean-architecture-design.md` +4. `docs/superpowers/plans/2026-08-30-m2-capability-pack-contract.md` +5. `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/progress.md` +6. `docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/goal.txt` +7. this handoff. + +## Completed and terminally verified + +### M0 compatibility baseline + +- M0 is locally closed and independently approved. +- The hash-manifested compatibility corpus and downstream public-facade + compile fixture are tracked. +- Terminal evidence recorded in the programme progress file includes format, + manifest, compatibility, diff, and privacy checks. + +### M1 private run application seam + +- M1 closure commit: + `b6318ea9837b76a6594258b0dc9f5f66967bd688`. +- Existing public wrappers route through the private dependency seam without + changing the protected public facade. +- Recorded terminal local evidence: strict Clippy PASS; six compatibility + contract targets PASS; full host suite 467 passed and 5 ignored across 27 + suites; specification and quality reviews approved with no findings. +- Hosted CI, push, and PR remain unproven. + +### M2 plan + +- Detailed TDD plan commit: + `a1c922d4f47fd06e74a336b89a19aecc703908bd`. +- Luna's first review found implicit type/helper/error contracts. They were + corrected; final bounded review verdict was READY with no Critical or + Important findings. +- M2 is deliberately library-first: strict bounded TOML, inert validation and + inspection, explicit single-profile expansion into existing schema-`1.3` + plan primitives, and separate pack/plan digests. +- M2 must not change current CLI, receipt, policy, configuration, matrix, or + verification bytes. + +## Saved but not yet qualified + +- All M2 production implementation is NOT_STARTED and NOT_QUALIFIED. +- The `rust-deep` and `secure-repository` packs are planned only. +- No official pack CLI, tool/image qualification, Docker execution, receipt + extension, stable installation, tag, package, or release exists from this + programme. +- The programme branch has not been pushed and has no hosted exact-head result. + +## Ignored local evidence preserved separately + +The following ignored SDD directories were approximately 272 KiB total and +were copied into the persistent restart archive because the disposable +worktree may disappear: + +- `.superpowers/sdd/2026-08-30-m0-compatibility-baseline/` +- `.superpowers/sdd/2026-08-30-m1-run-application-seam/` + +They contain briefs, implementer reports, review packages, and ledgers. They +are supporting process evidence. The tracked plans, code, tests, manifests, +and programme progress remain authoritative. + +The generated ignored directory +`tests/fixtures/public-api-compat/target/` was approximately 451 MiB and was +not archived. It is rebuildable Cargo output, excluded from evidence, and not +needed to resume. + +## Operational state before restart + +- Installed executable: + `/Users/marco1/.cargo/bin/commit-ci-preflight` +- Installed SHA-256: + `c8021e2322e172686c0a0c07d2b0260eafb5812d085d2306dbbde3fe4e964bd4` +- Installed version: `commit-ci-preflight 0.1.0` +- Admission: `active=false`, queue `0`, slot free. +- Resource policy: `macos-v4`, decision `deny`; available memory 42%, + reclaimable uncompressed memory 7,969,243,136 bytes, swap used + 10,252,252,610 bytes. The deny is expected to be re-sampled after reboot. +- Recovery: all listed journals were terminal except the preserved historical + `4911b8ac9cc284b5bbd0c9160747e8c1cebc97c572ef20775e98480092c87665`, + which remains `operator_required`. Do not modify or reinterpret it. +- Docker context: `orbstack`; no workload was started for this checkpoint. +- Spark restart review: NOT_RUN because the GPT-5.3 Codex Spark usage limit was + exhausted. +- Luna Git audit: branch and committed work recoverable; it identified the + ignored SDD ledgers as the missing durable material, which this checkpoint + archives separately. + +## Exact post-restart audit + +1. Read every canonical source listed above and the external + `RECOVERY_MANIFEST.md`. +2. Verify SHA-256 for the Git bundle, SDD archive, copied handoff, copied plan, + copied specification, copied progress file, and copied prompt. +3. Run `git bundle verify` on the persistent bundle. +4. Inspect the persistent repository with read-only Git commands: local branch + ref, exact HEAD/tree, `git status`, worktree registrations, remotes, and + last fetched `origin/main`. Do not prune, repair, delete, reset, clean, + stash, or overwrite anything. +5. If the `/private/tmp` worktree still exists, use it only if it is clean and + exactly at the recovery-manifest HEAD. If it is missing or mismatched, do + not mutate stale worktree registration automatically. Propose either a new + local clone from the verified bundle or a separately authorized worktree + repair. +6. Reverify absolute CCP path, complete executable SHA-256, and version. +7. Read-only check `admission status --json`, `resource status --json`, + `recover status --json`, Docker context, running containers, and relevant + processes. Preserve journal `4911b8ac...` exactly. +8. Compare live GitHub `origin/main`, any programme branch, PR, and hosted CI + only after a separately authorized fetch/API read. +9. Return Facts / Unknowns / proposed action / GO or NO-GO before mutation. + +## Next programme gate + +The next intended external gate remains: + +1. read-only fetch of `origin`; +2. proceed only if the reviewed base is unchanged or explicitly reconciled; +3. non-force push of the exact programme branch; +4. open a draft PR; +5. require terminal hosted `Rust CI` on the exact head; +6. only then start M2 Task 1 locally through Subagent-Driven Development. + +After restart, all exact hashes must be refreshed before this authorization is +requested or used. + +## Boundaries that survive the restart + +- The restart handoff is not permission to implement M2 or mutate GitHub. +- Do not run CCP heavy work for this public repository absent a separately + authorized non-economic exception. +- Do not run Docker workloads, build, full tests, push, create/mutate a PR, + merge, install, tag, release, publish, prune worktrees, or mutate recovery + state during the initial audit. +- Preserve the divergent primary checkout and all user work. +- Preserve existing CLI/receipt/policy/plan/schema/public-facade compatibility. +- Live exact-path, exact-hash, Git, CCP, Docker, and GitHub evidence outranks + this checkpoint when they differ. + diff --git a/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/restart/2026-08-30/CCP_CAPABILITY_PACKS_RESUME_PROMPT.md b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/restart/2026-08-30/CCP_CAPABILITY_PACKS_RESUME_PROMPT.md new file mode 100644 index 0000000..44dcac5 --- /dev/null +++ b/docs/superpowers/programmes/2026-08-30-capability-packs-clean-architecture/restart/2026-08-30/CCP_CAPABILITY_PACKS_RESUME_PROMPT.md @@ -0,0 +1,38 @@ +# Prompt da incollare in Codex dopo il riavvio + +Riprendi il programma Clean Architecture e Capability Packs di +Commit-CI-Preflight in modalità evidence-first. + +Prima di qualsiasi azione, leggi integralmente: + +1. `/Users/marco1/.codex/AGENTS.md` +2. `/Users/marco1/.codex/CCP_USAGE.md` +3. `/Users/marco1/.codex/handoffs/commit-ci-preflight/2026-08-30-capability-packs-restart/RECOVERY_MANIFEST.md` +4. `/Users/marco1/.codex/handoffs/commit-ci-preflight/2026-08-30-capability-packs-restart/CCP_CAPABILITY_PACKS_RESTART_HANDOFF.md` +5. la specifica, il piano M2, il progress e il goal copiati nella stessa directory. + +Usa Superpowers. Delega a GPT-5.6 Luna o Codex Spark soltanto inventari, +documentazione, test deterministici e review bounded; conserva centralmente +architettura, sicurezza, integrazione, qualificazione, release e decisione +finale. Tutti i comandi shell devono iniziare con `rtk`. + +Esegui esclusivamente l'audit read-only post-riavvio descritto nel handoff: + +- verifica ogni SHA-256 nel manifest e `git bundle verify`; +- verifica branch/ref/HEAD/tree/status/remoti e registrazioni worktree senza + reset, stash, clean, prune, repair o cancellazioni; +- non affidarti a `/private/tmp`: se il vecchio worktree è assente o diverge, + proponi una clone locale dal bundle verificato e fermati prima di crearla; +- verifica percorso, SHA-256 e versione del binario CCP stabile; +- verifica admission, risorse, recovery, Docker e processi soltanto read-only; +- preserva integralmente il journal `4911b8ac...`; +- non fare fetch o interrogazioni GitHub senza nuova autorizzazione. + +Poi restituisci Facts / Unknowns / proposta / GO o NO-GO e fermati al gate +esterno. Non avviare M2, build, test completi, CCP heavy, Docker workload, +cleanup, push, PR, merge, installazione, tag o release. + +Se tutti gli anchor locali sono integri, il passo successivo da autorizzare è +il fetch read-only, seguito solo su base riconciliata dal push non-forzato della +branch, draft PR e hosted `Rust CI` exact-head. M2 Task 1 può iniziare soltanto +dopo il successo hosted esatto.