Viewing files at commit 290f11640be1Commit detailsView latest

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