From e2c53be7703e8aa4ee83015006b663d10d2207b6 Mon Sep 17 00:00:00 2001 From: Mika Date: Sun, 25 Jan 2026 17:42:37 +0000 Subject: [PATCH] Add gate_decision/src/gate_decision/core.py --- gate_decision/src/gate_decision/core.py | 93 +++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 gate_decision/src/gate_decision/core.py diff --git a/gate_decision/src/gate_decision/core.py b/gate_decision/src/gate_decision/core.py new file mode 100644 index 0000000..ca5233a --- /dev/null +++ b/gate_decision/src/gate_decision/core.py @@ -0,0 +1,93 @@ +import json +import argparse +import os +import sys +from dataclasses import dataclass +from typing import Any, Dict + + +@dataclass +class GateDecision: + decision: str + explanation: str + + +class InvalidInputError(Exception): + """Raised when summary_data input is invalid.""" + pass + + +def _validate_summary_data(summary_data: Dict[str, Any]) -> None: + required_fields = ["mischfenster_p95", "retry_free_in_window_rate", "max_value"] + for field in required_fields: + if field not in summary_data: + raise InvalidInputError(f"Missing required field: {field}") + try: + float(summary_data["mischfenster_p95"]) + float(summary_data["retry_free_in_window_rate"]) + float(summary_data["max_value"]) + except (TypeError, ValueError) as e: + raise InvalidInputError(f"Invalid field type: {e}") from e + + +def make_decision(summary_data: Dict[str, Any]) -> Dict[str, Any]: + """Erzeugt eine Gate-Decision nach der v0-Regel basierend auf den Zusammenfassungsdaten eines Runs.""" + _validate_summary_data(summary_data) + + p95 = float(summary_data["mischfenster_p95"]) + retry_free = float(summary_data["retry_free_in_window_rate"]) + threshold_p95 = 200.0 + threshold_retry = 0.95 + + if p95 <= threshold_p95 and retry_free >= threshold_retry: + decision = GateDecision(decision="pass", explanation="Run stable: p95 <= 200ms and high retry-free rate.") + else: + reasons = [] + if p95 > threshold_p95: + reasons.append(f"p95 too high ({p95:.2f} > {threshold_p95})") + if retry_free < threshold_retry: + reasons.append(f"retry-free rate low ({retry_free:.2f} < {threshold_retry})") + decision = GateDecision(decision="fail", explanation="; ".join(reasons)) + + return {"decision": decision.decision, "explanation": decision.explanation} + + +def _load_json(path: str) -> Dict[str, Any]: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def _save_json(data: Dict[str, Any], path: str) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Gate Decision v0 CLI") + parser.add_argument("--input", required=True, help="Pfad zur JSON-Eingabedatei mit Run-Summary-Daten.") + parser.add_argument("--output", required=False, default="output/gate_decision.json", help="Pfad zur Ausgabe-Datei.") + return parser.parse_args() + + +def main() -> None: + args = _parse_args() + + try: + summary_data = _load_json(args.input) + result = make_decision(summary_data) + _save_json(result, args.output) + print(f"Gate decision written to {args.output}: {result['decision']}") + except InvalidInputError as e: + print(f"Invalid input: {e}", file=sys.stderr) + sys.exit(2) + except FileNotFoundError as e: + print(f"File error: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Unexpected error: {e}", file=sys.stderr) + sys.exit(3) + + +if __name__ == "__main__": + main() \ No newline at end of file