Log in
scripts/public_claim_gate.py 373 lines · 14.3 KB · python Blame
1
#!/usr/bin/env python3
2
"""Re-derive public benchmark claims from raw JSONL evidence.
3
4
This is deliberately separate from publish_gate.py. publish_gate checks whether
5
rows are mechanically publishable; this script checks whether a specific public
6
number still matches the raw rows it cites.
7
"""
8
9
from __future__ import annotations
10
11
import argparse
12
import json
13
import statistics
14
import sys
15
from pathlib import Path
16
from typing import Any
17
18
ROOT = Path(__file__).resolve().parents[1]
19
sys.path.insert(0, str(ROOT / "scripts"))
20
21
from oakbench.rows import row_returncode  # noqa: E402
22
23
KNOWN_TRANSPORTS = {"network", "local_file", "network_shaped"}
24
TRANSPORT_FIELDS = ("remote_transport", "workspace_transport")
25
FILTER_OPERATORS = {"exists", "eq", "ne", "in"}
26
CLEANUP_FAILURE_FIELDS = ("cleanup_failures", "cleanup_failure_count")
27
28
29
class ClaimError(Exception):
30
    pass
31
32
33
def load_rows(paths: list[Path]) -> list[dict[str, Any]]:
34
    rows: list[dict[str, Any]] = []
35
    for path in paths:
36
        for line_no, line in enumerate(path.read_text().splitlines(), start=1):
37
            if not line.strip():
38
                continue
39
            try:
40
                row = json.loads(line)
41
            except json.JSONDecodeError as exc:
42
                raise ClaimError(f"{path}:{line_no}: invalid JSON: {exc}") from exc
43
            rows.append(row)
44
    return rows
45
46
47
def dotted_get(row: dict[str, Any], key: str) -> Any:
48
    cur: Any = row
49
    for part in key.split("."):
50
        if not isinstance(cur, dict) or part not in cur:
51
            return None
52
        cur = cur[part]
53
    return cur
54
55
56
def row_matches(row: dict[str, Any], filters: dict[str, Any]) -> bool:
57
    for key, expected in filters.items():
58
        actual = dotted_get(row, key)
59
        if isinstance(expected, list):
60
            if actual not in expected:
61
                return False
62
        elif isinstance(expected, dict):
63
            unknown = set(expected) - FILTER_OPERATORS
64
            if unknown:
65
                raise ClaimError(
66
                    f"invalid filter operator(s) for {key}: {sorted(unknown)}; "
67
                    f"known: {sorted(FILTER_OPERATORS)}"
68
                )
69
            if "exists" in expected and (actual is not None) is not bool(expected["exists"]):
70
                return False
71
            if "eq" in expected and actual != expected["eq"]:
72
                return False
73
            if "ne" in expected and actual == expected["ne"]:
74
                return False
75
            if "in" in expected and actual not in expected["in"]:
76
                return False
77
        elif actual != expected:
78
            return False
79
    return True
80
81
82
def number(value: Any) -> float | None:
83
    if isinstance(value, bool) or value is None:
84
        return None
85
    if isinstance(value, (int, float)):
86
        return float(value)
87
    return None
88
89
90
def metric_values(rows: list[dict[str, Any]], metric: str) -> list[float]:
91
    values = []
92
    for row in rows:
93
        value = number(dotted_get(row, metric))
94
        if value is not None:
95
            values.append(value)
96
    return values
97
98
99
def statistic(values: list[float], name: str) -> float:
100
    if name == "median":
101
        return float(statistics.median(values))
102
    if name == "mean":
103
        return float(statistics.mean(values))
104
    if name == "sum":
105
        return float(sum(values))
106
    if name == "min":
107
        return float(min(values))
108
    if name == "max":
109
        return float(max(values))
110
    if name == "count":
111
        return float(len(values))
112
    raise ClaimError(f"unsupported statistic: {name}")
113
114
115
def enforce_row_guards(claim: dict[str, Any], rows: list[dict[str, Any]], label: str) -> list[str]:
116
    errors: list[str] = []
117
    if not rows:
118
        errors.append(f"{label}: no matching rows")
119
        return errors
120
121
    guards = claim.get("guards") if isinstance(claim.get("guards"), dict) else {}
122
    require_success = bool(guards.get("require_success", True))
123
    if require_success:
124
        failed = [row for row in rows if row_returncode(row) != 0]
125
        if failed:
126
            preview = [(row.get("operation"), row.get("returncode"), row.get("skip_reason")) for row in failed[:5]]
127
            errors.append(f"{label}: {len(failed)} row(s) not successful: {preview}")
128
129
    min_rows = int(guards.get("min_rows", 1))
130
    if len(rows) < min_rows:
131
        errors.append(f"{label}: {len(rows)} row(s) below min_rows={min_rows}")
132
133
    if not bool(guards.get("allow_fake_provider", False)):
134
        fake = [
135
            row
136
            for row in rows
137
            if row.get("driver") == "fake-provider" or row.get("branch_triage_provider") == "fake"
138
        ]
139
        if fake:
140
            errors.append(f"{label}: fake-provider rows are not public claim evidence")
141
142
    if any("json_oracle_passed" in row for row in rows):
143
        bad = [row for row in rows if row.get("json_oracle_passed") is not True]
144
        if bad:
145
            errors.append(f"{label}: {len(bad)} row(s) have json_oracle_passed != true")
146
    for cleanup_field in CLEANUP_FAILURE_FIELDS:
147
        bad = [
148
            row
149
            for row in rows
150
            if cleanup_field in row and number(row.get(cleanup_field)) != 0.0
151
        ]
152
        if bad:
153
            errors.append(f"{label}: {len(bad)} row(s) have {cleanup_field} != 0")
154
155
    for field in guards.get("fields_true", []):
156
        bad = [row for row in rows if dotted_get(row, field) is not True]
157
        if bad:
158
            errors.append(f"{label}: {len(bad)} row(s) have {field} != true")
159
    for field in guards.get("fields_false", []):
160
        bad = [row for row in rows if dotted_get(row, field) is not False]
161
        if bad:
162
            errors.append(f"{label}: {len(bad)} row(s) have {field} != false")
163
    for field in guards.get("fields_zero", []):
164
        bad = [row for row in rows if number(dotted_get(row, field)) != 0.0]
165
        if bad:
166
            errors.append(f"{label}: {len(bad)} row(s) have {field} != 0")
167
    for field in guards.get("fields_present", []):
168
        bad = [row for row in rows if dotted_get(row, field) is None]
169
        if bad:
170
            errors.append(f"{label}: {len(bad)} row(s) are missing {field}")
171
    for item in guards.get("fields_equal", []):
172
        if not isinstance(item, dict) or "field" not in item or "value" not in item:
173
            errors.append(f"{label}: invalid fields_equal guard {item!r}")
174
            continue
175
        field = str(item["field"])
176
        expected = item["value"]
177
        bad = [row for row in rows if dotted_get(row, field) != expected]
178
        if bad:
179
            errors.append(f"{label}: {len(bad)} row(s) have {field} != {expected!r}")
180
    return errors
181
182
183
def row_transport(row: dict[str, Any]) -> str | None:
184
    for field in TRANSPORT_FIELDS:
185
        value = row.get(field)
186
        if value:
187
            return str(value)
188
    return None
189
190
191
def transport_identity(row: dict[str, Any]) -> tuple[str, str | None, str | None]:
192
    return (
193
        row_transport(row) or "<missing>",
194
        str(row.get("netshape_profile")) if row.get("netshape_profile") is not None else None,
195
        str(row.get("remote_server") or row.get("server") or "")
196
        or None,
197
    )
198
199
200
def transport_set(rows: list[dict[str, Any]]) -> set[tuple[str, str | None, str | None]]:
201
    return {transport_identity(row) for row in rows if transport_identity(row)[0] != "<missing>"}
202
203
204
def transport_errors(rows: list[dict[str, Any]], label: str) -> list[str]:
205
    errors: list[str] = []
206
    identities: set[tuple[str, str | None, str | None]] = set()
207
    for index, row in enumerate(rows, 1):
208
        transport = row_transport(row)
209
        if (row.get("remote_repo") or row.get("remote_server")) and transport is None:
210
            errors.append(f"{label}: row {index} remote-touching row missing transport")
211
        if transport is not None and transport not in KNOWN_TRANSPORTS:
212
            errors.append(
213
                f"{label}: row {index} unknown transport {transport!r}; known: {sorted(KNOWN_TRANSPORTS)}"
214
            )
215
        if transport == "network_shaped" and not row.get("netshape_profile"):
216
            errors.append(f"{label}: row {index} shaped transport missing netshape_profile")
217
        if transport is not None:
218
            identities.add(transport_identity(row))
219
    if len(identities) > 1:
220
        errors.append(f"{label}: matched rows mix transport identities: {sorted(identities)}")
221
    return errors
222
223
224
def metric_coverage_errors(rows: list[dict[str, Any]], metric: str, values: list[float], label: str) -> list[str]:
225
    successful = [row for row in rows if row_returncode(row) == 0]
226
    if len(values) != len(successful):
227
        return [
228
            f"{label}: metric {metric} measured on {len(values)} of "
229
            f"{len(successful)} successful row(s)"
230
        ]
231
    return []
232
233
234
def evaluate_claim(claim: dict[str, Any], rows: list[dict[str, Any]]) -> dict[str, Any]:
235
    claim_id = str(claim.get("id") or "<missing-id>")
236
    filters = claim.get("row_filter")
237
    if not isinstance(filters, dict):
238
        raise ClaimError(f"{claim_id}: row_filter is required")
239
    metric = str(claim.get("metric") or "")
240
    if not metric:
241
        raise ClaimError(f"{claim_id}: metric is required")
242
    stat_name = str(claim.get("statistic") or "median")
243
    expected = claim.get("expected")
244
    if not isinstance(expected, dict):
245
        raise ClaimError(f"{claim_id}: expected is required")
246
247
    matched = [row for row in rows if row_matches(row, filters)]
248
    errors = enforce_row_guards(claim, matched, "candidate")
249
    errors.extend(transport_errors(matched, "candidate"))
250
    values = metric_values(matched, metric)
251
    errors.extend(metric_coverage_errors(matched, metric, values, "candidate"))
252
    if not values:
253
        errors.append(f"candidate: metric {metric} unmeasured")
254
        candidate_value = None
255
    else:
256
        candidate_value = statistic(values, stat_name)
257
258
    baseline_value = None
259
    ratio = None
260
    delta_pct = None
261
    reduction_pct = None
262
    baseline_filter = claim.get("baseline_filter")
263
    if isinstance(baseline_filter, dict):
264
        baseline = [row for row in rows if row_matches(row, baseline_filter)]
265
        errors.extend(enforce_row_guards(claim, baseline, "baseline"))
266
        errors.extend(transport_errors(baseline, "baseline"))
267
        baseline_values = metric_values(baseline, metric)
268
        errors.extend(metric_coverage_errors(baseline, metric, baseline_values, "baseline"))
269
        if not baseline_values:
270
            errors.append(f"baseline: metric {metric} unmeasured")
271
        else:
272
            baseline_value = statistic(baseline_values, stat_name)
273
            if baseline_value == 0:
274
                errors.append("baseline: value is zero; ratio/delta undefined")
275
            elif candidate_value is not None:
276
                ratio = candidate_value / baseline_value
277
                delta_pct = (candidate_value - baseline_value) / baseline_value * 100.0
278
                reduction_pct = (baseline_value - candidate_value) / baseline_value * 100.0
279
280
        if bool(claim.get("require_same_transport", True)):
281
            candidate_transport = transport_set(matched)
282
            baseline_transport = transport_set(baseline)
283
            if candidate_transport or baseline_transport:
284
                if candidate_transport != baseline_transport:
285
                    errors.append(
286
                        "transport mismatch: "
287
                        f"candidate={sorted(candidate_transport)} baseline={sorted(baseline_transport)}"
288
                    )
289
290
    actuals = {
291
        "value": candidate_value,
292
        "baseline_value": baseline_value,
293
        "ratio": ratio,
294
        "delta_pct": delta_pct,
295
        "reduction_pct": reduction_pct,
296
    }
297
    field = str(expected.get("field") or "value")
298
    actual = actuals.get(field)
299
    if actual is None:
300
        errors.append(f"expected field {field} is unavailable")
301
    else:
302
        if "value" in expected:
303
            target = float(expected["value"])
304
            tolerance_pct = float(expected.get("tolerance_pct", 0.0))
305
            allowed = abs(target) * tolerance_pct / 100.0
306
            if abs(actual - target) > allowed:
307
                errors.append(
308
                    f"{field} drifted: actual={actual:.6g} expected={target:.6g} "
309
                    f"tolerance_pct={tolerance_pct:.3g}"
310
                )
311
        if "min" in expected and actual < float(expected["min"]):
312
            errors.append(f"{field} below min: actual={actual:.6g} min={float(expected['min']):.6g}")
313
        if "max" in expected and actual > float(expected["max"]):
314
            errors.append(f"{field} above max: actual={actual:.6g} max={float(expected['max']):.6g}")
315
316
    return {
317
        "id": claim_id,
318
        "description": claim.get("description"),
319
        "metric": metric,
320
        "statistic": stat_name,
321
        "n": len(values),
322
        "matched_rows": len(matched),
323
        **actuals,
324
        "expected": expected,
325
        "verdict": "PASS" if not errors else "FAIL",
326
        "errors": errors,
327
    }
328
329
330
def load_spec(path: Path) -> list[dict[str, Any]]:
331
    spec = json.loads(path.read_text())
332
    if spec.get("schema_version") != 1:
333
        raise ClaimError(f"{path}: unsupported schema_version {spec.get('schema_version')!r}")
334
    claims = spec.get("claims")
335
    if not isinstance(claims, list):
336
        raise ClaimError(f"{path}: claims must be a list")
337
    return claims
338
339
340
def parse_args() -> argparse.Namespace:
341
    parser = argparse.ArgumentParser(description=__doc__)
342
    parser.add_argument("jsonl", nargs="+", type=Path, help="raw evidence JSONL")
343
    parser.add_argument("--claims", required=True, type=Path, help="claim spec JSON")
344
    parser.add_argument("--json", action="store_true", help="emit JSON results")
345
    return parser.parse_args()
346
347
348
def main() -> int:
349
    args = parse_args()
350
    try:
351
        rows = load_rows(args.jsonl)
352
        results = [evaluate_claim(claim, rows) for claim in load_spec(args.claims)]
353
    except ClaimError as exc:
354
        print(f"FAIL   {exc}", file=sys.stderr)
355
        return 2
356
357
    if args.json:
358
        print(json.dumps({"schema_version": 1, "claims": results}, indent=2, sort_keys=True))
359
    else:
360
        for result in results:
361
            print(f"{result['verdict']}   {result['id']}")
362
            print(
363
                "       "
364
                f"value={result.get('value')} baseline={result.get('baseline_value')} "
365
                f"ratio={result.get('ratio')} reduction_pct={result.get('reduction_pct')}"
366
            )
367
            for error in result["errors"]:
368
                print(f"       {error}")
369
    return 1 if any(result["verdict"] != "PASS" for result in results) else 0
370
371
372
if __name__ == "__main__":
373
    raise SystemExit(main())