Runners Log in

oak/benchmarks

scripts/devloop.py 1185 lines · 59.6 KB · python
1
#!/usr/bin/env python3
2
"""One command, one verdict: is this Oak changeset better, same, or worse?
3
4
The inner loop for agents developing Oak against this suite:
5
6
1. Resolve (or `cargo build --release`) the local Oak binary from the Oak
7
   source checkout.
8
2. Run the comparison lanes (core, workflow, contention) with git, the
9
   installed Oak baseline, an A/A control (the SAME baseline binary under the
10
   subject name ``oak_installed_aa``) and the local changeset, all interleaved
11
   in randomized order within every run.
12
3. Judge every (scenario, operation) cell on its own evidence
13
   (oakbench.cellstats): the cell's A/A floor, a bootstrap CI of the relative
14
   median delta, an exact permutation p-value and Benjamini-Hochberg across
15
   the comparison family. A latency change is claimed only when BH rejects,
16
   the CI excludes zero and lies wholly beyond the cell's A/A floor. There is
17
   no global threshold imported from another profile.
18
4. Run the roundtrip conformance matrix (lane ``conformance``) against the
19
   local changeset: a behaviour that newly fails vs
20
   config/conformance_expected_failures.json is REGRESSED; harness errors or
21
   missing behaviours are UNMEASURED; a listed failure that now passes is
22
   reported as FIXED (update the list).
23
5. Print a one-screen verdict and exit 0 (PASS), 1 (REGRESSED), or 2 (UNMEASURED).
24
25
Examples:
26
27
    python3 scripts/devloop.py                       # build ../oak, full quick loop
28
    python3 scripts/devloop.py --oak-local-bin ../oak/target/release/oak --skip-build
29
    python3 scripts/devloop.py --lanes core          # fastest signal only
30
31
Thresholds follow the suite's regression policy: efficiency gates (exact
32
metrics: tool calls, output bytes) apply to the Oak-baseline comparison only
33
(ADR-0004); the Git comparison is a latency guardrail.
34
"""
35
36
from __future__ import annotations
37
38
import argparse
39
import dataclasses
40
import json
41
import shutil
42
import statistics
43
import subprocess
44
import sys
45
import time
46
from datetime import datetime, timezone
47
from pathlib import Path
48
from typing import Any, Optional
49
50
from oakbench import cellstats
51
from oakbench import remotes as oakbench_remotes
52
from oakbench.reporting import percentile_nearest, slower_pct, symmetric_ratio_spread_pct
53
from oakbench.rows import output_bytes as row_output_bytes
54
from oakbench.rows import row_returncode
55
from oakbench.rows import token_total as row_token_total
56
from oakbench.rows import tool_calls as row_tool_calls
57
from oakbench.subjects import DEFAULT_OAK_REPO
58
59
ROOT = Path(__file__).resolve().parents[1]
60
SCRIPTS = ROOT / "scripts"
61
LANES = ("core", "workflow", "contention", "conformance")
62
DEFAULT_CONTENTION_WORKERS = "2,8"
63
DEFAULT_CONTENTION_COMMITS_PER_WORKER = 3
64
DEFAULT_CONTENTION_MODES = "shared_checkout,workspace_per_task,clone_push_storm"
65
SKIP_RETURNCODE = 77
66
AA_SUBJECT = "oak_installed_aa"
67
68
# Operations excluded from the noise floor: process spawn and sub-steps have
69
# the largest relative jitter and are not the deltas the verdict gates on.
70
NOISE_FLOOR_EXCLUDED = ("proc.spawn",)
71
NOISE_FLOOR_MIN_PCT = 3.0
72
73
74
def parse_args() -> argparse.Namespace:
75
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
76
    parser.add_argument("--oak-repo", type=Path, default=DEFAULT_OAK_REPO)
77
    parser.add_argument("--oak-local-bin", type=Path, help="Use this binary instead of building --oak-repo.")
78
    parser.add_argument(
79
        "--oak-baseline-bin",
80
        type=Path,
81
        help="Oak baseline binary (default: `oak` on PATH = released install).",
82
    )
83
    parser.add_argument("--skip-build", action="store_true", help="Never run cargo; require --oak-local-bin.")
84
    parser.add_argument("--lanes", default="core,workflow,contention,conformance", help=f"Comma list from: {', '.join(LANES)}")
85
    parser.add_argument(
86
        "--contention-workers",
87
        default=DEFAULT_CONTENTION_WORKERS,
88
        help="Comma-separated worker tiers for the contention lane; launch gates use 16,32,64,128.",
89
    )
90
    parser.add_argument(
91
        "--contention-commits-per-worker",
92
        type=int,
93
        default=DEFAULT_CONTENTION_COMMITS_PER_WORKER,
94
    )
95
    parser.add_argument(
96
        "--contention-modes",
97
        default=DEFAULT_CONTENTION_MODES,
98
        help="Comma-separated parallel_contention modes to run in devloop.",
99
    )
100
    parser.add_argument("--runs", type=int, default=5,
101
                        help="Repetitions per comparison lane (default 5; per-cell statistics need >= 3).")
102
    parser.add_argument("--noise-floor-runs", type=int, default=5,
103
                        help="Repetitions for the optional legacy global micro A/A (--global-noise-floor).")
104
    parser.add_argument("--skip-noise-floor", action="store_true",
105
                        help="Omit the in-lane A/A control subject; every latency cell is then UNMEASURED.")
106
    parser.add_argument("--global-noise-floor", action="store_true",
107
                        help="Also run the legacy micro-profile A/A (informational; never gates a cell).")
108
    parser.add_argument("--core-profiles", default="smoke",
109
                        help="Comma list of bench.py profiles for the core lane (e.g. micro,smoke).")
110
    parser.add_argument("--fdr", type=float, default=cellstats.DEFAULT_FDR,
111
                        help="Benjamini-Hochberg false discovery rate across each comparison family.")
112
    parser.add_argument("--results", type=Path, default=ROOT / "results" / "devloop")
113
    parser.add_argument("--oak-threshold-pct", type=float, default=10.0)
114
    parser.add_argument("--git-threshold-pct", type=float, default=25.0)
115
    parser.add_argument("--min-delta-ms", type=float, default=0.0,
116
                        help="Optional absolute floor in ms (default 0: each cell's A/A floor and CI decide).")
117
    parser.add_argument("--confirm-runs", type=int, default=20,
118
                        help="Runs per arm for the independent confirmation stage on screened cells (core lane).")
119
    parser.add_argument("--confirm-budget-s", type=float, default=900.0,
120
                        help="Wall-time budget for the confirmation stage; fewer runs are used if needed.")
121
    parser.add_argument("--target-mde-pct", type=float, default=20.0,
122
                        help="Cells whose minimum detectable effect exceeds this are re-measured in confirmation.")
123
    parser.add_argument("--no-confirm", action="store_true",
124
                        help="Skip the confirmation stage (unconfirmed candidates then make the verdict UNMEASURED).")
125
    parser.add_argument("--output-bytes-threshold-pct", type=float, default=25.0)
126
    parser.add_argument(
127
        "--scorecard",
128
        action="store_true",
129
        help="Append a target scorecard vs the CURRENT Git Baseline Book (informational; never changes the verdict exit code).",
130
    )
131
    return parser.parse_args()
132
133
134
def scorecard_lines(rows: list[dict[str, Any]], root: Path = ROOT, noise_floor_pct: float | None = None,
135
                    cell_floors: dict[tuple[str, str], float] | None = None) -> list[str]:
136
    """Target-TDD lines for the devloop report; regression-TDD owns the exit code."""
137
    targets_path = root / "config" / "targets.json"
138
    if not targets_path.exists():
139
        return ["- no targets config (config/targets.json missing)"]
140
    current = root / "baselines" / "git" / "CURRENT"
141
    if not current.exists():
142
        return ["- no baseline book (baselines/git/CURRENT missing); run scripts/baseline_campaign.py run"]
143
    book_id = current.read_text().strip()
144
    book_path = root / "baselines" / "git" / book_id / "book.json"
145
    if not book_path.exists():
146
        return [f"- baseline book {book_id!r} is named by CURRENT but has no book.json"]
147
    from oakbench.baseline_book import load_baseline_book, load_targets
148
    from oakbench.scorecard import evaluate_targets
149
150
    baseline = load_baseline_book(book_path)
151
    if noise_floor_pct is not None:
152
        baseline = {
153
            key: dataclasses.replace(
154
                cell,
155
                noise_floor_pct=max(float(cell.noise_floor_pct or 0.0), float(noise_floor_pct)),
156
            )
157
            for key, cell in baseline.items()
158
        }
159
    if cell_floors:
160
        # Each scorecard cell's noise floor is at least that cell's own A/A floor.
161
        baseline = {
162
            key: dataclasses.replace(
163
                cell,
164
                noise_floor_pct=max(float(cell.noise_floor_pct or 0.0),
165
                                    cell_floors.get((str(getattr(cell, "scenario", "")), str(getattr(cell, "operation", ""))), 0.0)),
166
            )
167
            for key, cell in baseline.items()
168
        }
169
    results = evaluate_targets(rows, load_targets(targets_path), baseline)
170
    lines = []
171
    for item in results:
172
        ratio = item.get("ratio")
173
        ratio_text = "" if ratio is None else f" ratio {ratio:.3f} vs goal {item['goal_value']:.3f}"
174
        notes = "; ".join(str(note) for note in item.get("messages") or [])
175
        lines.append(f"- {item['verdict']} `{item['target_id']}`{ratio_text}" + (f" — {notes}" if notes else ""))
176
    return lines or ["- no targets defined"]
177
178
179
def conformance_checks(rows: list[dict[str, Any]], expected_path: Path | None = None) -> tuple[list[str], list[str]]:
180
    """(regressions, measurement notes) from conformance rows vs the expected-failures list."""
181
    from oakbench import conformance_gate
182
    from oakbench.conformance import SCENARIOS
183
    from oakbench.conformance_matrix import BEHAVIOURS
184
    expected = conformance_gate.load_expected(expected_path or conformance_gate.EXPECTED_FAILURES)
185
    required = list(SCENARIOS) + [behaviour.id for behaviour in BEHAVIOURS]
186
    result = conformance_gate.evaluate(rows, expected, required)
187
    regressions = [f"[conformance] CONFORMANCE {item}" for item in result["regressions"]]
188
    notes = [f"UNMEASURED conformance: {item}" for item in result["unmeasured"]]
189
    notes += [f"INFO conformance FIXED {item}" for item in result["fixed"]]
190
    notes.append(f"INFO {conformance_gate.summary_lines(result)[0]}")
191
    return regressions, notes
192
193
194
def run_lane(command: list[str], label: str) -> float:
195
    print(f"[devloop] {label}: {' '.join(str(part) for part in command)}", flush=True)
196
    started = time.perf_counter()
197
    proc = subprocess.run(command, cwd=ROOT, check=False)
198
    # Lane exit codes signal benchmark-operation failures; the verdict reads
199
    # those from the rows, so a nonzero lane exit is not fatal here.
200
    if proc.returncode not in (0, 1):
201
        raise SystemExit(f"{label} failed to run (exit {proc.returncode})")
202
    return time.perf_counter() - started
203
204
205
def breach_cell(line: str) -> tuple[str, str] | None:
206
    """(scenario, operation) named in a compare_subjects line's first `a/b` span."""
207
    start = line.find("`")
208
    end = line.find("`", start + 1)
209
    if start < 0 or end < 0 or "/" not in line[start + 1:end]:
210
        return None
211
    scenario, operation = line[start + 1:end].split("/", 1)
212
    return scenario, operation
213
214
215
def measured_by_oak(rows: list[dict[str, Any]], key: tuple[str, str]) -> bool:
216
    """True when either Oak arm has a measured (non-skip) row for the cell."""
217
    return any(row.get("subject") in ("oak_local", "oak_installed") and not row.get("skipped")
218
               and (str(row.get("scenario")), str(row.get("operation"))) == key for row in rows)
219
220
221
def confirmation_plan(candidates: list[tuple[str, str]], stage1_seconds: float, stage1_runs: int,
222
                      scenario_count: int, confirm_runs: int, budget_s: float) -> tuple[list[str], int]:
223
    """(scenarios to re-run, runs per arm) within the wall-time budget."""
224
    scenarios = sorted({scenario for scenario, _ in candidates})
225
    if not scenarios:
226
        return [], 0
227
    per_scenario_run = stage1_seconds / max(1, stage1_runs * max(1, scenario_count))
228
    affordable = int(budget_s // max(1e-6, per_scenario_run * len(scenarios)))
229
    return scenarios, min(confirm_runs, affordable)
230
231
232
def _csv(value: str) -> list[str]:
233
    return [item.strip() for item in value.split(",") if item.strip()]
234
235
236
def remote_preflight_requirements(
237
    lanes: list[str],
238
    *,
239
    contention_modes: list[str] | None = None,
240
    contention_workers: list[str] | None = None,
241
) -> list[oakbench_remotes.RemoteRequirement]:
242
    requirements: list[oakbench_remotes.RemoteRequirement] = []
243
    if "core" in lanes:
244
        requirements.append(
245
            oakbench_remotes.RemoteRequirement(
246
                purpose="default",
247
                lane="devloop/core",
248
                operations=(
249
                    "remote.net.push.first",
250
                    "remote.net.clone.cold",
251
                    "remote.net.fetch.uptodate",
252
                ),
253
            )
254
        )
255
    if "workflow" in lanes:
256
        requirements.append(
257
            oakbench_remotes.RemoteRequirement(
258
                purpose="sync",
259
                lane="devloop/workflow",
260
                operations=(
261
                    "sync_push_divergence/workflow.skipped",
262
                    "sync_pull_upstream/workflow.skipped",
263
                    "sync_pull_dirty/workflow.skipped",
264
                    "sync_non_ff_after_amend/workflow.skipped",
265
                ),
266
            )
267
        )
268
    if "contention" in lanes:
269
        modes = contention_modes or _csv(DEFAULT_CONTENTION_MODES)
270
        workers = contention_workers or _csv(DEFAULT_CONTENTION_WORKERS)
271
        operations: list[str] = []
272
        if "workspace_per_task" in modes:
273
            operations.extend(
274
                f"contention_workspace_per_task_w{workers_level}/parallel.total"
275
                for workers_level in workers
276
            )
277
        if "clone_push_storm" in modes:
278
            operations.extend(
279
                f"contention_clone_push_storm_w{workers_level}/parallel.total"
280
                for workers_level in workers
281
            )
282
        if operations:
283
            requirements.append(
284
                oakbench_remotes.RemoteRequirement(
285
                    purpose="fleet",
286
                    lane="devloop/contention",
287
                    operations=tuple(operations),
288
                )
289
            )
290
    return requirements
291
292
293
def build_oak_local(oak_repo: Path) -> Path:
294
    cargo_toml = oak_repo / "Cargo.toml"
295
    if not cargo_toml.exists():
296
        raise SystemExit(
297
            f"No Cargo.toml at {oak_repo}; pass --oak-local-bin or --oak-repo. "
298
            "devloop never mutates the Oak source checkout, it only builds it."
299
        )
300
    if shutil.which("cargo") is None:
301
        raise SystemExit("cargo is not installed; pass --oak-local-bin instead of building.")
302
    print(f"[devloop] building oak from {oak_repo} (cargo build --release)", flush=True)
303
    start = time.perf_counter()
304
    proc = subprocess.run(
305
        ["cargo", "build", "--release"],
306
        cwd=oak_repo,
307
        text=True,
308
        stdout=subprocess.PIPE,
309
        stderr=subprocess.STDOUT,
310
        check=False,
311
    )
312
    elapsed = time.perf_counter() - start
313
    if proc.returncode != 0:
314
        tail = "\n".join(proc.stdout.splitlines()[-25:])
315
        raise SystemExit(f"cargo build failed (exit {proc.returncode}) after {elapsed:.0f}s:\n{tail}")
316
    binary = oak_repo / "target" / "release" / "oak"
317
    if not binary.exists():
318
        raise SystemExit(f"build succeeded but {binary} does not exist")
319
    print(f"[devloop] built {binary} in {elapsed:.0f}s", flush=True)
320
    return binary
321
322
323
def load_jsonl(path: Path) -> list[dict[str, Any]]:
324
    try:
325
        text = path.read_text()
326
    except FileNotFoundError:
327
        raise SystemExit(f"[devloop] result JSONL missing: {path}") from None
328
    except OSError as exc:
329
        raise SystemExit(f"[devloop] result JSONL unreadable: {path}: {exc}") from None
330
331
    rows: list[dict[str, Any]] = []
332
    for line_number, line in enumerate(text.splitlines(), start=1):
333
        stripped = line.strip()
334
        if not stripped:
335
            continue
336
        try:
337
            row = json.loads(stripped)
338
        except json.JSONDecodeError as exc:
339
            raise SystemExit(
340
                f"[devloop] malformed result JSONL: {path}:{line_number}: {exc.msg}"
341
            ) from None
342
        if not isinstance(row, dict):
343
            raise SystemExit(f"[devloop] result JSONL row is not an object: {path}:{line_number}") from None
344
        rows.append(row)
345
346
    if not rows:
347
        raise SystemExit(f"[devloop] result JSONL contains no rows: {path}")
348
    return rows
349
350
351
def median_by_key(
352
    rows: list[dict[str, Any]], value_of, *, successful_only: bool = True
353
) -> dict[tuple[str, str, str], float]:
354
    grouped: dict[tuple[str, str, str], list[float]] = {}
355
    for row in rows:
356
        operation = str(row.get("operation") or "")
357
        if operation.endswith((".add", ".commit")):
358
            continue
359
        if successful_only and row_returncode(row) != 0:
360
            continue
361
        value = value_of(row)
362
        if value is None:
363
            continue
364
        key = (str(row.get("subject")), str(row.get("scenario")), operation)
365
        grouped.setdefault(key, []).append(float(value))
366
    return {key: float(statistics.median(values)) for key, values in grouped.items()}
367
368
369
def outcome_counts_by_key(rows: list[dict[str, Any]]) -> dict[tuple[str, str, str], dict[str, int]]:
370
    counts: dict[tuple[str, str, str], dict[str, int]] = {}
371
    for row in rows:
372
        operation = str(row.get("operation") or "")
373
        if operation.endswith((".add", ".commit")):
374
            continue
375
        key = (str(row.get("subject")), str(row.get("scenario")), operation)
376
        item = counts.setdefault(key, {"success": 0, "failure": 0, "skip": 0})
377
        returncode = row_returncode(row)
378
        if returncode == 0 and not row.get("skipped"):
379
            item["success"] += 1
380
        elif returncode == SKIP_RETURNCODE or row.get("skipped"):
381
            item["skip"] += 1
382
        else:
383
            item["failure"] += 1
384
    return counts
385
386
387
def diagnostic_operation(operation: str) -> bool:
388
    return operation.endswith((".determinism", ".inforecall"))
389
390
391
def measure_noise_floor(aa_dir: Path, baseline_bin: Path, runs: int) -> tuple[float, int]:
392
    """A/A null test: same binary as both subjects. Returns (p95 symmetric ratio spread, ops)."""
393
    run_lane(
394
        [
395
            sys.executable,
396
            str(SCRIPTS / "bench.py"),
397
            "--profile",
398
            "micro",
399
            "--runs",
400
            str(runs),
401
            "--subjects",
402
            "oak_installed,oak_local",
403
            "--oak-installed-bin",
404
            str(baseline_bin),
405
            "--oak-local-bin",
406
            str(baseline_bin),
407
            "--skip-determinism-probe",
408
            "--skip-remote",
409
            "--randomize-subject-order",
410
            "--results",
411
            str(aa_dir),
412
        ],
413
        "noise floor (A/A)",
414
    )
415
    rows = load_jsonl(aa_dir / "latest.jsonl")
416
    medians = median_by_key(rows, lambda row: row.get("elapsed_ms"))
417
    spreads: list[float] = []
418
    for (subject, scenario, operation), value in medians.items():
419
        if subject != "oak_installed" or operation in NOISE_FLOOR_EXCLUDED or operation.startswith("mode.setup"):
420
            continue
421
        other = medians.get(("oak_local", scenario, operation))
422
        pct = symmetric_ratio_spread_pct(value, other)
423
        if pct is not None:
424
            spreads.append(pct)
425
    if not spreads:
426
        return NOISE_FLOOR_MIN_PCT, 0
427
    p95 = percentile_nearest(spreads, 95.0) or NOISE_FLOOR_MIN_PCT
428
    return max(p95, NOISE_FLOOR_MIN_PCT), len(spreads)
429
430
431
def vcs_operations(rows: list[dict[str, Any]]) -> set[tuple[str, str]]:
432
    """(scenario, operation) pairs that actually invoke the subject's VCS.
433
434
    Latency gates apply only to these: search/read/edit/test steps run
435
    identical commands for every subject, so a delta there is host noise, not
436
    the changeset.
437
    """
438
    ops: set[tuple[str, str]] = set()
439
    for row in rows:
440
        if int(row.get("vcs_tool_call_count", 0) or 0) >= 1:
441
            ops.add((str(row.get("scenario")), str(row.get("operation"))))
442
    return ops
443
444
445
def embedded_null_p95(rows: list[dict[str, Any]], target: str, baseline: str) -> tuple[float, int]:
446
    """Noise estimate from identical-command steps: the spread between subjects
447
    on operations with zero VCS calls is what zero looks like in this lane."""
448
    latency = median_by_key(rows, lambda row: row.get("elapsed_ms"))
449
    vcs_ops = vcs_operations(rows)
450
    outcomes = outcome_counts_by_key(rows)
451
    spreads: list[float] = []
452
    for (subject, scenario, operation), value in latency.items():
453
        if subject != baseline or (scenario, operation) in vcs_ops or diagnostic_operation(operation):
454
            continue
455
        baseline_outcome = outcomes.get((baseline, scenario, operation), {})
456
        target_outcome = outcomes.get((target, scenario, operation), {})
457
        if baseline_outcome.get("failure", 0) or baseline_outcome.get("skip", 0):
458
            continue
459
        if target_outcome.get("failure", 0) or target_outcome.get("skip", 0):
460
            continue
461
        pct = symmetric_ratio_spread_pct(value, latency.get((target, scenario, operation)))
462
        if pct is not None:
463
            spreads.append(pct)
464
    if not spreads:
465
        return 0.0, 0
466
    return percentile_nearest(spreads, 95.0) or 0.0, len(spreads)
467
468
469
def same_patch_failure(left: dict[str, Any], right: dict[str, Any]) -> bool:
470
    if left.get("patch_failure_source") == "subject_tree_manifest_comparison":
471
        fields = ("patch_failure_source", "patch_failure_reason", "patch_oracle_version",
472
                  "patch_input_sha256", "patch_expected_tree_sha256", "patch_subject_tree_after_sha256")
473
        return (left.get("patch_apply_ok") is False and right.get("patch_apply_ok") is False
474
                and left.get("patch_failure_reason") == "diff_mutated_subject_tree"
475
                and all(isinstance(left.get(field), str) and len(left[field]) == 64 for field in fields[3:])
476
                and all(left.get(field) == right.get(field) for field in fields))
477
    fields = ("patch_input_sha256", "patch_expected_tree_sha256", "patch_apply_returncode",
478
              "patch_failure_reason", "patch_oracle_version", "patch_instrument_sha256",
479
              "patch_instrument_version")
480
    fingerprints = all(isinstance(left.get(field), str) and len(left[field]) == 64
481
                       for field in fields[:2])
482
    return (fingerprints and left.get("patch_apply_ok") is False and right.get("patch_apply_ok") is False
483
            and type(left.get("patch_apply_returncode")) is int
484
            and bool(left.get("patch_failure_reason")) and bool(left.get("patch_oracle_version"))
485
            and isinstance(left.get("patch_instrument_sha256"), str) and len(left["patch_instrument_sha256"]) == 64
486
            and bool(left.get("patch_instrument_version"))
487
            and all(left.get(field) == right.get(field) for field in fields))
488
489
490
def preexisting_patch_breach(breach: str, rows: list[dict[str, Any]]) -> bool:
491
    target_rows = [row for row in rows if row.get("subject") == "oak_local"
492
                   and breach.startswith(f"{breach.split(' ', 1)[0]} `{row.get('scenario')}/{row.get('operation')}`:")]
493
    if not target_rows:
494
        return False
495
    for target_row in target_rows:
496
        baseline_rows = [row for row in rows if row.get("subject") == "oak_installed"
497
                         and (row.get("scenario"), row.get("operation")) ==
498
                         (target_row.get("scenario"), target_row.get("operation"))]
499
        baseline_rows = [base for base in baseline_rows if base.get("run") == target_row.get("run")]
500
        if not baseline_rows or not all(same_patch_failure(target_row, base) for base in baseline_rows):
501
            return False
502
    return True
503
504
505
def compare_subjects(
506
    rows: list[dict[str, Any]],
507
    target: str,
508
    baseline: str,
509
    label: str,
510
    latency_threshold_pct: float,
511
    min_delta_ms: float,
512
    *,
513
    efficiency_gates: bool,
514
    output_bytes_threshold_pct: float,
515
    diagnostics: list[str] | None = None,
516
    cell_verdicts: dict[tuple[str, str], dict[str, Any]] | None = None,
517
    unconfirmed_prefix: str = "UNCONFIRMED",
518
) -> tuple[list[str], list[str]]:
519
    """Returns (regressions, improvements) lines for one baseline comparison.
520
521
    With ``cell_verdicts`` (oakbench.cellstats), latency is judged per cell;
522
    the legacy single threshold is used only when none are supplied.
523
    """
524
    diagnostics = diagnostics if diagnostics is not None else []
525
    gate_ops = vcs_operations(rows)
526
    latency = median_by_key(rows, lambda row: row.get("elapsed_ms"))
527
    tools = median_by_key(rows, row_tool_calls)
528
    bytes_out = median_by_key(rows, row_output_bytes)
529
    tokens = median_by_key(rows, row_token_total)
530
    outcomes = outcome_counts_by_key(rows)
531
532
    regressions: list[str] = []
533
    improvements: list[str] = []
534
    scenario_ops = sorted(
535
        {
536
            (scenario, op)
537
            for subject, scenario, op in set(latency) | set(outcomes)
538
            if subject in {target, baseline}
539
        }
540
    )
541
    target_kinds = {row.get("subject_kind") for row in rows if row.get("subject") == target} - {None}
542
    baseline_kinds = {row.get("subject_kind") for row in rows if row.get("subject") == baseline} - {None}
543
    incompatible_remote = (target_kinds == {"oak"} and baseline_kinds == {"git"}) or (target_kinds == {"git"} and baseline_kinds == {"oak"})
544
    for scenario, operation in scenario_ops:
545
        if incompatible_remote and operation.startswith("remote."):
546
            diagnostics.append(f"INFO TRANSPORT `{scenario}/{operation}`: local Git and network Oak operations are not comparable")
547
            continue
548
        if (scenario, operation) not in gate_ops or diagnostic_operation(operation):
549
            continue
550
        target_key = (target, scenario, operation)
551
        baseline_key = (baseline, scenario, operation)
552
553
        target_outcome = outcomes.get(target_key, {"success": 0, "failure": 0, "skip": 0})
554
        baseline_outcome = outcomes.get(baseline_key, {"success": 0, "failure": 0, "skip": 0})
555
        target_successes = target_outcome["success"]
556
        target_failures = target_outcome["failure"]
557
        target_skips = target_outcome["skip"]
558
        baseline_successes = baseline_outcome["success"]
559
        baseline_failures = baseline_outcome["failure"]
560
        if baseline_successes and target_successes == 0 and target_failures == 0 and target_skips > 0:
561
            diagnostics.append(f"UNMEASURED `{scenario}/{operation}` vs {label}: "
562
                               f"target has no measured attempts ({target_skips} skipped)")
563
        elif baseline_successes and target_successes == 0:
564
            regressions.append(
565
                f"NO SUCCESS `{scenario}/{operation}` vs {label}: target had 0 successful run(s) "
566
                f"({target_failures} failed, {target_skips} skipped), baseline had {baseline_successes}"
567
            )
568
        else:
569
            target_attempts = target_successes + target_failures
570
            baseline_attempts = baseline_successes + baseline_failures
571
            if target_attempts and baseline_attempts:
572
                target_failure_rate = target_failures / target_attempts
573
                baseline_failure_rate = baseline_failures / baseline_attempts
574
                target_less_reliable = target_failures > baseline_failures or target_successes < baseline_successes
575
                if target_failure_rate > baseline_failure_rate and target_less_reliable:
576
                    if baseline_failures == 0:
577
                        regressions.append(
578
                            f"FAIL `{scenario}/{operation}` vs {label}: {target_failures} failed run(s), "
579
                            "baseline clean"
580
                        )
581
                    else:
582
                        regressions.append(
583
                            f"FAIL-RATE `{scenario}/{operation}` vs {label}: "
584
                            f"{baseline_failures}/{baseline_attempts} -> {target_failures}/{target_attempts} "
585
                            f"failed run(s); successes {baseline_successes} -> {target_successes}"
586
                        )
587
588
        if baseline_key not in latency:
589
            continue
590
591
        pct = slower_pct(latency[baseline_key], latency.get(target_key))
592
        delta = (latency.get(target_key) or 0) - latency[baseline_key]
593
        if cell_verdicts is not None:
594
            item = cell_verdicts.get((scenario, operation))
595
            verdict = item.get("verdict") if item else "unmeasured"
596
            if verdict == "regression":
597
                regressions.append("SLOWER " + cellstats.describe(item, label))
598
            elif verdict == "improvement":
599
                improvements.append("faster " + cellstats.describe(item, label))
600
            elif verdict == "unconfirmed_regression":
601
                diagnostics.append(unconfirmed_prefix + " slower " + cellstats.describe(item, label) +
602
                                   f"; not claimed; ~{item.get('required_n')} runs/arm to confirm")
603
            elif verdict == "unconfirmed_improvement":
604
                diagnostics.append("INFO UNCONFIRMED faster " + cellstats.describe(item, label) + "; not claimed")
605
            pct = None
606
        if pct is not None:
607
            if pct >= latency_threshold_pct and delta >= min_delta_ms:
608
                regressions.append(
609
                    f"SLOWER `{scenario}/{operation}` vs {label}: {pct:+.1f}% "
610
                    f"({latency[baseline_key]:.1f} -> {latency[target_key]:.1f} ms)"
611
                )
612
            elif pct <= -latency_threshold_pct and -delta >= min_delta_ms:
613
                improvements.append(
614
                    f"faster `{scenario}/{operation}` vs {label}: {pct:+.1f}% "
615
                    f"({latency[baseline_key]:.1f} -> {latency[target_key]:.1f} ms)"
616
                )
617
618
        if efficiency_gates:
619
            tool_pct = slower_pct(tools.get(baseline_key), tools.get(target_key))
620
            if tool_pct is not None and tool_pct > 0:
621
                regressions.append(
622
                    f"TOOLS `{scenario}/{operation}` vs {label}: {tool_pct:+.1f}% "
623
                    f"({tools[baseline_key]:.0f} -> {tools[target_key]:.0f} calls)"
624
                )
625
            bytes_pct = slower_pct(bytes_out.get(baseline_key), bytes_out.get(target_key))
626
            if bytes_pct is not None and bytes_pct > output_bytes_threshold_pct:
627
                regressions.append(
628
                    f"OUTPUT `{scenario}/{operation}` vs {label}: {bytes_pct:+.1f}% "
629
                    f"({bytes_out[baseline_key]:.0f} -> {bytes_out[target_key]:.0f} bytes)"
630
                )
631
            elif bytes_pct is not None and bytes_pct < -output_bytes_threshold_pct:
632
                improvements.append(
633
                    f"leaner `{scenario}/{operation}` vs {label}: {bytes_pct:+.1f}% output bytes"
634
                )
635
            token_pct = slower_pct(tokens.get(baseline_key), tokens.get(target_key))
636
            if token_pct is not None and token_pct < -output_bytes_threshold_pct:
637
                improvements.append(f"fewer tokens `{scenario}/{operation}` vs {label}: {token_pct:+.1f}%")
638
639
    # Output-sufficiency gates: exact semantic metrics from the inforecall
640
    # probe rows, always on. A changeset can win every token/latency gate by
641
    # dropping changed-file names from output or breaking piped-diff
642
    # structure; that is a regression, not an optimization.
643
    oracle_versions: dict[tuple[str, str, str], set[Any]] = {}
644
    for row in rows:
645
        if "information_recall" in row or "information_precision" in row:
646
            key = (str(row.get("subject")), str(row.get("scenario")), str(row.get("operation")))
647
            oracle_versions.setdefault(key, set()).add(row.get("output_oracle_version"))
648
    incompatible_output = set()
649
    for (subject, scenario, operation), versions in oracle_versions.items():
650
        if subject != target:
651
            continue
652
        baseline_versions = oracle_versions.get((baseline, scenario, operation))
653
        if baseline_versions is not None and (len(versions) != 1 or versions != baseline_versions):
654
            incompatible_output.add((scenario, operation))
655
            regressions.append(f"OUTPUT-ORACLE `{scenario}/{operation}`: incompatible output oracle versions")
656
    output_rows = [row for row in rows if (str(row.get("scenario")), str(row.get("operation"))) not in incompatible_output]
657
    recall = median_by_key(output_rows, lambda row: row.get("information_recall"))
658
    precision = median_by_key(output_rows, lambda row: row.get("information_precision"))
659
    pipe_compat = median_by_key(
660
        rows,
661
        lambda row: (
662
            1.0
663
            if row.get("pipe_compatible_unified") is True
664
            else (0.0 if row.get("pipe_compatible_unified") is False else None)
665
        ),
666
    )
667
    for (subject, scenario, operation), value in sorted(recall.items()):
668
        if subject != target:
669
            continue
670
        base_value = recall.get((baseline, scenario, operation))
671
        if base_value is not None and value < base_value - 1e-6:
672
            regressions.append(
673
                f"RECALL `{scenario}/{operation}` vs {label}: changed-file recall "
674
                f"{base_value:.3f} -> {value:.3f} (output got cheaper by losing information)"
675
            )
676
    for (subject, scenario, operation), value in sorted(pipe_compat.items()):
677
        if subject != target:
678
            continue
679
        base_value = pipe_compat.get((baseline, scenario, operation))
680
        if base_value is not None and value < base_value - 1e-6:
681
            regressions.append(
682
                f"PIPE-COMPAT `{scenario}/{operation}` vs {label}: unified-diff structure lost "
683
                f"(rate {base_value:.2f} -> {value:.2f}); this legacy metric checks structure only"
684
            )
685
686
    for (subject, scenario, operation), value in sorted(precision.items()):
687
        if subject == target:
688
            base_value = precision.get((baseline, scenario, operation))
689
            if base_value is not None and value < base_value - 1e-6:
690
                regressions.append(f"PRECISION `{scenario}/{operation}` vs {label}: {base_value:.3f} -> {value:.3f}")
691
    patch_cells = {}
692
    for item in rows:
693
        if item.get("patch_application_required") is True or "patch_apply_ok" in item:
694
            key = (str(item.get("subject")), str(item.get("scenario")), str(item.get("operation")))
695
            patch_cells.setdefault(key, []).append(item)
696
    for scenario, operation in sorted({(key[1], key[2]) for key in patch_cells if key[0] in {target, baseline}}):
697
        base_rows = patch_cells.get((baseline, scenario, operation), [])
698
        target_rows = patch_cells.get((target, scenario, operation), [])
699
        proven_baseline = any(item.get("patch_apply_ok") is True and item.get("patch_application_required") is True
700
                              for item in base_rows)
701
        prefix = f"PATCH-APPLY `{scenario}/{operation}`"
702
        if not target_rows:
703
            if proven_baseline:
704
                regressions.append(prefix + ": target omitted previously measured patch evidence")
705
            elif any(base.get("patch_apply_ok") is True for base in base_rows):
706
                diagnostics.append("INFO " + prefix + ": target omitted previously measured optional patch evidence")
707
            continue
708
        for item in target_rows:
709
            if item.get("patch_apply_ok") is None:
710
                severity = "UNMEASURED " if item.get("patch_application_required") is True else "INFO "
711
                diagnostics.append(severity + prefix + ": " + str(item.get("patch_skip_reason") or "patch evidence unavailable"))
712
                continue
713
            instrument_rows = [probe for probe in [item, *[base for base in base_rows if base.get("run") == item.get("run")]]
714
                               if probe.get("patch_apply_ok") is not None
715
                               and probe.get("patch_failure_source") != "subject_tree_manifest_comparison"]
716
            if any(not isinstance(probe.get("patch_instrument_sha256"), str)
717
                   or len(probe["patch_instrument_sha256"]) != 64
718
                   or not probe.get("patch_instrument_version") for probe in instrument_rows):
719
                severity = "UNMEASURED " if item.get("patch_application_required") is True else "INFO "
720
                diagnostics.append(severity + prefix + ": patch instrument identity unavailable")
721
                continue
722
            versions = {base.get("patch_oracle_version") for base in base_rows if base.get("patch_apply_ok") is not None}
723
            if versions and versions != {item.get("patch_oracle_version")}:
724
                message = f"PATCH-ORACLE `{scenario}/{operation}`: incompatible or missing patch oracle version"
725
                if item.get("patch_application_required") is True:
726
                    regressions.append(message)
727
                else:
728
                    diagnostics.append("INFO " + message)
729
            elif item.get("patch_apply_ok") is False and (item.get("patch_application_required") is True
730
                                                        or item.get("patch_failure_source") == "subject_tree_manifest_comparison"):
731
                matched_rows = [base for base in base_rows if base.get("run") == item.get("run")]
732
                identical_failure = bool(matched_rows) and all(same_patch_failure(item, base) for base in matched_rows)
733
                if identical_failure:
734
                    diagnostics.append("PREEXISTING " + prefix + ": same measured defect in baseline and target")
735
                else:
736
                    regressions.append(prefix + ": exact patch application failed")
737
            elif item.get("patch_apply_ok") is False:
738
                diagnostics.append("INFO " + prefix + ": optional exact patch application failed")
739
740
    return regressions, improvements
741
742
743
def contention_checks(rows: list[dict[str, Any]], target: str, baseline: str) -> tuple[list[str], list[str]]:
744
    regressions: list[str] = []
745
    improvements: list[str] = []
746
    totals: dict[tuple[str, str], dict[str, Any]] = {}
747
    for row in rows:
748
        if row.get("operation") != "parallel.total" or row.get("skipped"):
749
            continue
750
        totals[(str(row.get("subject")), str(row.get("scenario")))] = row
751
752
    for (subject, scenario), row in sorted(totals.items()):
753
        if subject != target:
754
            continue
755
        metrics = row.get("parallel_metrics", {})
756
        if metrics.get("integrity_check_passed") is False:
757
            regressions.append(f"INTEGRITY `{scenario}`: post-contention check failed for {target}")
758
        if (metrics.get("lost_updates_detected") or 0) > 0:
759
            regressions.append(
760
                f"LOST UPDATES `{scenario}`: {metrics['lost_updates_detected']} payload(s) lost for {target}"
761
            )
762
        base = totals.get((baseline, scenario))
763
        if base:
764
            target_tp = metrics.get("commit_throughput_per_s")
765
            base_tp = base.get("parallel_metrics", {}).get("commit_throughput_per_s")
766
            if target_tp and base_tp:
767
                pct = ((target_tp - base_tp) / base_tp) * 100.0
768
                if pct < -15.0:
769
                    regressions.append(
770
                        f"THROUGHPUT `{scenario}` vs Oak baseline: {pct:+.1f}% "
771
                        f"({base_tp:.1f} -> {target_tp:.1f} snapshots/s)"
772
                    )
773
                elif pct > 15.0:
774
                    improvements.append(f"throughput `{scenario}` vs Oak baseline: {pct:+.1f}%")
775
    return regressions, improvements
776
777
778
def main() -> int:
779
    args = parse_args()
780
    lanes = [lane.strip() for lane in args.lanes.split(",") if lane.strip()]
781
    unknown = [lane for lane in lanes if lane not in LANES]
782
    if unknown:
783
        raise SystemExit(f"unknown lanes: {', '.join(unknown)}; valid: {', '.join(LANES)}")
784
    contention_modes = _csv(args.contention_modes)
785
    contention_workers = _csv(args.contention_workers)
786
    oakbench_remotes.print_remote_preflight_warnings(
787
        oakbench_remotes.oak_remote_preflight_warnings(
788
            remote_preflight_requirements(
789
                lanes,
790
                contention_modes=contention_modes,
791
                contention_workers=contention_workers,
792
            )
793
        )
794
    )
795
796
    baseline_bin = args.oak_baseline_bin or (Path(shutil.which("oak")) if shutil.which("oak") else None)
797
    if baseline_bin is None:
798
        raise SystemExit("No Oak baseline: install oak or pass --oak-baseline-bin")
799
    if args.oak_local_bin:
800
        local_bin = args.oak_local_bin.resolve()
801
        if not local_bin.exists():
802
            raise SystemExit(f"--oak-local-bin does not exist: {local_bin}")
803
    elif args.skip_build:
804
        raise SystemExit("--skip-build requires --oak-local-bin")
805
    else:
806
        local_bin = build_oak_local(args.oak_repo)
807
808
    bench_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
809
    out = args.results / bench_id
810
    out.mkdir(parents=True, exist_ok=True)
811
    started = time.perf_counter()
812
813
    noise_pct = NOISE_FLOOR_MIN_PCT
814
    noise_ops = 0
815
    if args.global_noise_floor:
816
        noise_pct, noise_ops = measure_noise_floor(out / "aa", baseline_bin, args.noise_floor_runs)
817
    effective_oak_threshold = max(args.oak_threshold_pct, 2.0 * noise_pct)
818
    effective_git_threshold = max(args.git_threshold_pct, 2.0 * noise_pct)
819
    if args.runs < cellstats.MIN_SAMPLES:
820
        print(f"[devloop] WARNING --runs {args.runs} < {cellstats.MIN_SAMPLES}: every latency cell will be UNMEASURED",
821
              flush=True)
822
823
    aa_names = [] if args.skip_noise_floor else [AA_SUBJECT]
824
    subject_flags = [
825
        "--subjects",
826
        ",".join(["git", "oak_installed", *aa_names, "oak_local"]),
827
        "--oak-installed-bin",
828
        str(baseline_bin),
829
        *([] if args.skip_noise_floor else ["--oak-aa-bin", str(baseline_bin)]),
830
        "--oak-local-bin",
831
        str(local_bin),
832
    ]
833
    core_profiles = _csv(args.core_profiles)
834
    lane_dirs: list[tuple[str, Path]] = []
835
    core_meta: dict[str, dict[str, Any]] = {}
836
837
    if "core" in lanes:
838
        for profile in core_profiles:
839
            directory = out / ("core" if core_profiles == ["smoke"] else f"core-{profile}")
840
            seconds = run_lane(
841
                [
842
                    sys.executable,
843
                    str(SCRIPTS / "bench.py"),
844
                    "--profile",
845
                    profile,
846
                    "--runs",
847
                    str(args.runs),
848
                    "--randomize-subject-order",
849
                    *subject_flags,
850
                    "--results",
851
                    str(directory),
852
                ],
853
                f"core lane ({profile})",
854
            )
855
            lane_name = "core" if core_profiles == ["smoke"] else f"core:{profile}"
856
            lane_dirs.append((lane_name, directory))
857
            core_meta[lane_name] = {"profile": profile, "seconds": seconds, "directory": directory}
858
    if "workflow" in lanes:
859
        run_lane(
860
            [
861
                sys.executable,
862
                str(SCRIPTS / "workflow_ab.py"),
863
                "--workflows",
864
                "all",
865
                "--runs",
866
                str(args.runs),
867
                "--randomize-subject-order",
868
                *subject_flags,
869
                "--results",
870
                str(out / "workflow"),
871
            ],
872
            "workflow lane",
873
        )
874
        lane_dirs.append(("workflow", out / "workflow"))
875
    if "contention" in lanes:
876
        run_lane(
877
            [
878
                sys.executable,
879
                str(SCRIPTS / "parallel_contention.py"),
880
                "--workers",
881
                args.contention_workers,
882
                "--commits-per-worker",
883
                str(args.contention_commits_per_worker),
884
                "--modes",
885
                args.contention_modes,
886
                *subject_flags,
887
                "--results",
888
                str(out / "contention"),
889
            ],
890
            "contention lane",
891
        )
892
893
    if "conformance" in lanes:
894
        run_lane(
895
            [
896
                sys.executable,
897
                str(SCRIPTS / "vcs_conformance.py"),
898
                "--workdir",
899
                str(out / "conformance-work"),
900
                "--subjects",
901
                "git,oak",
902
                "--oak-bin",
903
                str(local_bin),
904
                "--output",
905
                str(out / "conformance" / "latest.jsonl"),
906
                "--quiet",
907
            ],
908
            "conformance lane",
909
        )
910
911
    regressions: list[str] = []
912
    improvements: list[str] = []
913
    guardrails: list[str] = []
914
    known_gaps: list[str] = []
915
    null_notes: list[str] = []
916
    measurement_notes: list[str] = []
917
    scorecard_rows: list[dict[str, Any]] = []
918
    cell_notes: list[str] = []
919
    cell_floor_map: dict[tuple[str, str], float] = {}
920
    cell_records: list[dict[str, Any]] = []
921
    detection_lines: list[str] = []
922
    for lane, directory in lane_dirs:
923
        rows = load_jsonl(directory / "latest.jsonl")
924
        scorecard_rows += rows
925
        latency_of = lambda row: row.get("elapsed_ms")
926
        gated = {key for key in vcs_operations(rows) if not diagnostic_operation(key[1])
927
                 and not key[1].endswith((".add", ".commit"))}
928
        floors = cellstats.cell_floors(rows, "oak_installed", AA_SUBJECT, latency_of) if aa_names else {}
929
        for key, value in floors.items():
930
            cell_floor_map[key] = max(value, cell_floor_map.get(key, 0.0))
931
932
        def verdicts(target: str, baseline: str, min_delta: float) -> dict[tuple[str, str], dict[str, Any]]:
933
            return cellstats.classify_cells(rows, target, baseline, floors, value_of=latency_of,
934
                                            cells=gated, min_delta_ms=min_delta, fdr=args.fdr)
935
936
        oak_cells = verdicts("oak_local", "oak_installed", args.min_delta_ms)
937
        for item in oak_cells.values():
938
            item["stage"] = "screen"
939
        # Adaptive confirmation (core lane): re-measure only screened cells'
940
        # scenarios with more runs, then decide those cells on the FRESH data
941
        # alone (independent replication; BH over the candidate family only).
942
        candidates = cellstats.screen_candidates(oak_cells, target_mde_pct=args.target_mde_pct) if aa_names else []
943
        meta = core_meta.get(lane)
944
        if candidates and meta and not args.no_confirm and args.confirm_runs >= cellstats.MIN_SAMPLES:
945
            scenario_count = len({key[0] for key in oak_cells})
946
            scenarios, confirm_runs = confirmation_plan(candidates, meta["seconds"], args.runs, scenario_count,
947
                                                        args.confirm_runs, args.confirm_budget_s)
948
            if confirm_runs >= cellstats.MIN_SAMPLES:
949
                confirm_dir = meta["directory"].with_name(meta["directory"].name + "-confirm")
950
                operations = {op for _, op in candidates}
951
                run_lane(
952
                    [sys.executable, str(SCRIPTS / "bench.py"), "--profile", meta["profile"],
953
                     "--scenarios", ",".join(scenarios), "--runs", str(confirm_runs), "--randomize-subject-order",
954
                     "--skip-determinism-probe",
955
                     *([] if any(op.startswith("remote.") for op in operations) else ["--skip-remote"]),
956
                     *([] if "diff.dirty" in operations else ["--skip-diff"]),
957
                     *subject_flags, "--results", str(confirm_dir)],
958
                    f"confirmation ({lane}: {len(candidates)} cell(s), {len(scenarios)} scenario(s), runs={confirm_runs})",
959
                )
960
                confirm_rows = load_jsonl(confirm_dir / "latest.jsonl")
961
                confirm_floors = cellstats.cell_floors(confirm_rows, "oak_installed", AA_SUBJECT, latency_of)
962
                confirmed = cellstats.classify_cells(confirm_rows, "oak_local", "oak_installed", confirm_floors,
963
                                                     value_of=latency_of, cells=candidates,
964
                                                     min_delta_ms=args.min_delta_ms, fdr=args.fdr)
965
                # QA R2-2: the A/A self-check covers the confirmation rows too.
966
                confirm_aa = cellstats.classify_cells(confirm_rows, AA_SUBJECT, "oak_installed", confirm_floors,
967
                                                      value_of=latency_of, cells=candidates,
968
                                                      min_delta_ms=args.min_delta_ms, fdr=args.fdr)
969
                confirm_aa_changes = [item for item in confirm_aa.values() if item["verdict"] in ("regression", "improvement")]
970
                if confirm_aa_changes:
971
                    measurement_notes.append(f"UNMEASURED [{lane}] A/A control shows {len(confirm_aa_changes)} change(s) "
972
                                             "between identical binaries in the confirmation stage; host too noisy")
973
                cell_records.extend({"lane": lane, "comparison": AA_SUBJECT + "_vs_oak_installed", "stage": "confirmation", **item}
974
                                    for item in confirm_aa.values())
975
                for key, item in confirmed.items():
976
                    item.update(stage="confirmation", screen_verdict=oak_cells[key]["verdict"],
977
                                screen_delta_pct=oak_cells[key]["delta_pct"], screen_p_value=oak_cells[key]["p_value"])
978
                    oak_cells[key] = item
979
                    if key in confirm_floors:
980
                        cell_floor_map[key] = max(confirm_floors[key], cell_floor_map.get(key, 0.0))
981
                cell_notes.append(f"[{lane}] confirmation: {len(candidates)} screened cell(s) re-measured at "
982
                                  f"runs={confirm_runs} in {', '.join(scenarios)}; decided on fresh data only")
983
            else:
984
                cell_notes.append(f"[{lane}] confirmation skipped: budget {args.confirm_budget_s:.0f}s affords "
985
                                  f"{confirm_runs} run(s) < {cellstats.MIN_SAMPLES}")
986
        # Fail closed (QA P1-1): a gated Oak-vs-Oak cell that either Oak arm
987
        # measured but that has no verdict makes the whole run UNMEASURED.
988
        for key, item in sorted(oak_cells.items()):
989
            if item["verdict"] == "unmeasured" and measured_by_oak(rows, key):
990
                measurement_notes.append(f"UNMEASURED [{lane}] `{key[0]}/{key[1]}` vs Oak baseline: "
991
                                         f"{item.get('reason') or 'no verdict'}")
992
        for item in oak_cells.values():
993
            # Without the confirmation stage a lone screen-stage change cannot
994
            # pass BH when the family is too large for n: no power statement.
995
            if (item["verdict"] == "no_change" and item.get("stage") == "screen"
996
                    and (args.no_confirm or not candidates or meta is None)
997
                    and cellstats.runs_for_bh(item.get("family_size") or 1, args.fdr,
998
                                              min(item["baseline_n"], item["target_n"])) > min(item["baseline_n"], item["target_n"])):
999
                item["mde_pct"] = None
1000
                item["reason"] = "single-cell change not confirmable at this n without the confirmation stage"
1001
        no_change = [item for item in oak_cells.values() if item["verdict"] == "no_change"]
1002
        if no_change:
1003
            mdes = sorted(item["mde_pct"] for item in no_change if item.get("mde_pct") is not None)
1004
            if mdes:
1005
                cell_notes.append(f"[{lane}] detection limits: {len(no_change)} cell(s) no change; minimum detectable "
1006
                                  f"effect at {cellstats.MDE_POWER:.0%} power median {statistics.median(mdes):.1f}%, "
1007
                                  f"max {mdes[-1]:.1f}%" + (f" ({len(no_change) - len(mdes)} cell(s) without a power "
1008
                                  "statement)" if len(mdes) < len(no_change) else ""))
1009
            detection_lines.extend(f"[{lane}] " + cellstats.describe(item, "Oak baseline") for item in no_change)
1010
        git_min_delta = max(args.min_delta_ms, 10.0)
1011
        local_git_cells = verdicts("oak_local", "git", git_min_delta)
1012
        baseline_git_cells = verdicts("oak_installed", "git", git_min_delta)
1013
        aa_cells = verdicts(AA_SUBJECT, "oak_installed", args.min_delta_ms) if aa_names else {}
1014
        tested = [item for item in oak_cells.values() if item["p_value"] is not None]
1015
        unmeasured = [item for item in oak_cells.values() if item["verdict"] == "unmeasured"]
1016
        if floors:
1017
            ordered = sorted(floors.values())
1018
            cell_notes.append(
1019
                f"[{lane}] per-cell A/A floors ({len(floors)} cells): median {statistics.median(ordered):.1f}%, "
1020
                f"max {ordered[-1]:.1f}%; {len(tested)} cells tested, BH q={args.fdr}; "
1021
                f"{len(unmeasured)} cell(s) unmeasured")
1022
        else:
1023
            cell_notes.append(f"[{lane}] no A/A control rows: every latency cell is UNMEASURED")
1024
        cell_records.extend({"lane": lane, "comparison": name, **item}
1025
                            for name, family in (("oak_local_vs_oak_installed", oak_cells),
1026
                                                 ("oak_local_vs_git", local_git_cells),
1027
                                                 ("oak_installed_vs_git", baseline_git_cells),
1028
                                                 (AA_SUBJECT + "_vs_oak_installed", aa_cells))
1029
                            for item in family.values())
1030
        aa_changes = [item for item in aa_cells.values() if item["verdict"] in ("regression", "improvement")]
1031
        if aa_changes:
1032
            measurement_notes.append(f"UNMEASURED [{lane}] A/A control shows {len(aa_changes)} change(s) "
1033
                                     "between identical binaries; host too noisy for latency verdicts")
1034
        # Per-lane embedded null control: identical-command steps (search,
1035
        # read, edit, test) cannot differ between subjects, so their spread is
1036
        # this lane's live noise measurement; thresholds rise to clear it.
1037
        null_pct, null_ops = embedded_null_p95(rows, "oak_local", "oak_installed")
1038
        lane_oak_threshold = max(effective_oak_threshold, 2.0 * null_pct)
1039
        lane_git_threshold = max(effective_git_threshold, 2.0 * null_pct)
1040
        if null_ops:
1041
            null_notes.append(
1042
                f"[{lane}] embedded null ({null_ops} identical-command ops): p95 spread {null_pct:.1f}%; "
1043
                f"lane threshold {lane_oak_threshold:.1f}%"
1044
            )
1045
        lane_regressions, lane_improvements = compare_subjects(
1046
            rows,
1047
            "oak_local",
1048
            "oak_installed",
1049
            "Oak baseline",
1050
            lane_oak_threshold,
1051
            args.min_delta_ms,
1052
            efficiency_gates=True,
1053
            output_bytes_threshold_pct=args.output_bytes_threshold_pct,
1054
            diagnostics=measurement_notes,
1055
            cell_verdicts=oak_cells,
1056
        )
1057
        regressions += [f"[{lane}] {item}" for item in lane_regressions]
1058
        improvements += [f"[{lane}] {item}" for item in lane_improvements]
1059
1060
        # Git guardrail: only breaches the CHANGESET introduced fail the
1061
        # verdict. Pre-existing Oak-vs-Git gaps (also breached by the installed
1062
        # baseline) are listed as known gaps — a verdict that fails every run
1063
        # on inherited gaps teaches agents to ignore it (ADR-0004's lesson).
1064
        local_breaches, git_improvements = compare_subjects(
1065
            rows,
1066
            "oak_local",
1067
            "git",
1068
            "Git guardrail",
1069
            lane_git_threshold,
1070
            max(args.min_delta_ms, 10.0),
1071
            efficiency_gates=False,
1072
            output_bytes_threshold_pct=args.output_bytes_threshold_pct,
1073
            diagnostics=measurement_notes,
1074
            cell_verdicts=local_git_cells,
1075
            # Git-guardrail candidates inform; only Oak-baseline candidates make the verdict inconclusive.
1076
            unconfirmed_prefix="INFO UNCONFIRMED",
1077
        )
1078
        baseline_breaches, _ = compare_subjects(
1079
            rows,
1080
            "oak_installed",
1081
            "git",
1082
            "Git guardrail",
1083
            lane_git_threshold,
1084
            max(args.min_delta_ms, 10.0),
1085
            efficiency_gates=False,
1086
            output_bytes_threshold_pct=args.output_bytes_threshold_pct,
1087
            cell_verdicts=baseline_git_cells,
1088
        )
1089
        preexisting = {breach.split(":")[0] for breach in baseline_breaches}
1090
        oak_regressed = {key for key, item in oak_cells.items() if item["verdict"] == "regression"}
1091
        for breach in local_breaches:
1092
            is_patch = breach.startswith(("PATCH-APPLY ", "PATCH-ORACLE "))
1093
            if breach.startswith("SLOWER "):
1094
                # QA P1-2: a latency breach is introduced by the changeset only
1095
                # when the SAME cell's Oak-vs-Oak comparison is a confirmed
1096
                # regression; otherwise it is a pre-existing Oak-vs-Git gap.
1097
                cell = breach_cell(breach)
1098
                if cell in oak_regressed:
1099
                    guardrails.append(f"[{lane}] {breach}")
1100
                else:
1101
                    known_gaps.append(f"[{lane}] {breach}")
1102
            elif breach.split(":")[0] in preexisting and (not is_patch or preexisting_patch_breach(breach, rows)):
1103
                known_gaps.append(f"[{lane}] {breach}")
1104
            else:
1105
                guardrails.append(f"[{lane}] {breach}")
1106
        improvements += [f"[{lane}] {item}" for item in git_improvements]
1107
    if "contention" in lanes:
1108
        rows = load_jsonl(out / "contention" / "latest.jsonl")
1109
        scorecard_rows += rows
1110
        lane_regressions, lane_improvements = contention_checks(rows, "oak_local", "oak_installed")
1111
        regressions += [f"[contention] {item}" for item in lane_regressions]
1112
        improvements += [f"[contention] {item}" for item in lane_improvements]
1113
1114
    if "conformance" in lanes:
1115
        conformance_regressions, conformance_notes = conformance_checks(
1116
            load_jsonl(out / "conformance" / "latest.jsonl"))
1117
        regressions += conformance_regressions
1118
        measurement_notes += conformance_notes
1119
1120
    elapsed_s = time.perf_counter() - started
1121
    verdict = ("REGRESSED" if regressions or guardrails else
1122
               "UNMEASURED" if any(note.startswith(("UNMEASURED", "UNCONFIRMED")) for note in measurement_notes)
1123
               else "PASS")
1124
    lines = [
1125
        "# Devloop Verdict",
1126
        "",
1127
        f"**{verdict}** — oak_local ({local_bin}) vs Oak baseline ({baseline_bin}) and Git, "
1128
        f"lanes: {', '.join(lanes)}, runs={args.runs}, total {elapsed_s:.0f}s.",
1129
        "",
1130
        f"- Latency verdicts are per cell (cellstats {cellstats.CELL_STATS_VERSION}): change only when "
1131
        f"BH-FDR q={args.fdr} rejects AND the 95% bootstrap CI excludes 0 AND the effect exceeds that "
1132
        f"cell's A/A floor ({AA_SUBJECT} = baseline binary, interleaved). Screened cells are re-measured "
1133
        f"(up to --confirm-runs {args.confirm_runs}) and decided on fresh data. UNCONFIRMED candidates are never claimed.",
1134
        *[f"- {note}" for note in cell_notes],
1135
        *([f"- Legacy global micro A/A (informational only, {noise_ops} ops): p95 spread {noise_pct:.1f}%."]
1136
          if args.global_noise_floor else []),
1137
        f"- Efficiency gates (exact metrics) vs Oak baseline only (ADR-0004): any tool-call increase, "
1138
        f">{args.output_bytes_threshold_pct:.0f}% output bytes.",
1139
        *[f"- {note}" for note in null_notes],
1140
        "",
1141
        f"## Regressions vs Oak baseline ({len(regressions)})",
1142
        "",
1143
        *([f"- {item}" for item in regressions] or ["- none"]),
1144
        "",
1145
        f"## New Git-guardrail breaches introduced by this changeset ({len(guardrails)})",
1146
        "",
1147
        *([f"- {item}" for item in guardrails] or ["- none"]),
1148
        "",
1149
        f"## Known Git gaps, pre-existing in the baseline ({len(known_gaps)}) — tracked, not failing",
1150
        "",
1151
        *([f"- {item}" for item in known_gaps] or ["- none"]),
1152
        "",
1153
        "## Correctness measurement notes",
1154
        "",
1155
        *([f"- {item}" for item in dict.fromkeys(measurement_notes)] or ["- none"]),
1156
        "",
1157
        f"## Improvements ({len(improvements)})",
1158
        "",
1159
        *([f"- {item}" for item in improvements[:20]] or ["- none"]),
1160
        "",
1161
        f"## Detection limits: no change >= X% at {cellstats.MDE_POWER:.0%} power ({len(detection_lines)} cells)",
1162
        "",
1163
        *([f"- {item}" for item in detection_lines] or ["- none"]),
1164
        "",
1165
        f"Raw rows: {out}/ (per-cell decisions: cells.json)",
1166
    ]
1167
    if args.scorecard:
1168
        lines += [
1169
            "",
1170
            "## Scorecard vs Git Baseline Book (informational)",
1171
            "",
1172
            *scorecard_lines(scorecard_rows, noise_floor_pct=noise_pct if args.global_noise_floor else None,
1173
                             cell_floors=cell_floor_map),
1174
        ]
1175
    report = "\n".join(lines) + "\n"
1176
    (out / "verdict.md").write_text(report)
1177
    # Every per-cell decision (floor, CI, p, BH) for audit; derived, never committed.
1178
    (out / "cells.json").write_text(json.dumps(cell_records, indent=1, sort_keys=True) + "\n")
1179
    print()
1180
    print(report)
1181
    return 0 if verdict == "PASS" else (2 if verdict == "UNMEASURED" else 1)
1182
1183
1184
if __name__ == "__main__":
1185
    raise SystemExit(main())