diff --git a/.github/workflows/proofpath-capability-manifest.yml b/.github/workflows/proofpath-capability-manifest.yml new file mode 100644 index 0000000..ac7129b --- /dev/null +++ b/.github/workflows/proofpath-capability-manifest.yml @@ -0,0 +1,55 @@ +name: ProofPath capability manifest + +on: + pull_request: + paths: + - 'governance/capability-manifest.v0.1.json' + - 'governance/fcrp-self-005.json' + - 'docs/CAPABILITY_PROMOTION_CONTRACT.md' + - 'scripts/validate_capability_manifest.py' + - 'tests/test_capability_manifest.py' + - '.github/workflows/proofpath-capability-manifest.yml' + push: + branches: [main] + paths: + - 'governance/capability-manifest.v0.1.json' + - 'governance/fcrp-self-005.json' + - 'docs/CAPABILITY_PROMOTION_CONTRACT.md' + - 'scripts/validate_capability_manifest.py' + - 'tests/test_capability_manifest.py' + - '.github/workflows/proofpath-capability-manifest.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + capability-canonicality: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Validate capability manifest + run: python3 scripts/validate_capability_manifest.py governance/capability-manifest.v0.1.json + + - name: Run canonicality regression tests + run: python3 -m unittest discover -s tests -p 'test_capability_manifest.py' -v + + - name: Verify FCRP-SELF-005 is bound to the manifest + run: | + python3 - <<'PY' + import json + from pathlib import Path + case = json.loads(Path('governance/fcrp-self-005.json').read_text()) + manifest = Path('governance/capability-manifest.v0.1.json').read_text() + assert case['caseId'] == 'FCRP-SELF-005' + assert case['divergence']['firstMeaningfulDivergence'] == 'N1' + assert case['divergence']['selectedRefactorPoint'] == 'N4' + assert case['expectedProtocolDecision'] == 'PASS' + assert 'proofpath.poci.contract.v0.1' in manifest + assert 'proofpath.scig.v0.1' in manifest + print('FCRP_SELF_005_BOUND') + PY diff --git a/docs/CAPABILITY_PROMOTION_CONTRACT.md b/docs/CAPABILITY_PROMOTION_CONTRACT.md new file mode 100644 index 0000000..dc30e37 --- /dev/null +++ b/docs/CAPABILITY_PROMOTION_CONTRACT.md @@ -0,0 +1,142 @@ +# ProofPath Capability Promotion Contract v0.1 + +## Purpose + +ProofPath has accumulated strong verified capabilities through stacked pull requests. The engineering problem is no longer lack of implementation; it is **canonical reality drift**: a capability may be real and well-tested on a branch while remaining absent from `main`, yet architecture documents or downstream repositories may begin to treat it as if it were canonical. + +This contract makes capability state explicit and machine-readable. + +## Source of truth + +`governance/capability-manifest.v0.1.json` is the repository-local source of truth for whether an audited capability is: + +- `CANONICAL` — present in reviewed repository history and allowed as a default dependency; +- `PROPOSED` — implemented on an exact PR head but not part of canonical repository state; +- `EXPERIMENTAL` — an opt-in experiment that must not be treated as a stable system contract; +- `SUPERSEDED` — preserved for history but replaced by another declared capability; +- `ARCHIVED` — retained as evidence/history and not an active implementation target. + +Open PR titles, roadmap prose, issue discussions, master-context documents, and successful branch CI do **not** promote a capability by themselves. + +## Consumer rule + +A cross-repository consumer may depend by default only on a capability with: + +```text +status = CANONICAL +consumer_default_allowed = true +canonical_commit = +``` + +Using a `PROPOSED` or `EXPERIMENTAL` capability is allowed only as an explicitly labelled experiment and requires: + +```text +explicit opt-in ++ exact source PR ++ exact head SHA ++ no claim that the dependency exists in ProofPath main +``` + +A proposed capability must never be referenced as `latest`, `production`, `canonical`, or an ambient ProofPath feature. + +## Promotion transition + +The only normal promotion edge is: + +```text +PROPOSED + -> reconcile with current canonical base + -> exact-head validation + -> review / trust-boundary check + -> merge + -> capability manifest update bound to merged commit + -> CANONICAL +``` + +For stacked capabilities, children may be developed before parents are promoted, but this does not create transitive canonicality. A child remains non-canonical until every required dependency is canonical or the child is deliberately refactored to remove that dependency. + +## Branch graph rule + +A long stack is evidence of an implementation trajectory, not of canonical repository state. + +For the current PoCI / Deploy Guard / Control Cloud trajectory: + +```text +#193 PoCI contract + -> #194 fixtures + -> #195 Python verifier + -> #196 CI/demo + -> #197 Rust verifier + -> #198 multigraph + -> #199 quorum + -> #200 signed runners + -> #201 federation + -> #202 org independence + -> #204 external SDK + -> #205 external admission + -> #206 Deploy Guard + -> #207 reusable Action + -> #208 Evidence Builder + -> #209 GitHub Collector + -> #211 Control Cloud + -> #213 ingestion + -> #214 Sigstore admission + -> #215 workflow governance + -> #216 reviewer separation +``` + +`#210 Assured Action Economy` is a parallel child of `#208`. `#217 MASTER_CONTEXT` is documentation and cannot promote runtime capabilities. `#218 Gonka Compute Witness` is an independent experiment. + +## FCRP-SELF-005 + +The first meaningful divergence is **PR #193**, not the later Control Cloud stack: + +```text +Idea: +ProofPath capabilities used by the ecosystem should have an unambiguous canonical identity. + +First divergence: +PoCI v0.1 became the foundation of a long implementation stack while remaining PROPOSED outside main. + +Symptom: +Downstream architecture can describe branch-only capabilities as if they are ordinary ProofPath surfaces. + +Refactor point: +Repository-level capability promotion/canonicality contract. +``` + +This is intentionally not solved by blindly merging the entire stack. Promotion is a trust transition and each dependency boundary still requires reconciliation with current `main`. + +## Positive control + +SAFE Causal Incident Graph v0.1 (`#219`) is already present in `main` at the observed audit head. It demonstrates that ProofPath can promote a capability normally; the defect is the absence of a general lifecycle contract for distinguishing that state from branch-only architecture. + +## Fail-closed invariants + +The validator rejects: + +- a non-canonical capability marked as a default consumer dependency; +- a canonical capability without an exact canonical commit; +- a canonical capability depending on a non-canonical capability; +- a proposed/experimental capability without an exact head SHA and source PR; +- unknown dependency IDs; +- dependency cycles; +- duplicate capability IDs or duplicate JSON keys; +- weakening of the manifest policy flags. + +## Ecosystem integration + +The same state vocabulary should be reused when the NEO REZONANS repositories are connected: + +```text +RESONANCE consumes published/canonical claims +CML consumes canonical memory contracts or exact experimental pins +FCRP records divergence and refactor decisions +LiminalOSAI evaluates authorization separately from evidence +ContractGraph-QA verifies state transitions +ProofPath emits evidence/provenance under explicit capability identity +LiminalDB persists only contracts whose import status is explicit +RINSE may reinterpret evidence but cannot silently promote capability status +``` + +The capability manifest is therefore the first repository-local building block for a later ecosystem-wide System Contract. diff --git a/governance/capability-manifest.v0.1.json b/governance/capability-manifest.v0.1.json new file mode 100644 index 0000000..6ee82ad --- /dev/null +++ b/governance/capability-manifest.v0.1.json @@ -0,0 +1,267 @@ +{ + "schema_version": "proofpath.capability-manifest.v0.1", + "repository": "safal207/ProofPath", + "observed_at": "2026-08-14", + "observed_main_sha": "685d50e256a5125a21f4c4584b326411caaa64ad", + "policy": { + "default_consumer_status": "CANONICAL", + "noncanonical_requires_exact_head_pin": true, + "noncanonical_requires_explicit_opt_in": true, + "canonical_dependency_must_be_canonical": true + }, + "capabilities": [ + { + "id": "proofpath.scig.v0.1", + "kind": "protocol", + "status": "CANONICAL", + "source_pr": 219, + "head_sha": null, + "canonical_commit": "685d50e256a5125a21f4c4584b326411caaa64ad", + "depends_on": [], + "consumer_default_allowed": true, + "note": "SAFE Causal Incident Graph v0.1 is present in main and is the positive control for the promotion model." + }, + { + "id": "proofpath.poci.contract.v0.1", + "kind": "protocol", + "status": "PROPOSED", + "source_pr": 193, + "head_sha": "643f3cda04f7e2e86935059083ea0424988852b0", + "canonical_commit": null, + "depends_on": [], + "consumer_default_allowed": false, + "note": "First open capability in the current PoCI stack and FCRP-SELF-005 first meaningful divergence." + }, + { + "id": "proofpath.poci.fixtures.v0.1", + "kind": "verification-fixtures", + "status": "PROPOSED", + "source_pr": 194, + "head_sha": "84b10f5071305602f561d9c67d4643263c5723b2", + "canonical_commit": null, + "depends_on": ["proofpath.poci.contract.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.poci.python-verifier.v0.1", + "kind": "verifier", + "status": "PROPOSED", + "source_pr": 195, + "head_sha": "05de05bb1aac72899e36fe65c2150269725ec2f8", + "canonical_commit": null, + "depends_on": ["proofpath.poci.fixtures.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.poci.ci-demo.v0.1", + "kind": "verification-workflow", + "status": "PROPOSED", + "source_pr": 196, + "head_sha": "1ec1ef999f2e54a2045662c1440f1f726d1db19c", + "canonical_commit": null, + "depends_on": ["proofpath.poci.python-verifier.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.poci.rust-verifier.v0.1", + "kind": "verifier", + "status": "PROPOSED", + "source_pr": 197, + "head_sha": "ff1b898babbb88ec98113c9d982680ed4a1e15f1", + "canonical_commit": null, + "depends_on": ["proofpath.poci.ci-demo.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.poci.multigraph.v0.1", + "kind": "protocol", + "status": "PROPOSED", + "source_pr": 198, + "head_sha": "2e5d0991a6fb96db8e7589fa312d6b312ca0e4e5", + "canonical_commit": null, + "depends_on": ["proofpath.poci.rust-verifier.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.poci.quorum.v0.1", + "kind": "verification-network", + "status": "PROPOSED", + "source_pr": 199, + "head_sha": "2558c00ae54e28fc6024cffd1590f9e49f50fff2", + "canonical_commit": null, + "depends_on": ["proofpath.poci.multigraph.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.poci.signed-multirunner.v0.1", + "kind": "verification-network", + "status": "PROPOSED", + "source_pr": 200, + "head_sha": "a726f2a8b3bd9c6c4469be542e30bb54b230d510", + "canonical_commit": null, + "depends_on": ["proofpath.poci.quorum.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.poci.cross-repo-federation.v0.1", + "kind": "verification-network", + "status": "PROPOSED", + "source_pr": 201, + "head_sha": "0a821562a7336c918ab76006dd47d8dc8f8a6fa6", + "canonical_commit": null, + "depends_on": ["proofpath.poci.signed-multirunner.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.poci.org-independence.v0.1", + "kind": "governance", + "status": "PROPOSED", + "source_pr": 202, + "head_sha": "25c2431d20915dd933e2d4a04be8d424df97554c", + "canonical_commit": null, + "depends_on": ["proofpath.poci.cross-repo-federation.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.poci.external-sdk.v0.1", + "kind": "sdk", + "status": "PROPOSED", + "source_pr": 204, + "head_sha": "469d742cfe59b12875c5c6e1fc0ce660c38ab1dd", + "canonical_commit": null, + "depends_on": ["proofpath.poci.org-independence.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.poci.external-admission.v0.1", + "kind": "admission", + "status": "PROPOSED", + "source_pr": 205, + "head_sha": "74c2f352aab0620f997042cd6bf7ec3ad7056e9b", + "canonical_commit": null, + "depends_on": ["proofpath.poci.external-sdk.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.deploy-guard.v0.1", + "kind": "product-protocol", + "status": "PROPOSED", + "source_pr": 206, + "head_sha": "384826649b5a8c24e0d31a57a0f8756f61053ca4", + "canonical_commit": null, + "depends_on": ["proofpath.poci.external-admission.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.deploy-guard-action.v0.1", + "kind": "integration", + "status": "PROPOSED", + "source_pr": 207, + "head_sha": "bb2aec89b3d2b8e9efd5a1996e6a2665aa53af3a", + "canonical_commit": null, + "depends_on": ["proofpath.deploy-guard.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.deploy-evidence-builder.v0.1", + "kind": "evidence-builder", + "status": "PROPOSED", + "source_pr": 208, + "head_sha": "38e699244a2af59fe45975a29e271e0fd0e1ac49", + "canonical_commit": null, + "depends_on": ["proofpath.deploy-guard-action.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.github-evidence-collector.v0.1", + "kind": "collector", + "status": "PROPOSED", + "source_pr": 209, + "head_sha": "bc7805b17324a92d4201b11f66192857d9a4c5f7", + "canonical_commit": null, + "depends_on": ["proofpath.deploy-evidence-builder.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.assured-action-economy.v0.1", + "kind": "economy-simulation", + "status": "PROPOSED", + "source_pr": 210, + "head_sha": "f6614d1faf4b3c0b5bf1400edee769f89efea780", + "canonical_commit": null, + "depends_on": ["proofpath.deploy-evidence-builder.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.control-cloud.v0.1", + "kind": "product-layer", + "status": "PROPOSED", + "source_pr": 211, + "head_sha": "aaf0fd0f53616b456eb4984f8d37384cb6688c95", + "canonical_commit": null, + "depends_on": ["proofpath.github-evidence-collector.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.control-cloud-ingestion.v0.1", + "kind": "ingestion", + "status": "PROPOSED", + "source_pr": 213, + "head_sha": "2b07fc3713b3d410d87247b22a4e023fd8dfb5cc", + "canonical_commit": null, + "depends_on": ["proofpath.control-cloud.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.sigstore-admission.v0.1", + "kind": "admission", + "status": "PROPOSED", + "source_pr": 214, + "head_sha": "eda93927ab82e4e9c8cb8e01ecadb02e6d13c910", + "canonical_commit": null, + "depends_on": ["proofpath.control-cloud-ingestion.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.workflow-governance.v0.1", + "kind": "governance", + "status": "PROPOSED", + "source_pr": 215, + "head_sha": "87be25b5383fbddf3fec437f53346d1865594672", + "canonical_commit": null, + "depends_on": ["proofpath.sigstore-admission.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.reviewer-separation.v0.1", + "kind": "governance", + "status": "PROPOSED", + "source_pr": 216, + "head_sha": "4e7a206bc8ea4e486318c77ab13f089017f4ab80", + "canonical_commit": null, + "depends_on": ["proofpath.workflow-governance.v0.1"], + "consumer_default_allowed": false + }, + { + "id": "proofpath.master-context.v0.3", + "kind": "documentation", + "status": "PROPOSED", + "source_pr": 217, + "head_sha": "5579316decdc84185bc3197e56c1d80ed2428b4a", + "canonical_commit": null, + "depends_on": [], + "consumer_default_allowed": false, + "note": "A context document cannot promote capabilities that are not present in main." + }, + { + "id": "proofpath.gonka-compute-witness-pilot.v0.1", + "kind": "experimental-adapter", + "status": "EXPERIMENTAL", + "source_pr": 218, + "head_sha": "390c03784289ad9c65001c28e0737baac6030069", + "canonical_commit": null, + "depends_on": [], + "consumer_default_allowed": false + } + ] +} diff --git a/governance/fcrp-self-005.json b/governance/fcrp-self-005.json new file mode 100644 index 0000000..2637a34 --- /dev/null +++ b/governance/fcrp-self-005.json @@ -0,0 +1,118 @@ +{ + "schema": "cgqa.fcrp-case.v0.1", + "caseId": "FCRP-SELF-005", + "scope": { + "nodeId": "proofpath-canonical-reality-drift", + "scale": "repository/capability-lifecycle/ecosystem-contract", + "idea": "A ProofPath capability must have an unambiguous canonical identity before downstream systems may consume it by default." + }, + "evidence": [ + { + "id": "E1", + "kind": "canonical-main", + "ref": "ProofPath main@685d50e256a5125a21f4c4584b326411caaa64ad", + "claim": "The audited main branch contains SCIG v0.1 as a promoted capability." + }, + { + "id": "E2", + "kind": "pull-request", + "ref": "https://github.com/safal207/ProofPath/pull/193", + "claim": "PoCI v0.1 is the earliest open capability in the current stack, is based on main, and remains unmerged." + }, + { + "id": "E3", + "kind": "repository-observation", + "ref": "docs/POCI_SPEC_V0_1.md@main", + "claim": "The PoCI specification path from PR #193 is absent from the audited main branch." + }, + { + "id": "E4", + "kind": "pull-request-graph", + "ref": "ProofPath open PRs #193-#216", + "claim": "Later PoCI, Deploy Guard, evidence, Control Cloud, admission and governance capabilities form a dependency stack rooted in unpromoted PoCI work." + }, + { + "id": "E5", + "kind": "local-contract", + "ref": "governance/capability-manifest.v0.1.json", + "claim": "The repository now declares capability state, exact branch identities and default-consumer eligibility explicitly." + }, + { + "id": "E6", + "kind": "local-validator", + "ref": "scripts/validate_capability_manifest.py", + "claim": "The fail-closed validator prevents proposed or experimental capabilities from becoming ambient default dependencies." + } + ], + "timeline": { + "past": { + "summary": "ProofPath developed PoCI and successive product layers through a long stacked PR trajectory without a general capability-promotion contract.", + "evidenceRefs": ["E2", "E4"] + }, + "present": { + "summary": "Main has continued to evolve and has promoted other work such as SCIG, while the PoCI-to-Control-Cloud stack remains branch-only and can be confused with canonical architecture.", + "evidenceRefs": ["E1", "E3", "E4"] + }, + "future": { + "summary": "Every capability has explicit lifecycle state and exact identity; cross-repository consumers default only to canonical capabilities and must opt in to exact non-canonical heads.", + "evidenceRefs": ["E5", "E6"] + } + }, + "causalPath": [ + { + "id": "N1", + "level": "repository-capability", + "phase": "first-divergence", + "statement": "PoCI v0.1 became the dependency root of subsequent ProofPath architecture while remaining outside main and lacking a repository-level promotion state contract.", + "evidenceRefs": ["E2", "E3"] + }, + { + "id": "N2", + "level": "branch-graph", + "phase": "amplification", + "statement": "Successive capabilities were stacked on the unpromoted root, increasing implementation depth without increasing canonicality.", + "evidenceRefs": ["E4"] + }, + { + "id": "N3", + "level": "ecosystem", + "phase": "symptom", + "statement": "Architecture and downstream consumers can mistake well-tested branch capabilities for ordinary ProofPath surfaces unless exact status is carried with the capability reference.", + "evidenceRefs": ["E4"] + }, + { + "id": "N4", + "level": "repository-governance", + "phase": "refactor", + "statement": "Introduce a machine-readable capability manifest and fail-closed promotion rules separating CANONICAL, PROPOSED, EXPERIMENTAL, SUPERSEDED and ARCHIVED states.", + "evidenceRefs": ["E5", "E6"] + } + ], + "divergence": { + "symptomPoint": "N3", + "firstMeaningfulDivergence": "N1", + "causePoint": "N1", + "selectedRefactorPoint": "N4", + "evidenceRefs": ["E2", "E3", "E4", "E5", "E6"] + }, + "navigation": { + "direction": "UP", + "reason": "The defect is not in one feature implementation; it is the repository-level lifecycle contract governing how feature branches become ecosystem-consumable capabilities." + }, + "refactor": { + "point": "N4", + "change": "Make canonicality explicit, prohibit ambient consumption of non-canonical capabilities, require exact head pins for experiments, and promote only through reconciled exact-head review and merge.", + "expectedEffect": "Branch innovation remains possible without allowing conceptual architecture to outrun canonical repository reality." + }, + "verification": { + "local": "PASS", + "upward": "PASS", + "evidenceRefs": ["E5", "E6"], + "stopConditions": { + "parentInvariantsPreserved": true, + "crossBoundaryEffectsAbsent": true, + "causalExplanationComplete": true + } + }, + "expectedProtocolDecision": "PASS" +} diff --git a/scripts/validate_capability_manifest.py b/scripts/validate_capability_manifest.py new file mode 100755 index 0000000..c52fc77 --- /dev/null +++ b/scripts/validate_capability_manifest.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Fail-closed validation for ProofPath capability canonicality. + +This validator intentionally does not query GitHub. It validates the repository's +checked-in declaration of capability state and prevents non-canonical capabilities +from being treated as default consumer contracts. +""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path +from typing import Any + +SHA40 = re.compile(r"^[0-9a-f]{40}$") +STATUSES = {"CANONICAL", "PROPOSED", "EXPERIMENTAL", "SUPERSEDED", "ARCHIVED"} + + +def _reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + out: dict[str, Any] = {} + for key, value in pairs: + if key in out: + raise ValueError(f"duplicate JSON key: {key}") + out[key] = value + return out + + +def load_manifest(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=_reject_duplicate_keys) + + +def validate_manifest(data: dict[str, Any]) -> list[str]: + errors: list[str] = [] + + if data.get("schema_version") != "proofpath.capability-manifest.v0.1": + errors.append("schema_version must be proofpath.capability-manifest.v0.1") + if data.get("repository") != "safal207/ProofPath": + errors.append("repository must be safal207/ProofPath") + + observed_main = data.get("observed_main_sha") + if not isinstance(observed_main, str) or not SHA40.fullmatch(observed_main): + errors.append("observed_main_sha must be a lowercase 40-character SHA") + + policy = data.get("policy") + if not isinstance(policy, dict): + errors.append("policy must be an object") + else: + if policy.get("default_consumer_status") != "CANONICAL": + errors.append("default_consumer_status must be CANONICAL") + for key in ( + "noncanonical_requires_exact_head_pin", + "noncanonical_requires_explicit_opt_in", + "canonical_dependency_must_be_canonical", + ): + if policy.get(key) is not True: + errors.append(f"policy.{key} must be true") + + capabilities = data.get("capabilities") + if not isinstance(capabilities, list) or not capabilities: + errors.append("capabilities must be a non-empty array") + return errors + + by_id: dict[str, dict[str, Any]] = {} + for index, capability in enumerate(capabilities): + prefix = f"capabilities[{index}]" + if not isinstance(capability, dict): + errors.append(f"{prefix} must be an object") + continue + + cid = capability.get("id") + if not isinstance(cid, str) or not cid.strip(): + errors.append(f"{prefix}.id must be non-empty") + continue + if cid in by_id: + errors.append(f"duplicate capability id: {cid}") + continue + by_id[cid] = capability + + status = capability.get("status") + if status not in STATUSES: + errors.append(f"{cid}: invalid status {status!r}") + continue + + deps = capability.get("depends_on") + if not isinstance(deps, list) or any(not isinstance(dep, str) or not dep for dep in deps): + errors.append(f"{cid}: depends_on must be an array of capability ids") + + consumer_default = capability.get("consumer_default_allowed") + if not isinstance(consumer_default, bool): + errors.append(f"{cid}: consumer_default_allowed must be boolean") + + canonical_commit = capability.get("canonical_commit") + head_sha = capability.get("head_sha") + source_pr = capability.get("source_pr") + + if status == "CANONICAL": + if not isinstance(canonical_commit, str) or not SHA40.fullmatch(canonical_commit): + errors.append(f"{cid}: CANONICAL requires canonical_commit") + if head_sha is not None: + errors.append(f"{cid}: CANONICAL must not expose a branch head_sha as its consumer identity") + if consumer_default is not True: + errors.append(f"{cid}: CANONICAL must allow default consumption") + else: + if canonical_commit is not None: + errors.append(f"{cid}: non-canonical capability must not claim canonical_commit") + if consumer_default is not False: + errors.append(f"{cid}: non-canonical capability cannot be a default consumer dependency") + if status in {"PROPOSED", "EXPERIMENTAL"}: + if not isinstance(source_pr, int) or source_pr <= 0: + errors.append(f"{cid}: {status} requires positive source_pr") + if not isinstance(head_sha, str) or not SHA40.fullmatch(head_sha): + errors.append(f"{cid}: {status} requires exact lowercase head_sha") + + for cid, capability in by_id.items(): + deps = capability.get("depends_on", []) + if not isinstance(deps, list): + continue + for dep in deps: + if dep not in by_id: + errors.append(f"{cid}: unknown dependency {dep}") + continue + if capability.get("status") == "CANONICAL" and by_id[dep].get("status") != "CANONICAL": + errors.append(f"{cid}: CANONICAL capability depends on non-canonical {dep}") + + visiting: set[str] = set() + visited: set[str] = set() + + def visit(cid: str, trail: list[str]) -> None: + if cid in visited: + return + if cid in visiting: + cycle = " -> ".join(trail + [cid]) + errors.append(f"dependency cycle: {cycle}") + return + visiting.add(cid) + capability = by_id.get(cid, {}) + deps = capability.get("depends_on", []) + if isinstance(deps, list): + for dep in deps: + if dep in by_id: + visit(dep, trail + [cid]) + visiting.remove(cid) + visited.add(cid) + + for cid in by_id: + visit(cid, []) + + return errors + + +def summarize(data: dict[str, Any]) -> dict[str, int]: + counts = {status: 0 for status in sorted(STATUSES)} + for capability in data.get("capabilities", []): + if isinstance(capability, dict) and capability.get("status") in counts: + counts[capability["status"]] += 1 + return counts + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "manifest", + nargs="?", + type=Path, + default=Path("governance/capability-manifest.v0.1.json"), + ) + args = parser.parse_args() + + try: + data = load_manifest(args.manifest) + except (OSError, ValueError, json.JSONDecodeError) as exc: + print(f"CAPABILITY_MANIFEST_INVALID: {exc}") + return 1 + + errors = validate_manifest(data) + if errors: + print("CAPABILITY_MANIFEST_INVALID") + for error in errors: + print(f"- {error}") + return 1 + + print("CAPABILITY_MANIFEST_VALID") + print(json.dumps(summarize(data), sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_capability_manifest.py b/tests/test_capability_manifest.py new file mode 100644 index 0000000..a53839a --- /dev/null +++ b/tests/test_capability_manifest.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import copy +import importlib.util +import json +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST = ROOT / "governance" / "capability-manifest.v0.1.json" +VALIDATOR = ROOT / "scripts" / "validate_capability_manifest.py" + +spec = importlib.util.spec_from_file_location("proofpath_capability_manifest", VALIDATOR) +assert spec is not None and spec.loader is not None +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + + +class CapabilityManifestTests(unittest.TestCase): + def load(self) -> dict: + return json.loads(MANIFEST.read_text(encoding="utf-8")) + + def test_committed_manifest_is_valid(self) -> None: + data = self.load() + self.assertEqual(module.validate_manifest(data), []) + + def test_noncanonical_capability_cannot_be_default_dependency(self) -> None: + data = self.load() + target = next(c for c in data["capabilities"] if c["id"] == "proofpath.poci.contract.v0.1") + target["consumer_default_allowed"] = True + errors = module.validate_manifest(data) + self.assertTrue(any("non-canonical capability cannot be a default consumer dependency" in e for e in errors)) + + def test_canonical_capability_cannot_depend_on_proposed_capability(self) -> None: + data = self.load() + scig = next(c for c in data["capabilities"] if c["id"] == "proofpath.scig.v0.1") + scig["depends_on"] = ["proofpath.poci.contract.v0.1"] + errors = module.validate_manifest(data) + self.assertTrue(any("CANONICAL capability depends on non-canonical" in e for e in errors)) + + def test_proposed_capability_requires_exact_head_sha(self) -> None: + data = self.load() + target = next(c for c in data["capabilities"] if c["id"] == "proofpath.deploy-guard.v0.1") + target["head_sha"] = "main" + errors = module.validate_manifest(data) + self.assertTrue(any("requires exact lowercase head_sha" in e for e in errors)) + + def test_dependency_cycle_fails_closed(self) -> None: + data = self.load() + root = next(c for c in data["capabilities"] if c["id"] == "proofpath.poci.contract.v0.1") + root["depends_on"] = ["proofpath.reviewer-separation.v0.1"] + errors = module.validate_manifest(data) + self.assertTrue(any("dependency cycle" in e for e in errors)) + + +if __name__ == "__main__": + unittest.main()