Viewing files at commit 62758990fe31Commit detailsView latest

Browse files
scripts/repro_verify.py 432 lines · 17.9 KB · python
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
    "oak_independent_reader_payload_sha256": 3,
71
    "git_lfs_worktree_payload_sha256": 1,
72
    "worktree_payload_sha256": 1,
73
    "mixed_payload_sources": 1,
74
}
75
76
77
def load_jsonl(paths: list[Path]) -> list[dict[str, Any]]:
78
    rows: list[dict[str, Any]] = []
79
    for path in paths:
80
        with path.open() as fh:
81
            for raw_line in fh:
82
                line = raw_line.strip()
83
                if line:
84
                    rows.append(json.loads(line))
85
    return rows
86
87
88
def group_key(row: dict[str, Any]) -> GroupKey:
89
    profile = row.get("profile")
90
    return (
91
        None if profile is None else str(profile),
92
        str(row.get("scenario") or ""),
93
        str(row.get("operation") or ""),
94
        str(row.get("subject") or ""),
95
    )
96
97
98
def group_rows(rows: list[dict[str, Any]]) -> dict[GroupKey, list[dict[str, Any]]]:
99
    grouped: dict[GroupKey, list[dict[str, Any]]] = {}
100
    for row in rows:
101
        if row_returncode(row) != 0:
102
            continue
103
        grouped.setdefault(group_key(row), []).append(row)
104
    return grouped
105
106
107
def metric_values(rows: list[dict[str, Any]], accessor: Callable[[dict[str, Any]], Optional[float]]) -> list[float]:
108
    values = [value for row in rows if (value := accessor(row)) is not None]
109
    return sorted(values)
110
111
112
def elapsed_values(rows: list[dict[str, Any]]) -> list[float]:
113
    return [value for row in rows if (value := number_or_none(row.get("elapsed_ms"))) is not None]
114
115
116
def row_subject_kind(rows: list[dict[str, Any]], fallback_subject: str) -> str:
117
    first_kind = rows[0].get("subject_kind") if rows else None
118
    if isinstance(first_kind, str) and first_kind:
119
        return first_kind
120
    return "oak" if fallback_subject.startswith("oak") else fallback_subject
121
122
123
def subjects_for_kind(
124
    groups: dict[GroupKey, list[dict[str, Any]]],
125
    profile: Optional[str],
126
    scenario: str,
127
    operation: str,
128
    wanted_kind: str,
129
) -> set[str]:
130
    subjects: set[str] = set()
131
    for (row_profile, row_scenario, row_operation, subject), rows in groups.items():
132
        if (row_profile, row_scenario, row_operation) != (profile, scenario, operation):
133
            continue
134
        if row_subject_kind(rows, subject) == wanted_kind:
135
            subjects.add(subject)
136
    return subjects
137
138
139
def format_key(key: GroupKey) -> str:
140
    profile, scenario, operation, subject = key
141
    return f"{profile or '-'}/{scenario}/{operation}/{subject}"
142
143
144
def parse_formatted_key(value: str) -> GroupKey:
145
    profile, scenario, operation, subject = value.split("/", 3)
146
    return (None if profile == "-" else profile, scenario, operation, subject)
147
148
149
def content_integrity_summary(
150
    rows: list[dict[str, Any]],
151
    source_strength: dict[str, int],
152
) -> dict[GroupKey, dict[str, Any]]:
153
    grouped: dict[GroupKey, dict[str, Any]] = {}
154
    for row in rows:
155
        source = row.get("content_integrity_source")
156
        sources = row.get("content_integrity_sources")
157
        if not isinstance(source, str) and not isinstance(sources, list):
158
            continue
159
        key = group_key(row)
160
        entry = grouped.setdefault(
161
            key,
162
            {
163
                "sources": set(),
164
                "rows": 0,
165
                "passed": 0,
166
                "failed": 0,
167
                "weakest_source_strength": None,
168
            },
169
        )
170
        row_sources = [value for value in ([source] + sources if isinstance(sources, list) else [source]) if isinstance(value, str)]
171
        for value in row_sources:
172
            entry["sources"].add(value)
173
            strength = int(source_strength.get(value, 0))
174
            current = entry["weakest_source_strength"]
175
            entry["weakest_source_strength"] = strength if current is None else min(current, strength)
176
        entry["rows"] += 1
177
        if row.get("content_integrity_check_passed") is True:
178
            entry["passed"] += 1
179
        elif row.get("content_integrity_check_passed") is False:
180
            entry["failed"] += 1
181
    return grouped
182
183
184
def intervals_overlap(a: tuple[float, float], b: tuple[float, float]) -> bool:
185
    return a[0] <= b[1] and b[0] <= a[1]
186
187
188
def interval_usable(interval: tuple[float, float]) -> bool:
189
    return all(math.isfinite(value) for value in interval)
190
191
192
def verify(
193
    bundle: dict[str, Any],
194
    replay_rows: list[dict[str, Any]],
195
    original_rows: list[dict[str, Any]] | None,
196
    original_hash_failures: list[str],
197
    uncovered_pinned_inputs: tuple[str, ...] = (),
198
    *,
199
    confidence: float = 0.95,
200
) -> tuple[str, list[str]]:
201
    details: list[str] = []
202
    divergent = False
203
    unverifiable = False
204
    ratio_checked = False
205
206
    replay_groups = group_rows(replay_rows)
207
    content_integrity = bundle.get("content_integrity") if isinstance(bundle.get("content_integrity"), dict) else {}
208
    source_strength = dict(DEFAULT_CONTENT_SOURCE_STRENGTH)
209
    if isinstance(content_integrity.get("source_strength"), dict):
210
        for key, value in content_integrity["source_strength"].items():
211
            try:
212
                source_strength[str(key)] = int(value)
213
            except (TypeError, ValueError):
214
                continue
215
    bundled_content_groups = content_integrity.get("groups", {})
216
    replay_content_groups = content_integrity_summary(replay_rows, source_strength)
217
    if isinstance(bundled_content_groups, dict):
218
        for raw_key, expected in sorted(bundled_content_groups.items()):
219
            if not isinstance(raw_key, str) or not isinstance(expected, dict):
220
                continue
221
            try:
222
                key = parse_formatted_key(raw_key)
223
            except ValueError:
224
                unverifiable = True
225
                details.append(f"{raw_key}: unverifiable:content_integrity_group_key_invalid")
226
                continue
227
            replay = replay_content_groups.get(key)
228
            if replay is None:
229
                unverifiable = True
230
                details.append(f"{raw_key}: unverifiable:content_integrity_absent_in_replay")
231
                continue
232
            expected_failed = int(expected.get("failed") or 0)
233
            replay_failed = int(replay.get("failed") or 0)
234
            if expected_failed == 0 and replay_failed:
235
                divergent = True
236
                details.append(f"{raw_key}: divergent:content_integrity_failed_in_replay")
237
            expected_strength = expected.get("weakest_source_strength")
238
            replay_strength = replay.get("weakest_source_strength")
239
            if isinstance(expected_strength, int) and isinstance(replay_strength, int) and replay_strength < expected_strength:
240
                divergent = True
241
                details.append(
242
                    f"{raw_key}: divergent:content_integrity_source_downgrade "
243
                    f"original_strength={expected_strength} replay_strength={replay_strength}"
244
                )
245
246
    for failure in original_hash_failures:
247
        divergent = True
248
        details.append(f"original {failure}: divergent:original_hash_not_pinned_in_bundle")
249
250
    # Every raw input pinned by the bundle must be covered by a hash-matching
251
    # --original file; otherwise machine-independent verification is
252
    # incomplete and the verdict caps at COMPARABLE.
253
    for pinned_path in uncovered_pinned_inputs:
254
        unverifiable = True
255
        details.append(f"unverifiable:bundle_input_not_provided:{pinned_path}")
256
257
    if original_rows is None:
258
        unverifiable = True
259
        for key in sorted(replay_groups, key=format_key):
260
            details.append(f"{format_key(key)}: unverifiable:original_rows_absent")
261
        if not replay_groups:
262
            details.append("(no successful replay groups): unverifiable:original_rows_absent")
263
    else:
264
        original_groups = group_rows(original_rows)
265
        all_keys = sorted(set(original_groups) | set(replay_groups), key=format_key)
266
        for key in all_keys:
267
            originals = original_groups.get(key)
268
            replays = replay_groups.get(key)
269
            if originals is None:
270
                divergent = True
271
                details.append(f"{format_key(key)}: divergent:group_missing_in_original")
272
                continue
273
            if replays is None:
274
                divergent = True
275
                details.append(f"{format_key(key)}: divergent:group_missing_in_replay")
276
                continue
277
            for metric, accessor in MACHINE_INDEPENDENT_METRICS:
278
                original_values = metric_values(originals, accessor)
279
                replay_values = metric_values(replays, accessor)
280
                if not original_values and not replay_values:
281
                    continue
282
                if not original_values or not replay_values:
283
                    unverifiable = True
284
                    side = "original" if not original_values else "replay"
285
                    details.append(f"{format_key(key)}: unverifiable:{metric}_unmeasured_in_{side}")
286
                    continue
287
                if original_values != replay_values:
288
                    divergent = True
289
                    details.append(
290
                        f"{format_key(key)}: divergent:{metric}_mismatch "
291
                        f"original={original_values} replay={replay_values}"
292
                    )
293
                else:
294
                    details.append(f"{format_key(key)}: {metric} exact-match (n={len(original_values)})")
295
296
        # Machine-dependent: oak/git elapsed-ratio CI overlap per
297
        # (profile, scenario, operation).
298
        ratio_keys = sorted(
299
            {
300
                (profile, scenario, operation)
301
                for (profile, scenario, operation, _subject) in set(original_groups) & set(replay_groups)
302
            }
303
        , key=lambda key: (key[0] or "", key[1], key[2]))
304
        for profile, scenario, operation in ratio_keys:
305
            common_oak_subjects = (
306
                subjects_for_kind(original_groups, profile, scenario, operation, "oak")
307
                & subjects_for_kind(replay_groups, profile, scenario, operation, "oak")
308
            )
309
            common_git_subjects = (
310
                subjects_for_kind(original_groups, profile, scenario, operation, "git")
311
                & subjects_for_kind(replay_groups, profile, scenario, operation, "git")
312
            )
313
            if "git" in common_git_subjects:
314
                common_git_subjects = {"git"}
315
            checked_for_operation = False
316
            for oak_subject in sorted(common_oak_subjects):
317
                for git_subject in sorted(common_git_subjects):
318
                    sides: list[tuple[float, float]] = []
319
                    for groups in (original_groups, replay_groups):
320
                        numerator = elapsed_values(groups.get((profile, scenario, operation, oak_subject), []))
321
                        denominator = elapsed_values(groups.get((profile, scenario, operation, git_subject), []))
322
                        if not numerator or not denominator:
323
                            break
324
                        sides.append(ratio_ci(numerator, denominator, statistic="median", confidence=confidence))
325
                    label = f"{profile or '-'}/{scenario}/{operation}/{oak_subject}_vs_{git_subject}"
326
                    if len(sides) != 2 or not all(interval_usable(side) for side in sides):
327
                        details.append(f"{label}: elapsed_ms ratio unavailable (missing oak/git elapsed data)")
328
                        continue
329
                    checked_for_operation = True
330
                    original_ci, replay_ci = sides
331
                    if intervals_overlap(original_ci, replay_ci):
332
                        ratio_checked = True
333
                        details.append(
334
                            f"{label}: elapsed_ms oak/git ratio CIs overlap "
335
                            f"original=[{original_ci[0]:.4f}, {original_ci[1]:.4f}] "
336
                            f"replay=[{replay_ci[0]:.4f}, {replay_ci[1]:.4f}]"
337
                        )
338
                    else:
339
                        divergent = True
340
                        details.append(
341
                            f"{label}: divergent:elapsed_ratio_ci_no_overlap "
342
                            f"original=[{original_ci[0]:.4f}, {original_ci[1]:.4f}] "
343
                            f"replay=[{replay_ci[0]:.4f}, {replay_ci[1]:.4f}]"
344
                        )
345
            if not checked_for_operation:
346
                label = f"{profile or '-'}/{scenario}/{operation}"
347
                details.append(f"{label}: elapsed_ms ratio unavailable (missing oak/git elapsed data)")
348
349
    if divergent:
350
        return "DIVERGENT", details
351
    if unverifiable:
352
        return "COMPARABLE", details
353
    if ratio_checked:
354
        return "VERIFIED-EXACT", details
355
    return "VERIFIED", details
356
357
358
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
359
    parser = argparse.ArgumentParser(description="Verify a third-party replay against a repro bundle.")
360
    parser.add_argument("replay", nargs="+", type=Path, help="replay row JSONL paths (the third party's rerun)")
361
    parser.add_argument("--bundle", type=Path, required=True, help="repro bundle JSON produced by repro_bundle.py")
362
    parser.add_argument(
363
        "--original",
364
        action="append",
365
        default=None,
366
        type=Path,
367
        dest="originals",
368
        help="original raw row JSONL path pinned by the bundle (repeatable)",
369
    )
370
    parser.add_argument("--confidence", type=float, default=0.95, help="ratio bootstrap CI confidence")
371
    return parser.parse_args(argv)
372
373
374
def main(argv: list[str] | None = None) -> int:
375
    args = parse_args(argv)
376
377
    if not args.bundle.is_file():
378
        print(f"error: bundle not found: {args.bundle}", file=sys.stderr)
379
        return 2
380
    try:
381
        bundle = json.loads(args.bundle.read_text())
382
    except json.JSONDecodeError as exc:
383
        print(f"error: bundle is not valid JSON: {exc}", file=sys.stderr)
384
        return 2
385
    if bundle.get("schema_version") != 1:
386
        print(f"error: unsupported bundle schema_version: {bundle.get('schema_version')!r}", file=sys.stderr)
387
        return 2
388
389
    missing = [str(path) for path in args.replay if not path.is_file()]
390
    if args.originals:
391
        missing.extend(str(path) for path in args.originals if not path.is_file())
392
    if missing:
393
        print(f"error: row file(s) not found: {', '.join(missing)}", file=sys.stderr)
394
        return 2
395
396
    replay_rows = load_jsonl(args.replay)
397
398
    raw_inputs = bundle.get("raw_inputs", [])
399
    pinned_hashes = {entry.get("sha256") for entry in raw_inputs}
400
    provided_hashes: set[str] = set()
401
    original_rows: list[dict[str, Any]] | None = None
402
    original_hash_failures: list[str] = []
403
    if args.originals:
404
        for path in args.originals:
405
            digest = sha256_file(path)
406
            if digest in pinned_hashes:
407
                provided_hashes.add(digest)
408
            else:
409
                original_hash_failures.append(str(path))
410
        original_rows = load_jsonl(args.originals)
411
    uncovered_pinned_inputs = tuple(
412
        str(entry.get("path"))
413
        for entry in raw_inputs
414
        if entry.get("sha256") not in provided_hashes
415
    )
416
417
    verdict, details = verify(
418
        bundle,
419
        replay_rows,
420
        original_rows,
421
        original_hash_failures,
422
        uncovered_pinned_inputs,
423
        confidence=args.confidence,
424
    )
425
    for detail in details:
426
        print(detail)
427
    print(f"verdict: {verdict}")
428
    return VERDICT_EXIT_CODES[verdict]
429
430
431
if __name__ == "__main__":
432
    sys.exit(main())