| 1 | """Git Baseline Book and target loading.
|
| 2 |
|
| 3 | Baseline Books are compact committed summaries (ADR-0006). Targets are the
|
| 4 | red/green contract for scorecards (ADR-0008). Both are JSON here so the
|
| 5 | harness stays stdlib-only.
|
| 6 | """
|
| 7 |
|
| 8 | from __future__ import annotations
|
| 9 |
|
| 10 | import json
|
| 11 | import hashlib
|
| 12 | from dataclasses import dataclass
|
| 13 | from datetime import datetime, timezone
|
| 14 | from pathlib import Path
|
| 15 | from typing import Any
|
| 16 |
|
| 17 | from oakbench.rows import number_or_none, output_bytes, row_returncode, token_total, tool_calls
|
| 18 | from oakbench.stats import bootstrap_ci, named_statistic, validate_interval_settings
|
| 19 | from oakbench.populations import comparison_identity, independent_run_count, population_conflicts
|
| 20 |
|
| 21 |
|
| 22 | @dataclass(frozen=True)
|
| 23 | class BaselineCell:
|
| 24 | lane: str
|
| 25 | profile: str | None
|
| 26 | scenario: str
|
| 27 | operation: str
|
| 28 | subject: str
|
| 29 | metric: str
|
| 30 | statistic: str
|
| 31 | value: float
|
| 32 | ci_low: float | None = None
|
| 33 | ci_high: float | None = None
|
| 34 | n: int | None = None
|
| 35 | noise_floor_pct: float | None = None
|
| 36 | comparison_identity: dict[str, Any] | None = None
|
| 37 | independent_n: int | None = None
|
| 38 | attempted_n: int | None = None
|
| 39 | failure_n: int | None = None
|
| 40 | skipped_n: int | None = None
|
| 41 | confidence: float | None = None
|
| 42 | # "per_cell_aa": floor measured for THIS cell from an interleaved A/A Git
|
| 43 | # control (ADR-0011); "operator_global": one CLI number for every cell.
|
| 44 | noise_floor_source: str | None = None
|
| 45 |
|
| 46 | @property
|
| 47 | def key(self) -> tuple[str, str | None, str, str, str, str, str]:
|
| 48 | return (self.lane, self.profile, self.scenario, self.operation, self.subject, self.metric, self.statistic)
|
| 49 |
|
| 50 |
|
| 51 | @dataclass(frozen=True)
|
| 52 | class Target:
|
| 53 | target_id: str
|
| 54 | lane: str
|
| 55 | profile: str | None
|
| 56 | scenario: str
|
| 57 | operation: str
|
| 58 | subject: str
|
| 59 | comparator: str
|
| 60 | metric: str
|
| 61 | statistic: str
|
| 62 | goal_type: str
|
| 63 | goal_value: float
|
| 64 | guards: dict[str, Any]
|
| 65 |
|
| 66 | @property
|
| 67 | def baseline_key(self) -> tuple[str, str | None, str, str, str, str, str]:
|
| 68 | return (self.lane, self.profile, self.scenario, self.operation, self.comparator, self.metric, self.statistic)
|
| 69 |
|
| 70 |
|
| 71 | def load_baseline_book(path: Path) -> dict[tuple[str, str | None, str, str, str, str, str], BaselineCell]:
|
| 72 | raw = json.loads(path.read_text())
|
| 73 | if raw.get("schema_version") not in (1, 2):
|
| 74 | raise ValueError(f"unsupported baseline book schema_version: {raw.get('schema_version')!r}")
|
| 75 | cells = raw.get("cells")
|
| 76 | if not isinstance(cells, list):
|
| 77 | raise ValueError("baseline book requires list-valued 'cells'")
|
| 78 | parsed = [_parse_cell(cell) for cell in cells]
|
| 79 | result = {}
|
| 80 | for cell in parsed:
|
| 81 | if cell.key in result:
|
| 82 | raise ValueError(f"duplicate baseline cell: {cell.key}")
|
| 83 | result[cell.key] = cell
|
| 84 | return result
|
| 85 |
|
| 86 |
|
| 87 | def _parse_cell(cell: dict[str, Any]) -> BaselineCell:
|
| 88 | required = ("lane", "scenario", "operation", "subject", "metric", "statistic", "value")
|
| 89 | missing = [key for key in required if key not in cell]
|
| 90 | if missing:
|
| 91 | raise ValueError(f"baseline cell missing keys: {', '.join(missing)}")
|
| 92 | identity = cell.get("comparison_identity")
|
| 93 | if identity is not None and (not isinstance(identity, dict) or identity.get("schema_version") != 1):
|
| 94 | raise ValueError("unsupported baseline comparison_identity")
|
| 95 | return BaselineCell(
|
| 96 | lane=str(cell["lane"]),
|
| 97 | profile=None if cell.get("profile") is None else str(cell.get("profile")),
|
| 98 | scenario=str(cell["scenario"]),
|
| 99 | operation=str(cell["operation"]),
|
| 100 | subject=str(cell["subject"]),
|
| 101 | metric=str(cell["metric"]),
|
| 102 | statistic=str(cell["statistic"]),
|
| 103 | value=float(cell["value"]),
|
| 104 | ci_low=_optional_float(cell.get("ci_low")),
|
| 105 | ci_high=_optional_float(cell.get("ci_high")),
|
| 106 | n=_optional_int(cell.get("n")),
|
| 107 | noise_floor_pct=_optional_float(cell.get("noise_floor_pct")),
|
| 108 | comparison_identity=identity,
|
| 109 | independent_n=_optional_int(cell.get("independent_n")),
|
| 110 | attempted_n=_optional_int(cell.get("attempted_n")),
|
| 111 | failure_n=_optional_int(cell.get("failure_n")),
|
| 112 | skipped_n=_optional_int(cell.get("skipped_n")),
|
| 113 | confidence=_optional_float(cell.get("confidence")),
|
| 114 | noise_floor_source=cell.get("noise_floor_source") if isinstance(cell.get("noise_floor_source"), str) else None,
|
| 115 | )
|
| 116 |
|
| 117 |
|
| 118 | def load_targets(path: Path) -> list[Target]:
|
| 119 | raw = json.loads(path.read_text())
|
| 120 | if raw.get("schema_version") != 1:
|
| 121 | raise ValueError(f"unsupported targets schema_version: {raw.get('schema_version')!r}")
|
| 122 | targets = raw.get("targets")
|
| 123 | if not isinstance(targets, list):
|
| 124 | raise ValueError("targets config requires list-valued 'targets'")
|
| 125 | return [_parse_target(target) for target in targets]
|
| 126 |
|
| 127 |
|
| 128 | def _parse_target(target: dict[str, Any]) -> Target:
|
| 129 | required = ("id", "lane", "scenario", "operation", "subject", "comparator", "metric", "goal")
|
| 130 | missing = [key for key in required if key not in target]
|
| 131 | if missing:
|
| 132 | raise ValueError(f"target missing keys: {', '.join(missing)}")
|
| 133 | goal = target["goal"]
|
| 134 | if not isinstance(goal, dict):
|
| 135 | raise ValueError(f"target {target.get('id')!r} goal must be an object")
|
| 136 | if "type" not in goal:
|
| 137 | raise ValueError(f"target {target.get('id')!r} goal requires a 'type' field")
|
| 138 | goal_type = str(goal["type"])
|
| 139 | if goal_type != "ratio_at_most":
|
| 140 | raise ValueError(f"target {target.get('id')!r} unsupported goal type {goal_type!r}")
|
| 141 | if number_or_none(goal.get("value")) is None:
|
| 142 | raise ValueError(f"target {target.get('id')!r} goal requires a numeric 'value' field")
|
| 143 | guards = target.get("guards", {})
|
| 144 | if not isinstance(guards, dict):
|
| 145 | raise ValueError(f"target {target.get('id')!r} guards must be an object")
|
| 146 | return Target(
|
| 147 | target_id=str(target["id"]),
|
| 148 | lane=str(target["lane"]),
|
| 149 | profile=None if target.get("profile") is None else str(target.get("profile")),
|
| 150 | scenario=str(target["scenario"]),
|
| 151 | operation=str(target["operation"]),
|
| 152 | subject=str(target["subject"]),
|
| 153 | comparator=str(target["comparator"]),
|
| 154 | metric=str(target["metric"]),
|
| 155 | statistic=str(target.get("statistic", "median")),
|
| 156 | goal_type=goal_type,
|
| 157 | goal_value=float(goal["value"]),
|
| 158 | guards=guards,
|
| 159 | )
|
| 160 |
|
| 161 |
|
| 162 | def _optional_float(value: Any) -> float | None:
|
| 163 | if value is None:
|
| 164 | return None
|
| 165 | return float(value)
|
| 166 |
|
| 167 |
|
| 168 | def _optional_int(value: Any) -> int | None:
|
| 169 | if value is None:
|
| 170 | return None
|
| 171 | if isinstance(value, bool):
|
| 172 | raise ValueError("boolean is not an integer")
|
| 173 | return int(value)
|
| 174 |
|
| 175 |
|
| 176 | DEFAULT_BASELINE_METRICS = ("elapsed_ms", "tokens", "tool_calls", "output_bytes")
|
| 177 | DEFAULT_BASELINE_STATISTICS = ("median", "mean")
|
| 178 | SUBOP_SUFFIXES = (".add", ".commit")
|
| 179 |
|
| 180 |
|
| 181 | def iter_jsonl(paths: list[Path]) -> list[dict[str, Any]]:
|
| 182 | rows: list[dict[str, Any]] = []
|
| 183 | for path in paths:
|
| 184 | with path.open() as fh:
|
| 185 | for raw_line in fh:
|
| 186 | line = raw_line.strip()
|
| 187 | if line:
|
| 188 | rows.append(json.loads(line))
|
| 189 | return rows
|
| 190 |
|
| 191 |
|
| 192 | def metric_value(row: dict[str, Any], metric: str) -> float | None:
|
| 193 | if metric == "elapsed_ms":
|
| 194 | return number_or_none(row.get("elapsed_ms"))
|
| 195 | if metric == "tokens":
|
| 196 | return token_total(row)
|
| 197 | if metric == "tool_calls":
|
| 198 | return tool_calls(row)
|
| 199 | if metric == "output_bytes":
|
| 200 | return output_bytes(row)
|
| 201 | return number_or_none(row.get(metric))
|
| 202 |
|
| 203 |
|
| 204 | def cells_from_rows(
|
| 205 | rows: list[dict[str, Any]],
|
| 206 | *,
|
| 207 | lane: str,
|
| 208 | metrics: tuple[str, ...] = DEFAULT_BASELINE_METRICS,
|
| 209 | statistics_names: tuple[str, ...] = DEFAULT_BASELINE_STATISTICS,
|
| 210 | subjects: set[str] | None = None,
|
| 211 | include_subops: bool = False,
|
| 212 | bootstrap_samples: int = 2000,
|
| 213 | confidence: float = 0.95,
|
| 214 | seed: int = 20260609,
|
| 215 | noise_floor_pct: float | None = None,
|
| 216 | aa_control_subject: str | None = "git_aa",
|
| 217 | ) -> list[BaselineCell]:
|
| 218 | validate_interval_settings(bootstrap_samples, confidence)
|
| 219 | from oakbench import cellstats
|
| 220 | aa_samples: dict[tuple[str | None, str, str, str], dict[int, float]] = {}
|
| 221 | grouped: dict[tuple[str | None, str, str, str, str], list[float]] = {}
|
| 222 | observations = {}
|
| 223 | outcomes = {}
|
| 224 | for row in rows:
|
| 225 | if str(row.get("subject_kind") or "") != "git":
|
| 226 | continue
|
| 227 | subject = str(row.get("subject") or "")
|
| 228 | if aa_control_subject and subject == aa_control_subject:
|
| 229 | # The A/A control only feeds per-cell floors; it is never a book cell.
|
| 230 | if row_returncode(row) == 0 and isinstance(row.get("elapsed_ms"), (int, float)):
|
| 231 | key = (None if row.get("profile") is None else str(row.get("profile")),
|
| 232 | str(row.get("scenario") or ""), str(row.get("operation") or ""), "elapsed_ms")
|
| 233 | aa_samples.setdefault(key, {})[len(aa_samples.get(key, {}))
|
| 234 | if type(row.get("run")) is not int else row["run"]] = float(row["elapsed_ms"])
|
| 235 | continue
|
| 236 | if subjects is not None and subject not in subjects:
|
| 237 | continue
|
| 238 | operation = str(row.get("operation") or "")
|
| 239 | if not include_subops and operation.endswith(SUBOP_SUFFIXES):
|
| 240 | continue
|
| 241 | profile = None if row.get("profile") is None else str(row.get("profile"))
|
| 242 | scenario = str(row.get("scenario") or "")
|
| 243 | outcomes.setdefault((profile, scenario, operation, subject), []).append(row)
|
| 244 | if row_returncode(row) != 0:
|
| 245 | continue
|
| 246 | for metric in metrics:
|
| 247 | value = metric_value(row, metric)
|
| 248 | if value is None:
|
| 249 | continue
|
| 250 | key = (profile, scenario, operation, subject, metric)
|
| 251 | grouped.setdefault(key, []).append(float(value))
|
| 252 | observations.setdefault(key, []).append(row)
|
| 253 |
|
| 254 | cells: list[BaselineCell] = []
|
| 255 | for (profile, scenario, operation, subject, metric), values in sorted(grouped.items()):
|
| 256 | observed = observations[(profile, scenario, operation, subject, metric)]
|
| 257 | all_rows = outcomes[(profile, scenario, operation, subject)]
|
| 258 | conflicts = population_conflicts(all_rows)
|
| 259 | if conflicts:
|
| 260 | raise ValueError(f"incompatible baseline population {scenario}/{operation}: {', '.join(conflicts)}")
|
| 261 | cell_floor = noise_floor_pct
|
| 262 | floor_source = "operator_global" if noise_floor_pct is not None else None
|
| 263 | if aa_control_subject and subject == "git" and metric == "elapsed_ms":
|
| 264 | base = {(row.get("run") if type(row.get("run")) is int else index): float(value)
|
| 265 | for index, (row, value) in enumerate(zip(observed, values))}
|
| 266 | per_cell = cellstats.aa_floor_pct(base, aa_samples.get((profile, scenario, operation, metric), {}))
|
| 267 | if per_cell is not None:
|
| 268 | cell_floor = max(per_cell, noise_floor_pct or 0.0)
|
| 269 | floor_source = "per_cell_aa"
|
| 270 | for statistic_name in statistics_names:
|
| 271 | stat = named_statistic(statistic_name)
|
| 272 | ci_low, ci_high = bootstrap_ci(
|
| 273 | values,
|
| 274 | statistic=statistic_name,
|
| 275 | samples=bootstrap_samples,
|
| 276 | confidence=confidence,
|
| 277 | seed=seed,
|
| 278 | )
|
| 279 | cells.append(
|
| 280 | BaselineCell(
|
| 281 | lane=lane,
|
| 282 | profile=profile,
|
| 283 | scenario=scenario,
|
| 284 | operation=operation,
|
| 285 | subject=subject,
|
| 286 | metric=metric,
|
| 287 | statistic=statistic_name,
|
| 288 | value=float(stat(values)),
|
| 289 | ci_low=ci_low,
|
| 290 | ci_high=ci_high,
|
| 291 | n=len(values),
|
| 292 | noise_floor_pct=cell_floor,
|
| 293 | noise_floor_source=floor_source,
|
| 294 | comparison_identity=comparison_identity(observed[0]),
|
| 295 | independent_n=independent_run_count(observed),
|
| 296 | attempted_n=len(all_rows),
|
| 297 | failure_n=sum(row_returncode(row) not in (0, 77) for row in all_rows),
|
| 298 | skipped_n=sum(row_returncode(row) == 77 for row in all_rows),
|
| 299 | confidence=confidence,
|
| 300 | )
|
| 301 | )
|
| 302 | return cells
|
| 303 |
|
| 304 |
|
| 305 | def book_json(cells: list[BaselineCell]) -> dict[str, Any]:
|
| 306 | return {
|
| 307 | "schema_version": 2,
|
| 308 | "cells": [
|
| 309 | {
|
| 310 | "lane": cell.lane,
|
| 311 | "profile": cell.profile,
|
| 312 | "scenario": cell.scenario,
|
| 313 | "operation": cell.operation,
|
| 314 | "subject": cell.subject,
|
| 315 | "metric": cell.metric,
|
| 316 | "statistic": cell.statistic,
|
| 317 | "value": cell.value,
|
| 318 | "ci_low": cell.ci_low,
|
| 319 | "ci_high": cell.ci_high,
|
| 320 | "n": cell.n,
|
| 321 | "noise_floor_pct": cell.noise_floor_pct,
|
| 322 | "noise_floor_source": cell.noise_floor_source,
|
| 323 | "comparison_identity": cell.comparison_identity,
|
| 324 | "independent_n": cell.independent_n,
|
| 325 | "attempted_n": cell.attempted_n,
|
| 326 | "failure_n": cell.failure_n,
|
| 327 | "skipped_n": cell.skipped_n,
|
| 328 | "confidence": cell.confidence,
|
| 329 | }
|
| 330 | for cell in cells
|
| 331 | ],
|
| 332 | }
|
| 333 |
|
| 334 |
|
| 335 | def manifest_json(
|
| 336 | *,
|
| 337 | book_id: str,
|
| 338 | row_paths: list[Path],
|
| 339 | runner_class: str | None,
|
| 340 | runner_id: str | None = None,
|
| 341 | runner_profile: dict[str, Any] | None = None,
|
| 342 | git_version: str | None = None,
|
| 343 | git_binary_sha256: str | None = None,
|
| 344 | raw_storage_keys: list[str] | None = None,
|
| 345 | suite_hash: str | None = None,
|
| 346 | ) -> dict[str, Any]:
|
| 347 | return {
|
| 348 | "schema_version": 1,
|
| 349 | "book_id": book_id,
|
| 350 | "generated_at_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 351 | "suite_hash": suite_hash,
|
| 352 | "runner_id": runner_id,
|
| 353 | "runner_class": runner_class,
|
| 354 | "runner_profile": runner_profile,
|
| 355 | "git_version": git_version,
|
| 356 | "git_binary_sha256": git_binary_sha256,
|
| 357 | "raw_storage_keys": raw_storage_keys or [],
|
| 358 | "raw_inputs": [
|
| 359 | {
|
| 360 | "path": str(path),
|
| 361 | "sha256": sha256_file(path),
|
| 362 | "bytes": path.stat().st_size,
|
| 363 | }
|
| 364 | for path in row_paths
|
| 365 | ],
|
| 366 | }
|
| 367 |
|
| 368 |
|
| 369 | def book_markdown(book_id: str, cells: list[BaselineCell]) -> str:
|
| 370 | lines = [
|
| 371 | f"# Git Baseline Book: `{book_id}`",
|
| 372 | "",
|
| 373 | "| Subject | Scenario / operation | Metric | Statistic | n | Value | CI |",
|
| 374 | "| --- | --- | --- | --- | ---: | ---: | --- |",
|
| 375 | ]
|
| 376 | for cell in cells:
|
| 377 | ci = ""
|
| 378 | if cell.ci_low is not None and cell.ci_high is not None:
|
| 379 | ci = f"[{cell.ci_low:.3f}, {cell.ci_high:.3f}]"
|
| 380 | lines.append(
|
| 381 | f"| `{cell.subject}` | `{cell.scenario}/{cell.operation}` | `{cell.metric}` | "
|
| 382 | f"`{cell.statistic}` | {cell.n or 0} | {cell.value:.3f} | {ci} |"
|
| 383 | )
|
| 384 | lines.append("")
|
| 385 | return "\n".join(lines)
|
| 386 |
|
| 387 |
|
| 388 | def sha256_file(path: Path) -> str:
|
| 389 | digest = hashlib.sha256()
|
| 390 | with path.open("rb") as fh:
|
| 391 | for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
| 392 | digest.update(chunk)
|
| 393 | return digest.hexdigest()
|