feat(review): add multi-owner contracts and receipts
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 25s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
tegwick 2026-08-22 20:57:51 +02:00
parent 52aefe39e4
commit 598f6418e7
13 changed files with 1401 additions and 9 deletions

View file

@ -18,6 +18,7 @@ import re
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
@ -571,6 +572,73 @@ def cmd_status(_args: argparse.Namespace) -> None:
print(f" [{deadline}] {d['title']}")
def _load_json_file(path: str) -> dict:
target = Path(path)
try:
value = json.loads(target.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print(f"ERROR: invalid JSON file {target}: {exc}")
sys.exit(2)
if not isinstance(value, dict):
print(f"ERROR: {target} must contain a JSON object")
sys.exit(2)
return value
def _git_source(path: str, repo_slug: str | None = None) -> tuple[Path, dict]:
target = Path(path).resolve()
try:
root = Path(subprocess.check_output(
["git", "-C", str(target.parent), "rev-parse", "--show-toplevel"],
text=True,
).strip())
revision = subprocess.check_output(
["git", "-C", str(root), "rev-parse", "HEAD"], text=True
).strip()
relative = target.relative_to(root).as_posix()
except (subprocess.CalledProcessError, ValueError) as exc:
print(f"ERROR: {target} must be inside a Git repository: {exc}")
sys.exit(2)
return target, {"repo": repo_slug or root.name, "path": relative, "revision": revision}
def cmd_review_project(args: argparse.Namespace) -> None:
"""Project a file-authoritative contract wrapper through the direct API."""
target, source = _git_source(args.file, args.source_repo)
contract = _load_json_file(str(target))
payload = contract if "contract" in contract and "source" in contract else {
"contract": contract,
"source": source,
"decision_id": args.decision_id,
"workplan_id": args.workplan_id,
"task_id": args.task_id,
"required_for_decision": args.required_for_decision,
}
print(json.dumps(
_api_post("/review-contracts/projections", payload),
indent=2,
))
def cmd_review_submit(args: argparse.Namespace) -> None:
"""Submit a file-authoritative receipt against an active contract."""
key = urllib.parse.quote(args.contract_key, safe="")
target, source = _git_source(args.file, args.source_repo)
receipt = _load_json_file(str(target))
if "source" not in receipt:
receipt["source"] = source
print(json.dumps(
_api_post(f"/review-contracts/{key}/receipts", receipt),
indent=2,
))
def cmd_review_status(args: argparse.Namespace) -> None:
"""Print the derived owner and gate matrix; never an execution authorization."""
key = urllib.parse.quote(args.contract_key, safe="")
print(json.dumps(_api_get(f"/review-contracts/{key}/aggregate"), indent=2))
def _outbox_store(args):
from api.edge.outbox import OutboxStore, default_outbox_path
@ -816,6 +884,26 @@ def main() -> None:
# status
sub.add_parser("status", help="Show State Hub health and summary totals")
# review — file-backed multi-owner review projection
review = sub.add_parser("review", help="Project and inspect multi-owner review evidence")
review_sub = review.add_subparsers(dest="review_command", required=True)
review_project = review_sub.add_parser("project", help="Project an authoritative contract wrapper")
review_project.add_argument("file")
review_project.add_argument("--source-repo", default=None)
review_project.add_argument("--decision-id", default=None)
review_project.add_argument("--workplan-id", default=None)
review_project.add_argument("--task-id", default=None)
review_project.add_argument("--required-for-decision", action="store_true")
review_project.set_defaults(func=cmd_review_project)
review_submit = review_sub.add_parser("submit", help="Submit an authoritative receipt file")
review_submit.add_argument("contract_key")
review_submit.add_argument("file")
review_submit.add_argument("--source-repo", default=None)
review_submit.set_defaults(func=cmd_review_submit)
review_status = review_sub.add_parser("status", help="Print owner and gate status")
review_status.add_argument("contract_key")
review_status.set_defaults(func=cmd_review_status)
# dev up — files-first local hub (CUST-WP-0054-T07)
dev = sub.add_parser("dev", help="Local dev-hub commands")
dev_sub = dev.add_subparsers(dest="dev_command", required=True)