88e86b3faccd
Rebuild benchmarks as a clean single-root reposi
2 months ago
| 1 | #!/usr/bin/env python3
|
| 2 | """Print a ranked, evidence-linked attack list from the latest benchmark rows.
|
| 3 |
|
| 4 | Standing orders for the Oak improvement swarm: instead of every agent
|
| 5 | re-deriving priorities from raw JSONL, this script turns the latest per-lane
|
| 6 | results into one ranked opportunity report.
|
| 7 |
|
| 8 | Sections, in order:
|
| 9 |
|
| 10 | 1. Where Oak loses to Git β per-(scenario, operation) median gaps on
|
| 11 | elapsed_ms, cost-weighted tokens, output bytes, and tool calls, scored by
|
| 12 | pct-worse times a documented operation-family frequency weight.
|
| 13 | 2. Absolute quality bar β output-quality violations independent of Git:
|
| 14 | broken pipe compatibility, information recall below 1.0, unstable output,
|
| 15 | ANSI escapes in non-TTY output. Oak subjects only.
|
| 16 | 3. Oak-only trends β remote.net.* and mount-lane medians with no Git
|
| 17 | comparator; the goal is making them smaller release over release.
|
| 18 | 4. Unmeasured β skip rows grouped by skip_reason: coverage to unlock.
|
| 19 | 5. Low confidence β every line above with sample count n < 3, repeated.
|
| 20 |
|
| 21 | Honesty rules (ADR-0002): skip rows (returncode 77 / skipped: true) never
|
| 22 | enter medians or section 1-3 counts; null metric values are unmeasured, never
|
| 23 | zero; the header states which input files were read and which were missing.
|
| 24 | Output is deterministic: the same inputs produce a byte-identical report.
|
| 25 | """
|
| 26 |
|
| 27 | from __future__ import annotations
|
| 28 |
|
| 29 | import argparse
|
| 30 | import json
|
| 31 | import sys
|
| 32 | from dataclasses import dataclass
|
| 33 | from pathlib import Path
|
| 34 | from typing import Any, Callable, Optional
|
| 35 |
|
| 36 | from oakbench.reporting import median
|
| 37 | from oakbench.rows import (
|
| 38 | number_or_none,
|
| 39 | output_bytes as row_output_bytes,
|
| 40 | tool_calls as row_tool_calls,
|
| 41 | )
|
| 42 |
|
| 43 |
|
| 44 | SKIP_RETURNCODE = 77
|
| 45 | LOW_N_THRESHOLD = 3
|
| 46 |
|
| 47 | # Input files, all optional: a missing file is reported as "lane not run".
|
| 48 | LANE_FILES = (
|
| 49 | ("core", "latest.jsonl"),
|
| 50 | ("mount", "latest.mount.jsonl"),
|
| 51 | ("contention", "parallel-contention/latest.jsonl"),
|
| 52 | )
|
| 53 |
|
| 54 | # How often agent fleets run each operation family. Static and documented on
|
| 55 | # purpose: opportunity scores must be reproducible from the rows alone, not
|
| 56 | # from telemetry that moves between runs. Keys match either the full operation
|
| 57 | # name or any dotted component of it; the highest matching weight wins.
|
| 58 | FREQUENCY_WEIGHTS = {
|
| 59 | "status": 10,
|
| 60 | "diff": 10,
|
| 61 | "commit": 10,
|
| 62 | "snapshot": 10,
|
| 63 | "proc.spawn": 10,
|
| 64 | "branch": 5,
|
| 65 | "log": 5,
|
| 66 | "show": 5,
|
| 67 | "init": 2,
|
| 68 | "clone": 2,
|
| 69 | "push": 2,
|
| 70 | "pull": 2,
|
| 71 | "fetch": 2,
|
| 72 | }
|
| 73 | DEFAULT_FREQUENCY_WEIGHT = 1
|
| 74 |
|
| 75 |
|
| 76 | def frequency_weight(operation: str) -> int:
|
| 77 | if operation in FREQUENCY_WEIGHTS:
|
| 78 | return FREQUENCY_WEIGHTS[operation]
|
| 79 | weight = DEFAULT_FREQUENCY_WEIGHT
|
| 80 | for part in operation.split("."):
|
| 81 | weight = max(weight, FREQUENCY_WEIGHTS.get(part, DEFAULT_FREQUENCY_WEIGHT))
|
| 82 | return weight
|
| 83 |
|
| 84 |
|
| 85 | def cost_weighted_tokens(row: dict[str, Any]) -> Optional[float]:
|
| 86 | # The envelope-priced total is the truer agent cost (tool-use/tool-result
|
| 87 | # framing per call); prefer it when the row carries it.
|
| 88 | value = number_or_none(row.get("estimated_cost_weighted_tokens_with_envelope"))
|
| 89 | if value is not None:
|
| 90 | return value
|
| 91 | return number_or_none(row.get("estimated_cost_weighted_tokens"))
|
| 92 |
|
| 93 |
|
| 94 | # (metric name, per-row extractor, display precision). Extractors return None
|
| 95 | # for unmeasured rows, which are excluded from medians, never treated as 0.
|
| 96 | METRICS: tuple[tuple[str, Callable[[dict[str, Any]], Optional[float]], int], ...] = (
|
| 97 | ("elapsed_ms", lambda row: number_or_none(row.get("elapsed_ms")), 1),
|
| 98 | ("estimated_cost_weighted_tokens", cost_weighted_tokens, 1),
|
| 99 | ("raw_output_bytes", row_output_bytes, 0),
|
| 100 | ("tool_call_count", row_tool_calls, 2),
|
| 101 | )
|
| 102 | METRIC_PRECISION = {name: precision for name, _, precision in METRICS}
|
| 103 |
|
| 104 | # Known skip reasons mapped to what would unlock the measurement. Unknown
|
| 105 | # reasons fall back to the reason text itself, which producers are required
|
| 106 | # to make actionable (rows.py rejects empty skip_reason).
|
| 107 | SKIP_REMEDIES = {
|
| 108 | "mount_not_started": (
|
| 109 | "make `oak mount` start on this host (macOS FSKit app enabled / Linux fuse3 installed); "
|
| 110 | "every in-mount measurement is gated on a successful mount.start"
|
| 111 | ),
|
| 112 | "requires_external_offline_harness": (
|
| 113 | "build the offline network harness so offline-after-partial-hydration can run"
|
| 114 | ),
|
| 115 | "huge_file_path_not_configured": (
|
| 116 | "configure the huge-file fixture path in scenarios/mount.yaml for the partial-read scenario"
|
| 117 | ),
|
| 118 | }
|
| 119 | DEFAULT_SKIP_REMEDY = "provide the capability described by the skip reason"
|
| 120 |
|
| 121 |
|
| 122 | @dataclass(frozen=True)
|
| 123 | class Opportunity:
|
| 124 | section: str
|
| 125 | scenario: Optional[str]
|
| 126 | operation: Optional[str]
|
| 127 | metric: str
|
| 128 | subject: Optional[str]
|
| 129 | value: Optional[float]
|
| 130 | baseline_value: Optional[float]
|
| 131 | pct_gap: Optional[float]
|
| 132 | score: Optional[float]
|
| 133 | n: int
|
| 134 | evidence_file: str
|
| 135 | detail: str = ""
|
| 136 |
|
| 137 | @property
|
| 138 | def low_n(self) -> bool:
|
| 139 | # Skip-row counts are exact tallies, not samples; low-n applies to
|
| 140 | # the median-based sections only.
|
| 141 | return self.section != "unmeasured" and self.n < LOW_N_THRESHOLD
|
| 142 |
|
| 143 |
|
| 144 | @dataclass
|
| 145 | class Lane:
|
| 146 | name: str
|
| 147 | path: Path
|
| 148 | missing: bool
|
| 149 | row_count: int = 0
|
| 150 | # (scenario, operation, subject) -> successful rows only
|
| 151 | success: Optional[dict[tuple[str, str, str], list[dict[str, Any]]]] = None
|
| 152 | skips: Optional[list[dict[str, Any]]] = None
|
| 153 |
|
| 154 |
|
| 155 | def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
|
| 156 | parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
| 157 | parser.add_argument(
|
| 158 | "jsonl",
|
| 159 | nargs="*",
|
| 160 | type=Path,
|
| 161 | help=(
|
| 162 | "Explicit benchmark JSONL files to read. When omitted, reads the "
|
| 163 | "standard latest lane files under --results."
|
| 164 | ),
|
| 165 | )
|
| 166 | parser.add_argument(
|
| 167 | "--results",
|
| 168 | type=Path,
|
| 169 | default=Path(__file__).resolve().parent.parent / "results",
|
| 170 | help="Results directory for default latest lane files (default: the repository's results/)",
|
| 171 | )
|
| 172 | parser.add_argument("--json", type=Path, help="Also write machine-readable JSON to this path")
|
| 173 | parser.add_argument("--git-subject", default="git")
|
| 174 | parser.add_argument(
|
| 175 | "--oak-baseline",
|
| 176 | help="Default: oak_main when present, otherwise oak_installed",
|
| 177 | )
|
| 178 | return parser.parse_args(argv)
|
| 179 |
|
| 180 |
|
| 181 | def is_skip(row: dict[str, Any]) -> bool:
|
| 182 | return row.get("returncode") == SKIP_RETURNCODE or bool(row.get("skipped"))
|
| 183 |
|
| 184 |
|
| 185 | def is_oak_subject(subject: str, rows: list[dict[str, Any]]) -> bool:
|
| 186 | kind = rows[0].get("subject_kind")
|
| 187 | if isinstance(kind, str) and kind:
|
| 188 | return kind == "oak"
|
| 189 | return subject.startswith("oak")
|
| 190 |
|
| 191 |
|
| 192 | def load_lane(name: str, path: Path) -> Lane:
|
| 193 | if not path.exists():
|
| 194 | return Lane(name=name, path=path, missing=True)
|
| 195 | success: dict[tuple[str, str, str], list[dict[str, Any]]] = {}
|
| 196 | skips: list[dict[str, Any]] = []
|
| 197 | row_count = 0
|
| 198 | with path.open() as fh:
|
| 199 | for line_number, raw_line in enumerate(fh, start=1):
|
| 200 | line = raw_line.strip()
|
| 201 | if not line:
|
| 202 | continue
|
| 203 | try:
|
| 204 | row = json.loads(line)
|
| 205 | except json.JSONDecodeError as exc:
|
| 206 | raise SystemExit(f"{path}:{line_number}: invalid JSON: {exc}") from exc
|
| 207 | if not isinstance(row, dict):
|
| 208 | raise SystemExit(f"{path}:{line_number}: expected a JSON object")
|
| 209 | row_count += 1
|
| 210 | if is_skip(row):
|
| 211 | skips.append(row)
|
| 212 | continue
|
| 213 | if row.get("returncode") != 0:
|
| 214 | continue
|
| 215 | subject = str(row.get("subject") or "")
|
| 216 | scenario = str(row.get("scenario") or "")
|
| 217 | operation = str(row.get("operation") or "")
|
| 218 | if not subject or not scenario or not operation:
|
| 219 | continue
|
| 220 | success.setdefault((scenario, operation, subject), []).append(row)
|
| 221 | return Lane(name=name, path=path, missing=False, row_count=row_count, success=success, skips=skips)
|
| 222 |
|
| 223 |
|
| 224 | def lane_name_for_path(path: Path, index: int) -> str:
|
| 225 | parts = path.parts
|
| 226 | if path.name == "latest.mount.jsonl":
|
| 227 | return "mount"
|
| 228 | if "parallel-contention" in parts:
|
| 229 | return "contention"
|
| 230 | if path.name == "latest.jsonl":
|
| 231 | return "core" if index == 0 else f"input-{index + 1}"
|
| 232 | return path.stem or f"input-{index + 1}"
|
| 233 |
|
| 234 |
|
| 235 | def load_lanes(results_dir: Path, input_paths: Optional[list[Path]] = None) -> list[Lane]:
|
| 236 | if input_paths:
|
| 237 | return [load_lane(lane_name_for_path(path, index), path) for index, path in enumerate(input_paths)]
|
| 238 | return [load_lane(name, results_dir / rel) for name, rel in LANE_FILES]
|
| 239 |
|
| 240 |
|
| 241 | def choose_oak_baseline(lanes: list[Lane], explicit: Optional[str]) -> Optional[str]:
|
| 242 | subjects = {
|
| 243 | subject
|
| 244 | for lane in lanes
|
| 245 | if not lane.missing
|
| 246 | for (_, _, subject) in lane.success
|
| 247 | }
|
| 248 | if explicit:
|
| 249 | return explicit if explicit in subjects else None
|
| 250 | if "oak_main" in subjects:
|
| 251 | return "oak_main"
|
| 252 | if "oak_installed" in subjects:
|
| 253 | return "oak_installed"
|
| 254 | return None
|
| 255 |
|
| 256 |
|
| 257 | def metric_samples(
|
| 258 | rows: list[dict[str, Any]],
|
| 259 | extract: Callable[[dict[str, Any]], Optional[float]],
|
| 260 | ) -> list[float]:
|
| 261 | return [value for value in (extract(row) for row in rows) if value is not None]
|
| 262 |
|
| 263 |
|
| 264 | def build_losses(lanes: list[Lane], git_subject: str, oak_baseline: Optional[str]) -> list[Opportunity]:
|
| 265 | """Section 1: scored per-(scenario, operation, metric) gaps vs Git."""
|
| 266 | if oak_baseline is None:
|
| 267 | return []
|
| 268 | entries: list[Opportunity] = []
|
| 269 | for lane in lanes:
|
| 270 | if lane.missing:
|
| 271 | continue
|
| 272 | scenario_ops = sorted({(scenario, op) for (scenario, op, _) in lane.success})
|
| 273 | for scenario, operation in scenario_ops:
|
| 274 | git_rows = lane.success.get((scenario, operation, git_subject))
|
| 275 | oak_rows = lane.success.get((scenario, operation, oak_baseline))
|
| 276 | if not git_rows or not oak_rows:
|
| 277 | continue
|
| 278 | for metric, extract, _ in METRICS:
|
| 279 | git_values = metric_samples(git_rows, extract)
|
| 280 | oak_values = metric_samples(oak_rows, extract)
|
| 281 | git_median = median(git_values)
|
| 282 | oak_median = median(oak_values)
|
| 283 | if git_median is None or oak_median is None or git_median <= 0:
|
| 284 | continue
|
| 285 | if oak_median <= git_median:
|
| 286 | continue
|
| 287 | pct_gap = ((oak_median - git_median) / git_median) * 100.0
|
| 288 | weight = frequency_weight(operation)
|
| 289 | entries.append(
|
| 290 | Opportunity(
|
| 291 | section="losses",
|
| 292 | scenario=scenario,
|
| 293 | operation=operation,
|
| 294 | metric=metric,
|
| 295 | subject=oak_baseline,
|
| 296 | value=oak_median,
|
| 297 | baseline_value=git_median,
|
| 298 | pct_gap=pct_gap,
|
| 299 | score=pct_gap * weight,
|
| 300 | n=min(len(git_values), len(oak_values)),
|
| 301 | evidence_file=str(lane.path),
|
| 302 | detail=f"weight {weight}",
|
| 303 | )
|
| 304 | )
|
| 305 | entries.sort(key=lambda e: (-e.score, e.scenario, e.operation, e.metric))
|
| 306 | return entries
|
| 307 |
|
| 308 |
|
| 309 | def build_quality_bar(lanes: list[Lane]) -> list[Opportunity]:
|
| 310 | """Section 2: quality violations on Oak subjects, independent of Git."""
|
| 311 | entries: list[Opportunity] = []
|
| 312 | for lane in lanes:
|
| 313 | if lane.missing:
|
| 314 | continue
|
| 315 | for (scenario, operation, subject), rows in sorted(lane.success.items()):
|
| 316 | if not is_oak_subject(subject, rows):
|
| 317 | continue
|
| 318 | recalls = [
|
| 319 | float(value)
|
| 320 | for value in (row.get("information_recall") for row in rows)
|
| 321 | if isinstance(value, (int, float)) and not isinstance(value, bool)
|
| 322 | ]
|
| 323 | precisions = [
|
| 324 | float(value)
|
| 325 | for value in (row.get("information_precision") for row in rows)
|
| 326 | if isinstance(value, (int, float)) and not isinstance(value, bool)
|
| 327 | ]
|
| 328 | pipes = [value for value in (row.get("pipe_compatible_unified") for row in rows) if isinstance(value, bool)]
|
| 329 | stables = [value for value in (row.get("output_stable") for row in rows) if isinstance(value, bool)]
|
| 330 | ansis = [
|
| 331 | int(value)
|
| 332 | for value in (row.get("ansi_escape_count_non_tty") for row in rows)
|
| 333 | if isinstance(value, int) and not isinstance(value, bool)
|
| 334 | ]
|
| 335 |
|
| 336 | def add(metric: str, value: float, n: int, detail: str) -> None:
|
| 337 | entries.append(
|
| 338 | Opportunity(
|
| 339 | section="quality",
|
| 340 | scenario=scenario,
|
| 341 | operation=operation,
|
| 342 | metric=metric,
|
| 343 | subject=subject,
|
| 344 | value=value,
|
| 345 | baseline_value=None,
|
| 346 | pct_gap=None,
|
| 347 | score=None,
|
| 348 | n=n,
|
| 349 | evidence_file=str(lane.path),
|
| 350 | detail=detail,
|
| 351 | )
|
| 352 | )
|
| 353 |
|
| 354 | if recalls and min(recalls) < 1.0:
|
| 355 | add(
|
| 356 | "information_recall",
|
| 357 | min(recalls),
|
| 358 | len(recalls),
|
| 359 | "output drops changed-path names; bytes saved by losing information are not a win",
|
| 360 | )
|
| 361 | if precisions and min(precisions) < 1.0:
|
| 362 | add(
|
| 363 | "information_precision",
|
| 364 | min(precisions),
|
| 365 | len(precisions),
|
| 366 | "output names paths outside the changed set; recall without precision can reward noisy dumps",
|
| 367 | )
|
| 368 | if pipes and not all(pipes):
|
| 369 | rate = sum(1.0 for value in pipes if value) / len(pipes)
|
| 370 | add(
|
| 371 | "pipe_compatible_unified",
|
| 372 | rate,
|
| 373 | len(pipes),
|
| 374 | "unified-diff structure broken for `patch`-style piped tooling",
|
| 375 | )
|
| 376 | if stables and not all(stables):
|
| 377 | rate = sum(1.0 for value in stables if value) / len(stables)
|
| 378 | add(
|
| 379 | "output_stable",
|
| 380 | rate,
|
| 381 | len(stables),
|
| 382 | "identical state produces different bytes; provider prompt caches miss",
|
| 383 | )
|
| 384 | if ansis and max(ansis) > 0:
|
| 385 | add(
|
| 386 | "ansi_escape_count_non_tty",
|
| 387 | float(max(ansis)),
|
| 388 | len(ansis),
|
| 389 | "ANSI escapes in piped output are pure token waste",
|
| 390 | )
|
| 391 | entries.sort(key=lambda e: (e.scenario, e.operation, e.metric, e.subject))
|
| 392 | return entries
|
| 393 |
|
| 394 |
|
| 395 | def build_trends(lanes: list[Lane]) -> list[Opportunity]:
|
| 396 | """Section 3: Oak-only medians (remote.net.* and the mount lane)."""
|
| 397 | entries: list[Opportunity] = []
|
| 398 | for lane in lanes:
|
| 399 | if lane.missing:
|
| 400 | continue
|
| 401 | for (scenario, operation, subject), rows in sorted(lane.success.items()):
|
| 402 | if not is_oak_subject(subject, rows):
|
| 403 | continue
|
| 404 | if lane.name != "mount" and not operation.startswith("remote.net."):
|
| 405 | continue
|
| 406 | for metric, extract, _ in METRICS:
|
| 407 | values = metric_samples(rows, extract)
|
| 408 | value = median(values)
|
| 409 | if value is None:
|
| 410 | continue
|
| 411 | entries.append(
|
| 412 | Opportunity(
|
| 413 | section="trends",
|
| 414 | scenario=scenario,
|
| 415 | operation=operation,
|
| 416 | metric=metric,
|
| 417 | subject=subject,
|
| 418 | value=value,
|
| 419 | baseline_value=None,
|
| 420 | pct_gap=None,
|
| 421 | score=None,
|
| 422 | n=len(values),
|
| 423 | evidence_file=str(lane.path),
|
| 424 | )
|
| 425 | )
|
| 426 | entries.sort(key=lambda e: (e.scenario, e.operation, e.metric, e.subject))
|
| 427 | return entries
|
| 428 |
|
| 429 |
|
| 430 | def build_unmeasured(lanes: list[Lane]) -> list[Opportunity]:
|
| 431 | """Section 4: skip rows grouped by skip_reason β coverage to unlock."""
|
| 432 | groups: dict[str, dict[str, Any]] = {}
|
| 433 | for lane in lanes:
|
| 434 | if lane.missing:
|
| 435 | continue
|
| 436 | for row in lane.skips:
|
| 437 | reason = str(row.get("skip_reason") or "unspecified")
|
| 438 | group = groups.setdefault(
|
| 439 | reason, {"count": 0, "lanes": set(), "ops": set(), "files": set()}
|
| 440 | )
|
| 441 | group["count"] += 1
|
| 442 | group["lanes"].add(lane.name)
|
| 443 | group["ops"].add(f"{row.get('scenario')}/{row.get('operation')}")
|
| 444 | group["files"].add(str(lane.path))
|
| 445 | entries: list[Opportunity] = []
|
| 446 | for reason in sorted(groups, key=lambda r: (-groups[r]["count"], r)):
|
| 447 | group = groups[reason]
|
| 448 | remedy = SKIP_REMEDIES.get(reason, DEFAULT_SKIP_REMEDY)
|
| 449 | detail = (
|
| 450 | f"lanes: {', '.join(sorted(group['lanes']))}; "
|
| 451 | f"operations: {', '.join(sorted(group['ops']))}; "
|
| 452 | f"unlock: {remedy}"
|
| 453 | )
|
| 454 | entries.append(
|
| 455 | Opportunity(
|
| 456 | section="unmeasured",
|
| 457 | scenario=None,
|
| 458 | operation=None,
|
| 459 | metric=f"skip:{reason}",
|
| 460 | subject=None,
|
| 461 | value=float(group["count"]),
|
| 462 | baseline_value=None,
|
| 463 | pct_gap=None,
|
| 464 | score=None,
|
| 465 | n=group["count"],
|
| 466 | evidence_file=", ".join(sorted(group["files"])),
|
| 467 | detail=detail,
|
| 468 | )
|
| 469 | )
|
| 470 | return entries
|
| 471 |
|
| 472 |
|
| 473 | def fmt_value(metric: str, value: Optional[float]) -> str:
|
| 474 | if value is None:
|
| 475 | return "unmeasured"
|
| 476 | precision = METRIC_PRECISION.get(metric, 2)
|
| 477 | return f"{value:.{precision}f}"
|
| 478 |
|
| 479 |
|
| 480 | def n_suffix(entry: Opportunity) -> str:
|
| 481 | return f"(n={entry.n}, low-n)" if entry.low_n else f"(n={entry.n})"
|
| 482 |
|
| 483 |
|
| 484 | def loss_line(rank: int, entry: Opportunity) -> str:
|
| 485 | return (
|
| 486 | f"{rank}. `{entry.scenario}/{entry.operation}` β `{entry.metric}`: "
|
| 487 | f"{entry.subject} {fmt_value(entry.metric, entry.value)} vs git "
|
| 488 | f"{fmt_value(entry.metric, entry.baseline_value)} "
|
| 489 | f"({entry.pct_gap:+.1f}% worse, {entry.detail}, score {entry.score:.0f}) "
|
| 490 | f"{n_suffix(entry)} β `{entry.evidence_file}`"
|
| 491 | )
|
| 492 |
|
| 493 |
|
| 494 | def quality_line(entry: Opportunity) -> str:
|
| 495 | return (
|
| 496 | f"- `{entry.scenario}/{entry.operation}` `{entry.subject}` β "
|
| 497 | f"`{entry.metric}` = {fmt_value(entry.metric, entry.value)}: {entry.detail} "
|
| 498 | f"{n_suffix(entry)} β `{entry.evidence_file}`"
|
| 499 | )
|
| 500 |
|
| 501 |
|
| 502 | def trend_line(entry: Opportunity) -> str:
|
| 503 | return (
|
| 504 | f"- `{entry.scenario}/{entry.operation}` `{entry.subject}` β "
|
| 505 | f"median `{entry.metric}` = {fmt_value(entry.metric, entry.value)} "
|
| 506 | f"{n_suffix(entry)} β `{entry.evidence_file}`"
|
| 507 | )
|
| 508 |
|
| 509 |
|
| 510 | def unmeasured_line(entry: Opportunity) -> str:
|
| 511 | reason = entry.metric[len("skip:"):]
|
| 512 | return f"- {entry.n}Γ `{reason}` β {entry.detail}"
|
| 513 |
|
| 514 |
|
| 515 | def generate_report(
|
| 516 | results_dir: Path,
|
| 517 | git_subject: str = "git",
|
| 518 | oak_baseline_arg: Optional[str] = None,
|
| 519 | input_paths: Optional[list[Path]] = None,
|
| 520 | ) -> tuple[str, list[dict[str, Any]]]:
|
| 521 | lanes = load_lanes(results_dir, input_paths)
|
| 522 | oak_baseline = choose_oak_baseline(lanes, oak_baseline_arg)
|
| 523 |
|
| 524 | losses = build_losses(lanes, git_subject, oak_baseline)
|
| 525 | quality = build_quality_bar(lanes)
|
| 526 | trends = build_trends(lanes)
|
| 527 | unmeasured = build_unmeasured(lanes)
|
| 528 | low_confidence = [entry for entry in losses + quality + trends if entry.low_n]
|
| 529 |
|
| 530 | lines: list[str] = [
|
| 531 | "# Oak Opportunity Report",
|
| 532 | "",
|
| 533 | (
|
| 534 | "Ranked attack list for the Oak improvement swarm. Skip rows never enter "
|
| 535 | "medians or counts; null metric values are unmeasured, never zero (ADR-0002)."
|
| 536 | ),
|
| 537 | "",
|
| 538 | "## Inputs",
|
| 539 | "",
|
| 540 | ]
|
| 541 | for lane in lanes:
|
| 542 | if lane.missing:
|
| 543 | lines.append(f"- {lane.name}: `{lane.path}` β missing (lane not run)")
|
| 544 | else:
|
| 545 | lines.append(f"- {lane.name}: `{lane.path}` β read, {lane.row_count} rows")
|
| 546 | lines.append(f"- Git baseline: `{git_subject}`")
|
| 547 | lines.append(f"- Oak baseline: `{oak_baseline}`" if oak_baseline else "- Oak baseline: missing")
|
| 548 |
|
| 549 | lines.extend(
|
| 550 | [
|
| 551 | "",
|
| 552 | "## 1. Where Oak loses to Git",
|
| 553 | "",
|
| 554 | (
|
| 555 | "Successful rows only; medians per (scenario, operation). Score = pct worse "
|
| 556 | "x operation-family frequency weight (status/diff/commit/snapshot/proc.spawn=10, "
|
| 557 | "branch/log/show=5, init/clone/push/pull/fetch=2, else 1)."
|
| 558 | ),
|
| 559 | "",
|
| 560 | ]
|
| 561 | )
|
| 562 | if oak_baseline is None:
|
| 563 | lines.append("No Oak baseline subject present; comparison not possible.")
|
| 564 | elif not losses:
|
| 565 | lines.append("No (scenario, operation, metric) where the Oak baseline is worse than Git.")
|
| 566 | else:
|
| 567 | lines.extend(loss_line(rank, entry) for rank, entry in enumerate(losses, start=1))
|
| 568 |
|
| 569 | lines.extend(["", "## 2. Absolute quality bar", ""])
|
| 570 | if not quality:
|
| 571 | lines.append("No quality-bar violations on Oak subjects.")
|
| 572 | else:
|
| 573 | lines.extend(quality_line(entry) for entry in quality)
|
| 574 |
|
| 575 | lines.extend(["", "## 3. Oak-only trends (no Git comparator)", ""])
|
| 576 | mount_lane = next((lane for lane in lanes if lane.name == "mount"), None)
|
| 577 | if mount_lane is None:
|
| 578 | lines.append("- mount: lane not provided")
|
| 579 | elif mount_lane.missing:
|
| 580 | lines.append("- mount: lane not run")
|
| 581 | if not trends:
|
| 582 | lines.append("No remote.net.* or mount-lane measurements available.")
|
| 583 | else:
|
| 584 | lines.extend(trend_line(entry) for entry in trends)
|
| 585 |
|
| 586 | lines.extend(["", "## 4. Unmeasured (coverage to unlock)", ""])
|
| 587 | if not unmeasured:
|
| 588 | lines.append("No skip rows recorded.")
|
| 589 | else:
|
| 590 | lines.extend(unmeasured_line(entry) for entry in unmeasured)
|
| 591 |
|
| 592 | lines.extend(["", "## 5. Low confidence", ""])
|
| 593 | if not low_confidence:
|
| 594 | lines.append("No line above is based on fewer than 3 samples.")
|
| 595 | else:
|
| 596 | lines.append("Lines above based on fewer than 3 samples; re-run with more runs before trusting the gap.")
|
| 597 | lines.append("")
|
| 598 | for entry in low_confidence:
|
| 599 | lines.append(
|
| 600 | f"- [{entry.section}] `{entry.scenario}/{entry.operation}` "
|
| 601 | f"`{entry.metric}` `{entry.subject}` (n={entry.n})"
|
| 602 | )
|
| 603 |
|
| 604 | backlog_rank: dict[Opportunity, int] = {}
|
| 605 | for rank, entry in enumerate(losses + quality + trends + unmeasured, start=1):
|
| 606 | backlog_rank[entry] = rank
|
| 607 |
|
| 608 | json_entries: list[dict[str, Any]] = []
|
| 609 | for section_entries in (losses, quality, trends, unmeasured, low_confidence):
|
| 610 | for rank, entry in enumerate(section_entries, start=1):
|
| 611 | section = entry.section if section_entries is not low_confidence else "low_confidence"
|
| 612 | json_entries.append(
|
| 613 | {
|
| 614 | "section": section,
|
| 615 | "rank": rank,
|
| 616 | "backlog_rank": backlog_rank.get(entry),
|
| 617 | "scenario": entry.scenario,
|
| 618 | "operation": entry.operation,
|
| 619 | "metric": entry.metric,
|
| 620 | "subject": entry.subject,
|
| 621 | "value": entry.value,
|
| 622 | "baseline_value": entry.baseline_value,
|
| 623 | "pct_gap": entry.pct_gap,
|
| 624 | "score": entry.score,
|
| 625 | "n": entry.n,
|
| 626 | "low_n": entry.low_n,
|
| 627 | "evidence_file": entry.evidence_file,
|
| 628 | }
|
| 629 | )
|
| 630 |
|
| 631 | return "\n".join(lines) + "\n", json_entries
|
| 632 |
|
| 633 |
|
| 634 | def main(argv: Optional[list[str]] = None) -> int:
|
| 635 | args = parse_args(argv)
|
| 636 | report, json_entries = generate_report(
|
| 637 | args.results,
|
| 638 | args.git_subject,
|
| 639 | args.oak_baseline,
|
| 640 | args.jsonl or None,
|
| 641 | )
|
| 642 | sys.stdout.write(report)
|
| 643 | if args.json:
|
| 644 | with args.json.open("w") as fh:
|
| 645 | json.dump(json_entries, fh, indent=2, sort_keys=True)
|
| 646 | fh.write("\n")
|
| 647 | return 0
|
| 648 |
|
| 649 |
|
| 650 | if __name__ == "__main__":
|
| 651 | raise SystemExit(main())
|