From 9a62cf67f44a9e8fec974de7c86069029927557c Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:06 -0700 Subject: [PATCH 01/37] chore(release): bump package version to 0.8.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4647cfd..37363ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scope-protocol" -version = "0.8.0" +version = "0.8.1" description = "Scoped Scientific Authorization Protocol for AI-shaped science" readme = "README.md" license = { text = "MIT" } From f5e464838a3aac7b62e8dcb0aebbed9f62aefeb0 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:06 -0700 Subject: [PATCH 02/37] chore(release): align scope._version with 0.8.1 --- scope/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scope/_version.py b/scope/_version.py index d6977ce..e5ac1c0 100644 --- a/scope/_version.py +++ b/scope/_version.py @@ -1,3 +1,3 @@ """Package version (single source of truth).""" -__version__ = "0.8.0" +__version__ = "0.8.1" From 4a00a7ef4261643d632bf4f50d4e0c02c0659b0e Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:06 -0700 Subject: [PATCH 03/37] feat(akta): bump AKTA review contract to scope-akta-review-v0.8.1 --- scope/integration_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scope/integration_versions.py b/scope/integration_versions.py index d27e61e..78fc19c 100644 --- a/scope/integration_versions.py +++ b/scope/integration_versions.py @@ -5,4 +5,4 @@ PF_CORE_VERSION = "pf-core-v0.5" PCS_MANIFEST_VERSION = "pcs-v0.5" SCOPE_CORE_VERSION = "scope-core-v0.8" -AKTA_REVIEW_CONTRACT_VERSION = "scope-akta-review-v0.8" +AKTA_REVIEW_CONTRACT_VERSION = "scope-akta-review-v0.8.1" From 49ce348ae8485284e6157fc2718294dc8502f5ff Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:07 -0700 Subject: [PATCH 04/37] test: expect packet_version 0.8.1 in golden and pilot packets --- tests/test_akta_golden.py | 2 +- tests/test_institutional_pilot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_akta_golden.py b/tests/test_akta_golden.py index 88b90e4..7824816 100644 --- a/tests/test_akta_golden.py +++ b/tests/test_akta_golden.py @@ -35,4 +35,4 @@ def test_nested_akta_record_golden_mapping(): assert constraints["allowed_next_steps"] == golden["allowed_next_steps"] assert req["requested_scope"] == golden["inferred_requested_scope"] assert req["scope_inference_source"] == golden["scope_inference_source"] - assert packet["packet_version"] == "0.8.0" + assert packet["packet_version"] == "0.8.1" diff --git a/tests/test_institutional_pilot.py b/tests/test_institutional_pilot.py index 7241258..de92219 100644 --- a/tests/test_institutional_pilot.py +++ b/tests/test_institutional_pilot.py @@ -15,7 +15,7 @@ def test_institutional_pilot_packet_regenerates(): trigger = json.loads((PILOT / "review_trigger.json").read_text(encoding="utf-8")) packet = engine.create_packet(record, trigger) assert packet["review_request"]["scientific_action_type"] == "A5_protocol_modification" - assert packet["packet_version"] == "0.8.0" + assert packet["packet_version"] == "0.8.1" fixture = json.loads((PILOT / "scope_packet.json").read_text(encoding="utf-8")) for key in ( "packet_version", From 56b4c69b42a64922355caf34448763fbeb6fc472 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:07 -0700 Subject: [PATCH 05/37] test: expect scope-akta-review-v0.8.1 adapter contract in summaries --- tests/test_akta_review_command.py | 2 +- tests/test_akta_review_session_mode.py | 2 +- tests/test_rest_api.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_akta_review_command.py b/tests/test_akta_review_command.py index b41e2d0..ee5a1d5 100644 --- a/tests/test_akta_review_command.py +++ b/tests/test_akta_review_command.py @@ -54,7 +54,7 @@ def test_akta_review_command_happy_path(tmp_path): assert summary["decision_path"].endswith("scope_decision.json") assert summary["grant_path"].endswith("scope_grant.json") assert summary["approved_scope"] == "protocol_draft" - assert summary["adapter_contract_version"] == "scope-akta-review-v0.8" + assert summary["adapter_contract_version"] == "scope-akta-review-v0.8.1" assert summary["identity_assurance_level"] == "IAL0" assert "signing_assurance_level" in summary assert isinstance(summary["blocked_tools"], list) diff --git a/tests/test_akta_review_session_mode.py b/tests/test_akta_review_session_mode.py index 391ce8a..3ea53a0 100644 --- a/tests/test_akta_review_session_mode.py +++ b/tests/test_akta_review_session_mode.py @@ -74,7 +74,7 @@ def test_multi_role_with_session_creates_session(tmp_path: Path) -> None: on_disk = json.loads((out_dir / "summary.json").read_text(encoding="utf-8")) validate_artifact(on_disk, "scope_akta_review_session_summary.schema.json") - assert on_disk["adapter_contract_version"] == "scope-akta-review-v0.8" + assert on_disk["adapter_contract_version"] == "scope-akta-review-v0.8.1" def test_akta_review_cli_session_flag(tmp_path: Path) -> None: diff --git a/tests/test_rest_api.py b/tests/test_rest_api.py index 00d14d9..aa0973a 100644 --- a/tests/test_rest_api.py +++ b/tests/test_rest_api.py @@ -406,7 +406,7 @@ def test_akta_review_rest_session_mode(client, tmp_path): summary = resp.json() assert summary["status"] == "session_required" assert summary["session_id"].startswith("SCOPE-SESS-") - assert summary["adapter_contract_version"] == "scope-akta-review-v0.8" + assert summary["adapter_contract_version"] == "scope-akta-review-v0.8.1" validate_artifact(summary, "scope_akta_review_session_summary.schema.json") assert (out_dir / "scope_review_packet.json").exists() assert not (out_dir / "scope_grant.json").exists() From 038eb179ad19cece0b4bc68d26d990512c6fb443 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:08 -0700 Subject: [PATCH 06/37] feat(schema): restrict completed AKTA summary to status completed --- schemas/scope_akta_review_summary.schema.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/schemas/scope_akta_review_summary.schema.json b/schemas/scope_akta_review_summary.schema.json index 8e66413..057a7c9 100644 --- a/schemas/scope_akta_review_summary.schema.json +++ b/schemas/scope_akta_review_summary.schema.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://scope.dev/schemas/scope_akta_review_summary.schema.json", - "title": "SCOPE AKTA Review Summary", + "title": "SCOPE AKTA Review Summary (completed)", + "description": "Adapter summary for completed AKTA reviews only (status=completed). Session summaries use scope_akta_review_session_summary.schema.json.", "type": "object", "required": [ + "status", "packet_path", "decision_path", "grant_path", @@ -15,7 +17,7 @@ "production_mode" ], "properties": { - "status": { "type": "string" }, + "status": { "const": "completed" }, "packet_path": { "type": "string" }, "decision_path": { "type": "string" }, "grant_path": { "type": "string" }, @@ -23,7 +25,7 @@ "decision_id": { "type": "string" }, "grant_id": { "type": "string" }, "approved_scope": { "type": "string" }, - "requested_scope": { "type": "string" }, + "requested_scope": { "type": ["string", "null"] }, "allowed_tools": { "type": "array", "items": { "type": "string" } @@ -49,5 +51,12 @@ }, "queue_id": { "type": "string" } }, + "not": { + "anyOf": [ + { "required": ["session_id"] }, + { "required": ["required_roles"] }, + { "required": ["message"] } + ] + }, "additionalProperties": true } From a7f16dbf52adf5f3d82e209d641ae0b8363571fd Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:08 -0700 Subject: [PATCH 07/37] feat(schema): restrict session AKTA summary to status session_required --- ...pe_akta_review_session_summary.schema.json | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/schemas/scope_akta_review_session_summary.schema.json b/schemas/scope_akta_review_session_summary.schema.json index d295bea..cd3f1ea 100644 --- a/schemas/scope_akta_review_session_summary.schema.json +++ b/schemas/scope_akta_review_session_summary.schema.json @@ -2,6 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://scope.dev/schemas/scope_akta_review_session_summary.schema.json", "title": "SCOPE AKTA Review Session Summary", + "description": "Adapter summary when multi-role review requires a session (status=session_required). Completed reviews use scope_akta_review_summary.schema.json.", "type": "object", "required": [ "status", @@ -18,7 +19,8 @@ "session_id": { "type": "string" }, "required_roles": { "type": "array", - "items": { "type": "string" } + "items": { "type": "string" }, + "minItems": 1 }, "message": { "type": "string" }, "requested_scope": { "type": ["string", "null"] }, @@ -26,5 +28,21 @@ "adapter_contract_version": { "type": "string" }, "production_mode": { "type": "boolean" } }, + "not": { + "anyOf": [ + { "required": ["decision_path"] }, + { "required": ["grant_path"] }, + { "required": ["decision_id"] }, + { "required": ["grant_id"] }, + { "required": ["approved_scope"] }, + { "required": ["identity_assurance_level"] }, + { "required": ["signing_assurance_level"] }, + { "required": ["allowed_tools"] }, + { "required": ["blocked_tools"] }, + { "required": ["decision_type"] }, + { "required": ["scope_trust_root_hash"] }, + { "required": ["queue_id"] } + ] + }, "additionalProperties": true } From 13f08dc0309a5d58e55477d545cd0d4f6dadb34c Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:10 -0700 Subject: [PATCH 08/37] feat(akta): centralize resolve_reviewer_id for all entry points --- scope/akta_review.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/scope/akta_review.py b/scope/akta_review.py index 90a7b44..40ca792 100644 --- a/scope/akta_review.py +++ b/scope/akta_review.py @@ -103,18 +103,18 @@ def _resolve_signer( return None -def _resolve_reviewer_id( +def resolve_reviewer_id( reviewer_data: dict[str, Any], reviewer_id: str | None, ) -> str: - """Validate optional CLI reviewer_id against reviewer artifact.""" + """Resolve and validate reviewer identity for all AKTA review entry points.""" artifact_id = reviewer_data.get("reviewer_id") if reviewer_id is not None: if not artifact_id: raise ScopeValidationError("Reviewer artifact missing reviewer_id") if str(reviewer_id) != str(artifact_id): raise ScopeValidationError( - f"--reviewer-id {reviewer_id!r} does not match reviewer artifact " + f"reviewer_id {reviewer_id!r} does not match reviewer artifact " f"reviewer_id {artifact_id!r}" ) return str(reviewer_id) @@ -123,6 +123,19 @@ def _resolve_reviewer_id( return str(artifact_id) +def validate_summary_artifact(summary: dict[str, Any]) -> None: + """Validate summary against the schema for its status branch.""" + status = summary.get("status") + if status == "completed": + validate_artifact(summary, "scope_akta_review_summary.schema.json") + elif status == "session_required": + validate_artifact(summary, "scope_akta_review_session_summary.schema.json") + else: + raise ScopeValidationError( + f"summary.status must be 'completed' or 'session_required', got {status!r}" + ) + + def _write_session_summary( out: Path, *, @@ -144,7 +157,7 @@ def _write_session_summary( "adapter_contract_version": AKTA_REVIEW_CONTRACT_VERSION, "production_mode": is_production_mode(), } - validate_artifact(summary, "scope_akta_review_session_summary.schema.json") + validate_summary_artifact(summary) with summary_path.open("w", encoding="utf-8") as fh: json.dump(summary, fh, indent=2, sort_keys=True) fh.write("\n") @@ -179,7 +192,7 @@ def run_akta_review( with Path(reviewer).open(encoding="utf-8") as fh: reviewer_data = json.load(fh) - resolved_reviewer_id = _resolve_reviewer_id(reviewer_data, reviewer_id) + resolved_reviewer_id = resolve_reviewer_id(reviewer_data, reviewer_id) if session_mode: session = engine.create_review_session(packet) @@ -283,7 +296,7 @@ def run_akta_review( } if queue_entry is not None: summary["queue_id"] = queue_entry.queue_id - validate_artifact(summary, "scope_akta_review_summary.schema.json") + validate_summary_artifact(summary) with summary_path.open("w", encoding="utf-8") as fh: json.dump(summary, fh, indent=2, sort_keys=True) fh.write("\n") From c66243ba24dc03f9770b711e88b7d9437f0c4c52 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:11 -0700 Subject: [PATCH 09/37] test(akta): assert completed status on summary contract fixtures --- tests/test_akta_review_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_akta_review_contract.py b/tests/test_akta_review_contract.py index 2ff00df..c9a5a5d 100644 --- a/tests/test_akta_review_contract.py +++ b/tests/test_akta_review_contract.py @@ -18,6 +18,7 @@ def test_summary_schema_fields() -> None: summary = { + "status": "completed", "packet_path": "/out/scope_review_packet.json", "decision_path": "/out/scope_decision.json", "grant_path": "/out/scope_grant.json", @@ -58,6 +59,7 @@ def test_akta_review_summary_contract(tmp_path: Path) -> None: assert result.exit_code == 0, result.output summary = json.loads((out_dir / "summary.json").read_text(encoding="utf-8")) validate_artifact(summary, "scope_akta_review_summary.schema.json") + assert summary["status"] == "completed" assert summary["adapter_contract_version"] == AKTA_REVIEW_CONTRACT_VERSION assert summary["identity_assurance_level"] == "IAL0" assert summary["requested_scope"] == "protocol_draft" From a83d78ffe05ef52810bd39087487812bfbda1ee7 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:11 -0700 Subject: [PATCH 10/37] test(akta): cover split summary schemas and validate_summary_artifact --- tests/test_akta_review_session_summary.py | 114 ++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 tests/test_akta_review_session_summary.py diff --git a/tests/test_akta_review_session_summary.py b/tests/test_akta_review_session_summary.py new file mode 100644 index 0000000..aeb2706 --- /dev/null +++ b/tests/test_akta_review_session_summary.py @@ -0,0 +1,114 @@ +"""Tests for split AKTA review summary schemas (SCOPE-1).""" + +from __future__ import annotations + +import jsonschema +import pytest + +from scope.akta_review import validate_summary_artifact +from scope.errors import ScopeValidationError +from scope.integration_versions import AKTA_REVIEW_CONTRACT_VERSION +from scope.schema_util import validate_artifact + + +def _completed_summary(**overrides: object) -> dict: + base: dict = { + "status": "completed", + "packet_path": "/out/scope_review_packet.json", + "decision_path": "/out/scope_decision.json", + "grant_path": "/out/scope_grant.json", + "approved_scope": "protocol_draft", + "requested_scope": "protocol_draft", + "adapter_contract_version": AKTA_REVIEW_CONTRACT_VERSION, + "identity_assurance_level": "IAL0", + "signing_assurance_level": "SAL0", + "production_mode": False, + } + base.update(overrides) + return base + + +def _session_summary(**overrides: object) -> dict: + base: dict = { + "status": "session_required", + "packet_id": "SCOPE-PKT-TEST01", + "session_id": "SCOPE-SESS-TEST01", + "required_roles": ["domain_scientist", "protocol_owner"], + "message": "Multi-role review session created; submit votes before grant issue.", + "adapter_contract_version": AKTA_REVIEW_CONTRACT_VERSION, + "production_mode": False, + } + base.update(overrides) + return base + + +def test_completed_summary_schema_accepts_valid_shape() -> None: + validate_artifact(_completed_summary(), "scope_akta_review_summary.schema.json") + validate_summary_artifact(_completed_summary()) + + +def test_session_summary_schema_accepts_valid_shape() -> None: + validate_artifact( + _session_summary(), + "scope_akta_review_session_summary.schema.json", + ) + validate_summary_artifact(_session_summary()) + + +def test_completed_schema_rejects_session_fields() -> None: + with pytest.raises(jsonschema.ValidationError): + validate_artifact( + _completed_summary(session_id="SCOPE-SESS-BAD"), + "scope_akta_review_summary.schema.json", + ) + + +def test_completed_schema_rejects_session_required_status() -> None: + with pytest.raises(jsonschema.ValidationError): + validate_artifact( + _session_summary(), + "scope_akta_review_summary.schema.json", + ) + + +def test_session_schema_rejects_completed_fields() -> None: + with pytest.raises(jsonschema.ValidationError): + validate_artifact( + _session_summary( + decision_path="/out/scope_decision.json", + grant_path="/out/scope_grant.json", + ), + "scope_akta_review_session_summary.schema.json", + ) + + +def test_session_schema_rejects_completed_status() -> None: + with pytest.raises(jsonschema.ValidationError): + validate_artifact( + _completed_summary(), + "scope_akta_review_session_summary.schema.json", + ) + + +def test_validate_summary_artifact_rejects_unknown_status() -> None: + with pytest.raises(ScopeValidationError, match="summary.status must be"): + validate_summary_artifact({"status": "pending"}) + + +def test_validate_summary_artifact_rejects_cross_branch_fields() -> None: + with pytest.raises(jsonschema.ValidationError): + validate_summary_artifact( + _session_summary( + decision_path="/out/scope_decision.json", + grant_path="/out/scope_grant.json", + ) + ) + with pytest.raises(jsonschema.ValidationError): + validate_summary_artifact(_completed_summary(session_id="SCOPE-SESS-BAD")) + + +def test_completed_schema_requires_status_completed() -> None: + summary = _completed_summary() + del summary["status"] + with pytest.raises(jsonschema.ValidationError): + validate_artifact(summary, "scope_akta_review_summary.schema.json") From 1904d1cec4e5b317144fd0ef10b16903d00b3801 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:11 -0700 Subject: [PATCH 11/37] chore(pilot): align single_reviewer_protocol_draft summary with contract --- examples/pilot/single_reviewer_protocol_draft/summary.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pilot/single_reviewer_protocol_draft/summary.json b/examples/pilot/single_reviewer_protocol_draft/summary.json index 2a1f20f..c98a2ef 100644 --- a/examples/pilot/single_reviewer_protocol_draft/summary.json +++ b/examples/pilot/single_reviewer_protocol_draft/summary.json @@ -1,5 +1,5 @@ { - "adapter_contract_version": "scope-akta-review-v0.8", + "adapter_contract_version": "scope-akta-review-v0.8.1", "allowed_tools": [ "protocol_editor.draft_change" ], From bd2316347dffc997deae5f566ed00fa73199e1f9 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:12 -0700 Subject: [PATCH 12/37] chore(pilot): align registry_signed_decision summary with contract --- examples/pilot/registry_signed_decision/summary.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pilot/registry_signed_decision/summary.json b/examples/pilot/registry_signed_decision/summary.json index 6ec1ef0..071f9e0 100644 --- a/examples/pilot/registry_signed_decision/summary.json +++ b/examples/pilot/registry_signed_decision/summary.json @@ -1,5 +1,5 @@ { - "adapter_contract_version": "scope-akta-review-v0.8", + "adapter_contract_version": "scope-akta-review-v0.8.1", "allowed_tools": [ "protocol_editor.draft_change" ], From b6080209f2a1f4bbdd0badb6a9a94ccc1752e2b1 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:12 -0700 Subject: [PATCH 13/37] chore(pilot): align multi_role_genomics_review summary with contract --- examples/pilot/multi_role_genomics_review/summary.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pilot/multi_role_genomics_review/summary.json b/examples/pilot/multi_role_genomics_review/summary.json index 643da03..62d427e 100644 --- a/examples/pilot/multi_role_genomics_review/summary.json +++ b/examples/pilot/multi_role_genomics_review/summary.json @@ -1,5 +1,5 @@ { - "adapter_contract_version": "scope-akta-review-v0.8", + "adapter_contract_version": "scope-akta-review-v0.8.1", "message": "Multi-role review session created; submit votes before grant issue.", "packet_id": "SCOPE-PKT-42A16F", "packet_path": "examples/pilot/multi_role_genomics_review/scope_review_packet.json", From 1b3675c22593ba5cd13c14c7558b22617eda4ef6 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:12 -0700 Subject: [PATCH 14/37] feat(schema): require session grant provenance when IAL markers present --- schemas/scope_grant.schema.json | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/schemas/scope_grant.schema.json b/schemas/scope_grant.schema.json index c1ec9e8..b332c1c 100644 --- a/schemas/scope_grant.schema.json +++ b/schemas/scope_grant.schema.json @@ -103,6 +103,7 @@ }, "contributing_identity_assurance_levels": { "type": "array", + "minItems": 1, "items": { "type": "object", "required": ["decision_id", "reviewer_id", "identity_assurance_level"], @@ -121,6 +122,7 @@ }, "contributing_authority_checks": { "type": "array", + "minItems": 1, "items": { "type": "object", "required": ["decision_id", "reviewer_id", "authority_checks"], @@ -152,5 +154,37 @@ }, "grant_hash": { "type": "string", "pattern": "^sha256:" } }, + "allOf": [ + { + "if": { + "properties": { + "provenance": { + "properties": { + "contributing_identity_assurance_levels": { + "type": "array", + "minItems": 1 + } + }, + "required": ["contributing_identity_assurance_levels"] + } + }, + "required": ["provenance"] + }, + "then": { + "properties": { + "provenance": { + "required": [ + "contributing_identity_assurance_levels", + "contributing_authority_checks", + "minimum_identity_assurance_level", + "minimum_signing_assurance_level", + "veto_roles_applied", + "quorum_policy_hash" + ] + } + } + } + } + ], "additionalProperties": true } From 178e97128317483e80c1cfb88ba66bd0b4c1bfe1 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:13 -0700 Subject: [PATCH 15/37] feat(grants): validate session grant provenance at runtime --- scope/session_provenance.py | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/scope/session_provenance.py b/scope/session_provenance.py index fbf8116..046052e 100644 --- a/scope/session_provenance.py +++ b/scope/session_provenance.py @@ -4,6 +4,7 @@ from typing import Any +from scope.errors import GrantValidationError from scope.hash import compute_hash from scope.identity_assurance import IAL0, IAL1, IAL2, IAL3, IAL4 from scope.review_session import ReviewSession @@ -12,6 +13,44 @@ IAL_RANK = {IAL0: 0, IAL1: 1, IAL2: 2, IAL3: 3, IAL4: 4} SAL_RANK = {SAL0: 0, SAL1: 1, SAL2: 2, SAL3: 3, SAL4: 4} +SESSION_PROVENANCE_FIELDS = ( + "contributing_identity_assurance_levels", + "contributing_authority_checks", + "minimum_identity_assurance_level", + "minimum_signing_assurance_level", + "veto_roles_applied", + "quorum_policy_hash", +) + + +def is_session_grant_provenance(provenance: dict[str, Any]) -> bool: + """True when provenance carries session aggregation markers.""" + levels = provenance.get("contributing_identity_assurance_levels") + return isinstance(levels, list) and len(levels) > 0 + + +def validate_session_grant_provenance(provenance: dict[str, Any]) -> None: + """Runtime check: session grants must include full provenance block.""" + if not is_session_grant_provenance(provenance): + return + missing = [field for field in SESSION_PROVENANCE_FIELDS if field not in provenance] + if missing: + raise GrantValidationError( + "Session grant provenance missing required fields: " + + ", ".join(sorted(missing)) + ) + if not provenance["contributing_identity_assurance_levels"]: + raise GrantValidationError( + "Session grant provenance requires non-empty " + "contributing_identity_assurance_levels" + ) + if not provenance["contributing_authority_checks"]: + raise GrantValidationError( + "Session grant provenance requires non-empty contributing_authority_checks" + ) + if not str(provenance["quorum_policy_hash"]).startswith("sha256:"): + raise GrantValidationError("Session grant provenance quorum_policy_hash invalid") + def _minimum_level(levels: list[str], rank: dict[str, int], default: str) -> str: if not levels: From 0430a670a07f139d91a44e75f622ed2b0cab8a72 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:13 -0700 Subject: [PATCH 16/37] feat(grants): wire session provenance checks in grant issuance --- scope/grants.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scope/grants.py b/scope/grants.py index 6f1ba1e..a4070c2 100644 --- a/scope/grants.py +++ b/scope/grants.py @@ -153,5 +153,9 @@ def check( return True, None, None def validate(self, grant: dict[str, Any]) -> None: + from scope.session_provenance import validate_session_grant_provenance + + provenance = grant.get("provenance") or {} + validate_session_grant_provenance(provenance) if self.schema: jsonschema.validate(instance=grant, schema=self.schema) From a9ebeee7d07d319c73475ab6227a8e7646058f5e Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:14 -0700 Subject: [PATCH 17/37] test(grants): require full session provenance on session grants --- .../test_session_grant_provenance_required.py | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tests/test_session_grant_provenance_required.py diff --git a/tests/test_session_grant_provenance_required.py b/tests/test_session_grant_provenance_required.py new file mode 100644 index 0000000..c7a2a6c --- /dev/null +++ b/tests/test_session_grant_provenance_required.py @@ -0,0 +1,158 @@ +"""Tests for conditional session grant provenance (SCOPE-2).""" + +from __future__ import annotations + +from pathlib import Path + +import jsonschema +import pytest + +from scope import ScopeEngine +from scope.errors import GrantValidationError +from scope.grants import GrantEngine +from scope.schema_util import validate_artifact +from scope.session_provenance import ( + SESSION_PROVENANCE_FIELDS, + validate_session_grant_provenance, +) + +ROOT = Path(__file__).resolve().parent.parent + + +def _a6_packet(engine: ScopeEngine) -> dict: + trigger = { + "akta_admissibility": "review_required", + "scientific_action_type": "A6_experimental_planning", + "requested_action": "plan_validation", + "requested_tool": "experiment_planner.create_validation_plan", + "requested_scope": "single_validation_plan", + "scientific_context": {"protocol_version": "protocol_v1"}, + } + record = {"record_id": "AKTA-A6-PROV-REQ", "scientific_action_type": "A6_experimental_planning"} + return engine.create_packet(record, trigger) + + +def test_single_reviewer_grant_without_session_fields_validates() -> None: + engine = ScopeEngine.from_policy_dir(ROOT / "policy") + packet = engine.create_packet( + {"record_id": "AKTA-SINGLE", "scientific_action_type": "A5_protocol_modification"}, + { + "akta_admissibility": "review_required", + "scientific_action_type": "A5_protocol_modification", + "requested_action": "draft_change", + "requested_tool": "protocol_editor.draft_change", + "requested_scope": "protocol_draft", + }, + ) + decision = engine.submit_decision( + packet, + {"reviewer_id": "po1", "role": "protocol_owner"}, + { + "type": "approve_narrower_scope", + "approved_scope": "protocol_draft", + "rationale": "ok", + }, + ) + grant = engine.issue_grant(packet, decision) + validate_artifact(grant, "scope_grant.schema.json") + validate_session_grant_provenance(grant["provenance"]) + + +def test_session_grant_missing_provenance_fields_fails_runtime() -> None: + engine = ScopeEngine.from_policy_dir(ROOT / "policy") + packet = _a6_packet(engine) + session = engine.create_review_session(packet) + d1 = engine.submit_session_decision( + session, + packet, + {"reviewer_id": "ds1", "role": "domain_scientist"}, + { + "type": "approve_narrower_scope", + "approved_scope": "single_validation_plan", + "rationale": "ok", + }, + ) + d2 = engine.submit_session_decision( + session, + packet, + {"reviewer_id": "po1", "role": "protocol_owner"}, + { + "type": "approve_narrower_scope", + "approved_scope": "single_validation_plan", + "rationale": "ok", + }, + ) + grant = engine.issue_grant_from_session(session, packet, [d1, d2]) + prov = dict(grant["provenance"]) + del prov["quorum_policy_hash"] + grant["provenance"] = prov + + grant_engine = GrantEngine(engine.policy, schema=engine._grant_engine.schema) + with pytest.raises(GrantValidationError, match="quorum_policy_hash"): + grant_engine.validate(grant) + + +def test_session_grant_partial_marker_fails_schema() -> None: + engine = ScopeEngine.from_policy_dir(ROOT / "policy") + packet = engine.create_packet( + {"record_id": "AKTA-PARTIAL", "scientific_action_type": "A5_protocol_modification"}, + { + "akta_admissibility": "review_required", + "scientific_action_type": "A5_protocol_modification", + "requested_action": "draft_change", + "requested_tool": "protocol_editor.draft_change", + "requested_scope": "protocol_draft", + }, + ) + decision = engine.submit_decision( + packet, + {"reviewer_id": "po1", "role": "protocol_owner"}, + { + "type": "approve_narrower_scope", + "approved_scope": "protocol_draft", + "rationale": "ok", + }, + ) + grant = engine.issue_grant(packet, decision) + grant["provenance"]["contributing_identity_assurance_levels"] = [ + { + "decision_id": decision["decision_id"], + "reviewer_id": "po1", + "identity_assurance_level": "IAL0", + } + ] + + grant_engine = GrantEngine(engine.policy, schema=engine._grant_engine.schema) + with pytest.raises((GrantValidationError, jsonschema.ValidationError)): + grant_engine.validate(grant) + + +def test_session_grant_includes_all_required_provenance_fields() -> None: + engine = ScopeEngine.from_policy_dir(ROOT / "policy") + packet = _a6_packet(engine) + session = engine.create_review_session(packet) + d1 = engine.submit_session_decision( + session, + packet, + {"reviewer_id": "ds1", "role": "domain_scientist"}, + { + "type": "approve_narrower_scope", + "approved_scope": "single_validation_plan", + "rationale": "ok", + }, + ) + d2 = engine.submit_session_decision( + session, + packet, + {"reviewer_id": "po1", "role": "protocol_owner"}, + { + "type": "approve_narrower_scope", + "approved_scope": "single_validation_plan", + "rationale": "ok", + }, + ) + grant = engine.issue_grant_from_session(session, packet, [d1, d2]) + prov = grant["provenance"] + for field in SESSION_PROVENANCE_FIELDS: + assert field in prov, f"missing {field}" + validate_artifact(grant, "scope_grant.schema.json") From 6ce0fd52ed079917aab3170cc24b4be03d34158f Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:14 -0700 Subject: [PATCH 18/37] test(grants): update session provenance aggregation expectations --- tests/test_session_grant_provenance.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_session_grant_provenance.py b/tests/test_session_grant_provenance.py index c93c9d4..97e2344 100644 --- a/tests/test_session_grant_provenance.py +++ b/tests/test_session_grant_provenance.py @@ -1,4 +1,4 @@ -"""Tests for session grant provenance aggregation (SCOPE-3).""" +"""Tests for session grant provenance aggregation (SCOPE-2).""" from __future__ import annotations @@ -7,7 +7,12 @@ from scope import ScopeEngine from scope.hash import compute_hash from scope.schema_util import validate_artifact -from scope.session_provenance import aggregate_session_grant_provenance +from scope.session_provenance import ( + SESSION_PROVENANCE_FIELDS, + aggregate_session_grant_provenance, + is_session_grant_provenance, + validate_session_grant_provenance, +) ROOT = Path(__file__).resolve().parent.parent @@ -72,16 +77,11 @@ def test_session_grant_includes_aggregated_provenance() -> None: assert prov["quorum_policy_hash"] == compute_hash(session.quorum_policy) assert prov["quorum_policy_hash"].startswith("sha256:") - session_fields = ( - "contributing_identity_assurance_levels", - "contributing_authority_checks", - "minimum_identity_assurance_level", - "minimum_signing_assurance_level", - "veto_roles_applied", - "quorum_policy_hash", - ) + session_fields = SESSION_PROVENANCE_FIELDS for field in session_fields: assert field in prov, f"missing session provenance field: {field}" + assert is_session_grant_provenance(prov) + validate_session_grant_provenance(prov) validate_artifact(grant, "scope_grant.schema.json") From 4105dacc39806cffda16d2150bac7fadafc74fdf Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:14 -0700 Subject: [PATCH 19/37] test(akta): cover centralized resolve_reviewer_id helper --- tests/test_akta_review_reviewer_id_binding.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_akta_review_reviewer_id_binding.py b/tests/test_akta_review_reviewer_id_binding.py index c316550..5302294 100644 --- a/tests/test_akta_review_reviewer_id_binding.py +++ b/tests/test_akta_review_reviewer_id_binding.py @@ -1,4 +1,4 @@ -"""Tests for --reviewer-id binding in akta review (SCOPE-2).""" +"""Tests for --reviewer-id binding in akta review (SCOPE-3).""" from __future__ import annotations @@ -11,7 +11,7 @@ from click.testing import CliRunner from scope import ScopeEngine -from scope.akta_review import run_akta_review +from scope.akta_review import resolve_reviewer_id, run_akta_review from scope.cli import main from scope.errors import ScopeValidationError from scope.key_registry import register_reviewer_key @@ -35,6 +35,14 @@ def _policy_with_registry_signer(tmp_path: Path) -> tuple[Path, Path]: return policy_dir, key +def test_resolve_reviewer_id_centralized_helper() -> None: + reviewer = {"reviewer_id": "reviewer_001", "role": "protocol_owner"} + assert resolve_reviewer_id(reviewer, None) == "reviewer_001" + assert resolve_reviewer_id(reviewer, "reviewer_001") == "reviewer_001" + with pytest.raises(ScopeValidationError, match="does not match"): + resolve_reviewer_id(reviewer, "wrong_id") + + def test_reviewer_id_mismatch_fails(tmp_path: Path) -> None: engine = ScopeEngine.from_policy_dir(ROOT / "policy") with pytest.raises(ScopeValidationError, match="does not match reviewer artifact"): From eae6e3f8f99cd27fb99fd731238726f54f21bfa8 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:15 -0700 Subject: [PATCH 20/37] feat(pilot): add offline pilot fixture verification script --- scripts/verify_pilot_fixtures.py | 211 +++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 scripts/verify_pilot_fixtures.py diff --git a/scripts/verify_pilot_fixtures.py b/scripts/verify_pilot_fixtures.py new file mode 100644 index 0000000..92b2f43 --- /dev/null +++ b/scripts/verify_pilot_fixtures.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +"""Offline verification of examples/pilot fixture pack (SCOPE-4).""" + +from __future__ import annotations + +import hashlib +import json +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parent.parent +PILOT = ROOT / "examples" / "pilot" + +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + + +def file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as fh: + for chunk in iter(lambda: fh.read(65536), b""): + digest.update(chunk) + return f"sha256:{digest.hexdigest()}" + + +def _load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def _verify_checksums( + scenario_dir: Path, + expected: dict[str, Any], + errors: list[str], +) -> None: + for rel_path, expected_hash in (expected.get("checksums") or {}).items(): + artifact = scenario_dir / rel_path + if not artifact.is_file(): + errors.append(f"{scenario_dir.name}: missing checksum target {rel_path}") + continue + actual = file_sha256(artifact) + if actual != expected_hash: + errors.append( + f"{scenario_dir.name}: checksum mismatch for {rel_path} " + f"(expected {expected_hash}, got {actual})" + ) + + +def _verify_summary(scenario_dir: Path, expected: dict[str, Any], errors: list[str]) -> None: + summary_spec = expected.get("summary") + if not summary_spec: + return + summary_path = scenario_dir / "summary.json" + if not summary_path.is_file(): + errors.append(f"{scenario_dir.name}: summary.json required but missing") + return + summary = _load_json(summary_path) + status = summary_spec.get("status") + if status and summary.get("status") != status: + errors.append( + f"{scenario_dir.name}: summary.status expected {status!r}, " + f"got {summary.get('status')!r}" + ) + for key, value in summary_spec.items(): + if key == "status": + continue + if summary.get(key) != value: + errors.append( + f"{scenario_dir.name}: summary.{key} expected {value!r}, " + f"got {summary.get(key)!r}" + ) + from scope.akta_review import validate_summary_artifact + + try: + validate_summary_artifact(summary) + except Exception as exc: + errors.append(f"{scenario_dir.name}: summary schema validation failed: {exc}") + + +def _verify_quality_snippet( + scenario_dir: Path, + expected: dict[str, Any], + errors: list[str], +) -> None: + snippet_spec = expected.get("quality_report_snippet") + if not snippet_spec: + return + snippet_path = scenario_dir / "quality_report_snippet.json" + if not snippet_path.is_file(): + errors.append(f"{scenario_dir.name}: quality_report_snippet.json missing") + return + snippet = _load_json(snippet_path) + for key, value in snippet_spec.items(): + if snippet.get(key) != value: + errors.append( + f"{scenario_dir.name}: quality_report_snippet.{key} expected " + f"{value!r}, got {snippet.get(key)!r}" + ) + + +def _verify_queue_states( + scenario_dir: Path, + expected: dict[str, Any], + errors: list[str], +) -> None: + from scope.schema_util import validate_artifact + + for rel_path, queue_spec in (expected.get("queue_states") or {}).items(): + queue_path = scenario_dir / rel_path + if not queue_path.is_file(): + errors.append(f"{scenario_dir.name}: queue artifact missing {rel_path}") + continue + queue = _load_json(queue_path) + validate_artifact(queue, "scope_review_queue.schema.json") + for key, value in queue_spec.items(): + if queue.get(key) != value: + errors.append( + f"{scenario_dir.name}: {rel_path}.{key} expected {value!r}, " + f"got {queue.get(key)!r}" + ) + + +def _verify_artifacts( + scenario_dir: Path, + manifest: dict[str, Any], + expected: dict[str, Any], + errors: list[str], +) -> None: + from scope.schema_util import validate_artifact + + forbidden = set(expected.get("forbidden_files") or []) + for rel in forbidden: + if (scenario_dir / rel).exists(): + errors.append(f"{scenario_dir.name}: forbidden artifact present: {rel}") + + for entry in manifest.get("artifacts") or []: + rel_path = entry["path"] + artifact_path = scenario_dir / rel_path + if not artifact_path.is_file(): + errors.append(f"{scenario_dir.name}: missing artifact {rel_path}") + continue + schema = entry.get("schema") + if schema and rel_path.endswith(".json"): + try: + validate_artifact(_load_json(artifact_path), schema) + except Exception as exc: + errors.append( + f"{scenario_dir.name}: schema validation failed for {rel_path}: {exc}" + ) + + +def verify_scenario(scenario_dir: Path) -> list[str]: + errors: list[str] = [] + manifest_path = scenario_dir / "manifest.json" + expected_path = scenario_dir / "expected_verification.json" + if not manifest_path.is_file(): + return [f"{scenario_dir.name}: missing manifest.json"] + if not expected_path.is_file(): + return [f"{scenario_dir.name}: missing expected_verification.json"] + + manifest = _load_json(manifest_path) + expected = _load_json(expected_path) + + if manifest.get("scenario") != scenario_dir.name: + errors.append( + f"{scenario_dir.name}: manifest.scenario mismatch " + f"({manifest.get('scenario')!r})" + ) + + _verify_artifacts(scenario_dir, manifest, expected, errors) + _verify_checksums(scenario_dir, expected, errors) + _verify_summary(scenario_dir, expected, errors) + _verify_quality_snippet(scenario_dir, expected, errors) + _verify_queue_states(scenario_dir, expected, errors) + return errors + + +def main() -> int: + if not PILOT.is_dir(): + print(f"pilot directory not found: {PILOT}", file=sys.stderr) + return 1 + + scenario_dirs = sorted( + path for path in PILOT.iterdir() if path.is_dir() and (path / "manifest.json").exists() + ) + if not scenario_dirs: + print("no pilot scenarios with manifest.json found", file=sys.stderr) + return 1 + + all_errors: list[str] = [] + passed = 0 + for scenario_dir in scenario_dirs: + errors = verify_scenario(scenario_dir) + if errors: + all_errors.extend(errors) + else: + passed += 1 + print(f"OK {scenario_dir.name}") + + if all_errors: + print(f"\n{len(all_errors)} verification error(s):", file=sys.stderr) + for err in all_errors: + print(f" - {err}", file=sys.stderr) + return 1 + + print(f"\nAll {passed} pilot fixture(s) verified.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From a48ed974bd71a285a2aaf0214da8c2c33ba3290a Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:15 -0700 Subject: [PATCH 21/37] ci: run pilot fixture verifier on Windows CI --- scripts/ci.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci.ps1 b/scripts/ci.ps1 index 65b58a7..3fc27c3 100644 --- a/scripts/ci.ps1 +++ b/scripts/ci.ps1 @@ -40,4 +40,5 @@ Invoke-Pip install --editable ".[dev]" ruff check scope tests evals adapters mypy scope pytest -python evals/run_review_cases.py --extended \ No newline at end of file +python evals/run_review_cases.py --extended +python scripts/verify_pilot_fixtures.py \ No newline at end of file From 011a220c41ac21be0a7e322f1d5a963c5e65ada1 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:15 -0700 Subject: [PATCH 22/37] ci: run pilot fixture verifier on Unix CI --- scripts/ci.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci.sh b/scripts/ci.sh index 6d746a0..4917800 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -6,4 +6,5 @@ pip install -e ".[dev]" ruff check scope tests evals adapters mypy scope pytest -python evals/run_review_cases.py --extended \ No newline at end of file +python evals/run_review_cases.py --extended +python scripts/verify_pilot_fixtures.py \ No newline at end of file From 9f835646fd1b84da79c30cee6c5e83d4ccae8cad Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:16 -0700 Subject: [PATCH 23/37] chore(pilot): add verification manifest for single reviewer draft --- .../expected_verification.json | 18 +++++++++++++ .../manifest.json | 27 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 examples/pilot/single_reviewer_protocol_draft/expected_verification.json create mode 100644 examples/pilot/single_reviewer_protocol_draft/manifest.json diff --git a/examples/pilot/single_reviewer_protocol_draft/expected_verification.json b/examples/pilot/single_reviewer_protocol_draft/expected_verification.json new file mode 100644 index 0000000..7daba8d --- /dev/null +++ b/examples/pilot/single_reviewer_protocol_draft/expected_verification.json @@ -0,0 +1,18 @@ +{ + "checksums": { + "quality_report_snippet.json": "sha256:be1186b24dbd1dda1e9bb9610d221e282e990be17723f6fbea9cc4388b338308", + "scope_decision.json": "sha256:b8becf4dc97c19a6474e8d489271ab5e6928447130bc28d2f72b2ddbc1271454", + "scope_grant.json": "sha256:09ca975d1728cc5ae6ddfbfa28e73f22411badfa8edadd0060e284182038a673", + "scope_review_packet.json": "sha256:b7052c64e809b1ca43b4dfc7900e3db788e23abb564dfff05d3f8186dc4078da", + "summary.json": "sha256:988e437ccde5c669c2352bc6d6487aac6b7cc8a206f565adb5d3150f3615873e" + }, + "forbidden_files": [], + "quality_report_snippet": { + "policy_version": "scope-core-v0.8" + }, + "summary": { + "adapter_contract_version": "scope-akta-review-v0.8.1", + "approved_scope": "protocol_draft", + "status": "completed" + } +} diff --git a/examples/pilot/single_reviewer_protocol_draft/manifest.json b/examples/pilot/single_reviewer_protocol_draft/manifest.json new file mode 100644 index 0000000..15e911b --- /dev/null +++ b/examples/pilot/single_reviewer_protocol_draft/manifest.json @@ -0,0 +1,27 @@ +{ + "scenario": "single_reviewer_protocol_draft", + "scope_version": "0.8.1", + "policy_version": "scope-core-v0.8", + "akta_review_contract_version": "scope-akta-review-v0.8.1", + "artifacts": [ + { + "path": "scope_review_packet.json", + "schema": "scope_packet.schema.json" + }, + { + "path": "scope_decision.json", + "schema": "scope_decision.schema.json" + }, + { + "path": "scope_grant.json", + "schema": "scope_grant.schema.json" + }, + { + "path": "summary.json", + "schema": "scope_akta_review_summary.schema.json" + }, + { + "path": "quality_report_snippet.json" + } + ] +} From 510146702f1a474254c0310df381d3de778c9363 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:16 -0700 Subject: [PATCH 24/37] chore(pilot): add verification manifest for registry signed decision --- .../expected_verification.json | 17 ++++++++++++ .../registry_signed_decision/manifest.json | 27 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 examples/pilot/registry_signed_decision/expected_verification.json create mode 100644 examples/pilot/registry_signed_decision/manifest.json diff --git a/examples/pilot/registry_signed_decision/expected_verification.json b/examples/pilot/registry_signed_decision/expected_verification.json new file mode 100644 index 0000000..47ecca5 --- /dev/null +++ b/examples/pilot/registry_signed_decision/expected_verification.json @@ -0,0 +1,17 @@ +{ + "checksums": { + "quality_report_snippet.json": "sha256:be1186b24dbd1dda1e9bb9610d221e282e990be17723f6fbea9cc4388b338308", + "scope_decision.json": "sha256:f7505daa632ffd9b0f5ba77c5e1c7e8db58ccdfa5b496cc9059689f2cce3d957", + "scope_grant.json": "sha256:f1f3e759ec212da83d5a60b1b311ab03cf26f2e8a73e1b96eb837e0a7c7b4074", + "scope_review_packet.json": "sha256:4b69910aa8594ce4750a5d2b484a25d5a9bff4bec93595b0aa6f62233ccaaa8c", + "summary.json": "sha256:41747d1c090324fb4e525ef45763b2de997149f69e101a5338bbdbb803968117" + }, + "quality_report_snippet": { + "policy_version": "scope-core-v0.8" + }, + "summary": { + "adapter_contract_version": "scope-akta-review-v0.8.1", + "approved_scope": "protocol_draft", + "status": "completed" + } +} diff --git a/examples/pilot/registry_signed_decision/manifest.json b/examples/pilot/registry_signed_decision/manifest.json new file mode 100644 index 0000000..6104f71 --- /dev/null +++ b/examples/pilot/registry_signed_decision/manifest.json @@ -0,0 +1,27 @@ +{ + "scenario": "registry_signed_decision", + "scope_version": "0.8.1", + "policy_version": "scope-core-v0.8", + "akta_review_contract_version": "scope-akta-review-v0.8.1", + "artifacts": [ + { + "path": "scope_review_packet.json", + "schema": "scope_packet.schema.json" + }, + { + "path": "scope_decision.json", + "schema": "scope_decision.schema.json" + }, + { + "path": "scope_grant.json", + "schema": "scope_grant.schema.json" + }, + { + "path": "summary.json", + "schema": "scope_akta_review_summary.schema.json" + }, + { + "path": "quality_report_snippet.json" + } + ] +} From e04bda9c66da87743899ae4741d4142727f650b8 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:17 -0700 Subject: [PATCH 25/37] chore(pilot): add verification manifest for multi-role genomics review --- .../expected_verification.json | 17 +++++++++++++ .../multi_role_genomics_review/manifest.json | 25 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 examples/pilot/multi_role_genomics_review/expected_verification.json create mode 100644 examples/pilot/multi_role_genomics_review/manifest.json diff --git a/examples/pilot/multi_role_genomics_review/expected_verification.json b/examples/pilot/multi_role_genomics_review/expected_verification.json new file mode 100644 index 0000000..3cc9762 --- /dev/null +++ b/examples/pilot/multi_role_genomics_review/expected_verification.json @@ -0,0 +1,17 @@ +{ + "checksums": { + "quality_report_snippet.json": "sha256:fc4d0f3857ed363ed3f8a8c298c15445140c5cca12bdf0c5506e1170b7018c36", + "scope_review_packet.json": "sha256:646694501a9e4cdc70aa48a6582950df06f8e0356695aacf58144f9a23b57fc6", + "summary.json": "sha256:2c3435097657e09cb2796bf86fd302e8d82b54d389eb1b14fdf327e4fbcf1045" + }, + "forbidden_files": [ + "scope_grant.json" + ], + "quality_report_snippet": { + "policy_version": "scope-core-v0.8" + }, + "summary": { + "adapter_contract_version": "scope-akta-review-v0.8.1", + "status": "session_required" + } +} diff --git a/examples/pilot/multi_role_genomics_review/manifest.json b/examples/pilot/multi_role_genomics_review/manifest.json new file mode 100644 index 0000000..50bef16 --- /dev/null +++ b/examples/pilot/multi_role_genomics_review/manifest.json @@ -0,0 +1,25 @@ +{ + "scenario": "multi_role_genomics_review", + "scope_version": "0.8.1", + "policy_version": "scope-core-v0.8", + "akta_review_contract_version": "scope-akta-review-v0.8.1", + "artifacts": [ + { + "path": "scope_review_packet.json", + "schema": "scope_packet.schema.json" + }, + { + "path": "summary.json", + "schema": "scope_akta_review_session_summary.schema.json" + }, + { + "path": "decision_protocol_owner.json" + }, + { + "path": "decision_domain_scientist.json" + }, + { + "path": "quality_report_snippet.json" + } + ] +} From 08bd440e94f08c35e467f5d61715690b4dbb7df3 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:17 -0700 Subject: [PATCH 26/37] chore(pilot): add verification manifest for needs information flow --- .../expected_verification.json | 21 ++++++++++++++++++ .../needs_information_flow/manifest.json | 22 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 examples/pilot/needs_information_flow/expected_verification.json create mode 100644 examples/pilot/needs_information_flow/manifest.json diff --git a/examples/pilot/needs_information_flow/expected_verification.json b/examples/pilot/needs_information_flow/expected_verification.json new file mode 100644 index 0000000..d7a2f40 --- /dev/null +++ b/examples/pilot/needs_information_flow/expected_verification.json @@ -0,0 +1,21 @@ +{ + "checksums": { + "quality_report_snippet.json": "sha256:be1186b24dbd1dda1e9bb9610d221e282e990be17723f6fbea9cc4388b338308", + "review_queue_in_review.json": "sha256:1d496f351bd82c46f6eaa3ffd7522461c5996cc9b5f54c15b7b383ebc5a81d39", + "review_queue_needs_information.json": "sha256:44c58e41aba993d81a90a8e0460b57f19d126daec54cc37b2f93be2fdd40c7c1", + "scope_review_packet.json": "sha256:24a7519aff68a84d66b7bb4e95aa549fdd07d899e00e9cc154633f2dd7fd62d0" + }, + "quality_report_snippet": { + "policy_version": "scope-core-v0.8" + }, + "queue_states": { + "review_queue_in_review.json": { + "packet_id": "SCOPE-PKT-92CBAD", + "status": "in_review" + }, + "review_queue_needs_information.json": { + "packet_id": "SCOPE-PKT-92CBAD", + "status": "needs_information" + } + } +} diff --git a/examples/pilot/needs_information_flow/manifest.json b/examples/pilot/needs_information_flow/manifest.json new file mode 100644 index 0000000..67b6abf --- /dev/null +++ b/examples/pilot/needs_information_flow/manifest.json @@ -0,0 +1,22 @@ +{ + "scenario": "needs_information_flow", + "scope_version": "0.8.1", + "policy_version": "scope-core-v0.8", + "artifacts": [ + { + "path": "scope_review_packet.json", + "schema": "scope_packet.schema.json" + }, + { + "path": "review_queue_needs_information.json", + "schema": "scope_review_queue.schema.json" + }, + { + "path": "review_queue_in_review.json", + "schema": "scope_review_queue.schema.json" + }, + { + "path": "quality_report_snippet.json" + } + ] +} From fead648911d2783d2a29cb5b71116d5b2804c0a8 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:17 -0700 Subject: [PATCH 27/37] chore(pilot): add verification manifest for expired queue reopen --- .../expected_verification.json | 21 ++++++++++++++++++ .../pilot/expired_queue_reopen/manifest.json | 22 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 examples/pilot/expired_queue_reopen/expected_verification.json create mode 100644 examples/pilot/expired_queue_reopen/manifest.json diff --git a/examples/pilot/expired_queue_reopen/expected_verification.json b/examples/pilot/expired_queue_reopen/expected_verification.json new file mode 100644 index 0000000..172f89d --- /dev/null +++ b/examples/pilot/expired_queue_reopen/expected_verification.json @@ -0,0 +1,21 @@ +{ + "checksums": { + "quality_report_snippet.json": "sha256:be1186b24dbd1dda1e9bb9610d221e282e990be17723f6fbea9cc4388b338308", + "review_queue_expired.json": "sha256:389c4b9d4057700657a3981bec4978dbe1d05c027ac82f80012a7564ea5f2a67", + "review_queue_reopened.json": "sha256:6d6e73a7ce2898083b7566a847fa00aa68c97667514b2c29918e3f2b9c24f992", + "scope_review_packet.json": "sha256:5cc481424042f5c5e2b2e5dbda08d3e5070ffb35c91fb38116eb98221b849413" + }, + "quality_report_snippet": { + "policy_version": "scope-core-v0.8" + }, + "queue_states": { + "review_queue_expired.json": { + "packet_id": "SCOPE-PKT-2F8C44", + "status": "expired" + }, + "review_queue_reopened.json": { + "packet_id": "SCOPE-PKT-2F8C44", + "status": "open" + } + } +} diff --git a/examples/pilot/expired_queue_reopen/manifest.json b/examples/pilot/expired_queue_reopen/manifest.json new file mode 100644 index 0000000..cc9eb12 --- /dev/null +++ b/examples/pilot/expired_queue_reopen/manifest.json @@ -0,0 +1,22 @@ +{ + "scenario": "expired_queue_reopen", + "scope_version": "0.8.1", + "policy_version": "scope-core-v0.8", + "artifacts": [ + { + "path": "scope_review_packet.json", + "schema": "scope_packet.schema.json" + }, + { + "path": "review_queue_expired.json", + "schema": "scope_review_queue.schema.json" + }, + { + "path": "review_queue_reopened.json", + "schema": "scope_review_queue.schema.json" + }, + { + "path": "quality_report_snippet.json" + } + ] +} From c625de9f3859fe2fb1269f73f905518f16ee96d5 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:18 -0700 Subject: [PATCH 28/37] test(pilot): integrate manifest verification into fixture tests --- tests/test_pilot_fixtures.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_pilot_fixtures.py b/tests/test_pilot_fixtures.py index 8e4986a..1190a04 100644 --- a/tests/test_pilot_fixtures.py +++ b/tests/test_pilot_fixtures.py @@ -3,6 +3,8 @@ from __future__ import annotations import json +import subprocess +import sys from pathlib import Path import pytest @@ -103,7 +105,7 @@ def test_pilot_summary_contract(scenario: str) -> None: spec = SCENARIOS[scenario] summary = json.loads((PILOT / scenario / "summary.json").read_text(encoding="utf-8")) assert summary["status"] == spec["summary_status"] - assert summary["adapter_contract_version"] == "scope-akta-review-v0.8" + assert summary["adapter_contract_version"] == "scope-akta-review-v0.8.1" schema = ( "scope_akta_review_session_summary.schema.json" if spec["summary_status"] == "session_required" @@ -116,3 +118,27 @@ def test_pilot_index_readme() -> None: readme = (PILOT / "README.md").read_text(encoding="utf-8") for scenario in SCENARIOS: assert scenario in readme + + +def test_verify_pilot_fixtures_script() -> None: + """CI verifier script must pass on the committed fixture pack.""" + result = subprocess.run( + [sys.executable, str(ROOT / "scripts" / "verify_pilot_fixtures.py")], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr or result.stdout + assert "All 5 pilot fixture(s) verified." in result.stdout + + +@pytest.mark.parametrize("scenario", list(SCENARIOS)) +def test_pilot_manifest_and_verification_files(scenario: str) -> None: + base = PILOT / scenario + assert (base / "manifest.json").is_file(), f"{scenario}: missing manifest.json" + assert (base / "expected_verification.json").is_file(), ( + f"{scenario}: missing expected_verification.json" + ) + manifest = json.loads((base / "manifest.json").read_text(encoding="utf-8")) + assert manifest["scenario"] == scenario From 8aa147fa50da02bea4589516150badf1a06057ff Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:19 -0700 Subject: [PATCH 29/37] docs(pilot): document manifest and expected_verification fixtures --- examples/pilot/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/pilot/README.md b/examples/pilot/README.md index b795f00..8c980be 100644 --- a/examples/pilot/README.md +++ b/examples/pilot/README.md @@ -1,6 +1,6 @@ -# SCOPE Pilot Fixture Pack (v0.8) +# SCOPE Pilot Fixture Pack (v0.8.1) -Institutional pilot scenarios generated with SCOPE v0.8 CLI and engine APIs. Each subdirectory is self-contained with review artifacts, queue state where applicable, rendered packet markdown, and a quality report snippet. +Institutional pilot scenarios generated with SCOPE v0.8.1 CLI and engine APIs. Each subdirectory is self-contained with review artifacts, queue state where applicable, rendered packet markdown, and a quality report snippet. | Scenario | Directory | Highlights | |----------|-----------|------------| @@ -10,4 +10,10 @@ Institutional pilot scenarios generated with SCOPE v0.8 CLI and engine APIs. Eac | Needs information flow | [needs_information_flow](needs_information_flow/) | `needs_information` to `in_review` | | Registry-signed decision | [registry_signed_decision](registry_signed_decision/) | `--signing-provider registry --reviewer-id` | -Policy bundle: `scope-core-v0.8`. AKTA review contract: `scope-akta-review-v0.8`. +Policy bundle: `scope-core-v0.8`. AKTA review contract: `scope-akta-review-v0.8.1`. + +Each scenario includes `manifest.json` (artifact inventory and schema versions) and `expected_verification.json` (checksums, status values, queue states). Verify offline: + +```bash +python scripts/verify_pilot_fixtures.py +``` From 3005f018c20426ddd6b8f443fd55bf593c1810da Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:19 -0700 Subject: [PATCH 30/37] docs: document split AKTA summary contract in akta_review_contract --- docs/akta_review_contract.md | 45 ++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/docs/akta_review_contract.md b/docs/akta_review_contract.md index 9edc47c..911897d 100644 --- a/docs/akta_review_contract.md +++ b/docs/akta_review_contract.md @@ -1,37 +1,49 @@ # AKTA Review Output Contract -SCOPE v0.8 freezes the `scope akta review` output contract under `out_dir/`. +SCOPE v0.8.1 splits the `scope akta review` output contract by `summary.status`. + +## Branching rule + +Consumers **must** branch on `summary.status`: + +| `summary.status` | Schema | Artifacts | +|------------------|--------|-----------| +| `completed` | `schemas/scope_akta_review_summary.schema.json` | packet, decision, grant, summary | +| `session_required` | `schemas/scope_akta_review_session_summary.schema.json` | packet, summary only | + +The two schemas are mutually exclusive: completed summaries cannot carry session fields (`session_id`, `required_roles`, `message`); session summaries cannot carry grant/decision fields (`decision_path`, `grant_path`, `approved_scope`, IAL/SAL, etc.). + +Contract version constant: `scope-akta-review-v0.8.1` (`scope.integration_versions.AKTA_REVIEW_CONTRACT_VERSION`). ## Artifacts | File | Description | |------|-------------| | `scope_review_packet.json` | Review packet | -| `scope_decision.json` | Signed or unsigned decision (completed path only) | -| `scope_grant.json` | Issued grant (completed path only) | -| `summary.json` | Adapter summary (validated against schema) | +| `scope_decision.json` | Signed or unsigned decision (`completed` only) | +| `scope_grant.json` | Issued grant (`completed` only) | +| `summary.json` | Adapter summary (schema selected by `status`) | ## summary.json contract (completed review) -Contract version constant: `scope-akta-review-v0.8` (`scope.integration_versions.AKTA_REVIEW_CONTRACT_VERSION`). - Required fields: ```json { + "status": "completed", "packet_path": "...", "decision_path": "...", "grant_path": "...", "approved_scope": "...", "requested_scope": "...", - "adapter_contract_version": "scope-akta-review-v0.8", + "adapter_contract_version": "scope-akta-review-v0.8.1", "identity_assurance_level": "IAL0", "signing_assurance_level": "SAL1", "production_mode": false } ``` -Optional fields: `status`, `packet_id`, `decision_id`, `grant_id`, `allowed_tools`, `blocked_tools`, `decision_type`, `scope_trust_root_hash`, `queue_id`. +Optional fields: `packet_id`, `decision_id`, `grant_id`, `allowed_tools`, `blocked_tools`, `decision_type`, `scope_trust_root_hash`, `queue_id`. Schema: `schemas/scope_akta_review_summary.schema.json` @@ -48,7 +60,7 @@ Required fields: "session_id": "SCOPE-SESS-...", "required_roles": ["domain_scientist", "protocol_owner"], "message": "Multi-role review session created; submit votes before grant issue.", - "adapter_contract_version": "scope-akta-review-v0.8", + "adapter_contract_version": "scope-akta-review-v0.8.1", "production_mode": false } ``` @@ -59,6 +71,8 @@ Schema: `schemas/scope_akta_review_session_summary.schema.json` Without `--session`, multi-role packets fail with an explicit error directing operators to re-run with `--session` or use `scope review session create`. +Runtime validation: `scope.akta_review.validate_summary_artifact(summary)` selects the schema from `summary.status`. + ## Session grant provenance When a grant is issued from a multi-reviewer session (`issue_grant_from_session`), provenance includes aggregated session fields: @@ -72,17 +86,24 @@ When a grant is issued from a multi-reviewer session (`issue_grant_from_session` | `veto_roles_applied` | Safety veto roles from quorum policy | | `quorum_policy_hash` | Digest of session quorum policy | -These fields are optional in `schemas/scope_grant.schema.json` (present only on session grants). Single-reviewer grants omit them. +When `contributing_identity_assurance_levels` is present, `schemas/scope_grant.schema.json` requires all session provenance fields. Single-reviewer grants omit them entirely. + +Runtime double-check: `scope.session_provenance.validate_session_grant_provenance`. ## Reviewer ID binding -When `--signing-provider registry` is used, pass `--reviewer-id` to bind the registry lookup. If provided, it must match `reviewer.json` `reviewer_id`; mismatch fails before signing. +All entry points (CLI, REST, Python API) call `scope.akta_review.resolve_reviewer_id()` before signing: + +- When `--signing-provider registry` is used, pass `--reviewer-id` to bind the registry lookup. +- If provided, it must match `reviewer.json` `reviewer_id`; mismatch fails before signing. +- Registry signing cannot proceed without an explicit, validated reviewer identity. ## Acceptance criteria `scope akta review` enforces: -- Summary validates against schema (completed or session mode) +- Summary validates against the schema for its `status` branch +- Completed and session summaries cannot be confused - Overbroad approval fails (approved scope stronger than requested) - Unsigned production grant fails without signing key/provider - Missing reviewer authority fails (two-stage RBAC + SCOPE policy) From a1789cba59c2540972ba0d77acc4371f09878076 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:20 -0700 Subject: [PATCH 31/37] docs: note v0.8.1 contract changes in limitations --- docs/limitations.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/limitations.md b/docs/limitations.md index 9b408f8..7f3e973 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -1,12 +1,21 @@ # Limitations +## Implemented in v0.8.1 + +- Summary contract split: `completed` vs `session_required` schemas; consumers branch on `summary.status` +- `validate_summary_artifact()` on all AKTA review write paths (CLI, REST, Python API) +- Conditional session grant provenance: schema if/then plus runtime check in `scope/session_provenance.py` +- Centralized `resolve_reviewer_id()` for CLI, REST, and Python API +- Verifiable pilot fixtures: per-scenario `manifest.json`, `expected_verification.json`, and `scripts/verify_pilot_fixtures.py` +- AKTA review contract `scope-akta-review-v0.8.1` (incompatible summary schema split; supersedes v0.8.0 contract shape) + ## Implemented in v0.8 - `scope akta review --session` for multi-role packets: session summary schema, explicit failure without `--session` - `--reviewer-id` binding for registry signing (must match reviewer artifact) - Session grant provenance aggregation: contributing IAL/SAL, authority checks, veto roles, quorum policy hash - Pilot fixture pack under `examples/pilot/` (five institutional scenarios) -- Policy bundle `scope-core-v0.8`; AKTA review contract `scope-akta-review-v0.8` +- Policy bundle `scope-core-v0.8` ## Implemented in v0.7 From 59ce02cef10c8e993d7be4f812da130375ccdbcb Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:20 -0700 Subject: [PATCH 32/37] docs: update external integration contracts for v0.8.1 --- docs/external_integration_contracts.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/external_integration_contracts.md b/docs/external_integration_contracts.md index 2aa67b7..5b8acb9 100644 --- a/docs/external_integration_contracts.md +++ b/docs/external_integration_contracts.md @@ -56,11 +56,18 @@ Evidence vocabulary mapping: [evidence_vocab_mapping.md](evidence_vocab_mapping. | File | Description | |------|-------------| | `scope_review_packet.json` | Review packet | -| `scope_decision.json` | Decision (signed in production mode) | -| `scope_grant.json` | Issued grant | -| `summary.json` | Adapter summary validated against `schemas/scope_akta_review_summary.schema.json` | +| `scope_decision.json` | Decision (signed in production mode; `completed` only) | +| `scope_grant.json` | Issued grant (`completed` only) | +| `summary.json` | Adapter summary; schema selected by `summary.status` | -Contract version: `scope-akta-review-v0.7`. Required `summary.json` fields include `adapter_contract_version`, `identity_assurance_level`, `signing_assurance_level`, and `production_mode`. +Contract version: `scope-akta-review-v0.8.1`. Branch on `summary.status`: + +| `summary.status` | Schema | +|------------------|--------| +| `completed` | `schemas/scope_akta_review_summary.schema.json` (paths, IAL/SAL, approved scope) | +| `session_required` | `schemas/scope_akta_review_session_summary.schema.json` (session_id, required_roles; no decision/grant artifacts) | + +Runtime validation: `scope.akta_review.validate_summary_artifact(summary)`. Full contract: [akta_review_contract.md](akta_review_contract.md). From 240e81071d7094db3760e56ea976f74ff5076fe1 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:20 -0700 Subject: [PATCH 33/37] docs: refresh AKTA demo doc for v0.8.1 --- docs/akta_scope_demo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/akta_scope_demo.md b/docs/akta_scope_demo.md index 6aa5bb5..a65ff1e 100644 --- a/docs/akta_scope_demo.md +++ b/docs/akta_scope_demo.md @@ -27,7 +27,7 @@ Outputs in `/tmp/akta_review_out/`: | `scope_grant.json` | Bounded authorization grant | | `summary.json` | Machine-readable contract summary (validated against schema) | -`summary.json` includes `adapter_contract_version` (`scope-akta-review-v0.7`), `identity_assurance_level`, `signing_assurance_level`, and `production_mode`. +`summary.json` includes `adapter_contract_version` (`scope-akta-review-v0.8.1`), branches on `status` (`completed` or `session_required`), and records `identity_assurance_level`, `signing_assurance_level`, and `production_mode` on completed summaries. SCOPE rejects overbroad `--grant-scope` values against the packet's `requested_scope`. From 50f2046fba4788a94ae0a8a0a212da95ef6c5b6f Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:21 -0700 Subject: [PATCH 34/37] docs: bump README release badge to v0.8.1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd8b5e3..8e008cb 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ **Scoped authorization for AI-shaped scientific work** -[![Version](https://img.shields.io/badge/version-0.8.0-blue)](https://github.com/fraware/SCOPE/releases) +[![Version](https://img.shields.io/badge/version-0.8.1-blue)](https://github.com/fraware/SCOPE/releases) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/) [![CI](https://github.com/fraware/SCOPE/actions/workflows/ci.yml/badge.svg)](https://github.com/fraware/SCOPE/actions/workflows/ci.yml) From 564f68243c3f53ee8b7e9bd303c70f1e27910362 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:21 -0700 Subject: [PATCH 35/37] docs: refresh package docstring for v0.8.1 --- scope/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scope/__init__.py b/scope/__init__.py index 162b967..0821b65 100644 --- a/scope/__init__.py +++ b/scope/__init__.py @@ -1,4 +1,4 @@ -"""Scoped Scientific Authorization Protocol (SCOPE) v0.8.0.""" +"""Scoped Scientific Authorization Protocol (SCOPE) v0.8.1.""" from __future__ import annotations From 42b77fa20dd0a203d42c8e5a3d800d3bf5aad030 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:28:21 -0700 Subject: [PATCH 36/37] docs(changelog): add v0.8.1 release notes --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7cf232..8b95309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## v0.8.1 (2026-06-29) + +Contract hardening and verifiable pilot fixtures: + +- **Summary contract split**: `scope_akta_review_summary.schema.json` (status `completed` only) and `scope_akta_review_session_summary.schema.json` (status `session_required` only); consumers branch on `summary.status`; `validate_summary_artifact()` in `scope/akta_review.py` +- **Conditional session grant provenance**: `scope_grant.schema.json` if/then requires full session provenance block when `contributing_identity_assurance_levels` present; runtime check in `scope/session_provenance.py` +- **Reviewer-ID binding**: centralized `resolve_reviewer_id()` used by CLI, REST, and Python API +- **Verifiable pilot fixture pack**: `manifest.json` and `expected_verification.json` per scenario; `scripts/verify_pilot_fixtures.py` for offline validation +- AKTA review contract bumped to `scope-akta-review-v0.8.1` (incompatible summary schema split) + ## v0.8.0 (2026-06-29) Session workflow and provenance release: From e078d02df6b3e24964b20dadc05d8f9eeaebc4d7 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 29 Jun 2026 13:41:27 -0700 Subject: [PATCH 37/37] fix(ci): resolve Linux CI failure for v0.8.1 Normalize LF line endings when hashing pilot JSON fixtures and align expected checksums so verify passes on Linux and Windows. --- .../expired_queue_reopen/expected_verification.json | 8 ++++---- .../expected_verification.json | 6 +++--- .../needs_information_flow/expected_verification.json | 8 ++++---- .../expected_verification.json | 10 +++++----- .../expected_verification.json | 10 +++++----- scripts/verify_pilot_fixtures.py | 9 ++++----- 6 files changed, 25 insertions(+), 26 deletions(-) diff --git a/examples/pilot/expired_queue_reopen/expected_verification.json b/examples/pilot/expired_queue_reopen/expected_verification.json index 172f89d..3304632 100644 --- a/examples/pilot/expired_queue_reopen/expected_verification.json +++ b/examples/pilot/expired_queue_reopen/expected_verification.json @@ -1,9 +1,9 @@ { "checksums": { - "quality_report_snippet.json": "sha256:be1186b24dbd1dda1e9bb9610d221e282e990be17723f6fbea9cc4388b338308", - "review_queue_expired.json": "sha256:389c4b9d4057700657a3981bec4978dbe1d05c027ac82f80012a7564ea5f2a67", - "review_queue_reopened.json": "sha256:6d6e73a7ce2898083b7566a847fa00aa68c97667514b2c29918e3f2b9c24f992", - "scope_review_packet.json": "sha256:5cc481424042f5c5e2b2e5dbda08d3e5070ffb35c91fb38116eb98221b849413" + "quality_report_snippet.json": "sha256:a6906f08514b0fe2b9e20ff574a388ab6e4298ee627e3c4d8b5313ad94fff778", + "review_queue_expired.json": "sha256:84e4a0b4b1bae21342a03f5beda110d0659cd4b3293cbf5ad7f5ea90c6c8e123", + "review_queue_reopened.json": "sha256:f1a30e848026ca360bcaa0cc5983cb4677a74a04b549967e4a67ad9a913cbfae", + "scope_review_packet.json": "sha256:0d575e95909dad39bd40df93d336acc140495ba812be5aaaec4277359772b4d1" }, "quality_report_snippet": { "policy_version": "scope-core-v0.8" diff --git a/examples/pilot/multi_role_genomics_review/expected_verification.json b/examples/pilot/multi_role_genomics_review/expected_verification.json index 3cc9762..1654c82 100644 --- a/examples/pilot/multi_role_genomics_review/expected_verification.json +++ b/examples/pilot/multi_role_genomics_review/expected_verification.json @@ -1,8 +1,8 @@ { "checksums": { - "quality_report_snippet.json": "sha256:fc4d0f3857ed363ed3f8a8c298c15445140c5cca12bdf0c5506e1170b7018c36", - "scope_review_packet.json": "sha256:646694501a9e4cdc70aa48a6582950df06f8e0356695aacf58144f9a23b57fc6", - "summary.json": "sha256:2c3435097657e09cb2796bf86fd302e8d82b54d389eb1b14fdf327e4fbcf1045" + "quality_report_snippet.json": "sha256:c949d9e671baa36c95d6c8bb20fb0f30e571173835dff2bc269a4c9d95da8051", + "scope_review_packet.json": "sha256:654292d3b72dfef1fbd5aa38ed191abdfdab4b3a7244255226fa8e74fb9752ac", + "summary.json": "sha256:9061c4c9d4e517d5ed60ce562fcdbe2bb0fc2d98143ad3d3b9826e090dbda9b3" }, "forbidden_files": [ "scope_grant.json" diff --git a/examples/pilot/needs_information_flow/expected_verification.json b/examples/pilot/needs_information_flow/expected_verification.json index d7a2f40..c001a14 100644 --- a/examples/pilot/needs_information_flow/expected_verification.json +++ b/examples/pilot/needs_information_flow/expected_verification.json @@ -1,9 +1,9 @@ { "checksums": { - "quality_report_snippet.json": "sha256:be1186b24dbd1dda1e9bb9610d221e282e990be17723f6fbea9cc4388b338308", - "review_queue_in_review.json": "sha256:1d496f351bd82c46f6eaa3ffd7522461c5996cc9b5f54c15b7b383ebc5a81d39", - "review_queue_needs_information.json": "sha256:44c58e41aba993d81a90a8e0460b57f19d126daec54cc37b2f93be2fdd40c7c1", - "scope_review_packet.json": "sha256:24a7519aff68a84d66b7bb4e95aa549fdd07d899e00e9cc154633f2dd7fd62d0" + "quality_report_snippet.json": "sha256:a6906f08514b0fe2b9e20ff574a388ab6e4298ee627e3c4d8b5313ad94fff778", + "review_queue_in_review.json": "sha256:a9dd176ba8d1dbe31e904e662a48d42875437136874f16807beb70d098068e14", + "review_queue_needs_information.json": "sha256:287003adb074f06d01f5f76d9c00255ebcf2e10a4a34ab77453011817d75b871", + "scope_review_packet.json": "sha256:fa8aa72047c2bbc29ec2e8b214ac2dfc8ee8b97c080a2286321f746496018960" }, "quality_report_snippet": { "policy_version": "scope-core-v0.8" diff --git a/examples/pilot/registry_signed_decision/expected_verification.json b/examples/pilot/registry_signed_decision/expected_verification.json index 47ecca5..440d9ac 100644 --- a/examples/pilot/registry_signed_decision/expected_verification.json +++ b/examples/pilot/registry_signed_decision/expected_verification.json @@ -1,10 +1,10 @@ { "checksums": { - "quality_report_snippet.json": "sha256:be1186b24dbd1dda1e9bb9610d221e282e990be17723f6fbea9cc4388b338308", - "scope_decision.json": "sha256:f7505daa632ffd9b0f5ba77c5e1c7e8db58ccdfa5b496cc9059689f2cce3d957", - "scope_grant.json": "sha256:f1f3e759ec212da83d5a60b1b311ab03cf26f2e8a73e1b96eb837e0a7c7b4074", - "scope_review_packet.json": "sha256:4b69910aa8594ce4750a5d2b484a25d5a9bff4bec93595b0aa6f62233ccaaa8c", - "summary.json": "sha256:41747d1c090324fb4e525ef45763b2de997149f69e101a5338bbdbb803968117" + "quality_report_snippet.json": "sha256:a6906f08514b0fe2b9e20ff574a388ab6e4298ee627e3c4d8b5313ad94fff778", + "scope_decision.json": "sha256:1627726d073cff8ff14ac149e8ed9be94e3b2a5f55596885925ea4f77e2a74e9", + "scope_grant.json": "sha256:cafe5cccb559769e20c3a5d1ef0242f3f388db66ce80161cffe6a01b5d3bde1c", + "scope_review_packet.json": "sha256:84eba11c3b0632906d74334b4c93b7a6630ddb7ed65441d22f25c14eb2ea1f8e", + "summary.json": "sha256:8b06e3c0832d779ce361ca91e8720e455b7dd291c4fafda0505896920d072132" }, "quality_report_snippet": { "policy_version": "scope-core-v0.8" diff --git a/examples/pilot/single_reviewer_protocol_draft/expected_verification.json b/examples/pilot/single_reviewer_protocol_draft/expected_verification.json index 7daba8d..64ecdbc 100644 --- a/examples/pilot/single_reviewer_protocol_draft/expected_verification.json +++ b/examples/pilot/single_reviewer_protocol_draft/expected_verification.json @@ -1,10 +1,10 @@ { "checksums": { - "quality_report_snippet.json": "sha256:be1186b24dbd1dda1e9bb9610d221e282e990be17723f6fbea9cc4388b338308", - "scope_decision.json": "sha256:b8becf4dc97c19a6474e8d489271ab5e6928447130bc28d2f72b2ddbc1271454", - "scope_grant.json": "sha256:09ca975d1728cc5ae6ddfbfa28e73f22411badfa8edadd0060e284182038a673", - "scope_review_packet.json": "sha256:b7052c64e809b1ca43b4dfc7900e3db788e23abb564dfff05d3f8186dc4078da", - "summary.json": "sha256:988e437ccde5c669c2352bc6d6487aac6b7cc8a206f565adb5d3150f3615873e" + "quality_report_snippet.json": "sha256:a6906f08514b0fe2b9e20ff574a388ab6e4298ee627e3c4d8b5313ad94fff778", + "scope_decision.json": "sha256:720fb6762fbb00e467a3709e6c17ffa4beaa6cd5ad80245d0093b9f2b0305bd5", + "scope_grant.json": "sha256:88414a99bd54bf18697db5002954c4c19dd589ee2683a7026aca75b5e13adf16", + "scope_review_packet.json": "sha256:26ea5cbbedb2ce606fd892a23e97df8528b53fd6a17ca48708f2943914066d7e", + "summary.json": "sha256:deecbf7bc3b462af97bebe1a522e9f6312291077edc5a624cdc450156470b47e" }, "forbidden_files": [], "quality_report_snippet": { diff --git a/scripts/verify_pilot_fixtures.py b/scripts/verify_pilot_fixtures.py index 92b2f43..096b5fb 100644 --- a/scripts/verify_pilot_fixtures.py +++ b/scripts/verify_pilot_fixtures.py @@ -17,13 +17,12 @@ def file_sha256(path: Path) -> str: + data = path.read_bytes() + if path.suffix.lower() == ".json": + data = data.replace(b"\r\n", b"\n") digest = hashlib.sha256() - with path.open("rb") as fh: - for chunk in iter(lambda: fh.read(65536), b""): - digest.update(chunk) + digest.update(data) return f"sha256:{digest.hexdigest()}" - - def _load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8"))