| 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())
|