GuestOps/deploy/automation_acceptance.py
wolf-demon a3ef408de6
Some checks failed
Build and verify web migration / verify (push) Has been cancelled
Prepare 0.2.0 Gate B pilot candidate
2026-09-29 21:17:40 +01:00

111 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Validate restricted Gate B knowledge, AI and FAQ acceptance evidence."""
from __future__ import annotations
import argparse
import datetime as dt
import json
from pathlib import Path
import re
from urllib.parse import urlparse
SCENARIOS = {
"knowledge-curation", "faq-positive-negative", "faq-stop-control",
"ai-suggestion-review", "staff-training", "monitoring-rollback",
}
def require(condition: bool, message: str) -> None:
if not condition:
raise ValueError(message)
def timestamp(value: object, field: str) -> dt.datetime:
require(isinstance(value, str) and value.endswith("Z"), f"{field} must be a UTC timestamp ending in Z.")
try:
return dt.datetime.fromisoformat(value.removesuffix("Z") + "+00:00")
except ValueError as error:
raise ValueError(f"{field} is not a valid timestamp.") from error
def name(value: object, field: str) -> str:
result = str(value or "").strip()
require(2 <= len(result) <= 120 and "@" not in result, f"{field} requires a name without an email address.")
return result
def refs(value: object, field: str) -> None:
require(isinstance(value, list) and 1 <= len(value) <= 10 and all(
isinstance(item, str) and 3 <= len(item) <= 200 and "@" not in item for item in value
), f"{field} requires safe opaque evidence references.")
def validate(record: object) -> None:
require(isinstance(record, dict), "Acceptance record must be a JSON object.")
require(record.get("schemaVersion") == 1, "Unsupported automation acceptance schema.")
require(record.get("system") == "guestops-automation-acceptance", "system must be guestops-automation-acceptance.")
require(record.get("targetGate") == "B", "Automation acceptance must target Gate B.")
require(record.get("dataClassification") == "synthetic-only", "Automation acceptance must use synthetic data only.")
require(re.fullmatch(r"[0-9a-f]{40}", str(record.get("releaseCommit", ""))) is not None, "releaseCommit must be a full lowercase Git SHA.")
require(re.fullmatch(r"[0-9a-f]{64}", str(record.get("releaseRecordSha256", ""))) is not None, "releaseRecordSha256 must be a SHA-256 digest.")
origin = urlparse(str(record.get("environment", "")))
require(origin.scheme == "https" and origin.hostname and origin.path in ("", "/") and not origin.query and not origin.fragment and origin.username is None and origin.password is None,
"environment must be an HTTPS origin without credentials, path, query or fragment.")
operator = name(record.get("operator"), "operator")
reviewer = name(record.get("reviewedBy"), "reviewedBy")
require(operator.casefold() != reviewer.casefold(), "operator and reviewedBy must be different people.")
started = timestamp(record.get("startedAt"), "startedAt")
ended = timestamp(record.get("endedAt"), "endedAt")
reviewed = timestamp(record.get("reviewedAt"), "reviewedAt")
require(started <= ended <= reviewed, "Acceptance timestamps are out of order.")
faq = record.get("faqEvaluation")
require(isinstance(faq, dict), "faqEvaluation is required.")
for field in ("positiveCases", "negativeCases"):
require(isinstance(faq.get(field), int) and not isinstance(faq.get(field), bool) and faq[field] > 0,
f"faqEvaluation.{field} must be a positive integer.")
require(faq.get("falsePositives") == 0 and faq.get("falseNegatives") == 0,
"FAQ activation requires zero false positives and zero false negatives.")
require(re.fullmatch(r"[0-9a-f]{64}", str(faq.get("reportSha256", ""))) is not None,
"faqEvaluation.reportSha256 must identify the retained report.")
ai = record.get("aiEvaluation")
require(isinstance(ai, dict) and isinstance(ai.get("casesReviewed"), int) and not isinstance(ai.get("casesReviewed"), bool) and ai["casesReviewed"] > 0,
"aiEvaluation requires at least one reviewed case.")
require(isinstance(ai.get("unsafeDraftsApproved"), int) and not isinstance(ai.get("unsafeDraftsApproved"), bool)
and ai["unsafeDraftsApproved"] == 0, "No unsafe AI draft may be approved.")
require(re.fullmatch(r"[0-9a-f]{64}", str(ai.get("reportSha256", ""))) is not None,
"aiEvaluation.reportSha256 must identify the retained report.")
require(isinstance(record.get("staffTrained"), int) and not isinstance(record.get("staffTrained"), bool) and record["staffTrained"] > 0,
"At least one pilot staff member must complete training.")
name(record.get("monitoringOwner"), "monitoringOwner")
name(record.get("rollbackOwner"), "rollbackOwner")
scenarios = record.get("scenarios")
require(isinstance(scenarios, list), "scenarios must be a list.")
ids = [item.get("id") for item in scenarios if isinstance(item, dict)]
require(len(ids) == len(scenarios) and len(ids) == len(set(ids)) and set(ids) == SCENARIOS,
"Acceptance record requires the exact automation scenario set.")
for item in scenarios:
require(item.get("status") == "pass", f"Scenario {item['id']} has not passed.")
refs(item.get("evidence"), f"Scenario {item['id']}")
require(record.get("postAcceptanceState") == {"faqMode": "off", "pmsWrites": "disabled", "paymentCreation": "disabled"},
"Acceptance must end with FAQ live mode, PMS writes and payment creation disabled.")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("record", type=Path)
args = parser.parse_args()
validate(json.loads(args.record.read_text(encoding="utf-8")))
print("Automation acceptance record is structurally complete. This validates the record, not its restricted evidence.")
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, json.JSONDecodeError) as error:
print(f"Automation acceptance record rejected: {error}", file=__import__("sys").stderr)
raise SystemExit(1)