Document system flow and next execution baseline
This commit is contained in:
50
scripts/test_owner_decision_intake.py
Normal file
50
scripts/test_owner_decision_intake.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[0]))
|
||||
from validate_owner_decision_intake import BLOCKERS, validate
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def complete_input(context_id):
|
||||
return {"schema": "owner-decision-intake.v1", "context_id": context_id, "decision_set_id": "test", "decisions": [{"blocker": blocker, "decision": "approve", "owner": "owner", "reviewer": "reviewer", "rationale": "reviewed", "evidence": [{"path": "context-lock.json", "sha256": "0" * 64}], "new_context_required": True} for blocker in BLOCKERS]}
|
||||
|
||||
|
||||
class OwnerDecisionIntakeTests(unittest.TestCase):
|
||||
def test_missing_input_fails_closed(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"}))
|
||||
report = validate(root, Path("owner-decisions.json"), Path("report.json"))
|
||||
self.assertEqual(report["status"], "blocked")
|
||||
self.assertEqual(report["checks"]["gate_refresh_allowed"]["status"], "blocked")
|
||||
self.assertEqual(report["checks"]["context_lock_unchanged"]["status"], "verified")
|
||||
|
||||
def test_complete_shape_is_valid_but_does_not_refresh_gate(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"}))
|
||||
(root / "owner-decisions.json").write_text(json.dumps(complete_input("ctx")))
|
||||
report = validate(root, Path("owner-decisions.json"), Path("report.json"))
|
||||
self.assertEqual(report["status"], "verified")
|
||||
self.assertEqual(report["checks"]["decision_approval"]["status"], "verified")
|
||||
self.assertEqual(report["checks"]["gate_refresh_allowed"]["status"], "blocked")
|
||||
|
||||
def test_duplicate_blocker_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"}))
|
||||
value = complete_input("ctx")
|
||||
value["decisions"][-1]["blocker"] = BLOCKERS[0]
|
||||
(root / "owner-decisions.json").write_text(json.dumps(value))
|
||||
report = validate(root, Path("owner-decisions.json"), Path("report.json"))
|
||||
self.assertEqual(report["status"], "blocked")
|
||||
self.assertIn(BLOCKERS[5], report["checks"]["six_blocker_identity"]["missing"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
114
scripts/validate_owner_decision_intake.py
Normal file
114
scripts/validate_owner_decision_intake.py
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate the external six-decision owner input without mutating release state."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
LOCK = Path("context-lock.json")
|
||||
INPUT = Path("coverage/owner-decisions.json")
|
||||
OUTPUT = Path("coverage/owner-decision-intake.report.json")
|
||||
TEMPLATE = Path("coverage/owner-decision-intake.template.json")
|
||||
BLOCKERS = ("manifest_seed_unassigned", "accepted_entries_missing", "official_oracle_missing", "configuration_resources_missing", "browser_matrix_unverified", "license_closure_incomplete")
|
||||
DECISIONS = {"approve", "reject", "defer"}
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def digest(value: Any) -> str:
|
||||
return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def load_json(path: Path) -> tuple[dict[str, Any], str | None]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict): raise ValueError("root must be object")
|
||||
return value, None
|
||||
except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc:
|
||||
return {}, str(exc)
|
||||
|
||||
|
||||
def validate(root: Path, input_path: Path = INPUT, output_path: Path = OUTPUT) -> dict[str, Any]:
|
||||
root = root.resolve()
|
||||
lock, lock_error = load_json(root / LOCK)
|
||||
value, input_error = load_json(root / input_path)
|
||||
input_present = (root / input_path).is_file()
|
||||
errors = [error for error in (lock_error, input_error) if error]
|
||||
decisions = value.get("decisions") if isinstance(value.get("decisions"), list) else []
|
||||
observed = [row.get("blocker") for row in decisions if isinstance(row, dict)]
|
||||
duplicate_ids = sorted({blocker for blocker in observed if observed.count(blocker) > 1 and blocker is not None})
|
||||
missing_ids = sorted(set(BLOCKERS) - set(observed))
|
||||
unknown_ids = sorted(set(observed) - set(BLOCKERS))
|
||||
row_errors: list[dict[str, Any]] = []
|
||||
for index, row in enumerate(decisions):
|
||||
if not isinstance(row, dict):
|
||||
row_errors.append({"index": index, "error": "decision_not_object"}); continue
|
||||
if row.get("blocker") not in BLOCKERS: row_errors.append({"index": index, "error": "unknown_blocker"})
|
||||
if row.get("decision") not in DECISIONS: row_errors.append({"index": index, "error": "decision_invalid"})
|
||||
for field in ("owner", "reviewer", "rationale"):
|
||||
if not isinstance(row.get(field), str) or not row[field].strip(): row_errors.append({"index": index, "error": f"{field}_missing"})
|
||||
if not isinstance(row.get("new_context_required"), bool): row_errors.append({"index": index, "error": "new_context_required_invalid"})
|
||||
evidence = row.get("evidence")
|
||||
if not isinstance(evidence, list) or not evidence: row_errors.append({"index": index, "error": "evidence_missing"})
|
||||
else:
|
||||
for evidence_row in evidence:
|
||||
if not isinstance(evidence_row, dict) or not isinstance(evidence_row.get("path"), str) or not isinstance(evidence_row.get("sha256"), str) or len(evidence_row["sha256"]) != 64:
|
||||
row_errors.append({"index": index, "error": "evidence_invalid"})
|
||||
context_ok = bool(lock.get("context_id")) and value.get("context_id") == lock.get("context_id") if input_present else False
|
||||
shape_ok = input_present and not errors and value.get("schema") == "owner-decision-intake.v1" and len(decisions) == 6 and not duplicate_ids and not missing_ids and not unknown_ids and not row_errors and context_ok
|
||||
all_approved = shape_ok and all(row.get("decision") == "approve" for row in decisions)
|
||||
checks = {
|
||||
"input_file_present": {"status": "verified" if input_present else "blocked", "path": input_path.as_posix()},
|
||||
"schema_shape": {"status": "verified" if shape_ok else "blocked", "schema": value.get("schema")},
|
||||
"six_blocker_identity": {"status": "verified" if not missing_ids and not unknown_ids and not duplicate_ids and len(decisions) == 6 else "blocked", "missing": missing_ids, "unknown": unknown_ids, "duplicates": duplicate_ids},
|
||||
"context_consistency": {"status": "verified" if context_ok else "blocked", "locked": lock.get("context_id"), "input": value.get("context_id")},
|
||||
"decision_approval": {"status": "verified" if all_approved else "blocked", "approved": sum(row.get("decision") == "approve" for row in decisions if isinstance(row, dict)), "required": 6},
|
||||
"gate_refresh_allowed": {"status": "blocked", "reason": "owner intake validation never refreshes the gate; reviewed new context is required after all six approvals"},
|
||||
"context_lock_unchanged": {"status": "verified", "action": "no mutation"},
|
||||
}
|
||||
report: dict[str, Any] = {
|
||||
"schema": "owner-decision-intake-report.v1",
|
||||
"context_id": lock.get("context_id"),
|
||||
"task_id": "K8-REL-18",
|
||||
"status": "verified" if shape_ok else "blocked",
|
||||
"implementation_status": "verified",
|
||||
"release_status": "blocked_external_owner",
|
||||
"candidate_eligible": False,
|
||||
"release_action": "STOP_RELEASE",
|
||||
"input": {"path": input_path.as_posix(), "status": "present" if input_present else "missing", "sha256": sha256(root / input_path) if input_present else None},
|
||||
"template": {"path": TEMPLATE.as_posix(), "sha256": sha256(root / TEMPLATE) if (root / TEMPLATE).is_file() else None},
|
||||
"checks": checks,
|
||||
"errors": errors + row_errors,
|
||||
"next_transition": "wait for owner-decisions.json or new locked context input; do not rerun browser or implementation matrices",
|
||||
"report_sha256": None,
|
||||
}
|
||||
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
|
||||
destination = output_path if output_path.is_absolute() else root / output_path
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
||||
return report
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
|
||||
parser.add_argument("--input", type=Path, default=INPUT)
|
||||
parser.add_argument("--output", type=Path, default=OUTPUT)
|
||||
args = parser.parse_args()
|
||||
report = validate(args.repo_root.resolve(), args.input, args.output)
|
||||
print(f"owner_decision_intake={report['status']} present={report['input']['status']} refresh={report['checks']['gate_refresh_allowed']['status']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user