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/.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..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: @@ -61,6 +65,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 +179,47 @@ 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 (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/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..cd866a3 --- /dev/null +++ b/corpus/pcs/claims/labtrust-qc-release-claim-001/read_model.json @@ -0,0 +1,238 @@ +{ + "artifact_hashes": [ + { + "algorithm": "sha256", + "digest": "sha256:claim-artifact-001", + "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", + "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:runtime-receipt-001", + "name": "signature_or_digest", + "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" + }, + { + "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": { + "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", + "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": { + "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", + "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." + ], + "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" + } + ], + "id": "verification-result-labtrust-qc-release", + "overall_outcome": "pass", + "signature_or_digest": "sha256:verification-result-001", + "source_commit": "abc123def456", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "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", + "just pcs-validate-bundle BUNDLE=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/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..2f7f559 --- /dev/null +++ b/docs/pcs-labtrust-import.md @@ -0,0 +1,138 @@ +# 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). 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: + +| 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 + +```bash +# 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 (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 (`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 | 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 +# 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..d16f4ec --- /dev/null +++ b/docs/pcs-rendering-contract.md @@ -0,0 +1,89 @@ +# 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` (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`) | + +## 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`. + +## 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: + +- 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..0581982 100644 --- a/pipeline/pyproject.toml +++ b/pipeline/pyproject.toml @@ -9,12 +9,18 @@ dependencies = [ "typer>=0.12", "rich>=13.7", "networkx>=3.3", - "httpx>=0.27", - "python-dotenv>=1.0" + "httpx>=0.28.1", + "python-dotenv>=1.0", ] [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" @@ -26,5 +32,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..4177bc2 --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_import/artifact_normalizer.py @@ -0,0 +1,378 @@ +"""Normalize PCS nested artifacts into a portal read model.""" + +from __future__ import annotations + +import json +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." +) + +_CHECK_OUTCOME_MAP = { + "passed": "pass", + "failed": "fail", + "skipped": "skip", + "warning": "warn", + "pass": "pass", + "fail": "fail", + "skip": "skip", + "warn": "warn", +} + + +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) + 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 _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(): + rows.append( + { + "name": key, + "digest": val.strip(), + "algorithm": "sha256", + "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 + + +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_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 { + "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 ""), + "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_raw = _get_claim(scb) + claim_id = _artifact_id(claim_raw) + + 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 = _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: 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)) + + 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 []) + 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 = _infer_guarantee_types( + 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, + "claim": { + "id": claim_id, + "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_raw.get(k) for k in ("producer", "producer_version", "created_at")}, + }, + "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, + "limitations": limitations, + "limitation_notice": LIMITATION_NOTICE, + "bundle_signature_or_digest": str( + bundle.get("signature_or_digest") or bundle.get("bundle_digest") or "" + ), + } 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/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..6a7c842 --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_import/science_claim_bundle_importer.py @@ -0,0 +1,122 @@ +"""Import signed LabTrust ScienceClaimBundle artifacts.""" + +from __future__ import annotations + +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, + verification_status_label, +) + + +@dataclass +class ImportResult: + claim_id: str + 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: + 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( + 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: + 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", + ) + 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, + ) + + +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..42f18da --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_validate/__init__.py @@ -0,0 +1,31 @@ +"""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, + validate_signed_bundle, + verification_status_label, +) + +__all__ = [ + "BundleValidationError", + "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 new file mode 100644 index 0000000..649ff20 --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_validate/pcs_core_hook.py @@ -0,0 +1,32 @@ +"""Validation via pcs-core (canonical PCS authority).""" + +from __future__ import annotations + +from typing import Any + + +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.""" + if not pcs_core_available(): + return [] + + 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/stale_checker.py b/pipeline/src/sm_pipeline/pcs_validate/stale_checker.py new file mode 100644 index 0000000..2bc571d --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_validate/stale_checker.py @@ -0,0 +1,49 @@ +"""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", + "claim_artifact", + "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) + + 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) + + 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..f39d8cc --- /dev/null +++ b/pipeline/src/sm_pipeline/pcs_validate/validator.py @@ -0,0 +1,165 @@ +"""Validate signed PCS bundles: pcs-core (canonical) + legacy mirrors.""" + +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 + +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, +) + + +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() + 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): + 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 = 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], + *, + repo_root: Path | None = None, + strict: bool = True, +) -> list[str]: + """ + Validate a signed science claim bundle. + + 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 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)) + + 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 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: + 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 new file mode 100644 index 0000000..00a7f7e --- /dev/null +++ b/portal/.generated/pcs-export.json @@ -0,0 +1,540 @@ +{ + "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": [ + { + "algorithm": "sha256", + "digest": "sha256:claim-artifact-001", + "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", + "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:runtime-receipt-001", + "name": "signature_or_digest", + "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" + }, + { + "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": { + "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", + "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": { + "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", + "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." + ], + "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" + } + ], + "id": "verification-result-labtrust-qc-release", + "overall_outcome": "pass", + "signature_or_digest": "sha256:verification-result-001", + "source_commit": "abc123def456", + "source_repo": "https://github.com/SentinelOps-CI/provability-fabric", + "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", + "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..c3c885d --- /dev/null +++ b/portal/components/pcs/ArtifactHashTable.tsx @@ -0,0 +1,64 @@ +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, 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

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

    No hashes recorded.

    + ) : ( +
    + + + + + + + + + {rows.map((row) => ( + + + + + ))} + +
    ArtifactDigest
    {row.name} + {row.digest} +
    +
    + )} +
    + ); +} 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/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/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..142ed1b --- /dev/null +++ b/portal/components/pcs/PcsClaimPage.tsx @@ -0,0 +1,52 @@ +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"; +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} +

    +
    + + + + + + {model.evidence_bundle?.id ? ( + + ) : null} + + + + + +
    + ); +} 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..9fc3f94 --- /dev/null +++ b/portal/components/pcs/RuntimeReceiptView.tsx @@ -0,0 +1,35 @@ +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.trace_hash != null && ( +

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

    + )} + {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..3f384ae --- /dev/null +++ b/portal/components/pcs/VerificationResultView.tsx @@ -0,0 +1,91 @@ +import type { PcsVerificationResult } from "@/lib/pcsTypes"; + +interface VerificationResultViewProps { + result: PcsVerificationResult | null | undefined; +} + +function MetaRow({ label, value }: { label: string; value: string }) { + if (!value) return null; + const testId = `pcs-vr-${label.toLowerCase().replace(/\s+/g, "-")}`; + return ( +

    + {label}:{" "} + + {value} + +

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

    Verification Result

    +

    + 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

    +

    Provability Fabric VerificationResult.v0

    + +
    + +

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

    + + + + + +
    + +

    checks

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

      {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..3fc8561 --- /dev/null +++ b/portal/lib/pcsTypes.ts @@ -0,0 +1,91 @@ +/** 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 | 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; + source_commit?: string; + checks?: PcsVerificationCheck[]; +}; + +export type PcsHashRow = { + name: string; + digest: string; + algorithm?: string; + 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; + claim: PcsClaimSection; + 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[]; + limitations: string[]; + limitation_notice: string; + bundle_signature_or_digest: string; +}; + +export type PcsPortalExport = { + schema_version: string; + claim_ids: string[]; + claims: Record; +}; 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 5866265..c2866b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,4 +12,5 @@ line-length = 100 target-version = "py311" [tool.pytest.ini_options] -testpaths = ["pipeline/tests", "kernels/adsorption/tests"] +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/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/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 new file mode 100644 index 0000000..787a0ee --- /dev/null +++ b/schemas/pcs/science_claim_bundle.schema.json @@ -0,0 +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 (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 new file mode 100644 index 0000000..2163bdc --- /dev/null +++ b/schemas/pcs/signed_science_claim_bundle.schema.json @@ -0,0 +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 (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 new file mode 100644 index 0000000..b5df207 --- /dev/null +++ b/schemas/pcs/verification_result.schema.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://scientific-memory.org/schemas/pcs/verification_result.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 new file mode 100644 index 0000000..ff00ab5 --- /dev/null +++ b/tests/pcs/conftest.py @@ -0,0 +1,38 @@ +"""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/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_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/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_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 new file mode 100644 index 0000000..de78ab0 --- /dev/null +++ b/tests/pcs/fixtures/valid_signed_pcs_core_bundle.json @@ -0,0 +1,161 @@ +{ + "schema_version": "SignedScienceClaimBundle.v0", + "signed_bundle_id": "signed-scb-qc-release-v0.1", + "science_claim_bundle": { + "bundle_id": "scb-qc-release-v0.1", + "schema_version": "v0", + "claim_artifact": { + "artifact_id": "claim-qc-release-v0.1", + "artifact_type": "ClaimArtifact.v0", + "schema_version": "v0", + "claim_text": "The qc-release simulation run satisfies the hospital lab QC release temporal policy under stated assumptions.", + "claim_kind": "temporal_claim", + "status": "CertificateChecked", + "assumption_set_ref": "as-labtrust-qc-v0.1", + "source_span_refs": ["span-qc-release-spec-1"], + "formal_statement": "G (release_ready -> 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", + "run_outcome": "passed", + "final_reason_code": "ok", + "released": true, + "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" + }, + "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/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/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_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_pcs_render.py b/tests/pcs/test_pcs_render.py new file mode 100644 index 0000000..c5292a9 --- /dev/null +++ b/tests/pcs/test_pcs_render.py @@ -0,0 +1,95 @@ +"""PCS portal read-model / rendering contract tests.""" + +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 + +from schema_fixtures import copy_pcs_schemas + +FIXTURES = Path(__file__).resolve().parent / "fixtures" +REPO_ROOT = Path(__file__).resolve().parents[2] + +REQUIRED_READ_MODEL_KEYS = ( + "claim", + "assumption_set", + "runtime_receipt", + "trace_certificate", + "evidence_bundle", + "verification_result", + "artifact_hashes", + "canonical_digests", + "source_repositories", + "reproduce_commands", + "verify_commands", + "limitations", + "limitation_notice", +) + +CANONICAL_DIGEST_KEYS = ( + "claim_artifact", + "runtime_receipt", + "trace_certificate", + "evidence_bundle", + "signed_bundle", +) + + +def test_render_claim_includes_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_render_claim_includes_limitation_notice() -> 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_render_claim_displays_source_repo_and_source_commit() -> 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_render_claim_displays_artifact_hashes() -> None: + bundle = json.loads( + (FIXTURES / "valid_signed_pcs_core_bundle.json").read_text(encoding="utf-8") + ) + read_model = normalize_signed_bundle(bundle) + 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: + copy_pcs_schemas(root) 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"