add final acceptance verification
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the deterministic, local Phase 17 final-acceptance gate.
|
||||
|
||||
This is a release-evidence collector, not a deployment tool. It performs only
|
||||
local tests and static/configuration checks; it never sends outreach or calls a
|
||||
provider. Capacity figures are bounded synthetic smoke measurements, not
|
||||
production capacity claims.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
API = ROOT / "apps" / "api"
|
||||
REPORT = ROOT / "docs" / "benchmarks" / "final_acceptance.latest.json"
|
||||
SCHEMA = ROOT / "docs" / "benchmarks" / "final_acceptance.schema.json"
|
||||
|
||||
|
||||
def deterministic_json(value: object) -> str:
|
||||
return json.dumps(value, indent=2, sort_keys=True, separators=(",", ": ")) + "\n"
|
||||
|
||||
|
||||
def build_capacity_smoke() -> dict:
|
||||
"""Measure fixed, in-memory bounds with no clock or network dependence."""
|
||||
total = 1000
|
||||
requested = 100
|
||||
page = list(range(total))[0:requested]
|
||||
batch_input = 5000
|
||||
retained = min(batch_input, 100)
|
||||
return {
|
||||
"bounded": len(page) <= requested and retained <= 100,
|
||||
"limits": {"max_page_size": 100, "max_batch_items_retained": 100},
|
||||
"pagination": {
|
||||
"synthetic_total": total,
|
||||
"requested_page_size": requested,
|
||||
"returned_items": len(page),
|
||||
"has_more": total > requested,
|
||||
},
|
||||
"large_batch": {
|
||||
"input_items": batch_input,
|
||||
"retained_items": retained,
|
||||
"truncated": batch_input > retained,
|
||||
},
|
||||
"interpretation": "Deterministic in-memory smoke only; not a production capacity or throughput claim.",
|
||||
}
|
||||
|
||||
|
||||
def validate_report(report: dict) -> list[str]:
|
||||
required = {"report", "version", "scope", "passed", "checks", "capacity_smoke", "blockers", "limitations"}
|
||||
errors = [f"missing:{key}" for key in sorted(required - set(report))]
|
||||
if report.get("report") != "phase17-final-acceptance": errors.append("report:const")
|
||||
if type(report.get("version")) is not int or report.get("version") != 1: errors.append("version:type-or-const")
|
||||
if report.get("scope") != "local-repository-acceptance": errors.append("scope:const")
|
||||
if not isinstance(report.get("passed"), bool): errors.append("passed:type")
|
||||
if not isinstance(report.get("checks"), dict): errors.append("checks:type")
|
||||
if not isinstance(report.get("capacity_smoke"), dict): errors.append("capacity_smoke:type")
|
||||
if not isinstance(report.get("limitations"), list) or not all(isinstance(x, str) for x in report.get("limitations", [])):
|
||||
errors.append("limitations:type")
|
||||
blockers = report.get("blockers", [])
|
||||
if not isinstance(blockers, list) or not all(isinstance(x, dict) for x in blockers):
|
||||
errors.append("blockers:type")
|
||||
else:
|
||||
for index, blocker in enumerate(blockers):
|
||||
if not {"gate", "status", "reason"}.issubset(blocker): errors.append(f"blockers[{index}]:required")
|
||||
return errors
|
||||
|
||||
|
||||
def run_command(name: str, args: list[str], *, cwd: Path = ROOT, timeout: int = 300) -> dict:
|
||||
if shutil.which(args[0]) is None:
|
||||
return {"passed": False, "status": "unavailable", "command": args, "summary": f"{args[0]} not installed"}
|
||||
try:
|
||||
completed = subprocess.run(args, cwd=cwd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"passed": False, "status": "timeout", "command": args, "summary": f"{name} exceeded {timeout}s"}
|
||||
output = completed.stdout or ""
|
||||
match = re.search(r"Ran (\d+) tests?", output)
|
||||
summary = f"exit={completed.returncode}"
|
||||
if match: summary += f", tests={match.group(1)}"
|
||||
if completed.returncode and output:
|
||||
summary += ": " + " ".join(output.strip().splitlines()[-2:])[:500]
|
||||
return {"passed": completed.returncode == 0, "status": "passed" if completed.returncode == 0 else "failed", "command": args, "summary": summary}
|
||||
|
||||
|
||||
def validate_json_files() -> dict:
|
||||
files = [ROOT / "docs" / "benchmarks" / "phase16.latest.json", ROOT / "docs" / "benchmarks" / "phase16.schema.json", SCHEMA]
|
||||
results = {}
|
||||
parsed = {}
|
||||
for path in files:
|
||||
key = str(path.relative_to(ROOT))
|
||||
try:
|
||||
parsed[key] = json.loads(path.read_text(encoding="utf-8"))
|
||||
results[key] = {"passed": True, "status": "valid_json"}
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
results[key] = {"passed": False, "status": "invalid_json", "summary": str(exc)}
|
||||
phase16 = parsed.get("docs/benchmarks/phase16.latest.json")
|
||||
if isinstance(phase16, dict):
|
||||
required = {"benchmark", "version", "offline", "limitations", "acceptance_thresholds", "results", "checks", "passed"}
|
||||
schema_ok = required.issubset(phase16) and phase16.get("benchmark") == "phase16" and phase16.get("version") == 1 and phase16.get("offline") is True and isinstance(phase16.get("passed"), bool)
|
||||
results["phase16_schema_contract"] = {"passed": schema_ok, "status": "schema_valid" if schema_ok else "schema_invalid"}
|
||||
else:
|
||||
results["phase16_schema_contract"] = {"passed": False, "status": "schema_invalid"}
|
||||
return {"passed": all(item["passed"] for item in results.values()), "files": results}
|
||||
|
||||
|
||||
def safety_checks() -> dict:
|
||||
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||
config = (API / "app" / "config.py").read_text(encoding="utf-8")
|
||||
main = (API / "app" / "main.py").read_text(encoding="utf-8")
|
||||
tests = "\n".join(p.read_text(encoding="utf-8") for p in (API / "tests").glob("test_*.py"))
|
||||
checks = {
|
||||
"outreach_disabled": 'AUTOMATED_OUTREACH_ENABLED: "false"' in compose and '"false"' in config and 'outreach_enabled' in main,
|
||||
"no_send_network": '"network_send": False' in main and "urllib.request" not in main and "smtplib" not in main,
|
||||
"tenant_routes": "organization_id=?" in main and "session_user" in main and "require_auth" in main and "tenant" in tests.lower(),
|
||||
}
|
||||
return {"passed": all(checks.values()), "checks": checks}
|
||||
|
||||
|
||||
def git_state() -> dict:
|
||||
result = run_command("git-state", ["git", "status", "--short", "--branch"])
|
||||
head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True, capture_output=True, check=False)
|
||||
porcelain = subprocess.run(["git", "status", "--porcelain"], cwd=ROOT, text=True, capture_output=True, check=False)
|
||||
return {
|
||||
"passed": result["passed"] and head.returncode == 0 and bool(head.stdout.strip()),
|
||||
"status": result["status"],
|
||||
"branch_status": result.get("summary", ""),
|
||||
"head": head.stdout.strip(),
|
||||
"working_tree_clean": porcelain.returncode == 0 and not porcelain.stdout.strip(),
|
||||
}
|
||||
|
||||
|
||||
def build_report() -> dict:
|
||||
checks = {
|
||||
"api_tests": run_command("api-tests", [sys.executable, "-m", "unittest", "discover", "-v", "-s", "apps/api/tests", "-t", "apps/api"]),
|
||||
"py_compile": run_command("py_compile", [sys.executable, "-m", "py_compile", *[str(p.relative_to(ROOT)) for p in sorted((ROOT / "scripts").glob("*.py"))], *[str(p.relative_to(ROOT)) for p in sorted((API / "app").glob("*.py"))]]),
|
||||
"benchmark_no_latency": run_command("benchmark-no-latency", [sys.executable, "scripts/benchmark_phase16.py", "--no-latency", "--output", "/tmp/prospect-phase16-acceptance.json"]),
|
||||
"shell_syntax": run_command("shell-syntax", ["bash", "-n", *[str(p.relative_to(ROOT)) for p in sorted((ROOT / "scripts").glob("*.sh"))]]),
|
||||
"json_validation": validate_json_files(),
|
||||
"compose_config": run_command("compose-config", ["docker", "compose", "-f", "docker-compose.yml", "config", "--quiet"]),
|
||||
"git_state": git_state(),
|
||||
"safety_invariants": safety_checks(),
|
||||
}
|
||||
blockers = [
|
||||
{"gate": "remote_auth", "status": "blocked", "reason": "Remote repository authentication and branch permission are not available to this local acceptance run."},
|
||||
{"gate": "deployment_access", "status": "blocked", "reason": "No production host, Docker/Compose, DNS/TLS, secrets, or deployment access is available; no deployment was attempted."},
|
||||
{"gate": "real_provider_legal", "status": "blocked", "reason": "Real source/provider enablement, consent/legal basis, terms, and operational approval remain explicit gates; outreach stays disabled."},
|
||||
]
|
||||
return {
|
||||
"report": "phase17-final-acceptance", "version": 1,
|
||||
"scope": "local-repository-acceptance",
|
||||
"passed": all(item.get("passed", False) for item in checks.values()),
|
||||
"checks": checks,
|
||||
"capacity_smoke": build_capacity_smoke(),
|
||||
"blockers": blockers,
|
||||
"limitations": ["Synthetic capacity smoke measurements do not establish production throughput, concurrency, durability, or availability."],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, default=REPORT)
|
||||
args = parser.parse_args()
|
||||
report = build_report()
|
||||
errors = validate_report(report)
|
||||
if errors: raise SystemExit("invalid acceptance report: " + ", ".join(errors))
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(deterministic_json(report), encoding="utf-8")
|
||||
print(deterministic_json(report), end="")
|
||||
return 0 if report["passed"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user