Log in
scripts/repro_verify.py 431 lines · 17.8 KB · python Blame
1
#!/usr/bin/env python3
2
"""Verify a third-party replay against a published repro bundle.
3
4
Third-party flow: regenerate fixtures byte-identically from the bundle's
5
seeds, re-run the suite, then run this verifier with the replay rows.
6
7
Machine-INDEPENDENT metrics (tokens, output bytes, tool calls) must match
8
the original rows EXACTLY (sorted value multisets per group). Machine-
9
DEPENDENT metrics (elapsed_ms) verify by ratio: the oak/git median-ratio
10
bootstrap CI on each side must overlap.
11
12
The bundle pins only hashes of the original raw rows, not the rows
13
themselves. Pass the original row files with --original to enable exact
14
machine-independent verification; without them those groups are
15
unverifiable and the best possible verdict is COMPARABLE.
16
17
Coverage is mandatory for full verification: EVERY raw_inputs entry pinned
18
by the bundle must be matched (by sha256) by a provided --original file.
19
Any pinned input not covered is reported as
20
"unverifiable:bundle_input_not_provided:<path>" and caps the verdict at
21
COMPARABLE -- a replay of a subset of the bundle's inputs can never reach
22
VERIFIED or VERIFIED-EXACT.
23
24
Verdicts and exit codes:
25
26
  VERIFIED-EXACT  all machine-independent values equal + ratio CIs overlap
27
                  + every pinned raw input covered by --original           -> 0
28
  VERIFIED        machine-independent values equal; ratio data absent;
29
                  every pinned raw input covered by --original             -> 0
30
  COMPARABLE      ratios overlap (or absent) but some machine-independent
31
                  values unverifiable or some pinned raw input uncovered   -> 3
32
  DIVERGENT       any exact mismatch or non-overlapping ratio CI           -> 1
33
  usage error                                                              -> 2
34
35
Null means unmeasured (ADR-0002): a metric absent on one side is reported
36
as unverifiable, never as a silent zero-mismatch.
37
38
Usage:
39
40
  python3 scripts/repro_verify.py replay-rows.jsonl ... --bundle bundle.json \
41
      [--original original-rows.jsonl ...] [--confidence 0.95]
42
"""
43
44
from __future__ import annotations
45
46
import argparse
47
import json
48
import math
49
import sys
50
from pathlib import Path
51
from typing import Any, Callable, Optional
52
53
ROOT = Path(__file__).resolve().parents[1]
54
sys.path.insert(0, str(ROOT / "scripts"))
55
56
from oakbench.baseline_book import sha256_file  # noqa: E402
57
from oakbench.rows import number_or_none, output_bytes, row_returncode, token_total, tool_calls  # noqa: E402
58
from oakbench.stats import ratio_ci  # noqa: E402
59
60
GroupKey = tuple[Optional[str], str, str, str]
61
62
MACHINE_INDEPENDENT_METRICS: tuple[tuple[str, Callable[[dict[str, Any]], Optional[float]]], ...] = (
63
    ("tokens", token_total),
64
    ("output_bytes", output_bytes),
65
    ("tool_calls", tool_calls),
66
)
67
VERDICT_EXIT_CODES = {"VERIFIED-EXACT": 0, "VERIFIED": 0, "DIVERGENT": 1, "COMPARABLE": 3}
68
DEFAULT_CONTENT_SOURCE_STRENGTH = {
69
    "git_head_payload_sha256": 3,
70
    "git_lfs_worktree_payload_sha256": 1,
71
    "worktree_payload_sha256": 1,
72
    "mixed_payload_sources": 1,
73
}
74
75
76
def load_jsonl(paths: list[Path]) -> list[dict[str, Any]]:
77
    rows: list[dict[str, Any]] = []
78
    for path in paths:
79
        with path.open() as fh:
80
            for raw_line in fh:
81
                line = raw_line.strip()
82
                if line:
83
                    rows.append(json.loads(line))
84
    return rows
85
86
87
def group_key(row: dict[str, Any]) -> GroupKey:
88
    profile = row.get("profile")
89
    return (
90
        None if profile is None else str(profile),
91
        str(row.get("scenario") or ""),
92
        str(row.get("operation") or ""),
93
        str(row.get("subject") or ""),
94
    )
95
96
97
def group_rows(rows: list[dict[str, Any]]) -> dict[GroupKey, list[dict[str, Any]]]:
98
    grouped: dict[GroupKey, list[dict[str, Any]]] = {}
99
    for row in rows:
100
        if row_returncode(row) != 0:
101
            continue
102
        grouped.setdefault(group_key(row), []).append(row)
103
    return grouped
104
105
106
def metric_values(rows: list[dict[str, Any]], accessor: Callable[[dict[str, Any]], Optional[float]]) -> list[float]:
107
    values = [value for row in rows if (value := accessor(row)) is not None]
108
    return sorted(values)
109
110
111
def elapsed_values(rows: list[dict[str, Any]]) -> list[float]:
112
    return [value for row in rows if (value := number_or_none(row.get("elapsed_ms"))) is not None]
113
114
115
def row_subject_kind(rows: list[dict[str, Any]], fallback_subject: str) -> str:
116
    first_kind = rows[0].get("subject_kind") if rows else None
117
    if isinstance(first_kind, str) and first_kind:
118
        return first_kind
119
    return "oak" if fallback_subject.startswith("oak") else fallback_subject
120
121
122
def subjects_for_kind(
123
    groups: dict[GroupKey, list[dict[str, Any]]],
124
    profile: Optional[str],
125
    scenario: str,
126
    operation: str,
127
    wanted_kind: str,
128
) -> set[str]:
129
    subjects: set[str] = set()
130
    for (row_profile, row_scenario, row_operation, subject), rows in groups.items():
131
        if (row_profile, row_scenario, row_operation) != (profile, scenario, operation):
132
            continue
133
        if row_subject_kind(rows, subject) == wanted_kind:
134
            subjects.add(subject)
135
    return subjects
136
137
138
def format_key(key: GroupKey) -> str:
139
    profile, scenario, operation, subject = key
140
    return f"{profile or '-'}/{scenario}/{operation}/{subject}"
141
142
143
def parse_formatted_key(value: str) -> GroupKey:
144
    profile, scenario, operation, subject = value.split("/", 3)
145
    return (None if profile == "-" else profile, scenario, operation, subject)
146
147
148
def content_integrity_summary(
149
    rows: list[dict[str, Any]],
150
    source_strength: dict[str, int],
151
) -> dict[GroupKey, dict[str, Any]]:
152
    grouped: dict[GroupKey, dict[str, Any]] = {}
153
    for row in rows:
154
        source = row.get("content_integrity_source")
155
        sources = row.get("content_integrity_sources")
156
        if not isinstance(source, str) and not isinstance(sources, list):
157
            continue
158
        key = group_key(row)
159
        entry = grouped.setdefault(
160
            key,
161
            {
162
                "sources": set(),
163
                "rows": 0,
164
                "passed": 0,
165
                "failed": 0,
166
                "weakest_source_strength": None,
167
            },
168
        )
169
        row_sources = [value for value in ([source] + sources if isinstance(sources, list) else [source]) if isinstance(value, str)]
170
        for value in row_sources:
171
            entry["sources"].add(value)
172
            strength = int(source_strength.get(value, 0))
173
            current = entry["weakest_source_strength"]
174
            entry["weakest_source_strength"] = strength if current is None else min(current, strength)
175
        entry["rows"] += 1
176
        if row.get("content_integrity_check_passed") is True:
177
            entry["passed"] += 1
178
        elif row.get("content_integrity_check_passed") is False:
179
            entry["failed"] += 1
180
    return grouped
181
182
183
def intervals_overlap(a: tuple[float, float], b: tuple[float, float]) -> bool:
184
    return a[0] <= b[1] and b[0] <= a[1]
185
186
187
def interval_usable(interval: tuple[float, float]) -> bool:
188
    return all(math.isfinite(value) for value in interval)
189
190
191
def verify(
192
    bundle: dict[str, Any],
193
    replay_rows: list[dict[str, Any]],
194
    original_rows: list[dict[str, Any]] | None,
195
    original_hash_failures: list[str],
196
    uncovered_pinned_inputs: tuple[str, ...] = (),
197
    *,
198
    confidence: float = 0.95,
199
) -> tuple[str, list[str]]:
200
    details: list[str] = []
201
    divergent = False
202
    unverifiable = False
203
    ratio_checked = False
204
205
    replay_groups = group_rows(replay_rows)
206
    content_integrity = bundle.get("content_integrity") if isinstance(bundle.get("content_integrity"), dict) else {}
207
    source_strength = dict(DEFAULT_CONTENT_SOURCE_STRENGTH)
208
    if isinstance(content_integrity.get("source_strength"), dict):
209
        for key, value in content_integrity["source_strength"].items():
210
            try:
211
                source_strength[str(key)] = int(value)
212
            except (TypeError, ValueError):
213
                continue
214
    bundled_content_groups = content_integrity.get("groups", {})
215
    replay_content_groups = content_integrity_summary(replay_rows, source_strength)
216
    if isinstance(bundled_content_groups, dict):
217
        for raw_key, expected in sorted(bundled_content_groups.items()):
218
            if not isinstance(raw_key, str) or not isinstance(expected, dict):
219
                continue
220
            try:
221
                key = parse_formatted_key(raw_key)
222
            except ValueError:
223
                unverifiable = True
224
                details.append(f"{raw_key}: unverifiable:content_integrity_group_key_invalid")
225
                continue
226
            replay = replay_content_groups.get(key)
227
            if replay is None:
228
                unverifiable = True
229
                details.append(f"{raw_key}: unverifiable:content_integrity_absent_in_replay")
230
                continue
231
            expected_failed = int(expected.get("failed") or 0)
232
            replay_failed = int(replay.get("failed") or 0)
233
            if expected_failed == 0 and replay_failed:
234
                divergent = True
235
                details.append(f"{raw_key}: divergent:content_integrity_failed_in_replay")
236
            expected_strength = expected.get("weakest_source_strength")
237
            replay_strength = replay.get("weakest_source_strength")
238
            if isinstance(expected_strength, int) and isinstance(replay_strength, int) and replay_strength < expected_strength:
239
                divergent = True
240
                details.append(
241
                    f"{raw_key}: divergent:content_integrity_source_downgrade "
242
                    f"original_strength={expected_strength} replay_strength={replay_strength}"
243
                )
244
245
    for failure in original_hash_failures:
246
        divergent = True
247
        details.append(f"original {failure}: divergent:original_hash_not_pinned_in_bundle")
248
249
    # Every raw input pinned by the bundle must be covered by a hash-matching
250
    # --original file; otherwise machine-independent verification is
251
    # incomplete and the verdict caps at COMPARABLE.
252
    for pinned_path in uncovered_pinned_inputs:
253
        unverifiable = True
254
        details.append(f"unverifiable:bundle_input_not_provided:{pinned_path}")
255
256
    if original_rows is None:
257
        unverifiable = True
258
        for key in sorted(replay_groups, key=format_key):
259
            details.append(f"{format_key(key)}: unverifiable:original_rows_absent")
260
        if not replay_groups:
261
            details.append("(no successful replay groups): unverifiable:original_rows_absent")
262
    else:
263
        original_groups = group_rows(original_rows)
264
        all_keys = sorted(set(original_groups) | set(replay_groups), key=format_key)
265
        for key in all_keys:
266
            originals = original_groups.get(key)
267
            replays = replay_groups.get(key)
268
            if originals is None:
269
                divergent = True
270
                details.append(f"{format_key(key)}: divergent:group_missing_in_original")
271
                continue
272
            if replays is None:
273
                divergent = True
274
                details.append(f"{format_key(key)}: divergent:group_missing_in_replay")
275
                continue
276
            for metric, accessor in MACHINE_INDEPENDENT_METRICS:
277
                original_values = metric_values(originals, accessor)
278
                replay_values = metric_values(replays, accessor)
279
                if not original_values and not replay_values:
280
                    continue
281
                if not original_values or not replay_values:
282
                    unverifiable = True
283
                    side = "original" if not original_values else "replay"
284
                    details.append(f"{format_key(key)}: unverifiable:{metric}_unmeasured_in_{side}")
285
                    continue
286
                if original_values != replay_values:
287
                    divergent = True
288
                    details.append(
289
                        f"{format_key(key)}: divergent:{metric}_mismatch "
290
                        f"original={original_values} replay={replay_values}"
291
                    )
292
                else:
293
                    details.append(f"{format_key(key)}: {metric} exact-match (n={len(original_values)})")
294
295
        # Machine-dependent: oak/git elapsed-ratio CI overlap per
296
        # (profile, scenario, operation).
297
        ratio_keys = sorted(
298
            {
299
                (profile, scenario, operation)
300
                for (profile, scenario, operation, _subject) in set(original_groups) & set(replay_groups)
301
            }
302
        , key=lambda key: (key[0] or "", key[1], key[2]))
303
        for profile, scenario, operation in ratio_keys:
304
            common_oak_subjects = (
305
                subjects_for_kind(original_groups, profile, scenario, operation, "oak")
306
                & subjects_for_kind(replay_groups, profile, scenario, operation, "oak")
307
            )
308
            common_git_subjects = (
309
                subjects_for_kind(original_groups, profile, scenario, operation, "git")
310
                & subjects_for_kind(replay_groups, profile, scenario, operation, "git")
311
            )
312
            if "git" in common_git_subjects:
313
                common_git_subjects = {"git"}
314
            checked_for_operation = False
315
            for oak_subject in sorted(common_oak_subjects):
316
                for git_subject in sorted(common_git_subjects):
317
                    sides: list[tuple[float, float]] = []
318
                    for groups in (original_groups, replay_groups):
319
                        numerator = elapsed_values(groups.get((profile, scenario, operation, oak_subject), []))
320
                        denominator = elapsed_values(groups.get((profile, scenario, operation, git_subject), []))
321
                        if not numerator or not denominator:
322
                            break
323
                        sides.append(ratio_ci(numerator, denominator, statistic="median", confidence=confidence))
324
                    label = f"{profile or '-'}/{scenario}/{operation}/{oak_subject}_vs_{git_subject}"
325
                    if len(sides) != 2 or not all(interval_usable(side) for side in sides):
326
                        details.append(f"{label}: elapsed_ms ratio unavailable (missing oak/git elapsed data)")
327
                        continue
328
                    checked_for_operation = True
329
                    original_ci, replay_ci = sides
330
                    if intervals_overlap(original_ci, replay_ci):
331
                        ratio_checked = True
332
                        details.append(
333
                            f"{label}: elapsed_ms oak/git ratio CIs overlap "
334
                            f"original=[{original_ci[0]:.4f}, {original_ci[1]:.4f}] "
335
                            f"replay=[{replay_ci[0]:.4f}, {replay_ci[1]:.4f}]"
336
                        )
337
                    else:
338
                        divergent = True
339
                        details.append(
340
                            f"{label}: divergent:elapsed_ratio_ci_no_overlap "
341
                            f"original=[{original_ci[0]:.4f}, {original_ci[1]:.4f}] "
342
                            f"replay=[{replay_ci[0]:.4f}, {replay_ci[1]:.4f}]"
343
                        )
344
            if not checked_for_operation:
345
                label = f"{profile or '-'}/{scenario}/{operation}"
346
                details.append(f"{label}: elapsed_ms ratio unavailable (missing oak/git elapsed data)")
347
348
    if divergent:
349
        return "DIVERGENT", details
350
    if unverifiable:
351
        return "COMPARABLE", details
352
    if ratio_checked:
353
        return "VERIFIED-EXACT", details
354
    return "VERIFIED", details
355
356
357
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
358
    parser = argparse.ArgumentParser(description="Verify a third-party replay against a repro bundle.")
359
    parser.add_argument("replay", nargs="+", type=Path, help="replay row JSONL paths (the third party's rerun)")
360
    parser.add_argument("--bundle", type=Path, required=True, help="repro bundle JSON produced by repro_bundle.py")
361
    parser.add_argument(
362
        "--original",
363
        action="append",
364
        default=None,
365
        type=Path,
366
        dest="originals",
367
        help="original raw row JSONL path pinned by the bundle (repeatable)",
368
    )
369
    parser.add_argument("--confidence", type=float, default=0.95, help="ratio bootstrap CI confidence")
370
    return parser.parse_args(argv)
371
372
373
def main(argv: list[str] | None = None) -> int:
374
    args = parse_args(argv)
375
376
    if not args.bundle.is_file():
377
        print(f"error: bundle not found: {args.bundle}", file=sys.stderr)
378
        return 2
379
    try:
380
        bundle = json.loads(args.bundle.read_text())
381
    except json.JSONDecodeError as exc:
382
        print(f"error: bundle is not valid JSON: {exc}", file=sys.stderr)
383
        return 2
384
    if bundle.get("schema_version") != 1:
385
        print(f"error: unsupported bundle schema_version: {bundle.get('schema_version')!r}", file=sys.stderr)
386
        return 2
387
388
    missing = [str(path) for path in args.replay if not path.is_file()]
389
    if args.originals:
390
        missing.extend(str(path) for path in args.originals if not path.is_file())
391
    if missing:
392
        print(f"error: row file(s) not found: {', '.join(missing)}", file=sys.stderr)
393
        return 2
394
395
    replay_rows = load_jsonl(args.replay)
396
397
    raw_inputs = bundle.get("raw_inputs", [])
398
    pinned_hashes = {entry.get("sha256") for entry in raw_inputs}
399
    provided_hashes: set[str] = set()
400
    original_rows: list[dict[str, Any]] | None = None
401
    original_hash_failures: list[str] = []
402
    if args.originals:
403
        for path in args.originals:
404
            digest = sha256_file(path)
405
            if digest in pinned_hashes:
406
                provided_hashes.add(digest)
407
            else:
408
                original_hash_failures.append(str(path))
409
        original_rows = load_jsonl(args.originals)
410
    uncovered_pinned_inputs = tuple(
411
        str(entry.get("path"))
412
        for entry in raw_inputs
413
        if entry.get("sha256") not in provided_hashes
414
    )
415
416
    verdict, details = verify(
417
        bundle,
418
        replay_rows,
419
        original_rows,
420
        original_hash_failures,
421
        uncovered_pinned_inputs,
422
        confidence=args.confidence,
423
    )
424
    for detail in details:
425
        print(detail)
426
    print(f"verdict: {verdict}")
427
    return VERDICT_EXIT_CODES[verdict]
428
429
430
if __name__ == "__main__":
431
    sys.exit(main())