docs(accuracy): refresh governed scan and training evidence
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -13,6 +14,14 @@ SUPPORTED_MODEL_SUFFIXES = {".pt", ".onnx", ".engine"}
|
||||
ENV_KEYS = ("GEOINTEL_INSTALL_AI", "YOLO_ENABLED", "YOLO_MODELS_DIR", "YOLO_MODEL_PATH")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _asset_id(path: Path) -> str:
|
||||
raw = f"{path.stem}-{path.suffix.lower().lstrip('.')}"
|
||||
normalized = re.sub(r"[^a-z0-9]+", "-", raw.lower()).strip("-")
|
||||
@@ -116,6 +125,36 @@ def _gate_failures(report: dict[str, Any], candidate: dict[str, Any]) -> list[st
|
||||
return failures
|
||||
|
||||
|
||||
def _governed_release_failures(
|
||||
release_gate: dict[str, Any],
|
||||
*,
|
||||
candidate_key: str,
|
||||
model_sha256: str,
|
||||
) -> list[str]:
|
||||
"""Require the Phase-4/5 product decision that the legacy gate never had."""
|
||||
|
||||
failures: list[str] = []
|
||||
if release_gate.get("status") != "pass":
|
||||
failures.append("phase4_release_gate_not_passed")
|
||||
if release_gate.get("product_benchmark_status") != "pass":
|
||||
failures.append("product_benchmark_not_passed")
|
||||
if release_gate.get("promotion_allowed") is not True:
|
||||
failures.append("governed_promotion_not_allowed")
|
||||
if release_gate.get("phase_decision") not in {"done", "ready", "complete"}:
|
||||
failures.append("phase5_not_ready")
|
||||
if release_gate.get("candidate_key") != candidate_key:
|
||||
failures.append("governed_candidate_key_mismatch")
|
||||
recorded_sha256 = str(release_gate.get("candidate_model_sha256") or "").lower()
|
||||
if not re.fullmatch(r"[0-9a-f]{64}", recorded_sha256):
|
||||
failures.append("governed_candidate_model_sha256_missing")
|
||||
elif recorded_sha256 != model_sha256:
|
||||
failures.append("governed_candidate_model_sha256_mismatch")
|
||||
benchmark_sha256 = str(release_gate.get("benchmark_manifest_sha256") or "").lower()
|
||||
if not re.fullmatch(r"[0-9a-f]{64}", benchmark_sha256):
|
||||
failures.append("governed_benchmark_manifest_sha256_missing")
|
||||
return failures
|
||||
|
||||
|
||||
def _resolve_model_asset(models_dir: Path, model_asset_id: str) -> Path | None:
|
||||
for path in _candidate_paths(models_dir):
|
||||
if _asset_id(path) == model_asset_id:
|
||||
@@ -126,6 +165,7 @@ def _resolve_model_asset(models_dir: Path, model_asset_id: str) -> Path | None:
|
||||
def _base_payload(args: argparse.Namespace) -> dict[str, Any]:
|
||||
return {
|
||||
"promotion_report": str(Path(args.promotion_report).resolve()),
|
||||
"phase4_release_gate_report": str(Path(args.phase4_release_gate_report).resolve()),
|
||||
"candidate_key": args.candidate_key,
|
||||
"models_dir": str(Path(args.models_dir).resolve()),
|
||||
"env_file": str(Path(args.env_file).resolve()),
|
||||
@@ -188,6 +228,33 @@ def activate(args: argparse.Namespace) -> tuple[int, dict[str, Any]]:
|
||||
)
|
||||
return 2, payload
|
||||
|
||||
model_sha256 = _sha256(selected_model)
|
||||
try:
|
||||
release_gate_path = Path(args.phase4_release_gate_report)
|
||||
release_gate = _load_report(release_gate_path)
|
||||
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||
payload.update({"status": "invalid_phase4_release_gate", "message": str(exc)})
|
||||
return 2, payload
|
||||
governance_failures = _governed_release_failures(
|
||||
release_gate,
|
||||
candidate_key=args.candidate_key,
|
||||
model_sha256=model_sha256,
|
||||
)
|
||||
if governance_failures:
|
||||
payload.update(
|
||||
{
|
||||
"status": "governed_release_not_approved",
|
||||
"message": (
|
||||
"Candidate passed the legacy diagnostic gates but lacks a matching "
|
||||
"successful governed product benchmark and Phase-4/5 release decision."
|
||||
),
|
||||
"rejection_reasons": governance_failures,
|
||||
"selected_host_model_path": str(selected_model),
|
||||
"selected_model_sha256": model_sha256,
|
||||
}
|
||||
)
|
||||
return 3, payload
|
||||
|
||||
selected_container_path = _container_path(selected_model, models_dir, args.container_model_dir)
|
||||
updates = {
|
||||
"GEOINTEL_INSTALL_AI": "true",
|
||||
@@ -200,7 +267,9 @@ def activate(args: argparse.Namespace) -> tuple[int, dict[str, Any]]:
|
||||
"status": "ready_to_apply",
|
||||
"message": "Promoted YOLO candidate validated. Re-run with --apply to update the environment file.",
|
||||
"selected_host_model_path": str(selected_model),
|
||||
"selected_model_sha256": model_sha256,
|
||||
"selected_container_model_path": selected_container_path,
|
||||
"phase4_release_gate_sha256": _sha256(release_gate_path),
|
||||
"env_updates": updates,
|
||||
"docker_restart_required": True,
|
||||
}
|
||||
@@ -241,6 +310,14 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
description="Activate an existing local YOLO model only after a promotion report recommends the exact candidate key."
|
||||
)
|
||||
parser.add_argument("--promotion-report", required=True, help="Path to detection_model_promotion_report.json.")
|
||||
parser.add_argument(
|
||||
"--phase4-release-gate-report",
|
||||
required=True,
|
||||
help=(
|
||||
"Path to the governed Phase-4/5 release-gate report. It must bind the exact "
|
||||
"candidate key and model SHA-256 and explicitly allow promotion."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--candidate-key", required=True, help="Exact promoted candidate key from the report.")
|
||||
parser.add_argument(
|
||||
"--models-dir",
|
||||
|
||||
Reference in New Issue
Block a user