Log in
scripts/opportunities.py 651 lines · 24.1 KB · python Blame
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())