diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f883b5..41186a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to AKTA are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.8.0] - 2026-06-29 + +### Added + +- `akta-review-cli` mode requires AKTA record before subprocess (`--akta-record`); fails closed with clear error +- First-class `04_scope_review_summary.json` artifact in reconstructable demo; PCS bundle includes `scope_review_summary.json` +- Fixture-driven SCOPE narrowing: `tests/fixtures/scope_scope_order.json`, `scope_valid_narrowing.json` +- Makefile targets: `demo-reconstructable-cross-repo`, `verify-reconstructable-cross-repo` +- Tests: `test_akta_review_requires_record.py`, `test_reconstructable_summary_artifact.py`, `test_scope_narrowing_fixture.py` + +### Changed + +- `validate_approval_grant()` uses fixture narrowing pairs instead of hard-coded `active_protocol_update` → `protocol_draft` only +- Reconstructable demo artifact numbering 00–12 with `10_pcs_bundle/`; cross-repo output at `dist/reconstructable_cross_repo/` +- `reconstruction_report.md` cites `summary.json` contract and checks assurance levels and tool lists +- `ScopeAdapterResult` includes `summary` for all adapter modes + ## [0.7.1] - 2026-06-28 ### Added diff --git a/Makefile b/Makefile index ddcb93c..05ac52f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install test eval-canonical eval-public eval-public-100 eval-oracle eval-holdout eval-v06 demo-weak-evidence demo-akta-weak-evidence demo-akta-scope-protocol-drift demo-reconstructable ci +.PHONY: install test eval-canonical eval-public eval-public-100 eval-oracle eval-holdout eval-v06 demo-weak-evidence demo-akta-weak-evidence demo-akta-scope-protocol-drift demo-reconstructable demo-reconstructable-cross-repo verify-reconstructable-cross-repo ci PYTHON ?= python @@ -26,6 +26,13 @@ eval-v06: demo-reconstructable: $(PYTHON) scripts/demo_reconstructable_experiment.py +demo-reconstructable-cross-repo: + $(PYTHON) -c "import os,sys; ok=bool(os.environ.get('SCOPE_REPO_PATH') or os.environ.get('SCOPE_CLI')); (print('Set SCOPE_REPO_PATH or SCOPE_CLI for live SCOPE') if not ok else None); sys.exit(0 if ok else 1)" + $(PYTHON) scripts/demo_reconstructable_experiment.py --cross-repo + +verify-reconstructable-cross-repo: + $(PYTHON) scripts/verify_reconstructable_cross_repo.py + demo-weak-evidence: $(PYTHON) scripts/demo_weak_evidence.py diff --git a/README.md b/README.md index 1f3d7e4..e60f974 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ REST API, MCP server, and batch evaluation are available for integrators — see | **PF-Core** | Runtime proof obligations tied to admissibility decisions | [PF-Core bridge](docs/pf_core_bridge.md) | | **PCS** | Packages decisions, records, and hashes for release verification | [PCS export](docs/pcs_export.md) | -SCOPE connects in several modes — offline simulation (default), Python import from a sibling checkout, subprocess CLI, or a one-shot review command. Details and conformance checks: [scope_live_conformance.md](docs/scope_live_conformance.md). +SCOPE connects in several modes — offline simulation (default), Python import from a sibling checkout, subprocess CLI, or a one-shot review command (`scope akta review` requires both AKTA trigger and record). Reconstructable cross-repo demo: `make demo-reconstructable-cross-repo` then `make verify-reconstructable-cross-repo`. Details: [scope_live_conformance.md](docs/scope_live_conformance.md). Cross-repo environment variables for optional CI jobs: [.github/CROSS_REPO_CI.md](.github/CROSS_REPO_CI.md). @@ -200,7 +200,7 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) for pull-request expectations, [GOVERNAN ## Project status -**v0.7.1** (`akta-protocol`) is the current reference kernel. It implements the full admissibility loop — gate, record, review triggers, grant re-gating, PF export, and PCS packaging — with 385+ tests and a green `make ci` suite. Biology, chemistry, and clinical domain overlays are experimental and not deployment-ready without institutional governance. Fully autonomous scientific operator profiles are defined for taxonomy only and are not supported at runtime. AKTA is a reference implementation under the MIT license, not a safety certification. Known gaps and non-goals: [docs/limitations.md](docs/limitations.md). +**v0.8.0** (`akta-protocol`) is the current reference kernel. Biology, chemistry, and clinical domain overlays are experimental and not deployment-ready without institutional governance. Fully autonomous scientific operator profiles are defined for taxonomy only and are not supported at runtime. AKTA is a reference implementation under the MIT license, not a safety certification. Known gaps and non-goals: [docs/limitations.md](docs/limitations.md). --- diff --git a/adapters/pcs/export_artifact.py b/adapters/pcs/export_artifact.py index 4326c6e..db2ed35 100644 --- a/adapters/pcs/export_artifact.py +++ b/adapters/pcs/export_artifact.py @@ -31,6 +31,7 @@ def build_pcs_manifest( *, file_hashes: dict[str, str] | None = None, include_review_trigger: bool = False, + include_scope_summary: bool = False, include_scope_packet: bool = False, include_scope_decision: bool = False, include_scope_grant: bool = False, @@ -42,6 +43,8 @@ def build_pcs_manifest( files = list(CORE_FILES) if include_review_trigger: files.append("review_trigger.json") + if include_scope_summary: + files.append("scope_review_summary.json") if include_scope_packet: files.append("scope_review_packet.json") if include_scope_decision: @@ -107,6 +110,7 @@ def validate_pcs_bundle(bundle_dir: str | Path) -> None: json.loads((bundle_dir / "akta_decision.json").read_text(encoding="utf-8")), file_hashes=file_hashes, include_review_trigger=(bundle_dir / "review_trigger.json").exists(), + include_scope_summary=(bundle_dir / "scope_review_summary.json").exists(), include_scope_packet=(bundle_dir / "scope_review_packet.json").exists(), include_scope_decision=(bundle_dir / "scope_decision.json").exists(), include_scope_grant=(bundle_dir / "scope_grant.json").exists(), @@ -122,6 +126,7 @@ def export_pcs_bundle( out_dir: str | Path, *, decision: dict[str, Any] | None = None, + scope_review_summary: dict[str, Any] | None = None, scope_review_packet: dict[str, Any] | None = None, scope_decision: dict[str, Any] | None = None, scope_grant: dict[str, Any] | None = None, @@ -190,6 +195,10 @@ def _write(name: str, content: str) -> None: if review_trigger: _write("review_trigger.json", json.dumps(review_trigger, indent=2)) + has_scope_summary = scope_review_summary is not None + if scope_review_summary is not None: + _write("scope_review_summary.json", json.dumps(scope_review_summary, indent=2)) + has_scope_packet = scope_review_packet is not None if scope_review_packet is not None: _write("scope_review_packet.json", json.dumps(scope_review_packet, indent=2)) @@ -220,6 +229,7 @@ def _write(name: str, content: str) -> None: decision_payload, file_hashes=artifacts, include_review_trigger=has_review_trigger, + include_scope_summary=has_scope_summary, include_scope_packet=has_scope_packet, include_scope_decision=has_scope_decision, include_scope_grant=has_scope_grant, diff --git a/adapters/scope/client.py b/adapters/scope/client.py index 7e1dde3..7b3ab7f 100644 --- a/adapters/scope/client.py +++ b/adapters/scope/client.py @@ -37,6 +37,7 @@ class ScopeAdapterResult: review_packet: dict[str, Any] | None = None grant: dict[str, Any] | None = None decision: dict[str, Any] | None = None + summary: dict[str, Any] | None = None error: str | None = None @@ -176,7 +177,7 @@ def _decision_input_for_grant(granted: str, trigger: dict[str, Any]) -> dict[str requested = trigger.get("requested_scope", "protocol_draft") if granted == requested: return { - "type": "approve_scope", + "type": "approve", "approved_scope": granted, "rationale": "Approved at requested scope.", } @@ -255,6 +256,13 @@ def _python_import_scope( review_packet=packet, grant=grant, decision=decision, + summary=_synthesize_scope_summary( + adapter_mode=ADAPTER_MODE_PYTHON_IMPORT, + trigger=trigger, + packet=packet, + decision=decision, + grant=grant, + ), ) @@ -296,12 +304,51 @@ def _validate_akta_review_summary(summary: dict[str, Any]) -> None: validate_against_schema(summary, "scope_akta_review_summary.schema.json") +def _synthesize_scope_summary( + *, + adapter_mode: str, + trigger: dict[str, Any], + packet: dict[str, Any], + decision: dict[str, Any], + grant: dict[str, Any], +) -> dict[str, Any]: + """Build summary.json-compatible contract for non-akta-review adapter modes.""" + auth = grant.get("authorization") or {} + approved = auth.get("approved_scope") or grant.get("granted_scope") or "" + requested = ( + grant.get("requested_scope") + or (grant.get("source") or {}).get("requested_scope") + or trigger.get("requested_scope") + or "" + ) + return { + "status": "completed", + "approved_scope": approved, + "requested_scope": requested, + "allowed_tools": list(auth.get("allowed_tools") or grant.get("allowed_tools") or []), + "blocked_tools": list(auth.get("blocked_tools") or grant.get("blocked_tools") or []), + "adapter_contract_version": "scope-akta-review-v0.8", + "identity_assurance_level": "IAL0", + "signing_assurance_level": "SAL1", + "adapter_mode": adapter_mode, + "packet_id": packet.get("packet_id"), + "decision_id": decision.get("decision_id"), + "grant_id": grant.get("grant_id"), + } + + def _akta_review_cli_scope( trigger: dict[str, Any], record: dict[str, Any] | None, granted: str, reviewer_id: str, ) -> ScopeAdapterResult: + if record is None: + return ScopeAdapterResult( + adapter_mode=ADAPTER_MODE_AKTA_REVIEW_CLI, + error="SCOPE akta-review CLI mode requires an AKTA record.", + ) + cli = os.environ.get("SCOPE_CLI", "scope") with tempfile.TemporaryDirectory(prefix="akta-scope-akta-review-") as tmp: tmp_path = Path(tmp) @@ -388,6 +435,7 @@ def _akta_review_cli_scope( review_packet=packet, grant=grant, decision=decision, + summary=summary, ) @@ -492,6 +540,13 @@ def _cli_scope( review_packet=packet, grant=grant, decision=decision, + summary=_synthesize_scope_summary( + adapter_mode=ADAPTER_MODE_CLI, + trigger=trigger, + packet=packet, + decision=decision, + grant=grant, + ), ) @@ -532,6 +587,13 @@ def _simulated_scope( review_packet=packet, grant=grant, decision=decision, + summary=_synthesize_scope_summary( + adapter_mode=ADAPTER_MODE_SIMULATED, + trigger=trigger, + packet=packet, + decision=decision, + grant=grant, + ), ) diff --git a/akta/review_loop.py b/akta/review_loop.py index 209ba9a..9ed5bc4 100644 --- a/akta/review_loop.py +++ b/akta/review_loop.py @@ -17,38 +17,49 @@ enforce_grant_expiry, is_review_expired, ) -from akta.scope_contract import _scope_grant_approved_scope, _scope_grant_requested_scope +from akta.scope_contract import ( + _scope_grant_approved_scope, + _scope_grant_requested_scope, + is_valid_narrowing_grant, + scope_rank, +) from akta.scope_mapping import VALID_REQUESTED_SCOPES SCOPE_RANK_ORDER: dict[str, int] = { - "protocol_draft": 1, - "active_protocol_update": 2, - "single_validation_plan": 3, - "single_validation_run_draft": 3, - "single_run_queue_priority": 4, - "robot_queue_submission": 5, - "execution_payload_preparation": 5, - "publication_claim": 6, - "scientific_memory_import": 4, - "draft_only": 0, + scope: scope_rank(scope) + for scope in ( + "protocol_draft", + "active_protocol_update", + "single_validation_plan", + "single_validation_run_draft", + "single_run_queue_priority", + "robot_queue_submission", + "execution_payload_preparation", + "publication_claim", + "scientific_memory_import", + "draft_only", + ) } +def _scope_rank(scope: str) -> int: + try: + return scope_rank(scope) + except FileNotFoundError: + return SCOPE_RANK_ORDER.get(scope, 99) + + def _evidence_rank(evidence_state: str) -> int: if evidence_state.startswith("E") and len(evidence_state) > 1 and evidence_state[1].isdigit(): return int(evidence_state[1]) return 0 -def _scope_rank(scope: str) -> int: - return SCOPE_RANK_ORDER.get(scope, 99) - - def grant_scope_covers_action(granted_scope: str, requested_scope: str) -> bool: """Return True when granted scope covers the requested scope (narrow grants only).""" if granted_scope == requested_scope: return True - if requested_scope == "active_protocol_update" and granted_scope == "protocol_draft": + if is_valid_narrowing_grant(granted_scope=granted_scope, requested_scope=requested_scope): return True return _scope_rank(granted_scope) >= _scope_rank(requested_scope) diff --git a/akta/scope_contract.py b/akta/scope_contract.py index 5a677b1..5626cd4 100644 --- a/akta/scope_contract.py +++ b/akta/scope_contract.py @@ -7,12 +7,19 @@ from __future__ import annotations +import json +import os +from functools import lru_cache +from pathlib import Path from typing import Any from akta.scope_mapping import VALID_REQUESTED_SCOPES SCOPE_APPROVAL_SCOPES = frozenset(VALID_REQUESTED_SCOPES) +_AKTA_ROOT = Path(__file__).resolve().parent.parent +_DEFAULT_FIXTURES_DIR = _AKTA_ROOT / "tests" / "fixtures" + # v0.2 review_scope vocabulary -> v0.3 requested_scope (compat for SCOPE simulator). LEGACY_REVIEW_SCOPE_MAP: dict[str, str] = { "draft_only": "protocol_draft", @@ -34,6 +41,72 @@ } +def _fixture_candidates(basename: str) -> list[Path]: + """Resolve fixture path: SCOPE repo (live contract) then AKTA tests/fixtures.""" + candidates: list[Path] = [] + scope_repo = os.environ.get("SCOPE_REPO_PATH", "").strip() + if scope_repo: + root = Path(scope_repo) + for sub in ("schemas", "tests/fixtures", "fixtures"): + candidates.append(root / sub / basename) + candidates.append(root / sub / basename.replace(".json", ".fixture.json")) + candidates.append(_DEFAULT_FIXTURES_DIR / basename) + return candidates + + +def _load_fixture_json(basename: str) -> dict[str, Any]: + for path in _fixture_candidates(basename): + if path.is_file(): + return json.loads(path.read_text(encoding="utf-8")) + raise FileNotFoundError( + f"SCOPE contract fixture not found: {basename} " + f"(checked SCOPE_REPO_PATH and {_DEFAULT_FIXTURES_DIR})" + ) + + +@lru_cache(maxsize=1) +def load_scope_order() -> tuple[str, ...]: + """Ordered SCOPE approval scopes (narrowest to broadest) from fixture contract.""" + data = _load_fixture_json("scope_scope_order.json") + order = data.get("scope_order") + if not isinstance(order, list) or not order: + raise ValueError("scope_scope_order.json missing non-empty scope_order list") + return tuple(str(s) for s in order) + + +@lru_cache(maxsize=1) +def load_valid_narrowing_pairs() -> frozenset[tuple[str, str]]: + """Valid (requested_scope, granted_scope) narrowing pairs from fixture contract.""" + data = _load_fixture_json("scope_valid_narrowing.json") + pairs = data.get("valid_narrowing_pairs") + if not isinstance(pairs, list): + raise ValueError("scope_valid_narrowing.json missing valid_narrowing_pairs list") + result: set[tuple[str, str]] = set() + for entry in pairs: + if not isinstance(entry, dict): + continue + requested = entry.get("requested_scope") + granted = entry.get("granted_scope") + if requested and granted: + result.add((str(requested), str(granted))) + return frozenset(result) + + +def scope_rank(scope: str) -> int: + """Return 1-based rank from fixture scope order; unknown scopes rank last.""" + try: + return load_scope_order().index(scope) + 1 + except ValueError: + return 99 + + +def is_valid_narrowing_grant(*, granted_scope: str, requested_scope: str) -> bool: + """True when granted scope equals or validly narrows requested scope per fixture.""" + if granted_scope == requested_scope: + return True + return (requested_scope, granted_scope) in load_valid_narrowing_pairs() + + def resolve_trigger_requested_scope(trigger: dict[str, Any]) -> str | None: """Resolve requested_scope from v0.3 field or legacy review_scope fallback.""" scope = trigger.get("requested_scope") @@ -120,14 +193,15 @@ def validate_approval_grant( if granted_scope not in SCOPE_APPROVAL_SCOPES: raise ValueError(f"invalid granted_scope: {granted_scope}") - scope_ok = granted_scope == requested_scope or ( - requested_scope == "active_protocol_update" and granted_scope == "protocol_draft" - ) - if not scope_ok: + if not is_valid_narrowing_grant( + granted_scope=granted_scope, + requested_scope=requested_scope, + ): raise ValueError( f"grant scope {granted_scope} does not cover requested_scope {requested_scope}" ) + narrowed = granted_scope != requested_scope return { "granted_scope": granted_scope, "requested_scope": requested_scope, @@ -135,8 +209,11 @@ def validate_approval_grant( "blocked_tools": blocked_tools or [], "scope_covered": granted_scope == requested_scope, "narrow_draft_grant": ( - requested_scope == "active_protocol_update" and granted_scope == "protocol_draft" + narrowed + and requested_scope == "active_protocol_update" + and granted_scope == "protocol_draft" ), + "narrow_grant": narrowed, } diff --git a/docs/RELEASE.md b/docs/RELEASE.md index b6aa110..4c27d6a 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -1,4 +1,4 @@ -# Public release verification (v0.7.1) +# Public release verification (v0.8.0) Use this checklist before tagging a public release of the AKTA reference implementation. AKTA is an open protocol with a reference kernel — not a safety certification. @@ -22,6 +22,15 @@ pip install -e ".[dev,security]" | Protocol-drift demo | `make demo-akta-scope-protocol-drift` | Artifacts under `examples/integrated_protocol_drift/` | | Reconstructable chain | `make demo-reconstructable` | Canonical chain under `dist/reconstructable_experiment/` | +## Required when SCOPE sibling is available (v0.8 cross-repo gate) + +Set `SCOPE_REPO_PATH` to a SCOPE v0.8+ checkout, then: + +| Step | Command | Expected | +|------|---------|----------| +| Cross-repo demo | `make demo-reconstructable-cross-repo` | Live chain under `dist/reconstructable_cross_repo/` including `04_scope_review_summary.json` | +| Cross-repo verify | `make verify-reconstructable-cross-repo` | All required artifacts present; PCS bundle includes `scope_review_summary.json`; post-grant decision stays blocked | + ## Optional (live SCOPE sibling) When a SCOPE v0.5+ checkout is available: @@ -64,7 +73,8 @@ Experimental domain overlays (biology, chemistry, clinical) must be refused in p ## Documentation sanity -- README acceptance tables include v0.7.1 through v0.1 +- README acceptance tables include v0.8.0 through v0.1 +- Cross-repo release gate documented in README (`demo-reconstructable-cross-repo`, `verify-reconstructable-cross-repo`) - [limitations.md](limitations.md) states non-certification and authority boundary - CHANGELOG includes the release version and date - `pyproject.toml` version matches the tag diff --git a/docs/limitations.md b/docs/limitations.md index 4450a60..4388360 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -1,5 +1,19 @@ # AKTA Limitations +## v0.8 + +AKTA v0.8 adds fixture-driven SCOPE narrowing validation, first-class `summary.json` integration contract, and cross-repo reconstructable release gates. It explicitly does not: + +- Permit `akta-review-cli` without an AKTA record (both `--akta-trigger` and `--akta-record` required) +- Accept grant narrowing pairs outside the pinned fixture contract (unless SCOPE repo exposes updated fixtures via `SCOPE_REPO_PATH`) +- Treat simulated SCOPE artifacts as valid in cross-repo release verification + +### v0.8 integration limits + +- **SCOPE summary contract**: `04_scope_review_summary.json` mirrors `scope akta review` `summary.json`; synthesized in simulated/python-import/CLI modes +- **Cross-repo demo**: `dist/reconstructable_cross_repo/` when `SCOPE_REPO_PATH` or `SCOPE_CLI` is set; `make verify-reconstructable-cross-repo` rejects simulated adapter markers +- **Narrowing fixtures**: `tests/fixtures/scope_valid_narrowing.json` is source of truth when SCOPE repo does not expose narrowing rules + ## v0.7.1 AKTA v0.7.1 tightens grant-exact re-gating after SCOPE authorization. It explicitly does not: diff --git a/evals/reports/canonical_5.json b/evals/reports/canonical_5.json index 28752b4..668681d 100644 --- a/evals/reports/canonical_5.json +++ b/evals/reports/canonical_5.json @@ -78,7 +78,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V3_preliminary_experimental_support", "requested_scope": "active_protocol_update", - "review_trigger_id": "AKTA-REVTRIG-0339F665", + "review_trigger_id": "AKTA-REVTRIG-84C6B4F7", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] diff --git a/evals/reports/public_100.json b/evals/reports/public_100.json index 6cd1902..a1f3aeb 100644 --- a/evals/reports/public_100.json +++ b/evals/reports/public_100.json @@ -167,7 +167,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V3_preliminary_experimental_support", "requested_scope": "active_protocol_update", - "review_trigger_id": "AKTA-REVTRIG-06CAA152", + "review_trigger_id": "AKTA-REVTRIG-BDAE559B", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -213,7 +213,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V4_internally_replicated", "requested_scope": "active_protocol_update", - "review_trigger_id": "AKTA-REVTRIG-5E80C984", + "review_trigger_id": "AKTA-REVTRIG-20235C28", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -456,7 +456,7 @@ "evidence_state": "E5_internally_replicated_evidence", "validation_status": "V4_internally_replicated", "requested_scope": "robot_queue_submission", - "review_trigger_id": "AKTA-REVTRIG-7D7D96B6", + "review_trigger_id": "AKTA-REVTRIG-ED3B146D", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -493,7 +493,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V3_preliminary_experimental_support", "requested_scope": "active_protocol_update", - "review_trigger_id": "AKTA-REVTRIG-1EC7CD87", + "review_trigger_id": "AKTA-REVTRIG-A3F3AD37", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -612,7 +612,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V3_preliminary_experimental_support", "requested_scope": "active_protocol_update", - "review_trigger_id": "AKTA-REVTRIG-E1D613DA", + "review_trigger_id": "AKTA-REVTRIG-3ED1F819", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -1337,7 +1337,7 @@ "evidence_state": "E5_internally_replicated_evidence", "validation_status": "V5_independently_replicated", "requested_scope": "single_run_queue_priority", - "review_trigger_id": "AKTA-REVTRIG-70E42CD8", + "review_trigger_id": "AKTA-REVTRIG-C560B63A", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -1373,7 +1373,7 @@ "evidence_state": "E6_independently_reproduced_evidence", "validation_status": "V5_independently_replicated", "requested_scope": "single_run_queue_priority", - "review_trigger_id": "AKTA-REVTRIG-6D849D9D", + "review_trigger_id": "AKTA-REVTRIG-9C4DAEC9", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -1490,7 +1490,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V3_preliminary_experimental_support", "requested_scope": "single_validation_plan", - "review_trigger_id": "AKTA-REVTRIG-5BA4FD82", + "review_trigger_id": "AKTA-REVTRIG-B13CB159", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -1601,7 +1601,7 @@ "evidence_state": "E7_deployment_validated_evidence", "validation_status": "V5_independently_replicated", "requested_scope": "robot_queue_submission", - "review_trigger_id": "AKTA-REVTRIG-27D0B2B9", + "review_trigger_id": "AKTA-REVTRIG-E8FC5581", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -2315,7 +2315,7 @@ "evidence_state": "E3_noisy_or_conflicting_evidence", "validation_status": "V0_unvalidated", "requested_scope": "publication_claim", - "review_trigger_id": "AKTA-REVTRIG-5022C076", + "review_trigger_id": "AKTA-REVTRIG-1CCEB35F", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -2387,7 +2387,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V0_unvalidated", "requested_scope": "active_protocol_update", - "review_trigger_id": "AKTA-REVTRIG-C8340363", + "review_trigger_id": "AKTA-REVTRIG-ABB9E7A4", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -2873,7 +2873,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V0_unvalidated", "requested_scope": "single_validation_plan", - "review_trigger_id": "AKTA-REVTRIG-6EF6A940", + "review_trigger_id": "AKTA-REVTRIG-CBB9D8AA", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -2991,7 +2991,7 @@ "evidence_state": "E5_internally_replicated_evidence", "validation_status": "V5_independently_replicated", "requested_scope": "single_run_queue_priority", - "review_trigger_id": "AKTA-REVTRIG-AF81477D", + "review_trigger_id": "AKTA-REVTRIG-3A8B8720", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3027,7 +3027,7 @@ "evidence_state": "E6_independently_reproduced_evidence", "validation_status": "V0_unvalidated", "requested_scope": "robot_queue_submission", - "review_trigger_id": "AKTA-REVTRIG-53FFEE19", + "review_trigger_id": "AKTA-REVTRIG-85768071", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3097,7 +3097,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V0_unvalidated", "requested_scope": "single_validation_plan", - "review_trigger_id": "AKTA-REVTRIG-1551F3ED", + "review_trigger_id": "AKTA-REVTRIG-A6AF0C29", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3133,7 +3133,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V0_unvalidated", "requested_scope": "single_validation_plan", - "review_trigger_id": "AKTA-REVTRIG-52804CF3", + "review_trigger_id": "AKTA-REVTRIG-75329D16", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3169,7 +3169,7 @@ "evidence_state": "E5_internally_replicated_evidence", "validation_status": "V0_unvalidated", "requested_scope": "execution_payload_preparation", - "review_trigger_id": "AKTA-REVTRIG-EB85FC4C", + "review_trigger_id": "AKTA-REVTRIG-1E77DCE0", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3246,7 +3246,7 @@ "evidence_state": "E2_preliminary_signal", "validation_status": "V0_unvalidated", "requested_scope": "active_protocol_update", - "review_trigger_id": "AKTA-REVTRIG-FFE7380D", + "review_trigger_id": "AKTA-REVTRIG-87EEFC6C", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3282,7 +3282,7 @@ "evidence_state": "E7_deployment_validated_evidence", "validation_status": "V6_operationally_validated", "requested_scope": "single_run_queue_priority", - "review_trigger_id": "AKTA-REVTRIG-A3E514E8", + "review_trigger_id": "AKTA-REVTRIG-937C3D00", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3352,7 +3352,7 @@ "evidence_state": "E5_internally_replicated_evidence", "validation_status": "V0_unvalidated", "requested_scope": "protocol_draft", - "review_trigger_id": "AKTA-REVTRIG-C86FC079", + "review_trigger_id": "AKTA-REVTRIG-8B838AA0", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3462,7 +3462,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V0_unvalidated", "requested_scope": "single_validation_plan", - "review_trigger_id": "AKTA-REVTRIG-E520CDDE", + "review_trigger_id": "AKTA-REVTRIG-634EB6E3", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3619,7 +3619,7 @@ "evidence_state": "E6_independently_reproduced_evidence", "validation_status": "V0_unvalidated", "requested_scope": "single_validation_plan", - "review_trigger_id": "AKTA-REVTRIG-C57365D6", + "review_trigger_id": "AKTA-REVTRIG-2F417E87", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3689,7 +3689,7 @@ "evidence_state": "E5_internally_replicated_evidence", "validation_status": "V0_unvalidated", "requested_scope": "publication_claim", - "review_trigger_id": "AKTA-REVTRIG-3483438B", + "review_trigger_id": "AKTA-REVTRIG-0F69495B", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3766,7 +3766,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V0_unvalidated", "requested_scope": "active_protocol_update", - "review_trigger_id": "AKTA-REVTRIG-8B890697", + "review_trigger_id": "AKTA-REVTRIG-CF66CCC4", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -3842,7 +3842,7 @@ "evidence_state": "E7_deployment_validated_evidence", "validation_status": "V0_unvalidated", "requested_scope": "robot_queue_submission", - "review_trigger_id": "AKTA-REVTRIG-C592636D", + "review_trigger_id": "AKTA-REVTRIG-28AC7B25", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -4032,7 +4032,7 @@ "evidence_state": "E5_internally_replicated_evidence", "validation_status": "V0_unvalidated", "requested_scope": "single_validation_plan", - "review_trigger_id": "AKTA-REVTRIG-3162C6F7", + "review_trigger_id": "AKTA-REVTRIG-D21987EE", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -4069,7 +4069,7 @@ "evidence_state": "E6_independently_reproduced_evidence", "validation_status": "V0_unvalidated", "requested_scope": "robot_queue_submission", - "review_trigger_id": "AKTA-REVTRIG-BD0734AE", + "review_trigger_id": "AKTA-REVTRIG-9CA0F7C2", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -4183,7 +4183,7 @@ "evidence_state": "E3_noisy_or_conflicting_evidence", "validation_status": "V0_unvalidated", "requested_scope": "single_validation_plan", - "review_trigger_id": "AKTA-REVTRIG-634A07DF", + "review_trigger_id": "AKTA-REVTRIG-23FF272A", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -4261,7 +4261,7 @@ "evidence_state": "E4_internally_consistent_evidence", "validation_status": "V0_unvalidated", "requested_scope": "active_protocol_update", - "review_trigger_id": "AKTA-REVTRIG-356D2D0C", + "review_trigger_id": "AKTA-REVTRIG-28E6FDE1", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -4298,7 +4298,7 @@ "evidence_state": "E3_noisy_or_conflicting_evidence", "validation_status": "V0_unvalidated", "requested_scope": "single_validation_run_draft", - "review_trigger_id": "AKTA-REVTRIG-ED3E5C66", + "review_trigger_id": "AKTA-REVTRIG-99E3C1A6", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] @@ -4376,7 +4376,7 @@ "evidence_state": "E5_internally_replicated_evidence", "validation_status": "V0_unvalidated", "requested_scope": "execution_payload_preparation", - "review_trigger_id": "AKTA-REVTRIG-C8E711E7", + "review_trigger_id": "AKTA-REVTRIG-CEF0E859", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "next_admissible_steps": [] diff --git a/examples/integrated_protocol_drift/akta_decision_active_update.json b/examples/integrated_protocol_drift/akta_decision_active_update.json index cd49163..37bc7b6 100644 --- a/examples/integrated_protocol_drift/akta_decision_active_update.json +++ b/examples/integrated_protocol_drift/akta_decision_active_update.json @@ -91,7 +91,7 @@ "allowed_next_steps": [], "approval_effect": "Allows scoped next step only; not global permission.", "default_expiration": "single_run", - "expires_at": "2026-06-28T18:43:32Z", + "expires_at": "2026-06-29T09:56:59Z", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "classifier_confidence": 0.98, @@ -105,6 +105,6 @@ }, "review_route": "active_protocol_review", "domain_overlay_hash": "sha256:9043be6ab410404da7d6b98e28db21395884745b29372310d24310714f8fe117", - "review_trigger_hash": "sha256:8651c9ab953b986db9fb96d63575ffb9fd4303d0b97376d675e0ba1b33c93d93" + "review_trigger_hash": "sha256:d5a907835d13bc7d1c20ebed3efe29472dfa26d9d043187674eb91bb660b503c" } } \ No newline at end of file diff --git a/examples/integrated_protocol_drift/akta_record_active_update.json b/examples/integrated_protocol_drift/akta_record_active_update.json index 8035d1a..8618bed 100644 --- a/examples/integrated_protocol_drift/akta_record_active_update.json +++ b/examples/integrated_protocol_drift/akta_record_active_update.json @@ -101,7 +101,7 @@ "allowed_next_steps": [], "approval_effect": "Allows scoped next step only; not global permission.", "default_expiration": "single_run", - "expires_at": "2026-06-28T18:43:32Z", + "expires_at": "2026-06-29T09:56:59Z", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "classifier_confidence": 0.98, @@ -115,7 +115,7 @@ }, "review_route": "active_protocol_review", "domain_overlay_hash": "sha256:9043be6ab410404da7d6b98e28db21395884745b29372310d24310714f8fe117", - "review_trigger_hash": "sha256:8651c9ab953b986db9fb96d63575ffb9fd4303d0b97376d675e0ba1b33c93d93" + "review_trigger_hash": "sha256:d5a907835d13bc7d1c20ebed3efe29472dfa26d9d043187674eb91bb660b503c" }, - "record_hash": "sha256:2fbd2512b08f0602028d4970792e0c1e487f24979690ca56c6d71f9ae2dbf53f" + "record_hash": "sha256:48b1a5297388b0dc789d2f81df74fab7f0f8e271832bb5df3092a64eabb3a026" } \ No newline at end of file diff --git a/examples/integrated_protocol_drift/pcs_bundle/akta_decision.json b/examples/integrated_protocol_drift/pcs_bundle/akta_decision.json index cd49163..37bc7b6 100644 --- a/examples/integrated_protocol_drift/pcs_bundle/akta_decision.json +++ b/examples/integrated_protocol_drift/pcs_bundle/akta_decision.json @@ -91,7 +91,7 @@ "allowed_next_steps": [], "approval_effect": "Allows scoped next step only; not global permission.", "default_expiration": "single_run", - "expires_at": "2026-06-28T18:43:32Z", + "expires_at": "2026-06-29T09:56:59Z", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "classifier_confidence": 0.98, @@ -105,6 +105,6 @@ }, "review_route": "active_protocol_review", "domain_overlay_hash": "sha256:9043be6ab410404da7d6b98e28db21395884745b29372310d24310714f8fe117", - "review_trigger_hash": "sha256:8651c9ab953b986db9fb96d63575ffb9fd4303d0b97376d675e0ba1b33c93d93" + "review_trigger_hash": "sha256:d5a907835d13bc7d1c20ebed3efe29472dfa26d9d043187674eb91bb660b503c" } } \ No newline at end of file diff --git a/examples/integrated_protocol_drift/pcs_bundle/akta_record.json b/examples/integrated_protocol_drift/pcs_bundle/akta_record.json index 8035d1a..8618bed 100644 --- a/examples/integrated_protocol_drift/pcs_bundle/akta_record.json +++ b/examples/integrated_protocol_drift/pcs_bundle/akta_record.json @@ -101,7 +101,7 @@ "allowed_next_steps": [], "approval_effect": "Allows scoped next step only; not global permission.", "default_expiration": "single_run", - "expires_at": "2026-06-28T18:43:32Z", + "expires_at": "2026-06-29T09:56:59Z", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "classifier_confidence": 0.98, @@ -115,7 +115,7 @@ }, "review_route": "active_protocol_review", "domain_overlay_hash": "sha256:9043be6ab410404da7d6b98e28db21395884745b29372310d24310714f8fe117", - "review_trigger_hash": "sha256:8651c9ab953b986db9fb96d63575ffb9fd4303d0b97376d675e0ba1b33c93d93" + "review_trigger_hash": "sha256:d5a907835d13bc7d1c20ebed3efe29472dfa26d9d043187674eb91bb660b503c" }, - "record_hash": "sha256:2fbd2512b08f0602028d4970792e0c1e487f24979690ca56c6d71f9ae2dbf53f" + "record_hash": "sha256:48b1a5297388b0dc789d2f81df74fab7f0f8e271832bb5df3092a64eabb3a026" } \ No newline at end of file diff --git a/examples/integrated_protocol_drift/pcs_bundle/manifest.json b/examples/integrated_protocol_drift/pcs_bundle/manifest.json index 1c7be51..fe0f94d 100644 --- a/examples/integrated_protocol_drift/pcs_bundle/manifest.json +++ b/examples/integrated_protocol_drift/pcs_bundle/manifest.json @@ -1,7 +1,7 @@ { "artifact_type": "akta_scientific_action_record", "schema_version": "akta-record-v0.5", - "record_hash": "sha256:2fbd2512b08f0602028d4970792e0c1e487f24979690ca56c6d71f9ae2dbf53f", + "record_hash": "sha256:48b1a5297388b0dc789d2f81df74fab7f0f8e271832bb5df3092a64eabb3a026", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "integrity_mode": "deployment_hmac_attested", "domain_overlay_hash": "sha256:9043be6ab410404da7d6b98e28db21395884745b29372310d24310714f8fe117", @@ -18,19 +18,21 @@ "scope_decision.json", "scope_grant.json", "scope_review_packet.json", + "scope_review_summary.json", "tool_registry_hash.txt" ], "file_hashes": { - "akta_decision.json": "sha256:20a7c22936cff3146cfb463ae5a080560897b8d8a5b5d8dca9cd0d03e7af27c2", - "akta_record.json": "sha256:e5257442a9e787b0c9f63ace9302fb954c741efbf76c122805f785962146561f", + "akta_decision.json": "sha256:adda94e1240c358669f4a2ee95def552a794f093502a43625e91f4f6041291df", + "akta_record.json": "sha256:8c333bbe89285061dd02cce631ccdceb6df44eca984ea9a2d4113bc5872fd63c", "domain_overlay_hash.txt": "sha256:aff97a5c94f9f99549b9f4043617ba8dcfce973312fde13d63101d173bcb0860", - "pf_obligation.json": "sha256:c73218627d5511e7b2994286be4a8669f25592e35e82b17214aa5af55a3cc209", + "pf_obligation.json": "sha256:184226a39879f05af9c1765bb0079ce96f5505e67ecc152940278e5d8c7f68ae", "policy_hash.txt": "sha256:918d683b234533a1c7da8178e581eff636b2786f9b7cc373873ea805994b15ff", - "review_trigger.json": "sha256:a283d1cec616f1c1c0585c6b9321efc6c641cb2d99141fbbbfc23988a8d78d16", + "review_trigger.json": "sha256:f4e83d97fd9c20b9c066ab7f16b06047578edba3611e0a7fbfd7f86a7c907bb1", "scope_decision.json": "sha256:59ce03995c2b8c0685e82eed1efa992d7bf76e1fcce25bea0ffb43b78d2ad572", "scope_grant.json": "sha256:82295bc191264da408db4389909f051852aedbf649983d0821ea11783dcf3c1d", "scope_review_packet.json": "sha256:f18ef6c084047e2af76b31c40864279ba537edbc6e3742c9b78e75ca6d9eb15f", + "scope_review_summary.json": "sha256:37e4746f8c504841b593b855b4cd1709d457db558ebce81091b452bf82caf3d8", "tool_registry_hash.txt": "sha256:a5e313d4198cc3479985a6db476f6f330c8f45024c42580cff52ab7ee0c1f371" }, - "manifest_hash": "sha256:e5a00cebf7d11fd9f226255d439420022056ed565408c2d2d5361c229dadabdc" + "manifest_hash": "sha256:cedf85c2ff48096f168acb2dd7a17e1217abd7ce238811e75ede3853d70792b1" } \ No newline at end of file diff --git a/examples/integrated_protocol_drift/pcs_bundle/pf_obligation.json b/examples/integrated_protocol_drift/pcs_bundle/pf_obligation.json index adb2a8c..1a27388 100644 --- a/examples/integrated_protocol_drift/pcs_bundle/pf_obligation.json +++ b/examples/integrated_protocol_drift/pcs_bundle/pf_obligation.json @@ -17,7 +17,7 @@ "domain_overlay_hash": "sha256:9043be6ab410404da7d6b98e28db21395884745b29372310d24310714f8fe117", "scope_grant_ref": null, "review_trigger_id": "AKTA-REVTRIG-DRIFT0001", - "expires_at": "2026-06-28T18:43:32Z", + "expires_at": "2026-06-29T09:56:59Z", "enforcement_mode": "review_gate", "required_runtime_behavior": { "block_execution": false, @@ -33,5 +33,5 @@ "next_admissible_steps": [], "required_review_role": "protocol_owner", "consequentiality": true, - "obligation_hash": "sha256:17e5fdb63e75dac47072bae3581b5e8b43e31da8a762e6f9698542666bdbab84" + "obligation_hash": "sha256:f4aed790b90c4627149a1341f0b8a1b431ed0b2b5c1079d733afe1c5cb3171ab" } \ No newline at end of file diff --git a/examples/integrated_protocol_drift/pcs_bundle/review_trigger.json b/examples/integrated_protocol_drift/pcs_bundle/review_trigger.json index 2d72ab7..21e964f 100644 --- a/examples/integrated_protocol_drift/pcs_bundle/review_trigger.json +++ b/examples/integrated_protocol_drift/pcs_bundle/review_trigger.json @@ -33,7 +33,7 @@ "allowed_next_steps": [], "approval_effect": "Allows scoped next step only; not global permission.", "default_expiration": "single_run", - "expires_at": "2026-06-28T18:43:32Z", + "expires_at": "2026-06-29T09:56:59Z", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "classifier_confidence": 0.98, @@ -47,5 +47,5 @@ }, "review_route": "active_protocol_review", "domain_overlay_hash": "sha256:9043be6ab410404da7d6b98e28db21395884745b29372310d24310714f8fe117", - "review_trigger_hash": "sha256:8651c9ab953b986db9fb96d63575ffb9fd4303d0b97376d675e0ba1b33c93d93" + "review_trigger_hash": "sha256:d5a907835d13bc7d1c20ebed3efe29472dfa26d9d043187674eb91bb660b503c" } \ No newline at end of file diff --git a/examples/integrated_protocol_drift/pcs_bundle/scope_review_summary.json b/examples/integrated_protocol_drift/pcs_bundle/scope_review_summary.json new file mode 100644 index 0000000..53b7e39 --- /dev/null +++ b/examples/integrated_protocol_drift/pcs_bundle/scope_review_summary.json @@ -0,0 +1,14 @@ +{ + "status": "completed", + "approved_scope": "protocol_draft", + "requested_scope": "active_protocol_update", + "allowed_tools": [], + "blocked_tools": [], + "adapter_contract_version": "scope-akta-review-v0.8", + "identity_assurance_level": "IAL0", + "signing_assurance_level": "SAL1", + "adapter_mode": "simulated", + "packet_id": null, + "decision_id": null, + "grant_id": "SCOPE-GRANT-AKTA-REVTRIG-DRIFT0001" +} \ No newline at end of file diff --git a/examples/integrated_protocol_drift/pf_obligation.json b/examples/integrated_protocol_drift/pf_obligation.json index adb2a8c..1a27388 100644 --- a/examples/integrated_protocol_drift/pf_obligation.json +++ b/examples/integrated_protocol_drift/pf_obligation.json @@ -17,7 +17,7 @@ "domain_overlay_hash": "sha256:9043be6ab410404da7d6b98e28db21395884745b29372310d24310714f8fe117", "scope_grant_ref": null, "review_trigger_id": "AKTA-REVTRIG-DRIFT0001", - "expires_at": "2026-06-28T18:43:32Z", + "expires_at": "2026-06-29T09:56:59Z", "enforcement_mode": "review_gate", "required_runtime_behavior": { "block_execution": false, @@ -33,5 +33,5 @@ "next_admissible_steps": [], "required_review_role": "protocol_owner", "consequentiality": true, - "obligation_hash": "sha256:17e5fdb63e75dac47072bae3581b5e8b43e31da8a762e6f9698542666bdbab84" + "obligation_hash": "sha256:f4aed790b90c4627149a1341f0b8a1b431ed0b2b5c1079d733afe1c5cb3171ab" } \ No newline at end of file diff --git a/examples/integrated_protocol_drift/review_trigger.json b/examples/integrated_protocol_drift/review_trigger.json index 2d72ab7..21e964f 100644 --- a/examples/integrated_protocol_drift/review_trigger.json +++ b/examples/integrated_protocol_drift/review_trigger.json @@ -33,7 +33,7 @@ "allowed_next_steps": [], "approval_effect": "Allows scoped next step only; not global permission.", "default_expiration": "single_run", - "expires_at": "2026-06-28T18:43:32Z", + "expires_at": "2026-06-29T09:56:59Z", "policy_hash": "sha256:b2878971b3236b33c5011d6e12ed37ed85582f16996b9d538b584733d9f65464", "tool_registry_hash": "sha256:9c712243ab9fd22c3308df161ca78b03cae0b8e697034eb0f54f8ddfaf18f4e8", "classifier_confidence": 0.98, @@ -47,5 +47,5 @@ }, "review_route": "active_protocol_review", "domain_overlay_hash": "sha256:9043be6ab410404da7d6b98e28db21395884745b29372310d24310714f8fe117", - "review_trigger_hash": "sha256:8651c9ab953b986db9fb96d63575ffb9fd4303d0b97376d675e0ba1b33c93d93" + "review_trigger_hash": "sha256:d5a907835d13bc7d1c20ebed3efe29472dfa26d9d043187674eb91bb660b503c" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2cda17b..142f6c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "akta-protocol" -version = "0.7.1" +version = "0.8.0" description = "Open protocol for deciding when AI-generated scientific outputs are admissible to shape what science does next" readme = "README.md" license = { text = "MIT" } diff --git a/scripts/demo_akta_scope_protocol_drift.py b/scripts/demo_akta_scope_protocol_drift.py index 80be421..eb904dd 100644 --- a/scripts/demo_akta_scope_protocol_drift.py +++ b/scripts/demo_akta_scope_protocol_drift.py @@ -146,6 +146,7 @@ def run_demo() -> int: scope_packet = scope_result.review_packet or {} scope_grant = scope_result.grant or {} scope_decision = scope_result.decision or {} + scope_summary = scope_result.summary or {} (demo_dir / "scope_review_packet.json").write_text( json.dumps(scope_packet, indent=2), encoding="utf-8" ) @@ -198,6 +199,7 @@ def run_demo() -> int: AKTARecord(active_record), pcs_dir, decision=active_d, + scope_review_summary=scope_summary, scope_review_packet=scope_packet, scope_decision=scope_decision, scope_grant=scope_grant, diff --git a/scripts/demo_reconstructable_experiment.py b/scripts/demo_reconstructable_experiment.py index aafff9a..984b60c 100644 --- a/scripts/demo_reconstructable_experiment.py +++ b/scripts/demo_reconstructable_experiment.py @@ -1,20 +1,32 @@ -"""Reconstructable experiment demo — canonical AKTA v0.7 integration chain.""" +"""Reconstructable experiment demo — canonical AKTA v0.8 integration chain.""" from __future__ import annotations import json +import os import shutil import sys from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parent.parent -OUT_DIR = ROOT / "dist" / "reconstructable_experiment" +DEFAULT_OUT_DIR = ROOT / "dist" / "reconstructable_experiment" +CROSS_REPO_OUT_DIR = ROOT / "dist" / "reconstructable_cross_repo" DEMO_TIMESTAMP = "2026-06-28T14:00:00Z" DEMO_DECISION_ID = "AKTA-DEC-RECON0001" DEMO_RECORD_ID = "AKTA-SAR-RECON0001" DEMO_REVIEW_TRIGGER_ID = "AKTA-REVTRIG-RECON0001" +# SCOPE policy authority for single_run_queue_priority (reviewer_roles.yaml). +SCOPE_QUEUE_REVIEWER_ROLE = "lab_operations_lead" +SCOPE_QUEUE_REVIEWER_ID = "lol1" + + +def resolve_out_dir(*, cross_repo: bool | None = None) -> Path: + """Select output directory: cross-repo when live SCOPE env is set or forced.""" + if cross_repo is None: + cross_repo = bool(os.environ.get("SCOPE_REPO_PATH") or os.environ.get("SCOPE_CLI")) + return CROSS_REPO_OUT_DIR if cross_repo else DEFAULT_OUT_DIR def _rehash_record(record: dict[str, Any]) -> dict[str, Any]: @@ -64,13 +76,55 @@ def _write_json(path: Path, payload: dict[str, Any]) -> None: path.write_text(json.dumps(payload, indent=2), encoding="utf-8") +def _summary_contract_checks(summary: dict[str, Any], trigger: dict[str, Any]) -> dict[str, Any]: + """Validate summary.json fields as stable SCOPE integration contract.""" + checks: list[dict[str, Any]] = [] + + def _check(name: str, ok: bool, detail: str = "") -> None: + checks.append({"field": name, "ok": ok, "detail": detail}) + + approved = summary.get("approved_scope") + requested = summary.get("requested_scope") or trigger.get("requested_scope") + _check( + "summary.approved_scope", + bool(approved), + str(approved or ""), + ) + _check( + "summary.requested_scope", + bool(requested), + str(requested or ""), + ) + _check( + "summary.identity_assurance_level", + summary.get("identity_assurance_level") in ("IAL0", "IAL1", "IAL2", "IAL3", "IAL4"), + str(summary.get("identity_assurance_level") or ""), + ) + _check( + "summary.signing_assurance_level", + summary.get("signing_assurance_level") in ("SAL0", "SAL1", "SAL2", "SAL3", "SAL4"), + str(summary.get("signing_assurance_level") or ""), + ) + allowed = summary.get("allowed_tools") + blocked = summary.get("blocked_tools") + _check("summary.allowed_tools", isinstance(allowed, list), f"{len(allowed or [])} tools") + _check("summary.blocked_tools", isinstance(blocked, list), f"{len(blocked or [])} tools") + + return { + "summary_contract": "scope_akta_review_summary.schema.json", + "checks": checks, + "all_ok": all(c["ok"] for c in checks), + } + + def _linkage_report(artifacts: dict[str, Path]) -> dict[str, Any]: links: list[dict[str, str]] = [] decision = json.loads(artifacts["01_akta_decision.json"].read_text(encoding="utf-8")) record = json.loads(artifacts["02_akta_record.json"].read_text(encoding="utf-8")) trigger = json.loads(artifacts["03_review_trigger.json"].read_text(encoding="utf-8")) - grant = json.loads(artifacts["06_scope_grant.json"].read_text(encoding="utf-8")) - manifest = json.loads((artifacts["09_pcs_bundle"] / "manifest.json").read_text(encoding="utf-8")) + summary = json.loads(artifacts["04_scope_review_summary.json"].read_text(encoding="utf-8")) + grant = json.loads(artifacts["07_scope_grant.json"].read_text(encoding="utf-8")) + manifest = json.loads((artifacts["10_pcs_bundle"] / "manifest.json").read_text(encoding="utf-8")) links.append({ "from": "01_akta_decision.json", @@ -85,16 +139,22 @@ def _linkage_report(artifacts: dict[str, Path]) -> dict[str, Any]: "ok": trigger.get("akta_decision_id") == decision["decision_id"], }) links.append({ - "from": "06_scope_grant.json", + "from": "04_scope_review_summary.json", "to": "03_review_trigger.json", "field": "requested_scope", + "ok": summary.get("requested_scope") == trigger.get("requested_scope"), + }) + links.append({ + "from": "07_scope_grant.json", + "to": "04_scope_review_summary.json", + "field": "approved_scope", "ok": ( - (grant.get("source") or {}).get("requested_scope") - or grant.get("requested_scope") - ) == trigger.get("requested_scope"), + (grant.get("authorization") or {}).get("approved_scope") + or grant.get("granted_scope") + ) == summary.get("approved_scope"), }) links.append({ - "from": "09_pcs_bundle/manifest.json", + "from": "10_pcs_bundle/manifest.json", "to": "02_akta_record.json", "field": "record_hash", "ok": manifest.get("record_hash") == record.get("record_hash"), @@ -102,25 +162,27 @@ def _linkage_report(artifacts: dict[str, Path]) -> dict[str, Any]: return {"linkage": links, "all_linked": all(l["ok"] for l in links)} -def run_demo() -> int: +def run_demo(*, cross_repo: bool | None = None) -> int: from akta import AKTAGate, AKTAContext - from akta.records import AKTARecord, AKTADecision + from akta.records import AKTARecord from adapters.labtrust_gym.import_scenario import convert_labtrust_scenario from adapters.pcs.export_artifact import export_pcs_bundle, validate_pcs_bundle from adapters.pcs_bench.runner import AKTABenchScenario, run_suite from adapters.pf_core.export_obligation import build_pf_obligation from adapters.pf_core.import_trace import merge_pf_trace_into_context - from adapters.scope.client import detect_adapter_mode, submit_review_trigger + from adapters.scope.client import ADAPTER_MODE_SIMULATED, detect_adapter_mode, submit_review_trigger from adapters.scientific_memory.import_memory import export_memory_entry, import_from_pcs_bundle from adapters.vsa.import_report import import_vsa_report, validate_vsa_report - if OUT_DIR.exists(): - shutil.rmtree(OUT_DIR) - OUT_DIR.mkdir(parents=True, exist_ok=True) - pcs_dir = OUT_DIR / "09_pcs_bundle" + out_dir = resolve_out_dir(cross_repo=cross_repo) + if out_dir.exists(): + shutil.rmtree(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + pcs_dir = out_dir / "10_pcs_bundle" adapter_mode = detect_adapter_mode() - print("=== AKTA v0.7 Demo: Reconstructable Experiment Chain ===") + print("=== AKTA v0.8 Demo: Reconstructable Experiment Chain ===") + print(f"Output: {out_dir}") print(f"SCOPE adapter: {adapter_mode}\n") vsa_report = json.loads( @@ -144,7 +206,7 @@ def run_demo() -> int: } ] validate_vsa_report(vsa_report) - _write_json(OUT_DIR / "00_vsa_report.json", vsa_report) + _write_json(out_dir / "00_vsa_report.json", vsa_report) context_data = { "evidence_state": "E2_preliminary_signal", @@ -166,7 +228,6 @@ def run_demo() -> int: gate = AKTAGate.from_policy_dir(ROOT / "policy", overlays_dir=ROOT / "overlays") ai_output = {"summary": "Prioritize condition B based on preliminary VSA signal."} - # Case A: weak evidence + P2 + queue priority must not pass silently. case_a = gate.evaluate( ai_output=ai_output, requested_tool="lab_scheduler.prioritize", @@ -193,7 +254,7 @@ def run_demo() -> int: trigger = build_review_trigger( decision_id=case_a_dict["decision_id"], record_id=case_a_dict["decision_id"].replace("DEC", "SAR"), - role=case_a_dict.get("required_review_role") or "lab_manager", + role=SCOPE_QUEUE_REVIEWER_ROLE, action_type=case_a_dict["scientific_action_type"], requested_tool=case_a_dict["requested_tool"], requested_action=case_a_dict["requested_action"], @@ -232,44 +293,46 @@ def run_demo() -> int: }) case_a_dict["review_trigger"] = trigger d = case_a_dict - _write_json(OUT_DIR / "01_akta_decision.json", d) + _write_json(out_dir / "01_akta_decision.json", d) record = case_a.to_record(ai_output=ai_output, context=context_data) record_data = _rehash_record(record.to_dict()) record_data["record_id"] = DEMO_RECORD_ID record_data["timestamp"] = DEMO_TIMESTAMP record_data["review_trigger"] = trigger - _write_json(OUT_DIR / "02_akta_record.json", record_data) + _write_json(out_dir / "02_akta_record.json", record_data) - _write_json(OUT_DIR / "03_review_trigger.json", trigger) + _write_json(out_dir / "03_review_trigger.json", trigger) - # Case B: SCOPE grants single_run_queue_priority (does not override AKTA policy). scope_result = submit_review_trigger( trigger, record=record_data, grant_scope="single_run_queue_priority", - reviewer_id="lab_manager", + reviewer_id=SCOPE_QUEUE_REVIEWER_ID, ) if scope_result.error: print(f"SCOPE adapter error: {scope_result.error}") return 1 + scope_summary = scope_result.summary or {} scope_packet = scope_result.review_packet or {} scope_grant = scope_result.grant or {} scope_decision = scope_result.decision or {} approved_scope = ( - (scope_grant.get("authorization") or {}).get("approved_scope") + scope_summary.get("approved_scope") + or (scope_grant.get("authorization") or {}).get("approved_scope") or scope_grant.get("granted_scope") ) assert approved_scope == "single_run_queue_priority", ( f"Case B expected single_run_queue_priority grant, got {approved_scope}" ) print(f"Case B (SCOPE grant): {approved_scope}") - _write_json(OUT_DIR / "04_scope_packet.json", scope_packet) - _write_json(OUT_DIR / "05_scope_decision.json", scope_decision) - _write_json(OUT_DIR / "06_scope_grant.json", scope_grant) + _write_json(out_dir / "04_scope_review_summary.json", scope_summary) + _write_json(out_dir / "05_scope_packet.json", scope_packet) + _write_json(out_dir / "06_scope_decision.json", scope_decision) + _write_json(out_dir / "07_scope_grant.json", scope_grant) pf_obligation = build_pf_obligation(record_data, decision_id=d["decision_id"]) - _write_json(OUT_DIR / "07_pf_obligation.json", pf_obligation) + _write_json(out_dir / "08_pf_obligation.json", pf_obligation) pf_trace = { "certificate_id": pf_obligation.get("obligation_id", "PF-TRACE-RECON"), @@ -280,7 +343,7 @@ def run_demo() -> int: "enforcement_mode": pf_obligation.get("enforcement_mode"), "trace": {"steps": ["obligation_exported", "trace_bound_to_record"]}, } - _write_json(OUT_DIR / "08_pf_trace_certificate.json", pf_trace) + _write_json(out_dir / "09_pf_trace_certificate.json", pf_trace) context_with_trace = merge_pf_trace_into_context(context_data, pf_obligation) @@ -298,9 +361,8 @@ def run_demo() -> int: trigger=trigger, ) post_grant_decision = regate.to_dict() - _write_json(OUT_DIR / "01_akta_decision_after_grant.json", post_grant_decision) + _write_json(out_dir / "01_akta_decision_after_grant.json", post_grant_decision) - # Case C: grant must not silently override weak-evidence P2 policy. assert post_grant_decision["admissibility"] in ( "blocked", "review_required", @@ -324,6 +386,7 @@ def run_demo() -> int: AKTARecord(record_data), pcs_dir, decision=d, + scope_review_summary=scope_summary, scope_review_packet=scope_packet, scope_decision=scope_decision, scope_grant=scope_grant, @@ -334,7 +397,7 @@ def run_demo() -> int: validate_pcs_bundle(pcs_dir) memory_entry = import_from_pcs_bundle(pcs_dir) - export_memory_entry(memory_entry, OUT_DIR / "10_scientific_memory_import.json") + export_memory_entry(memory_entry, out_dir / "11_scientific_memory_import.json") ltg_scenario = convert_labtrust_scenario({ "scenario_id": "recon_demo_01", @@ -350,27 +413,42 @@ def run_demo() -> int: "deployment_profile": "P2_analysis_assistant", }) bench_result = run_suite([bench], gate) - _write_json(OUT_DIR / "11_pcs_bench_report.json", bench_result) + _write_json(out_dir / "12_pcs_bench_report.json", bench_result) artifact_paths = { - "00_vsa_report.json": OUT_DIR / "00_vsa_report.json", - "01_akta_decision.json": OUT_DIR / "01_akta_decision.json", - "02_akta_record.json": OUT_DIR / "02_akta_record.json", - "03_review_trigger.json": OUT_DIR / "03_review_trigger.json", - "06_scope_grant.json": OUT_DIR / "06_scope_grant.json", - "09_pcs_bundle": pcs_dir, + "01_akta_decision.json": out_dir / "01_akta_decision.json", + "02_akta_record.json": out_dir / "02_akta_record.json", + "03_review_trigger.json": out_dir / "03_review_trigger.json", + "04_scope_review_summary.json": out_dir / "04_scope_review_summary.json", + "07_scope_grant.json": out_dir / "07_scope_grant.json", + "10_pcs_bundle": pcs_dir, } linkage = _linkage_report(artifact_paths) + summary_checks = _summary_contract_checks(scope_summary, trigger) readme = ( - "# Reconstructable Experiment (AKTA v0.7)\n\n" + "# Reconstructable Experiment (AKTA v0.8)\n\n" "Regenerate: `python scripts/demo_reconstructable_experiment.py`\n\n" + f"Output directory: `{out_dir.relative_to(ROOT)}`\n" f"SCOPE adapter mode: {adapter_mode}\n" f"Policy integrity mode: {gate.policy.integrity_mode}\n" ) - (OUT_DIR / "README.md").write_text(readme, encoding="utf-8") + (out_dir / "README.md").write_text(readme, encoding="utf-8") recon_md = "# Reconstruction Report\n\n" + recon_md += "## SCOPE summary.json integration contract\n\n" + recon_md += ( + "`04_scope_review_summary.json` is the stable SCOPE integration contract " + "(schema: `scope_akta_review_summary.schema.json`). AKTA treats summary fields " + "as authoritative for approved scope, assurance levels, and tool lists.\n\n" + ) + recon_md += "### Summary contract checks\n\n" + for check in summary_checks["checks"]: + status = "OK" if check["ok"] else "FAIL" + detail = f" ({check['detail']})" if check.get("detail") else "" + recon_md += f"- {check['field']}: {status}{detail}\n" + recon_md += f"- All summary checks passed: {summary_checks['all_ok']}\n\n" + recon_md += "## SCOPE grant vs AKTA policy layers\n\n" recon_md += ( "SCOPE grants scoped authorization; AKTA re-gate applies grant metadata then " @@ -388,21 +466,29 @@ def run_demo() -> int: for link in linkage["linkage"]: recon_md += f"- {link['from']} -> {link['to']} ({link['field']}): {'OK' if link['ok'] else 'FAIL'}\n" recon_md += f"- PCS-Bench: {bench_result['passed_count']}/{bench_result['total']} passed\n" - (OUT_DIR / "reconstruction_report.md").write_text(recon_md, encoding="utf-8") + if adapter_mode == ADAPTER_MODE_SIMULATED and out_dir == CROSS_REPO_OUT_DIR: + recon_md += "\n**Warning:** cross-repo output dir with simulated SCOPE adapter.\n" + (out_dir / "reconstruction_report.md").write_text(recon_md, encoding="utf-8") print(f"Decision: {d['admissibility']}") if post_grant_decision: print(f"Post-grant: {post_grant_decision['admissibility']}") print(f"VSA claims: {len(vsa_report.get('claims', []))}") print(f"PCS bundle: {pcs_dir / 'manifest.json'}") - print(f"Memory import: {OUT_DIR / '10_scientific_memory_import.json'}") + print(f"Memory import: {out_dir / '11_scientific_memory_import.json'}") print(f"PCS-Bench: {bench_result['passed_count']}/{bench_result['total']} passed") print(f"Linkage: {'OK' if linkage['all_linked'] else 'FAILED'}") - print(f"\nArtifacts in {OUT_DIR}") + print(f"Summary checks: {'OK' if summary_checks['all_ok'] else 'FAILED'}") + print(f"\nArtifacts in {out_dir}") - ok = linkage["all_linked"] and bench_result.get("passed", False) + ok = ( + linkage["all_linked"] + and summary_checks["all_ok"] + and bench_result.get("passed", False) + ) return 0 if ok else 1 if __name__ == "__main__": - sys.exit(run_demo()) + cross_repo = "--cross-repo" in sys.argv + sys.exit(run_demo(cross_repo=cross_repo if cross_repo else None)) diff --git a/scripts/verify_reconstructable_cross_repo.py b/scripts/verify_reconstructable_cross_repo.py new file mode 100644 index 0000000..627d6a4 --- /dev/null +++ b/scripts/verify_reconstructable_cross_repo.py @@ -0,0 +1,144 @@ +"""Verify reconstructable cross-repo demo artifacts (AKTA v0.8 release gate).""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +CROSS_REPO_OUT_DIR = ROOT / "dist" / "reconstructable_cross_repo" + +REQUIRED_ARTIFACTS = [ + "00_vsa_report.json", + "01_akta_decision.json", + "02_akta_record.json", + "03_review_trigger.json", + "04_scope_review_summary.json", + "05_scope_packet.json", + "06_scope_decision.json", + "07_scope_grant.json", + "08_pf_obligation.json", + "09_pf_trace_certificate.json", + "10_pcs_bundle", + "11_scientific_memory_import.json", + "12_pcs_bench_report.json", + "01_akta_decision_after_grant.json", + "README.md", + "reconstruction_report.md", +] + +REQUIRED_PCS_FILES = [ + "akta_decision.json", + "akta_record.json", + "review_trigger.json", + "scope_review_summary.json", + "scope_review_packet.json", + "scope_decision.json", + "scope_grant.json", + "pf_obligation.json", + "vsa_report.json", + "manifest.json", +] + + +def _load_json(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def _find_simulated_markers(out_dir: Path) -> list[str]: + violations: list[str] = [] + for path in out_dir.rglob("*.json"): + try: + data = _load_json(path) + except (json.JSONDecodeError, OSError): + continue + if data.get("adapter_mode") == "simulated": + violations.append(f"{path.relative_to(out_dir)}: adapter_mode=simulated") + return violations + + +def verify(out_dir: Path = CROSS_REPO_OUT_DIR) -> int: + errors: list[str] = [] + + if not out_dir.is_dir(): + errors.append(f"Missing output directory: {out_dir}") + _report(errors) + return 1 + + for name in REQUIRED_ARTIFACTS: + if not (out_dir / name).exists(): + errors.append(f"Missing artifact: {name}") + + simulated = _find_simulated_markers(out_dir) + errors.extend(simulated) + + post_grant_path = out_dir / "01_akta_decision_after_grant.json" + if post_grant_path.is_file(): + post_grant = _load_json(post_grant_path) + admissibility = post_grant.get("admissibility") + if admissibility in ("allowed", "allowed_with_logging", "draft_only"): + errors.append( + f"Post-grant decision must not silently pass; got {admissibility}" + ) + if admissibility not in ("blocked", "review_required", "authorization_required"): + errors.append(f"Unexpected post-grant admissibility: {admissibility}") + else: + errors.append("Missing post-grant decision: 01_akta_decision_after_grant.json") + + pcs_dir = out_dir / "10_pcs_bundle" + if pcs_dir.is_dir(): + for fname in REQUIRED_PCS_FILES: + if not (pcs_dir / fname).exists(): + errors.append(f"PCS bundle missing: {fname}") + + from adapters.pcs.export_artifact import validate_pcs_bundle + + try: + validate_pcs_bundle(pcs_dir) + except ValueError as exc: + errors.append(f"PCS bundle validation failed: {exc}") + else: + errors.append("Missing PCS bundle: 10_pcs_bundle") + + summary_path = out_dir / "04_scope_review_summary.json" + if summary_path.is_file(): + summary = _load_json(summary_path) + for field in ( + "approved_scope", + "requested_scope", + "identity_assurance_level", + "signing_assurance_level", + ): + if not summary.get(field): + errors.append(f"SCOPE summary missing required field: {field}") + if not isinstance(summary.get("allowed_tools"), list): + errors.append("SCOPE summary missing allowed_tools list") + if not isinstance(summary.get("blocked_tools"), list): + errors.append("SCOPE summary missing blocked_tools list") + + recon_path = out_dir / "reconstruction_report.md" + if recon_path.is_file(): + recon = recon_path.read_text(encoding="utf-8") + if "summary.json" not in recon.lower() and "scope_review_summary" not in recon.lower(): + errors.append("reconstruction_report.md must cite summary.json contract") + else: + errors.append("Missing reconstruction_report.md") + + _report(errors, out_dir=out_dir) + return 0 if not errors else 1 + + +def _report(errors: list[str], *, out_dir: Path | None = None) -> None: + if errors: + print("verify-reconstructable-cross-repo: FAILED") + for err in errors: + print(f" - {err}") + else: + target = out_dir or CROSS_REPO_OUT_DIR + print(f"verify-reconstructable-cross-repo: OK ({target})") + + +if __name__ == "__main__": + target = Path(sys.argv[1]) if len(sys.argv) > 1 else CROSS_REPO_OUT_DIR + sys.exit(verify(target)) diff --git a/tests/contracts/test_akta_review_requires_record.py b/tests/contracts/test_akta_review_requires_record.py new file mode 100644 index 0000000..747e6da --- /dev/null +++ b/tests/contracts/test_akta_review_requires_record.py @@ -0,0 +1,101 @@ +"""SCOPE akta-review CLI requires AKTA record (AKTA-1).""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import patch + +import pytest + +from adapters.scope.client import ( + ADAPTER_MODE_AKTA_REVIEW_CLI, + SCOPE_CLI_MODE_AKTA_REVIEW, + submit_review_trigger, +) + + +def test_akta_review_cli_without_record_fails_before_subprocess( + monkeypatch: pytest.MonkeyPatch, +) -> None: + trigger = { + "review_trigger_id": "AKTA-REVTRIG-NO-RECORD", + "requested_scope": "single_run_queue_priority", + } + monkeypatch.setenv("SCOPE_CLI", "scope") + monkeypatch.setenv("SCOPE_CLI_MODE", SCOPE_CLI_MODE_AKTA_REVIEW) + monkeypatch.delenv("SCOPE_REPO_PATH", raising=False) + + with patch("adapters.scope.client.subprocess.run") as mock_run: + result = submit_review_trigger(trigger, grant_scope="single_run_queue_priority") + + mock_run.assert_not_called() + assert result.adapter_mode == ADAPTER_MODE_AKTA_REVIEW_CLI + assert result.error == "SCOPE akta-review CLI mode requires an AKTA record." + + +def test_akta_review_cli_with_record_succeeds(monkeypatch: pytest.MonkeyPatch) -> None: + from pathlib import Path + + trigger = { + "review_trigger_id": "AKTA-REVTRIG-WITH-RECORD", + "requested_scope": "single_run_queue_priority", + "required_review_role": "lab_manager", + } + record = {"record_id": "AKTA-SAR-WITH-RECORD", "review_trigger": trigger} + monkeypatch.setenv("SCOPE_CLI", "scope") + monkeypatch.setenv("SCOPE_CLI_MODE", SCOPE_CLI_MODE_AKTA_REVIEW) + monkeypatch.delenv("SCOPE_REPO_PATH", raising=False) + + def fake_run(cmd, **kwargs): + proc = type("Proc", (), {"returncode": 0, "stdout": "", "stderr": ""})() + if cmd[1:3] == ["akta", "review"]: + out_dir = Path(cmd[cmd.index("--out-dir") + 1]) + out_dir.mkdir(parents=True, exist_ok=True) + packet_path = out_dir / "scope_review_packet.json" + decision_path = out_dir / "scope_decision.json" + grant_path = out_dir / "scope_grant.json" + packet_path.write_text( + '{"packet_id": "SCOPE-PKT-REC", "review_request": {"requested_scope": "single_run_queue_priority"}}', + encoding="utf-8", + ) + decision_path.write_text( + '{"decision_id": "SCOPE-DEC-REC", "decision": {"type": "approve", "approved_scope": "single_run_queue_priority"}}', + encoding="utf-8", + ) + grant_path.write_text( + '{"grant_id": "SCOPE-GRANT-REC", "authorization": {"approved_scope": "single_run_queue_priority"}, "source": {"requested_scope": "single_run_queue_priority"}}', + encoding="utf-8", + ) + summary: dict[str, Any] = { + "status": "completed", + "packet_path": str(packet_path), + "decision_path": str(decision_path), + "grant_path": str(grant_path), + "approved_scope": "single_run_queue_priority", + "requested_scope": "single_run_queue_priority", + "adapter_contract_version": "scope-akta-review-v0.8", + "identity_assurance_level": "IAL0", + "signing_assurance_level": "SAL1", + "scope_trust_root_hash": "sha256:" + ("a" * 64), + "allowed_tools": [], + "blocked_tools": [], + } + (out_dir / "summary.json").write_text( + __import__("json").dumps(summary, indent=2), + encoding="utf-8", + ) + return proc + + with patch("adapters.scope.client.subprocess.run", side_effect=fake_run) as mock_run: + result = submit_review_trigger( + trigger, + record=record, + grant_scope="single_run_queue_priority", + reviewer_id="lab_manager", + ) + + assert mock_run.call_count == 1 + assert result.error is None + assert result.grant is not None + assert result.summary is not None + assert result.summary["approved_scope"] == "single_run_queue_priority" diff --git a/tests/contracts/test_pcs_full_chain_tamper.py b/tests/contracts/test_pcs_full_chain_tamper.py index ffca0d8..b5e2756 100644 --- a/tests/contracts/test_pcs_full_chain_tamper.py +++ b/tests/contracts/test_pcs_full_chain_tamper.py @@ -36,6 +36,15 @@ def _full_chain_bundle(tmp_path: Path) -> Path: AKTARecord(record), out, decision=d, + scope_review_summary={ + "status": "completed", + "approved_scope": "protocol_draft", + "requested_scope": "active_protocol_update", + "identity_assurance_level": "IAL0", + "signing_assurance_level": "SAL1", + "allowed_tools": [], + "blocked_tools": [], + }, scope_review_packet={"packet_type": "scope_review_packet"}, scope_decision={"status": "granted", "granted_scope": "protocol_draft"}, scope_grant={ @@ -54,7 +63,8 @@ def test_pcs_full_chain_fixture_validates() -> None: validate_pcs_bundle(bundle) manifest = json.loads((bundle / "manifest.json").read_text(encoding="utf-8")) assert manifest["schema_version"] == "akta-record-v0.5" - assert len(manifest["file_hashes"]) == 10 + assert len(manifest["file_hashes"]) == 11 + assert "scope_review_summary.json" in manifest.get("files", []) @pytest.mark.parametrize( @@ -66,6 +76,7 @@ def test_pcs_full_chain_fixture_validates() -> None: "domain_overlay_hash.txt", "tool_registry_hash.txt", "review_trigger.json", + "scope_review_summary.json", "scope_review_packet.json", "scope_decision.json", "scope_grant.json", diff --git a/tests/contracts/test_scope_akta_review_cli.py b/tests/contracts/test_scope_akta_review_cli.py index 525ce77..35097d0 100644 --- a/tests/contracts/test_scope_akta_review_cli.py +++ b/tests/contracts/test_scope_akta_review_cli.py @@ -126,6 +126,7 @@ def test_scope_akta_review_cli_validates_summary(monkeypatch: pytest.MonkeyPatch "review_trigger_id": "AKTA-REVTRIG-AKTA-REVIEW02", "requested_scope": "single_run_queue_priority", } + record = {"record_id": "AKTA-SAR-AKTA-REVIEW02", "review_trigger": trigger} monkeypatch.setenv("SCOPE_CLI", "scope") monkeypatch.setenv("SCOPE_CLI_MODE", SCOPE_CLI_MODE_AKTA_REVIEW) monkeypatch.delenv("SCOPE_REPO_PATH", raising=False) @@ -142,7 +143,11 @@ def bad_summary_run(cmd, **kwargs): return proc with patch("adapters.scope.client.subprocess.run", side_effect=bad_summary_run): - result = submit_review_trigger(trigger, grant_scope="single_run_queue_priority") + result = submit_review_trigger( + trigger, + record=record, + grant_scope="single_run_queue_priority", + ) assert result.adapter_mode == ADAPTER_MODE_AKTA_REVIEW_CLI assert result.error is not None diff --git a/tests/contracts/test_scope_narrowing_fixture.py b/tests/contracts/test_scope_narrowing_fixture.py new file mode 100644 index 0000000..8a8e602 --- /dev/null +++ b/tests/contracts/test_scope_narrowing_fixture.py @@ -0,0 +1,86 @@ +"""Fixture-driven SCOPE narrowing contract tests (AKTA-3).""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from akta.scope_contract import ( + is_valid_narrowing_grant, + load_valid_narrowing_pairs, + scope_rank, + validate_approval_grant, +) +from adapters.pcs.export_artifact import export_pcs_bundle +from akta import AKTAGate, AKTAContext +from akta.records import AKTARecord + +ROOT = Path(__file__).resolve().parent.parent.parent +FIXTURES = Path(__file__).resolve().parent.parent / "fixtures" + + +def test_fixture_narrowing_pairs_all_validate() -> None: + raw = json.loads((FIXTURES / "scope_valid_narrowing.json").read_text(encoding="utf-8")) + for entry in raw["valid_narrowing_pairs"]: + requested = entry["requested_scope"] + granted = entry["granted_scope"] + result = validate_approval_grant( + granted_scope=granted, + requested_scope=requested, + ) + assert result["narrow_grant"] is True + assert is_valid_narrowing_grant(granted_scope=granted, requested_scope=requested) + + +def test_same_rank_unlisted_pair_blocked() -> None: + with pytest.raises(ValueError, match="does not cover"): + validate_approval_grant( + granted_scope="single_validation_run_draft", + requested_scope="single_validation_plan", + ) + + +def test_overbroad_grant_fails_pcs_export(tmp_path: Path) -> None: + gate = AKTAGate.from_policy_dir(ROOT / "policy", overlays_dir=ROOT / "overlays") + decision = gate.evaluate( + ai_output={"summary": "Update active protocol threshold."}, + requested_tool="protocol_editor.update_active_protocol", + requested_action="update_threshold", + context=AKTAContext.from_dict({ + "evidence_state": "E4_internally_consistent_evidence", + "validation_status": "V3_preliminary_experimental_support", + }), + deployment_profile="P4_protocol_drafting_assistant", + domain_overlay="generic_lab_v0", + ) + d = decision.to_dict() + record = decision.to_record().to_dict() + record["review_trigger"] = d["review_trigger"] + overbroad_grant = { + "grant_id": "SCOPE-GRANT-OVERBROAD", + "authorization": {"approved_scope": "robot_queue_submission"}, + "source": {"requested_scope": "protocol_draft"}, + } + with pytest.raises(ValueError, match="Invalid SCOPE grant"): + export_pcs_bundle( + AKTARecord(record), + tmp_path, + decision=d, + scope_grant=overbroad_grant, + validate=True, + ) + + +def test_scope_order_fixture_is_monotonic() -> None: + order = json.loads((FIXTURES / "scope_scope_order.json").read_text(encoding="utf-8"))["scope_order"] + ranks = [scope_rank(s) for s in order] + assert ranks == sorted(ranks) + assert len(set(ranks)) == len(ranks) + + +def test_narrowing_pairs_subset_of_loaded_fixture() -> None: + pairs = load_valid_narrowing_pairs() + assert ("active_protocol_update", "protocol_draft") in pairs + assert len(pairs) >= 1 diff --git a/tests/fixtures/scope_scope_order.json b/tests/fixtures/scope_scope_order.json new file mode 100644 index 0000000..13f37b6 --- /dev/null +++ b/tests/fixtures/scope_scope_order.json @@ -0,0 +1,14 @@ +{ + "fixture_version": "akta-scope-contract-v0.8", + "scope_order": [ + "protocol_draft", + "active_protocol_update", + "single_validation_plan", + "single_validation_run_draft", + "single_run_queue_priority", + "scientific_memory_import", + "robot_queue_submission", + "execution_payload_preparation", + "publication_claim" + ] +} diff --git a/tests/fixtures/scope_valid_narrowing.json b/tests/fixtures/scope_valid_narrowing.json new file mode 100644 index 0000000..94dd745 --- /dev/null +++ b/tests/fixtures/scope_valid_narrowing.json @@ -0,0 +1,25 @@ +{ + "fixture_version": "akta-scope-contract-v0.8", + "valid_narrowing_pairs": [ + { + "requested_scope": "active_protocol_update", + "granted_scope": "protocol_draft", + "rationale": "Protocol owner may approve draft-only changes instead of active update" + }, + { + "requested_scope": "single_validation_plan", + "granted_scope": "protocol_draft", + "rationale": "Validation plan request narrowed to protocol draft only" + }, + { + "requested_scope": "single_run_queue_priority", + "granted_scope": "single_validation_plan", + "rationale": "Queue priority narrowed to validation planning" + }, + { + "requested_scope": "robot_queue_submission", + "granted_scope": "single_run_queue_priority", + "rationale": "Robot submission narrowed to queue prioritization only" + } + ] +} diff --git a/tests/integration/test_reconstructable_experiment_demo.py b/tests/integration/test_reconstructable_experiment_demo.py index 76dba38..c452073 100644 --- a/tests/integration/test_reconstructable_experiment_demo.py +++ b/tests/integration/test_reconstructable_experiment_demo.py @@ -1,4 +1,4 @@ -"""Integration test for reconstructable experiment demo (v0.7).""" +"""Integration test for reconstructable experiment demo (v0.8).""" from __future__ import annotations @@ -9,21 +9,21 @@ import pytest ROOT = Path(__file__).resolve().parent.parent.parent -OUT_DIR = ROOT / "dist" / "reconstructable_experiment" EXPECTED_ARTIFACTS = [ "00_vsa_report.json", "01_akta_decision.json", "02_akta_record.json", "03_review_trigger.json", - "04_scope_packet.json", - "05_scope_decision.json", - "06_scope_grant.json", - "07_pf_obligation.json", - "08_pf_trace_certificate.json", - "09_pcs_bundle", - "10_scientific_memory_import.json", - "11_pcs_bench_report.json", + "04_scope_review_summary.json", + "05_scope_packet.json", + "06_scope_decision.json", + "07_scope_grant.json", + "08_pf_obligation.json", + "09_pf_trace_certificate.json", + "10_pcs_bundle", + "11_scientific_memory_import.json", + "12_pcs_bench_report.json", "01_akta_decision_after_grant.json", "README.md", "reconstruction_report.md", @@ -31,26 +31,30 @@ @pytest.fixture -def clean_out_dir() -> None: - if OUT_DIR.exists(): - shutil.rmtree(OUT_DIR) +def clean_out_dir(monkeypatch: pytest.MonkeyPatch) -> Path: + from scripts.demo_reconstructable_experiment import DEFAULT_OUT_DIR - -def test_reconstructable_experiment_demo_generates_chain(clean_out_dir: None, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("SCOPE_REPO_PATH", raising=False) monkeypatch.delenv("SCOPE_CLI", raising=False) + if DEFAULT_OUT_DIR.exists(): + shutil.rmtree(DEFAULT_OUT_DIR) + return DEFAULT_OUT_DIR + +def test_reconstructable_experiment_demo_generates_chain( + clean_out_dir: Path, +) -> None: from scripts.demo_reconstructable_experiment import run_demo - code = run_demo() + code = run_demo(cross_repo=False) assert code == 0 for name in EXPECTED_ARTIFACTS: - path = OUT_DIR / name + path = clean_out_dir / name assert path.exists(), f"Missing artifact: {name}" - manifest = json.loads((OUT_DIR / "09_pcs_bundle" / "manifest.json").read_text(encoding="utf-8")) - record = json.loads((OUT_DIR / "02_akta_record.json").read_text(encoding="utf-8")) + manifest = json.loads((clean_out_dir / "10_pcs_bundle" / "manifest.json").read_text(encoding="utf-8")) + record = json.loads((clean_out_dir / "02_akta_record.json").read_text(encoding="utf-8")) assert manifest.get("record_hash") == record.get("record_hash") assert manifest.get("integrity_mode") in ( None, @@ -58,13 +62,15 @@ def test_reconstructable_experiment_demo_generates_chain(clean_out_dir: None, mo "deployment_hmac_attested", "release_ed25519_signed", ) + assert "scope_review_summary.json" in manifest.get("files", []) - recon = (OUT_DIR / "reconstruction_report.md").read_text(encoding="utf-8") + recon = (clean_out_dir / "reconstruction_report.md").read_text(encoding="utf-8") assert "linkage" in recon.lower() or "Linkage" in recon assert "Case C" in recon or "post-grant" in recon.lower() + assert "summary" in recon.lower() post_grant = json.loads( - (OUT_DIR / "01_akta_decision_after_grant.json").read_text(encoding="utf-8") + (clean_out_dir / "01_akta_decision_after_grant.json").read_text(encoding="utf-8") ) assert post_grant["admissibility"] in ("blocked", "review_required", "authorization_required") assert post_grant["admissibility"] not in ("allowed", "allowed_with_logging", "draft_only") diff --git a/tests/integration/test_reconstructable_summary_artifact.py b/tests/integration/test_reconstructable_summary_artifact.py new file mode 100644 index 0000000..402c4f1 --- /dev/null +++ b/tests/integration/test_reconstructable_summary_artifact.py @@ -0,0 +1,57 @@ +"""Integration test for reconstructable experiment summary artifact (v0.8).""" + +from __future__ import annotations + +import json +import shutil +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parent.parent.parent + + +@pytest.fixture +def clean_out_dir(monkeypatch: pytest.MonkeyPatch) -> Path: + from scripts.demo_reconstructable_experiment import DEFAULT_OUT_DIR + + monkeypatch.delenv("SCOPE_REPO_PATH", raising=False) + monkeypatch.delenv("SCOPE_CLI", raising=False) + if DEFAULT_OUT_DIR.exists(): + shutil.rmtree(DEFAULT_OUT_DIR) + return DEFAULT_OUT_DIR + + +def test_reconstructable_summary_artifact(clean_out_dir: Path) -> None: + from scripts.demo_reconstructable_experiment import run_demo + + code = run_demo(cross_repo=False) + assert code == 0 + + summary_path = clean_out_dir / "04_scope_review_summary.json" + assert summary_path.is_file(), "Missing first-class SCOPE summary artifact" + + summary = json.loads(summary_path.read_text(encoding="utf-8")) + assert summary.get("approved_scope") == "single_run_queue_priority" + assert summary.get("requested_scope") == "single_run_queue_priority" + assert summary.get("identity_assurance_level") in ("IAL0", "IAL1", "IAL2", "IAL3", "IAL4") + assert summary.get("signing_assurance_level") in ("SAL0", "SAL1", "SAL2", "SAL3", "SAL4") + assert isinstance(summary.get("allowed_tools"), list) + assert isinstance(summary.get("blocked_tools"), list) + + pcs_summary = json.loads( + (clean_out_dir / "10_pcs_bundle" / "scope_review_summary.json").read_text(encoding="utf-8") + ) + assert pcs_summary["approved_scope"] == summary["approved_scope"] + + recon = (clean_out_dir / "reconstruction_report.md").read_text(encoding="utf-8") + assert "summary.json" in recon.lower() or "scope_review_summary" in recon.lower() + assert "summary.approved_scope" in recon + assert "summary.requested_scope" in recon + assert "identity_assurance_level" in recon + assert "signing_assurance_level" in recon + assert "allowed_tools" in recon + assert "blocked_tools" in recon + + manifest = json.loads((clean_out_dir / "10_pcs_bundle" / "manifest.json").read_text(encoding="utf-8")) + assert "scope_review_summary.json" in manifest.get("files", []) diff --git a/tests/test_rest_api.py b/tests/test_rest_api.py index cbb2139..9ffdd3a 100644 --- a/tests/test_rest_api.py +++ b/tests/test_rest_api.py @@ -49,7 +49,7 @@ def test_health(server_url: str) -> None: data = _get(server_url, "/v0/health") assert data["status"] == "ok" assert data["api_version"] == "v0.6" - assert data["version"] == "0.7.1" + assert data["version"] == "0.8.0" def test_policy(server_url: str) -> None: