diff --git a/tests/fixtures/bounded-action-loop/valid.blocked-intervention.json b/tests/fixtures/bounded-action-loop/valid.blocked-intervention.json new file mode 100644 index 00000000..0c3b8269 --- /dev/null +++ b/tests/fixtures/bounded-action-loop/valid.blocked-intervention.json @@ -0,0 +1,30 @@ +{ + "schema_version": "0.1", + "kind": "bounded_action_loop", + "loop_id": "loop-blocked-intervention-001", + "action_proposal": { + "proposal_id": "proposal-blocked-intervention-001", + "action_type": "emit_audit_packet", + "risk_class": "high", + "evidence_refs": [ + "evidence://agentplane/run/blocked-intervention-001/anomaly-signal" + ], + "requested_result": "emit audit packet for high-risk anomalous action" + }, + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-block-high-risk-001", + "runtime_trace": { + "trace_id": "trace-blocked-intervention-001", + "proposal_ref": "proposal-blocked-intervention-001", + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-block-high-risk-001", + "trace_status": "blocked", + "no_external_side_effects": true, + "audit_ref": "SocioProphet/model-governance-ledger#20:audit-block-001" + }, + "outcome_record": { + "outcome_id": "outcome-blocked-intervention-001", + "proposal_ref": "proposal-blocked-intervention-001", + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-block-high-risk-001", + "result": "blocked", + "audit_ref": "SocioProphet/model-governance-ledger#20:audit-block-001" + } +} diff --git a/tests/fixtures/bounded-action-loop/valid.modified-action.json b/tests/fixtures/bounded-action-loop/valid.modified-action.json new file mode 100644 index 00000000..a5f3e716 --- /dev/null +++ b/tests/fixtures/bounded-action-loop/valid.modified-action.json @@ -0,0 +1,40 @@ +{ + "schema_version": "0.1", + "kind": "bounded_action_loop", + "loop_id": "loop-modified-action-001", + "action_proposal": { + "proposal_id": "proposal-modified-action-001", + "action_type": "record_diagnostic_finding", + "risk_class": "moderate", + "evidence_refs": [ + "evidence://agentplane/run/modified-action-001/diagnostic-signal", + "evidence://agentplane/run/modified-action-001/policy-constraint" + ], + "requested_result": "record diagnostic finding with scope modification applied" + }, + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-modify-scope-001", + "runtime_trace": { + "trace_id": "trace-modified-action-001", + "proposal_ref": "proposal-modified-action-001", + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-modify-scope-001", + "trace_status": "modified", + "no_external_side_effects": true, + "audit_ref": "SocioProphet/model-governance-ledger#20:audit-modified-001" + }, + "outcome_record": { + "outcome_id": "outcome-modified-action-001", + "proposal_ref": "proposal-modified-action-001", + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-modify-scope-001", + "result": "modified", + "audit_ref": "SocioProphet/model-governance-ledger#20:audit-modified-001" + }, + "trait_baseline": { + "baseline_id": "baseline-modified-observational-001", + "observational_only": true + }, + "trait_drift_metric": { + "metric_id": "trait-drift-modified-001", + "observational_only": true, + "value": 0.15 + } +} diff --git a/tools/check_bounded_action_loop.py b/tools/check_bounded_action_loop.py index 6a8b2c9c..da59ff9f 100644 --- a/tools/check_bounded_action_loop.py +++ b/tools/check_bounded_action_loop.py @@ -13,7 +13,8 @@ ROOT = Path(__file__).resolve().parents[1] SCHEMA = ROOT / "schemas" / "bounded-action-loop.v0.schema.json" FIXTURES = ROOT / "tests" / "fixtures" / "bounded-action-loop" -VALID = FIXTURES / "valid.record-event-instance.json" + +INTERVENTION_STATUSES = {"blocked", "modified", "escalated"} def load_json(path: Path) -> dict[str, Any]: @@ -41,6 +42,12 @@ def check_data(data: dict[str, Any]) -> None: raise ValueError("proposal evidence refs required") if proposal["risk_class"] != "low" and trace["trace_status"] == "recorded": raise ValueError("recorded trace requires low risk in v0") + # Runtime trace must always be present and consistent with outcome + if trace["trace_status"] != outcome["result"]: + raise ValueError("trace_status and outcome result must match") + # Intervention outcomes (blocked/modified/escalated) must have audit_ref + if outcome["result"] in INTERVENTION_STATUSES and not outcome.get("audit_ref"): + raise ValueError(f"intervention outcome result={outcome['result']} requires audit_ref") def validate_file(path: Path, schema: dict[str, Any]) -> None: @@ -51,7 +58,11 @@ def validate_file(path: Path, schema: dict[str, Any]) -> None: def main() -> int: schema = load_json(SCHEMA) - validate_file(VALID, schema) + valids = sorted(FIXTURES.glob("valid.*.json")) + if not valids: + raise SystemExit("missing valid bounded-action-loop fixtures") + for path in valids: + validate_file(path, schema) invalids = sorted(FIXTURES.glob("invalid.*.json")) if not invalids: raise SystemExit("missing invalid bounded-action-loop fixtures")