From df930423e2312f27c426a3c641a28d2b645fee5f Mon Sep 17 00:00:00 2001 From: fraware Date: Sat, 16 May 2026 10:28:46 -0700 Subject: [PATCH 1/4] Add PCS v0.1 LabTrust import, portal rendering, and check tests Co-authored-by: Cursor --- .gitignore | 8 +- JUSTFILE | 15 ++ .../import_manifest.json | 6 + .../read_model.json | 177 +++++++++++++++++ .../signed_bundle.json | 146 ++++++++++++++ docs/pcs-labtrust-import.md | 91 +++++++++ docs/pcs-rendering-contract.md | 66 +++++++ pipeline/pyproject.toml | 4 + pipeline/src/sm_pipeline/cli/__init__.py | 6 + pipeline/src/sm_pipeline/cli/pcs.py | 66 +++++++ .../src/sm_pipeline/pcs_import/__init__.py | 8 + .../pcs_import/artifact_normalizer.py | 153 +++++++++++++++ .../sm_pipeline/pcs_import/portal_export.py | 49 +++++ .../science_claim_bundle_importer.py | 94 +++++++++ .../verification_result_importer.py | 35 ++++ .../src/sm_pipeline/pcs_validate/__init__.py | 13 ++ .../sm_pipeline/pcs_validate/stale_checker.py | 40 ++++ .../src/sm_pipeline/pcs_validate/validator.py | 138 +++++++++++++ portal/.generated/pcs-export.json | 185 ++++++++++++++++++ portal/app/layout.tsx | 3 + portal/app/page.tsx | 12 -- portal/app/pcs/claims/[claimId]/page.tsx | 34 ++++ portal/app/pcs/page.tsx | 53 +++++ portal/components/SiteNav.tsx | 26 +++ portal/components/pcs/ArtifactHashTable.tsx | 46 +++++ portal/components/pcs/AssumptionSetView.tsx | 41 ++++ portal/components/pcs/ClaimArtifactView.tsx | 48 +++++ portal/components/pcs/LimitationNotice.tsx | 25 +++ portal/components/pcs/PcsClaimPage.tsx | 45 +++++ portal/components/pcs/ReplayCommand.tsx | 46 +++++ portal/components/pcs/RuntimeReceiptView.tsx | 30 +++ portal/components/pcs/SourceRepositories.tsx | 38 ++++ .../components/pcs/TraceCertificateView.tsx | 30 +++ .../components/pcs/VerificationResultView.tsx | 61 ++++++ portal/lib/pcsData.ts | 39 ++++ portal/lib/pcsTypes.ts | 77 ++++++++ pyproject.toml | 2 +- schemas/pcs/artifact_base.schema.json | 73 +++++++ schemas/pcs/science_claim_bundle.schema.json | 107 ++++++++++ .../signed_science_claim_bundle.schema.json | 29 +++ schemas/pcs/verification_result.schema.json | 48 +++++ tests/pcs/conftest.py | 8 + .../fixtures/invalid_missing_signature.json | 65 ++++++ tests/pcs/fixtures/missing_assumptions.json | 61 ++++++ .../fixtures/missing_verification_result.json | 70 +++++++ .../valid_signed_science_claim_bundle.json | 146 ++++++++++++++ tests/pcs/test_import_labtrust_bundle.py | 58 ++++++ tests/pcs/test_import_verification_result.py | 61 ++++++ tests/pcs/test_reject_invalid_bundle.py | 43 ++++ tests/pcs/test_render_pcs_claim.py | 89 +++++++++ 50 files changed, 2797 insertions(+), 17 deletions(-) create mode 100644 corpus/pcs/claims/labtrust-qc-release-claim-001/import_manifest.json create mode 100644 corpus/pcs/claims/labtrust-qc-release-claim-001/read_model.json create mode 100644 corpus/pcs/claims/labtrust-qc-release-claim-001/signed_bundle.json create mode 100644 docs/pcs-labtrust-import.md create mode 100644 docs/pcs-rendering-contract.md create mode 100644 pipeline/src/sm_pipeline/cli/pcs.py create mode 100644 pipeline/src/sm_pipeline/pcs_import/__init__.py create mode 100644 pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py create mode 100644 pipeline/src/sm_pipeline/pcs_import/portal_export.py create mode 100644 pipeline/src/sm_pipeline/pcs_import/science_claim_bundle_importer.py create mode 100644 pipeline/src/sm_pipeline/pcs_import/verification_result_importer.py create mode 100644 pipeline/src/sm_pipeline/pcs_validate/__init__.py create mode 100644 pipeline/src/sm_pipeline/pcs_validate/stale_checker.py create mode 100644 pipeline/src/sm_pipeline/pcs_validate/validator.py create mode 100644 portal/.generated/pcs-export.json create mode 100644 portal/app/pcs/claims/[claimId]/page.tsx create mode 100644 portal/app/pcs/page.tsx create mode 100644 portal/components/SiteNav.tsx create mode 100644 portal/components/pcs/ArtifactHashTable.tsx create mode 100644 portal/components/pcs/AssumptionSetView.tsx create mode 100644 portal/components/pcs/ClaimArtifactView.tsx create mode 100644 portal/components/pcs/LimitationNotice.tsx create mode 100644 portal/components/pcs/PcsClaimPage.tsx create mode 100644 portal/components/pcs/ReplayCommand.tsx create mode 100644 portal/components/pcs/RuntimeReceiptView.tsx create mode 100644 portal/components/pcs/SourceRepositories.tsx create mode 100644 portal/components/pcs/TraceCertificateView.tsx create mode 100644 portal/components/pcs/VerificationResultView.tsx create mode 100644 portal/lib/pcsData.ts create mode 100644 portal/lib/pcsTypes.ts create mode 100644 schemas/pcs/artifact_base.schema.json create mode 100644 schemas/pcs/science_claim_bundle.schema.json create mode 100644 schemas/pcs/signed_science_claim_bundle.schema.json create mode 100644 schemas/pcs/verification_result.schema.json create mode 100644 tests/pcs/conftest.py create mode 100644 tests/pcs/fixtures/invalid_missing_signature.json create mode 100644 tests/pcs/fixtures/missing_assumptions.json create mode 100644 tests/pcs/fixtures/missing_verification_result.json create mode 100644 tests/pcs/fixtures/valid_signed_science_claim_bundle.json create mode 100644 tests/pcs/test_import_labtrust_bundle.py create mode 100644 tests/pcs/test_import_verification_result.py create mode 100644 tests/pcs/test_reject_invalid_bundle.py create mode 100644 tests/pcs/test_render_pcs_claim.py diff --git a/.gitignore b/.gitignore index d4d5b48..372314c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ # ============================================================================= # Scientific Memory — ignore rules # ============================================================================= -# Note: `portal/.generated/corpus-export.json` is intentionally TRACKED so -# `pnpm --dir portal build` and portal CI succeed without a prior -# `just export-portal-data` step. Do not add `portal/.generated/` here unless -# you change CI to generate the bundle first. +# Note: `portal/.generated/corpus-export.json` and `portal/.generated/pcs-export.json` +# are intentionally TRACKED so `pnpm --dir portal build` and portal CI succeed +# without a prior export step. Do not add `portal/.generated/` here unless you +# change CI to generate those bundles first. # ----------------------------------------------------------------------------- # Lean / Lake diff --git a/JUSTFILE b/JUSTFILE index 9dbf0de..e7f3502 100644 --- a/JUSTFILE +++ b/JUSTFILE @@ -61,6 +61,11 @@ test: uv run --project kernels/adsorption pytest bash tests/smoke/test_repo_bootstrap.sh +# PCS LabTrust import contract tests (also run via `just test`) +test-pcs: + @echo "==> test-pcs" + uv run --project pipeline pytest ../tests/pcs + benchmark: uv run --project pipeline python -m sm_pipeline.cli benchmark @@ -170,3 +175,13 @@ mcp-server: # Derived metrics from corpus (SPEC 12): median intake time, dependency reuse, symbol conflict metrics *ARGS: uv run --project pipeline python -m sm_pipeline.cli metrics {{ARGS}} + +# PCS LabTrust v0.1: import, validate, and render proof-carrying science claims +pcs-import-bundle BUNDLE: + uv run --project pipeline python -m sm_pipeline.cli pcs-import-bundle --bundle {{BUNDLE}} + +pcs-validate-bundle BUNDLE: + uv run --project pipeline python -m sm_pipeline.cli pcs-validate-bundle --bundle {{BUNDLE}} + +pcs-render-claim CLAIM_ID: + uv run --project pipeline python -m sm_pipeline.cli pcs-render-claim --claim-id {{CLAIM_ID}} diff --git a/corpus/pcs/claims/labtrust-qc-release-claim-001/import_manifest.json b/corpus/pcs/claims/labtrust-qc-release-claim-001/import_manifest.json new file mode 100644 index 0000000..8b9d7a2 --- /dev/null +++ b/corpus/pcs/claims/labtrust-qc-release-claim-001/import_manifest.json @@ -0,0 +1,6 @@ +{ + "claim_id": "labtrust-qc-release-claim-001", + "imported_from": "C:\\Users\\mateo\\scientific-memory\\tests\\pcs\\fixtures\\valid_signed_science_claim_bundle.json", + "warnings": [], + "stale_artifacts": [] +} diff --git a/corpus/pcs/claims/labtrust-qc-release-claim-001/read_model.json b/corpus/pcs/claims/labtrust-qc-release-claim-001/read_model.json new file mode 100644 index 0000000..becd8c5 --- /dev/null +++ b/corpus/pcs/claims/labtrust-qc-release-claim-001/read_model.json @@ -0,0 +1,177 @@ +{ + "artifact_hashes": [ + { + "algorithm": "sha256", + "digest": "sha256:claim-artifact-001", + "name": "claim_digest", + "source_artifact": "labtrust-qc-release-claim-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:trace-json-qc-release", + "name": "trace_hash", + "source_artifact": "runtime-receipt-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:events-qc-release", + "name": "events_hash", + "source_artifact": "runtime-receipt-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:temporal-policy-qc-release", + "name": "policy_hash", + "source_artifact": "trace-certificate-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:qc-release-stl", + "name": "spec_hash", + "source_artifact": "trace-certificate-qc-release" + } + ], + "assumption_set": { + "assumptions": [ + { + "id": "assume-sim-lab", + "kind": "simulation_scope", + "status": "RuntimeObserved", + "text": "The environment is the LabTrust-Gym simulated hospital laboratory, not a production clinical site." + }, + { + "id": "assume-qc-policy", + "kind": "policy", + "status": "CertificateChecked", + "text": "QC release policy template hospital_lab/qc_release.stl governs acceptable release transitions." + } + ], + "created_at": "2026-05-01T11:00:00Z", + "id": "labtrust-qc-release-assumptions", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "schema_version": "AssumptionSet.v0", + "signature_or_digest": "sha256:assumption-set-001", + "source_commit": "labtrust-qc-release-demo", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "status": "RuntimeObserved" + }, + "bundle_signature_or_digest": "sha256:signed-bundle-labtrust-demo-001", + "claim": { + "created_at": "2026-05-01T11:00:00Z", + "guarantee_types": { + "certificate_checked": true, + "empirically_measured": false, + "formally_checked": true, + "human_reviewed": false, + "runtime_observed": true, + "unchecked_advisory": false + }, + "id": "labtrust-qc-release-claim-001", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "signature_or_digest": "sha256:claim-artifact-001", + "status": "RuntimeChecked", + "text": "The qc-release workflow run observed protocol-compliant specimen handling and release gating in the LabTrust-Gym hospital-lab simulation." + }, + "claim_id": "labtrust-qc-release-claim-001", + "limitation_notice": "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory.", + "limitations": [ + "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory." + ], + "reproduce_commands": [ + "labtrust run-demo qc-release", + "labtrust export-pcs --run runs/qc-release --out science_claim_bundle.pending.json" + ], + "runtime_receipt": { + "created_at": "2026-05-01T11:15:00Z", + "events_hash": "sha256:events-qc-release", + "id": "runtime-receipt-qc-release", + "payload": { + "release_gate": "passed", + "run_id": "runs/qc-release", + "steps_observed": 42 + }, + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "schema_version": "RuntimeReceipt.v0", + "signature_or_digest": "sha256:runtime-receipt-001", + "source_commit": "labtrust-qc-release-demo", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "status": "RuntimeChecked", + "summary": "Observed workflow steps, instrument events, and release gate transitions for qc-release.", + "trace_hash": "sha256:trace-json-qc-release" + }, + "schema_version": "PcsClaimReadModel.v0", + "source_repositories": [ + { + "source_commit": "labtrust-qc-release-demo", + "source_repo": "https://github.com/fraware/LabTrust-Gym" + }, + { + "source_commit": "certifyedge-demo", + "source_repo": "https://github.com/fraware/CertifyEdge" + }, + { + "source_commit": "abc123def456", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric" + } + ], + "trace_certificate": { + "created_at": "2026-05-01T11:20:00Z", + "id": "trace-certificate-qc-release", + "payload": { + "certificate_status": "valid", + "spec": "templates/hospital_lab/qc_release.stl" + }, + "policy_hash": "sha256:temporal-policy-qc-release", + "producer": "CertifyEdge", + "producer_version": "0.1.0", + "schema_version": "TraceCertificate.v0", + "signature_or_digest": "sha256:trace-certificate-001", + "source_commit": "certifyedge-demo", + "source_repo": "https://github.com/fraware/CertifyEdge", + "spec_hash": "sha256:qc-release-stl", + "status": "CertificateChecked", + "summary": "Temporal certificate attests trace.json satisfies qc_release.stl." + }, + "verification_result": { + "checks": [ + { + "detail": "RuntimeReceipt.v0 digest matches bundle linkage.", + "guarantee_type": "runtime_observed", + "id": "check-runtime-receipt", + "name": "Runtime receipt hash chain", + "outcome": "pass" + }, + { + "detail": "TraceCertificate.v0 status CertificateChecked.", + "guarantee_type": "certificate_checked", + "id": "check-trace-certificate", + "name": "Trace certificate temporal policy", + "outcome": "pass" + }, + { + "detail": "Provability Fabric signing check passed.", + "guarantee_type": "formally_checked", + "id": "check-bundle-signature", + "name": "Bundle signature", + "outcome": "pass" + } + ], + "created_at": "2026-05-01T12:00:00Z", + "id": "verification-result-labtrust-qc-release", + "overall_outcome": "pass", + "producer": "provability-fabric", + "producer_version": "0.1.0", + "schema_version": "VerificationResult.v0", + "signature_or_digest": "sha256:verification-result-001", + "source_commit": "abc123def456", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "status": "ProofChecked" + }, + "verify_commands": [ + "pf verify science-claim signed_science_claim_bundle.json", + "just pcs-validate-bundle BUNDLE=signed_science_claim_bundle.json" + ] +} diff --git a/corpus/pcs/claims/labtrust-qc-release-claim-001/signed_bundle.json b/corpus/pcs/claims/labtrust-qc-release-claim-001/signed_bundle.json new file mode 100644 index 0000000..8d32bc7 --- /dev/null +++ b/corpus/pcs/claims/labtrust-qc-release-claim-001/signed_bundle.json @@ -0,0 +1,146 @@ +{ + "reproduce_commands": [ + "labtrust run-demo qc-release", + "labtrust export-pcs --run runs/qc-release --out science_claim_bundle.pending.json" + ], + "schema_version": "SignedScienceClaimBundle.v0", + "science_claim_bundle": { + "assumption_set": { + "assumptions": [ + { + "id": "assume-sim-lab", + "kind": "simulation_scope", + "status": "RuntimeObserved", + "text": "The environment is the LabTrust-Gym simulated hospital laboratory, not a production clinical site." + }, + { + "id": "assume-qc-policy", + "kind": "policy", + "status": "CertificateChecked", + "text": "QC release policy template hospital_lab/qc_release.stl governs acceptable release transitions." + } + ], + "created_at": "2026-05-01T11:00:00Z", + "id": "labtrust-qc-release-assumptions", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "schema_version": "AssumptionSet.v0", + "signature_or_digest": "sha256:assumption-set-001", + "source_commit": "labtrust-qc-release-demo", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "status": "RuntimeObserved" + }, + "claim": { + "claim_text": "The qc-release workflow run observed protocol-compliant specimen handling and release gating in the LabTrust-Gym hospital-lab simulation.", + "created_at": "2026-05-01T11:00:00Z", + "guarantee_types": { + "certificate_checked": true, + "empirically_measured": false, + "formally_checked": true, + "human_reviewed": false, + "runtime_observed": true, + "unchecked_advisory": false + }, + "id": "labtrust-qc-release-claim-001", + "output_hashes": [ + { + "algorithm": "sha256", + "digest": "sha256:claim-artifact-001", + "name": "claim_digest" + } + ], + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "schema_version": "ClaimArtifact.v0", + "signature_or_digest": "sha256:claim-artifact-001", + "source_commit": "labtrust-qc-release-demo", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "status": "RuntimeChecked" + }, + "created_at": "2026-05-01T11:30:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "runtime_receipt": { + "created_at": "2026-05-01T11:15:00Z", + "events_hash": "sha256:events-qc-release", + "id": "runtime-receipt-qc-release", + "payload": { + "release_gate": "passed", + "run_id": "runs/qc-release", + "steps_observed": 42 + }, + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "schema_version": "RuntimeReceipt.v0", + "signature_or_digest": "sha256:runtime-receipt-001", + "source_commit": "labtrust-qc-release-demo", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "status": "RuntimeChecked", + "summary": "Observed workflow steps, instrument events, and release gate transitions for qc-release.", + "trace_hash": "sha256:trace-json-qc-release" + }, + "schema_version": "ScienceClaimBundle.v0", + "signature_or_digest": "sha256:science-claim-bundle-001", + "source_commit": "labtrust-qc-release-demo", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "status": "RuntimeChecked", + "trace_certificate": { + "created_at": "2026-05-01T11:20:00Z", + "id": "trace-certificate-qc-release", + "payload": { + "certificate_status": "valid", + "spec": "templates/hospital_lab/qc_release.stl" + }, + "policy_hash": "sha256:temporal-policy-qc-release", + "producer": "CertifyEdge", + "producer_version": "0.1.0", + "schema_version": "TraceCertificate.v0", + "signature_or_digest": "sha256:trace-certificate-001", + "source_commit": "certifyedge-demo", + "source_repo": "https://github.com/fraware/CertifyEdge", + "spec_hash": "sha256:qc-release-stl", + "status": "CertificateChecked", + "summary": "Temporal certificate attests trace.json satisfies qc_release.stl." + } + }, + "signature_or_digest": "sha256:signed-bundle-labtrust-demo-001", + "verification_result": { + "checks": [ + { + "detail": "RuntimeReceipt.v0 digest matches bundle linkage.", + "guarantee_type": "runtime_observed", + "id": "check-runtime-receipt", + "name": "Runtime receipt hash chain", + "outcome": "pass" + }, + { + "detail": "TraceCertificate.v0 status CertificateChecked.", + "guarantee_type": "certificate_checked", + "id": "check-trace-certificate", + "name": "Trace certificate temporal policy", + "outcome": "pass" + }, + { + "detail": "Provability Fabric signing check passed.", + "guarantee_type": "formally_checked", + "id": "check-bundle-signature", + "name": "Bundle signature", + "outcome": "pass" + } + ], + "created_at": "2026-05-01T12:00:00Z", + "id": "verification-result-labtrust-qc-release", + "overall_outcome": "pass", + "producer": "provability-fabric", + "producer_version": "0.1.0", + "schema_version": "VerificationResult.v0", + "signature_or_digest": "sha256:verification-result-001", + "source_commit": "abc123def456", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "status": "ProofChecked" + }, + "verify_commands": [ + "pf verify science-claim signed_science_claim_bundle.json", + "just pcs-validate-bundle BUNDLE=signed_science_claim_bundle.json" + ] +} diff --git a/docs/pcs-labtrust-import.md b/docs/pcs-labtrust-import.md new file mode 100644 index 0000000..4478883 --- /dev/null +++ b/docs/pcs-labtrust-import.md @@ -0,0 +1,91 @@ +# PCS LabTrust import + +Scientific Memory imports **signed** `ScienceClaimBundle` artifacts produced by the LabTrust v0.1 demonstration workflow and verified or signed by Provability Fabric. + +## Expected input + +- File: `signed_science_claim_bundle.json` +- Top-level `schema_version`: `SignedScienceClaimBundle.v0` +- Nested `science_claim_bundle` with `ScienceClaimBundle.v0` +- Optional top-level `verification_result` (`VerificationResult.v0`) +- Top-level `signature_or_digest` + +Canonical artifact vocabulary is defined in [pcs-core](https://github.com/SentinelOps-CI/pcs-core). This repository validates against vendored JSON Schemas under `schemas/pcs/` and will call `pcs_core` when that package is installed. + +## Commands + +```bash +just pcs-validate-bundle BUNDLE=path/to/signed_science_claim_bundle.json +just pcs-import-bundle BUNDLE=path/to/signed_science_claim_bundle.json +just pcs-render-claim CLAIM_ID= +``` + +`pcs-import-bundle` writes: + +- `corpus/pcs/claims//signed_bundle.json` — preserved signed input +- `corpus/pcs/claims//read_model.json` — portal read model +- `corpus/pcs/claims//import_manifest.json` — warnings and provenance + +`pcs-render-claim` refreshes `portal/.generated/pcs-export.json` for static portal build. + +## Import behavior + +| Behavior | Detail | +|----------|--------| +| Validation | JSON Schema + optional `pcs_core` hook | +| Reject invalid | Default (`strict=true`) | +| Preserve IDs | Claim, assumption set, receipt, certificate IDs unchanged | +| Preserve provenance | `source_repo`, `source_commit`, `signature_or_digest` on each artifact | +| Preserve checks | VerificationResult `checks` list stored verbatim | +| Warn: no VerificationResult | Import continues; portal shows advisory | +| Warn: certificate not checked | When `trace_certificate.status` ≠ `CertificateChecked` | +| Reject: empty assumptions | `assumption_set.assumptions` must be non-empty | + +## End-to-end LabTrust flow (reference) + +```bash +# LabTrust-Gym +labtrust run-demo qc-release +labtrust export-trace --run runs/qc-release --out trace.json +labtrust export-runtime-receipt --run runs/qc-release --out runtime_receipt.json +labtrust export-pcs --run runs/qc-release --out science_claim_bundle.pending.json + +# CertifyEdge +certifyedge emit-pcs-certificate \ + --spec templates/hospital_lab/qc_release.stl \ + --trace trace.json \ + --out trace_certificate.json + +labtrust attach-certificate \ + --bundle science_claim_bundle.pending.json \ + --certificate trace_certificate.json \ + --out science_claim_bundle.certified.json + +# Provability Fabric +pf verify science-claim science_claim_bundle.certified.json +pf sign science-claim science_claim_bundle.certified.json \ + --out signed_science_claim_bundle.json + +# Scientific Memory +just pcs-import-bundle BUNDLE=signed_science_claim_bundle.json +just pcs-render-claim CLAIM_ID= +``` + +## What is checked vs not checked + +**Checked (when present in bundle):** + +- Schema shape for signed bundle, science claim bundle, and verification result +- Non-empty assumption set +- Artifact hash fields on nested artifacts (surfaced in portal hash table) +- Provability Fabric verification checks (listed under Verification Result) +- Runtime receipt and trace certificate status values (displayed, not re-verified) + +**Not checked by Scientific Memory v0.1:** + +- Live LabTrust simulation re-run +- CertifyEdge certificate re-generation +- Temporal trace re-validation against production hospital systems +- Clinical or production medical certification + +See [pcs-rendering-contract.md](./pcs-rendering-contract.md) for portal section requirements. diff --git a/docs/pcs-rendering-contract.md b/docs/pcs-rendering-contract.md new file mode 100644 index 0000000..57b86ec --- /dev/null +++ b/docs/pcs-rendering-contract.md @@ -0,0 +1,66 @@ +# PCS rendering contract + +Every LabTrust PCS claim page at `/pcs/claims/` must render the following sections in order. + +| # | Section title | Component | Data source | +|---|---------------|-----------|-------------| +| 1 | Claim | `ClaimArtifactView` | `read_model.claim` | +| 2 | Assumptions | `AssumptionSetView` | `read_model.assumption_set` | +| 3 | Runtime Evidence | `RuntimeReceiptView` | `read_model.runtime_receipt` | +| 4 | Temporal Certificate | `TraceCertificateView` | `read_model.trace_certificate` | +| 5 | Verification Result | `VerificationResultView` | `read_model.verification_result` | +| 6 | Artifact Hashes | `ArtifactHashTable` | `read_model.artifact_hashes` | +| 7 | Source Repositories | `SourceRepositories` | `read_model.source_repositories` | +| 8 | Reproduce / Verify | `ReplayCommand` | `reproduce_commands`, `verify_commands` | +| 9 | Limitations | `LimitationNotice` | `limitation_notice` (+ optional `limitations`) | + +## Guarantee-type separation + +The claim section must show boolean flags for: + +- `formally_checked` +- `certificate_checked` +- `runtime_observed` +- `empirically_measured` +- `human_reviewed` +- `unchecked_advisory` + +Values come from `ClaimArtifact.guarantee_types` when present; otherwise they are inferred from nested artifact statuses and verification checks. + +## Required limitation notice + +Every page must display the following notice verbatim (or substantively identical wording): + +> This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory. + +The canonical string is defined in `sm_pipeline.pcs_import.artifact_normalizer.LIMITATION_NOTICE`. + +## Status visibility + +Status enums must be visible for: + +- Claim artifact +- Assumption set and individual assumptions (when provided) +- Runtime receipt +- Trace certificate +- Verification result and each check outcome + +Statuses use the pcs-core canonical enum (`Draft`, `RuntimeObserved`, `CertificateChecked`, etc.). + +## Reproduce / verify commands + +Commands are copied from the signed bundle (top-level or `science_claim_bundle` fields) without modification. They are shown as shell snippets for external verification, for example: + +- `pf verify science-claim …` +- `just pcs-validate-bundle BUNDLE=…` + +## Test IDs + +Portal components expose `data-testid` attributes for contract tests: + +- `pcs-claim-page` +- `pcs-section-claim`, `pcs-section-assumptions`, … +- `pcs-limitation-notice` +- `pcs-hash-table`, `pcs-source-repo`, `pcs-source-commit` + +Python tests in `tests/pcs/` validate import behavior and read-model completeness. diff --git a/pipeline/pyproject.toml b/pipeline/pyproject.toml index 038e469..d068f10 100644 --- a/pipeline/pyproject.toml +++ b/pipeline/pyproject.toml @@ -26,5 +26,9 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/sm_pipeline"] +[tool.pytest.ini_options] +pythonpath = ["src"] +testpaths = ["tests", "../tests/pcs"] + [tool.ruff] line-length = 100 diff --git a/pipeline/src/sm_pipeline/cli/__init__.py b/pipeline/src/sm_pipeline/cli/__init__.py index 719427d..b4e2a71 100644 --- a/pipeline/src/sm_pipeline/cli/__init__.py +++ b/pipeline/src/sm_pipeline/cli/__init__.py @@ -9,6 +9,7 @@ ingest, llm_proposals, metrics, + pcs, publish, validate_cmd, ) @@ -60,3 +61,8 @@ app.command("llm-apply-mapping-proposals")(llm_proposals.llm_apply_mapping_proposals) app.command("llm-lean-proposals")(llm_proposals.llm_lean_proposals) app.command("llm-lean-proposals-to-apply-bundle")(llm_proposals.llm_lean_proposals_to_apply_bundle) + +# PCS LabTrust import (v0.1) +app.command("pcs-import-bundle")(pcs.pcs_import_bundle) +app.command("pcs-validate-bundle")(pcs.pcs_validate_bundle) +app.command("pcs-render-claim")(pcs.pcs_render_claim) diff --git a/pipeline/src/sm_pipeline/cli/pcs.py b/pipeline/src/sm_pipeline/cli/pcs.py new file mode 100644 index 0000000..649bd4a --- /dev/null +++ b/pipeline/src/sm_pipeline/cli/pcs.py @@ -0,0 +1,66 @@ +"""CLI: PCS LabTrust bundle import, validate, and portal render.""" + +from __future__ import annotations + +from pathlib import Path + +import typer +from rich.console import Console + +from sm_pipeline.pcs_import.portal_export import write_pcs_portal_export +from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle +from sm_pipeline.pcs_validate.validator import BundleValidationError, validate_signed_bundle + +console = Console() +_REPO_ROOT = Path(__file__).resolve().parents[4] + + +def pcs_import_bundle( + bundle: Path = typer.Option(..., "--bundle", "-b", help="Signed bundle JSON path"), + strict: bool = typer.Option(True, help="Reject invalid bundles"), +) -> None: + """Import a signed LabTrust PCS bundle into corpus/pcs/claims/.""" + try: + result = import_signed_bundle(bundle, repo_root=_REPO_ROOT, strict=strict) + except BundleValidationError as exc: + console.print(f"[red]Import rejected:[/red] {exc}") + raise typer.Exit(code=1) from exc + + write_pcs_portal_export(_REPO_ROOT) + console.print(f"[green]Imported claim[/green] {result.claim_id}") + console.print(f" -> {result.import_dir}") + for w in result.warnings: + console.print(f"[yellow]Warning:[/yellow] {w}") + + +def pcs_validate_bundle( + bundle: Path = typer.Option(..., "--bundle", "-b", help="Signed bundle JSON path"), +) -> None: + """Validate a signed bundle without importing.""" + import json + + raw = json.loads(bundle.read_text(encoding="utf-8")) + if not isinstance(raw, dict): + console.print("[red]Bundle must be a JSON object[/red]") + raise typer.Exit(code=1) + try: + warnings = validate_signed_bundle(raw, repo_root=_REPO_ROOT, strict=True) + except BundleValidationError as exc: + console.print(f"[red]Invalid bundle:[/red] {exc}") + raise typer.Exit(code=1) from exc + + console.print("[green]Bundle is valid[/green]") + for w in warnings: + console.print(f"[yellow]Warning:[/yellow] {w}") + + +def pcs_render_claim( + claim_id: str = typer.Option(..., "--claim-id", help="PCS claim artifact id"), +) -> None: + """Export portal PCS read model for rendering.""" + try: + out = write_pcs_portal_export(_REPO_ROOT, claim_id=claim_id) + except FileNotFoundError as exc: + console.print(f"[red]{exc}[/red]") + raise typer.Exit(code=1) from exc + console.print(f"[green]PCS portal export[/green] -> {out}") diff --git a/pipeline/src/sm_pipeline/pcs_import/__init__.py b/pipeline/src/sm_pipeline/pcs_import/__init__.py new file mode 100644 index 0000000..44d260d --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_import/__init__.py @@ -0,0 +1,8 @@ +"""Import signed LabTrust PCS bundles into Scientific Memory.""" + +from sm_pipeline.pcs_import.science_claim_bundle_importer import ( + ImportResult, + import_signed_bundle, +) + +__all__ = ["ImportResult", "import_signed_bundle"] diff --git a/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py b/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py new file mode 100644 index 0000000..98c9ebf --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py @@ -0,0 +1,153 @@ +"""Normalize PCS nested artifacts into a portal read model.""" + +from __future__ import annotations + +from typing import Any + +GUARANTEE_KEYS = ( + "formally_checked", + "certificate_checked", + "runtime_observed", + "empirically_measured", + "human_reviewed", + "unchecked_advisory", +) + +LIMITATION_NOTICE = ( + "This artifact is a proof-carrying simulation result. It demonstrates " + "protocol-level and runtime-evidence verification inside LabTrust-Gym. It is " + "not a clinical validation, production medical certification, or guarantee " + "about a real hospital laboratory." +) + + +def _hash_rows(artifact: dict[str, Any] | None) -> list[dict[str, str]]: + if not isinstance(artifact, dict): + return [] + rows: list[dict[str, str]] = [] + for field in ("input_hashes", "output_hashes"): + entries = artifact.get(field) + if not isinstance(entries, list): + continue + for entry in entries: + if not isinstance(entry, dict): + continue + name = str(entry.get("name") or entry.get("id") or "artifact") + digest = str(entry.get("digest") or entry.get("hash") or "") + if digest: + rows.append( + { + "name": name, + "digest": digest, + "algorithm": str(entry.get("algorithm") or "sha256"), + "source_artifact": str(artifact.get("id") or field), + } + ) + for key in ("trace_hash", "policy_hash", "events_hash", "spec_hash"): + val = artifact.get(key) + if isinstance(val, str) and val.strip(): + rows.append( + { + "name": key, + "digest": val.strip(), + "algorithm": "sha256", + "source_artifact": str(artifact.get("id") or "metadata"), + } + ) + return rows + + +def _source_metadata(artifact: dict[str, Any] | None) -> dict[str, str]: + if not isinstance(artifact, dict): + return {"source_repo": "", "source_commit": ""} + return { + "source_repo": str(artifact.get("source_repo") or ""), + "source_commit": str(artifact.get("source_commit") or ""), + } + + +def normalize_signed_bundle(bundle: dict[str, Any]) -> dict[str, Any]: + """Build durable portal read model from a signed ScienceClaimBundle.""" + scb = bundle["science_claim_bundle"] + claim = scb["claim"] + claim_id = str(claim["id"]) + + vr = bundle.get("verification_result") + if vr is None: + vr = scb.get("verification_result") + + assumption_set = scb.get("assumption_set") or {} + runtime_receipt = scb.get("runtime_receipt") or {} + trace_certificate = scb.get("trace_certificate") or {} + + hash_artifacts = [claim, assumption_set, runtime_receipt, trace_certificate] + if isinstance(vr, dict): + hash_artifacts.append(vr) + evidence = scb.get("evidence_bundle") + if isinstance(evidence, dict): + hash_artifacts.append(evidence) + + artifact_hashes: list[dict[str, str]] = [] + for art in hash_artifacts: + artifact_hashes.extend(_hash_rows(art if isinstance(art, dict) else None)) + + sources: list[dict[str, str]] = [] + seen: set[tuple[str, str]] = set() + for art in hash_artifacts: + if not isinstance(art, dict): + continue + meta = _source_metadata(art) + key = (meta["source_repo"], meta["source_commit"]) + if key in seen or not meta["source_repo"]: + continue + seen.add(key) + sources.append(meta) + + reproduce = list(bundle.get("reproduce_commands") or scb.get("reproduce_commands") or []) + verify = list(bundle.get("verify_commands") or scb.get("verify_commands") or []) + + limitations = list(scb.get("limitations") or []) + if LIMITATION_NOTICE not in limitations: + limitations = [LIMITATION_NOTICE, *limitations] + + guarantee_types = claim.get("guarantee_types") + if not isinstance(guarantee_types, dict): + guarantee_types = {k: False for k in GUARANTEE_KEYS} + guarantee_types["runtime_observed"] = str(runtime_receipt.get("status")) in ( + "RuntimeObserved", + "RuntimeChecked", + ) + guarantee_types["certificate_checked"] = str(trace_certificate.get("status")) == ( + "CertificateChecked" + ) + if isinstance(vr, dict): + for check in vr.get("checks") or []: + if not isinstance(check, dict): + continue + gt = str(check.get("guarantee_type") or "") + if gt in GUARANTEE_KEYS and check.get("outcome") == "pass": + guarantee_types[gt] = True + + return { + "schema_version": "PcsClaimReadModel.v0", + "claim_id": claim_id, + "claim": { + "id": claim_id, + "text": str(claim.get("claim_text") or ""), + "status": str(claim.get("status") or ""), + "signature_or_digest": str(claim.get("signature_or_digest") or ""), + "guarantee_types": guarantee_types, + **{k: claim.get(k) for k in ("producer", "producer_version", "created_at")}, + }, + "assumption_set": assumption_set, + "runtime_receipt": runtime_receipt, + "trace_certificate": trace_certificate, + "verification_result": vr, + "artifact_hashes": artifact_hashes, + "source_repositories": sources, + "reproduce_commands": reproduce, + "verify_commands": verify, + "limitations": limitations, + "limitation_notice": LIMITATION_NOTICE, + "bundle_signature_or_digest": str(bundle.get("signature_or_digest") or ""), + } diff --git a/pipeline/src/sm_pipeline/pcs_import/portal_export.py b/pipeline/src/sm_pipeline/pcs_import/portal_export.py new file mode 100644 index 0000000..7eabe36 --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_import/portal_export.py @@ -0,0 +1,49 @@ +"""Export PCS claims for the Next.js portal.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + + +def build_pcs_portal_export(repo_root: Path) -> dict[str, Any]: + root = repo_root.resolve() + claims_dir = root / "corpus" / "pcs" / "claims" + claims: dict[str, dict[str, Any]] = {} + claim_ids: list[str] = [] + + if claims_dir.is_dir(): + for claim_dir in sorted(claims_dir.iterdir()): + if not claim_dir.is_dir(): + continue + read_model_path = claim_dir / "read_model.json" + if not read_model_path.is_file(): + continue + data = json.loads(read_model_path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + continue + claim_id = str(data.get("claim_id") or claim_dir.name) + claims[claim_id] = data + claim_ids.append(claim_id) + + return { + "schema_version": "PcsPortalExport.v0", + "claim_ids": sorted(claim_ids), + "claims": claims, + } + + +def write_pcs_portal_export(repo_root: Path, claim_id: str | None = None) -> Path: + """Write portal/.generated/pcs-export.json (all claims or validate one exists).""" + root = repo_root.resolve() + export = build_pcs_portal_export(root) + if claim_id is not None and claim_id not in export["claims"]: + raise FileNotFoundError( + f"No imported PCS claim with id {claim_id!r}; run pcs-import-bundle first." + ) + out_dir = root / "portal" / ".generated" + out_dir.mkdir(parents=True, exist_ok=True) + out_path = out_dir / "pcs-export.json" + out_path.write_text(json.dumps(export, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return out_path diff --git a/pipeline/src/sm_pipeline/pcs_import/science_claim_bundle_importer.py b/pipeline/src/sm_pipeline/pcs_import/science_claim_bundle_importer.py new file mode 100644 index 0000000..d479c26 --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_import/science_claim_bundle_importer.py @@ -0,0 +1,94 @@ +"""Import signed LabTrust ScienceClaimBundle artifacts.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from sm_pipeline.pcs_import.artifact_normalizer import normalize_signed_bundle +from sm_pipeline.pcs_validate.stale_checker import find_stale_artifacts +from sm_pipeline.pcs_validate.validator import BundleValidationError, validate_signed_bundle + + +@dataclass +class ImportResult: + claim_id: str + import_dir: Path + warnings: list[str] = field(default_factory=list) + stale_artifacts: list[str] = field(default_factory=list) + + +def _repo_root(repo_root: Path | None) -> Path: + if repo_root is not None: + return repo_root.resolve() + return Path(__file__).resolve().parents[4] + + +def _import_root(repo_root: Path) -> Path: + return repo_root / "corpus" / "pcs" / "claims" + + +def import_signed_bundle( + bundle_path: Path, + *, + repo_root: Path | None = None, + strict: bool = True, + write: bool = True, +) -> ImportResult: + """ + Validate and import a signed science claim bundle. + + Preserves artifact IDs, source_repo, source_commit, signature_or_digest, + and verification checks. Rejects invalid bundles when strict=True. + """ + root = _repo_root(repo_root) + raw = json.loads(bundle_path.read_text(encoding="utf-8")) + if not isinstance(raw, dict): + raise BundleValidationError("Bundle must be a JSON object") + + warnings = validate_signed_bundle(raw, repo_root=root, strict=strict) + stale = find_stale_artifacts(raw) + if stale: + warnings.extend(f"Stale or deprecated artifact: {p}" for p in stale) + + read_model = normalize_signed_bundle(raw) + claim_id = read_model["claim_id"] + import_dir = _import_root(root) / claim_id + + if write: + import_dir.mkdir(parents=True, exist_ok=True) + (import_dir / "signed_bundle.json").write_text( + json.dumps(raw, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + (import_dir / "read_model.json").write_text( + json.dumps(read_model, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + manifest = { + "claim_id": claim_id, + "imported_from": str(bundle_path.resolve()), + "warnings": warnings, + "stale_artifacts": stale, + } + (import_dir / "import_manifest.json").write_text( + json.dumps(manifest, indent=2) + "\n", + encoding="utf-8", + ) + + return ImportResult( + claim_id=claim_id, + import_dir=import_dir, + warnings=warnings, + stale_artifacts=stale, + ) + + +def load_read_model(repo_root: Path, claim_id: str) -> dict[str, Any] | None: + path = _import_root(repo_root.resolve()) / claim_id / "read_model.json" + if not path.is_file(): + return None + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else None diff --git a/pipeline/src/sm_pipeline/pcs_import/verification_result_importer.py b/pipeline/src/sm_pipeline/pcs_import/verification_result_importer.py new file mode 100644 index 0000000..0b309fe --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_import/verification_result_importer.py @@ -0,0 +1,35 @@ +"""Import VerificationResult.v0 artifacts (embedded or sidecar).""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from sm_pipeline.pcs_validate.validator import BundleValidationError, validator_for + + +def load_verification_result(path: Path, *, repo_root: Path) -> dict[str, Any]: + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise BundleValidationError("VerificationResult must be a JSON object") + validator = validator_for("verification_result.schema.json", repo_root) + errors = [e.message for e in validator.iter_errors(data)] + if errors: + raise BundleValidationError("; ".join(errors)) + return data + + +def merge_verification_result( + bundle: dict[str, Any], + verification_result: dict[str, Any], +) -> dict[str, Any]: + """Attach verification result without mutating nested artifact IDs.""" + merged = dict(bundle) + merged["verification_result"] = verification_result + scb = merged.get("science_claim_bundle") + if isinstance(scb, dict) and scb.get("verification_result") is None: + scb_copy = dict(scb) + scb_copy["verification_result"] = verification_result + merged["science_claim_bundle"] = scb_copy + return merged diff --git a/pipeline/src/sm_pipeline/pcs_validate/__init__.py b/pipeline/src/sm_pipeline/pcs_validate/__init__.py new file mode 100644 index 0000000..8035190 --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_validate/__init__.py @@ -0,0 +1,13 @@ +"""PCS bundle validation against pcs-core (when installed) and vendored schemas.""" + +from sm_pipeline.pcs_validate.validator import ( + BundleValidationError, + collect_import_warnings, + validate_signed_bundle, +) + +__all__ = [ + "BundleValidationError", + "collect_import_warnings", + "validate_signed_bundle", +] diff --git a/pipeline/src/sm_pipeline/pcs_validate/stale_checker.py b/pipeline/src/sm_pipeline/pcs_validate/stale_checker.py new file mode 100644 index 0000000..192e41f --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_validate/stale_checker.py @@ -0,0 +1,40 @@ +"""Flag PCS artifacts whose status is Stale or Deprecated.""" + +from __future__ import annotations + +from typing import Any + + +STALE_STATUSES = frozenset({"Stale", "Deprecated"}) + + +def find_stale_artifacts(bundle: dict[str, Any]) -> list[str]: + """Return artifact paths (dot-separated) with Stale or Deprecated status.""" + stale: list[str] = [] + scb = bundle.get("science_claim_bundle") + if not isinstance(scb, dict): + return stale + + for key in ( + "claim", + "assumption_set", + "runtime_receipt", + "trace_certificate", + "evidence_bundle", + "verification_result", + ): + artifact = scb.get(key) + if isinstance(artifact, dict): + _check_artifact(f"science_claim_bundle.{key}", artifact, stale) + + vr = bundle.get("verification_result") + if isinstance(vr, dict): + _check_artifact("verification_result", vr, stale) + + return stale + + +def _check_artifact(path: str, artifact: dict[str, Any], out: list[str]) -> None: + status = str(artifact.get("status") or "") + if status in STALE_STATUSES: + out.append(path) diff --git a/pipeline/src/sm_pipeline/pcs_validate/validator.py b/pipeline/src/sm_pipeline/pcs_validate/validator.py new file mode 100644 index 0000000..5c33cc6 --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_validate/validator.py @@ -0,0 +1,138 @@ +"""Validate signed PCS bundles (pcs-core hook + vendored JSON Schema).""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from jsonschema import Draft202012Validator +from referencing import Registry, Resource +from referencing.jsonschema import DRAFT202012 + +PCS_SCHEMA_BASE_URI = "https://scientific-memory.org/schemas/pcs/" + + +class BundleValidationError(ValueError): + """Raised when a PCS bundle fails validation.""" + + +def _repo_root_from_here() -> Path: + return Path(__file__).resolve().parents[4] + + +def _pcs_schemas_dir(repo_root: Path | None = None) -> Path: + root = repo_root or _repo_root_from_here() + return root / "schemas" / "pcs" + + +def _load_json(path: Path) -> object: + return json.loads(path.read_text(encoding="utf-8")) + + +def _build_pcs_registry(repo_root: Path) -> Registry: + registry = Registry() + for path in sorted(_pcs_schemas_dir(repo_root).glob("*.json")): + schema = _load_json(path) + schema_id = schema.get("$id") + if not isinstance(schema_id, str): + continue + registry = registry.with_resource( + schema_id, + Resource.from_contents(schema, default_specification=DRAFT202012), + ) + return registry + + +def validator_for(schema_name: str, repo_root: Path) -> Draft202012Validator: + schema_path = _pcs_schemas_dir(repo_root) / schema_name + schema = _load_json(schema_path) + registry = _build_pcs_registry(repo_root) + return Draft202012Validator(schema, registry=registry) + + +def _try_pcs_core_validate(bundle: dict[str, Any]) -> None: + """Optional pcs-core validation hook (no-op if package unavailable).""" + try: + from pcs_core.validate import validate_signed_science_claim_bundle # type: ignore + except ImportError: + try: + from pcs_core import validate_signed_science_claim_bundle # type: ignore + except ImportError: + return + validate_signed_science_claim_bundle(bundle) + + +def validate_signed_bundle( + bundle: dict[str, Any], + *, + repo_root: Path | None = None, + strict: bool = True, +) -> list[str]: + """ + Validate a signed science claim bundle. + + Returns import warnings (non-fatal). Raises BundleValidationError when strict + and validation fails. + """ + root = (repo_root or _repo_root_from_here()).resolve() + errors: list[str] = [] + + if strict: + _try_pcs_core_validate(bundle) + + validator = validator_for("signed_science_claim_bundle.schema.json", root) + for err in sorted(validator.iter_errors(bundle), key=lambda e: e.path): + errors.append(err.message) + + scb = bundle.get("science_claim_bundle") + if isinstance(scb, dict): + scb_validator = validator_for("science_claim_bundle.schema.json", root) + for err in sorted(scb_validator.iter_errors(scb), key=lambda e: e.path): + errors.append(f"science_claim_bundle: {err.message}") + + assumption_set = scb.get("assumption_set") + assumptions = ( + assumption_set.get("assumptions") + if isinstance(assumption_set, dict) + else None + ) + if not assumptions: + errors.append("science_claim_bundle.assumption_set.assumptions is required") + + vr = bundle.get("verification_result") + if vr is None and isinstance(scb, dict): + vr = scb.get("verification_result") + if isinstance(vr, dict): + vr_validator = validator_for("verification_result.schema.json", root) + for err in sorted(vr_validator.iter_errors(vr), key=lambda e: e.path): + errors.append(f"verification_result: {err.message}") + + if errors and strict: + raise BundleValidationError("; ".join(errors)) + + return collect_import_warnings(bundle) if not errors else [] + + +def collect_import_warnings(bundle: dict[str, Any]) -> list[str]: + """Non-fatal warnings required by the PCS import contract.""" + warnings: list[str] = [] + scb = bundle.get("science_claim_bundle") + if not isinstance(scb, dict): + return warnings + + vr = bundle.get("verification_result") + if vr is None: + vr = scb.get("verification_result") + if vr is None: + warnings.append("VerificationResult is absent; import proceeds with advisory only.") + + trace_cert = scb.get("trace_certificate") + if isinstance(trace_cert, dict): + status = str(trace_cert.get("status") or "") + if status != "CertificateChecked": + warnings.append( + f"trace_certificate.status is {status!r}, expected CertificateChecked." + ) + + return warnings diff --git a/portal/.generated/pcs-export.json b/portal/.generated/pcs-export.json new file mode 100644 index 0000000..c84b937 --- /dev/null +++ b/portal/.generated/pcs-export.json @@ -0,0 +1,185 @@ +{ + "claim_ids": [ + "labtrust-qc-release-claim-001" + ], + "claims": { + "labtrust-qc-release-claim-001": { + "artifact_hashes": [ + { + "algorithm": "sha256", + "digest": "sha256:claim-artifact-001", + "name": "claim_digest", + "source_artifact": "labtrust-qc-release-claim-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:trace-json-qc-release", + "name": "trace_hash", + "source_artifact": "runtime-receipt-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:events-qc-release", + "name": "events_hash", + "source_artifact": "runtime-receipt-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:temporal-policy-qc-release", + "name": "policy_hash", + "source_artifact": "trace-certificate-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:qc-release-stl", + "name": "spec_hash", + "source_artifact": "trace-certificate-qc-release" + } + ], + "assumption_set": { + "assumptions": [ + { + "id": "assume-sim-lab", + "kind": "simulation_scope", + "status": "RuntimeObserved", + "text": "The environment is the LabTrust-Gym simulated hospital laboratory, not a production clinical site." + }, + { + "id": "assume-qc-policy", + "kind": "policy", + "status": "CertificateChecked", + "text": "QC release policy template hospital_lab/qc_release.stl governs acceptable release transitions." + } + ], + "created_at": "2026-05-01T11:00:00Z", + "id": "labtrust-qc-release-assumptions", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "schema_version": "AssumptionSet.v0", + "signature_or_digest": "sha256:assumption-set-001", + "source_commit": "labtrust-qc-release-demo", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "status": "RuntimeObserved" + }, + "bundle_signature_or_digest": "sha256:signed-bundle-labtrust-demo-001", + "claim": { + "created_at": "2026-05-01T11:00:00Z", + "guarantee_types": { + "certificate_checked": true, + "empirically_measured": false, + "formally_checked": true, + "human_reviewed": false, + "runtime_observed": true, + "unchecked_advisory": false + }, + "id": "labtrust-qc-release-claim-001", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "signature_or_digest": "sha256:claim-artifact-001", + "status": "RuntimeChecked", + "text": "The qc-release workflow run observed protocol-compliant specimen handling and release gating in the LabTrust-Gym hospital-lab simulation." + }, + "claim_id": "labtrust-qc-release-claim-001", + "limitation_notice": "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory.", + "limitations": [ + "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory." + ], + "reproduce_commands": [ + "labtrust run-demo qc-release", + "labtrust export-pcs --run runs/qc-release --out science_claim_bundle.pending.json" + ], + "runtime_receipt": { + "created_at": "2026-05-01T11:15:00Z", + "events_hash": "sha256:events-qc-release", + "id": "runtime-receipt-qc-release", + "payload": { + "release_gate": "passed", + "run_id": "runs/qc-release", + "steps_observed": 42 + }, + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "schema_version": "RuntimeReceipt.v0", + "signature_or_digest": "sha256:runtime-receipt-001", + "source_commit": "labtrust-qc-release-demo", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "status": "RuntimeChecked", + "summary": "Observed workflow steps, instrument events, and release gate transitions for qc-release.", + "trace_hash": "sha256:trace-json-qc-release" + }, + "schema_version": "PcsClaimReadModel.v0", + "source_repositories": [ + { + "source_commit": "labtrust-qc-release-demo", + "source_repo": "https://github.com/fraware/LabTrust-Gym" + }, + { + "source_commit": "certifyedge-demo", + "source_repo": "https://github.com/fraware/CertifyEdge" + }, + { + "source_commit": "abc123def456", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric" + } + ], + "trace_certificate": { + "created_at": "2026-05-01T11:20:00Z", + "id": "trace-certificate-qc-release", + "payload": { + "certificate_status": "valid", + "spec": "templates/hospital_lab/qc_release.stl" + }, + "policy_hash": "sha256:temporal-policy-qc-release", + "producer": "CertifyEdge", + "producer_version": "0.1.0", + "schema_version": "TraceCertificate.v0", + "signature_or_digest": "sha256:trace-certificate-001", + "source_commit": "certifyedge-demo", + "source_repo": "https://github.com/fraware/CertifyEdge", + "spec_hash": "sha256:qc-release-stl", + "status": "CertificateChecked", + "summary": "Temporal certificate attests trace.json satisfies qc_release.stl." + }, + "verification_result": { + "checks": [ + { + "detail": "RuntimeReceipt.v0 digest matches bundle linkage.", + "guarantee_type": "runtime_observed", + "id": "check-runtime-receipt", + "name": "Runtime receipt hash chain", + "outcome": "pass" + }, + { + "detail": "TraceCertificate.v0 status CertificateChecked.", + "guarantee_type": "certificate_checked", + "id": "check-trace-certificate", + "name": "Trace certificate temporal policy", + "outcome": "pass" + }, + { + "detail": "Provability Fabric signing check passed.", + "guarantee_type": "formally_checked", + "id": "check-bundle-signature", + "name": "Bundle signature", + "outcome": "pass" + } + ], + "created_at": "2026-05-01T12:00:00Z", + "id": "verification-result-labtrust-qc-release", + "overall_outcome": "pass", + "producer": "provability-fabric", + "producer_version": "0.1.0", + "schema_version": "VerificationResult.v0", + "signature_or_digest": "sha256:verification-result-001", + "source_commit": "abc123def456", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "status": "ProofChecked" + }, + "verify_commands": [ + "pf verify science-claim signed_science_claim_bundle.json", + "just pcs-validate-bundle BUNDLE=signed_science_claim_bundle.json" + ] + } + }, + "schema_version": "PcsPortalExport.v0" +} diff --git a/portal/app/layout.tsx b/portal/app/layout.tsx index 21938c3..1adc66e 100644 --- a/portal/app/layout.tsx +++ b/portal/app/layout.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; +import { SiteNav } from "@/components/SiteNav"; + export const metadata: Metadata = { title: "Scientific Memory", description: "Buildable, machine-checkable scientific knowledge.", @@ -13,6 +15,7 @@ export default function RootLayout({ return ( + {children} diff --git a/portal/app/page.tsx b/portal/app/page.tsx index 85c94b3..e947cf8 100644 --- a/portal/app/page.tsx +++ b/portal/app/page.tsx @@ -11,18 +11,6 @@ export default async function HomePage() { Buildable, machine-checkable scientific knowledge.

- -

Papers

    diff --git a/portal/app/pcs/claims/[claimId]/page.tsx b/portal/app/pcs/claims/[claimId]/page.tsx new file mode 100644 index 0000000..f68a04a --- /dev/null +++ b/portal/app/pcs/claims/[claimId]/page.tsx @@ -0,0 +1,34 @@ +import { PcsClaimPage } from "@/components/pcs/PcsClaimPage"; +import { getAllPcsClaimIds, getPcsClaimById } from "@/lib/pcsData"; + +export async function generateStaticParams() { + const ids = await getAllPcsClaimIds(); + return ids.map((claimId) => ({ claimId })); +} + +export const dynamicParams = false; + +export default async function PcsClaimRoute({ + params, +}: { + params: Promise<{ claimId: string }>; +}) { + const { claimId } = await params; + const model = await getPcsClaimById(claimId); + + if (!model) { + return ( +
    +

    PCS claim not found

    +

    + No imported claim with id “{claimId}”. Run{" "} + just pcs-import-bundle{" "} + then{" "} + just pcs-render-claim. +

    +
    + ); + } + + return ; +} diff --git a/portal/app/pcs/page.tsx b/portal/app/pcs/page.tsx new file mode 100644 index 0000000..aff0043 --- /dev/null +++ b/portal/app/pcs/page.tsx @@ -0,0 +1,53 @@ +import Link from "next/link"; + +import { getAllPcsClaimIds, getPcsClaimById } from "@/lib/pcsData"; + +export default async function PcsClaimsIndexPage() { + const claimIds = await getAllPcsClaimIds(); + + return ( +
    +

    PCS claims

    +

    + Proof-carrying science artifacts imported from signed LabTrust bundles. +

    + + {claimIds.length === 0 ? ( +

    + No claims imported yet. Run{" "} + just pcs-import-bundle{" "} + then{" "} + just pcs-render-claim. +

    + ) : ( +
      + {await Promise.all( + claimIds.map(async (claimId) => { + const model = await getPcsClaimById(claimId); + return ( +
    • + + {claimId} + + {model?.claim?.text != null && ( +

      + {model.claim.text} +

      + )} + {model?.claim?.status != null && ( +

      + Status: {model.claim.status} +

      + )} +
    • + ); + }), + )} +
    + )} +
    + ); +} diff --git a/portal/components/SiteNav.tsx b/portal/components/SiteNav.tsx new file mode 100644 index 0000000..c0db075 --- /dev/null +++ b/portal/components/SiteNav.tsx @@ -0,0 +1,26 @@ +import Link from "next/link"; + +const links = [ + { href: "/", label: "Home" }, + { href: "/search", label: "Search" }, + { href: "/dashboard", label: "Dashboard" }, + { href: "/diff", label: "Diff" }, + { href: "/pcs", label: "PCS claims" }, +] as const; + +export function SiteNav() { + return ( + + ); +} diff --git a/portal/components/pcs/ArtifactHashTable.tsx b/portal/components/pcs/ArtifactHashTable.tsx new file mode 100644 index 0000000..41f250f --- /dev/null +++ b/portal/components/pcs/ArtifactHashTable.tsx @@ -0,0 +1,46 @@ +import type { PcsHashRow } from "@/lib/pcsTypes"; + +interface ArtifactHashTableProps { + hashes: PcsHashRow[]; +} + +export function ArtifactHashTable({ hashes }: ArtifactHashTableProps) { + return ( +
    +

    Artifact Hashes

    + {hashes.length === 0 ? ( +

    No hashes recorded.

    + ) : ( +
    + + + + + + + + + + + {hashes.map((row) => ( + + + + + + + ))} + +
    NameDigestAlgorithmSource artifact
    {row.name} + {row.digest} + {row.algorithm ?? "sha256"} + {row.source_artifact ?? "—"} +
    +
    + )} +
    + ); +} diff --git a/portal/components/pcs/AssumptionSetView.tsx b/portal/components/pcs/AssumptionSetView.tsx new file mode 100644 index 0000000..81805f5 --- /dev/null +++ b/portal/components/pcs/AssumptionSetView.tsx @@ -0,0 +1,41 @@ +import type { PcsAssumption, PcsNamedArtifact } from "@/lib/pcsTypes"; + +interface AssumptionSetViewProps { + assumptionSet: PcsNamedArtifact & { assumptions?: PcsAssumption[] }; +} + +export function AssumptionSetView({ assumptionSet }: AssumptionSetViewProps) { + const assumptions = assumptionSet.assumptions ?? []; + return ( +
    +

    Assumptions

    + {assumptionSet.status != null && ( +

    + Set status:{" "} + + {String(assumptionSet.status)} + +

    + )} +
      + {assumptions.map((a) => ( +
    • + {a.id} + {a.kind != null && ( + ({a.kind}) + )} + {a.status != null && ( + + {a.status} + + )} +

      {a.text}

      +
    • + ))} +
    +
    + ); +} diff --git a/portal/components/pcs/ClaimArtifactView.tsx b/portal/components/pcs/ClaimArtifactView.tsx new file mode 100644 index 0000000..6e59289 --- /dev/null +++ b/portal/components/pcs/ClaimArtifactView.tsx @@ -0,0 +1,48 @@ +import type { PcsClaimSection } from "@/lib/pcsTypes"; + +const GUARANTEE_LABELS: Record = { + formally_checked: "Formally checked", + certificate_checked: "Certificate checked", + runtime_observed: "Runtime observed", + empirically_measured: "Empirically measured", + human_reviewed: "Human reviewed", + unchecked_advisory: "Unchecked advisory", +}; + +interface ClaimArtifactViewProps { + claim: PcsClaimSection; +} + +export function ClaimArtifactView({ claim }: ClaimArtifactViewProps) { + const guarantees = claim.guarantee_types ?? {}; + return ( +
    +

    Claim

    +

    {claim.text}

    +

    + Status:{" "} + + {claim.status} + +

    +
    +

    Guarantee separation

    +
      + {Object.entries(GUARANTEE_LABELS).map(([key, label]) => ( +
    • + + {label}: {guarantees[key] ? "yes" : "no"} + +
    • + ))} +
    +
    +
    + ); +} diff --git a/portal/components/pcs/LimitationNotice.tsx b/portal/components/pcs/LimitationNotice.tsx new file mode 100644 index 0000000..73c571f --- /dev/null +++ b/portal/components/pcs/LimitationNotice.tsx @@ -0,0 +1,25 @@ +interface LimitationNoticeProps { + notice: string; + additional?: string[]; +} + +export function LimitationNotice({ notice, additional }: LimitationNoticeProps) { + const extras = (additional ?? []).filter((line) => line.trim() && line !== notice); + return ( + + ); +} diff --git a/portal/components/pcs/PcsClaimPage.tsx b/portal/components/pcs/PcsClaimPage.tsx new file mode 100644 index 0000000..5701f71 --- /dev/null +++ b/portal/components/pcs/PcsClaimPage.tsx @@ -0,0 +1,45 @@ +import type { PcsClaimReadModel } from "@/lib/pcsTypes"; + +import { ArtifactHashTable } from "./ArtifactHashTable"; +import { AssumptionSetView } from "./AssumptionSetView"; +import { ClaimArtifactView } from "./ClaimArtifactView"; +import { LimitationNotice } from "./LimitationNotice"; +import { ReplayCommand } from "./ReplayCommand"; +import { RuntimeReceiptView } from "./RuntimeReceiptView"; +import { SourceRepositories } from "./SourceRepositories"; +import { TraceCertificateView } from "./TraceCertificateView"; +import { VerificationResultView } from "./VerificationResultView"; + +interface PcsClaimPageProps { + model: PcsClaimReadModel; +} + +export function PcsClaimPage({ model }: PcsClaimPageProps) { + const extraLimitations = model.limitations.filter( + (l) => l !== model.limitation_notice, + ); + return ( +
    +
    +

    LabTrust PCS claim

    +

    {model.claim_id}

    +

    + Bundle digest: {model.bundle_signature_or_digest} +

    +
    + + + + + + + + + + +
    + ); +} diff --git a/portal/components/pcs/ReplayCommand.tsx b/portal/components/pcs/ReplayCommand.tsx new file mode 100644 index 0000000..1cd498a --- /dev/null +++ b/portal/components/pcs/ReplayCommand.tsx @@ -0,0 +1,46 @@ +interface ReplayCommandProps { + reproduceCommands: string[]; + verifyCommands: string[]; +} + +export function ReplayCommand({ + reproduceCommands, + verifyCommands, +}: ReplayCommandProps) { + return ( +
    +

    Reproduce / Verify

    + {reproduceCommands.length > 0 && ( +
    +

    Reproduce

    +
      + {reproduceCommands.map((cmd) => ( +
    • +
      +                  {cmd}
      +                
      +
    • + ))} +
    +
    + )} + {verifyCommands.length > 0 && ( +
    +

    Verify externally

    +
      + {verifyCommands.map((cmd) => ( +
    • +
      +                  {cmd}
      +                
      +
    • + ))} +
    +
    + )} + {reproduceCommands.length === 0 && verifyCommands.length === 0 && ( +

    No reproduce or verify commands recorded.

    + )} +
    + ); +} diff --git a/portal/components/pcs/RuntimeReceiptView.tsx b/portal/components/pcs/RuntimeReceiptView.tsx new file mode 100644 index 0000000..f3f2418 --- /dev/null +++ b/portal/components/pcs/RuntimeReceiptView.tsx @@ -0,0 +1,30 @@ +import type { PcsNamedArtifact } from "@/lib/pcsTypes"; + +interface RuntimeReceiptViewProps { + receipt: PcsNamedArtifact; +} + +export function RuntimeReceiptView({ receipt }: RuntimeReceiptViewProps) { + return ( +
    +

    Runtime Evidence

    +

    + Status:{" "} + + {String(receipt.status ?? "unknown")} + +

    + {receipt.summary != null && ( +

    {String(receipt.summary)}

    + )} + {receipt.payload != null && ( +
    +          {JSON.stringify(receipt.payload, null, 2)}
    +        
    + )} +
    + ); +} diff --git a/portal/components/pcs/SourceRepositories.tsx b/portal/components/pcs/SourceRepositories.tsx new file mode 100644 index 0000000..e00f3ee --- /dev/null +++ b/portal/components/pcs/SourceRepositories.tsx @@ -0,0 +1,38 @@ +interface SourceRepositoriesProps { + sources: { source_repo: string; source_commit: string }[]; +} + +export function SourceRepositories({ sources }: SourceRepositoriesProps) { + return ( +
    +

    Source Repositories

    + {sources.length === 0 ? ( +

    No source metadata recorded.

    + ) : ( +
      + {sources.map((s) => ( +
    • +

      + Repository:{" "} + + {s.source_repo} + +

      +

      + Commit: {s.source_commit} +

      +
    • + ))} +
    + )} +
    + ); +} diff --git a/portal/components/pcs/TraceCertificateView.tsx b/portal/components/pcs/TraceCertificateView.tsx new file mode 100644 index 0000000..3a739f4 --- /dev/null +++ b/portal/components/pcs/TraceCertificateView.tsx @@ -0,0 +1,30 @@ +import type { PcsNamedArtifact } from "@/lib/pcsTypes"; + +interface TraceCertificateViewProps { + certificate: PcsNamedArtifact; +} + +export function TraceCertificateView({ certificate }: TraceCertificateViewProps) { + return ( +
    +

    Temporal Certificate

    +

    + Status:{" "} + + {String(certificate.status ?? "unknown")} + +

    + {certificate.summary != null && ( +

    {String(certificate.summary)}

    + )} + {certificate.payload != null && ( +
    +          {JSON.stringify(certificate.payload, null, 2)}
    +        
    + )} +
    + ); +} diff --git a/portal/components/pcs/VerificationResultView.tsx b/portal/components/pcs/VerificationResultView.tsx new file mode 100644 index 0000000..8d35536 --- /dev/null +++ b/portal/components/pcs/VerificationResultView.tsx @@ -0,0 +1,61 @@ +import type { PcsVerificationResult } from "@/lib/pcsTypes"; + +interface VerificationResultViewProps { + result: PcsVerificationResult | null | undefined; +} + +export function VerificationResultView({ result }: VerificationResultViewProps) { + if (!result) { + return ( +
    +

    Verification Result

    +

    + No VerificationResult was bundled. Checks from Provability Fabric are not shown. +

    +
    + ); + } + + const checks = result.checks ?? []; + return ( +
    +

    Verification Result

    +

    + Status:{" "} + + {String(result.status ?? "unknown")} + + {result.overall_outcome != null && ( + + Overall:{" "} + + {result.overall_outcome} + + + )} +

    +
      + {checks.map((c) => ( +
    • +
      + {c.name} + + {c.outcome} + + {c.guarantee_type != null && ( + {c.guarantee_type} + )} +
      + {c.detail != null &&

      {c.detail}

      } +
    • + ))} +
    +
    + ); +} diff --git a/portal/lib/pcsData.ts b/portal/lib/pcsData.ts new file mode 100644 index 0000000..2fc4e42 --- /dev/null +++ b/portal/lib/pcsData.ts @@ -0,0 +1,39 @@ +import { promises as fs } from "fs"; +import path from "path"; + +import type { PcsClaimReadModel, PcsPortalExport } from "./pcsTypes"; + +const ROOT = process.cwd(); +const PCS_EXPORT = path.join(ROOT, ".generated", "pcs-export.json"); +const CORPUS_PCS = path.join(ROOT, "..", "corpus", "pcs", "claims"); + +async function readPcsExport(): Promise { + const raw = await fs.readFile(PCS_EXPORT, "utf8").catch(() => null); + if (!raw) return null; + return JSON.parse(raw) as PcsPortalExport; +} + +async function readClaimFromCorpus(claimId: string): Promise { + const p = path.join(CORPUS_PCS, claimId, "read_model.json"); + const raw = await fs.readFile(p, "utf8").catch(() => null); + if (!raw) return null; + return JSON.parse(raw) as PcsClaimReadModel; +} + +export async function getAllPcsClaimIds(): Promise { + const exported = await readPcsExport(); + if (exported?.claim_ids?.length) { + return exported.claim_ids; + } + const entries = await fs.readdir(CORPUS_PCS, { withFileTypes: true }).catch(() => []); + return entries.filter((e) => e.isDirectory()).map((e) => e.name); +} + +export async function getPcsClaimById( + claimId: string, +): Promise { + const exported = await readPcsExport(); + const fromExport = exported?.claims?.[claimId]; + if (fromExport) return fromExport; + return readClaimFromCorpus(claimId); +} diff --git a/portal/lib/pcsTypes.ts b/portal/lib/pcsTypes.ts new file mode 100644 index 0000000..0a3de11 --- /dev/null +++ b/portal/lib/pcsTypes.ts @@ -0,0 +1,77 @@ +/** PCS portal read model (from pipeline pcs_import/artifact_normalizer). */ + +export type PcsGuaranteeTypes = Record; + +export type PcsClaimSection = { + id: string; + text: string; + status: string; + signature_or_digest: string; + guarantee_types: PcsGuaranteeTypes; +}; + +export type PcsAssumption = { + id: string; + text: string; + kind?: string; + status?: string; +}; + +export type PcsNamedArtifact = { + id: string; + schema_version?: string; + status?: string; + signature_or_digest?: string; + source_repo?: string; + source_commit?: string; + summary?: string; + payload?: Record; + [key: string]: unknown; +}; + +export type PcsVerificationCheck = { + id: string; + name: string; + outcome: string; + detail?: string; + guarantee_type?: string; +}; + +export type PcsVerificationResult = { + status?: string; + overall_outcome?: string; + signature_or_digest?: string; + source_repo?: string; + source_commit?: string; + checks?: PcsVerificationCheck[]; +}; + +export type PcsHashRow = { + name: string; + digest: string; + algorithm?: string; + source_artifact?: string; +}; + +export type PcsClaimReadModel = { + schema_version: string; + claim_id: string; + claim: PcsClaimSection; + assumption_set: PcsNamedArtifact & { assumptions?: PcsAssumption[] }; + runtime_receipt: PcsNamedArtifact; + trace_certificate: PcsNamedArtifact; + verification_result?: PcsVerificationResult | null; + artifact_hashes: PcsHashRow[]; + source_repositories: { source_repo: string; source_commit: string }[]; + reproduce_commands: string[]; + verify_commands: string[]; + limitations: string[]; + limitation_notice: string; + bundle_signature_or_digest: string; +}; + +export type PcsPortalExport = { + schema_version: string; + claim_ids: string[]; + claims: Record; +}; diff --git a/pyproject.toml b/pyproject.toml index 5866265..18bdc61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,4 +12,4 @@ line-length = 100 target-version = "py311" [tool.pytest.ini_options] -testpaths = ["pipeline/tests", "kernels/adsorption/tests"] +testpaths = ["pipeline/tests", "kernels/adsorption/tests", "tests/pcs"] diff --git a/schemas/pcs/artifact_base.schema.json b/schemas/pcs/artifact_base.schema.json new file mode 100644 index 0000000..032296b --- /dev/null +++ b/schemas/pcs/artifact_base.schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://scientific-memory.org/schemas/pcs/artifact_base.schema.json", + "title": "PCS Artifact Base (v0.1)", + "type": "object", + "additionalProperties": true, + "required": [ + "schema_version", + "created_at", + "producer", + "producer_version", + "source_repo", + "source_commit", + "status", + "signature_or_digest" + ], + "properties": { + "schema_version": { "type": "string", "minLength": 1 }, + "created_at": { "type": "string", "minLength": 1 }, + "producer": { "type": "string", "minLength": 1 }, + "producer_version": { "type": "string", "minLength": 1 }, + "source_repo": { "type": "string", "minLength": 1 }, + "source_commit": { "type": "string", "minLength": 1 }, + "status": { + "type": "string", + "enum": [ + "Draft", + "Extracted", + "HumanReviewed", + "Formalized", + "ProofPending", + "ProofChecked", + "CertificatePending", + "CertificateChecked", + "RuntimeObserved", + "RuntimeChecked", + "Rejected", + "EmpiricalOnly", + "Deprecated", + "Stale" + ] + }, + "signature_or_digest": { "type": "string", "minLength": 1 }, + "input_hashes": { + "type": "array", + "items": { "$ref": "#/$defs/hashEntry" } + }, + "output_hashes": { + "type": "array", + "items": { "$ref": "#/$defs/hashEntry" } + }, + "dependencies": { + "type": "array", + "items": { "type": "string" } + }, + "trace_hash": { "type": "string" }, + "policy_hash": { "type": "string" }, + "events_hash": { "type": "string" }, + "spec_hash": { "type": "string" } + }, + "$defs": { + "hashEntry": { + "type": "object", + "required": ["name", "digest"], + "properties": { + "name": { "type": "string" }, + "digest": { "type": "string" }, + "algorithm": { "type": "string" } + }, + "additionalProperties": true + } + } +} diff --git a/schemas/pcs/science_claim_bundle.schema.json b/schemas/pcs/science_claim_bundle.schema.json new file mode 100644 index 0000000..05dbce0 --- /dev/null +++ b/schemas/pcs/science_claim_bundle.schema.json @@ -0,0 +1,107 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://scientific-memory.org/schemas/pcs/science_claim_bundle.schema.json", + "title": "ScienceClaimBundle.v0", + "type": "object", + "additionalProperties": true, + "required": [ + "schema_version", + "created_at", + "producer", + "producer_version", + "source_repo", + "source_commit", + "status", + "signature_or_digest", + "claim", + "assumption_set", + "runtime_receipt", + "trace_certificate" + ], + "properties": { + "schema_version": { "const": "ScienceClaimBundle.v0" }, + "claim": { "$ref": "#/$defs/claimArtifact" }, + "assumption_set": { "$ref": "#/$defs/assumptionSet" }, + "runtime_receipt": { "$ref": "#/$defs/namedArtifact" }, + "trace_certificate": { "$ref": "#/$defs/namedArtifact" }, + "evidence_bundle": { "$ref": "#/$defs/namedArtifact" }, + "verification_result": { "$ref": "#/$defs/namedArtifact" }, + "limitations": { + "type": "array", + "items": { "type": "string" } + }, + "reproduce_commands": { + "type": "array", + "items": { "type": "string" } + }, + "verify_commands": { + "type": "array", + "items": { "type": "string" } + } + }, + "$defs": { + "artifactBase": { + "$ref": "https://scientific-memory.org/schemas/pcs/artifact_base.schema.json" + }, + "claimArtifact": { + "allOf": [ + { "$ref": "#/$defs/artifactBase" }, + { + "type": "object", + "required": ["id", "claim_text", "guarantee_types"], + "properties": { + "schema_version": { "const": "ClaimArtifact.v0" }, + "id": { "type": "string", "minLength": 1 }, + "claim_text": { "type": "string", "minLength": 1 }, + "guarantee_types": { + "type": "object", + "additionalProperties": { "type": "boolean" } + } + } + } + ] + }, + "assumptionSet": { + "allOf": [ + { "$ref": "#/$defs/artifactBase" }, + { + "type": "object", + "required": ["id", "assumptions"], + "properties": { + "schema_version": { "const": "AssumptionSet.v0" }, + "id": { "type": "string", "minLength": 1 }, + "assumptions": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["id", "text"], + "properties": { + "id": { "type": "string" }, + "text": { "type": "string" }, + "kind": { "type": "string" }, + "status": { "type": "string" } + }, + "additionalProperties": true + } + } + } + } + ] + }, + "namedArtifact": { + "allOf": [ + { "$ref": "#/$defs/artifactBase" }, + { + "type": "object", + "required": ["id"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "payload": { "type": "object" }, + "summary": { "type": "string" } + } + } + ] + } + } +} diff --git a/schemas/pcs/signed_science_claim_bundle.schema.json b/schemas/pcs/signed_science_claim_bundle.schema.json new file mode 100644 index 0000000..f6c22ed --- /dev/null +++ b/schemas/pcs/signed_science_claim_bundle.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://scientific-memory.org/schemas/pcs/signed_science_claim_bundle.schema.json", + "title": "SignedScienceClaimBundle (LabTrust v0.1)", + "type": "object", + "additionalProperties": true, + "required": ["schema_version", "science_claim_bundle", "signature_or_digest"], + "properties": { + "schema_version": { + "type": "string", + "pattern": "^SignedScienceClaimBundle\\.v0$" + }, + "science_claim_bundle": { + "$ref": "https://scientific-memory.org/schemas/pcs/science_claim_bundle.schema.json" + }, + "verification_result": { + "$ref": "https://scientific-memory.org/schemas/pcs/verification_result.schema.json" + }, + "signature_or_digest": { "type": "string", "minLength": 1 }, + "reproduce_commands": { + "type": "array", + "items": { "type": "string" } + }, + "verify_commands": { + "type": "array", + "items": { "type": "string" } + } + } +} diff --git a/schemas/pcs/verification_result.schema.json b/schemas/pcs/verification_result.schema.json new file mode 100644 index 0000000..50fde71 --- /dev/null +++ b/schemas/pcs/verification_result.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://scientific-memory.org/schemas/pcs/verification_result.schema.json", + "title": "VerificationResult.v0", + "type": "object", + "additionalProperties": true, + "required": [ + "schema_version", + "created_at", + "producer", + "producer_version", + "source_repo", + "source_commit", + "status", + "signature_or_digest", + "checks" + ], + "properties": { + "schema_version": { "const": "VerificationResult.v0" }, + "checks": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "name", "outcome"], + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "outcome": { + "type": "string", + "enum": ["pass", "fail", "skip", "warn"] + }, + "detail": { "type": "string" }, + "guarantee_type": { "type": "string" } + }, + "additionalProperties": true + } + }, + "overall_outcome": { + "type": "string", + "enum": ["pass", "fail", "partial"] + } + }, + "allOf": [ + { + "$ref": "https://scientific-memory.org/schemas/pcs/artifact_base.schema.json" + } + ] +} diff --git a/tests/pcs/conftest.py b/tests/pcs/conftest.py new file mode 100644 index 0000000..215e1c4 --- /dev/null +++ b/tests/pcs/conftest.py @@ -0,0 +1,8 @@ +"""Ensure pipeline package is importable when running PCS tests from repo root.""" + +import sys +from pathlib import Path + +_PIPELINE_SRC = Path(__file__).resolve().parents[2] / "pipeline" / "src" +if str(_PIPELINE_SRC) not in sys.path: + sys.path.insert(0, str(_PIPELINE_SRC)) diff --git a/tests/pcs/fixtures/invalid_missing_signature.json b/tests/pcs/fixtures/invalid_missing_signature.json new file mode 100644 index 0000000..dfad584 --- /dev/null +++ b/tests/pcs/fixtures/invalid_missing_signature.json @@ -0,0 +1,65 @@ +{ + "schema_version": "SignedScienceClaimBundle.v0", + "science_claim_bundle": { + "schema_version": "ScienceClaimBundle.v0", + "created_at": "2026-05-01T11:30:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "bad", + "status": "Draft", + "signature_or_digest": "sha256:bundle", + "claim": { + "schema_version": "ClaimArtifact.v0", + "id": "bad-claim", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "bad", + "status": "Draft", + "signature_or_digest": "sha256:claim", + "claim_text": "Incomplete bundle.", + "guarantee_types": {} + }, + "assumption_set": { + "schema_version": "AssumptionSet.v0", + "id": "assumptions", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "bad", + "status": "Draft", + "signature_or_digest": "sha256:assumptions", + "assumptions": [ + { + "id": "a1", + "text": "One assumption." + } + ] + }, + "runtime_receipt": { + "schema_version": "RuntimeReceipt.v0", + "id": "receipt", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "bad", + "status": "RuntimeObserved", + "signature_or_digest": "sha256:receipt" + }, + "trace_certificate": { + "schema_version": "TraceCertificate.v0", + "id": "cert", + "created_at": "2026-05-01T11:00:00Z", + "producer": "CertifyEdge", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/CertifyEdge", + "source_commit": "bad", + "status": "CertificatePending", + "signature_or_digest": "sha256:cert" + } + } +} diff --git a/tests/pcs/fixtures/missing_assumptions.json b/tests/pcs/fixtures/missing_assumptions.json new file mode 100644 index 0000000..b39a98e --- /dev/null +++ b/tests/pcs/fixtures/missing_assumptions.json @@ -0,0 +1,61 @@ +{ + "schema_version": "SignedScienceClaimBundle.v0", + "signature_or_digest": "sha256:missing-assumptions", + "science_claim_bundle": { + "schema_version": "ScienceClaimBundle.v0", + "created_at": "2026-05-01T11:30:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "bad", + "status": "Draft", + "signature_or_digest": "sha256:bundle", + "claim": { + "schema_version": "ClaimArtifact.v0", + "id": "claim-no-assumptions", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "bad", + "status": "Draft", + "signature_or_digest": "sha256:claim", + "claim_text": "Claim without assumptions.", + "guarantee_types": {} + }, + "assumption_set": { + "schema_version": "AssumptionSet.v0", + "id": "empty-assumptions", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "bad", + "status": "Draft", + "signature_or_digest": "sha256:assumptions", + "assumptions": [] + }, + "runtime_receipt": { + "schema_version": "RuntimeReceipt.v0", + "id": "receipt", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "bad", + "status": "RuntimeObserved", + "signature_or_digest": "sha256:receipt" + }, + "trace_certificate": { + "schema_version": "TraceCertificate.v0", + "id": "cert", + "created_at": "2026-05-01T11:00:00Z", + "producer": "CertifyEdge", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/CertifyEdge", + "source_commit": "bad", + "status": "CertificateChecked", + "signature_or_digest": "sha256:cert" + } + } +} diff --git a/tests/pcs/fixtures/missing_verification_result.json b/tests/pcs/fixtures/missing_verification_result.json new file mode 100644 index 0000000..17b2618 --- /dev/null +++ b/tests/pcs/fixtures/missing_verification_result.json @@ -0,0 +1,70 @@ +{ + "schema_version": "SignedScienceClaimBundle.v0", + "signature_or_digest": "sha256:no-verification-result", + "science_claim_bundle": { + "schema_version": "ScienceClaimBundle.v0", + "created_at": "2026-05-01T11:30:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "warn-demo", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:bundle", + "claim": { + "schema_version": "ClaimArtifact.v0", + "id": "claim-missing-vr", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "warn-demo", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:claim", + "claim_text": "Bundle without verification result.", + "guarantee_types": { + "runtime_observed": true, + "certificate_checked": false + } + }, + "assumption_set": { + "schema_version": "AssumptionSet.v0", + "id": "assumptions", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "warn-demo", + "status": "RuntimeObserved", + "signature_or_digest": "sha256:assumptions", + "assumptions": [ + { + "id": "a1", + "text": "Simulation only." + } + ] + }, + "runtime_receipt": { + "schema_version": "RuntimeReceipt.v0", + "id": "receipt", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "warn-demo", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:receipt", + "trace_hash": "sha256:trace" + }, + "trace_certificate": { + "schema_version": "TraceCertificate.v0", + "id": "cert", + "created_at": "2026-05-01T11:00:00Z", + "producer": "CertifyEdge", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/CertifyEdge", + "source_commit": "warn-demo", + "status": "CertificatePending", + "signature_or_digest": "sha256:cert" + } + } +} diff --git a/tests/pcs/fixtures/valid_signed_science_claim_bundle.json b/tests/pcs/fixtures/valid_signed_science_claim_bundle.json new file mode 100644 index 0000000..440a73d --- /dev/null +++ b/tests/pcs/fixtures/valid_signed_science_claim_bundle.json @@ -0,0 +1,146 @@ +{ + "schema_version": "SignedScienceClaimBundle.v0", + "signature_or_digest": "sha256:signed-bundle-labtrust-demo-001", + "reproduce_commands": [ + "labtrust run-demo qc-release", + "labtrust export-pcs --run runs/qc-release --out science_claim_bundle.pending.json" + ], + "verify_commands": [ + "pf verify science-claim signed_science_claim_bundle.json", + "just pcs-validate-bundle BUNDLE=signed_science_claim_bundle.json" + ], + "verification_result": { + "schema_version": "VerificationResult.v0", + "id": "verification-result-labtrust-qc-release", + "created_at": "2026-05-01T12:00:00Z", + "producer": "provability-fabric", + "producer_version": "0.1.0", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "source_commit": "abc123def456", + "status": "ProofChecked", + "signature_or_digest": "sha256:verification-result-001", + "overall_outcome": "pass", + "checks": [ + { + "id": "check-runtime-receipt", + "name": "Runtime receipt hash chain", + "outcome": "pass", + "guarantee_type": "runtime_observed", + "detail": "RuntimeReceipt.v0 digest matches bundle linkage." + }, + { + "id": "check-trace-certificate", + "name": "Trace certificate temporal policy", + "outcome": "pass", + "guarantee_type": "certificate_checked", + "detail": "TraceCertificate.v0 status CertificateChecked." + }, + { + "id": "check-bundle-signature", + "name": "Bundle signature", + "outcome": "pass", + "guarantee_type": "formally_checked", + "detail": "Provability Fabric signing check passed." + } + ] + }, + "science_claim_bundle": { + "schema_version": "ScienceClaimBundle.v0", + "created_at": "2026-05-01T11:30:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "labtrust-qc-release-demo", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:science-claim-bundle-001", + "claim": { + "schema_version": "ClaimArtifact.v0", + "id": "labtrust-qc-release-claim-001", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "labtrust-qc-release-demo", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:claim-artifact-001", + "claim_text": "The qc-release workflow run observed protocol-compliant specimen handling and release gating in the LabTrust-Gym hospital-lab simulation.", + "guarantee_types": { + "formally_checked": true, + "certificate_checked": true, + "runtime_observed": true, + "empirically_measured": false, + "human_reviewed": false, + "unchecked_advisory": false + }, + "output_hashes": [ + { + "name": "claim_digest", + "digest": "sha256:claim-artifact-001", + "algorithm": "sha256" + } + ] + }, + "assumption_set": { + "schema_version": "AssumptionSet.v0", + "id": "labtrust-qc-release-assumptions", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "labtrust-qc-release-demo", + "status": "RuntimeObserved", + "signature_or_digest": "sha256:assumption-set-001", + "assumptions": [ + { + "id": "assume-sim-lab", + "text": "The environment is the LabTrust-Gym simulated hospital laboratory, not a production clinical site.", + "kind": "simulation_scope", + "status": "RuntimeObserved" + }, + { + "id": "assume-qc-policy", + "text": "QC release policy template hospital_lab/qc_release.stl governs acceptable release transitions.", + "kind": "policy", + "status": "CertificateChecked" + } + ] + }, + "runtime_receipt": { + "schema_version": "RuntimeReceipt.v0", + "id": "runtime-receipt-qc-release", + "created_at": "2026-05-01T11:15:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "labtrust-qc-release-demo", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:runtime-receipt-001", + "summary": "Observed workflow steps, instrument events, and release gate transitions for qc-release.", + "trace_hash": "sha256:trace-json-qc-release", + "events_hash": "sha256:events-qc-release", + "payload": { + "run_id": "runs/qc-release", + "steps_observed": 42, + "release_gate": "passed" + } + }, + "trace_certificate": { + "schema_version": "TraceCertificate.v0", + "id": "trace-certificate-qc-release", + "created_at": "2026-05-01T11:20:00Z", + "producer": "CertifyEdge", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/CertifyEdge", + "source_commit": "certifyedge-demo", + "status": "CertificateChecked", + "signature_or_digest": "sha256:trace-certificate-001", + "spec_hash": "sha256:qc-release-stl", + "policy_hash": "sha256:temporal-policy-qc-release", + "summary": "Temporal certificate attests trace.json satisfies qc_release.stl.", + "payload": { + "certificate_status": "valid", + "spec": "templates/hospital_lab/qc_release.stl" + } + } + } +} diff --git a/tests/pcs/test_import_labtrust_bundle.py b/tests/pcs/test_import_labtrust_bundle.py new file mode 100644 index 0000000..c4b10b2 --- /dev/null +++ b/tests/pcs/test_import_labtrust_bundle.py @@ -0,0 +1,58 @@ +import json +import shutil +import tempfile +from pathlib import Path + +from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle + +FIXTURES = Path(__file__).resolve().parent / "fixtures" +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_valid_bundle_imports() -> None: + bundle_path = FIXTURES / "valid_signed_science_claim_bundle.json" + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + result = import_signed_bundle( + bundle_path, + repo_root=root, + write=True, + ) + assert result.claim_id == "labtrust-qc-release-claim-001" + read_model_path = ( + root / "corpus" / "pcs" / "claims" / result.claim_id / "read_model.json" + ) + assert read_model_path.is_file() + read_model = json.loads(read_model_path.read_text(encoding="utf-8")) + assert read_model["claim"]["text"] + assert read_model["source_repositories"] + assert any( + s["source_repo"] == "https://github.com/fraware/LabTrust-Gym" + for s in read_model["source_repositories"] + ) + + +def test_import_preserves_source_repo_and_commit() -> None: + bundle_path = FIXTURES / "valid_signed_science_claim_bundle.json" + raw = json.loads(bundle_path.read_text(encoding="utf-8")) + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + import_signed_bundle(bundle_path, repo_root=root, write=True) + stored = json.loads( + (root / "corpus" / "pcs" / "claims" / "labtrust-qc-release-claim-001" / "signed_bundle.json").read_text( + encoding="utf-8" + ) + ) + scb = stored["science_claim_bundle"] + assert scb["claim"]["source_repo"] == raw["science_claim_bundle"]["claim"]["source_repo"] + assert scb["claim"]["source_commit"] == raw["science_claim_bundle"]["claim"]["source_commit"] + assert scb["claim"]["signature_or_digest"] == raw["science_claim_bundle"]["claim"]["signature_or_digest"] + + +def _copy_schemas(root: Path) -> None: + dest = root / "schemas" / "pcs" + dest.mkdir(parents=True) + for f in (REPO_ROOT / "schemas" / "pcs").glob("*.json"): + shutil.copy(f, dest / f.name) diff --git a/tests/pcs/test_import_verification_result.py b/tests/pcs/test_import_verification_result.py new file mode 100644 index 0000000..4966318 --- /dev/null +++ b/tests/pcs/test_import_verification_result.py @@ -0,0 +1,61 @@ +import json +import shutil +import tempfile +from pathlib import Path + +from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle +from sm_pipeline.pcs_import.verification_result_importer import merge_verification_result +from sm_pipeline.pcs_validate.validator import collect_import_warnings + +FIXTURES = Path(__file__).resolve().parent / "fixtures" +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_missing_verification_result_warning() -> None: + bundle = json.loads( + (FIXTURES / "missing_verification_result.json").read_text(encoding="utf-8") + ) + warnings = collect_import_warnings(bundle) + assert any("VerificationResult is absent" in w for w in warnings) + + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + result = import_signed_bundle( + FIXTURES / "missing_verification_result.json", + repo_root=root, + write=True, + ) + assert any("VerificationResult is absent" in w for w in result.warnings) + read_model = json.loads( + (root / "corpus" / "pcs" / "claims" / result.claim_id / "read_model.json").read_text( + encoding="utf-8" + ) + ) + assert read_model.get("verification_result") is None + + +def test_merge_verification_result_preserves_checks() -> None: + bundle = json.loads( + (FIXTURES / "missing_verification_result.json").read_text(encoding="utf-8") + ) + vr = { + "schema_version": "VerificationResult.v0", + "created_at": "2026-05-01T12:00:00Z", + "producer": "provability-fabric", + "producer_version": "0.1.0", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "source_commit": "abc", + "status": "ProofChecked", + "signature_or_digest": "sha256:vr", + "checks": [{"id": "c1", "name": "test", "outcome": "pass"}], + } + merged = merge_verification_result(bundle, vr) + assert merged["verification_result"]["checks"][0]["id"] == "c1" + + +def _copy_schemas(root: Path) -> None: + dest = root / "schemas" / "pcs" + dest.mkdir(parents=True) + for f in (REPO_ROOT / "schemas" / "pcs").glob("*.json"): + shutil.copy(f, dest / f.name) diff --git a/tests/pcs/test_reject_invalid_bundle.py b/tests/pcs/test_reject_invalid_bundle.py new file mode 100644 index 0000000..7ec566f --- /dev/null +++ b/tests/pcs/test_reject_invalid_bundle.py @@ -0,0 +1,43 @@ +import shutil +import tempfile +from pathlib import Path + +import pytest + +from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle +from sm_pipeline.pcs_validate.validator import BundleValidationError, validate_signed_bundle + +FIXTURES = Path(__file__).resolve().parent / "fixtures" +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_invalid_bundle_rejected() -> None: + import json + + bundle = json.loads( + (FIXTURES / "invalid_missing_signature.json").read_text(encoding="utf-8") + ) + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + with pytest.raises(BundleValidationError): + validate_signed_bundle(bundle, repo_root=root, strict=True) + + +def test_missing_assumption_rejected() -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + with pytest.raises(BundleValidationError): + import_signed_bundle( + FIXTURES / "missing_assumptions.json", + repo_root=root, + write=False, + ) + + +def _copy_schemas(root: Path) -> None: + dest = root / "schemas" / "pcs" + dest.mkdir(parents=True) + for f in (REPO_ROOT / "schemas" / "pcs").glob("*.json"): + shutil.copy(f, dest / f.name) diff --git a/tests/pcs/test_render_pcs_claim.py b/tests/pcs/test_render_pcs_claim.py new file mode 100644 index 0000000..ce39abc --- /dev/null +++ b/tests/pcs/test_render_pcs_claim.py @@ -0,0 +1,89 @@ +import json +import shutil +import tempfile +from pathlib import Path + +from sm_pipeline.pcs_import.artifact_normalizer import LIMITATION_NOTICE, normalize_signed_bundle +from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle + +FIXTURES = Path(__file__).resolve().parent / "fixtures" +REPO_ROOT = Path(__file__).resolve().parents[2] + +REQUIRED_READ_MODEL_KEYS = ( + "claim", + "assumption_set", + "runtime_receipt", + "trace_certificate", + "artifact_hashes", + "source_repositories", + "reproduce_commands", + "verify_commands", + "limitations", + "limitation_notice", +) + + +def test_portal_read_model_has_all_required_sections() -> None: + bundle = json.loads( + (FIXTURES / "valid_signed_science_claim_bundle.json").read_text(encoding="utf-8") + ) + read_model = normalize_signed_bundle(bundle) + for key in REQUIRED_READ_MODEL_KEYS: + assert key in read_model, f"missing read_model.{key}" + + +def test_limitations_notice_present() -> None: + bundle = json.loads( + (FIXTURES / "valid_signed_science_claim_bundle.json").read_text(encoding="utf-8") + ) + read_model = normalize_signed_bundle(bundle) + assert read_model["limitation_notice"] == LIMITATION_NOTICE + assert LIMITATION_NOTICE in read_model["limitations"] + + +def test_artifact_hashes_displayed() -> None: + bundle = json.loads( + (FIXTURES / "valid_signed_science_claim_bundle.json").read_text(encoding="utf-8") + ) + read_model = normalize_signed_bundle(bundle) + assert len(read_model["artifact_hashes"]) >= 1 + assert all("digest" in row and "name" in row for row in read_model["artifact_hashes"]) + + +def test_source_repo_and_commit_displayed() -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + result = import_signed_bundle( + FIXTURES / "valid_signed_science_claim_bundle.json", + repo_root=root, + write=True, + ) + read_model = json.loads( + (root / "corpus" / "pcs" / "claims" / result.claim_id / "read_model.json").read_text( + encoding="utf-8" + ) + ) + repos = {s["source_repo"] for s in read_model["source_repositories"]} + assert "https://github.com/fraware/LabTrust-Gym" in repos + commits = {s["source_commit"] for s in read_model["source_repositories"]} + assert "labtrust-qc-release-demo" in commits + + +def test_status_values_preserved() -> None: + bundle = json.loads( + (FIXTURES / "valid_signed_science_claim_bundle.json").read_text(encoding="utf-8") + ) + read_model = normalize_signed_bundle(bundle) + assert read_model["claim"]["status"] == "RuntimeChecked" + assert read_model["trace_certificate"]["status"] == "CertificateChecked" + vr = read_model["verification_result"] + assert vr is not None + assert vr["status"] == "ProofChecked" + + +def _copy_schemas(root: Path) -> None: + dest = root / "schemas" / "pcs" + dest.mkdir(parents=True) + for f in (REPO_ROOT / "schemas" / "pcs").glob("*.json"): + shutil.copy(f, dest / f.name) From 074c393a8d30a69dbd8c174e9d59ee1ffebe2b83 Mon Sep 17 00:00:00 2001 From: fraware Date: Sat, 16 May 2026 15:43:35 -0700 Subject: [PATCH 2/4] adding pcs core configuration --- docs/pcs-labtrust-import.md | 9 +- pipeline/pyproject.toml | 6 +- .../pcs_import/artifact_normalizer.py | 264 +++++++++++++++--- .../sm_pipeline/pcs_validate/pcs_core_hook.py | 43 +++ .../sm_pipeline/pcs_validate/stale_checker.py | 9 + .../src/sm_pipeline/pcs_validate/validator.py | 38 ++- portal/components/pcs/RuntimeReceiptView.tsx | 5 + .../components/pcs/VerificationResultView.tsx | 2 +- .../signed_science_claim_bundle.schema.json | 9 +- .../valid_signed_pcs_core_bundle.json | 161 +++++++++++ tests/pcs/test_import_pcs_core_bundle.py | 35 +++ 11 files changed, 521 insertions(+), 60 deletions(-) create mode 100644 pipeline/src/sm_pipeline/pcs_validate/pcs_core_hook.py create mode 100644 tests/pcs/fixtures/valid_signed_pcs_core_bundle.json create mode 100644 tests/pcs/test_import_pcs_core_bundle.py diff --git a/docs/pcs-labtrust-import.md b/docs/pcs-labtrust-import.md index 4478883..de061c9 100644 --- a/docs/pcs-labtrust-import.md +++ b/docs/pcs-labtrust-import.md @@ -10,7 +10,14 @@ Scientific Memory imports **signed** `ScienceClaimBundle` artifacts produced by - Optional top-level `verification_result` (`VerificationResult.v0`) - Top-level `signature_or_digest` -Canonical artifact vocabulary is defined in [pcs-core](https://github.com/SentinelOps-CI/pcs-core). This repository validates against vendored JSON Schemas under `schemas/pcs/` and will call `pcs_core` when that package is installed. +Canonical artifact vocabulary is defined in [pcs-core](https://github.com/SentinelOps-CI/pcs-core). This repository validates against vendored JSON Schemas under `schemas/pcs/` and calls `pcs_core` (editable path dependency in `pipeline/pyproject.toml`) for canonical `ScienceClaimBundle.v0` and `VerificationResult.v0` shapes. + +Two bundle shapes are supported: + +| Shape | Nested claim | Receipt / certificate | +|-------|----------------|----------------------| +| LabTrust portal (legacy) | `science_claim_bundle.claim` | singular `runtime_receipt`, `trace_certificate` | +| pcs-core / Provability Fabric | `science_claim_bundle.claim_artifact` | `runtime_receipts[]`, `certificates[]` | ## Commands diff --git a/pipeline/pyproject.toml b/pipeline/pyproject.toml index d068f10..9724d28 100644 --- a/pipeline/pyproject.toml +++ b/pipeline/pyproject.toml @@ -10,9 +10,13 @@ dependencies = [ "rich>=13.7", "networkx>=3.3", "httpx>=0.27", - "python-dotenv>=1.0" + "python-dotenv>=1.0", + "pcs-core>=0.1.0", ] +[tool.uv.sources] +pcs-core = { path = "../../pcs-core/python", editable = true } + [project.optional-dependencies] mcp = ["mcp>=1.0"] diff --git a/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py b/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py index 98c9ebf..7c50625 100644 --- a/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py +++ b/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from typing import Any GUARANTEE_KEYS = ( @@ -20,10 +21,90 @@ "about a real hospital laboratory." ) +_CHECK_OUTCOME_MAP = { + "passed": "pass", + "failed": "fail", + "skipped": "skip", + "warning": "warn", + "pass": "pass", + "fail": "fail", + "skip": "skip", + "warn": "warn", +} -def _hash_rows(artifact: dict[str, Any] | None) -> list[dict[str, str]]: - if not isinstance(artifact, dict): - return [] + +def _artifact_id(artifact: dict[str, Any]) -> str: + for key in ( + "id", + "artifact_id", + "receipt_id", + "certificate_id", + "assumption_set_id", + "bundle_id", + "verification_id", + ): + val = artifact.get(key) + if isinstance(val, str) and val.strip(): + return val.strip() + return "" + + +def _get_claim(scb: dict[str, Any]) -> dict[str, Any]: + claim = scb.get("claim_artifact") or scb.get("claim") + return claim if isinstance(claim, dict) else {} + + +def _get_runtime_receipt(scb: dict[str, Any]) -> dict[str, Any]: + receipt = scb.get("runtime_receipt") + if isinstance(receipt, dict): + return receipt + receipts = scb.get("runtime_receipts") + if isinstance(receipts, list) and receipts and isinstance(receipts[0], dict): + return receipts[0] + return {} + + +def _get_trace_certificate(scb: dict[str, Any]) -> dict[str, Any]: + cert = scb.get("trace_certificate") + if isinstance(cert, dict): + return cert + certificates = scb.get("certificates") + if isinstance(certificates, list) and certificates and isinstance(certificates[0], dict): + return certificates[0] + return {} + + +def _normalize_assumption(raw: dict[str, Any]) -> dict[str, str]: + return { + "id": str(raw.get("id") or raw.get("assumption_id") or ""), + "text": str(raw.get("text") or ""), + "kind": str(raw.get("kind") or "") or None, + "status": str(raw.get("status") or "") or None, + } + + +def _normalize_assumption_set(raw: dict[str, Any]) -> dict[str, Any]: + assumptions = raw.get("assumptions") + normalized_assumptions: list[dict[str, str | None]] = [] + if isinstance(assumptions, list): + for item in assumptions: + if isinstance(item, dict): + normalized_assumptions.append(_normalize_assumption(item)) + out = dict(raw) + out["id"] = _artifact_id(raw) or out.get("id", "") + out["assumptions"] = normalized_assumptions + return out + + +def _normalize_named_artifact(raw: dict[str, Any]) -> dict[str, Any]: + out = dict(raw) + aid = _artifact_id(raw) + if aid: + out["id"] = aid + return out + + +def _hash_rows_from_list_entries(artifact: dict[str, Any]) -> list[dict[str, str]]: rows: list[dict[str, str]] = [] for field in ("input_hashes", "output_hashes"): entries = artifact.get(field) @@ -40,9 +121,35 @@ def _hash_rows(artifact: dict[str, Any] | None) -> list[dict[str, str]]: "name": name, "digest": digest, "algorithm": str(entry.get("algorithm") or "sha256"), - "source_artifact": str(artifact.get("id") or field), + "source_artifact": str(artifact.get("id") or _artifact_id(artifact) or field), } ) + return rows + + +def _hash_rows_from_map_entries(artifact: dict[str, Any]) -> list[dict[str, str]]: + rows: list[dict[str, str]] = [] + for field in ("input_hashes", "output_hashes", "artifact_hashes"): + entries = artifact.get(field) + if not isinstance(entries, dict): + continue + for name, digest in entries.items(): + if isinstance(digest, str) and digest.strip(): + rows.append( + { + "name": str(name), + "digest": digest.strip(), + "algorithm": "sha256", + "source_artifact": str(artifact.get("id") or _artifact_id(artifact) or field), + } + ) + return rows + + +def _hash_rows(artifact: dict[str, Any] | None) -> list[dict[str, str]]: + if not isinstance(artifact, dict): + return [] + rows = _hash_rows_from_list_entries(artifact) + _hash_rows_from_map_entries(artifact) for key in ("trace_hash", "policy_hash", "events_hash", "spec_hash"): val = artifact.get(key) if isinstance(val, str) and val.strip(): @@ -51,9 +158,19 @@ def _hash_rows(artifact: dict[str, Any] | None) -> list[dict[str, str]]: "name": key, "digest": val.strip(), "algorithm": "sha256", - "source_artifact": str(artifact.get("id") or "metadata"), + "source_artifact": str(artifact.get("id") or _artifact_id(artifact) or "metadata"), } ) + sig = artifact.get("signature_or_digest") + if isinstance(sig, str) and sig.strip(): + rows.append( + { + "name": "signature_or_digest", + "digest": sig.strip(), + "algorithm": "sha256", + "source_artifact": str(artifact.get("id") or _artifact_id(artifact) or "artifact"), + } + ) return rows @@ -66,30 +183,104 @@ def _source_metadata(artifact: dict[str, Any] | None) -> dict[str, str]: } +def _normalize_verification_result(vr: dict[str, Any] | None) -> dict[str, Any] | None: + if not isinstance(vr, dict): + return None + checks_out: list[dict[str, str]] = [] + for check in vr.get("checks") or []: + if not isinstance(check, dict): + continue + raw_outcome = check.get("outcome") or check.get("status") or "" + outcome = _CHECK_OUTCOME_MAP.get(str(raw_outcome).lower(), str(raw_outcome)) + details = check.get("details") + detail_text = check.get("detail") + if detail_text is None and isinstance(details, dict): + detail_text = json.dumps(details, sort_keys=True) + elif detail_text is None and details is not None: + detail_text = str(details) + checks_out.append( + { + "id": str(check.get("id") or check.get("check_id") or ""), + "name": str(check.get("name") or check.get("description") or ""), + "outcome": outcome, + "detail": str(detail_text or ""), + "guarantee_type": str(check.get("guarantee_type") or "") or None, + } + ) + overall = vr.get("overall_outcome") + if overall is None and vr.get("status"): + status = str(vr["status"]) + if status in ("ProofChecked", "CertificateChecked", "RuntimeChecked"): + overall = "pass" + return { + "id": _artifact_id(vr) or vr.get("verification_id"), + "status": str(vr.get("status") or ""), + "overall_outcome": overall, + "signature_or_digest": str(vr.get("signature_or_digest") or ""), + "source_repo": str(vr.get("source_repo") or ""), + "source_commit": str(vr.get("source_commit") or ""), + "checks": checks_out, + } + + +def _infer_guarantee_types( + claim: dict[str, Any], + runtime_receipt: dict[str, Any], + trace_certificate: dict[str, Any], + verification_result: dict[str, Any] | None, +) -> dict[str, bool]: + guarantee_types = claim.get("guarantee_types") + if isinstance(guarantee_types, dict): + return {k: bool(guarantee_types.get(k)) for k in GUARANTEE_KEYS} + + inferred = {k: False for k in GUARANTEE_KEYS} + runtime_status = str(runtime_receipt.get("status") or "") + inferred["runtime_observed"] = runtime_status in ("RuntimeObserved", "RuntimeChecked") + inferred["certificate_checked"] = str(trace_certificate.get("status") or "") == ( + "CertificateChecked" + ) + if isinstance(verification_result, dict): + for check in verification_result.get("checks") or []: + if not isinstance(check, dict): + continue + gt = str(check.get("guarantee_type") or "") + outcome = str(check.get("outcome") or "") + if gt in GUARANTEE_KEYS and outcome == "pass": + inferred[gt] = True + if outcome == "pass" and gt == "formally_checked": + inferred["formally_checked"] = True + return inferred + + def normalize_signed_bundle(bundle: dict[str, Any]) -> dict[str, Any]: """Build durable portal read model from a signed ScienceClaimBundle.""" scb = bundle["science_claim_bundle"] - claim = scb["claim"] - claim_id = str(claim["id"]) + claim_raw = _get_claim(scb) + claim_id = _artifact_id(claim_raw) - vr = bundle.get("verification_result") - if vr is None: - vr = scb.get("verification_result") + vr_raw = bundle.get("verification_result") + if vr_raw is None: + vr_raw = scb.get("verification_result") + verification_result = _normalize_verification_result( + vr_raw if isinstance(vr_raw, dict) else None + ) - assumption_set = scb.get("assumption_set") or {} - runtime_receipt = scb.get("runtime_receipt") or {} - trace_certificate = scb.get("trace_certificate") or {} + assumption_set = _normalize_assumption_set( + scb.get("assumption_set") if isinstance(scb.get("assumption_set"), dict) else {} + ) + runtime_receipt = _normalize_named_artifact(_get_runtime_receipt(scb)) + trace_certificate = _normalize_named_artifact(_get_trace_certificate(scb)) - hash_artifacts = [claim, assumption_set, runtime_receipt, trace_certificate] - if isinstance(vr, dict): - hash_artifacts.append(vr) + hash_artifacts: list[dict[str, Any]] = [claim_raw, assumption_set, runtime_receipt, trace_certificate] + if isinstance(vr_raw, dict): + hash_artifacts.append(vr_raw) evidence = scb.get("evidence_bundle") if isinstance(evidence, dict): hash_artifacts.append(evidence) artifact_hashes: list[dict[str, str]] = [] for art in hash_artifacts: - artifact_hashes.extend(_hash_rows(art if isinstance(art, dict) else None)) + artifact_hashes.extend(_hash_rows(art)) sources: list[dict[str, str]] = [] seen: set[tuple[str, str]] = set() @@ -105,49 +296,42 @@ def normalize_signed_bundle(bundle: dict[str, Any]) -> dict[str, Any]: reproduce = list(bundle.get("reproduce_commands") or scb.get("reproduce_commands") or []) verify = list(bundle.get("verify_commands") or scb.get("verify_commands") or []) + if not verify: + verify = [ + "pf verify science-claim signed_science_claim_bundle.json", + "just pcs-validate-bundle BUNDLE=signed_science_claim_bundle.json", + ] limitations = list(scb.get("limitations") or []) if LIMITATION_NOTICE not in limitations: limitations = [LIMITATION_NOTICE, *limitations] - guarantee_types = claim.get("guarantee_types") - if not isinstance(guarantee_types, dict): - guarantee_types = {k: False for k in GUARANTEE_KEYS} - guarantee_types["runtime_observed"] = str(runtime_receipt.get("status")) in ( - "RuntimeObserved", - "RuntimeChecked", - ) - guarantee_types["certificate_checked"] = str(trace_certificate.get("status")) == ( - "CertificateChecked" - ) - if isinstance(vr, dict): - for check in vr.get("checks") or []: - if not isinstance(check, dict): - continue - gt = str(check.get("guarantee_type") or "") - if gt in GUARANTEE_KEYS and check.get("outcome") == "pass": - guarantee_types[gt] = True + guarantee_types = _infer_guarantee_types( + claim_raw, runtime_receipt, trace_certificate, verification_result + ) return { "schema_version": "PcsClaimReadModel.v0", "claim_id": claim_id, "claim": { "id": claim_id, - "text": str(claim.get("claim_text") or ""), - "status": str(claim.get("status") or ""), - "signature_or_digest": str(claim.get("signature_or_digest") or ""), + "text": str(claim_raw.get("claim_text") or ""), + "status": str(claim_raw.get("status") or ""), + "signature_or_digest": str(claim_raw.get("signature_or_digest") or ""), "guarantee_types": guarantee_types, - **{k: claim.get(k) for k in ("producer", "producer_version", "created_at")}, + **{k: claim_raw.get(k) for k in ("producer", "producer_version", "created_at")}, }, "assumption_set": assumption_set, "runtime_receipt": runtime_receipt, "trace_certificate": trace_certificate, - "verification_result": vr, + "verification_result": verification_result, "artifact_hashes": artifact_hashes, "source_repositories": sources, "reproduce_commands": reproduce, "verify_commands": verify, "limitations": limitations, "limitation_notice": LIMITATION_NOTICE, - "bundle_signature_or_digest": str(bundle.get("signature_or_digest") or ""), + "bundle_signature_or_digest": str( + bundle.get("signature_or_digest") or bundle.get("bundle_digest") or "" + ), } diff --git a/pipeline/src/sm_pipeline/pcs_validate/pcs_core_hook.py b/pipeline/src/sm_pipeline/pcs_validate/pcs_core_hook.py new file mode 100644 index 0000000..59c719c --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_validate/pcs_core_hook.py @@ -0,0 +1,43 @@ +"""Optional validation via pcs-core (canonical PCS protocol).""" + +from __future__ import annotations + +from typing import Any + + +def is_pcs_core_science_claim_bundle(scb: dict[str, Any]) -> bool: + if scb.get("schema_version") == "v0": + return True + return "claim_artifact" in scb + + +def is_pcs_core_verification_result(vr: dict[str, Any]) -> bool: + return "verification_id" in vr and "verifier" in vr + + +def validate_with_pcs_core(bundle: dict[str, Any]) -> list[str]: + """Run pcs-core schema + semantic validation when the package is installed.""" + try: + from pcs_core.validate import ValidationError as PcsCoreValidationError + from pcs_core.validate import validate_artifact + except ImportError: + return [] + + errors: list[str] = [] + scb = bundle.get("science_claim_bundle") + if isinstance(scb, dict) and is_pcs_core_science_claim_bundle(scb): + try: + validate_artifact(scb, "ScienceClaimBundle.v0") + except PcsCoreValidationError as exc: + errors.extend(exc.errors or [str(exc)]) + + vr = bundle.get("verification_result") + if vr is None and isinstance(scb, dict): + vr = scb.get("verification_result") + if isinstance(vr, dict) and is_pcs_core_verification_result(vr): + try: + validate_artifact(vr, "VerificationResult.v0") + except PcsCoreValidationError as exc: + errors.extend(exc.errors or [str(exc)]) + + return errors diff --git a/pipeline/src/sm_pipeline/pcs_validate/stale_checker.py b/pipeline/src/sm_pipeline/pcs_validate/stale_checker.py index 192e41f..2bc571d 100644 --- a/pipeline/src/sm_pipeline/pcs_validate/stale_checker.py +++ b/pipeline/src/sm_pipeline/pcs_validate/stale_checker.py @@ -17,6 +17,7 @@ def find_stale_artifacts(bundle: dict[str, Any]) -> list[str]: for key in ( "claim", + "claim_artifact", "assumption_set", "runtime_receipt", "trace_certificate", @@ -27,6 +28,14 @@ def find_stale_artifacts(bundle: dict[str, Any]) -> list[str]: if isinstance(artifact, dict): _check_artifact(f"science_claim_bundle.{key}", artifact, stale) + for idx, receipt in enumerate(scb.get("runtime_receipts") or []): + if isinstance(receipt, dict): + _check_artifact(f"science_claim_bundle.runtime_receipts.{idx}", receipt, stale) + + for idx, cert in enumerate(scb.get("certificates") or []): + if isinstance(cert, dict): + _check_artifact(f"science_claim_bundle.certificates.{idx}", cert, stale) + vr = bundle.get("verification_result") if isinstance(vr, dict): _check_artifact("verification_result", vr, stale) diff --git a/pipeline/src/sm_pipeline/pcs_validate/validator.py b/pipeline/src/sm_pipeline/pcs_validate/validator.py index 5c33cc6..a617a08 100644 --- a/pipeline/src/sm_pipeline/pcs_validate/validator.py +++ b/pipeline/src/sm_pipeline/pcs_validate/validator.py @@ -10,6 +10,12 @@ from referencing import Registry, Resource from referencing.jsonschema import DRAFT202012 +from sm_pipeline.pcs_validate.pcs_core_hook import ( + is_pcs_core_science_claim_bundle, + is_pcs_core_verification_result, + validate_with_pcs_core, +) + PCS_SCHEMA_BASE_URI = "https://scientific-memory.org/schemas/pcs/" @@ -51,18 +57,6 @@ def validator_for(schema_name: str, repo_root: Path) -> Draft202012Validator: return Draft202012Validator(schema, registry=registry) -def _try_pcs_core_validate(bundle: dict[str, Any]) -> None: - """Optional pcs-core validation hook (no-op if package unavailable).""" - try: - from pcs_core.validate import validate_signed_science_claim_bundle # type: ignore - except ImportError: - try: - from pcs_core import validate_signed_science_claim_bundle # type: ignore - except ImportError: - return - validate_signed_science_claim_bundle(bundle) - - def validate_signed_bundle( bundle: dict[str, Any], *, @@ -79,14 +73,14 @@ def validate_signed_bundle( errors: list[str] = [] if strict: - _try_pcs_core_validate(bundle) + errors.extend(validate_with_pcs_core(bundle)) validator = validator_for("signed_science_claim_bundle.schema.json", root) for err in sorted(validator.iter_errors(bundle), key=lambda e: e.path): errors.append(err.message) scb = bundle.get("science_claim_bundle") - if isinstance(scb, dict): + if isinstance(scb, dict) and not is_pcs_core_science_claim_bundle(scb): scb_validator = validator_for("science_claim_bundle.schema.json", root) for err in sorted(scb_validator.iter_errors(scb), key=lambda e: e.path): errors.append(f"science_claim_bundle: {err.message}") @@ -100,10 +94,20 @@ def validate_signed_bundle( if not assumptions: errors.append("science_claim_bundle.assumption_set.assumptions is required") + if isinstance(scb, dict) and is_pcs_core_science_claim_bundle(scb): + assumption_set = scb.get("assumption_set") + assumptions = ( + assumption_set.get("assumptions") + if isinstance(assumption_set, dict) + else None + ) + if not assumptions: + errors.append("science_claim_bundle.assumption_set.assumptions is required") + vr = bundle.get("verification_result") if vr is None and isinstance(scb, dict): vr = scb.get("verification_result") - if isinstance(vr, dict): + if isinstance(vr, dict) and not is_pcs_core_verification_result(vr): vr_validator = validator_for("verification_result.schema.json", root) for err in sorted(vr_validator.iter_errors(vr), key=lambda e: e.path): errors.append(f"verification_result: {err.message}") @@ -128,6 +132,10 @@ def collect_import_warnings(bundle: dict[str, Any]) -> list[str]: warnings.append("VerificationResult is absent; import proceeds with advisory only.") trace_cert = scb.get("trace_certificate") + if not isinstance(trace_cert, dict): + certificates = scb.get("certificates") + if isinstance(certificates, list) and certificates and isinstance(certificates[0], dict): + trace_cert = certificates[0] if isinstance(trace_cert, dict): status = str(trace_cert.get("status") or "") if status != "CertificateChecked": diff --git a/portal/components/pcs/RuntimeReceiptView.tsx b/portal/components/pcs/RuntimeReceiptView.tsx index f3f2418..9fc3f94 100644 --- a/portal/components/pcs/RuntimeReceiptView.tsx +++ b/portal/components/pcs/RuntimeReceiptView.tsx @@ -20,6 +20,11 @@ export function RuntimeReceiptView({ receipt }: RuntimeReceiptViewProps) { {receipt.summary != null && (

    {String(receipt.summary)}

    )} + {receipt.trace_hash != null && ( +

    + trace_hash: {String(receipt.trace_hash)} +

    + )} {receipt.payload != null && (
               {JSON.stringify(receipt.payload, null, 2)}
    diff --git a/portal/components/pcs/VerificationResultView.tsx b/portal/components/pcs/VerificationResultView.tsx
    index 8d35536..9d84730 100644
    --- a/portal/components/pcs/VerificationResultView.tsx
    +++ b/portal/components/pcs/VerificationResultView.tsx
    @@ -39,7 +39,7 @@ export function VerificationResultView({ result }: VerificationResultViewProps)
           

      {checks.map((c) => ( -
    • +
    • {c.name} F[0,24h] verified)", + "certificate_refs": ["cert-trace-qc-release-v0.1"], + "runtime_receipt_refs": ["receipt-qc-release-run-001"], + "created_at": "2026-05-16T12:05:00Z", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature_or_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222" + }, + "assumption_set": { + "assumption_set_id": "as-labtrust-qc-v0.1", + "schema_version": "v0", + "created_at": "2026-05-16T12:00:00Z", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "assumptions": [ + { + "assumption_id": "asm-domain-qc-sim", + "text": "Simulation models a hospital QC release workflow, not a live clinical laboratory.", + "kind": "domain", + "status": "HumanReviewed", + "source_span_refs": ["span-qc-release-spec-1"] + } + ], + "human_review_status": "approved", + "status": "HumanReviewed", + "signature_or_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "runtime_receipts": [ + { + "receipt_id": "receipt-qc-release-run-001", + "schema_version": "v0", + "run_id": "runs/qc-release", + "environment": { "platform": "linux", "labtrust_version": "0.1.0" }, + "started_at": "2026-05-16T11:58:00Z", + "ended_at": "2026-05-16T12:00:00Z", + "status": "RuntimeObserved", + "events_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "policy_hash": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "trace_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "input_hashes": { + "spec": "sha256:6666666666666666666666666666666666666666666666666666666666666666" + }, + "output_hashes": { + "trace.json": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + }, + "signature_or_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777" + } + ], + "certificates": [ + { + "certificate_id": "cert-trace-qc-release-v0.1", + "schema_version": "v0", + "trace_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "spec_hash": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "property_id": "qc_release.temporal.safety", + "checker": "certifyedge", + "checker_version": "0.1.0", + "status": "CertificateChecked", + "counterexample_ref": null, + "created_at": "2026-05-16T12:10:00Z", + "producer": "certifyedge", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/CertifyEdge", + "source_commit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "signature_or_digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888" + } + ], + "evidence_bundle": { + "bundle_id": "evidence-qc-release-v0.1", + "schema_version": "v0", + "claim_refs": ["claim-qc-release-v0.1"], + "assumption_set_refs": ["as-labtrust-qc-v0.1"], + "runtime_receipt_refs": ["receipt-qc-release-run-001"], + "certificate_refs": ["cert-trace-qc-release-v0.1"], + "artifact_hashes": { + "claim-qc-release-v0.1": "sha256:2222222222222222222222222222222222222222222222222222222222222222" + }, + "created_at": "2026-05-16T12:12:00Z", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature_or_digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999" + }, + "verification_policy": { + "policy_id": "labtrust-v0.1-qc-release", + "required_checks": ["schema-valid", "trace-hash-alignment"] + }, + "created_at": "2026-05-16T12:15:00Z", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "signature_or_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } +} diff --git a/tests/pcs/test_import_pcs_core_bundle.py b/tests/pcs/test_import_pcs_core_bundle.py new file mode 100644 index 0000000..49bcf88 --- /dev/null +++ b/tests/pcs/test_import_pcs_core_bundle.py @@ -0,0 +1,35 @@ +import json +import shutil +import tempfile +from pathlib import Path + +from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle + +FIXTURES = Path(__file__).resolve().parent / "fixtures" +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_pcs_core_signed_bundle_imports() -> None: + bundle_path = FIXTURES / "valid_signed_pcs_core_bundle.json" + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + result = import_signed_bundle(bundle_path, repo_root=root, write=True) + assert result.claim_id == "claim-qc-release-v0.1" + read_model = json.loads( + (root / "corpus" / "pcs" / "claims" / result.claim_id / "read_model.json").read_text( + encoding="utf-8" + ) + ) + assert read_model["claim"]["text"] + assert read_model["trace_certificate"]["status"] == "CertificateChecked" + assert read_model["verification_result"] is not None + assert read_model["verification_result"]["checks"][0]["outcome"] == "pass" + assert any(h["name"] == "trace_hash" for h in read_model["artifact_hashes"]) + + +def _copy_schemas(root: Path) -> None: + dest = root / "schemas" / "pcs" + dest.mkdir(parents=True) + for f in (REPO_ROOT / "schemas" / "pcs").glob("*.json"): + shutil.copy(f, dest / f.name) From f1215258a35add8bbd0237b9170fd475011fb51e Mon Sep 17 00:00:00 2001 From: fraware Date: Sun, 17 May 2026 00:57:27 -0700 Subject: [PATCH 3/4] PCS pcs-core alignment, integration tests, and portal setup Co-authored-by: Cursor --- .github/workflows/corpus-validation.yml | 10 + JUSTFILE | 58 ++- .../import_manifest.json | 6 + .../claim-qc-release-v0.1/read_model.json | 293 ++++++++++++++ .../scientific_memory_import_report.json | 9 + .../claim-qc-release-v0.1/signed_bundle.json | 183 +++++++++ .../read_model.json | 71 +++- .../scientific_memory_import_report.json | 9 + docs/pcs-labtrust-import.md | 58 ++- docs/pcs-rendering-contract.md | 27 +- pipeline/pyproject.toml | 10 +- .../pcs_import/artifact_normalizer.py | 43 ++- .../sm_pipeline/pcs_import/bundle_utils.py | 15 + .../science_claim_bundle_importer.py | 32 +- .../src/sm_pipeline/pcs_validate/__init__.py | 24 +- .../pcs_validate/bundle_detection.py | 37 ++ .../pcs_validate/bundle_semantics.py | 204 ++++++++++ .../sm_pipeline/pcs_validate/pcs_core_hook.py | 53 +-- .../pcs_validate/schema_registry.py | 34 ++ .../src/sm_pipeline/pcs_validate/validator.py | 169 ++++---- .../src/sm_pipeline/validate/gate_engine.py | 7 + .../src/sm_pipeline/validate/pcs_corpus.py | 95 +++++ portal/.generated/pcs-export.json | 365 +++++++++++++++++- portal/components/pcs/ArtifactHashTable.tsx | 42 +- portal/components/pcs/EvidenceBundleView.tsx | 34 ++ portal/components/pcs/PcsClaimPage.tsx | 9 +- .../components/pcs/VerificationResultView.tsx | 68 +++- portal/lib/pcsTypes.ts | 16 +- portal/scripts/generate-search-index.mjs | 19 +- pyproject.toml | 1 + schemas/pcs/AssumptionSet.v0.schema.json | 53 +++ schemas/pcs/ClaimArtifact.v0.schema.json | 54 +++ schemas/pcs/EvidenceBundle.v0.schema.json | 37 ++ schemas/pcs/RuntimeReceipt.v0.schema.json | 55 +++ schemas/pcs/SCHEMA_MIRROR.json | 21 + schemas/pcs/ScienceClaimBundle.v0.schema.json | 57 +++ .../SignedScienceClaimBundle.v0.schema.json | 30 ++ schemas/pcs/SourceSpan.v0.schema.json | 55 +++ schemas/pcs/TraceCertificate.v0.schema.json | 43 +++ schemas/pcs/VerificationResult.v0.schema.json | 49 +++ schemas/pcs/common.defs.json | 73 ++++ ...LabTrust.ScienceClaimBundle.v0.schema.json | 32 ++ ...st.SignedScienceClaimBundle.v0.schema.json | 23 ++ ...LabTrust.VerificationResult.v0.schema.json | 25 ++ schemas/pcs/science_claim_bundle.schema.json | 106 +---- .../signed_science_claim_bundle.schema.json | 33 +- schemas/pcs/verification_result.schema.json | 47 +-- scripts/bootstrap.sh | 4 + scripts/just_env.sh | 2 + scripts/run_pcs_integration_tests.sh | 17 + scripts/sm_python.sh | 41 ++ scripts/sync_pcs_schemas.py | 80 ++++ tests/pcs/__init__.py | 0 tests/pcs/conftest.py | 32 +- .../fixtures/failed_verification_result.json | 81 ++++ .../pcs/fixtures/missing_assumption_set.json | 62 +++ .../pcs/fixtures/valid_pf_handoff_bundle.json | 9 + .../valid_signed_pcs_core_bundle.json | 88 ++--- tests/pcs/schema_fixtures.py | 20 + tests/pcs/test_import_labtrust_bundle.py | 58 --- tests/pcs/test_import_pcs_core_bundle.py | 35 -- tests/pcs/test_import_verification_result.py | 61 --- tests/pcs/test_pcs_import.py | 130 +++++++ tests/pcs/test_pcs_integration.py | 57 +++ ...render_pcs_claim.py => test_pcs_render.py} | 52 +-- tests/pcs/test_reject_invalid_bundle.py | 43 --- uv.lock | 30 +- 67 files changed, 3068 insertions(+), 628 deletions(-) create mode 100644 corpus/pcs/claims/claim-qc-release-v0.1/import_manifest.json create mode 100644 corpus/pcs/claims/claim-qc-release-v0.1/read_model.json create mode 100644 corpus/pcs/claims/claim-qc-release-v0.1/scientific_memory_import_report.json create mode 100644 corpus/pcs/claims/claim-qc-release-v0.1/signed_bundle.json create mode 100644 corpus/pcs/claims/labtrust-qc-release-claim-001/scientific_memory_import_report.json create mode 100644 pipeline/src/sm_pipeline/pcs_import/bundle_utils.py create mode 100644 pipeline/src/sm_pipeline/pcs_validate/bundle_detection.py create mode 100644 pipeline/src/sm_pipeline/pcs_validate/bundle_semantics.py create mode 100644 pipeline/src/sm_pipeline/pcs_validate/schema_registry.py create mode 100644 pipeline/src/sm_pipeline/validate/pcs_corpus.py create mode 100644 portal/components/pcs/EvidenceBundleView.tsx create mode 100644 schemas/pcs/AssumptionSet.v0.schema.json create mode 100644 schemas/pcs/ClaimArtifact.v0.schema.json create mode 100644 schemas/pcs/EvidenceBundle.v0.schema.json create mode 100644 schemas/pcs/RuntimeReceipt.v0.schema.json create mode 100644 schemas/pcs/SCHEMA_MIRROR.json create mode 100644 schemas/pcs/ScienceClaimBundle.v0.schema.json create mode 100644 schemas/pcs/SignedScienceClaimBundle.v0.schema.json create mode 100644 schemas/pcs/SourceSpan.v0.schema.json create mode 100644 schemas/pcs/TraceCertificate.v0.schema.json create mode 100644 schemas/pcs/VerificationResult.v0.schema.json create mode 100644 schemas/pcs/common.defs.json create mode 100644 schemas/pcs/legacy/LabTrust.ScienceClaimBundle.v0.schema.json create mode 100644 schemas/pcs/legacy/LabTrust.SignedScienceClaimBundle.v0.schema.json create mode 100644 schemas/pcs/legacy/LabTrust.VerificationResult.v0.schema.json create mode 100644 scripts/run_pcs_integration_tests.sh create mode 100644 scripts/sm_python.sh create mode 100644 scripts/sync_pcs_schemas.py create mode 100644 tests/pcs/__init__.py create mode 100644 tests/pcs/fixtures/failed_verification_result.json create mode 100644 tests/pcs/fixtures/missing_assumption_set.json create mode 100644 tests/pcs/fixtures/valid_pf_handoff_bundle.json create mode 100644 tests/pcs/schema_fixtures.py delete mode 100644 tests/pcs/test_import_labtrust_bundle.py delete mode 100644 tests/pcs/test_import_pcs_core_bundle.py delete mode 100644 tests/pcs/test_import_verification_result.py create mode 100644 tests/pcs/test_pcs_import.py create mode 100644 tests/pcs/test_pcs_integration.py rename tests/pcs/{test_render_pcs_claim.py => test_pcs_render.py} (65%) delete mode 100644 tests/pcs/test_reject_invalid_bundle.py diff --git a/.github/workflows/corpus-validation.yml b/.github/workflows/corpus-validation.yml index 794cc7e..cbcc0c8 100644 --- a/.github/workflows/corpus-validation.yml +++ b/.github/workflows/corpus-validation.yml @@ -11,6 +11,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Checkout pcs-core (canonical PCS schemas) + uses: actions/checkout@v4 + with: + repository: SentinelOps-CI/pcs-core + path: pcs-core + continue-on-error: true + - uses: astral-sh/setup-uv@v4 - name: Install Python deps @@ -25,6 +32,9 @@ jobs: - name: Run pipeline tests run: uv run --project pipeline pytest + - name: Run PCS contract tests + run: uv run --project pipeline pytest ../tests/pcs + - name: Run kernel tests (numeric witness) run: uv run --project kernels/adsorption pytest diff --git a/JUSTFILE b/JUSTFILE index e7f3502..1451a90 100644 --- a/JUSTFILE +++ b/JUSTFILE @@ -52,7 +52,11 @@ repo-snapshot: # Alias for SPEC 17 contributor flow (same as validate; no nested just) validate-corpus: validate -portal: +# Install portal deps first (pnpm workspace; node_modules lives under portal/) +portal-install: + pnpm install + +portal: portal-install pnpm --dir portal dev test: @@ -176,12 +180,46 @@ mcp-server: metrics *ARGS: uv run --project pipeline python -m sm_pipeline.cli metrics {{ARGS}} -# PCS LabTrust v0.1: import, validate, and render proof-carrying science claims -pcs-import-bundle BUNDLE: - uv run --project pipeline python -m sm_pipeline.cli pcs-import-bundle --bundle {{BUNDLE}} - -pcs-validate-bundle BUNDLE: - uv run --project pipeline python -m sm_pipeline.cli pcs-validate-bundle --bundle {{BUNDLE}} - -pcs-render-claim CLAIM_ID: - uv run --project pipeline python -m sm_pipeline.cli pcs-render-claim --claim-id {{CLAIM_ID}} +# PCS LabTrust v0.1 (just uses positional args, not make-style VAR=value) +# just pcs-import-bundle tests/pcs/fixtures/valid_signed_science_claim_bundle.json +# just pcs-render-claim labtrust-qc-release-claim-001 +pcs-import-bundle bundle: + bash scripts/sm_python.sh -m sm_pipeline.cli pcs-import-bundle --bundle "{{bundle}}" + +pcs-validate-bundle bundle: + bash scripts/sm_python.sh -m sm_pipeline.cli pcs-validate-bundle --bundle "{{bundle}}" + +pcs-render-claim claim_id: + bash scripts/sm_python.sh -m sm_pipeline.cli pcs-render-claim --claim-id "{{claim_id}}" + +# Optional: uv sync --project pipeline --extra pcs (requires ../../pcs-core/python) +sync-pipeline-pcs: + @if [ -d "pcs-core/python" ] || [ -d "../pcs-core/python" ]; then \ + uv sync --native-tls --project pipeline --extra pcs; \ + else \ + echo "pcs-core not found; using schema mirrors only (legacy import still works)"; \ + uv sync --native-tls --project pipeline; \ + fi + +# Copy canonical schemas from pcs-core (sibling ../pcs-core or repo pcs-core/) +sync-pcs-schemas: + uv run python scripts/sync_pcs_schemas.py + +pcs-import-labtrust-demo: + just pcs-import-bundle tests/pcs/fixtures/valid_signed_science_claim_bundle.json + +pcs-import-pcs-core-demo: + just sync-pipeline-pcs + just pcs-import-bundle tests/pcs/fixtures/valid_signed_pcs_core_bundle.json + +pcs-refresh-demo: + just pcs-import-bundle tests/pcs/fixtures/valid_signed_science_claim_bundle.json + just sync-pipeline-pcs + just pcs-import-bundle tests/pcs/fixtures/valid_signed_pcs_core_bundle.json + just pcs-render-claim labtrust-qc-release-claim-001 + just pcs-render-claim claim-qc-release-v0.1 + +# Live pcs-core validation (requires: just sync-pipeline-pcs first) +test-pcs-integration: + just sync-pipeline-pcs + bash scripts/run_pcs_integration_tests.sh diff --git a/corpus/pcs/claims/claim-qc-release-v0.1/import_manifest.json b/corpus/pcs/claims/claim-qc-release-v0.1/import_manifest.json new file mode 100644 index 0000000..d8bffe2 --- /dev/null +++ b/corpus/pcs/claims/claim-qc-release-v0.1/import_manifest.json @@ -0,0 +1,6 @@ +{ + "claim_id": "claim-qc-release-v0.1", + "imported_from": "C:\\Users\\mateo\\scientific-memory\\tests\\pcs\\fixtures\\valid_signed_pcs_core_bundle.json", + "warnings": [], + "stale_artifacts": [] +} diff --git a/corpus/pcs/claims/claim-qc-release-v0.1/read_model.json b/corpus/pcs/claims/claim-qc-release-v0.1/read_model.json new file mode 100644 index 0000000..bf188d4 --- /dev/null +++ b/corpus/pcs/claims/claim-qc-release-v0.1/read_model.json @@ -0,0 +1,293 @@ +{ + "artifact_hashes": [ + { + "algorithm": "sha256", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "name": "signature_or_digest", + "source_artifact": "claim-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "name": "signature_or_digest", + "source_artifact": "as-labtrust-qc-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "name": "spec", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "name": "trace.json", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "name": "trace_hash", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "name": "policy_hash", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "name": "events_hash", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777", + "name": "signature_or_digest", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "name": "trace_hash", + "source_artifact": "cert-trace-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "name": "spec_hash", + "source_artifact": "cert-trace-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888", + "name": "signature_or_digest", + "source_artifact": "cert-trace-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "signature_or_digest", + "source_artifact": "scb-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "name": "claim-qc-release-v0.1", + "source_artifact": "evidence-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999", + "name": "signature_or_digest", + "source_artifact": "evidence-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "name": "claim_artifact", + "source_artifact": "claim_artifact" + }, + { + "algorithm": "sha256", + "digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777", + "name": "runtime_receipt", + "source_artifact": "runtime_receipt" + }, + { + "algorithm": "sha256", + "digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888", + "name": "trace_certificate", + "source_artifact": "trace_certificate" + }, + { + "algorithm": "sha256", + "digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999", + "name": "evidence_bundle", + "source_artifact": "evidence_bundle" + }, + { + "algorithm": "sha256", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "name": "signed_bundle", + "source_artifact": "signed_bundle" + } + ], + "assumption_set": { + "assumption_set_id": "as-labtrust-qc-v0.1", + "assumptions": [ + { + "id": "asm-domain-qc-sim", + "kind": "domain", + "status": "HumanReviewed", + "text": "Simulation models a hospital QC release workflow, not a live clinical laboratory." + } + ], + "created_at": "2026-05-16T12:00:00Z", + "human_review_status": "approved", + "id": "as-labtrust-qc-v0.1", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "schema_version": "v0", + "signature_or_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "status": "HumanReviewed" + }, + "bundle_signature_or_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "canonical_digests": { + "claim_artifact": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "evidence_bundle": "sha256:9999999999999999999999999999999999999999999999999999999999999999", + "runtime_receipt": "sha256:7777777777777777777777777777777777777777777777777777777777777777", + "signed_bundle": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "trace_certificate": "sha256:8888888888888888888888888888888888888888888888888888888888888888" + }, + "claim": { + "created_at": "2026-05-16T12:05:00Z", + "guarantee_types": { + "certificate_checked": true, + "empirically_measured": false, + "formally_checked": false, + "human_reviewed": false, + "runtime_observed": true, + "unchecked_advisory": false + }, + "id": "claim-qc-release-v0.1", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "signature_or_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "status": "CertificateChecked", + "text": "The qc-release simulation run satisfies the hospital lab QC release temporal policy under stated assumptions." + }, + "claim_id": "claim-qc-release-v0.1", + "evidence_bundle": { + "artifact_hashes": { + "claim-qc-release-v0.1": "sha256:2222222222222222222222222222222222222222222222222222222222222222" + }, + "assumption_set_refs": [ + "as-labtrust-qc-v0.1" + ], + "bundle_id": "evidence-qc-release-v0.1", + "certificate_refs": [ + "cert-trace-qc-release-v0.1" + ], + "claim_refs": [ + "claim-qc-release-v0.1" + ], + "created_at": "2026-05-16T12:12:00Z", + "id": "evidence-qc-release-v0.1", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "runtime_receipt_refs": [ + "receipt-qc-release-run-001" + ], + "schema_version": "v0", + "signature_or_digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym" + }, + "limitation_notice": "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory.", + "limitations": [ + "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory." + ], + "reproduce_commands": [ + "labtrust run-demo qc-release", + "pf verify science-claim signed_science_claim_bundle.json" + ], + "runtime_receipt": { + "ended_at": "2026-05-16T12:00:00Z", + "environment": { + "labtrust_version": "0.1.0", + "platform": "linux" + }, + "events_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "final_reason_code": "ok", + "id": "receipt-qc-release-run-001", + "input_hashes": { + "spec": "sha256:6666666666666666666666666666666666666666666666666666666666666666" + }, + "output_hashes": { + "trace.json": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + }, + "policy_hash": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "receipt_id": "receipt-qc-release-run-001", + "released": true, + "run_id": "runs/qc-release", + "run_outcome": "passed", + "schema_version": "v0", + "signature_or_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "started_at": "2026-05-16T11:58:00Z", + "status": "RuntimeObserved", + "trace_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + }, + "schema_version": "PcsClaimReadModel.v0", + "source_repositories": [ + { + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym" + }, + { + "source_commit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_repo": "https://github.com/fraware/CertifyEdge" + }, + { + "source_commit": "cccccccccccccccccccccccccccccccccccccccc", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric" + } + ], + "trace_certificate": { + "certificate_id": "cert-trace-qc-release-v0.1", + "checker": "certifyedge", + "checker_version": "0.1.0", + "counterexample_ref": null, + "created_at": "2026-05-16T12:10:00Z", + "id": "cert-trace-qc-release-v0.1", + "producer": "certifyedge", + "producer_version": "0.1.0", + "property_id": "qc_release.temporal.safety", + "schema_version": "v0", + "signature_or_digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888", + "source_commit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_repo": "https://github.com/fraware/CertifyEdge", + "spec_hash": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "status": "CertificateChecked", + "trace_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + }, + "verification_result": { + "checks": [ + { + "detail": "{}", + "guarantee_type": null, + "id": "schema-valid", + "name": "ScienceClaimBundle conforms to schema", + "outcome": "pass" + }, + { + "detail": "{}", + "guarantee_type": null, + "id": "trace-hash-alignment", + "name": "Receipt trace_hash matches certificate", + "outcome": "pass" + } + ], + "id": "verify-scb-qc-release-v0.1", + "overall_outcome": "pass", + "signature_or_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_commit": "cccccccccccccccccccccccccccccccccccccccc", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "status": "ProofChecked", + "verification_id": "verify-scb-qc-release-v0.1", + "verifier": "provability-fabric", + "verifier_version": "0.1.0" + }, + "verify_commands": [ + "pf verify science-claim signed_science_claim_bundle.json", + "just pcs-validate-bundle BUNDLE=signed_science_claim_bundle.json" + ] +} diff --git a/corpus/pcs/claims/claim-qc-release-v0.1/scientific_memory_import_report.json b/corpus/pcs/claims/claim-qc-release-v0.1/scientific_memory_import_report.json new file mode 100644 index 0000000..ea3c5f9 --- /dev/null +++ b/corpus/pcs/claims/claim-qc-release-v0.1/scientific_memory_import_report.json @@ -0,0 +1,9 @@ +{ + "claim_id": "claim-qc-release-v0.1", + "imported_at": "2026-05-17T07:54:16.708572+00:00", + "render_path": "/pcs/claims/claim-qc-release-v0.1", + "source_bundle_path": "C:\\Users\\mateo\\scientific-memory\\tests\\pcs\\fixtures\\valid_signed_pcs_core_bundle.json", + "stale_artifacts": [], + "verification_status": "passed", + "warnings": [] +} diff --git a/corpus/pcs/claims/claim-qc-release-v0.1/signed_bundle.json b/corpus/pcs/claims/claim-qc-release-v0.1/signed_bundle.json new file mode 100644 index 0000000..6830906 --- /dev/null +++ b/corpus/pcs/claims/claim-qc-release-v0.1/signed_bundle.json @@ -0,0 +1,183 @@ +{ + "reproduce_commands": [ + "labtrust run-demo qc-release", + "pf verify science-claim signed_science_claim_bundle.json" + ], + "schema_version": "SignedScienceClaimBundle.v0", + "science_claim_bundle": { + "assumption_set": { + "assumption_set_id": "as-labtrust-qc-v0.1", + "assumptions": [ + { + "assumption_id": "asm-domain-qc-sim", + "kind": "domain", + "source_span_refs": [ + "span-qc-release-spec-1" + ], + "status": "HumanReviewed", + "text": "Simulation models a hospital QC release workflow, not a live clinical laboratory." + } + ], + "created_at": "2026-05-16T12:00:00Z", + "human_review_status": "approved", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "schema_version": "v0", + "signature_or_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "status": "HumanReviewed" + }, + "bundle_id": "scb-qc-release-v0.1", + "certificates": [ + { + "certificate_id": "cert-trace-qc-release-v0.1", + "checker": "certifyedge", + "checker_version": "0.1.0", + "counterexample_ref": null, + "created_at": "2026-05-16T12:10:00Z", + "producer": "certifyedge", + "producer_version": "0.1.0", + "property_id": "qc_release.temporal.safety", + "schema_version": "v0", + "signature_or_digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888", + "source_commit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_repo": "https://github.com/fraware/CertifyEdge", + "spec_hash": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "status": "CertificateChecked", + "trace_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + } + ], + "claim_artifact": { + "artifact_id": "claim-qc-release-v0.1", + "artifact_type": "ClaimArtifact.v0", + "assumption_set_ref": "as-labtrust-qc-v0.1", + "certificate_refs": [ + "cert-trace-qc-release-v0.1" + ], + "claim_kind": "temporal_claim", + "claim_text": "The qc-release simulation run satisfies the hospital lab QC release temporal policy under stated assumptions.", + "created_at": "2026-05-16T12:05:00Z", + "formal_statement": "G (release_ready -> F[0,24h] verified)", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "runtime_receipt_refs": [ + "receipt-qc-release-run-001" + ], + "schema_version": "v0", + "signature_or_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_span_refs": [ + "span-qc-release-spec-1" + ], + "status": "CertificateChecked" + }, + "created_at": "2026-05-16T12:15:00Z", + "evidence_bundle": { + "artifact_hashes": { + "claim-qc-release-v0.1": "sha256:2222222222222222222222222222222222222222222222222222222222222222" + }, + "assumption_set_refs": [ + "as-labtrust-qc-v0.1" + ], + "bundle_id": "evidence-qc-release-v0.1", + "certificate_refs": [ + "cert-trace-qc-release-v0.1" + ], + "claim_refs": [ + "claim-qc-release-v0.1" + ], + "created_at": "2026-05-16T12:12:00Z", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "runtime_receipt_refs": [ + "receipt-qc-release-run-001" + ], + "schema_version": "v0", + "signature_or_digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym" + }, + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "runtime_receipts": [ + { + "ended_at": "2026-05-16T12:00:00Z", + "environment": { + "labtrust_version": "0.1.0", + "platform": "linux" + }, + "events_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "final_reason_code": "ok", + "input_hashes": { + "spec": "sha256:6666666666666666666666666666666666666666666666666666666666666666" + }, + "output_hashes": { + "trace.json": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + }, + "policy_hash": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "receipt_id": "receipt-qc-release-run-001", + "released": true, + "run_id": "runs/qc-release", + "run_outcome": "passed", + "schema_version": "v0", + "signature_or_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "started_at": "2026-05-16T11:58:00Z", + "status": "RuntimeObserved", + "trace_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + } + ], + "schema_version": "v0", + "signature_or_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "verification_policy": { + "policy_id": "labtrust-v0.1-qc-release", + "required_checks": [ + "schema-valid", + "trace-hash-alignment" + ] + } + }, + "signature_or_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "signed_at": "2026-05-16T12:25:00Z", + "signed_bundle_id": "signed-scb-qc-release-v0.1", + "signer": "Provability Fabric", + "source_commit": "cccccccccccccccccccccccccccccccccccccccc", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "verification_result": { + "bundle_id": "scb-qc-release-v0.1", + "checks": [ + { + "check_id": "schema-valid", + "description": "ScienceClaimBundle conforms to schema", + "details": {}, + "status": "passed" + }, + { + "check_id": "trace-hash-alignment", + "description": "Receipt trace_hash matches certificate", + "details": {}, + "status": "passed" + } + ], + "created_at": "2026-05-16T12:20:00Z", + "schema_version": "v0", + "signature_or_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_commit": "cccccccccccccccccccccccccccccccccccccccc", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "status": "ProofChecked", + "verification_id": "verify-scb-qc-release-v0.1", + "verifier": "provability-fabric", + "verifier_version": "0.1.0" + }, + "verify_commands": [ + "pf verify science-claim signed_science_claim_bundle.json", + "just pcs-validate-bundle BUNDLE=signed_science_claim_bundle.json" + ] +} diff --git a/corpus/pcs/claims/labtrust-qc-release-claim-001/read_model.json b/corpus/pcs/claims/labtrust-qc-release-claim-001/read_model.json index becd8c5..cd866a3 100644 --- a/corpus/pcs/claims/labtrust-qc-release-claim-001/read_model.json +++ b/corpus/pcs/claims/labtrust-qc-release-claim-001/read_model.json @@ -6,6 +6,18 @@ "name": "claim_digest", "source_artifact": "labtrust-qc-release-claim-001" }, + { + "algorithm": "sha256", + "digest": "sha256:claim-artifact-001", + "name": "signature_or_digest", + "source_artifact": "labtrust-qc-release-claim-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:assumption-set-001", + "name": "signature_or_digest", + "source_artifact": "labtrust-qc-release-assumptions" + }, { "algorithm": "sha256", "digest": "sha256:trace-json-qc-release", @@ -18,6 +30,12 @@ "name": "events_hash", "source_artifact": "runtime-receipt-qc-release" }, + { + "algorithm": "sha256", + "digest": "sha256:runtime-receipt-001", + "name": "signature_or_digest", + "source_artifact": "runtime-receipt-qc-release" + }, { "algorithm": "sha256", "digest": "sha256:temporal-policy-qc-release", @@ -29,6 +47,42 @@ "digest": "sha256:qc-release-stl", "name": "spec_hash", "source_artifact": "trace-certificate-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:trace-certificate-001", + "name": "signature_or_digest", + "source_artifact": "trace-certificate-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:verification-result-001", + "name": "signature_or_digest", + "source_artifact": "verification-result-labtrust-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:claim-artifact-001", + "name": "claim_artifact", + "source_artifact": "claim_artifact" + }, + { + "algorithm": "sha256", + "digest": "sha256:runtime-receipt-001", + "name": "runtime_receipt", + "source_artifact": "runtime_receipt" + }, + { + "algorithm": "sha256", + "digest": "sha256:trace-certificate-001", + "name": "trace_certificate", + "source_artifact": "trace_certificate" + }, + { + "algorithm": "sha256", + "digest": "sha256:signed-bundle-labtrust-demo-001", + "name": "signed_bundle", + "source_artifact": "signed_bundle" } ], "assumption_set": { @@ -57,6 +111,13 @@ "status": "RuntimeObserved" }, "bundle_signature_or_digest": "sha256:signed-bundle-labtrust-demo-001", + "canonical_digests": { + "claim_artifact": "sha256:claim-artifact-001", + "evidence_bundle": "", + "runtime_receipt": "sha256:runtime-receipt-001", + "signed_bundle": "sha256:signed-bundle-labtrust-demo-001", + "trace_certificate": "sha256:trace-certificate-001" + }, "claim": { "created_at": "2026-05-01T11:00:00Z", "guarantee_types": { @@ -75,6 +136,7 @@ "text": "The qc-release workflow run observed protocol-compliant specimen handling and release gating in the LabTrust-Gym hospital-lab simulation." }, "claim_id": "labtrust-qc-release-claim-001", + "evidence_bundle": {}, "limitation_notice": "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory.", "limitations": [ "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory." @@ -159,16 +221,15 @@ "outcome": "pass" } ], - "created_at": "2026-05-01T12:00:00Z", "id": "verification-result-labtrust-qc-release", "overall_outcome": "pass", - "producer": "provability-fabric", - "producer_version": "0.1.0", - "schema_version": "VerificationResult.v0", "signature_or_digest": "sha256:verification-result-001", "source_commit": "abc123def456", "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", - "status": "ProofChecked" + "status": "ProofChecked", + "verification_id": "verification-result-labtrust-qc-release", + "verifier": "provability-fabric", + "verifier_version": "0.1.0" }, "verify_commands": [ "pf verify science-claim signed_science_claim_bundle.json", diff --git a/corpus/pcs/claims/labtrust-qc-release-claim-001/scientific_memory_import_report.json b/corpus/pcs/claims/labtrust-qc-release-claim-001/scientific_memory_import_report.json new file mode 100644 index 0000000..a3171f7 --- /dev/null +++ b/corpus/pcs/claims/labtrust-qc-release-claim-001/scientific_memory_import_report.json @@ -0,0 +1,9 @@ +{ + "claim_id": "labtrust-qc-release-claim-001", + "imported_at": "2026-05-17T07:54:11.988638+00:00", + "render_path": "/pcs/claims/labtrust-qc-release-claim-001", + "source_bundle_path": "C:\\Users\\mateo\\scientific-memory\\tests\\pcs\\fixtures\\valid_signed_science_claim_bundle.json", + "stale_artifacts": [], + "verification_status": "passed", + "warnings": [] +} diff --git a/docs/pcs-labtrust-import.md b/docs/pcs-labtrust-import.md index de061c9..2f7f559 100644 --- a/docs/pcs-labtrust-import.md +++ b/docs/pcs-labtrust-import.md @@ -10,7 +10,13 @@ Scientific Memory imports **signed** `ScienceClaimBundle` artifacts produced by - Optional top-level `verification_result` (`VerificationResult.v0`) - Top-level `signature_or_digest` -Canonical artifact vocabulary is defined in [pcs-core](https://github.com/SentinelOps-CI/pcs-core). This repository validates against vendored JSON Schemas under `schemas/pcs/` and calls `pcs_core` (editable path dependency in `pipeline/pyproject.toml`) for canonical `ScienceClaimBundle.v0` and `VerificationResult.v0` shapes. +Canonical artifact vocabulary is defined in [pcs-core](https://github.com/SentinelOps-CI/pcs-core). Scientific Memory does **not** own PCS schemas; it mirrors pcs-core under `schemas/pcs/` and calls `pcs_core` when installed: + +| Canonical (pcs-core) | Legacy alias (deprecated) | +|----------------------|---------------------------| +| `SignedScienceClaimBundle.v0.schema.json` | `signed_science_claim_bundle.schema.json` | +| `ScienceClaimBundle.v0.schema.json` | `science_claim_bundle.schema.json` | +| `VerificationResult.v0.schema.json` | `verification_result.schema.json` | Two bundle shapes are supported: @@ -22,32 +28,66 @@ Two bundle shapes are supported: ## Commands ```bash -just pcs-validate-bundle BUNDLE=path/to/signed_science_claim_bundle.json -just pcs-import-bundle BUNDLE=path/to/signed_science_claim_bundle.json -just pcs-render-claim CLAIM_ID= +# just uses positional arguments (not make-style BUNDLE=path) +just pcs-validate-bundle path/to/signed_science_claim_bundle.json +just pcs-import-bundle path/to/signed_science_claim_bundle.json +just pcs-render-claim + +# shortcuts +just pcs-import-labtrust-demo +just pcs-refresh-demo ``` +**Windows:** run `just` from Git Bash (configured in the justfile). For live pcs-core tests: `just test-pcs-integration`. Install pcs-core when cloned as a sibling repo: `just sync-pipeline-pcs`. + `pcs-import-bundle` writes: - `corpus/pcs/claims//signed_bundle.json` — preserved signed input - `corpus/pcs/claims//read_model.json` — portal read model -- `corpus/pcs/claims//import_manifest.json` — warnings and provenance +- `corpus/pcs/claims//import_manifest.json` — warnings and provenance (legacy) +- `corpus/pcs/claims//scientific_memory_import_report.json` — import report (`claim_id`, `imported_at`, `source_bundle_path`, `verification_status`, `warnings`, `stale_artifacts`, `render_path`) `pcs-render-claim` refreshes `portal/.generated/pcs-export.json` for static portal build. +```bash +just sync-pcs-schemas # copy canonical schemas from repo-root/pcs-core +just pcs-refresh-demo # re-import demo fixtures into corpus/pcs/claims/ +``` + +Schema layout: + +- `schemas/pcs/*.schema.json` — mirrors synced from pcs-core (canonical) +- `schemas/pcs/legacy/*.schema.json` — LabTrust portal legacy envelopes only +- `schemas/pcs/SCHEMA_MIRROR.json` — provenance manifest from last sync + +Top-level `reproduce_commands` / `verify_commands` are Scientific Memory extensions; they are stripped before pcs-core validation and preserved in the stored signed bundle and read model. + ## Import behavior | Behavior | Detail | |----------|--------| | Validation | JSON Schema + optional `pcs_core` hook | -| Reject invalid | Default (`strict=true`) | +| Reject invalid (`strict=true`, default) | Missing `science_claim_bundle`, claim, assumption set, runtime receipt, certificate (signed bundles), failed verification, empty assumptions, missing `source_commit` / `signature_or_digest` on major artifacts | +| `strict=false` | May import without `VerificationResult` (warning only); certificate status warnings | | Preserve IDs | Claim, assumption set, receipt, certificate IDs unchanged | | Preserve provenance | `source_repo`, `source_commit`, `signature_or_digest` on each artifact | -| Preserve checks | VerificationResult `checks` list stored verbatim | -| Warn: no VerificationResult | Import continues; portal shows advisory | -| Warn: certificate not checked | When `trace_certificate.status` ≠ `CertificateChecked` | +| Preserve checks | Provability Fabric `VerificationResult.v0` `checks` stored verbatim | | Reject: empty assumptions | `assumption_set.assumptions` must be non-empty | +### Provability Fabric handoff shape + +```json +{ + "schema_version": "v0", + "signed_bundle_id": "...", + "science_claim_bundle": {}, + "verification_result": {}, + "signer": "Provability Fabric", + "signed_at": "...", + "signature_or_digest": "sha256:..." +} +``` + ## End-to-end LabTrust flow (reference) ```bash diff --git a/docs/pcs-rendering-contract.md b/docs/pcs-rendering-contract.md index 57b86ec..d16f4ec 100644 --- a/docs/pcs-rendering-contract.md +++ b/docs/pcs-rendering-contract.md @@ -8,8 +8,8 @@ Every LabTrust PCS claim page at `/pcs/claims/` must render the follow | 2 | Assumptions | `AssumptionSetView` | `read_model.assumption_set` | | 3 | Runtime Evidence | `RuntimeReceiptView` | `read_model.runtime_receipt` | | 4 | Temporal Certificate | `TraceCertificateView` | `read_model.trace_certificate` | -| 5 | Verification Result | `VerificationResultView` | `read_model.verification_result` | -| 6 | Artifact Hashes | `ArtifactHashTable` | `read_model.artifact_hashes` | +| 5 | Verification Result | `VerificationResultView` | `read_model.verification_result` (Provability Fabric `VerificationResult.v0`) | +| 6 | Artifact Hashes | `ArtifactHashTable` | `read_model.canonical_digests` + `artifact_hashes` | | 7 | Source Repositories | `SourceRepositories` | `read_model.source_repositories` | | 8 | Reproduce / Verify | `ReplayCommand` | `reproduce_commands`, `verify_commands` | | 9 | Limitations | `LimitationNotice` | `limitation_notice` (+ optional `limitations`) | @@ -35,6 +35,29 @@ Every page must display the following notice verbatim (or substantively identica The canonical string is defined in `sm_pipeline.pcs_import.artifact_normalizer.LIMITATION_NOTICE`. +## Verification Result fields + +`VerificationResultView` must show: + +- `verification_id` +- `status` +- `verifier` +- `verifier_version` +- `checks` (each with outcome) +- `source_repo` +- `source_commit` +- `signature_or_digest` + +## Artifact hash table + +`ArtifactHashTable` must show canonical digests for: + +- Claim artifact +- Runtime receipt +- Trace certificate +- Evidence bundle (when present) +- Signed bundle + ## Status visibility Status enums must be visible for: diff --git a/pipeline/pyproject.toml b/pipeline/pyproject.toml index 9724d28..6eb0228 100644 --- a/pipeline/pyproject.toml +++ b/pipeline/pyproject.toml @@ -11,14 +11,16 @@ dependencies = [ "networkx>=3.3", "httpx>=0.27", "python-dotenv>=1.0", - "pcs-core>=0.1.0", ] -[tool.uv.sources] -pcs-core = { path = "../../pcs-core/python", editable = true } - [project.optional-dependencies] mcp = ["mcp>=1.0"] +# Install when pcs-core is available: uv sync --project pipeline --extra pcs +pcs = ["pcs-core>=0.1.0"] + +[tool.uv.sources] +# Sibling checkout: /pcs-core. CI: clone into scientific-memory/pcs-core. +pcs-core = { path = "../../pcs-core/python", editable = true } [project.scripts] sm-pipeline = "sm_pipeline.cli:app" diff --git a/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py b/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py index 7c50625..4177bc2 100644 --- a/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py +++ b/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py @@ -213,8 +213,15 @@ def _normalize_verification_result(vr: dict[str, Any] | None) -> dict[str, Any] if status in ("ProofChecked", "CertificateChecked", "RuntimeChecked"): overall = "pass" return { - "id": _artifact_id(vr) or vr.get("verification_id"), + "verification_id": str( + vr.get("verification_id") or _artifact_id(vr) or vr.get("id") or "" + ), + "id": str(vr.get("verification_id") or _artifact_id(vr) or vr.get("id") or ""), "status": str(vr.get("status") or ""), + "verifier": str(vr.get("verifier") or vr.get("producer") or ""), + "verifier_version": str( + vr.get("verifier_version") or vr.get("producer_version") or "" + ), "overall_outcome": overall, "signature_or_digest": str(vr.get("signature_or_digest") or ""), "source_repo": str(vr.get("source_repo") or ""), @@ -310,6 +317,38 @@ def normalize_signed_bundle(bundle: dict[str, Any]) -> dict[str, Any]: claim_raw, runtime_receipt, trace_certificate, verification_result ) + evidence_raw = scb.get("evidence_bundle") + evidence_bundle = ( + _normalize_named_artifact(evidence_raw) if isinstance(evidence_raw, dict) else {} + ) + evidence_digest = "" + if isinstance(evidence_raw, dict): + evidence_digest = str(evidence_raw.get("signature_or_digest") or "") + if not evidence_digest: + ah = evidence_raw.get("artifact_hashes") + if isinstance(ah, dict) and ah: + evidence_digest = str(next(iter(ah.values()))) + + canonical_digests = { + "claim_artifact": str(claim_raw.get("signature_or_digest") or ""), + "runtime_receipt": str(runtime_receipt.get("signature_or_digest") or ""), + "trace_certificate": str(trace_certificate.get("signature_or_digest") or ""), + "evidence_bundle": evidence_digest, + "signed_bundle": str( + bundle.get("signature_or_digest") or bundle.get("bundle_digest") or "" + ), + } + for role, digest in canonical_digests.items(): + if digest: + artifact_hashes.append( + { + "name": role, + "digest": digest, + "algorithm": "sha256", + "source_artifact": role, + } + ) + return { "schema_version": "PcsClaimReadModel.v0", "claim_id": claim_id, @@ -324,8 +363,10 @@ def normalize_signed_bundle(bundle: dict[str, Any]) -> dict[str, Any]: "assumption_set": assumption_set, "runtime_receipt": runtime_receipt, "trace_certificate": trace_certificate, + "evidence_bundle": evidence_bundle, "verification_result": verification_result, "artifact_hashes": artifact_hashes, + "canonical_digests": canonical_digests, "source_repositories": sources, "reproduce_commands": reproduce, "verify_commands": verify, diff --git a/pipeline/src/sm_pipeline/pcs_import/bundle_utils.py b/pipeline/src/sm_pipeline/pcs_import/bundle_utils.py new file mode 100644 index 0000000..29320a0 --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_import/bundle_utils.py @@ -0,0 +1,15 @@ +"""Helpers for signed bundle import (SM extensions vs pcs-core envelope).""" + +from __future__ import annotations + +from typing import Any + +SM_EXTENSION_KEYS = frozenset({"reproduce_commands", "verify_commands"}) + + +def bundle_for_validation(bundle: dict[str, Any]) -> dict[str, Any]: + """Return a copy suitable for pcs-core / strict schema validation.""" + out = dict(bundle) + for key in SM_EXTENSION_KEYS: + out.pop(key, None) + return out diff --git a/pipeline/src/sm_pipeline/pcs_import/science_claim_bundle_importer.py b/pipeline/src/sm_pipeline/pcs_import/science_claim_bundle_importer.py index d479c26..6a7c842 100644 --- a/pipeline/src/sm_pipeline/pcs_import/science_claim_bundle_importer.py +++ b/pipeline/src/sm_pipeline/pcs_import/science_claim_bundle_importer.py @@ -4,12 +4,18 @@ import json from dataclasses import dataclass, field +from datetime import datetime, timezone from pathlib import Path from typing import Any from sm_pipeline.pcs_import.artifact_normalizer import normalize_signed_bundle +from sm_pipeline.pcs_import.bundle_utils import bundle_for_validation from sm_pipeline.pcs_validate.stale_checker import find_stale_artifacts -from sm_pipeline.pcs_validate.validator import BundleValidationError, validate_signed_bundle +from sm_pipeline.pcs_validate.validator import ( + BundleValidationError, + validate_signed_bundle, + verification_status_label, +) @dataclass @@ -18,6 +24,8 @@ class ImportResult: import_dir: Path warnings: list[str] = field(default_factory=list) stale_artifacts: list[str] = field(default_factory=list) + verification_status: str = "absent" + render_path: str = "" def _repo_root(repo_root: Path | None) -> Path: @@ -48,13 +56,18 @@ def import_signed_bundle( if not isinstance(raw, dict): raise BundleValidationError("Bundle must be a JSON object") - warnings = validate_signed_bundle(raw, repo_root=root, strict=strict) + warnings = validate_signed_bundle( + bundle_for_validation(raw), repo_root=root, strict=strict + ) stale = find_stale_artifacts(raw) if stale: warnings.extend(f"Stale or deprecated artifact: {p}" for p in stale) read_model = normalize_signed_bundle(raw) claim_id = read_model["claim_id"] + render_path = f"/pcs/claims/{claim_id}" + verification_status = verification_status_label(raw) + import_dir = _import_root(root) / claim_id if write: @@ -77,12 +90,27 @@ def import_signed_bundle( json.dumps(manifest, indent=2) + "\n", encoding="utf-8", ) + import_report = { + "claim_id": claim_id, + "imported_at": datetime.now(timezone.utc).isoformat(), + "source_bundle_path": str(bundle_path.resolve()), + "verification_status": verification_status, + "warnings": warnings, + "stale_artifacts": stale, + "render_path": render_path, + } + (import_dir / "scientific_memory_import_report.json").write_text( + json.dumps(import_report, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) return ImportResult( claim_id=claim_id, import_dir=import_dir, warnings=warnings, stale_artifacts=stale, + verification_status=verification_status, + render_path=render_path, ) diff --git a/pipeline/src/sm_pipeline/pcs_validate/__init__.py b/pipeline/src/sm_pipeline/pcs_validate/__init__.py index 8035190..42f18da 100644 --- a/pipeline/src/sm_pipeline/pcs_validate/__init__.py +++ b/pipeline/src/sm_pipeline/pcs_validate/__init__.py @@ -1,13 +1,31 @@ -"""PCS bundle validation against pcs-core (when installed) and vendored schemas.""" +"""PCS bundle validation against pcs-core (when installed) and schema mirrors.""" +from sm_pipeline.pcs_validate.bundle_semantics import ( + collect_semantic_errors, + collect_semantic_warnings, + verification_result_passed, +) +from sm_pipeline.pcs_validate.schema_registry import ( + SCIENCE_CLAIM_BUNDLE_SCHEMA, + SIGNED_BUNDLE_SCHEMA, + VERIFICATION_RESULT_SCHEMA, + resolve_schema_path, +) from sm_pipeline.pcs_validate.validator import ( BundleValidationError, - collect_import_warnings, validate_signed_bundle, + verification_status_label, ) __all__ = [ "BundleValidationError", - "collect_import_warnings", + "SCIENCE_CLAIM_BUNDLE_SCHEMA", + "SIGNED_BUNDLE_SCHEMA", + "VERIFICATION_RESULT_SCHEMA", + "collect_semantic_errors", + "collect_semantic_warnings", + "resolve_schema_path", "validate_signed_bundle", + "verification_result_passed", + "verification_status_label", ] diff --git a/pipeline/src/sm_pipeline/pcs_validate/bundle_detection.py b/pipeline/src/sm_pipeline/pcs_validate/bundle_detection.py new file mode 100644 index 0000000..03b2bfe --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_validate/bundle_detection.py @@ -0,0 +1,37 @@ +"""Detect LabTrust legacy vs pcs-core signed bundle shapes.""" + +from __future__ import annotations + +from typing import Any + + +def get_science_claim_bundle(bundle: dict[str, Any]) -> dict[str, Any] | None: + scb = bundle.get("science_claim_bundle") + return scb if isinstance(scb, dict) else None + + +def is_legacy_signed_bundle(bundle: dict[str, Any]) -> bool: + """ + Portal/LabTrust envelope: nested ScienceClaimBundle.v0 with claim, runtime_receipt, + trace_certificate (singular). + """ + scb = get_science_claim_bundle(bundle) + if scb is None: + return False + if scb.get("schema_version") == "ScienceClaimBundle.v0": + return True + if "claim" in scb and "claim_artifact" not in scb: + return True + return False + + +def is_pcs_core_signed_bundle(bundle: dict[str, Any]) -> bool: + """Provability Fabric / pcs-core SignedScienceClaimBundle.v0 envelope.""" + if bundle.get("signed_bundle_id") and get_science_claim_bundle(bundle): + return True + if bundle.get("signer") and bundle.get("signed_at") and get_science_claim_bundle(bundle): + return True + scb = get_science_claim_bundle(bundle) + if isinstance(scb, dict) and scb.get("schema_version") == "v0" and "claim_artifact" in scb: + return True + return False diff --git a/pipeline/src/sm_pipeline/pcs_validate/bundle_semantics.py b/pipeline/src/sm_pipeline/pcs_validate/bundle_semantics.py new file mode 100644 index 0000000..ad5e2ac --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_validate/bundle_semantics.py @@ -0,0 +1,204 @@ +"""Semantic PCS bundle validation (strict import contract; pcs-core is canonical).""" + +from __future__ import annotations + +from typing import Any + +from sm_pipeline.pcs_validate.bundle_detection import is_legacy_signed_bundle + +_CHECK_PASS = frozenset({"pass", "passed", "ok", "success"}) +_CHECK_FAIL = frozenset({"fail", "failed", "error", "rejected"}) +_VR_SUCCESS_STATUS = frozenset( + { + "passed", + "pass", + "proofchecked", + "runtimechecked", + "certificatechecked", + } +) +_VR_FAIL_STATUS = frozenset({"failed", "fail", "rejected", "error"}) + + +def _get_scb(bundle: dict[str, Any]) -> dict[str, Any] | None: + scb = bundle.get("science_claim_bundle") + return scb if isinstance(scb, dict) else None + + +def _get_verification_result(bundle: dict[str, Any], scb: dict[str, Any] | None) -> dict[str, Any] | None: + vr = bundle.get("verification_result") + if isinstance(vr, dict): + return vr + if scb is not None: + nested = scb.get("verification_result") + if isinstance(nested, dict): + return nested + return None + + +def _get_claim(scb: dict[str, Any]) -> dict[str, Any] | None: + claim = scb.get("claim_artifact") or scb.get("claim") + return claim if isinstance(claim, dict) else None + + +def _get_assumption_set(scb: dict[str, Any]) -> dict[str, Any] | None: + assumption_set = scb.get("assumption_set") + return assumption_set if isinstance(assumption_set, dict) else None + + +def _get_runtime_receipt(scb: dict[str, Any]) -> dict[str, Any] | None: + receipt = scb.get("runtime_receipt") + if isinstance(receipt, dict): + return receipt + receipts = scb.get("runtime_receipts") + if isinstance(receipts, list) and receipts and isinstance(receipts[0], dict): + return receipts[0] + return None + + +def _get_trace_certificate(scb: dict[str, Any]) -> dict[str, Any] | None: + cert = scb.get("trace_certificate") + if isinstance(cert, dict): + return cert + certificates = scb.get("certificates") + if isinstance(certificates, list) and certificates and isinstance(certificates[0], dict): + return certificates[0] + return None + + +def _is_signed_bundle(bundle: dict[str, Any]) -> bool: + return bool( + str(bundle.get("signature_or_digest") or bundle.get("bundle_digest") or "").strip() + ) + + +def _check_passed(check: dict[str, Any]) -> bool: + raw = check.get("outcome") or check.get("status") or "" + return str(raw).lower() in _CHECK_PASS + + +def _check_failed(check: dict[str, Any]) -> bool: + raw = check.get("outcome") or check.get("status") or "" + return str(raw).lower() in _CHECK_FAIL + + +def verification_result_passed(vr: dict[str, Any]) -> bool: + status = str(vr.get("status") or "").lower() + if status in _VR_FAIL_STATUS: + return False + overall = str(vr.get("overall_outcome") or "").lower() + if overall in _CHECK_FAIL: + return False + checks = vr.get("checks") + if isinstance(checks, list) and checks: + if any(_check_failed(c) for c in checks if isinstance(c, dict)): + return False + if all(_check_passed(c) for c in checks if isinstance(c, dict)): + return True + if status in _VR_SUCCESS_STATUS or overall in _CHECK_PASS: + return True + if status == "passed": + return True + return False + + +def _require_signature(artifact: dict[str, Any], path: str, errors: list[str]) -> None: + if not str(artifact.get("signature_or_digest") or "").strip(): + errors.append(f"{path}: signature_or_digest is required") + + +def _require_source_commit(artifact: dict[str, Any], path: str, errors: list[str]) -> None: + if not str(artifact.get("source_commit") or "").strip(): + errors.append(f"{path}: source_commit is required") + + +def collect_semantic_errors(bundle: dict[str, Any], *, strict: bool) -> list[str]: + """Return validation errors; empty when bundle satisfies strict import semantics.""" + errors: list[str] = [] + + if not _get_scb(bundle): + errors.append("science_claim_bundle is required") + return errors + + scb = _get_scb(bundle) + assert scb is not None + + claim = _get_claim(scb) + if claim is None: + errors.append("science_claim_bundle.claim_artifact (or claim) is required") + + assumption_set = _get_assumption_set(scb) + if assumption_set is None: + errors.append("science_claim_bundle.assumption_set is required") + else: + assumptions = assumption_set.get("assumptions") + if not isinstance(assumptions, list) or len(assumptions) == 0: + errors.append("science_claim_bundle.assumption_set.assumptions must be non-empty") + + runtime_receipt = _get_runtime_receipt(scb) + if runtime_receipt is None: + errors.append("science_claim_bundle.runtime_receipt (or runtime_receipts) is required") + + trace_cert = _get_trace_certificate(scb) + if _is_signed_bundle(bundle): + if trace_cert is None: + errors.append( + "signed bundle requires trace_certificate or certificates[0]" + ) + elif str(trace_cert.get("status") or "") != "CertificateChecked": + if strict: + errors.append( + f"trace_certificate.status must be CertificateChecked " + f"(got {trace_cert.get('status')!r})" + ) + + vr = _get_verification_result(bundle, scb) + if vr is None: + if strict: + errors.append("verification_result is required in strict mode") + elif not verification_result_passed(vr): + errors.append("verification_result did not pass (status or checks)") + + if _is_signed_bundle(bundle): + _require_signature(bundle, "signed_bundle", errors) + + for label, artifact in ( + ("science_claim_bundle.claim_artifact", claim), + ("science_claim_bundle.assumption_set", assumption_set), + ("science_claim_bundle.runtime_receipt", runtime_receipt), + ("science_claim_bundle.trace_certificate", trace_cert), + ("verification_result", vr), + ): + if isinstance(artifact, dict): + _require_source_commit(artifact, label, errors) + _require_signature(artifact, label, errors) + + if isinstance(scb, dict): + _require_source_commit(scb, "science_claim_bundle", errors) + if is_legacy_signed_bundle({"science_claim_bundle": scb}): + _require_signature(scb, "science_claim_bundle", errors) + + return errors + + +def collect_semantic_warnings(bundle: dict[str, Any]) -> list[str]: + """Non-fatal warnings when strict=False allows import without verification_result.""" + warnings: list[str] = [] + scb = _get_scb(bundle) + if scb is None: + return warnings + + vr = _get_verification_result(bundle, scb) + if vr is None: + warnings.append( + "VerificationResult is absent; import proceeds with advisory only." + ) + + trace_cert = _get_trace_certificate(scb) + if isinstance(trace_cert, dict): + status = str(trace_cert.get("status") or "") + if status != "CertificateChecked": + warnings.append( + f"trace_certificate.status is {status!r}, expected CertificateChecked." + ) + return warnings diff --git a/pipeline/src/sm_pipeline/pcs_validate/pcs_core_hook.py b/pipeline/src/sm_pipeline/pcs_validate/pcs_core_hook.py index 59c719c..649ff20 100644 --- a/pipeline/src/sm_pipeline/pcs_validate/pcs_core_hook.py +++ b/pipeline/src/sm_pipeline/pcs_validate/pcs_core_hook.py @@ -1,43 +1,32 @@ -"""Optional validation via pcs-core (canonical PCS protocol).""" +"""Validation via pcs-core (canonical PCS authority).""" from __future__ import annotations from typing import Any -def is_pcs_core_science_claim_bundle(scb: dict[str, Any]) -> bool: - if scb.get("schema_version") == "v0": - return True - return "claim_artifact" in scb - - -def is_pcs_core_verification_result(vr: dict[str, Any]) -> bool: - return "verification_id" in vr and "verifier" in vr +def pcs_core_available() -> bool: + try: + import pcs_core.validate # noqa: F401 + except ImportError: + return False + return True def validate_with_pcs_core(bundle: dict[str, Any]) -> list[str]: - """Run pcs-core schema + semantic validation when the package is installed.""" - try: - from pcs_core.validate import ValidationError as PcsCoreValidationError - from pcs_core.validate import validate_artifact - except ImportError: + """Run pcs-core schema + semantic validation.""" + if not pcs_core_available(): return [] - errors: list[str] = [] - scb = bundle.get("science_claim_bundle") - if isinstance(scb, dict) and is_pcs_core_science_claim_bundle(scb): - try: - validate_artifact(scb, "ScienceClaimBundle.v0") - except PcsCoreValidationError as exc: - errors.extend(exc.errors or [str(exc)]) - - vr = bundle.get("verification_result") - if vr is None and isinstance(scb, dict): - vr = scb.get("verification_result") - if isinstance(vr, dict) and is_pcs_core_verification_result(vr): - try: - validate_artifact(vr, "VerificationResult.v0") - except PcsCoreValidationError as exc: - errors.extend(exc.errors or [str(exc)]) - - return errors + from pcs_core.validate import ValidationError as PcsCoreValidationError + from pcs_core.validate import detect_artifact_type, validate_artifact + + from sm_pipeline.pcs_import.bundle_utils import bundle_for_validation + + payload = bundle_for_validation(bundle) + try: + artifact_type = detect_artifact_type(payload) or "SignedScienceClaimBundle.v0" + validate_artifact(payload, artifact_type) + except PcsCoreValidationError as exc: + return list(exc.errors or [str(exc)]) + return [] diff --git a/pipeline/src/sm_pipeline/pcs_validate/schema_registry.py b/pipeline/src/sm_pipeline/pcs_validate/schema_registry.py new file mode 100644 index 0000000..239207d --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_validate/schema_registry.py @@ -0,0 +1,34 @@ +"""Resolve pcs-core canonical schema file names (Scientific Memory consumes, does not own).""" + +from __future__ import annotations + +from pathlib import Path + +# Canonical pcs-core v0.1 schema filenames (see SentinelOps-CI/pcs-core). +SIGNED_BUNDLE_SCHEMA = "SignedScienceClaimBundle.v0.schema.json" +SCIENCE_CLAIM_BUNDLE_SCHEMA = "ScienceClaimBundle.v0.schema.json" +VERIFICATION_RESULT_SCHEMA = "VerificationResult.v0.schema.json" + +LEGACY_SIGNED_BUNDLE_SCHEMA = "legacy/LabTrust.SignedScienceClaimBundle.v0.schema.json" +LEGACY_SCIENCE_CLAIM_BUNDLE_SCHEMA = "legacy/LabTrust.ScienceClaimBundle.v0.schema.json" +LEGACY_VERIFICATION_RESULT_SCHEMA = "legacy/LabTrust.VerificationResult.v0.schema.json" + +# Legacy aliases kept for compatibility; new code and docs must use canonical names. +SCHEMA_ALIASES: dict[str, str] = { + "signed_science_claim_bundle.schema.json": SIGNED_BUNDLE_SCHEMA, + "science_claim_bundle.schema.json": SCIENCE_CLAIM_BUNDLE_SCHEMA, + "verification_result.schema.json": VERIFICATION_RESULT_SCHEMA, +} + + +def resolve_schema_path(schemas_dir: Path, name: str) -> Path: + """Return path to schema file, resolving legacy alias to canonical filename.""" + canonical = SCHEMA_ALIASES.get(name, name) + path = schemas_dir / canonical + if path.is_file(): + return path + # Fallback: requested name as-is (e.g. artifact_base). + fallback = schemas_dir / name + if fallback.is_file(): + return fallback + raise FileNotFoundError(f"PCS schema not found: {name} (resolved {canonical})") diff --git a/pipeline/src/sm_pipeline/pcs_validate/validator.py b/pipeline/src/sm_pipeline/pcs_validate/validator.py index a617a08..f39d8cc 100644 --- a/pipeline/src/sm_pipeline/pcs_validate/validator.py +++ b/pipeline/src/sm_pipeline/pcs_validate/validator.py @@ -1,4 +1,4 @@ -"""Validate signed PCS bundles (pcs-core hook + vendored JSON Schema).""" +"""Validate signed PCS bundles: pcs-core (canonical) + legacy mirrors.""" from __future__ import annotations @@ -10,13 +10,24 @@ from referencing import Registry, Resource from referencing.jsonschema import DRAFT202012 -from sm_pipeline.pcs_validate.pcs_core_hook import ( - is_pcs_core_science_claim_bundle, - is_pcs_core_verification_result, - validate_with_pcs_core, +from sm_pipeline.pcs_validate.bundle_detection import ( + get_science_claim_bundle, + is_legacy_signed_bundle, + is_pcs_core_signed_bundle, +) +from sm_pipeline.pcs_validate.bundle_semantics import ( + collect_semantic_errors, + collect_semantic_warnings, + verification_result_passed, +) +from sm_pipeline.pcs_validate.pcs_core_hook import pcs_core_available, validate_with_pcs_core +from sm_pipeline.pcs_validate.schema_registry import ( + LEGACY_SCIENCE_CLAIM_BUNDLE_SCHEMA, + LEGACY_SIGNED_BUNDLE_SCHEMA, + LEGACY_VERIFICATION_RESULT_SCHEMA, + SCHEMA_ALIASES, + resolve_schema_path, ) - -PCS_SCHEMA_BASE_URI = "https://scientific-memory.org/schemas/pcs/" class BundleValidationError(ValueError): @@ -38,7 +49,12 @@ def _load_json(path: Path) -> object: def _build_pcs_registry(repo_root: Path) -> Registry: registry = Registry() - for path in sorted(_pcs_schemas_dir(repo_root).glob("*.json")): + schemas_dir = _pcs_schemas_dir(repo_root) + skip = set(SCHEMA_ALIASES.keys()) + paths = list(schemas_dir.glob("*.json")) + list(schemas_dir.glob("legacy/*.json")) + for path in sorted(paths): + if path.name in skip: + continue schema = _load_json(path) schema_id = schema.get("$id") if not isinstance(schema_id, str): @@ -51,12 +67,47 @@ def _build_pcs_registry(repo_root: Path) -> Registry: def validator_for(schema_name: str, repo_root: Path) -> Draft202012Validator: - schema_path = _pcs_schemas_dir(repo_root) / schema_name + schema_path = resolve_schema_path(_pcs_schemas_dir(repo_root), schema_name) schema = _load_json(schema_path) registry = _build_pcs_registry(repo_root) return Draft202012Validator(schema, registry=registry) +def _get_verification_result(bundle: dict[str, Any], scb: dict[str, Any] | None) -> dict[str, Any] | None: + vr = bundle.get("verification_result") + if isinstance(vr, dict): + return vr + if scb is not None: + nested = scb.get("verification_result") + if isinstance(nested, dict): + return nested + return None + + +def _validate_legacy_bundle(bundle: dict[str, Any], repo_root: Path) -> list[str]: + """LabTrust portal legacy envelopes use schemas/pcs/legacy/* mirrors.""" + errors: list[str] = [] + signed_validator = validator_for(LEGACY_SIGNED_BUNDLE_SCHEMA, repo_root) + for err in sorted(signed_validator.iter_errors(bundle), key=lambda e: e.path): + errors.append(err.message) + + scb = get_science_claim_bundle(bundle) + if scb is None: + errors.append("science_claim_bundle is required") + else: + scb_validator = validator_for(LEGACY_SCIENCE_CLAIM_BUNDLE_SCHEMA, repo_root) + for err in sorted(scb_validator.iter_errors(scb), key=lambda e: e.path): + errors.append(f"science_claim_bundle: {err.message}") + + vr = _get_verification_result(bundle, scb) + if isinstance(vr, dict): + vr_validator = validator_for(LEGACY_VERIFICATION_RESULT_SCHEMA, repo_root) + for err in sorted(vr_validator.iter_errors(vr), key=lambda e: e.path): + errors.append(f"verification_result: {err.message}") + + return errors + + def validate_signed_bundle( bundle: dict[str, Any], *, @@ -66,81 +117,49 @@ def validate_signed_bundle( """ Validate a signed science claim bundle. - Returns import warnings (non-fatal). Raises BundleValidationError when strict - and validation fails. + pcs-core bundles are validated by pcs-core when installed. Legacy LabTrust portal + bundles use vendored schema mirrors plus bundle_semantics. """ root = (repo_root or _repo_root_from_here()).resolve() errors: list[str] = [] - if strict: - errors.extend(validate_with_pcs_core(bundle)) - - validator = validator_for("signed_science_claim_bundle.schema.json", root) - for err in sorted(validator.iter_errors(bundle), key=lambda e: e.path): - errors.append(err.message) - - scb = bundle.get("science_claim_bundle") - if isinstance(scb, dict) and not is_pcs_core_science_claim_bundle(scb): - scb_validator = validator_for("science_claim_bundle.schema.json", root) - for err in sorted(scb_validator.iter_errors(scb), key=lambda e: e.path): - errors.append(f"science_claim_bundle: {err.message}") - - assumption_set = scb.get("assumption_set") - assumptions = ( - assumption_set.get("assumptions") - if isinstance(assumption_set, dict) - else None - ) - if not assumptions: - errors.append("science_claim_bundle.assumption_set.assumptions is required") - - if isinstance(scb, dict) and is_pcs_core_science_claim_bundle(scb): - assumption_set = scb.get("assumption_set") - assumptions = ( - assumption_set.get("assumptions") - if isinstance(assumption_set, dict) - else None - ) - if not assumptions: - errors.append("science_claim_bundle.assumption_set.assumptions is required") - - vr = bundle.get("verification_result") - if vr is None and isinstance(scb, dict): - vr = scb.get("verification_result") - if isinstance(vr, dict) and not is_pcs_core_verification_result(vr): - vr_validator = validator_for("verification_result.schema.json", root) - for err in sorted(vr_validator.iter_errors(vr), key=lambda e: e.path): - errors.append(f"verification_result: {err.message}") + if is_legacy_signed_bundle(bundle): + errors.extend(_validate_legacy_bundle(bundle, root)) + errors.extend(collect_semantic_errors(bundle, strict=strict)) + elif is_pcs_core_signed_bundle(bundle): + if pcs_core_available(): + errors.extend(validate_with_pcs_core(bundle)) + else: + errors.append( + "pcs-core SignedScienceClaimBundle requires the pcs-core package " + "(uv sync with pcs-core at repo-root/pcs-core)" + ) + else: + errors.extend(_validate_legacy_bundle(bundle, root)) + errors.extend(collect_semantic_errors(bundle, strict=strict)) if errors and strict: raise BundleValidationError("; ".join(errors)) - return collect_import_warnings(bundle) if not errors else [] + if strict: + return collect_semantic_warnings(bundle) + warnings = collect_semantic_warnings(bundle) + scb = get_science_claim_bundle(bundle) + vr = _get_verification_result(bundle, scb) + if vr is None: + warnings.append( + "VerificationResult is absent; import proceeds with advisory only." + ) + return warnings -def collect_import_warnings(bundle: dict[str, Any]) -> list[str]: - """Non-fatal warnings required by the PCS import contract.""" - warnings: list[str] = [] - scb = bundle.get("science_claim_bundle") - if not isinstance(scb, dict): - return warnings - vr = bundle.get("verification_result") - if vr is None: - vr = scb.get("verification_result") +def verification_status_label(bundle: dict[str, Any]) -> str: + """Summary status for scientific_memory_import_report.json.""" + scb = get_science_claim_bundle(bundle) + vr = _get_verification_result(bundle, scb) if vr is None: - warnings.append("VerificationResult is absent; import proceeds with advisory only.") - - trace_cert = scb.get("trace_certificate") - if not isinstance(trace_cert, dict): - certificates = scb.get("certificates") - if isinstance(certificates, list) and certificates and isinstance(certificates[0], dict): - trace_cert = certificates[0] - if isinstance(trace_cert, dict): - status = str(trace_cert.get("status") or "") - if status != "CertificateChecked": - warnings.append( - f"trace_certificate.status is {status!r}, expected CertificateChecked." - ) - - return warnings + return "absent" + if verification_result_passed(vr): + return "passed" + return "failed" diff --git a/pipeline/src/sm_pipeline/validate/gate_engine.py b/pipeline/src/sm_pipeline/validate/gate_engine.py index 03941e7..af95db3 100644 --- a/pipeline/src/sm_pipeline/validate/gate_engine.py +++ b/pipeline/src/sm_pipeline/validate/gate_engine.py @@ -86,6 +86,12 @@ def _echo_recommendations(repo_root: Path) -> None: print(msg, file=sys.stderr) +def _validate_pcs_corpus(repo_root: Path) -> None: + from sm_pipeline.validate.pcs_corpus import validate_pcs_corpus + + validate_pcs_corpus(repo_root) + + def run_all_gates(repo_root: Path) -> GateReport: """ Run all validation checks in deterministic order (parity with legacy validate_repo). @@ -107,6 +113,7 @@ def run_all_gates(repo_root: Path) -> GateReport: ("gate2", "reviewer_lifecycle", validate_reviewer_lifecycle), ("gate2", "theorem_card_reviewer", validate_theorem_card_reviewer), ("gate4", "coverage_integrity", validate_coverage), + ("gate2", "pcs_corpus", _validate_pcs_corpus), ] for gate_id, check_id, fn in checks: diff --git a/pipeline/src/sm_pipeline/validate/pcs_corpus.py b/pipeline/src/sm_pipeline/validate/pcs_corpus.py new file mode 100644 index 0000000..42a384b --- /dev/null +++ b/pipeline/src/sm_pipeline/validate/pcs_corpus.py @@ -0,0 +1,95 @@ +"""Validate imported PCS claims under corpus/pcs/claims/.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from sm_pipeline.pcs_import.bundle_utils import bundle_for_validation +from sm_pipeline.pcs_import.science_claim_bundle_importer import load_read_model +from sm_pipeline.pcs_validate.validator import BundleValidationError, validate_signed_bundle + +REQUIRED_READ_MODEL_KEYS = frozenset( + { + "claim", + "assumption_set", + "runtime_receipt", + "trace_certificate", + "verification_result", + "artifact_hashes", + "canonical_digests", + "source_repositories", + "reproduce_commands", + "verify_commands", + "limitations", + "limitation_notice", + } +) + +IMPORT_REPORT_KEYS = frozenset( + { + "claim_id", + "imported_at", + "source_bundle_path", + "verification_status", + "warnings", + "stale_artifacts", + "render_path", + } +) + + +def validate_pcs_corpus(repo_root: Path) -> None: + """Re-validate signed bundles and check read models for all imported PCS claims.""" + repo_root = repo_root.resolve() + claims_root = repo_root / "corpus" / "pcs" / "claims" + if not claims_root.is_dir(): + return + + errors: list[str] = [] + for claim_dir in sorted(claims_root.iterdir()): + if not claim_dir.is_dir(): + continue + claim_id = claim_dir.name + signed_path = claim_dir / "signed_bundle.json" + if not signed_path.is_file(): + errors.append(f"{claim_id}: missing signed_bundle.json") + continue + + bundle = json.loads(signed_path.read_text(encoding="utf-8")) + if not isinstance(bundle, dict): + errors.append(f"{claim_id}: signed_bundle.json must be an object") + continue + + try: + validate_signed_bundle( + bundle_for_validation(bundle), repo_root=repo_root, strict=True + ) + except BundleValidationError as exc: + errors.append(f"{claim_id}: bundle validation failed: {exc}") + + read_model = load_read_model(repo_root, claim_id) + if read_model is None: + errors.append(f"{claim_id}: missing read_model.json") + continue + + missing = REQUIRED_READ_MODEL_KEYS - set(read_model.keys()) + if missing: + errors.append(f"{claim_id}: read_model missing keys: {sorted(missing)}") + + report_path = claim_dir / "scientific_memory_import_report.json" + if not report_path.is_file(): + errors.append(f"{claim_id}: missing scientific_memory_import_report.json") + else: + report = json.loads(report_path.read_text(encoding="utf-8")) + if not isinstance(report, dict): + errors.append(f"{claim_id}: import report must be an object") + else: + missing_report = IMPORT_REPORT_KEYS - set(report.keys()) + if missing_report: + errors.append( + f"{claim_id}: import report missing keys: {sorted(missing_report)}" + ) + + if errors: + raise ValueError("PCS corpus validation failed:\n" + "\n".join(errors)) diff --git a/portal/.generated/pcs-export.json b/portal/.generated/pcs-export.json index c84b937..00a7f7e 100644 --- a/portal/.generated/pcs-export.json +++ b/portal/.generated/pcs-export.json @@ -1,8 +1,302 @@ { "claim_ids": [ + "claim-qc-release-v0.1", "labtrust-qc-release-claim-001" ], "claims": { + "claim-qc-release-v0.1": { + "artifact_hashes": [ + { + "algorithm": "sha256", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "name": "signature_or_digest", + "source_artifact": "claim-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "name": "signature_or_digest", + "source_artifact": "as-labtrust-qc-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "name": "spec", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "name": "trace.json", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "name": "trace_hash", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "name": "policy_hash", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "name": "events_hash", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777", + "name": "signature_or_digest", + "source_artifact": "receipt-qc-release-run-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "name": "trace_hash", + "source_artifact": "cert-trace-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "name": "spec_hash", + "source_artifact": "cert-trace-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888", + "name": "signature_or_digest", + "source_artifact": "cert-trace-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "signature_or_digest", + "source_artifact": "scb-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "name": "claim-qc-release-v0.1", + "source_artifact": "evidence-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999", + "name": "signature_or_digest", + "source_artifact": "evidence-qc-release-v0.1" + }, + { + "algorithm": "sha256", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "name": "claim_artifact", + "source_artifact": "claim_artifact" + }, + { + "algorithm": "sha256", + "digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777", + "name": "runtime_receipt", + "source_artifact": "runtime_receipt" + }, + { + "algorithm": "sha256", + "digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888", + "name": "trace_certificate", + "source_artifact": "trace_certificate" + }, + { + "algorithm": "sha256", + "digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999", + "name": "evidence_bundle", + "source_artifact": "evidence_bundle" + }, + { + "algorithm": "sha256", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "name": "signed_bundle", + "source_artifact": "signed_bundle" + } + ], + "assumption_set": { + "assumption_set_id": "as-labtrust-qc-v0.1", + "assumptions": [ + { + "id": "asm-domain-qc-sim", + "kind": "domain", + "status": "HumanReviewed", + "text": "Simulation models a hospital QC release workflow, not a live clinical laboratory." + } + ], + "created_at": "2026-05-16T12:00:00Z", + "human_review_status": "approved", + "id": "as-labtrust-qc-v0.1", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "schema_version": "v0", + "signature_or_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "status": "HumanReviewed" + }, + "bundle_signature_or_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "canonical_digests": { + "claim_artifact": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "evidence_bundle": "sha256:9999999999999999999999999999999999999999999999999999999999999999", + "runtime_receipt": "sha256:7777777777777777777777777777777777777777777777777777777777777777", + "signed_bundle": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "trace_certificate": "sha256:8888888888888888888888888888888888888888888888888888888888888888" + }, + "claim": { + "created_at": "2026-05-16T12:05:00Z", + "guarantee_types": { + "certificate_checked": true, + "empirically_measured": false, + "formally_checked": false, + "human_reviewed": false, + "runtime_observed": true, + "unchecked_advisory": false + }, + "id": "claim-qc-release-v0.1", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "signature_or_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "status": "CertificateChecked", + "text": "The qc-release simulation run satisfies the hospital lab QC release temporal policy under stated assumptions." + }, + "claim_id": "claim-qc-release-v0.1", + "evidence_bundle": { + "artifact_hashes": { + "claim-qc-release-v0.1": "sha256:2222222222222222222222222222222222222222222222222222222222222222" + }, + "assumption_set_refs": [ + "as-labtrust-qc-v0.1" + ], + "bundle_id": "evidence-qc-release-v0.1", + "certificate_refs": [ + "cert-trace-qc-release-v0.1" + ], + "claim_refs": [ + "claim-qc-release-v0.1" + ], + "created_at": "2026-05-16T12:12:00Z", + "id": "evidence-qc-release-v0.1", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "runtime_receipt_refs": [ + "receipt-qc-release-run-001" + ], + "schema_version": "v0", + "signature_or_digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym" + }, + "limitation_notice": "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory.", + "limitations": [ + "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory." + ], + "reproduce_commands": [ + "labtrust run-demo qc-release", + "pf verify science-claim signed_science_claim_bundle.json" + ], + "runtime_receipt": { + "ended_at": "2026-05-16T12:00:00Z", + "environment": { + "labtrust_version": "0.1.0", + "platform": "linux" + }, + "events_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "final_reason_code": "ok", + "id": "receipt-qc-release-run-001", + "input_hashes": { + "spec": "sha256:6666666666666666666666666666666666666666666666666666666666666666" + }, + "output_hashes": { + "trace.json": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + }, + "policy_hash": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "producer": "labtrust-gym", + "producer_version": "0.1.0", + "receipt_id": "receipt-qc-release-run-001", + "released": true, + "run_id": "runs/qc-release", + "run_outcome": "passed", + "schema_version": "v0", + "signature_or_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777", + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "started_at": "2026-05-16T11:58:00Z", + "status": "RuntimeObserved", + "trace_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + }, + "schema_version": "PcsClaimReadModel.v0", + "source_repositories": [ + { + "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_repo": "https://github.com/fraware/LabTrust-Gym" + }, + { + "source_commit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_repo": "https://github.com/fraware/CertifyEdge" + }, + { + "source_commit": "cccccccccccccccccccccccccccccccccccccccc", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric" + } + ], + "trace_certificate": { + "certificate_id": "cert-trace-qc-release-v0.1", + "checker": "certifyedge", + "checker_version": "0.1.0", + "counterexample_ref": null, + "created_at": "2026-05-16T12:10:00Z", + "id": "cert-trace-qc-release-v0.1", + "producer": "certifyedge", + "producer_version": "0.1.0", + "property_id": "qc_release.temporal.safety", + "schema_version": "v0", + "signature_or_digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888", + "source_commit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "source_repo": "https://github.com/fraware/CertifyEdge", + "spec_hash": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "status": "CertificateChecked", + "trace_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + }, + "verification_result": { + "checks": [ + { + "detail": "{}", + "guarantee_type": null, + "id": "schema-valid", + "name": "ScienceClaimBundle conforms to schema", + "outcome": "pass" + }, + { + "detail": "{}", + "guarantee_type": null, + "id": "trace-hash-alignment", + "name": "Receipt trace_hash matches certificate", + "outcome": "pass" + } + ], + "id": "verify-scb-qc-release-v0.1", + "overall_outcome": "pass", + "signature_or_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "source_commit": "cccccccccccccccccccccccccccccccccccccccc", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "status": "ProofChecked", + "verification_id": "verify-scb-qc-release-v0.1", + "verifier": "provability-fabric", + "verifier_version": "0.1.0" + }, + "verify_commands": [ + "pf verify science-claim signed_science_claim_bundle.json", + "just pcs-validate-bundle BUNDLE=signed_science_claim_bundle.json" + ] + }, "labtrust-qc-release-claim-001": { "artifact_hashes": [ { @@ -11,6 +305,18 @@ "name": "claim_digest", "source_artifact": "labtrust-qc-release-claim-001" }, + { + "algorithm": "sha256", + "digest": "sha256:claim-artifact-001", + "name": "signature_or_digest", + "source_artifact": "labtrust-qc-release-claim-001" + }, + { + "algorithm": "sha256", + "digest": "sha256:assumption-set-001", + "name": "signature_or_digest", + "source_artifact": "labtrust-qc-release-assumptions" + }, { "algorithm": "sha256", "digest": "sha256:trace-json-qc-release", @@ -23,6 +329,12 @@ "name": "events_hash", "source_artifact": "runtime-receipt-qc-release" }, + { + "algorithm": "sha256", + "digest": "sha256:runtime-receipt-001", + "name": "signature_or_digest", + "source_artifact": "runtime-receipt-qc-release" + }, { "algorithm": "sha256", "digest": "sha256:temporal-policy-qc-release", @@ -34,6 +346,42 @@ "digest": "sha256:qc-release-stl", "name": "spec_hash", "source_artifact": "trace-certificate-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:trace-certificate-001", + "name": "signature_or_digest", + "source_artifact": "trace-certificate-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:verification-result-001", + "name": "signature_or_digest", + "source_artifact": "verification-result-labtrust-qc-release" + }, + { + "algorithm": "sha256", + "digest": "sha256:claim-artifact-001", + "name": "claim_artifact", + "source_artifact": "claim_artifact" + }, + { + "algorithm": "sha256", + "digest": "sha256:runtime-receipt-001", + "name": "runtime_receipt", + "source_artifact": "runtime_receipt" + }, + { + "algorithm": "sha256", + "digest": "sha256:trace-certificate-001", + "name": "trace_certificate", + "source_artifact": "trace_certificate" + }, + { + "algorithm": "sha256", + "digest": "sha256:signed-bundle-labtrust-demo-001", + "name": "signed_bundle", + "source_artifact": "signed_bundle" } ], "assumption_set": { @@ -62,6 +410,13 @@ "status": "RuntimeObserved" }, "bundle_signature_or_digest": "sha256:signed-bundle-labtrust-demo-001", + "canonical_digests": { + "claim_artifact": "sha256:claim-artifact-001", + "evidence_bundle": "", + "runtime_receipt": "sha256:runtime-receipt-001", + "signed_bundle": "sha256:signed-bundle-labtrust-demo-001", + "trace_certificate": "sha256:trace-certificate-001" + }, "claim": { "created_at": "2026-05-01T11:00:00Z", "guarantee_types": { @@ -80,6 +435,7 @@ "text": "The qc-release workflow run observed protocol-compliant specimen handling and release gating in the LabTrust-Gym hospital-lab simulation." }, "claim_id": "labtrust-qc-release-claim-001", + "evidence_bundle": {}, "limitation_notice": "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory.", "limitations": [ "This artifact is a proof-carrying simulation result. It demonstrates protocol-level and runtime-evidence verification inside LabTrust-Gym. It is not a clinical validation, production medical certification, or guarantee about a real hospital laboratory." @@ -164,16 +520,15 @@ "outcome": "pass" } ], - "created_at": "2026-05-01T12:00:00Z", "id": "verification-result-labtrust-qc-release", "overall_outcome": "pass", - "producer": "provability-fabric", - "producer_version": "0.1.0", - "schema_version": "VerificationResult.v0", "signature_or_digest": "sha256:verification-result-001", "source_commit": "abc123def456", "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", - "status": "ProofChecked" + "status": "ProofChecked", + "verification_id": "verification-result-labtrust-qc-release", + "verifier": "provability-fabric", + "verifier_version": "0.1.0" }, "verify_commands": [ "pf verify science-claim signed_science_claim_bundle.json", diff --git a/portal/components/pcs/ArtifactHashTable.tsx b/portal/components/pcs/ArtifactHashTable.tsx index 41f250f..c3c885d 100644 --- a/portal/components/pcs/ArtifactHashTable.tsx +++ b/portal/components/pcs/ArtifactHashTable.tsx @@ -1,40 +1,58 @@ -import type { PcsHashRow } from "@/lib/pcsTypes"; +import type { PcsCanonicalDigests, PcsHashRow } from "@/lib/pcsTypes"; + +const CANONICAL_LABELS: { key: keyof PcsCanonicalDigests; label: string }[] = [ + { key: "claim_artifact", label: "Claim artifact digest" }, + { key: "runtime_receipt", label: "Runtime receipt digest" }, + { key: "trace_certificate", label: "Trace certificate digest" }, + { key: "evidence_bundle", label: "Evidence bundle digest" }, + { key: "signed_bundle", label: "Signed bundle digest" }, +]; interface ArtifactHashTableProps { hashes: PcsHashRow[]; + canonicalDigests?: PcsCanonicalDigests; } -export function ArtifactHashTable({ hashes }: ArtifactHashTableProps) { +export function ArtifactHashTable({ hashes, canonicalDigests }: ArtifactHashTableProps) { + const canonicalRows = + canonicalDigests != null + ? CANONICAL_LABELS.map(({ key, label }) => ({ + name: label, + digest: canonicalDigests[key] || "", + source_artifact: key, + })).filter((row) => row.digest.length > 0) + : []; + + const extraHashes = hashes.filter( + (row) => !canonicalRows.some((c) => c.digest === row.digest && c.name === row.name), + ); + + const rows = [...canonicalRows, ...extraHashes]; + return (

      Artifact Hashes

      - {hashes.length === 0 ? ( + {rows.length === 0 ? (

      No hashes recorded.

      ) : (
      - + - - - {hashes.map((row) => ( + {rows.map((row) => ( - + - - ))} diff --git a/portal/components/pcs/EvidenceBundleView.tsx b/portal/components/pcs/EvidenceBundleView.tsx new file mode 100644 index 0000000..b159f15 --- /dev/null +++ b/portal/components/pcs/EvidenceBundleView.tsx @@ -0,0 +1,34 @@ +import type { PcsNamedArtifact } from "@/lib/pcsTypes"; + +interface EvidenceBundleViewProps { + evidence: PcsNamedArtifact; +} + +export function EvidenceBundleView({ evidence }: EvidenceBundleViewProps) { + if (!evidence?.id && !evidence?.signature_or_digest) { + return null; + } + return ( +
      +

      Evidence Bundle

      + {evidence.id != null && ( +

      + ID: {String(evidence.id)} +

      + )} + {evidence.status != null && ( +

      + Status:{" "} + + {String(evidence.status)} + +

      + )} + {evidence.payload != null && ( +
      +          {JSON.stringify(evidence.payload, null, 2)}
      +        
      + )} +
      + ); +} diff --git a/portal/components/pcs/PcsClaimPage.tsx b/portal/components/pcs/PcsClaimPage.tsx index 5701f71..142ed1b 100644 --- a/portal/components/pcs/PcsClaimPage.tsx +++ b/portal/components/pcs/PcsClaimPage.tsx @@ -3,6 +3,7 @@ import type { PcsClaimReadModel } from "@/lib/pcsTypes"; import { ArtifactHashTable } from "./ArtifactHashTable"; import { AssumptionSetView } from "./AssumptionSetView"; import { ClaimArtifactView } from "./ClaimArtifactView"; +import { EvidenceBundleView } from "./EvidenceBundleView"; import { LimitationNotice } from "./LimitationNotice"; import { ReplayCommand } from "./ReplayCommand"; import { RuntimeReceiptView } from "./RuntimeReceiptView"; @@ -32,8 +33,14 @@ export function PcsClaimPage({ model }: PcsClaimPageProps) { + {model.evidence_bundle?.id ? ( + + ) : null} - + + {label}:{" "} + + {value} + +

      + ); +} + export function VerificationResultView({ result }: VerificationResultViewProps) { if (!result) { return (

      Verification Result

      - No VerificationResult was bundled. Checks from Provability Fabric are not shown. + No VerificationResult.v0 from Provability Fabric was bundled. External + verification checks are not shown.

      ); } const checks = result.checks ?? []; + const verificationId = result.verification_id ?? result.id ?? ""; + return (

      Verification Result

      -

      - Status:{" "} - - {String(result.status ?? "unknown")} - - {result.overall_outcome != null && ( - - Overall:{" "} - - {result.overall_outcome} - +

      Provability Fabric VerificationResult.v0

      + +
      + +

      + status:{" "} + + {String(result.status ?? "unknown")} - )} -

      -
        + {result.overall_outcome != null && ( + + overall:{" "} + + {result.overall_outcome} + + + )} +

        + + + + + +
      + +

      checks

      +
        {checks.map((c) => (
      • @@ -52,7 +80,9 @@ export function VerificationResultView({ result }: VerificationResultViewProps) {c.guarantee_type} )}
        - {c.detail != null &&

        {c.detail}

        } + {c.detail != null && c.detail !== "" && ( +

        {c.detail}

        + )}
      • ))}
      diff --git a/portal/lib/pcsTypes.ts b/portal/lib/pcsTypes.ts index 0a3de11..3fc8561 100644 --- a/portal/lib/pcsTypes.ts +++ b/portal/lib/pcsTypes.ts @@ -34,11 +34,15 @@ export type PcsVerificationCheck = { name: string; outcome: string; detail?: string; - guarantee_type?: string; + guarantee_type?: string | null; }; export type PcsVerificationResult = { + verification_id?: string; + id?: string; status?: string; + verifier?: string; + verifier_version?: string; overall_outcome?: string; signature_or_digest?: string; source_repo?: string; @@ -53,6 +57,14 @@ export type PcsHashRow = { source_artifact?: string; }; +export type PcsCanonicalDigests = { + claim_artifact: string; + runtime_receipt: string; + trace_certificate: string; + evidence_bundle: string; + signed_bundle: string; +}; + export type PcsClaimReadModel = { schema_version: string; claim_id: string; @@ -60,8 +72,10 @@ export type PcsClaimReadModel = { assumption_set: PcsNamedArtifact & { assumptions?: PcsAssumption[] }; runtime_receipt: PcsNamedArtifact; trace_certificate: PcsNamedArtifact; + evidence_bundle?: PcsNamedArtifact; verification_result?: PcsVerificationResult | null; artifact_hashes: PcsHashRow[]; + canonical_digests?: PcsCanonicalDigests; source_repositories: { source_repo: string; source_commit: string }[]; reproduce_commands: string[]; verify_commands: string[]; diff --git a/portal/scripts/generate-search-index.mjs b/portal/scripts/generate-search-index.mjs index ab8009d..185014d 100644 --- a/portal/scripts/generate-search-index.mjs +++ b/portal/scripts/generate-search-index.mjs @@ -38,7 +38,24 @@ for (const p of papers) { } } -const searchIndex = { papers, claims }; +const pcsClaims = []; +const pcsRoot = path.join(CORPUS, "pcs", "claims"); +if (fs.existsSync(pcsRoot)) { + for (const claimId of fs.readdirSync(pcsRoot)) { + const readModelPath = path.join(pcsRoot, claimId, "read_model.json"); + if (!fs.existsSync(readModelPath)) continue; + const model = readJson(readModelPath, null); + if (!model?.claim_id) continue; + const text = String(model.claim?.text ?? "").slice(0, SNIPPET_LENGTH); + pcsClaims.push({ + id: String(model.claim_id), + informal_text: text, + href: `/pcs/claims/${model.claim_id}`, + }); + } +} + +const searchIndex = { papers, claims, pcs_claims: pcsClaims }; const outDir = path.join(PORTAL_ROOT, "public"); if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); fs.writeFileSync( diff --git a/pyproject.toml b/pyproject.toml index 18bdc61..c2866b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,3 +13,4 @@ target-version = "py311" [tool.pytest.ini_options] testpaths = ["pipeline/tests", "kernels/adsorption/tests", "tests/pcs"] +pythonpath = ["."] diff --git a/schemas/pcs/AssumptionSet.v0.schema.json b/schemas/pcs/AssumptionSet.v0.schema.json new file mode 100644 index 0000000..25cd295 --- /dev/null +++ b/schemas/pcs/AssumptionSet.v0.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/AssumptionSet.v0.schema.json", + "title": "AssumptionSet.v0", + "type": "object", + "required": [ + "assumption_set_id", + "schema_version", + "created_at", + "producer", + "producer_version", + "source_repo", + "source_commit", + "assumptions", + "human_review_status", + "status", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "assumption_set_id": { "type": "string", "minLength": 1 }, + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "created_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, + "producer": { "type": "string", "minLength": 1 }, + "producer_version": { "type": "string", "minLength": 1 }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "assumptions": { + "type": "array", + "items": { + "type": "object", + "required": ["assumption_id", "text", "kind", "status", "source_span_refs"], + "additionalProperties": false, + "properties": { + "assumption_id": { "type": "string", "minLength": 1 }, + "text": { "type": "string", "minLength": 1 }, + "kind": { + "type": "string", + "enum": ["domain", "operational", "formal", "empirical", "security", "policy"] + }, + "status": { "$ref": "common.defs.json#/$defs/artifact_status" }, + "source_span_refs": { "$ref": "common.defs.json#/$defs/ref_list" } + } + } + }, + "human_review_status": { + "type": "string", + "enum": ["pending", "approved", "rejected", "not_required"] + }, + "status": { "$ref": "common.defs.json#/$defs/artifact_status" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/pcs/ClaimArtifact.v0.schema.json b/schemas/pcs/ClaimArtifact.v0.schema.json new file mode 100644 index 0000000..360cd59 --- /dev/null +++ b/schemas/pcs/ClaimArtifact.v0.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/ClaimArtifact.v0.schema.json", + "title": "ClaimArtifact.v0", + "type": "object", + "required": [ + "artifact_id", + "artifact_type", + "schema_version", + "claim_text", + "claim_kind", + "status", + "assumption_set_ref", + "source_span_refs", + "formal_statement", + "certificate_refs", + "runtime_receipt_refs", + "created_at", + "producer", + "producer_version", + "source_repo", + "source_commit", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "artifact_id": { "type": "string", "minLength": 1 }, + "artifact_type": { "const": "ClaimArtifact.v0" }, + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "claim_text": { "type": "string", "minLength": 1 }, + "claim_kind": { + "type": "string", + "enum": [ + "scientific_claim", + "protocol_safety_claim", + "runtime_safety_claim", + "temporal_claim", + "policy_claim" + ] + }, + "status": { "$ref": "common.defs.json#/$defs/artifact_status" }, + "assumption_set_ref": { "type": "string", "minLength": 1 }, + "source_span_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "formal_statement": { "type": "string" }, + "certificate_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "runtime_receipt_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "created_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, + "producer": { "type": "string", "minLength": 1 }, + "producer_version": { "type": "string", "minLength": 1 }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/pcs/EvidenceBundle.v0.schema.json b/schemas/pcs/EvidenceBundle.v0.schema.json new file mode 100644 index 0000000..93b707d --- /dev/null +++ b/schemas/pcs/EvidenceBundle.v0.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/EvidenceBundle.v0.schema.json", + "title": "EvidenceBundle.v0", + "type": "object", + "required": [ + "bundle_id", + "schema_version", + "claim_refs", + "assumption_set_refs", + "runtime_receipt_refs", + "certificate_refs", + "artifact_hashes", + "created_at", + "producer", + "producer_version", + "source_repo", + "source_commit", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "bundle_id": { "type": "string", "minLength": 1 }, + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "claim_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "assumption_set_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "runtime_receipt_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "certificate_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "artifact_hashes": { "$ref": "common.defs.json#/$defs/hash_map" }, + "created_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, + "producer": { "type": "string", "minLength": 1 }, + "producer_version": { "type": "string", "minLength": 1 }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/pcs/RuntimeReceipt.v0.schema.json b/schemas/pcs/RuntimeReceipt.v0.schema.json new file mode 100644 index 0000000..98a034c --- /dev/null +++ b/schemas/pcs/RuntimeReceipt.v0.schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/RuntimeReceipt.v0.schema.json", + "title": "RuntimeReceipt.v0", + "type": "object", + "required": [ + "receipt_id", + "schema_version", + "run_id", + "environment", + "started_at", + "ended_at", + "status", + "run_outcome", + "final_reason_code", + "released", + "events_hash", + "policy_hash", + "trace_hash", + "producer", + "producer_version", + "source_repo", + "source_commit", + "input_hashes", + "output_hashes", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "receipt_id": { "type": "string", "minLength": 1 }, + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "run_id": { "type": "string", "minLength": 1 }, + "environment": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "started_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, + "ended_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, + "status": { "$ref": "common.defs.json#/$defs/artifact_status" }, + "run_outcome": { "type": "string", "enum": ["passed", "failed"] }, + "final_reason_code": { "type": "string", "minLength": 1 }, + "released": { "type": "boolean" }, + "events_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "policy_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "trace_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "producer": { "type": "string", "minLength": 1 }, + "producer_version": { "type": "string", "minLength": 1 }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "local_dev": { "type": "boolean" }, + "input_hashes": { "$ref": "common.defs.json#/$defs/hash_map" }, + "output_hashes": { "$ref": "common.defs.json#/$defs/hash_map" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/pcs/SCHEMA_MIRROR.json b/schemas/pcs/SCHEMA_MIRROR.json new file mode 100644 index 0000000..fdf59a9 --- /dev/null +++ b/schemas/pcs/SCHEMA_MIRROR.json @@ -0,0 +1,21 @@ +{ + "source": "C:\\Users\\mateo\\pcs-core\\schemas", + "canonical_schemas": [ + "SignedScienceClaimBundle.v0.schema.json", + "ScienceClaimBundle.v0.schema.json", + "VerificationResult.v0.schema.json", + "ClaimArtifact.v0.schema.json", + "AssumptionSet.v0.schema.json", + "RuntimeReceipt.v0.schema.json", + "TraceCertificate.v0.schema.json", + "EvidenceBundle.v0.schema.json", + "SourceSpan.v0.schema.json", + "common.defs.json" + ], + "legacy_aliases": { + "signed_science_claim_bundle.schema.json": "SignedScienceClaimBundle.v0.schema.json", + "science_claim_bundle.schema.json": "ScienceClaimBundle.v0.schema.json", + "verification_result.schema.json": "VerificationResult.v0.schema.json" + }, + "note": "Scientific Memory mirrors pcs-core; pcs-core remains canonical." +} diff --git a/schemas/pcs/ScienceClaimBundle.v0.schema.json b/schemas/pcs/ScienceClaimBundle.v0.schema.json new file mode 100644 index 0000000..e220949 --- /dev/null +++ b/schemas/pcs/ScienceClaimBundle.v0.schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/ScienceClaimBundle.v0.schema.json", + "title": "ScienceClaimBundle.v0", + "type": "object", + "required": [ + "bundle_id", + "schema_version", + "claim_artifact", + "assumption_set", + "runtime_receipts", + "certificates", + "evidence_bundle", + "verification_policy", + "created_at", + "producer", + "producer_version", + "source_repo", + "source_commit", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "bundle_id": { "type": "string", "minLength": 1 }, + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "claim_artifact": { "$ref": "ClaimArtifact.v0.schema.json" }, + "assumption_set": { "$ref": "AssumptionSet.v0.schema.json" }, + "runtime_receipts": { + "type": "array", + "items": { "$ref": "RuntimeReceipt.v0.schema.json" }, + "minItems": 1 + }, + "certificates": { + "type": "array", + "items": { "$ref": "TraceCertificate.v0.schema.json" } + }, + "evidence_bundle": { "$ref": "EvidenceBundle.v0.schema.json" }, + "verification_policy": { + "type": "object", + "required": ["policy_id", "required_checks"], + "additionalProperties": true, + "properties": { + "policy_id": { "type": "string", "minLength": 1 }, + "required_checks": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + } + }, + "created_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, + "producer": { "type": "string", "minLength": 1 }, + "producer_version": { "type": "string", "minLength": 1 }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/pcs/SignedScienceClaimBundle.v0.schema.json b/schemas/pcs/SignedScienceClaimBundle.v0.schema.json new file mode 100644 index 0000000..e6293cb --- /dev/null +++ b/schemas/pcs/SignedScienceClaimBundle.v0.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/SignedScienceClaimBundle.v0.schema.json", + "title": "SignedScienceClaimBundle.v0", + "type": "object", + "required": [ + "schema_version", + "signed_bundle_id", + "science_claim_bundle", + "verification_result", + "signer", + "signed_at", + "source_repo", + "source_commit", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "const": "SignedScienceClaimBundle.v0" }, + "signed_bundle_id": { "type": "string", "minLength": 1 }, + "science_claim_bundle": { "$ref": "ScienceClaimBundle.v0.schema.json" }, + "verification_result": { "$ref": "VerificationResult.v0.schema.json" }, + "signer": { "type": "string", "minLength": 1 }, + "signed_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 1 }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "local_dev": { "type": "boolean" } + } +} diff --git a/schemas/pcs/SourceSpan.v0.schema.json b/schemas/pcs/SourceSpan.v0.schema.json new file mode 100644 index 0000000..fe2e606 --- /dev/null +++ b/schemas/pcs/SourceSpan.v0.schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/SourceSpan.v0.schema.json", + "title": "SourceSpan.v0", + "type": "object", + "required": [ + "source_span_id", + "schema_version", + "source_type", + "source_uri", + "start", + "end", + "hash", + "description" + ], + "additionalProperties": false, + "properties": { + "source_span_id": { "type": "string", "minLength": 1 }, + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "source_type": { + "type": "string", + "enum": [ + "paper", + "policy_file", + "trace_event", + "lean_file", + "protocol_file", + "runtime_log", + "manual_note", + "source_code" + ] + }, + "source_uri": { "type": "string", "minLength": 1 }, + "start": { + "type": "object", + "required": ["line", "column"], + "additionalProperties": false, + "properties": { + "line": { "type": "integer", "minimum": 1 }, + "column": { "type": "integer", "minimum": 0 } + } + }, + "end": { + "type": "object", + "required": ["line", "column"], + "additionalProperties": false, + "properties": { + "line": { "type": "integer", "minimum": 1 }, + "column": { "type": "integer", "minimum": 0 } + } + }, + "hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "description": { "type": "string" } + } +} diff --git a/schemas/pcs/TraceCertificate.v0.schema.json b/schemas/pcs/TraceCertificate.v0.schema.json new file mode 100644 index 0000000..a4d344f --- /dev/null +++ b/schemas/pcs/TraceCertificate.v0.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/TraceCertificate.v0.schema.json", + "title": "TraceCertificate.v0", + "type": "object", + "required": [ + "certificate_id", + "schema_version", + "trace_hash", + "spec_hash", + "property_id", + "checker", + "checker_version", + "status", + "counterexample_ref", + "created_at", + "producer", + "producer_version", + "source_repo", + "source_commit", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "certificate_id": { "type": "string", "minLength": 1 }, + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "trace_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "spec_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "property_id": { "type": "string", "minLength": 1 }, + "checker": { "type": "string", "minLength": 1 }, + "checker_version": { "type": "string", "minLength": 1 }, + "status": { "$ref": "common.defs.json#/$defs/trace_certificate_status" }, + "counterexample_ref": { + "oneOf": [{ "type": "null" }, { "type": "string", "minLength": 1 }] + }, + "created_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, + "producer": { "type": "string", "minLength": 1 }, + "producer_version": { "type": "string", "minLength": 1 }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/pcs/VerificationResult.v0.schema.json b/schemas/pcs/VerificationResult.v0.schema.json new file mode 100644 index 0000000..be045ed --- /dev/null +++ b/schemas/pcs/VerificationResult.v0.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/VerificationResult.v0.schema.json", + "title": "VerificationResult.v0", + "type": "object", + "required": [ + "verification_id", + "schema_version", + "bundle_id", + "verifier", + "verifier_version", + "status", + "checks", + "created_at", + "source_repo", + "source_commit", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "verification_id": { "type": "string", "minLength": 1 }, + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "bundle_id": { "type": "string", "minLength": 1 }, + "verifier": { "type": "string", "minLength": 1 }, + "verifier_version": { "type": "string", "minLength": 1 }, + "status": { "$ref": "common.defs.json#/$defs/artifact_status" }, + "checks": { + "type": "array", + "items": { + "type": "object", + "required": ["check_id", "description", "status", "details"], + "additionalProperties": false, + "properties": { + "check_id": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 }, + "status": { + "type": "string", + "enum": ["passed", "failed", "skipped", "warning"] + }, + "details": { "type": "object" } + } + } + }, + "created_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/pcs/common.defs.json b/schemas/pcs/common.defs.json new file mode 100644 index 0000000..20f6f47 --- /dev/null +++ b/schemas/pcs/common.defs.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/common.defs.json", + "title": "PCS Common Definitions", + "$defs": { + "schema_version": { + "type": "string", + "pattern": "^v0$" + }, + "iso8601_datetime": { + "type": "string", + "format": "date-time" + }, + "hex_digest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "artifact_status": { + "type": "string", + "enum": [ + "Draft", + "Extracted", + "HumanReviewed", + "Formalized", + "ProofPending", + "ProofChecked", + "CertificatePending", + "CertificateChecked", + "RuntimeObserved", + "RuntimeChecked", + "Rejected", + "EmpiricalOnly", + "Deprecated", + "Stale" + ] + }, + "trace_certificate_status": { + "type": "string", + "enum": ["CertificatePending", "CertificateChecked", "Rejected", "Stale"] + }, + "producer_metadata": { + "type": "object", + "required": [ + "schema_version", + "created_at", + "producer", + "producer_version", + "source_repo", + "source_commit", + "status", + "signature_or_digest" + ], + "properties": { + "schema_version": { "$ref": "#/$defs/schema_version" }, + "created_at": { "$ref": "#/$defs/iso8601_datetime" }, + "producer": { "type": "string", "minLength": 1 }, + "producer_version": { "type": "string", "minLength": 1 }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "status": { "$ref": "#/$defs/artifact_status" }, + "signature_or_digest": { "$ref": "#/$defs/hex_digest" } + } + }, + "hash_map": { + "type": "object", + "additionalProperties": { "$ref": "#/$defs/hex_digest" } + }, + "ref_list": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + } +} diff --git a/schemas/pcs/legacy/LabTrust.ScienceClaimBundle.v0.schema.json b/schemas/pcs/legacy/LabTrust.ScienceClaimBundle.v0.schema.json new file mode 100644 index 0000000..27ca1c6 --- /dev/null +++ b/schemas/pcs/legacy/LabTrust.ScienceClaimBundle.v0.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://scientific-memory.org/schemas/pcs/legacy/LabTrust.ScienceClaimBundle.v0.schema.json", + "title": "LabTrust legacy ScienceClaimBundle", + "type": "object", + "additionalProperties": true, + "required": [ + "schema_version", + "created_at", + "producer", + "producer_version", + "source_repo", + "source_commit", + "status", + "signature_or_digest", + "claim", + "assumption_set", + "runtime_receipt", + "trace_certificate" + ], + "properties": { + "schema_version": { "const": "ScienceClaimBundle.v0" }, + "claim": { "type": "object" }, + "assumption_set": { "type": "object" }, + "runtime_receipt": { "type": "object" }, + "trace_certificate": { "type": "object" }, + "evidence_bundle": { "type": "object" }, + "limitations": { "type": "array", "items": { "type": "string" } }, + "reproduce_commands": { "type": "array", "items": { "type": "string" } }, + "verify_commands": { "type": "array", "items": { "type": "string" } } + } +} diff --git a/schemas/pcs/legacy/LabTrust.SignedScienceClaimBundle.v0.schema.json b/schemas/pcs/legacy/LabTrust.SignedScienceClaimBundle.v0.schema.json new file mode 100644 index 0000000..496dc67 --- /dev/null +++ b/schemas/pcs/legacy/LabTrust.SignedScienceClaimBundle.v0.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://scientific-memory.org/schemas/pcs/legacy/LabTrust.SignedScienceClaimBundle.v0.schema.json", + "title": "LabTrust legacy signed bundle (portal import)", + "description": "Legacy mirror for Scientific Memory portal imports. Canonical PCS: pcs-core SignedScienceClaimBundle.v0.", + "type": "object", + "additionalProperties": true, + "required": ["schema_version", "science_claim_bundle", "signature_or_digest"], + "properties": { + "schema_version": { + "type": "string", + "anyOf": [ + { "const": "SignedScienceClaimBundle.v0" }, + { "const": "v0" } + ] + }, + "science_claim_bundle": { "type": "object" }, + "verification_result": { "type": "object" }, + "signature_or_digest": { "type": "string", "minLength": 1 }, + "reproduce_commands": { "type": "array", "items": { "type": "string" } }, + "verify_commands": { "type": "array", "items": { "type": "string" } } + } +} diff --git a/schemas/pcs/legacy/LabTrust.VerificationResult.v0.schema.json b/schemas/pcs/legacy/LabTrust.VerificationResult.v0.schema.json new file mode 100644 index 0000000..356f356 --- /dev/null +++ b/schemas/pcs/legacy/LabTrust.VerificationResult.v0.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://scientific-memory.org/schemas/pcs/legacy/LabTrust.VerificationResult.v0.schema.json", + "title": "LabTrust legacy VerificationResult", + "type": "object", + "additionalProperties": true, + "properties": { + "schema_version": { + "type": "string", + "anyOf": [{ "const": "VerificationResult.v0" }, { "const": "v0" }] + }, + "id": { "type": "string" }, + "verification_id": { "type": "string" }, + "verifier": { "type": "string" }, + "verifier_version": { "type": "string" }, + "producer": { "type": "string" }, + "producer_version": { "type": "string" }, + "status": { "type": "string" }, + "source_repo": { "type": "string" }, + "source_commit": { "type": "string" }, + "signature_or_digest": { "type": "string" }, + "checks": { "type": "array" }, + "overall_outcome": { "type": "string" } + } +} diff --git a/schemas/pcs/science_claim_bundle.schema.json b/schemas/pcs/science_claim_bundle.schema.json index 05dbce0..787a0ee 100644 --- a/schemas/pcs/science_claim_bundle.schema.json +++ b/schemas/pcs/science_claim_bundle.schema.json @@ -1,107 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://scientific-memory.org/schemas/pcs/science_claim_bundle.schema.json", - "title": "ScienceClaimBundle.v0", - "type": "object", - "additionalProperties": true, - "required": [ - "schema_version", - "created_at", - "producer", - "producer_version", - "source_repo", - "source_commit", - "status", - "signature_or_digest", - "claim", - "assumption_set", - "runtime_receipt", - "trace_certificate" - ], - "properties": { - "schema_version": { "const": "ScienceClaimBundle.v0" }, - "claim": { "$ref": "#/$defs/claimArtifact" }, - "assumption_set": { "$ref": "#/$defs/assumptionSet" }, - "runtime_receipt": { "$ref": "#/$defs/namedArtifact" }, - "trace_certificate": { "$ref": "#/$defs/namedArtifact" }, - "evidence_bundle": { "$ref": "#/$defs/namedArtifact" }, - "verification_result": { "$ref": "#/$defs/namedArtifact" }, - "limitations": { - "type": "array", - "items": { "type": "string" } - }, - "reproduce_commands": { - "type": "array", - "items": { "type": "string" } - }, - "verify_commands": { - "type": "array", - "items": { "type": "string" } - } - }, - "$defs": { - "artifactBase": { - "$ref": "https://scientific-memory.org/schemas/pcs/artifact_base.schema.json" - }, - "claimArtifact": { - "allOf": [ - { "$ref": "#/$defs/artifactBase" }, - { - "type": "object", - "required": ["id", "claim_text", "guarantee_types"], - "properties": { - "schema_version": { "const": "ClaimArtifact.v0" }, - "id": { "type": "string", "minLength": 1 }, - "claim_text": { "type": "string", "minLength": 1 }, - "guarantee_types": { - "type": "object", - "additionalProperties": { "type": "boolean" } - } - } - } - ] - }, - "assumptionSet": { - "allOf": [ - { "$ref": "#/$defs/artifactBase" }, - { - "type": "object", - "required": ["id", "assumptions"], - "properties": { - "schema_version": { "const": "AssumptionSet.v0" }, - "id": { "type": "string", "minLength": 1 }, - "assumptions": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": ["id", "text"], - "properties": { - "id": { "type": "string" }, - "text": { "type": "string" }, - "kind": { "type": "string" }, - "status": { "type": "string" } - }, - "additionalProperties": true - } - } - } - } - ] - }, - "namedArtifact": { - "allOf": [ - { "$ref": "#/$defs/artifactBase" }, - { - "type": "object", - "required": ["id"], - "properties": { - "id": { "type": "string", "minLength": 1 }, - "payload": { "type": "object" }, - "summary": { "type": "string" } - } - } - ] - } - } + "title": "ScienceClaimBundle (legacy alias)", + "description": "DEPRECATED alias. Use ScienceClaimBundle.v0.schema.json (pcs-core canonical).", + "$ref": "https://pcs-core/schemas/ScienceClaimBundle.v0.schema.json" } diff --git a/schemas/pcs/signed_science_claim_bundle.schema.json b/schemas/pcs/signed_science_claim_bundle.schema.json index 558cd0a..2163bdc 100644 --- a/schemas/pcs/signed_science_claim_bundle.schema.json +++ b/schemas/pcs/signed_science_claim_bundle.schema.json @@ -1,34 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://scientific-memory.org/schemas/pcs/signed_science_claim_bundle.schema.json", - "title": "SignedScienceClaimBundle (LabTrust v0.1)", - "type": "object", - "additionalProperties": true, - "required": ["schema_version", "science_claim_bundle", "signature_or_digest"], - "properties": { - "schema_version": { - "type": "string", - "pattern": "^SignedScienceClaimBundle\\.v0$" - }, - "bundle_id": { "type": "string", "minLength": 1 }, - "bundle_digest": { "type": "string", "minLength": 1 }, - "signed_at": { "type": "string", "minLength": 1 }, - "science_claim_bundle": { - "type": "object", - "description": "Validated by pcs-core or schemas/pcs/science_claim_bundle.schema.json" - }, - "verification_result": { - "type": "object", - "description": "Validated by pcs-core or schemas/pcs/verification_result.schema.json" - }, - "signature_or_digest": { "type": "string", "minLength": 1 }, - "reproduce_commands": { - "type": "array", - "items": { "type": "string" } - }, - "verify_commands": { - "type": "array", - "items": { "type": "string" } - } - } + "title": "SignedScienceClaimBundle (legacy alias)", + "description": "DEPRECATED alias. Use SignedScienceClaimBundle.v0.schema.json (pcs-core canonical).", + "$ref": "https://pcs-core/schemas/SignedScienceClaimBundle.v0.schema.json" } diff --git a/schemas/pcs/verification_result.schema.json b/schemas/pcs/verification_result.schema.json index 50fde71..b5df207 100644 --- a/schemas/pcs/verification_result.schema.json +++ b/schemas/pcs/verification_result.schema.json @@ -1,48 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://scientific-memory.org/schemas/pcs/verification_result.schema.json", - "title": "VerificationResult.v0", - "type": "object", - "additionalProperties": true, - "required": [ - "schema_version", - "created_at", - "producer", - "producer_version", - "source_repo", - "source_commit", - "status", - "signature_or_digest", - "checks" - ], - "properties": { - "schema_version": { "const": "VerificationResult.v0" }, - "checks": { - "type": "array", - "items": { - "type": "object", - "required": ["id", "name", "outcome"], - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "outcome": { - "type": "string", - "enum": ["pass", "fail", "skip", "warn"] - }, - "detail": { "type": "string" }, - "guarantee_type": { "type": "string" } - }, - "additionalProperties": true - } - }, - "overall_outcome": { - "type": "string", - "enum": ["pass", "fail", "partial"] - } - }, - "allOf": [ - { - "$ref": "https://scientific-memory.org/schemas/pcs/artifact_base.schema.json" - } - ] + "title": "VerificationResult (legacy alias)", + "description": "DEPRECATED alias. Use VerificationResult.v0.schema.json (pcs-core canonical).", + "$ref": "https://pcs-core/schemas/VerificationResult.v0.schema.json" } diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 91e238e..43675b5 100644 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -3,6 +3,10 @@ set -euo pipefail echo "==> installing python deps" uv sync --all-packages +if [ -d "../pcs-core/python" ] || [ -d "../../pcs-core/python" ] || [ -d "pcs-core/python" ]; then + echo "==> installing pcs-core extra (pipeline)" + uv sync --project pipeline --extra pcs || true +fi echo "==> installing node deps" pnpm install diff --git a/scripts/just_env.sh b/scripts/just_env.sh index 2b297a8..da6f5a6 100755 --- a/scripts/just_env.sh +++ b/scripts/just_env.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash # Shared PATH bootstrap for non-interactive bash (just recipes, smoke tests). +# Prefer OS TLS trust store for uv (Windows CRYPT_E_NO_REVOCATION_CHECK / UnknownIssuer). +export UV_NATIVE_TLS="${UV_NATIVE_TLS:-1}" # Git Bash on Windows usually inherits a full PATH; WSL/bash may not (uv only). _winroot="" for r in /c /mnt/c; do diff --git a/scripts/run_pcs_integration_tests.sh b/scripts/run_pcs_integration_tests.sh new file mode 100644 index 0000000..5ada091 --- /dev/null +++ b/scripts/run_pcs_integration_tests.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# PCS integration tests: live pcs-core validation (PCS_INTEGRATION=1 disables test mocks). +set -euo pipefail +_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=just_env.sh +source "$_root/scripts/just_env.sh" + +export PCS_INTEGRATION=1 +export PYTHONPATH="$_root/pipeline/src${PYTHONPATH:+:$PYTHONPATH}" + +for py in python python3; do + if command -v "$py" >/dev/null 2>&1 && "$py" -c "import pytest" 2>/dev/null; then + exec "$py" -m pytest "$_root/tests/pcs/test_pcs_integration.py" -v "$@" + fi +done + +exec uv run --no-sync --native-tls pytest "$_root/tests/pcs/test_pcs_integration.py" -v "$@" diff --git a/scripts/sm_python.sh b/scripts/sm_python.sh new file mode 100644 index 0000000..d58ebf2 --- /dev/null +++ b/scripts/sm_python.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Run pipeline CLI with an interpreter that already has deps (avoids PyPI on every just recipe). +set -euo pipefail +_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=just_env.sh +source "$_root/scripts/just_env.sh" + +export PYTHONPATH="$_root/pipeline/src${PYTHONPATH:+:$PYTHONPATH}" + +_python_candidates() { + # Prefer active conda/system Python (has pipeline deps); root .venv often lacks sm-pipeline. + command -v python 2>/dev/null || true + command -v python3 2>/dev/null || true + if [ -x "$_root/pipeline/.venv/Scripts/python.exe" ]; then + echo "$_root/pipeline/.venv/Scripts/python.exe" + fi + if [ -x "$_root/pipeline/.venv/bin/python" ]; then + echo "$_root/pipeline/.venv/bin/python" + fi + if [ -x "$_root/.venv/Scripts/python.exe" ]; then + echo "$_root/.venv/Scripts/python.exe" + fi + if [ -x "$_root/.venv/bin/python" ]; then + echo "$_root/.venv/bin/python" + fi +} + +if [ "${SM_FORCE_UV:-0}" != "1" ]; then + while IFS= read -r py; do + [ -n "$py" ] || continue + if "$py" -c "import typer" 2>/dev/null; then + exec "$py" "$@" + fi + done < <(_python_candidates) +fi + +if uv run --help 2>/dev/null | grep -q -- '--no-sync'; then + exec uv run --no-sync --native-tls --project "$_root/pipeline" python "$@" +fi + +exec uv run --native-tls --project "$_root/pipeline" python "$@" diff --git a/scripts/sync_pcs_schemas.py b/scripts/sync_pcs_schemas.py new file mode 100644 index 0000000..7cbf885 --- /dev/null +++ b/scripts/sync_pcs_schemas.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Copy canonical PCS schemas from pcs-core into scientific-memory (consumer mirror).""" + +from __future__ import annotations + +import json +import shutil +import sys +from pathlib import Path + +# Canonical v0.1 artifacts Scientific Memory imports/renders. +CANONICAL_SCHEMAS = ( + "SignedScienceClaimBundle.v0.schema.json", + "ScienceClaimBundle.v0.schema.json", + "VerificationResult.v0.schema.json", + "ClaimArtifact.v0.schema.json", + "AssumptionSet.v0.schema.json", + "RuntimeReceipt.v0.schema.json", + "TraceCertificate.v0.schema.json", + "EvidenceBundle.v0.schema.json", + "SourceSpan.v0.schema.json", + "common.defs.json", +) + +LEGACY_ALIASES = { + "signed_science_claim_bundle.schema.json": "SignedScienceClaimBundle.v0.schema.json", + "science_claim_bundle.schema.json": "ScienceClaimBundle.v0.schema.json", + "verification_result.schema.json": "VerificationResult.v0.schema.json", +} + + +def _find_pcs_core_schemas(repo_root: Path) -> Path: + candidates = [ + repo_root / "pcs-core" / "schemas", + repo_root.parent / "pcs-core" / "schemas", + Path(__file__).resolve().parents[1].parent / "pcs-core" / "schemas", + ] + for path in candidates: + if (path / "SignedScienceClaimBundle.v0.schema.json").is_file(): + return path + raise FileNotFoundError( + "pcs-core schemas not found. Clone pcs-core adjacent to scientific-memory " + "or set PCS_CORE_SCHEMAS_DIR." + ) + + +def main() -> int: + repo_root = Path(__file__).resolve().parents[1] + src = Path(sys.argv[1]) if len(sys.argv) > 1 else _find_pcs_core_schemas(repo_root) + dest = repo_root / "schemas" / "pcs" + dest.mkdir(parents=True, exist_ok=True) + # Never overwrite LabTrust legacy mirrors (schemas/pcs/legacy/). + (dest / "legacy").mkdir(exist_ok=True) + + copied: list[str] = [] + for name in CANONICAL_SCHEMAS: + source = src / name + if not source.is_file(): + print(f"skip missing: {name}", file=sys.stderr) + continue + shutil.copy2(source, dest / name) + copied.append(name) + + manifest = { + "source": str(src.resolve()), + "canonical_schemas": copied, + "legacy_aliases": LEGACY_ALIASES, + "note": "Scientific Memory mirrors pcs-core; pcs-core remains canonical.", + } + (dest / "SCHEMA_MIRROR.json").write_text( + json.dumps(manifest, indent=2) + "\n", + encoding="utf-8", + ) + + print(f"Synced {len(copied)} schemas from {src} -> {dest}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/pcs/__init__.py b/tests/pcs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pcs/conftest.py b/tests/pcs/conftest.py index 215e1c4..ff00ab5 100644 --- a/tests/pcs/conftest.py +++ b/tests/pcs/conftest.py @@ -1,8 +1,38 @@ -"""Ensure pipeline package is importable when running PCS tests from repo root.""" +"""PCS tests: pipeline on PYTHONPATH; unit tests use mirrors unless PCS_INTEGRATION=1.""" +import os +import shutil import sys from pathlib import Path +import pytest + _PIPELINE_SRC = Path(__file__).resolve().parents[2] / "pipeline" / "src" if str(_PIPELINE_SRC) not in sys.path: sys.path.insert(0, str(_PIPELINE_SRC)) +_PCS_TESTS = Path(__file__).resolve().parent +if str(_PCS_TESTS) not in sys.path: + sys.path.insert(0, str(_PCS_TESTS)) + + +def copy_pcs_schemas(root: Path) -> None: + dest = root / "schemas" / "pcs" + dest.mkdir(parents=True, exist_ok=True) + src = REPO_ROOT / "schemas" / "pcs" + for f in src.glob("*.json"): + shutil.copy(f, dest / f.name) + legacy_dest = dest / "legacy" + legacy_dest.mkdir(exist_ok=True) + for f in src.glob("legacy/*.json"): + shutil.copy(f, legacy_dest / f.name) + + +@pytest.fixture(autouse=True) +def _unit_tests_use_schema_mirrors(monkeypatch: pytest.MonkeyPatch) -> None: + """Contract tests use vendored mirrors; set PCS_INTEGRATION=1 for live pcs-core.""" + if os.environ.get("PCS_INTEGRATION") == "1": + return + monkeypatch.setattr( + "sm_pipeline.pcs_validate.validator.validate_with_pcs_core", + lambda _bundle: [], + ) diff --git a/tests/pcs/fixtures/failed_verification_result.json b/tests/pcs/fixtures/failed_verification_result.json new file mode 100644 index 0000000..53c554c --- /dev/null +++ b/tests/pcs/fixtures/failed_verification_result.json @@ -0,0 +1,81 @@ +{ + "schema_version": "SignedScienceClaimBundle.v0", + "signature_or_digest": "sha256:failed-vr", + "verification_result": { + "schema_version": "VerificationResult.v0", + "id": "vr-failed", + "created_at": "2026-05-01T12:00:00Z", + "producer": "provability-fabric", + "producer_version": "0.1.0", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "source_commit": "abc123def456", + "status": "failed", + "signature_or_digest": "sha256:vr-failed", + "overall_outcome": "fail", + "checks": [ + { + "id": "check-1", + "name": "Bundle signature", + "outcome": "fail", + "detail": "Signature mismatch." + } + ] + }, + "science_claim_bundle": { + "schema_version": "ScienceClaimBundle.v0", + "created_at": "2026-05-01T11:30:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "labtrust-demo", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:bundle", + "claim": { + "schema_version": "ClaimArtifact.v0", + "id": "claim-failed-vr", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "labtrust-demo", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:claim", + "claim_text": "Verification failed.", + "guarantee_types": {} + }, + "assumption_set": { + "schema_version": "AssumptionSet.v0", + "id": "assumptions", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "labtrust-demo", + "status": "RuntimeObserved", + "signature_or_digest": "sha256:assumptions", + "assumptions": [{ "id": "a1", "text": "Simulation only." }] + }, + "runtime_receipt": { + "schema_version": "RuntimeReceipt.v0", + "id": "receipt", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "labtrust-demo", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:receipt" + }, + "trace_certificate": { + "schema_version": "TraceCertificate.v0", + "id": "cert", + "created_at": "2026-05-01T11:00:00Z", + "producer": "CertifyEdge", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/CertifyEdge", + "source_commit": "cert-demo", + "status": "CertificateChecked", + "signature_or_digest": "sha256:cert" + } + } +} diff --git a/tests/pcs/fixtures/missing_assumption_set.json b/tests/pcs/fixtures/missing_assumption_set.json new file mode 100644 index 0000000..c5fb5d7 --- /dev/null +++ b/tests/pcs/fixtures/missing_assumption_set.json @@ -0,0 +1,62 @@ +{ + "schema_version": "SignedScienceClaimBundle.v0", + "signature_or_digest": "sha256:missing-assumption-set", + "verification_result": { + "schema_version": "VerificationResult.v0", + "id": "vr-1", + "created_at": "2026-05-01T12:00:00Z", + "producer": "provability-fabric", + "producer_version": "0.1.0", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "source_commit": "abc123def456", + "status": "ProofChecked", + "signature_or_digest": "sha256:vr", + "overall_outcome": "pass", + "checks": [{ "id": "c1", "name": "ok", "outcome": "pass" }] + }, + "science_claim_bundle": { + "schema_version": "ScienceClaimBundle.v0", + "created_at": "2026-05-01T11:30:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "bad-commit", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:bundle", + "claim": { + "schema_version": "ClaimArtifact.v0", + "id": "claim-no-assumption-set", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "bad-commit", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:claim", + "claim_text": "Missing assumption set.", + "guarantee_types": {} + }, + "runtime_receipt": { + "schema_version": "RuntimeReceipt.v0", + "id": "receipt", + "created_at": "2026-05-01T11:00:00Z", + "producer": "LabTrust-Gym", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/LabTrust-Gym", + "source_commit": "bad-commit", + "status": "RuntimeChecked", + "signature_or_digest": "sha256:receipt" + }, + "trace_certificate": { + "schema_version": "TraceCertificate.v0", + "id": "cert", + "created_at": "2026-05-01T11:00:00Z", + "producer": "CertifyEdge", + "producer_version": "0.1.0", + "source_repo": "https://github.com/fraware/CertifyEdge", + "source_commit": "bad-commit", + "status": "CertificateChecked", + "signature_or_digest": "sha256:cert" + } + } +} diff --git a/tests/pcs/fixtures/valid_pf_handoff_bundle.json b/tests/pcs/fixtures/valid_pf_handoff_bundle.json new file mode 100644 index 0000000..482ede0 --- /dev/null +++ b/tests/pcs/fixtures/valid_pf_handoff_bundle.json @@ -0,0 +1,9 @@ +{ + "schema_version": "v0", + "signed_bundle_id": "signed-scb-qc-release-pf", + "science_claim_bundle": {}, + "verification_result": {}, + "signer": "Provability Fabric", + "signed_at": "2026-05-16T12:30:00Z", + "signature_or_digest": "sha256:pf-handoff-placeholder" +} diff --git a/tests/pcs/fixtures/valid_signed_pcs_core_bundle.json b/tests/pcs/fixtures/valid_signed_pcs_core_bundle.json index fcd4f0b..de78ab0 100644 --- a/tests/pcs/fixtures/valid_signed_pcs_core_bundle.json +++ b/tests/pcs/fixtures/valid_signed_pcs_core_bundle.json @@ -1,48 +1,6 @@ { "schema_version": "SignedScienceClaimBundle.v0", - "bundle_id": "scb-qc-release-v0.1", - "bundle_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "signed_at": "2026-05-16T12:25:00Z", - "signature_or_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "reproduce_commands": [ - "labtrust run-demo qc-release", - "labtrust export-trace --run runs/qc-release --out trace.json", - "labtrust export-runtime-receipt --run runs/qc-release --out runtime_receipt.json", - "labtrust export-pcs --run runs/qc-release --out science_claim_bundle.pending.json" - ], - "verify_commands": [ - "pf verify science-claim signed_science_claim_bundle.json", - "pf sign science-claim science_claim_bundle.certified.json --out signed_science_claim_bundle.json", - "just pcs-validate-bundle BUNDLE=signed_science_claim_bundle.json" - ], - "verification_result": { - "verification_id": "verify-scb-qc-release-v0.1", - "schema_version": "v0", - "bundle_id": "scb-qc-release-v0.1", - "verifier": "provability-fabric", - "verifier_version": "0.1.0", - "status": "ProofChecked", - "checks": [ - { - "check_id": "schema-valid", - "description": "ScienceClaimBundle conforms to ScienceClaimBundle.v0 schema", - "status": "passed", - "details": {} - }, - { - "check_id": "trace-hash-alignment", - "description": "Runtime receipt trace_hash matches certificate trace_hash", - "status": "passed", - "details": { - "trace_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555" - } - } - ], - "created_at": "2026-05-16T12:20:00Z", - "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", - "source_commit": "cccccccccccccccccccccccccccccccccccccccc", - "signature_or_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - }, + "signed_bundle_id": "signed-scb-qc-release-v0.1", "science_claim_bundle": { "bundle_id": "scb-qc-release-v0.1", "schema_version": "v0", @@ -95,6 +53,9 @@ "started_at": "2026-05-16T11:58:00Z", "ended_at": "2026-05-16T12:00:00Z", "status": "RuntimeObserved", + "run_outcome": "passed", + "final_reason_code": "ok", + "released": true, "events_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", "policy_hash": "sha256:4444444444444444444444444444444444444444444444444444444444444444", "trace_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555", @@ -157,5 +118,44 @@ "source_repo": "https://github.com/fraware/LabTrust-Gym", "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "signature_or_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - } + }, + "verification_result": { + "verification_id": "verify-scb-qc-release-v0.1", + "schema_version": "v0", + "bundle_id": "scb-qc-release-v0.1", + "verifier": "provability-fabric", + "verifier_version": "0.1.0", + "status": "ProofChecked", + "checks": [ + { + "check_id": "schema-valid", + "description": "ScienceClaimBundle conforms to schema", + "status": "passed", + "details": {} + }, + { + "check_id": "trace-hash-alignment", + "description": "Receipt trace_hash matches certificate", + "status": "passed", + "details": {} + } + ], + "created_at": "2026-05-16T12:20:00Z", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "source_commit": "cccccccccccccccccccccccccccccccccccccccc", + "signature_or_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "signer": "Provability Fabric", + "signed_at": "2026-05-16T12:25:00Z", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "source_commit": "cccccccccccccccccccccccccccccccccccccccc", + "signature_or_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "reproduce_commands": [ + "labtrust run-demo qc-release", + "pf verify science-claim signed_science_claim_bundle.json" + ], + "verify_commands": [ + "pf verify science-claim signed_science_claim_bundle.json", + "just pcs-validate-bundle BUNDLE=signed_science_claim_bundle.json" + ] } diff --git a/tests/pcs/schema_fixtures.py b/tests/pcs/schema_fixtures.py new file mode 100644 index 0000000..9610283 --- /dev/null +++ b/tests/pcs/schema_fixtures.py @@ -0,0 +1,20 @@ +"""Copy PCS schema mirrors into a temp repo root for isolated tests.""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def copy_pcs_schemas(root: Path) -> None: + dest = root / "schemas" / "pcs" + dest.mkdir(parents=True, exist_ok=True) + src = REPO_ROOT / "schemas" / "pcs" + for f in src.glob("*.json"): + shutil.copy(f, dest / f.name) + legacy_dest = dest / "legacy" + legacy_dest.mkdir(exist_ok=True) + for f in src.glob("legacy/*.json"): + shutil.copy(f, legacy_dest / f.name) diff --git a/tests/pcs/test_import_labtrust_bundle.py b/tests/pcs/test_import_labtrust_bundle.py deleted file mode 100644 index c4b10b2..0000000 --- a/tests/pcs/test_import_labtrust_bundle.py +++ /dev/null @@ -1,58 +0,0 @@ -import json -import shutil -import tempfile -from pathlib import Path - -from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle - -FIXTURES = Path(__file__).resolve().parent / "fixtures" -REPO_ROOT = Path(__file__).resolve().parents[2] - - -def test_valid_bundle_imports() -> None: - bundle_path = FIXTURES / "valid_signed_science_claim_bundle.json" - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - _copy_schemas(root) - result = import_signed_bundle( - bundle_path, - repo_root=root, - write=True, - ) - assert result.claim_id == "labtrust-qc-release-claim-001" - read_model_path = ( - root / "corpus" / "pcs" / "claims" / result.claim_id / "read_model.json" - ) - assert read_model_path.is_file() - read_model = json.loads(read_model_path.read_text(encoding="utf-8")) - assert read_model["claim"]["text"] - assert read_model["source_repositories"] - assert any( - s["source_repo"] == "https://github.com/fraware/LabTrust-Gym" - for s in read_model["source_repositories"] - ) - - -def test_import_preserves_source_repo_and_commit() -> None: - bundle_path = FIXTURES / "valid_signed_science_claim_bundle.json" - raw = json.loads(bundle_path.read_text(encoding="utf-8")) - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - _copy_schemas(root) - import_signed_bundle(bundle_path, repo_root=root, write=True) - stored = json.loads( - (root / "corpus" / "pcs" / "claims" / "labtrust-qc-release-claim-001" / "signed_bundle.json").read_text( - encoding="utf-8" - ) - ) - scb = stored["science_claim_bundle"] - assert scb["claim"]["source_repo"] == raw["science_claim_bundle"]["claim"]["source_repo"] - assert scb["claim"]["source_commit"] == raw["science_claim_bundle"]["claim"]["source_commit"] - assert scb["claim"]["signature_or_digest"] == raw["science_claim_bundle"]["claim"]["signature_or_digest"] - - -def _copy_schemas(root: Path) -> None: - dest = root / "schemas" / "pcs" - dest.mkdir(parents=True) - for f in (REPO_ROOT / "schemas" / "pcs").glob("*.json"): - shutil.copy(f, dest / f.name) diff --git a/tests/pcs/test_import_pcs_core_bundle.py b/tests/pcs/test_import_pcs_core_bundle.py deleted file mode 100644 index 49bcf88..0000000 --- a/tests/pcs/test_import_pcs_core_bundle.py +++ /dev/null @@ -1,35 +0,0 @@ -import json -import shutil -import tempfile -from pathlib import Path - -from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle - -FIXTURES = Path(__file__).resolve().parent / "fixtures" -REPO_ROOT = Path(__file__).resolve().parents[2] - - -def test_pcs_core_signed_bundle_imports() -> None: - bundle_path = FIXTURES / "valid_signed_pcs_core_bundle.json" - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - _copy_schemas(root) - result = import_signed_bundle(bundle_path, repo_root=root, write=True) - assert result.claim_id == "claim-qc-release-v0.1" - read_model = json.loads( - (root / "corpus" / "pcs" / "claims" / result.claim_id / "read_model.json").read_text( - encoding="utf-8" - ) - ) - assert read_model["claim"]["text"] - assert read_model["trace_certificate"]["status"] == "CertificateChecked" - assert read_model["verification_result"] is not None - assert read_model["verification_result"]["checks"][0]["outcome"] == "pass" - assert any(h["name"] == "trace_hash" for h in read_model["artifact_hashes"]) - - -def _copy_schemas(root: Path) -> None: - dest = root / "schemas" / "pcs" - dest.mkdir(parents=True) - for f in (REPO_ROOT / "schemas" / "pcs").glob("*.json"): - shutil.copy(f, dest / f.name) diff --git a/tests/pcs/test_import_verification_result.py b/tests/pcs/test_import_verification_result.py deleted file mode 100644 index 4966318..0000000 --- a/tests/pcs/test_import_verification_result.py +++ /dev/null @@ -1,61 +0,0 @@ -import json -import shutil -import tempfile -from pathlib import Path - -from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle -from sm_pipeline.pcs_import.verification_result_importer import merge_verification_result -from sm_pipeline.pcs_validate.validator import collect_import_warnings - -FIXTURES = Path(__file__).resolve().parent / "fixtures" -REPO_ROOT = Path(__file__).resolve().parents[2] - - -def test_missing_verification_result_warning() -> None: - bundle = json.loads( - (FIXTURES / "missing_verification_result.json").read_text(encoding="utf-8") - ) - warnings = collect_import_warnings(bundle) - assert any("VerificationResult is absent" in w for w in warnings) - - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - _copy_schemas(root) - result = import_signed_bundle( - FIXTURES / "missing_verification_result.json", - repo_root=root, - write=True, - ) - assert any("VerificationResult is absent" in w for w in result.warnings) - read_model = json.loads( - (root / "corpus" / "pcs" / "claims" / result.claim_id / "read_model.json").read_text( - encoding="utf-8" - ) - ) - assert read_model.get("verification_result") is None - - -def test_merge_verification_result_preserves_checks() -> None: - bundle = json.loads( - (FIXTURES / "missing_verification_result.json").read_text(encoding="utf-8") - ) - vr = { - "schema_version": "VerificationResult.v0", - "created_at": "2026-05-01T12:00:00Z", - "producer": "provability-fabric", - "producer_version": "0.1.0", - "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", - "source_commit": "abc", - "status": "ProofChecked", - "signature_or_digest": "sha256:vr", - "checks": [{"id": "c1", "name": "test", "outcome": "pass"}], - } - merged = merge_verification_result(bundle, vr) - assert merged["verification_result"]["checks"][0]["id"] == "c1" - - -def _copy_schemas(root: Path) -> None: - dest = root / "schemas" / "pcs" - dest.mkdir(parents=True) - for f in (REPO_ROOT / "schemas" / "pcs").glob("*.json"): - shutil.copy(f, dest / f.name) diff --git a/tests/pcs/test_pcs_import.py b/tests/pcs/test_pcs_import.py new file mode 100644 index 0000000..4a4ffb4 --- /dev/null +++ b/tests/pcs/test_pcs_import.py @@ -0,0 +1,130 @@ +"""PCS import contract tests (canonical pcs-core schema names).""" + +import json +import shutil +import tempfile +from pathlib import Path + +import pytest + +from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle +from sm_pipeline.pcs_validate.schema_registry import ( + SIGNED_BUNDLE_SCHEMA, + resolve_schema_path, +) +from sm_pipeline.pcs_validate.validator import BundleValidationError, validate_signed_bundle + +from schema_fixtures import copy_pcs_schemas + +FIXTURES = Path(__file__).resolve().parent / "fixtures" +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_import_signed_science_claim_bundle_valid() -> None: + bundle_path = FIXTURES / "valid_signed_science_claim_bundle.json" + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + result = import_signed_bundle(bundle_path, repo_root=root, write=True) + assert result.claim_id == "labtrust-qc-release-claim-001" + report_path = ( + root + / "corpus" + / "pcs" + / "claims" + / result.claim_id + / "scientific_memory_import_report.json" + ) + assert report_path.is_file() + report = json.loads(report_path.read_text(encoding="utf-8")) + assert report["verification_status"] == "passed" + assert report["render_path"] == f"/pcs/claims/{result.claim_id}" + + +def test_import_pcs_core_signed_bundle_valid() -> None: + bundle_path = FIXTURES / "valid_signed_pcs_core_bundle.json" + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + result = import_signed_bundle(bundle_path, repo_root=root, write=True) + assert result.claim_id == "claim-qc-release-v0.1" + read_model = json.loads( + (root / "corpus" / "pcs" / "claims" / result.claim_id / "read_model.json").read_text( + encoding="utf-8" + ) + ) + vr = read_model["verification_result"] + assert vr["verification_id"] == "verify-scb-qc-release-v0.1" + assert vr["verifier"] == "provability-fabric" + assert read_model["canonical_digests"]["signed_bundle"] + + +def test_import_rejects_missing_assumption_set() -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + with pytest.raises(BundleValidationError): + import_signed_bundle( + FIXTURES / "missing_assumption_set.json", + repo_root=root, + write=False, + ) + + +def test_import_rejects_empty_assumptions() -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + with pytest.raises(BundleValidationError): + import_signed_bundle( + FIXTURES / "missing_assumptions.json", + repo_root=root, + write=False, + ) + + +def test_import_rejects_failed_verification_result() -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + with pytest.raises(BundleValidationError): + import_signed_bundle( + FIXTURES / "failed_verification_result.json", + repo_root=root, + write=False, + ) + + +def test_import_warns_or_rejects_missing_verification_result_depending_on_strict() -> None: + bundle_path = FIXTURES / "missing_verification_result.json" + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + with pytest.raises(BundleValidationError): + import_signed_bundle(bundle_path, repo_root=root, strict=True, write=False) + + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + _copy_schemas(root) + result = import_signed_bundle(bundle_path, repo_root=root, strict=False, write=True) + assert any("VerificationResult is absent" in w for w in result.warnings) + report = json.loads( + ( + root + / "corpus" + / "pcs" + / "claims" + / result.claim_id + / "scientific_memory_import_report.json" + ).read_text(encoding="utf-8") + ) + assert report["verification_status"] == "absent" + + +def test_canonical_schema_files_exist() -> None: + schemas_dir = REPO_ROOT / "schemas" / "pcs" + assert resolve_schema_path(schemas_dir, SIGNED_BUNDLE_SCHEMA).name == SIGNED_BUNDLE_SCHEMA + + +def _copy_schemas(root: Path) -> None: + copy_pcs_schemas(root) diff --git a/tests/pcs/test_pcs_integration.py b/tests/pcs/test_pcs_integration.py new file mode 100644 index 0000000..38838c0 --- /dev/null +++ b/tests/pcs/test_pcs_integration.py @@ -0,0 +1,57 @@ +"""Integration tests with live pcs-core (run: PCS_INTEGRATION=1 pytest tests/pcs/test_pcs_integration.py).""" + +from __future__ import annotations + +import json +import os +from pathlib import Path + +import pytest + +pytestmark = pytest.mark.skipif( + os.environ.get("PCS_INTEGRATION") != "1", + reason="Set PCS_INTEGRATION=1 to run live pcs-core validation", +) + +pcs_core = pytest.importorskip("pcs_core") + +from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle +from sm_pipeline.pcs_validate.pcs_core_hook import validate_with_pcs_core + +from schema_fixtures import copy_pcs_schemas + +FIXTURES = Path(__file__).resolve().parent / "fixtures" +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_pcs_core_validates_official_example() -> None: + bundle = json.loads( + (FIXTURES / "valid_signed_pcs_core_bundle.json").read_text(encoding="utf-8") + ) + errors = validate_with_pcs_core(bundle) + assert errors == [] + + +def test_pcs_core_bundle_imports_end_to_end(tmp_path: Path) -> None: + root = tmp_path + _copy_schemas(root) + result = import_signed_bundle( + FIXTURES / "valid_signed_pcs_core_bundle.json", + repo_root=root, + write=True, + ) + report = json.loads( + ( + root + / "corpus" + / "pcs" + / "claims" + / result.claim_id + / "scientific_memory_import_report.json" + ).read_text(encoding="utf-8") + ) + assert report["verification_status"] == "passed" + + +def _copy_schemas(root: Path) -> None: + copy_pcs_schemas(root) diff --git a/tests/pcs/test_render_pcs_claim.py b/tests/pcs/test_pcs_render.py similarity index 65% rename from tests/pcs/test_render_pcs_claim.py rename to tests/pcs/test_pcs_render.py index ce39abc..c5292a9 100644 --- a/tests/pcs/test_render_pcs_claim.py +++ b/tests/pcs/test_pcs_render.py @@ -1,3 +1,5 @@ +"""PCS portal read-model / rendering contract tests.""" + import json import shutil import tempfile @@ -6,6 +8,8 @@ from sm_pipeline.pcs_import.artifact_normalizer import LIMITATION_NOTICE, normalize_signed_bundle from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle +from schema_fixtures import copy_pcs_schemas + FIXTURES = Path(__file__).resolve().parent / "fixtures" REPO_ROOT = Path(__file__).resolve().parents[2] @@ -14,7 +18,10 @@ "assumption_set", "runtime_receipt", "trace_certificate", + "evidence_bundle", + "verification_result", "artifact_hashes", + "canonical_digests", "source_repositories", "reproduce_commands", "verify_commands", @@ -22,8 +29,16 @@ "limitation_notice", ) +CANONICAL_DIGEST_KEYS = ( + "claim_artifact", + "runtime_receipt", + "trace_certificate", + "evidence_bundle", + "signed_bundle", +) + -def test_portal_read_model_has_all_required_sections() -> None: +def test_render_claim_includes_all_required_sections() -> None: bundle = json.loads( (FIXTURES / "valid_signed_science_claim_bundle.json").read_text(encoding="utf-8") ) @@ -32,7 +47,7 @@ def test_portal_read_model_has_all_required_sections() -> None: assert key in read_model, f"missing read_model.{key}" -def test_limitations_notice_present() -> None: +def test_render_claim_includes_limitation_notice() -> None: bundle = json.loads( (FIXTURES / "valid_signed_science_claim_bundle.json").read_text(encoding="utf-8") ) @@ -41,16 +56,7 @@ def test_limitations_notice_present() -> None: assert LIMITATION_NOTICE in read_model["limitations"] -def test_artifact_hashes_displayed() -> None: - bundle = json.loads( - (FIXTURES / "valid_signed_science_claim_bundle.json").read_text(encoding="utf-8") - ) - read_model = normalize_signed_bundle(bundle) - assert len(read_model["artifact_hashes"]) >= 1 - assert all("digest" in row and "name" in row for row in read_model["artifact_hashes"]) - - -def test_source_repo_and_commit_displayed() -> None: +def test_render_claim_displays_source_repo_and_source_commit() -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) _copy_schemas(root) @@ -70,20 +76,20 @@ def test_source_repo_and_commit_displayed() -> None: assert "labtrust-qc-release-demo" in commits -def test_status_values_preserved() -> None: +def test_render_claim_displays_artifact_hashes() -> None: bundle = json.loads( - (FIXTURES / "valid_signed_science_claim_bundle.json").read_text(encoding="utf-8") + (FIXTURES / "valid_signed_pcs_core_bundle.json").read_text(encoding="utf-8") ) read_model = normalize_signed_bundle(bundle) - assert read_model["claim"]["status"] == "RuntimeChecked" - assert read_model["trace_certificate"]["status"] == "CertificateChecked" - vr = read_model["verification_result"] - assert vr is not None - assert vr["status"] == "ProofChecked" + digests = read_model["canonical_digests"] + for key in CANONICAL_DIGEST_KEYS: + assert key in digests + assert digests["claim_artifact"].startswith("sha256:") + assert digests["runtime_receipt"].startswith("sha256:") + assert digests["trace_certificate"].startswith("sha256:") + assert digests["signed_bundle"].startswith("sha256:") + assert len(read_model["artifact_hashes"]) >= 5 def _copy_schemas(root: Path) -> None: - dest = root / "schemas" / "pcs" - dest.mkdir(parents=True) - for f in (REPO_ROOT / "schemas" / "pcs").glob("*.json"): - shutil.copy(f, dest / f.name) + copy_pcs_schemas(root) diff --git a/tests/pcs/test_reject_invalid_bundle.py b/tests/pcs/test_reject_invalid_bundle.py deleted file mode 100644 index 7ec566f..0000000 --- a/tests/pcs/test_reject_invalid_bundle.py +++ /dev/null @@ -1,43 +0,0 @@ -import shutil -import tempfile -from pathlib import Path - -import pytest - -from sm_pipeline.pcs_import.science_claim_bundle_importer import import_signed_bundle -from sm_pipeline.pcs_validate.validator import BundleValidationError, validate_signed_bundle - -FIXTURES = Path(__file__).resolve().parent / "fixtures" -REPO_ROOT = Path(__file__).resolve().parents[2] - - -def test_invalid_bundle_rejected() -> None: - import json - - bundle = json.loads( - (FIXTURES / "invalid_missing_signature.json").read_text(encoding="utf-8") - ) - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - _copy_schemas(root) - with pytest.raises(BundleValidationError): - validate_signed_bundle(bundle, repo_root=root, strict=True) - - -def test_missing_assumption_rejected() -> None: - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - _copy_schemas(root) - with pytest.raises(BundleValidationError): - import_signed_bundle( - FIXTURES / "missing_assumptions.json", - repo_root=root, - write=False, - ) - - -def _copy_schemas(root: Path) -> None: - dest = root / "schemas" / "pcs" - dest.mkdir(parents=True) - for f in (REPO_ROOT / "schemas" / "pcs").glob("*.json"): - shutil.copy(f, dest / f.name) diff --git a/uv.lock b/uv.lock index 4b8e856..fe0c417 100644 --- a/uv.lock +++ b/uv.lock @@ -400,6 +400,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 }, ] +[[package]] +name = "pcs-core" +version = "0.1.0" +source = { editable = "../pcs-core/python" } +dependencies = [ + { name = "jsonschema" }, + { name = "referencing" }, +] + +[package.metadata] +requires-dist = [ + { name = "jsonschema", specifier = ">=4.23.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "referencing", specifier = ">=0.35.0,<0.37.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.0" }, +] +provides-extras = ["dev"] + [[package]] name = "pluggy" version = "1.6.0" @@ -614,16 +632,16 @@ wheels = [ [[package]] name = "referencing" -version = "0.37.0" +version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 }, + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, ] [[package]] @@ -786,6 +804,9 @@ dependencies = [ mcp = [ { name = "mcp" }, ] +pcs = [ + { name = "pcs-core" }, +] [package.metadata] requires-dist = [ @@ -793,13 +814,14 @@ requires-dist = [ { name = "jsonschema", specifier = ">=4.23" }, { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.0" }, { name = "networkx", specifier = ">=3.3" }, + { name = "pcs-core", marker = "extra == 'pcs'", editable = "../pcs-core/python" }, { name = "pydantic", specifier = ">=2.8" }, { name = "python-dotenv", specifier = ">=1.0" }, { name = "referencing", specifier = ">=0.35" }, { name = "rich", specifier = ">=13.7" }, { name = "typer", specifier = ">=0.12" }, ] -provides-extras = ["mcp"] +provides-extras = ["mcp", "pcs"] [[package]] name = "sortedcontainers" From df2ae28a2db9ab29d346b09a99769fa7b5e0d739 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 07:59:44 +0000 Subject: [PATCH 4/4] Update mcp requirement from >=1.0 to >=1.27.1 in /pipeline Updates the requirements on [mcp](https://github.com/modelcontextprotocol/python-sdk) to permit the latest version. - [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases) - [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md) - [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v1.0.0...v1.27.1) --- updated-dependencies: - dependency-name: mcp dependency-version: 1.27.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- pipeline/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline/pyproject.toml b/pipeline/pyproject.toml index 6eb0228..011c8e6 100644 --- a/pipeline/pyproject.toml +++ b/pipeline/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ ] [project.optional-dependencies] -mcp = ["mcp>=1.0"] +mcp = ["mcp>=1.27.1"] # Install when pcs-core is available: uv sync --project pipeline --extra pcs pcs = ["pcs-core>=0.1.0"]
      NameArtifact DigestAlgorithmSource artifact
      {row.name}{row.name} {row.digest} {row.algorithm ?? "sha256"} - {row.source_artifact ?? "—"} -