|
|
@@ -0,0 +1,642 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+"""Fail-closed validator for the P3-WP01 enterprise acceptance package."""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import argparse
|
|
|
+import csv
|
|
|
+import hashlib
|
|
|
+import io
|
|
|
+import json
|
|
|
+import math
|
|
|
+import re
|
|
|
+from datetime import datetime
|
|
|
+from pathlib import Path
|
|
|
+from typing import Any
|
|
|
+
|
|
|
+BLOCKED = "ENGINEERING_READY_BLOCKED_EXTERNAL"
|
|
|
+COMPLETE = "ENTERPRISE_ACCEPTANCE_COMPLETE"
|
|
|
+EXPECTED_VERSION = "v0.3.0-phase2"
|
|
|
+EXPECTED_COMMIT = "95023e59bfb9f00be4ed61f7a8307fac56fbcee9"
|
|
|
+PLACEHOLDERS = {"", "TBD", "TBD_EXTERNAL", "UNKNOWN", "N/A", "PLACEHOLDER"}
|
|
|
+PURE_EVIDENCE_PLACEHOLDERS = PLACEHOLDERS | {"BLOCKED_EXTERNAL", "PASS_LOCAL"}
|
|
|
+UNFINISHED_STATE_VALUES = {
|
|
|
+ "TBD_EXTERNAL",
|
|
|
+ "BLOCKED_EXTERNAL",
|
|
|
+ "PASS_LOCAL",
|
|
|
+ "ENGINEERING_READY_BLOCKED_EXTERNAL",
|
|
|
+}
|
|
|
+INCOMPLETE_EVIDENCE_VALUES = PURE_EVIDENCE_PLACEHOLDERS | UNFINISHED_STATE_VALUES
|
|
|
+CASE_IDS = {f"P2-WP13-UAT-{number:03d}" for number in range(21, 25)}
|
|
|
+METRIC_TARGETS = {
|
|
|
+ "core_object_onboarding_rate": 95,
|
|
|
+ "incremental_change_accuracy": 99,
|
|
|
+ "responsibility_coverage": 95,
|
|
|
+ "quality_issue_closure_rate": 85,
|
|
|
+ "incident_evidence_coverage": 100,
|
|
|
+ "data_product_evidence_coverage": 100,
|
|
|
+}
|
|
|
+REHEARSAL_STEPS = {"install", "upgrade", "backup", "candidate_rollback", "rto_rpo"}
|
|
|
+TRAINING_IDS = {"user_training", "operations_training"}
|
|
|
+SIGNOFF_ROLES = {
|
|
|
+ "product_owner",
|
|
|
+ "business_owner",
|
|
|
+ "technical_owner",
|
|
|
+ "security_owner",
|
|
|
+ "operations_owner",
|
|
|
+}
|
|
|
+SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
|
+P1_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$")
|
|
|
+CURRENT_STATUS_RE = re.compile(
|
|
|
+ r"^\s*(?:[-*]\s*)?(?:\*\*)?(?:status|state|状态|当前状态)(?:\*\*)?\s*[::=]\s*"
|
|
|
+ r"[`'\"*]*\s*(?:TBD_EXTERNAL|BLOCKED_EXTERNAL|PASS_LOCAL|ENGINEERING_READY_BLOCKED_EXTERNAL)"
|
|
|
+ r"\s*[`'\"*]*(?:\s|[。;;,.]|$)",
|
|
|
+ re.IGNORECASE | re.MULTILINE,
|
|
|
+)
|
|
|
+HISTORICAL_ENGLISH_TOKENS = {
|
|
|
+ "history",
|
|
|
+ "historical",
|
|
|
+ "previous",
|
|
|
+ "prior",
|
|
|
+ "superseded",
|
|
|
+}
|
|
|
+HISTORICAL_CHINESE_KEYS = {"原状态", "先前状态", "前一状态"}
|
|
|
+
|
|
|
+
|
|
|
+def _missing(value: Any) -> bool:
|
|
|
+ return value is None or (isinstance(value, str) and value.strip().upper() in PLACEHOLDERS)
|
|
|
+
|
|
|
+
|
|
|
+def _reject_json_constant(value: str) -> None:
|
|
|
+ raise ValueError(f"non-finite JSON number is prohibited: {value}")
|
|
|
+
|
|
|
+
|
|
|
+def _is_finite_number(value: Any) -> bool:
|
|
|
+ return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)
|
|
|
+
|
|
|
+
|
|
|
+def _parse_iso_time(value: Any) -> datetime | None:
|
|
|
+ if not isinstance(value, str) or _missing(value):
|
|
|
+ return None
|
|
|
+ try:
|
|
|
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
|
+ except ValueError:
|
|
|
+ return None
|
|
|
+ return parsed if parsed.tzinfo is not None else None
|
|
|
+
|
|
|
+
|
|
|
+def _normalized_cell(value: str) -> str:
|
|
|
+ return value.strip().strip("`'\"* ").upper()
|
|
|
+
|
|
|
+
|
|
|
+def _is_historical_key(value: Any) -> bool:
|
|
|
+ if not isinstance(value, str):
|
|
|
+ return False
|
|
|
+ key = value.strip().lower()
|
|
|
+ if key in HISTORICAL_ENGLISH_TOKENS:
|
|
|
+ return True
|
|
|
+ if any(
|
|
|
+ key.startswith(f"{token}{separator}")
|
|
|
+ for token in HISTORICAL_ENGLISH_TOKENS
|
|
|
+ for separator in ("_", "-", " ")
|
|
|
+ ):
|
|
|
+ return True
|
|
|
+ if any(key.endswith(f"{separator}history") for separator in ("_", "-", " ")):
|
|
|
+ return True
|
|
|
+ if key == "历史" or key.startswith("历史"):
|
|
|
+ return True
|
|
|
+ return key in HISTORICAL_CHINESE_KEYS or any(
|
|
|
+ key.startswith(f"{token}{separator}")
|
|
|
+ for token in HISTORICAL_CHINESE_KEYS
|
|
|
+ for separator in ("_", "-", " ")
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _json_contains_incomplete_value(value: Any, *, historical_context: bool = False) -> bool:
|
|
|
+ if isinstance(value, dict):
|
|
|
+ for key, child in value.items():
|
|
|
+ child_is_historical = historical_context or _is_historical_key(key)
|
|
|
+ if _json_contains_incomplete_value(child, historical_context=child_is_historical):
|
|
|
+ return True
|
|
|
+ return False
|
|
|
+ if isinstance(value, list):
|
|
|
+ return any(
|
|
|
+ _json_contains_incomplete_value(child, historical_context=historical_context)
|
|
|
+ for child in value
|
|
|
+ )
|
|
|
+ return (
|
|
|
+ not historical_context
|
|
|
+ and isinstance(value, str)
|
|
|
+ and value.strip().upper() in INCOMPLETE_EVIDENCE_VALUES
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _is_separator_row(cells: list[str]) -> bool:
|
|
|
+ return bool(cells) and all(not cell or re.fullmatch(r":?-{3,}:?", cell.strip()) for cell in cells)
|
|
|
+
|
|
|
+
|
|
|
+def _table_contains_current_placeholder(rows: list[list[str]]) -> bool:
|
|
|
+ rows = [[cell.strip() for cell in row] for row in rows if any(cell.strip() for cell in row)]
|
|
|
+ if not rows:
|
|
|
+ return False
|
|
|
+ header = rows[0]
|
|
|
+ historical_columns = {
|
|
|
+ index for index, cell in enumerate(header) if _is_historical_key(_normalized_cell(cell))
|
|
|
+ }
|
|
|
+ for cells in rows:
|
|
|
+ if _is_separator_row(cells):
|
|
|
+ continue
|
|
|
+ if cells and _is_historical_key(_normalized_cell(cells[0])):
|
|
|
+ continue
|
|
|
+ for index, cell in enumerate(cells):
|
|
|
+ if (
|
|
|
+ index not in historical_columns
|
|
|
+ and _normalized_cell(cell) in INCOMPLETE_EVIDENCE_VALUES
|
|
|
+ ):
|
|
|
+ return True
|
|
|
+ return False
|
|
|
+
|
|
|
+
|
|
|
+def _is_placeholder_evidence(evidence_bytes: bytes) -> bool:
|
|
|
+ """Reject structured current-state placeholders without guessing about binary evidence."""
|
|
|
+ try:
|
|
|
+ evidence_text = evidence_bytes.decode("utf-8")
|
|
|
+ except UnicodeDecodeError:
|
|
|
+ return False
|
|
|
+ if evidence_text.strip().upper() in INCOMPLETE_EVIDENCE_VALUES:
|
|
|
+ return True
|
|
|
+ try:
|
|
|
+ parsed_json = json.loads(evidence_text)
|
|
|
+ except json.JSONDecodeError:
|
|
|
+ parsed_json = None
|
|
|
+ if parsed_json is not None and _json_contains_incomplete_value(parsed_json):
|
|
|
+ return True
|
|
|
+ if CURRENT_STATUS_RE.search(evidence_text):
|
|
|
+ return True
|
|
|
+ markdown_rows = [line.split("|")[1:-1] if line.strip().startswith("|") and line.strip().endswith("|") else line.split("|") for line in evidence_text.splitlines() if "|" in line]
|
|
|
+ if _table_contains_current_placeholder(markdown_rows):
|
|
|
+ return True
|
|
|
+ try:
|
|
|
+ csv_rows = list(csv.reader(io.StringIO(evidence_text)))
|
|
|
+ except csv.Error:
|
|
|
+ csv_rows = []
|
|
|
+ return any(len(row) > 1 for row in csv_rows) and _table_contains_current_placeholder(csv_rows)
|
|
|
+
|
|
|
+
|
|
|
+def _records_by_id(records: Any, key: str, expected: set[str], failures: list[str], label: str) -> dict[str, dict]:
|
|
|
+ if not isinstance(records, list):
|
|
|
+ failures.append(f"{label} must be a list")
|
|
|
+ return {}
|
|
|
+ mapped: dict[str, dict] = {}
|
|
|
+ for record in records:
|
|
|
+ if (
|
|
|
+ not isinstance(record, dict)
|
|
|
+ or not isinstance(record.get(key), str)
|
|
|
+ or _missing(record.get(key))
|
|
|
+ ):
|
|
|
+ failures.append(f"{label} contains an invalid record")
|
|
|
+ continue
|
|
|
+ record_id = record[key]
|
|
|
+ if record_id in mapped:
|
|
|
+ failures.append(f"{label} contains duplicate {record_id}")
|
|
|
+ mapped[record_id] = record
|
|
|
+ if set(mapped) != expected:
|
|
|
+ failures.append(f"{label} ids must be exactly {sorted(expected)}")
|
|
|
+ return mapped
|
|
|
+
|
|
|
+
|
|
|
+def _validate_acceptance_bundle(package_path: Path, evidence_root: Path | None = None) -> dict[str, Any]:
|
|
|
+ package_path = Path(package_path).resolve()
|
|
|
+ failures: list[str] = []
|
|
|
+ blockers: list[str] = []
|
|
|
+ try:
|
|
|
+ payload = json.loads(
|
|
|
+ package_path.read_text(encoding="utf-8"), parse_constant=_reject_json_constant
|
|
|
+ )
|
|
|
+ except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
|
+ return {"status": "FAILED", "declared_status": None, "blockers": [], "failures": [f"cannot read package: {exc}"]}
|
|
|
+ if not isinstance(payload, dict):
|
|
|
+ return {"status": "FAILED", "declared_status": None, "blockers": [], "failures": ["package root must be an object"]}
|
|
|
+
|
|
|
+ declared = payload.get("declared_status")
|
|
|
+ if not isinstance(declared, str) or declared not in {BLOCKED, COMPLETE}:
|
|
|
+ failures.append(f"invalid declared_status: {declared!r}")
|
|
|
+ enterprise_claim = declared == COMPLETE
|
|
|
+ root = Path(evidence_root).resolve() if evidence_root else (
|
|
|
+ package_path.parent.parent.resolve() if package_path.parent.name == "acceptance" else package_path.parent.resolve()
|
|
|
+ )
|
|
|
+
|
|
|
+ def pending_or_fail(label: str) -> None:
|
|
|
+ message = f"{label} is missing or placeholder"
|
|
|
+ (failures if enterprise_claim else blockers).append(message)
|
|
|
+
|
|
|
+ def require(value: Any, label: str) -> bool:
|
|
|
+ if _missing(value):
|
|
|
+ pending_or_fail(label)
|
|
|
+ return False
|
|
|
+ return True
|
|
|
+
|
|
|
+ def require_string(value: Any, label: str) -> bool:
|
|
|
+ if value is None or (
|
|
|
+ isinstance(value, str)
|
|
|
+ and value.strip().upper() in INCOMPLETE_EVIDENCE_VALUES
|
|
|
+ ):
|
|
|
+ pending_or_fail(label)
|
|
|
+ return False
|
|
|
+ if not isinstance(value, str) or not value.strip():
|
|
|
+ failures.append(f"{label} must be a non-empty, non-placeholder string")
|
|
|
+ return False
|
|
|
+ return True
|
|
|
+
|
|
|
+ def object_field(container: dict[str, Any], key: str, label: str) -> dict[str, Any]:
|
|
|
+ value = container.get(key)
|
|
|
+ if not isinstance(value, dict):
|
|
|
+ failures.append(f"{label} must be an object")
|
|
|
+ return {}
|
|
|
+ return value
|
|
|
+
|
|
|
+ def require_time(value: Any, label: str) -> datetime | None:
|
|
|
+ if not require_string(value, label):
|
|
|
+ return None
|
|
|
+ parsed = _parse_iso_time(value)
|
|
|
+ if parsed is None:
|
|
|
+ failures.append(f"{label} must be an ISO-8601 timestamp with timezone")
|
|
|
+ return parsed
|
|
|
+
|
|
|
+ def validate_refs(refs: Any, label: str) -> None:
|
|
|
+ if not isinstance(refs, list):
|
|
|
+ failures.append(f"{label}.evidence_refs must be a list")
|
|
|
+ return
|
|
|
+ if not refs:
|
|
|
+ pending_or_fail(f"{label}.evidence_refs")
|
|
|
+ return
|
|
|
+ for index, ref in enumerate(refs):
|
|
|
+ ref_label = f"{label}.evidence_refs[{index}]"
|
|
|
+ if not isinstance(ref, dict):
|
|
|
+ failures.append(f"{ref_label} must be an object")
|
|
|
+ continue
|
|
|
+ raw_path = ref.get("path")
|
|
|
+ if not require_string(raw_path, f"{ref_label}.path"):
|
|
|
+ continue
|
|
|
+ relative = Path(raw_path)
|
|
|
+ if relative.is_absolute() or ".." in relative.parts:
|
|
|
+ failures.append(f"{ref_label} evidence path escapes evidence root")
|
|
|
+ continue
|
|
|
+ resolved = (root / relative).resolve()
|
|
|
+ try:
|
|
|
+ resolved.relative_to(root)
|
|
|
+ except ValueError:
|
|
|
+ failures.append(f"{ref_label} evidence path escapes evidence root")
|
|
|
+ continue
|
|
|
+ digest = ref.get("sha256")
|
|
|
+ level = ref.get("evidence_level")
|
|
|
+ if not resolved.is_file():
|
|
|
+ if enterprise_claim or not _missing(digest):
|
|
|
+ failures.append(f"{ref_label} missing evidence file: {raw_path}")
|
|
|
+ else:
|
|
|
+ blockers.append(f"{ref_label} external evidence file is not yet captured")
|
|
|
+ if require_string(digest, f"{ref_label}.sha256") and not SHA256_RE.fullmatch(digest):
|
|
|
+ failures.append(f"{ref_label}.sha256 must be a lowercase SHA-256 digest")
|
|
|
+ elif isinstance(digest, str) and SHA256_RE.fullmatch(digest) and resolved.is_file():
|
|
|
+ evidence_bytes = resolved.read_bytes()
|
|
|
+ actual = hashlib.sha256(evidence_bytes).hexdigest()
|
|
|
+ if actual != digest:
|
|
|
+ failures.append(f"{ref_label} digest mismatch")
|
|
|
+ if _is_placeholder_evidence(evidence_bytes):
|
|
|
+ failures.append(f"{ref_label} placeholder evidence cannot satisfy enterprise acceptance")
|
|
|
+ if require_string(level, f"{ref_label}.evidence_level") and level != "ENTERPRISE_FORMAL":
|
|
|
+ failures.append(f"{ref_label} evidence level {level!r} is not ENTERPRISE_FORMAL")
|
|
|
+
|
|
|
+ if payload.get("schema_version") != "1.0" or payload.get("work_package") != "P3-WP01":
|
|
|
+ failures.append("schema_version/work_package contract mismatch")
|
|
|
+ source_cases = payload.get("source_cases")
|
|
|
+ if (
|
|
|
+ not isinstance(source_cases, list)
|
|
|
+ or any(not isinstance(item, str) or _missing(item) for item in source_cases)
|
|
|
+ or set(source_cases) != CASE_IDS
|
|
|
+ ):
|
|
|
+ failures.append("source_cases must contain UAT-021 through UAT-024 exactly")
|
|
|
+
|
|
|
+ context = object_field(payload, "execution_context", "execution_context")
|
|
|
+ for field in ("enterprise_name", "environment_id"):
|
|
|
+ require_string(context.get(field), f"execution_context.{field}")
|
|
|
+ if require_string(context.get("environment_type"), "execution_context.environment_type") and context.get("environment_type") != "PREPRODUCTION":
|
|
|
+ failures.append("execution_context.environment_type must be PREPRODUCTION")
|
|
|
+ commit = context.get("executed_commit")
|
|
|
+ if require_string(commit, "execution_context.executed_commit") and commit != EXPECTED_COMMIT:
|
|
|
+ failures.append(f"execution_context.executed_commit must equal {EXPECTED_COMMIT}")
|
|
|
+ context_started = require_time(context.get("started_at"), "execution_context.started_at")
|
|
|
+ context_completed = require_time(context.get("completed_at"), "execution_context.completed_at")
|
|
|
+ if context_started and context_completed and context_started > context_completed:
|
|
|
+ failures.append("execution_context.started_at must not be after completed_at")
|
|
|
+
|
|
|
+ completion_events: list[tuple[str, datetime]] = []
|
|
|
+
|
|
|
+ def bind_time(value: Any, label: str, *, completion_event: bool = False) -> datetime | None:
|
|
|
+ parsed = require_time(value, label)
|
|
|
+ if parsed and context_started and parsed < context_started:
|
|
|
+ failures.append(f"{label} must be within execution_context time range")
|
|
|
+ if parsed and context_completed and parsed > context_completed:
|
|
|
+ failures.append(f"{label} must be within execution_context time range")
|
|
|
+ if parsed and completion_event:
|
|
|
+ completion_events.append((label, parsed))
|
|
|
+ return parsed
|
|
|
+
|
|
|
+ source = object_field(payload, "source", "source")
|
|
|
+ for field in ("source_id", "source_type", "network_zone"):
|
|
|
+ require_string(source.get(field), f"source.{field}")
|
|
|
+ if source.get("readonly") is None:
|
|
|
+ pending_or_fail("source.readonly")
|
|
|
+ elif source.get("readonly") is not True:
|
|
|
+ failures.append("source.readonly must be true")
|
|
|
+ if source.get("credentials_in_package") is not False:
|
|
|
+ failures.append("source.credentials_in_package must be false")
|
|
|
+
|
|
|
+ sample = object_field(payload, "sample_summary", "sample_summary")
|
|
|
+ if require_string(sample.get("classification"), "sample_summary.classification") and sample.get("classification") != "DESENSITIZED_ENTERPRISE_SAMPLE":
|
|
|
+ failures.append("sample_summary.classification must be DESENSITIZED_ENTERPRISE_SAMPLE")
|
|
|
+ for field in ("object_count", "row_count"):
|
|
|
+ value = sample.get(field)
|
|
|
+ if not require(value, f"sample_summary.{field}"):
|
|
|
+ continue
|
|
|
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
|
|
|
+ failures.append(f"sample_summary.{field} must be a positive integer")
|
|
|
+ sample_digest = sample.get("sha256")
|
|
|
+ if require_string(sample_digest, "sample_summary.sha256") and not SHA256_RE.fullmatch(sample_digest):
|
|
|
+ failures.append("sample_summary.sha256 must be a lowercase SHA-256 digest")
|
|
|
+ if sample.get("raw_data_in_evidence") is not False:
|
|
|
+ failures.append("sample_summary.raw_data_in_evidence must be false")
|
|
|
+
|
|
|
+ artifact = object_field(payload, "release_artifact", "release_artifact")
|
|
|
+ version = artifact.get("version")
|
|
|
+ if require_string(version, "release_artifact.version") and version != EXPECTED_VERSION:
|
|
|
+ failures.append(f"release_artifact.version must equal {EXPECTED_VERSION}")
|
|
|
+ require_string(artifact.get("artifact_name"), "release_artifact.artifact_name")
|
|
|
+ artifact_digest = artifact.get("sha256")
|
|
|
+ if require_string(artifact_digest, "release_artifact.sha256") and not SHA256_RE.fullmatch(artifact_digest):
|
|
|
+ failures.append("release_artifact.sha256 must be a lowercase SHA-256 digest")
|
|
|
+ validate_refs(artifact.get("evidence_refs"), "release_artifact")
|
|
|
+
|
|
|
+ cases = _records_by_id(payload.get("uat_cases"), "id", CASE_IDS, failures, "uat_cases")
|
|
|
+ for case_id, case in cases.items():
|
|
|
+ result = case.get("result")
|
|
|
+ if result == "PASS_LOCAL":
|
|
|
+ failures.append(f"{case_id} PASS_LOCAL cannot satisfy enterprise acceptance")
|
|
|
+ result_valid = False
|
|
|
+ else:
|
|
|
+ result_valid = require_string(result, f"{case_id}.result")
|
|
|
+ if result_valid and result != "ENTERPRISE_PASS":
|
|
|
+ if enterprise_claim:
|
|
|
+ failures.append(f"{case_id} must be ENTERPRISE_PASS")
|
|
|
+ else:
|
|
|
+ blockers.append(f"{case_id} awaits enterprise execution")
|
|
|
+ for field in ("environment_id", "executed_by_person_id", "approved_by_person_id"):
|
|
|
+ require_string(case.get(field), f"{case_id}.{field}")
|
|
|
+ if isinstance(case.get("environment_id"), str) and isinstance(context.get("environment_id"), str) and case.get("environment_id") != context.get("environment_id"):
|
|
|
+ failures.append(f"{case_id}.environment_id must match execution_context.environment_id")
|
|
|
+ bind_time(case.get("executed_at"), f"{case_id}.executed_at", completion_event=True)
|
|
|
+ if case.get("executed_by_person_id") and case.get("executed_by_person_id") == case.get("approved_by_person_id"):
|
|
|
+ failures.append(f"{case_id} self-approval is prohibited")
|
|
|
+ validate_refs(case.get("evidence_refs"), case_id)
|
|
|
+
|
|
|
+ metrics = _records_by_id(payload.get("metrics"), "id", set(METRIC_TARGETS), failures, "metrics")
|
|
|
+ for metric_id, metric in metrics.items():
|
|
|
+ target = METRIC_TARGETS[metric_id]
|
|
|
+ comparator_valid = require_string(metric.get("comparator"), f"{metric_id}.comparator")
|
|
|
+ unit_valid = require_string(metric.get("unit"), f"{metric_id}.unit")
|
|
|
+ target_value = metric.get("target")
|
|
|
+ target_ready = require(target_value, f"{metric_id}.target")
|
|
|
+ target_valid = target_ready and _is_finite_number(target_value)
|
|
|
+ if target_ready and not target_valid:
|
|
|
+ failures.append(f"{metric_id}.target must be a finite number")
|
|
|
+ if not (comparator_valid and unit_valid and target_valid) or metric.get("comparator") != "GTE" or target_value != target or metric.get("unit") != "percent":
|
|
|
+ failures.append(f"{metric_id} threshold contract must be GTE {target} percent")
|
|
|
+ metric_status_valid = require_string(metric.get("status"), f"{metric_id}.status")
|
|
|
+ if metric_status_valid and metric.get("status") != "ENTERPRISE_PASS":
|
|
|
+ (failures if enterprise_claim else blockers).append(f"{metric_id} awaits enterprise measurement")
|
|
|
+ numerator, denominator, value = metric.get("numerator"), metric.get("denominator"), metric.get("value")
|
|
|
+ values_ready = True
|
|
|
+ for item, field in ((numerator, "numerator"), (denominator, "denominator"), (value, "value")):
|
|
|
+ if not require(item, f"{metric_id}.{field}"):
|
|
|
+ values_ready = False
|
|
|
+ elif not _is_finite_number(item):
|
|
|
+ failures.append(f"{metric_id}.{field} must be a finite number")
|
|
|
+ values_ready = False
|
|
|
+ if values_ready:
|
|
|
+ if numerator < 0 or denominator <= 0 or value < 0:
|
|
|
+ failures.append(f"{metric_id} metric values are invalid")
|
|
|
+ else:
|
|
|
+ calculated = numerator / denominator * 100
|
|
|
+ if abs(calculated - value) > 1e-6:
|
|
|
+ failures.append(f"{metric_id} value does not match numerator/denominator")
|
|
|
+ if value < target:
|
|
|
+ failures.append(f"{metric_id} threshold {target}% is not met")
|
|
|
+ window_start = bind_time(metric.get("window_start"), f"{metric_id}.window_start")
|
|
|
+ window_end = bind_time(metric.get("window_end"), f"{metric_id}.window_end", completion_event=True)
|
|
|
+ if window_start and window_end and window_start > window_end:
|
|
|
+ failures.append(f"{metric_id}.window_start must not be after window_end")
|
|
|
+ require_string(metric.get("approved_by_person_id"), f"{metric_id}.approved_by_person_id")
|
|
|
+ validate_refs(metric.get("evidence_refs"), metric_id)
|
|
|
+
|
|
|
+ rehearsal = object_field(payload, "preproduction_rehearsal", "preproduction_rehearsal")
|
|
|
+ rehearsal_status_valid = require_string(rehearsal.get("status"), "preproduction_rehearsal.status")
|
|
|
+ if rehearsal_status_valid and rehearsal.get("status") != "ENTERPRISE_PASS":
|
|
|
+ (failures if enterprise_claim else blockers).append("preproduction rehearsal is incomplete")
|
|
|
+ for field in ("environment_id", "executed_by_person_id", "approved_by_person_id"):
|
|
|
+ require_string(rehearsal.get(field), f"preproduction_rehearsal.{field}")
|
|
|
+ if isinstance(rehearsal.get("environment_id"), str) and isinstance(context.get("environment_id"), str) and rehearsal.get("environment_id") != context.get("environment_id"):
|
|
|
+ failures.append("preproduction_rehearsal.environment_id must match execution_context.environment_id")
|
|
|
+ bind_time(rehearsal.get("executed_at"), "preproduction_rehearsal.executed_at", completion_event=True)
|
|
|
+ if rehearsal.get("executed_by_person_id") and rehearsal.get("executed_by_person_id") == rehearsal.get("approved_by_person_id"):
|
|
|
+ failures.append("preproduction rehearsal self-approval is prohibited")
|
|
|
+ steps = _records_by_id(rehearsal.get("steps"), "id", REHEARSAL_STEPS, failures, "preproduction rehearsal steps")
|
|
|
+ for step_id, step in steps.items():
|
|
|
+ step_status_valid = require_string(step.get("status"), f"preproduction rehearsal step {step_id}.status")
|
|
|
+ if step_status_valid and step.get("status") != "ENTERPRISE_PASS":
|
|
|
+ (failures if enterprise_claim else blockers).append(f"preproduction rehearsal step {step_id} is incomplete")
|
|
|
+ for observed, target_field in (("rto_minutes", "rto_target_minutes"), ("rpo_minutes", "rpo_target_minutes")):
|
|
|
+ target_value = rehearsal.get(target_field)
|
|
|
+ target_valid = require(target_value, f"preproduction_rehearsal.{target_field}")
|
|
|
+ if target_valid and (not _is_finite_number(target_value) or target_value <= 0):
|
|
|
+ failures.append(f"preproduction_rehearsal.{target_field} must be a positive finite number")
|
|
|
+ target_valid = False
|
|
|
+ value = rehearsal.get(observed)
|
|
|
+ value_valid = require(value, f"preproduction_rehearsal.{observed}")
|
|
|
+ if value_valid and (not _is_finite_number(value) or value < 0):
|
|
|
+ failures.append(f"preproduction_rehearsal.{observed} must be a non-negative finite number")
|
|
|
+ value_valid = False
|
|
|
+ if target_valid and value_valid and value > target_value:
|
|
|
+ failures.append(f"preproduction rehearsal {observed} exceeds its target")
|
|
|
+ validate_refs(rehearsal.get("evidence_refs"), "preproduction_rehearsal")
|
|
|
+
|
|
|
+ trainings = _records_by_id(payload.get("training_records"), "id", TRAINING_IDS, failures, "training_records")
|
|
|
+ for training_id, record in trainings.items():
|
|
|
+ training_status_valid = require_string(record.get("status"), f"training {training_id}.status")
|
|
|
+ if training_status_valid and record.get("status") != "ENTERPRISE_PASS":
|
|
|
+ (failures if enterprise_claim else blockers).append(f"training {training_id} is incomplete")
|
|
|
+ participants = record.get("participants")
|
|
|
+ if not isinstance(participants, list):
|
|
|
+ failures.append(f"training {training_id}.participants must be a list")
|
|
|
+ elif not participants:
|
|
|
+ pending_or_fail(f"training {training_id}.participants")
|
|
|
+ else:
|
|
|
+ for index, participant in enumerate(participants):
|
|
|
+ require_string(participant, f"training {training_id}.participants[{index}]")
|
|
|
+ require_string(record.get("trainer_person_id"), f"training {training_id}.trainer_person_id")
|
|
|
+ bind_time(record.get("completed_at"), f"training {training_id}.completed_at", completion_event=True)
|
|
|
+ if record.get("exercise_passed") is not True:
|
|
|
+ (failures if enterprise_claim else blockers).append(f"training {training_id} exercise is incomplete")
|
|
|
+ if record.get("retraining_required") is None:
|
|
|
+ pending_or_fail(f"training {training_id}.retraining_required")
|
|
|
+ elif not isinstance(record.get("retraining_required"), bool):
|
|
|
+ failures.append(f"training {training_id}.retraining_required must be boolean")
|
|
|
+ if record.get("retraining_completed") is None:
|
|
|
+ pending_or_fail(f"training {training_id}.retraining_completed")
|
|
|
+ elif not isinstance(record.get("retraining_completed"), bool):
|
|
|
+ failures.append(f"training {training_id}.retraining_completed must be boolean")
|
|
|
+ if record.get("retraining_required") is True and record.get("retraining_completed") is not True:
|
|
|
+ failures.append(f"training {training_id} required retraining is incomplete")
|
|
|
+ validate_refs(record.get("evidence_refs"), f"training {training_id}")
|
|
|
+
|
|
|
+ defects = object_field(payload, "defect_gate", "defect_gate")
|
|
|
+ defect_status_valid = require_string(defects.get("status"), "defect_gate.status")
|
|
|
+ if defect_status_valid and defects.get("status") != "ENTERPRISE_PASS":
|
|
|
+ (failures if enterprise_claim else blockers).append("defect gate awaits enterprise review")
|
|
|
+ p0_open = defects.get("p0_open")
|
|
|
+ if require(p0_open, "defect_gate.p0_open") and (
|
|
|
+ not isinstance(p0_open, int) or isinstance(p0_open, bool) or p0_open != 0
|
|
|
+ ):
|
|
|
+ failures.append("defect_gate.p0_open must be 0")
|
|
|
+ p1_open = defects.get("p1_open")
|
|
|
+ p1_ready = require(p1_open, "defect_gate.p1_open")
|
|
|
+ if p1_ready and (not isinstance(p1_open, int) or isinstance(p1_open, bool) or p1_open < 0):
|
|
|
+ failures.append("defect_gate.p1_open must be a non-negative integer")
|
|
|
+ p1_ready = False
|
|
|
+ open_p1_ids = defects.get("open_p1_ids")
|
|
|
+
|
|
|
+ def valid_nonnegative_count(value: Any, label: str) -> bool:
|
|
|
+ if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
|
+ failures.append(f"{label} must be a non-negative integer")
|
|
|
+ return False
|
|
|
+ return True
|
|
|
+
|
|
|
+ def valid_p1_ids(value: Any, label: str) -> bool:
|
|
|
+ if not isinstance(value, list):
|
|
|
+ failures.append(f"{label} must be a list")
|
|
|
+ return False
|
|
|
+ if any(not isinstance(item, str) or _missing(item) or not P1_ID_RE.fullmatch(item) for item in value):
|
|
|
+ failures.append(f"{label} contains an invalid or placeholder P1 id")
|
|
|
+ return False
|
|
|
+ if len(value) != len(set(value)):
|
|
|
+ failures.append(f"{label} must contain unique P1 ids")
|
|
|
+ return False
|
|
|
+ return True
|
|
|
+
|
|
|
+ disposition = object_field(defects, "p1_disposition", "defect_gate.p1_disposition")
|
|
|
+ disposition_status = disposition.get("status")
|
|
|
+ if not require_string(disposition_status, "defect_gate.p1_disposition.status"):
|
|
|
+ pass
|
|
|
+ elif p1_ready and p1_open == 0:
|
|
|
+ if open_p1_ids != []:
|
|
|
+ failures.append("defect_gate.open_p1_ids must be empty when p1_open is 0")
|
|
|
+ if disposition_status != "NOT_REQUIRED":
|
|
|
+ failures.append("defect_gate.p1_disposition.status must be NOT_REQUIRED when p1_open is 0")
|
|
|
+ covered_count = disposition.get("covered_p1_count")
|
|
|
+ if not valid_nonnegative_count(covered_count, "defect_gate.p1_disposition.covered_p1_count") or covered_count != 0:
|
|
|
+ failures.append("defect_gate.p1_disposition.covered_p1_count must be 0 when p1_open is 0")
|
|
|
+ if disposition.get("covered_p1_ids") != []:
|
|
|
+ failures.append("defect_gate.p1_disposition.covered_p1_ids must be empty when p1_open is 0")
|
|
|
+ elif p1_ready and p1_open > 0:
|
|
|
+ open_ids_valid = valid_p1_ids(open_p1_ids, "defect_gate.open_p1_ids")
|
|
|
+ if open_ids_valid and len(open_p1_ids) != p1_open:
|
|
|
+ failures.append("defect_gate.open_p1_ids count must equal p1_open")
|
|
|
+ if disposition_status != "WRITTEN_DECISION_APPROVED":
|
|
|
+ failures.append("open P1 defects require WRITTEN_DECISION_APPROVED disposition")
|
|
|
+ covered_count = disposition.get("covered_p1_count")
|
|
|
+ if not valid_nonnegative_count(covered_count, "defect_gate.p1_disposition.covered_p1_count") or covered_count != p1_open:
|
|
|
+ failures.append("P1 written decision must cover every open P1 defect")
|
|
|
+ covered_p1_ids = disposition.get("covered_p1_ids")
|
|
|
+ covered_ids_valid = valid_p1_ids(covered_p1_ids, "defect_gate.p1_disposition.covered_p1_ids")
|
|
|
+ if open_ids_valid and covered_ids_valid and set(covered_p1_ids) != set(open_p1_ids):
|
|
|
+ failures.append("P1 written decision covered_p1_ids must exactly match open_p1_ids")
|
|
|
+ disposition_business = disposition.get("business_person_id")
|
|
|
+ disposition_technical = disposition.get("technical_person_id")
|
|
|
+ require_string(disposition_business, "defect_gate.p1_disposition.business_person_id")
|
|
|
+ require_string(disposition_technical, "defect_gate.p1_disposition.technical_person_id")
|
|
|
+ if disposition_business and disposition_business == disposition_technical:
|
|
|
+ failures.append("P1 written decision business and technical approvers must be distinct")
|
|
|
+ bind_time(disposition.get("decided_at"), "defect_gate.p1_disposition.decided_at", completion_event=True)
|
|
|
+ validate_refs(disposition.get("evidence_refs"), "defect_gate.p1_disposition")
|
|
|
+ business_reviewer = defects.get("reviewed_by_business_person_id")
|
|
|
+ technical_reviewer = defects.get("reviewed_by_technical_person_id")
|
|
|
+ require_string(business_reviewer, "defect_gate.reviewed_by_business_person_id")
|
|
|
+ require_string(technical_reviewer, "defect_gate.reviewed_by_technical_person_id")
|
|
|
+ if business_reviewer and business_reviewer == technical_reviewer:
|
|
|
+ failures.append("defect gate business and technical reviewers must be distinct")
|
|
|
+ bind_time(defects.get("reviewed_at"), "defect_gate.reviewed_at", completion_event=True)
|
|
|
+ validate_refs(defects.get("evidence_refs"), "defect_gate")
|
|
|
+
|
|
|
+ signoffs = _records_by_id(payload.get("signoffs"), "role", SIGNOFF_ROLES, failures, "signoffs")
|
|
|
+ signer_ids: list[str] = []
|
|
|
+ signoff_times: list[tuple[str, datetime]] = []
|
|
|
+ for role, signoff in signoffs.items():
|
|
|
+ signoff_status_valid = require_string(signoff.get("status"), f"{role}.status")
|
|
|
+ if signoff_status_valid and signoff.get("status") != "SIGNED":
|
|
|
+ (failures if enterprise_claim else blockers).append(f"{role} signoff is not signed")
|
|
|
+ if require_string(signoff.get("person_id"), f"{role}.person_id"):
|
|
|
+ signer_ids.append(signoff["person_id"])
|
|
|
+ require_string(signoff.get("person_name"), f"{role}.person_name")
|
|
|
+ signed_at = bind_time(signoff.get("signed_at"), f"{role}.signed_at")
|
|
|
+ if signed_at:
|
|
|
+ signoff_times.append((role, signed_at))
|
|
|
+ validate_refs(signoff.get("evidence_refs"), role)
|
|
|
+ if len(signer_ids) != len(set(signer_ids)):
|
|
|
+ failures.append("five-party proxy signing is prohibited; each role needs a distinct person_id")
|
|
|
+ if completion_events:
|
|
|
+ latest_label, latest_completion = max(completion_events, key=lambda item: item[1])
|
|
|
+ for role, signed_at in signoff_times:
|
|
|
+ if signed_at < latest_completion:
|
|
|
+ failures.append(f"{role}.signed_at must not precede completion event {latest_label}")
|
|
|
+
|
|
|
+ failures = list(dict.fromkeys(failures))
|
|
|
+ blockers = list(dict.fromkeys(blockers))
|
|
|
+ if failures:
|
|
|
+ status = "FAILED"
|
|
|
+ elif blockers:
|
|
|
+ status = "FAILED" if enterprise_claim else "BLOCKED"
|
|
|
+ if enterprise_claim:
|
|
|
+ failures = [f"acceptance claim is incomplete: {blocker}" for blocker in blockers]
|
|
|
+ blockers = []
|
|
|
+ elif enterprise_claim:
|
|
|
+ status = "ACCEPTED"
|
|
|
+ else:
|
|
|
+ status = "FAILED"
|
|
|
+ failures = ["blocked package contains no external blockers; declared status is inconsistent"]
|
|
|
+ return {"status": status, "declared_status": declared, "blockers": blockers, "failures": failures}
|
|
|
+
|
|
|
+
|
|
|
+def validate_acceptance_bundle(package_path: Path, evidence_root: Path | None = None) -> dict[str, Any]:
|
|
|
+ """Validate a bundle and convert expected malformed-input errors into FAILED."""
|
|
|
+ try:
|
|
|
+ return _validate_acceptance_bundle(package_path, evidence_root)
|
|
|
+ except (KeyError, OverflowError, TypeError, ValueError) as exc:
|
|
|
+ return {
|
|
|
+ "status": "FAILED",
|
|
|
+ "declared_status": None,
|
|
|
+ "blockers": [],
|
|
|
+ "failures": [f"malformed acceptance package: {type(exc).__name__}: {exc}"],
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def main() -> int:
|
|
|
+ parser = argparse.ArgumentParser(description=__doc__)
|
|
|
+ parser.add_argument("package", type=Path, help="enterprise acceptance JSON package")
|
|
|
+ parser.add_argument("--evidence-root", type=Path)
|
|
|
+ args = parser.parse_args()
|
|
|
+ result = validate_acceptance_bundle(args.package, args.evidence_root)
|
|
|
+ print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
|
+ return {"ACCEPTED": 0, "FAILED": 2, "BLOCKED": 3}[result["status"]]
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ raise SystemExit(main())
|