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..a142e21 100644
--- a/pipeline/pyproject.toml
+++ b/pipeline/pyproject.toml
@@ -10,11 +10,17 @@ dependencies = [
"rich>=13.7",
"networkx>=3.3",
"httpx>=0.27",
- "python-dotenv>=1.0"
+ "python-dotenv>=1.2.2",
]
[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.
+
+ ) : (
+
+ )}
+
+ );
+}
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.
+ ) : (
+
+
+
+
+ | Artifact |
+ Digest |
+
+
+
+ {rows.map((row) => (
+
+ | {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 (
+
+
+
+
+
+
+
+ {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.
+ ) : (
+
+ )}
+
+ );
+}
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
+
+
+ );
+}
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"