runflect 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
runflect/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """Runflect: turn agent execution into actionable evidence."""
2
+
3
+ from .recorder import Run, Span, run
4
+ from .policy import (
5
+ Approval,
6
+ ApprovalAuthority,
7
+ ApprovalRequest,
8
+ ApprovalRequiredError,
9
+ AuditWriteError,
10
+ InvalidApprovalError,
11
+ PolicyConfigurationError,
12
+ PolicyEvaluationError,
13
+ ToolDecision,
14
+ ToolDenied,
15
+ ToolGuard,
16
+ ToolPolicy,
17
+ policy_audit_context,
18
+ )
19
+
20
+ __all__ = [
21
+ "Run", "Span", "run", "Approval", "ApprovalAuthority", "ApprovalRequest",
22
+ "ApprovalRequiredError", "AuditWriteError", "InvalidApprovalError",
23
+ "PolicyConfigurationError", "PolicyEvaluationError", "ToolDecision",
24
+ "ToolDenied", "ToolGuard", "ToolPolicy", "policy_audit_context",
25
+ ]
26
+ __version__ = "0.1.0"
runflect/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Support ``python -m runflect`` with the same behavior as the console script."""
2
+
3
+ from .cli import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ """Runflect Bench: local, deterministic checks of policy and recording behavior."""
2
+
3
+ BENCHMARK_VERSION = "0.1.0"
4
+ REPORT_SCHEMA = "runflect-bench-report/v1"
@@ -0,0 +1,33 @@
1
+ """Harmless deterministic tool fixtures used by the Bench suite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+
9
+ class BenchmarkToolError(RuntimeError):
10
+ """Expected, controlled tool failure used to test error propagation."""
11
+
12
+
13
+ @dataclass
14
+ class SideEffectCounter:
15
+ calls: int = 0
16
+
17
+
18
+ def sync_fixture(counter: SideEffectCounter, behavior: str):
19
+ def tool(**_arguments: Any) -> str:
20
+ counter.calls += 1
21
+ if behavior == "raise":
22
+ raise BenchmarkToolError("controlled benchmark tool failure")
23
+ return "fixture-ok"
24
+ return tool
25
+
26
+
27
+ def async_fixture(counter: SideEffectCounter, behavior: str):
28
+ async def tool(**_arguments: Any) -> str:
29
+ counter.calls += 1
30
+ if behavior == "raise":
31
+ raise BenchmarkToolError("controlled benchmark tool failure")
32
+ return "fixture-ok"
33
+ return tool
@@ -0,0 +1,91 @@
1
+ """Precisely defined aggregate metrics for benchmark outcomes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from statistics import median
7
+ from typing import Any, Iterable
8
+
9
+
10
+ def _ratio(numerator: float, denominator: float) -> float | None:
11
+ return numerator / denominator if denominator else None
12
+
13
+
14
+ def latency_summary(samples_ms: Iterable[float]) -> dict[str, Any]:
15
+ values = [float(value) for value in samples_ms]
16
+ if not values:
17
+ return {"sample_count": 0, "median_ms": None, "p95_ms": None, "samples_ms": []}
18
+ if any(not math.isfinite(value) or value < 0 for value in values):
19
+ raise ValueError("latency samples must be finite and non-negative")
20
+ ordered = sorted(values)
21
+ p95 = ordered[math.ceil(0.95 * len(ordered)) - 1] if len(ordered) >= 20 else None
22
+ return {"sample_count": len(values), "median_ms": median(values), "p95_ms": p95, "samples_ms": values}
23
+
24
+
25
+ def calculate_metrics(results: list[dict[str, Any]], mode: str) -> dict[str, Any]:
26
+ """Aggregate scenario rows. Counts are scenario-action units unless specified."""
27
+ selected = [row for row in results if row["configuration"] == mode]
28
+ if not selected:
29
+ raise ValueError(f"no results for configuration {mode}")
30
+ total_rows = len(selected)
31
+ passed = sum(bool(row["scenario_passed"]) for row in selected)
32
+ enforce_rows = [row for row in selected if mode != "baseline"]
33
+ decision_n = len(enforce_rows)
34
+ decision_correct = sum(row["actual_decision"] == row["expected_decision"] for row in enforce_rows)
35
+ unauthorized_actions = sum(
36
+ row["attempts"] for row in selected if not row["expected_execution"]
37
+ )
38
+ unauthorized_executions = sum(
39
+ min(row["actual_executions"], row["attempts"])
40
+ for row in selected if not row["expected_execution"]
41
+ )
42
+ blocked_actions = unauthorized_actions - unauthorized_executions
43
+ permitted_actions = sum(row["attempts"] for row in selected if row["expected_execution"])
44
+ false_blocks = sum(
45
+ max(0, row["attempts"] - min(row["actual_executions"], row["attempts"]))
46
+ for row in selected if row["expected_execution"]
47
+ )
48
+ approved_policy_rows = [row for row in enforce_rows if row["category"] == "approval"]
49
+ trace_rows = [row for row in selected if row.get("trace_complete") is not None]
50
+ tool_failure_count = sum(row["tool_failures"] for row in selected)
51
+ expected_tool_failure_rows = [row for row in selected if row["behavior"] == "raise"]
52
+ expected_tool_failure_handled = sum(row["expected_tool_failure_handled"] for row in expected_tool_failure_rows)
53
+ return {
54
+ "scenario_count": total_rows,
55
+ "scenario_pass_rate": {"numerator": passed, "denominator": total_rows, "value": passed / total_rows},
56
+ "policy_decision_accuracy": {
57
+ "numerator": decision_correct, "denominator": decision_n,
58
+ "value": _ratio(decision_correct, decision_n),
59
+ "scope": "policy and recorded configurations; baseline has no decision",
60
+ },
61
+ "unauthorized_execution_rate": {
62
+ "numerator": unauthorized_executions, "denominator": unauthorized_actions,
63
+ "value": _ratio(unauthorized_executions, unauthorized_actions),
64
+ },
65
+ "correctly_blocked_action_rate": {
66
+ "numerator": blocked_actions, "denominator": unauthorized_actions,
67
+ "value": _ratio(blocked_actions, unauthorized_actions),
68
+ },
69
+ "false_block_rate": {
70
+ "numerator": false_blocks, "denominator": permitted_actions,
71
+ "value": _ratio(false_blocks, permitted_actions),
72
+ },
73
+ "approval_enforcement_accuracy": {
74
+ "numerator": sum(row["scenario_passed"] for row in approved_policy_rows),
75
+ "denominator": len(approved_policy_rows),
76
+ "value": _ratio(sum(row["scenario_passed"] for row in approved_policy_rows), len(approved_policy_rows)),
77
+ "scope": "approval category under policy-enforced configurations",
78
+ },
79
+ "trace_completeness": {
80
+ "numerator": sum(row["trace_complete"] is True for row in trace_rows),
81
+ "denominator": len(trace_rows),
82
+ "value": _ratio(sum(row["trace_complete"] is True for row in trace_rows), len(trace_rows)),
83
+ "scope": "recorded configuration only",
84
+ },
85
+ "tool_failures_observed": tool_failure_count,
86
+ "expected_tool_failure_handling_rate": {
87
+ "numerator": expected_tool_failure_handled,
88
+ "denominator": len(expected_tool_failure_rows),
89
+ "value": _ratio(expected_tool_failure_handled, len(expected_tool_failure_rows)),
90
+ },
91
+ }
@@ -0,0 +1,320 @@
1
+ """Validated Bench baseline snapshots and scenario-level regression comparison."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import math
8
+ import re
9
+ import uuid
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from . import REPORT_SCHEMA
15
+
16
+ BASELINE_SCHEMA = "runflect-bench-baseline/v1"
17
+ COMPARISON_SCHEMA = "runflect-bench-comparison/v1"
18
+ _ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$")
19
+ _LABEL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.: -]{0,119}$")
20
+ _MODES = ("policy_enforcement", "recording_and_enforcement")
21
+ _CONFIGURATIONS = ("baseline", *_MODES)
22
+
23
+
24
+ class BenchReportError(ValueError):
25
+ """A Bench report or baseline is malformed or incompatible."""
26
+
27
+
28
+ class BenchRegressionError(OSError):
29
+ """A baseline or comparison report could not be read or written."""
30
+
31
+
32
+ def _canonical(value: Any) -> str:
33
+ try:
34
+ return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"), allow_nan=False)
35
+ except (TypeError, ValueError) as error:
36
+ raise BenchReportError("report must contain finite JSON values") from error
37
+
38
+
39
+ def _read_json(path: str | Path, label: str) -> Any:
40
+ try:
41
+ source = Path(path)
42
+ if source.stat().st_size > 50 * 1024 * 1024:
43
+ raise BenchReportError(f"{label} exceeds the 50 MiB input limit")
44
+ return json.loads(source.read_text(encoding="utf-8"))
45
+ except BenchReportError:
46
+ raise
47
+ except FileNotFoundError as error:
48
+ raise BenchReportError(f"{label} file was not found") from error
49
+ except json.JSONDecodeError as error:
50
+ raise BenchReportError(f"{label} file is malformed JSON") from error
51
+ except UnicodeError as error:
52
+ raise BenchReportError(f"{label} file is not UTF-8 JSON") from error
53
+ except OSError as error:
54
+ raise BenchRegressionError(f"{label} file could not be read") from error
55
+
56
+
57
+ def _sample_stat(value: Any) -> dict[str, Any]:
58
+ if not isinstance(value, dict):
59
+ return {"sample_count": 0, "median_ms": None, "p95_ms": None}
60
+ count = value.get("sample_count")
61
+ count = count if isinstance(count, int) and not isinstance(count, bool) and 0 <= count <= 10000 else 0
62
+ result = {"sample_count": count}
63
+ for key in ("median_ms", "p95_ms"):
64
+ number = value.get(key)
65
+ result[key] = number if isinstance(number, (int, float)) and not isinstance(number, bool) and math.isfinite(number) and number >= 0 else None
66
+ return result
67
+
68
+
69
+ def validate_report(value: Any) -> dict[str, Any]:
70
+ if not isinstance(value, dict) or value.get("schema") != REPORT_SCHEMA:
71
+ raise BenchReportError("unsupported Bench report schema")
72
+ benchmark_version = value.get("benchmark_version")
73
+ suite = value.get("suite")
74
+ results = value.get("scenario_results")
75
+ if (not isinstance(benchmark_version, str) or not _ID.fullmatch(benchmark_version)
76
+ or not isinstance(suite, dict) or not isinstance(results, list)):
77
+ raise BenchReportError("Bench report metadata is incomplete")
78
+ suite_id, suite_version = suite.get("id"), suite.get("version")
79
+ digest, scenario_ids = suite.get("digest"), suite.get("scenario_ids")
80
+ if (not isinstance(suite_id, str) or not _ID.fullmatch(suite_id)
81
+ or not isinstance(suite_version, str) or not _ID.fullmatch(suite_version)
82
+ or not isinstance(digest, str) or not re.fullmatch(r"[a-f0-9]{64}", digest)
83
+ or not isinstance(scenario_ids, list) or not scenario_ids or len(scenario_ids) > 500
84
+ or any(not isinstance(item, str) or not _ID.fullmatch(item) for item in scenario_ids)
85
+ or len(scenario_ids) != len(set(scenario_ids))):
86
+ raise BenchReportError("Bench suite identity is invalid")
87
+ outcomes = {mode: {} for mode in _CONFIGURATIONS}
88
+ for row in results:
89
+ if not isinstance(row, dict):
90
+ raise BenchReportError("Bench scenario result is invalid")
91
+ scenario_id, mode, passed = row.get("scenario_id"), row.get("configuration"), row.get("scenario_passed")
92
+ if (not isinstance(scenario_id, str) or scenario_id not in scenario_ids
93
+ or not isinstance(mode, str) or mode not in outcomes or type(passed) is not bool):
94
+ raise BenchReportError("Bench scenario result contains unsupported identity or outcome")
95
+ if scenario_id in outcomes[mode]:
96
+ raise BenchReportError("Bench report contains duplicate scenario results")
97
+ outcomes[mode][scenario_id] = passed
98
+ required = set(scenario_ids)
99
+ if any(set(outcomes[mode]) != required for mode in _CONFIGURATIONS):
100
+ raise BenchReportError("Bench report is missing one or more configured scenario outcomes")
101
+ run = value.get("run") if isinstance(value.get("run"), dict) else {}
102
+ run_id = run.get("id")
103
+ if not isinstance(run_id, str):
104
+ run_id = None
105
+ else:
106
+ try:
107
+ run_id = str(uuid.UUID(run_id))
108
+ except ValueError:
109
+ run_id = None
110
+ performance = value.get("performance") if isinstance(value.get("performance"), dict) else {}
111
+ statistics = performance.get("statistics") if isinstance(performance.get("statistics"), dict) else {}
112
+ return {
113
+ "benchmark_version": benchmark_version,
114
+ "suite": {"id": suite_id, "version": suite_version, "digest": digest, "scenario_ids": list(scenario_ids)},
115
+ "run_id": run_id,
116
+ "report_digest": hashlib.sha256(_canonical(value).encode("ascii")).hexdigest(),
117
+ "outcomes": outcomes,
118
+ "performance": {mode: _sample_stat(statistics.get(mode)) for mode in _CONFIGURATIONS},
119
+ }
120
+
121
+
122
+ def load_report(path: str | Path) -> tuple[dict[str, Any], dict[str, Any]]:
123
+ document = _read_json(path, "Bench report")
124
+ return document, validate_report(document)
125
+
126
+
127
+ def create_baseline(report: dict[str, Any], *, label: str | None = None) -> dict[str, Any]:
128
+ validated = validate_report(report)
129
+ if label is not None and (not isinstance(label, str) or not _LABEL.fullmatch(label.strip())):
130
+ raise BenchReportError("baseline label must contain 1 to 120 characters")
131
+ return {
132
+ "schema": BASELINE_SCHEMA,
133
+ "baseline_id": str(uuid.uuid4()),
134
+ "label": label.strip() if label else "baseline",
135
+ "created_at": datetime.now(timezone.utc).isoformat(),
136
+ "benchmark_version": validated["benchmark_version"],
137
+ "suite": validated["suite"],
138
+ "source_run_id": validated["run_id"],
139
+ "source_report_digest": validated["report_digest"],
140
+ "outcomes": validated["outcomes"],
141
+ "performance": validated["performance"],
142
+ }
143
+
144
+
145
+ def validate_baseline(value: Any) -> dict[str, Any]:
146
+ if not isinstance(value, dict) or value.get("schema") != BASELINE_SCHEMA:
147
+ raise BenchReportError("unsupported Bench baseline schema")
148
+ required = {"baseline_id", "benchmark_version", "suite", "outcomes", "performance", "source_report_digest"}
149
+ if not required.issubset(value):
150
+ raise BenchReportError("Bench baseline metadata is incomplete")
151
+ try:
152
+ uuid.UUID(value["baseline_id"])
153
+ except (ValueError, TypeError, AttributeError):
154
+ raise BenchReportError("Bench baseline identifier is invalid") from None
155
+ if not isinstance(value["source_report_digest"], str) or not re.fullmatch(r"[a-f0-9]{64}", value["source_report_digest"]):
156
+ raise BenchReportError("Bench baseline digest is invalid")
157
+ label = value.get("label", "baseline")
158
+ if not isinstance(label, str) or not _LABEL.fullmatch(label):
159
+ raise BenchReportError("Bench baseline label is invalid")
160
+ source_run_id = value.get("source_run_id")
161
+ if source_run_id is not None:
162
+ try:
163
+ uuid.UUID(source_run_id)
164
+ except (ValueError, TypeError, AttributeError):
165
+ raise BenchReportError("Bench baseline source run reference is invalid") from None
166
+ suite = value["suite"]
167
+ if not isinstance(suite, dict):
168
+ raise BenchReportError("Bench baseline suite metadata is invalid")
169
+ outcomes = value.get("outcomes")
170
+ if not isinstance(outcomes, dict) or set(outcomes) != set(_CONFIGURATIONS):
171
+ raise BenchReportError("Bench baseline outcomes are invalid")
172
+ for mode in _CONFIGURATIONS:
173
+ scenarios = outcomes[mode]
174
+ if not isinstance(scenarios, dict) or any(not isinstance(key, str) or type(passed) is not bool
175
+ for key, passed in scenarios.items()):
176
+ raise BenchReportError("Bench baseline scenario outcomes are invalid")
177
+ performance = value.get("performance")
178
+ if not isinstance(performance, dict) or set(performance) != set(_CONFIGURATIONS):
179
+ raise BenchReportError("Bench baseline performance metadata is invalid")
180
+ for mode in _CONFIGURATIONS:
181
+ stat = performance[mode]
182
+ if not isinstance(stat, dict):
183
+ raise BenchReportError("Bench baseline performance metadata is invalid")
184
+ baseline_report = {
185
+ "schema": REPORT_SCHEMA,
186
+ "benchmark_version": value.get("benchmark_version"),
187
+ "suite": suite,
188
+ "scenario_results": [
189
+ {"scenario_id": scenario_id, "configuration": mode, "scenario_passed": passed}
190
+ for mode, scenarios in value["outcomes"].items()
191
+ for scenario_id, passed in scenarios.items()
192
+ ],
193
+ "performance": {"statistics": performance},
194
+ }
195
+ normalized = validate_report(baseline_report)
196
+ return normalized
197
+
198
+
199
+ def load_baseline(path: str | Path) -> tuple[dict[str, Any], dict[str, Any]]:
200
+ document = _read_json(path, "Bench baseline")
201
+ return document, validate_baseline(document)
202
+
203
+
204
+ def compare(baseline_doc: dict[str, Any], baseline: dict[str, Any],
205
+ current_doc: dict[str, Any], current: dict[str, Any], *,
206
+ max_regressions: int = 0, max_pass_rate_drop: float | None = None,
207
+ max_p95_regression_percent: float | None = None) -> dict[str, Any]:
208
+ if isinstance(max_regressions, bool) or not isinstance(max_regressions, int) or max_regressions < 0:
209
+ raise BenchReportError("max_regressions must be a non-negative integer")
210
+ for key, threshold in (("max_pass_rate_drop", max_pass_rate_drop),
211
+ ("max_p95_regression_percent", max_p95_regression_percent)):
212
+ maximum = 1 if key == "max_pass_rate_drop" else 1000
213
+ if threshold is not None and (isinstance(threshold, bool) or not isinstance(threshold, (int, float))
214
+ or not math.isfinite(threshold) or threshold < 0 or threshold > maximum):
215
+ raise BenchReportError(f"{key} must be a finite non-negative number")
216
+ if isinstance(max_regressions, bool) or not isinstance(max_regressions, int) or not 0 <= max_regressions <= 1500:
217
+ raise BenchReportError("max_regressions must be between 0 and 1500")
218
+ if baseline["benchmark_version"] != current["benchmark_version"] or baseline["suite"] != current["suite"]:
219
+ raise BenchReportError("Bench reports are incompatible; benchmark and suite identity, version, digest, and scenario IDs must match")
220
+
221
+ config_results = {}
222
+ regressions = []
223
+ improvements = []
224
+ for mode in _MODES:
225
+ base = baseline["outcomes"][mode]
226
+ now = current["outcomes"][mode]
227
+ failures = []
228
+ better = []
229
+ for scenario_id in baseline["suite"]["scenario_ids"]:
230
+ if base[scenario_id] and not now[scenario_id]:
231
+ failures.append(scenario_id)
232
+ regressions.append({"configuration": mode, "scenario_id": scenario_id})
233
+ elif not base[scenario_id] and now[scenario_id]:
234
+ better.append(scenario_id)
235
+ improvements.append({"configuration": mode, "scenario_id": scenario_id})
236
+ base_rate = sum(base.values()) / len(base) if base else None
237
+ current_rate = sum(now.values()) / len(now) if now else None
238
+ config_results[mode] = {
239
+ "baseline_pass_rate": base_rate,
240
+ "current_pass_rate": current_rate,
241
+ "pass_rate_delta": current_rate - base_rate if current_rate is not None and base_rate is not None else None,
242
+ "regression_count": len(failures), "regressed_scenarios": failures,
243
+ "improved_scenarios": better,
244
+ }
245
+ performance_result: dict[str, Any] = {"status": "not_requested"}
246
+ performance_inconclusive = False
247
+ performance_failed = False
248
+ if max_p95_regression_percent is not None:
249
+ performance_result = {"status": "evaluated", "threshold_percent": max_p95_regression_percent, "modes": {}}
250
+ for mode in _MODES:
251
+ before = baseline["performance"][mode]
252
+ after = current["performance"][mode]
253
+ if (before["sample_count"] < 20 or after["sample_count"] < 20
254
+ or before["p95_ms"] is None or after["p95_ms"] is None or before["p95_ms"] == 0):
255
+ performance_inconclusive = True
256
+ performance_result["status"] = "inconclusive"
257
+ performance_result["modes"][mode] = {
258
+ "status": "insufficient_samples", "minimum_sample_count": 20,
259
+ "baseline_sample_count": before["sample_count"], "current_sample_count": after["sample_count"],
260
+ "baseline_p95_ms": before["p95_ms"], "current_p95_ms": after["p95_ms"],
261
+ }
262
+ continue
263
+ delta = (after["p95_ms"] - before["p95_ms"]) / before["p95_ms"] * 100
264
+ failed = delta > max_p95_regression_percent
265
+ performance_failed = performance_failed or failed
266
+ performance_result["modes"][mode] = {
267
+ "status": "failed" if failed else "passed", "baseline_sample_count": before["sample_count"],
268
+ "current_sample_count": after["sample_count"], "baseline_p95_ms": before["p95_ms"],
269
+ "current_p95_ms": after["p95_ms"], "p95_delta_percent": delta,
270
+ }
271
+
272
+ pass_rate_failed = bool(max_pass_rate_drop is not None and any(
273
+ -item["pass_rate_delta"] > max_pass_rate_drop for item in config_results.values()
274
+ if item["pass_rate_delta"] is not None
275
+ ))
276
+ functional_failed = len(regressions) > max_regressions or pass_rate_failed
277
+ status = "failed" if functional_failed or performance_failed else (
278
+ "inconclusive" if performance_inconclusive else "passed"
279
+ )
280
+ return {
281
+ "schema": COMPARISON_SCHEMA,
282
+ "comparison_id": str(uuid.uuid4()),
283
+ "generated_at": datetime.now(timezone.utc).isoformat(),
284
+ "status": status,
285
+ "baseline": {"id": baseline_doc["baseline_id"], "label": baseline_doc.get("label", "baseline"),
286
+ "source_run_id": baseline_doc.get("source_run_id"), "report_digest": baseline_doc["source_report_digest"]},
287
+ "current": {"run_id": current["run_id"], "report_digest": current["report_digest"]},
288
+ "suite": {"id": current["suite"]["id"], "version": current["suite"]["version"],
289
+ "digest": current["suite"]["digest"], "scenario_count": len(current["suite"]["scenario_ids"])},
290
+ "thresholds": {"max_regressions": max_regressions, "max_pass_rate_drop": max_pass_rate_drop,
291
+ "max_p95_regression_percent": max_p95_regression_percent},
292
+ "summary": {"regression_count": len(regressions), "improvement_count": len(improvements),
293
+ "configuration_results": config_results},
294
+ "regressions": regressions,
295
+ "improvements": improvements,
296
+ "performance": performance_result,
297
+ "limitations": [
298
+ "This compares deterministic Bench scenario outcomes and configured local timing observations; it does not measure general agent quality.",
299
+ "P95 thresholds require at least 20 samples in both reports and are host-specific observations, not a statistical significance test.",
300
+ "Reports are comparable only when benchmark version, suite ID/version/digest, and scenario IDs match exactly.",
301
+ ],
302
+ }
303
+
304
+
305
+ def write_json(value: dict[str, Any], path: str | Path) -> None:
306
+ target = Path(path).expanduser()
307
+ try:
308
+ target.parent.mkdir(parents=True, exist_ok=True)
309
+ temporary = target.with_name(f".{target.name}.{uuid.uuid4().hex}.tmp")
310
+ temporary.write_text(json.dumps(value, ensure_ascii=True, sort_keys=True, indent=2, allow_nan=False) + "\n", encoding="utf-8")
311
+ temporary.replace(target)
312
+ except OSError as error:
313
+ raise BenchRegressionError("comparison artifact could not be written") from error
314
+
315
+
316
+ def save_baseline(report_path: str | Path, output_path: str | Path, *, label: str | None = None) -> dict[str, Any]:
317
+ document, _ = load_report(report_path)
318
+ baseline = create_baseline(document, label=label)
319
+ write_json(baseline, output_path)
320
+ return baseline
@@ -0,0 +1,76 @@
1
+ """Stable JSON report construction and serialization for Runflect Bench."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.metadata
6
+ import json
7
+ import platform
8
+ import sys
9
+ import uuid
10
+ from datetime import datetime, timezone
11
+ from typing import Any
12
+
13
+ from .. import __version__
14
+ from . import BENCHMARK_VERSION, REPORT_SCHEMA
15
+ from .schema import BenchSuite
16
+
17
+
18
+ def _package_version(name: str) -> str | None:
19
+ try:
20
+ return importlib.metadata.version(name)
21
+ except importlib.metadata.PackageNotFoundError:
22
+ return None
23
+
24
+
25
+ def build_report(suite: BenchSuite, *, results: list[dict[str, Any]],
26
+ metrics: dict[str, Any], performance: dict[str, Any],
27
+ policy_summary: dict[str, Any]) -> dict[str, Any]:
28
+ """Assemble the versioned, argument-free benchmark report."""
29
+ enforced_pass = all(row["scenario_passed"] for row in results
30
+ if row["configuration"] != "baseline")
31
+ return {
32
+ "schema": REPORT_SCHEMA,
33
+ "status": "passed" if enforced_pass else "scenario_failures",
34
+ "enforcement_passed": enforced_pass,
35
+ "benchmark_version": BENCHMARK_VERSION,
36
+ "suite": {"id": suite.suite_id, "version": suite.suite_version,
37
+ "digest": suite.digest, "source": suite.source,
38
+ "scenario_ids": [item.scenario_id for item in suite.scenarios]},
39
+ "run": {"id": str(uuid.uuid4()), "timestamp_utc": datetime.now(timezone.utc).isoformat(),
40
+ "random_seed": None, "runtime": {"python": sys.version.split()[0],
41
+ "platform": platform.platform(), "runflect": __version__,
42
+ "langgraph": _package_version("langgraph"),
43
+ "langchain_core": _package_version("langchain-core")}},
44
+ "configurations": {
45
+ "baseline": {"label": "baseline", "includes": [],
46
+ "excludes": ["policy enforcement", "policy audit", "trace recording"]},
47
+ "policy_enforcement": {"label": "policy_enforcement",
48
+ "includes": ["ToolGuard policy evaluation", "durable policy audit"],
49
+ "excludes": ["Run/Span trace recording"]},
50
+ "recording_and_enforcement": {"label": "recording_and_enforcement",
51
+ "includes": ["ToolGuard policy evaluation", "durable policy audit",
52
+ "Run/Span trace recording"]},
53
+ },
54
+ "policies": policy_summary,
55
+ "scenario_results": results,
56
+ "metrics": metrics,
57
+ "performance": performance,
58
+ "measurement_limitations": [
59
+ "Fixtures are deterministic and local; they do not call a model or external service.",
60
+ "The pure scenario evaluator supplies an off-path reference decision; baseline tool calls themselves bypass policy, and reference evaluation is excluded from latency measurements.",
61
+ "Prompt-injection scenarios are strings passed to controlled tools, not evidence of real-world injection resistance.",
62
+ "The baseline intentionally bypasses policy and may execute harmless simulated unauthorized actions.",
63
+ "Policy enforcement includes durable SQLite policy-event writes; recording_and_enforcement adds Run/Span writes.",
64
+ "Database setup and policy revision import are excluded from timed observations; per-call SQLite writes are included.",
65
+ "Latency results depend on this host, Python, SQLite, filesystem, and concurrent system load; no performance threshold is asserted.",
66
+ "No memory measurement is reported because this harness does not collect a reliable comparable metric.",
67
+ "A local digest and report aid reproducibility but do not establish independent integrity or trustworthiness.",
68
+ ],
69
+ }
70
+
71
+
72
+ def serialize_report(report: dict[str, Any], *, pretty: bool = False) -> str:
73
+ """Serialize reports deterministically, with compact and readable forms."""
74
+ if pretty:
75
+ return json.dumps(report, ensure_ascii=True, sort_keys=True, indent=2)
76
+ return json.dumps(report, ensure_ascii=True, sort_keys=True, separators=(",", ":"))