agentloop-profiler 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.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- dashboard/value_view.py +105 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""Evaluate canonical findings against a frozen corpus without executing judges."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import platform
|
|
7
|
+
from collections import Counter, defaultdict
|
|
8
|
+
from time import perf_counter_ns
|
|
9
|
+
|
|
10
|
+
from agentloop import rules
|
|
11
|
+
from agentloop.finding_benchmark_types import FINDING_BENCHMARK_VERSION, FindingBenchmark
|
|
12
|
+
from agentloop.judgment_types import canonical, fingerprint, finite, text
|
|
13
|
+
from agentloop.markdown import markdown_table_cell
|
|
14
|
+
from agentloop.semantic_waste import read_semantic_waste
|
|
15
|
+
from agentloop.timing import event_interval_ms
|
|
16
|
+
from agentloop.tracer import AgentTrace
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _copy(value):
|
|
20
|
+
return json.loads(canonical(value))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ratio(numerator, denominator):
|
|
24
|
+
return numerator / denominator if denominator else None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _evidence(case, trace):
|
|
28
|
+
semantic = read_semantic_waste(trace)
|
|
29
|
+
selected = [
|
|
30
|
+
row
|
|
31
|
+
for row in (semantic or {}).get("cases", [])
|
|
32
|
+
if row["definition"]["family"] == case["rule_id"]
|
|
33
|
+
]
|
|
34
|
+
checks = {
|
|
35
|
+
"timing": bool(trace.events)
|
|
36
|
+
and all(event_interval_ms(event) is not None for event in trace.events),
|
|
37
|
+
"tokens": bool(trace.events)
|
|
38
|
+
and all(
|
|
39
|
+
event.token_provenance in {"provider", "tokenizer", "user_supplied"}
|
|
40
|
+
for event in trace.events
|
|
41
|
+
if event.event_type == "model_call"
|
|
42
|
+
),
|
|
43
|
+
"cost": all(
|
|
44
|
+
finite(event.metadata.get("provider_reported_cost_usd"))
|
|
45
|
+
and event.metadata["provider_reported_cost_usd"] >= 0
|
|
46
|
+
for event in trace.events
|
|
47
|
+
if event.event_type == "model_call"
|
|
48
|
+
),
|
|
49
|
+
"semantic": bool(selected)
|
|
50
|
+
and all(row["status"] in {"supported", "not_supported", "retained"} for row in selected),
|
|
51
|
+
}
|
|
52
|
+
backends = {}
|
|
53
|
+
for row in selected:
|
|
54
|
+
for judgment in row["judgments"]:
|
|
55
|
+
identity = judgment["judge"]
|
|
56
|
+
backends[fingerprint(identity)] = identity
|
|
57
|
+
return {
|
|
58
|
+
"required": case["required_evidence"],
|
|
59
|
+
"missing": [key for key in case["required_evidence"] if not checks[key]],
|
|
60
|
+
"accounting": "recorded timing; exact token provenance; reported model billing; validated saved semantic investigations",
|
|
61
|
+
"semantic_statuses": [
|
|
62
|
+
{
|
|
63
|
+
"id": row["definition"]["investigation_id"],
|
|
64
|
+
"status": row["status"],
|
|
65
|
+
"basis": row["basis"],
|
|
66
|
+
}
|
|
67
|
+
for row in selected
|
|
68
|
+
],
|
|
69
|
+
"backends": [backends[key] for key in sorted(backends)],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_finding_benchmark(protocol: FindingBenchmark, *, split, source_revision):
|
|
74
|
+
"""Analyze each selected native trace once. No callbacks, retries or tuning.
|
|
75
|
+
|
|
76
|
+
Timings include native report construction and all canonical rules, not an
|
|
77
|
+
invented allocation of that time to individual rules. Only the case's labeled
|
|
78
|
+
rule is scored; other rules' outputs are outside that case's label scope.
|
|
79
|
+
"""
|
|
80
|
+
if type(protocol) is not FindingBenchmark:
|
|
81
|
+
raise ValueError("a frozen FindingBenchmark is required")
|
|
82
|
+
text(source_revision, "source_revision")
|
|
83
|
+
cases = protocol.cases(split)
|
|
84
|
+
registry = {rule.rule_id: rule.version for rule in rules.BUILTIN_RULES}
|
|
85
|
+
identities = {case["rule_id"] for case in cases}
|
|
86
|
+
if not identities <= registry.keys():
|
|
87
|
+
raise ValueError("corpus refers to an unavailable rule")
|
|
88
|
+
rows = []
|
|
89
|
+
for case in cases:
|
|
90
|
+
trace = AgentTrace.from_dict(_copy(case["trace"]))
|
|
91
|
+
start = perf_counter_ns()
|
|
92
|
+
try:
|
|
93
|
+
report = trace.report()
|
|
94
|
+
findings = [
|
|
95
|
+
item for item in report["finding_candidates"] if item["rule_id"] == case["rule_id"]
|
|
96
|
+
]
|
|
97
|
+
errors = [item for item in report["rule_errors"] if item["rule_id"] == case["rule_id"]]
|
|
98
|
+
status = "error" if errors else "emitted" if findings else "abstained"
|
|
99
|
+
except Exception:
|
|
100
|
+
# Keep failed analyses in the denominator without publishing payloads.
|
|
101
|
+
findings, status = [], "error"
|
|
102
|
+
elapsed = (perf_counter_ns() - start) / 1_000_000
|
|
103
|
+
rows.append(
|
|
104
|
+
{
|
|
105
|
+
"case_id": case["id"],
|
|
106
|
+
"rule_id": case["rule_id"],
|
|
107
|
+
"rule_version": registry[case["rule_id"]],
|
|
108
|
+
"trace_hash": fingerprint(case["trace"]),
|
|
109
|
+
"status": status,
|
|
110
|
+
"findings": _copy(findings),
|
|
111
|
+
"analysis_ms": elapsed,
|
|
112
|
+
"evidence": _evidence(case, trace),
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
result = {
|
|
116
|
+
"schema_version": FINDING_BENCHMARK_VERSION,
|
|
117
|
+
"corpus_hash": protocol.corpus_hash,
|
|
118
|
+
"split": split,
|
|
119
|
+
"policy": protocol.to_dict()["policy"],
|
|
120
|
+
"source_revision": source_revision,
|
|
121
|
+
"python": platform.python_version(),
|
|
122
|
+
"rules": {key: registry[key] for key in sorted(identities)},
|
|
123
|
+
"rows": rows,
|
|
124
|
+
}
|
|
125
|
+
return {**result, "summary": summarize_finding_benchmark(protocol, result)}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _validated_rows(protocol, result):
|
|
129
|
+
if (
|
|
130
|
+
result.get("schema_version") != FINDING_BENCHMARK_VERSION
|
|
131
|
+
or result.get("corpus_hash") != protocol.corpus_hash
|
|
132
|
+
):
|
|
133
|
+
raise ValueError("result does not match the frozen corpus")
|
|
134
|
+
if result.get("policy") != protocol.to_dict()["policy"]:
|
|
135
|
+
raise ValueError("frozen policy mismatch")
|
|
136
|
+
text(result.get("source_revision"), "source_revision")
|
|
137
|
+
cases = protocol.cases(result.get("split"))
|
|
138
|
+
versions = result.get("rules")
|
|
139
|
+
if not isinstance(versions, dict) or set(versions) != {case["rule_id"] for case in cases}:
|
|
140
|
+
raise ValueError("result must declare every planned rule")
|
|
141
|
+
for version in versions.values():
|
|
142
|
+
text(version, "rule version")
|
|
143
|
+
rows = result.get("rows")
|
|
144
|
+
if not isinstance(rows, list):
|
|
145
|
+
raise ValueError("result rows must be a list")
|
|
146
|
+
by_id, expected = {}, {case["id"]: case for case in cases}
|
|
147
|
+
for row in rows:
|
|
148
|
+
identity = row.get("case_id")
|
|
149
|
+
if identity not in expected or identity in by_id:
|
|
150
|
+
raise ValueError("duplicate or unexpected result case")
|
|
151
|
+
case = expected[identity]
|
|
152
|
+
if (
|
|
153
|
+
row.get("trace_hash") != fingerprint(case["trace"])
|
|
154
|
+
or row.get("rule_id") != case["rule_id"]
|
|
155
|
+
or row.get("rule_version") != versions[case["rule_id"]]
|
|
156
|
+
):
|
|
157
|
+
raise ValueError("result provenance mismatch")
|
|
158
|
+
status, findings = row.get("status"), row.get("findings")
|
|
159
|
+
if status not in {"error", "abstained", "emitted"} or not isinstance(findings, list):
|
|
160
|
+
raise ValueError("invalid result status or findings")
|
|
161
|
+
if status != "error" and bool(findings) != (status == "emitted"):
|
|
162
|
+
raise ValueError("result status contradicts its findings")
|
|
163
|
+
if not finite(row.get("analysis_ms")) or row["analysis_ms"] < 0:
|
|
164
|
+
raise ValueError("analysis timing must be finite and nonnegative")
|
|
165
|
+
for finding in findings:
|
|
166
|
+
if (
|
|
167
|
+
finding.get("rule_id") != row["rule_id"]
|
|
168
|
+
or finding.get("rule_version") != row["rule_version"]
|
|
169
|
+
):
|
|
170
|
+
raise ValueError("finding rule identity mismatch")
|
|
171
|
+
text(finding.get("type"), "finding family")
|
|
172
|
+
spans = finding.get("affected_nodes")
|
|
173
|
+
if not isinstance(spans, list) or any(not isinstance(span, str) for span in spans):
|
|
174
|
+
raise ValueError("finding spans must be strings")
|
|
175
|
+
text(finding.get("confidence"), "finding confidence")
|
|
176
|
+
# Reconstruct evidence completeness and backend provenance from the frozen
|
|
177
|
+
# trace, never from a mutable summary or caller-supplied claimed score.
|
|
178
|
+
row = {**row, "evidence": _evidence(case, AgentTrace.from_dict(_copy(case["trace"])))}
|
|
179
|
+
by_id[identity] = row
|
|
180
|
+
return [
|
|
181
|
+
(
|
|
182
|
+
case,
|
|
183
|
+
by_id.get(
|
|
184
|
+
case["id"],
|
|
185
|
+
{
|
|
186
|
+
"case_id": case["id"],
|
|
187
|
+
"rule_id": case["rule_id"],
|
|
188
|
+
"rule_version": versions[case["rule_id"]],
|
|
189
|
+
"status": "missing",
|
|
190
|
+
"findings": [],
|
|
191
|
+
"analysis_ms": None,
|
|
192
|
+
"evidence": _evidence(case, AgentTrace.from_dict(_copy(case["trace"]))),
|
|
193
|
+
},
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
for case in cases
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _score(case, row):
|
|
201
|
+
known = case["label"] in {"opportunity", "no_opportunity"}
|
|
202
|
+
expected = {(item["family"], tuple(sorted(item["spans"]))) for item in case["opportunities"]}
|
|
203
|
+
matched, scored = set(), []
|
|
204
|
+
for finding in row["findings"]:
|
|
205
|
+
signature = (finding["type"], tuple(sorted(finding["affected_nodes"])))
|
|
206
|
+
correct = signature in expected and signature not in matched if known else None
|
|
207
|
+
if correct:
|
|
208
|
+
matched.add(signature)
|
|
209
|
+
observations = finding.get("observations") or {}
|
|
210
|
+
scored.append(
|
|
211
|
+
{
|
|
212
|
+
"confidence": finding["confidence"],
|
|
213
|
+
"basis": observations.get("basis", "deterministic_rule"),
|
|
214
|
+
"correct": correct,
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
return scored, len(expected - matched)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _aggregate(pairs):
|
|
221
|
+
counts = Counter(
|
|
222
|
+
{
|
|
223
|
+
key: 0
|
|
224
|
+
for key in (
|
|
225
|
+
"planned_cases",
|
|
226
|
+
"emitted_cases",
|
|
227
|
+
"abstained_cases",
|
|
228
|
+
"error_cases",
|
|
229
|
+
"missing_cases",
|
|
230
|
+
"opportunity_cases",
|
|
231
|
+
"negative_cases",
|
|
232
|
+
"unknown_cases",
|
|
233
|
+
"ambiguous_cases",
|
|
234
|
+
"negative_cases_with_findings",
|
|
235
|
+
"true_positive_findings",
|
|
236
|
+
"false_positive_findings",
|
|
237
|
+
"unscored_findings",
|
|
238
|
+
"missed_opportunities",
|
|
239
|
+
"complete_evidence_cases",
|
|
240
|
+
"incomplete_evidence_findings",
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
confidence, timings, backends = defaultdict(Counter), [], {}
|
|
245
|
+
for case, row in pairs:
|
|
246
|
+
counts["planned_cases"] += 1
|
|
247
|
+
counts[f"{row['status']}_cases"] += 1
|
|
248
|
+
counts[
|
|
249
|
+
{
|
|
250
|
+
"opportunity": "opportunity_cases",
|
|
251
|
+
"no_opportunity": "negative_cases",
|
|
252
|
+
"unknown": "unknown_cases",
|
|
253
|
+
"ambiguous": "ambiguous_cases",
|
|
254
|
+
}[case["label"]]
|
|
255
|
+
] += 1
|
|
256
|
+
counts["complete_evidence_cases"] += not row["evidence"]["missing"]
|
|
257
|
+
if row["evidence"]["missing"]:
|
|
258
|
+
counts["incomplete_evidence_findings"] += len(row["findings"])
|
|
259
|
+
counts["negative_cases_with_findings"] += case["label"] == "no_opportunity" and bool(
|
|
260
|
+
row["findings"]
|
|
261
|
+
)
|
|
262
|
+
scored, missed = _score(case, row)
|
|
263
|
+
counts["missed_opportunities"] += missed
|
|
264
|
+
for finding in scored:
|
|
265
|
+
key = (
|
|
266
|
+
"unscored_findings"
|
|
267
|
+
if finding["correct"] is None
|
|
268
|
+
else "true_positive_findings"
|
|
269
|
+
if finding["correct"]
|
|
270
|
+
else "false_positive_findings"
|
|
271
|
+
)
|
|
272
|
+
counts[key] += 1
|
|
273
|
+
confidence[(finding["basis"], finding["confidence"])][key] += 1
|
|
274
|
+
if row["analysis_ms"] is not None:
|
|
275
|
+
timings.append(row["analysis_ms"])
|
|
276
|
+
for backend in row["evidence"]["backends"]:
|
|
277
|
+
backends[fingerprint(backend)] = backend
|
|
278
|
+
return {
|
|
279
|
+
**counts,
|
|
280
|
+
"precision": _ratio(
|
|
281
|
+
counts["true_positive_findings"],
|
|
282
|
+
counts["true_positive_findings"] + counts["false_positive_findings"],
|
|
283
|
+
),
|
|
284
|
+
"false_positive_rate": _ratio(
|
|
285
|
+
counts["negative_cases_with_findings"], counts["negative_cases"]
|
|
286
|
+
),
|
|
287
|
+
"coverage": _ratio(counts["emitted_cases"], counts["planned_cases"]),
|
|
288
|
+
"abstention_rate": _ratio(counts["abstained_cases"], counts["planned_cases"]),
|
|
289
|
+
"evidence_completeness": _ratio(counts["complete_evidence_cases"], counts["planned_cases"]),
|
|
290
|
+
"confidence_counts": [
|
|
291
|
+
{"basis": key[0], "confidence": key[1], **dict(value)}
|
|
292
|
+
for key, value in sorted(confidence.items())
|
|
293
|
+
],
|
|
294
|
+
"confidence_semantics": "ordinal per rule; not probabilities; numeric calibration unavailable",
|
|
295
|
+
"analysis_timing": {
|
|
296
|
+
"scope": "native report including all canonical rules per selected trace",
|
|
297
|
+
"measured_cases": len(timings),
|
|
298
|
+
"total_ms": sum(timings),
|
|
299
|
+
"mean_ms": _ratio(sum(timings), len(timings)),
|
|
300
|
+
},
|
|
301
|
+
"backends": [backends[key] for key in sorted(backends)],
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def summarize_finding_benchmark(protocol, result):
|
|
306
|
+
pairs = _validated_rows(protocol, result)
|
|
307
|
+
grouped = defaultdict(list)
|
|
308
|
+
for case, row in pairs:
|
|
309
|
+
grouped[(row["rule_id"], row["rule_version"])].append((case, row))
|
|
310
|
+
overall = _aggregate(pairs)
|
|
311
|
+
# The same word may represent unrelated evidence policies across rules.
|
|
312
|
+
overall["confidence_counts"] = []
|
|
313
|
+
overall["confidence_semantics"] = (
|
|
314
|
+
"See per-rule and evidence-basis strata; confidence is never pooled across rules."
|
|
315
|
+
)
|
|
316
|
+
return {
|
|
317
|
+
"synthetic": protocol.to_dict()["synthetic"],
|
|
318
|
+
"unit": "exact family and affected-span finding; FPR is any emission per known-negative case",
|
|
319
|
+
"overall": overall,
|
|
320
|
+
"per_rule": [
|
|
321
|
+
{"rule_id": identity, "rule_version": version, **_aggregate(group)}
|
|
322
|
+
for (identity, version), group in sorted(grouped.items())
|
|
323
|
+
],
|
|
324
|
+
"limits": "Corpus-specific diagnostics, not universal precision or production savings. Abstention means no emitted finding, not a proven negative. Unknown and ambiguous labels are not correctness trials. Timings include saved evidence validation, not judge inference.",
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def compare_finding_benchmarks(protocol, baseline, candidate, *, review_ref=None):
|
|
329
|
+
"""Strict zero-regression gate on the same frozen evaluation split.
|
|
330
|
+
|
|
331
|
+
A review reference acknowledges a regression, not missing/invalid evidence.
|
|
332
|
+
It is a recorded host declaration, not an authentication mechanism.
|
|
333
|
+
"""
|
|
334
|
+
if baseline.get("split") != "evaluation" or candidate.get("split") != "evaluation":
|
|
335
|
+
raise ValueError("release gates require the held-out evaluation split")
|
|
336
|
+
before = summarize_finding_benchmark(protocol, baseline)
|
|
337
|
+
after = summarize_finding_benchmark(protocol, candidate)
|
|
338
|
+
if review_ref is not None:
|
|
339
|
+
text(review_ref, "review_ref")
|
|
340
|
+
old = {row["rule_id"]: row for row in before["per_rule"]}
|
|
341
|
+
changes, incomplete = [], []
|
|
342
|
+
for row in after["per_rule"]:
|
|
343
|
+
previous = old[row["rule_id"]]
|
|
344
|
+
for key in ("error_cases", "missing_cases"):
|
|
345
|
+
if row[key] or previous[key]:
|
|
346
|
+
incomplete.append({"rule_id": row["rule_id"], "metric": key})
|
|
347
|
+
for key in (
|
|
348
|
+
"false_positive_findings",
|
|
349
|
+
"negative_cases_with_findings",
|
|
350
|
+
"missed_opportunities",
|
|
351
|
+
"unscored_findings",
|
|
352
|
+
"incomplete_evidence_findings",
|
|
353
|
+
):
|
|
354
|
+
if row[key] > previous[key]:
|
|
355
|
+
changes.append(
|
|
356
|
+
{
|
|
357
|
+
"rule_id": row["rule_id"],
|
|
358
|
+
"baseline_version": previous["rule_version"],
|
|
359
|
+
"candidate_version": row["rule_version"],
|
|
360
|
+
"metric": key,
|
|
361
|
+
"baseline": previous[key],
|
|
362
|
+
"candidate": row[key],
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
return {
|
|
366
|
+
"passed": not incomplete and (not changes or review_ref is not None),
|
|
367
|
+
"regressions": changes,
|
|
368
|
+
"incomplete": incomplete,
|
|
369
|
+
"review_ref": review_ref,
|
|
370
|
+
"review_applied": bool(changes and review_ref and not incomplete),
|
|
371
|
+
"corpus_hash": protocol.corpus_hash,
|
|
372
|
+
"policy": protocol.to_dict()["policy"],
|
|
373
|
+
"baseline_revision": baseline["source_revision"],
|
|
374
|
+
"candidate_revision": candidate["source_revision"],
|
|
375
|
+
"threshold_policy": "No increase in false positives, missed opportunities, unscored emissions or findings with incomplete required evidence; errors/missing cases always fail.",
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def finding_benchmark_markdown(protocol, result, gate=None):
|
|
380
|
+
summary = summarize_finding_benchmark(protocol, result)
|
|
381
|
+
lines = [
|
|
382
|
+
"# Finding benchmark",
|
|
383
|
+
"",
|
|
384
|
+
summary["limits"],
|
|
385
|
+
"",
|
|
386
|
+
f"Corpus: `{protocol.corpus_hash}`",
|
|
387
|
+
"",
|
|
388
|
+
"| Rule | Version | Cases | TP | FP | Unknown/ambiguous | Abstentions | Precision | FPR |",
|
|
389
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
390
|
+
]
|
|
391
|
+
for row in summary["per_rule"]:
|
|
392
|
+
values = [
|
|
393
|
+
row["rule_id"],
|
|
394
|
+
row["rule_version"],
|
|
395
|
+
row["planned_cases"],
|
|
396
|
+
row["true_positive_findings"],
|
|
397
|
+
row["false_positive_findings"],
|
|
398
|
+
row["unknown_cases"] + row["ambiguous_cases"],
|
|
399
|
+
row["abstained_cases"],
|
|
400
|
+
row["precision"],
|
|
401
|
+
row["false_positive_rate"],
|
|
402
|
+
]
|
|
403
|
+
lines.append(
|
|
404
|
+
"| "
|
|
405
|
+
+ " | ".join(
|
|
406
|
+
markdown_table_cell("unavailable" if value is None else str(value))
|
|
407
|
+
for value in values
|
|
408
|
+
)
|
|
409
|
+
+ " |"
|
|
410
|
+
)
|
|
411
|
+
lines.extend(
|
|
412
|
+
[
|
|
413
|
+
"",
|
|
414
|
+
"Confidence labels are ordinal within each rule; they are not calibrated probabilities. JSON retains coverage, evidence completeness, backend/config identities, per-case findings and measured analysis overhead.",
|
|
415
|
+
]
|
|
416
|
+
)
|
|
417
|
+
if gate is not None:
|
|
418
|
+
lines.extend(
|
|
419
|
+
[
|
|
420
|
+
"",
|
|
421
|
+
f"Release gate: {'PASS' if gate['passed'] else 'FAIL'}; regressions: {len(gate['regressions'])}; incomplete: {len(gate['incomplete'])}; explicit review: {markdown_table_cell(gate['review_ref'] or 'none')}.",
|
|
422
|
+
]
|
|
423
|
+
)
|
|
424
|
+
return "\n".join(lines) + "\n"
|