Viewing files at commit c85a464b4b25Commit detailsView latest

Browse files
scripts/publish_gate.py 557 lines · 23.7 KB · python
1
#!/usr/bin/env python3
2
"""Pre-publish gate: mechanical checks before any public table ships.
3
4
Run against the result files you intend to publish:
5
6
  python3 scripts/publish_gate.py results/latest.jsonl results/latest.mount.jsonl ...
7
8
Checks (fail = exit 1; manual items print as MANUAL and never fail the gate):
9
10
1. calibration     docs/token-calibration-factors.md exists and is committed —
11
                   published token deltas must cite calibrated factors, not
12
                   raw char/4 (README warns char/4 is biased for VCS output).
13
2. tuned-git       core-lane results include git_untracked_cache AND
14
                   git_fsmonitor subject rows. Stock git alone overstates oak
15
                   wins on wide trees.
16
3. transports      no row set mixes remote transports under one operation
17
                   name (remote.net.* vs remote.*); rows touching a remote
18
                   carry remote_transport.
19
4. levels          agent rows carry instruction_level and the file holds only
20
                   one level (never aggregate across levels).
21
5. nulls           null means unmeasured: summaries must not render null as 0.
22
                   (Spot-checked by scanning for '| 0 |' columns whose JSONL
23
                   value is null — heuristic, reported as MANUAL.)
24
6. skips           every returncode-77 row carries skip_reason (work items,
25
                   never silent absence); count is printed for the summary.
26
7. tails           rows with tail percentiles carry their sample counts
27
                   (tail_latency_summary embeds n; absence is a failure).
28
8. provenance      published Oak subject rows carry clean Oak source
29
                   provenance (hash present, status clean).
30
9. samples         public core rows carry enough repetitions per
31
                   subject/scenario/operation group (n>=30).
32
10. randomization  public core rows state whether subject order was randomized;
33
                   randomized rows carry the per-run subject order.
34
11. failures       public rows do not hide failed operations behind successful
35
                   averages.
36
12. tracks         each result file holds one benchmark_track.
37
13. env-isolation  public core rows carry one env isolation version so rows
38
                   measured before/after harness environment policy changes
39
                   cannot be compared silently.
40
14. conformance    inputs holding Oak rows also hold diagnostic conformance rows
41
                   (scripts/vcs_conformance.py --output) for every published Oak
42
                   binary; the Oak rows pass the conformance gate against
43
                   config/conformance_expected_failures.json (no regression, no
44
                   missing behaviour, no FIXED-but-still-listed entry).
45
"""
46
47
from __future__ import annotations
48
49
import json
50
import sys
51
from pathlib import Path
52
53
from oakbench.integrity import content_integrity_public_trust_passed
54
from oakbench.public_output import output_evidence_failures, workflow_evidence_failures, workflow_qualification_failures
55
from oakbench.rows import row_returncode
56
57
ROOT = Path(__file__).resolve().parents[1]
58
CALIBRATION_DOC = ROOT / "docs" / "token-calibration-factors.md"
59
# Tuned subjects whose mode.setup.* rows must all succeed before any of their
60
# rows are publishable (a failed setup means stock git measured under a tuned
61
# label).
62
TUNED_GIT_SUBJECTS = {"git_untracked_cache", "git_fsmonitor", "git_lfs"}
63
# Tuned subjects every core publish must include. git_lfs is deliberately not
64
# required here: it only applies to the binary-fixture scenarios listed in
65
# config/git_modes.json and needs git-lfs installed, so its absence is an
66
# honest coverage gap (skip rows), while a present-but-failed lfs setup still
67
# fails the gate via TUNED_GIT_SUBJECTS above.
68
REQUIRED_TUNED_GIT_SUBJECTS = {"git_untracked_cache", "git_fsmonitor"}
69
CORE_PROFILES = {"smoke", "standard", "large", "micro"}
70
PUBLIC_CORE_PROFILES = {"standard", "large", "micro"}
71
PUBLIC_CORE_MIN_SAMPLES = 30
72
EXCLUDED_SAMPLE_OP_SUFFIXES = (".add", ".commit", ".determinism", ".inforecall")
73
EXCLUDED_SAMPLE_OP_PREFIXES = ("mode.setup",)
74
RANDOMIZATION_FIELDS = ("randomized_subject_order", "randomize_subject_order", "subject_order_randomized")
75
SUBJECT_ORDER_FIELDS = ("subject_order", "run_subject_order")
76
77
78
def load_rows(path: Path) -> list[dict]:
79
    rows = []
80
    for line in path.read_text().splitlines():
81
        line = line.strip()
82
        if line:
83
            rows.append(json.loads(line))
84
    return rows
85
86
87
def is_oak_subject(row: dict) -> bool:
88
    subject = str(row.get("subject", ""))
89
    if subject.startswith("oak"):
90
        return True
91
    if row.get("subject_kind") == "oak":
92
        return True
93
    details = row.get("subject_details")
94
    if isinstance(details, dict):
95
        subject_detail = details.get(subject)
96
        if isinstance(subject_detail, dict):
97
            return subject_detail.get("kind") == "oak" or str(
98
                subject_detail.get("role", "")
99
            ).startswith("oak_")
100
    return False
101
102
103
def source_status_is_clean(status: object) -> bool:
104
    if not isinstance(status, str):
105
        return False
106
    normalized = " ".join(status.strip().lower().split())
107
    return normalized in {
108
        "",
109
        "clean",
110
        "no changes",
111
        "nothing to commit",
112
        "nothing to commit, working tree clean",
113
    }
114
115
116
def randomization_value(row: dict) -> object:
117
    for field in RANDOMIZATION_FIELDS:
118
        if field in row:
119
            return row[field]
120
    return None
121
122
123
def subject_order(row: dict) -> object:
124
    for field in SUBJECT_ORDER_FIELDS:
125
        if field in row:
126
            return row[field]
127
    return None
128
129
130
def is_skip(row: dict) -> bool:
131
    return row_returncode(row) == 77 or bool(row.get("skipped"))
132
133
134
def is_excluded_sample_operation(operation: str) -> bool:
135
    return operation.endswith(EXCLUDED_SAMPLE_OP_SUFFIXES) or operation.startswith(
136
        EXCLUDED_SAMPLE_OP_PREFIXES
137
    )
138
139
140
def sample_run_id(row: dict, fallback: int) -> str:
141
    for field in ("run", "run_index"):
142
        value = row.get(field)
143
        if value is not None:
144
            return str(value)
145
    return f"row:{fallback}"
146
147
148
def oak_binary_identity(row: dict) -> str | None:
149
    """SHA-256 of the Oak binary that produced a row, from any lane's identity field."""
150
    for key in ("binary_sha256", "subject_binary_sha256", "workflow_subject_binary_sha256"):
151
        value = row.get(key)
152
        if isinstance(value, str) and len(value) == 64:
153
            return value
154
    details = row.get("subject_details")
155
    if isinstance(details, dict):
156
        item = details.get(str(row.get("subject")))
157
        if isinstance(item, dict) and isinstance(item.get("binary_sha256"), str) and len(item["binary_sha256"]) == 64:
158
            return item["binary_sha256"]
159
    return None
160
161
162
def conformance_gate_lines(all_rows: list[tuple[Path, dict]]) -> tuple[list[str], list[str]]:
163
    """Check 14: every published Oak binary passes the conformance gate on ITS OWN rows."""
164
    from oakbench import conformance_gate
165
    from oakbench.conformance import SCENARIOS
166
    from oakbench.conformance_matrix import BEHAVIOURS
167
    conformance_rows = [row for _, row in all_rows if row.get("evidence_class") == "diagnostic_conformance"]
168
    oak_rows = [(path, row) for path, row in all_rows
169
                if is_oak_subject(row) and row.get("evidence_class") != "diagnostic_conformance"]
170
    if not oak_rows:
171
        return [], ["PASS   conformance: no Oak rows among inputs"]
172
    anonymous = [(str(path), row.get("subject"), row.get("operation")) for path, row in oak_rows
173
                 if oak_binary_identity(row) is None]
174
    failures = []
175
    if anonymous:
176
        failures.append("FAIL   conformance: Oak rows lack binary identity (binary_sha256 or subject_details), so "
177
                        f"their conformance evidence cannot be matched: {anonymous[:5]}")
178
    published = sorted({oak_binary_identity(row) for _, row in oak_rows} - {None})
179
    if not conformance_rows:
180
        failures.append("FAIL   conformance: Oak rows published without diagnostic conformance rows; run "
181
                        "scripts/vcs_conformance.py --output <file> with the published Oak binary and pass it here")
182
        return failures, []
183
    try:
184
        expected = conformance_gate.load_expected()
185
    except (OSError, ValueError) as exc:
186
        return failures + [f"FAIL   conformance: expected-failures list unreadable: {exc}"], []
187
    required = list(SCENARIOS) + [behaviour.id for behaviour in BEHAVIOURS]
188
    notes = []
189
    for sha in published:
190
        # Only this binary's subject rows (plus reference rows) are evaluated:
191
        # rows of another Oak build can neither satisfy nor fail this one.
192
        rows = [row for row in conformance_rows
193
                if row.get("subject") != expected["subject"] or row.get("binary_sha256") == sha]
194
        result = conformance_gate.evaluate(rows, expected, required)
195
        if result["verdict"] != "PASS":
196
            failures.append(f"FAIL   conformance[{sha[:12]}]: {result['verdict']}: "
197
                            f"{(result['regressions'] + result['unmeasured'])[:5]}")
198
        if result["fixed"]:
199
            failures.append(f"FAIL   conformance[{sha[:12]}]: expected-failures list is stale (now passing): "
200
                            f"{result['fixed'][:5]}")
201
        if result["verdict"] == "PASS" and not result["fixed"]:
202
            notes.append(f"PASS   [{sha[:12]}] " + conformance_gate.summary_lines(result)[0])
203
    return failures, ([] if failures else notes)
204
205
206
def main(paths: list[str]) -> int:
207
    failures: list[str] = []
208
    notes: list[str] = []
209
210
    if CALIBRATION_DOC.exists():
211
        notes.append(f"PASS   calibration: {CALIBRATION_DOC.name} present")
212
    else:
213
        failures.append(
214
            "FAIL   calibration: docs/token-calibration-factors.md missing — run "
215
            "token_calibration.py per output style before publishing token deltas"
216
        )
217
218
    all_rows: list[tuple[Path, dict]] = []
219
    for raw in paths:
220
        path = Path(raw)
221
        if not path.exists():
222
            failures.append(f"FAIL   missing result file: {path}")
223
            continue
224
        for row in load_rows(path):
225
            all_rows.append((path, row))
226
227
    core_rows = [row for _, row in all_rows if row.get("profile") in CORE_PROFILES]
228
    if core_rows:
229
        subjects = {
230
            str(row.get("subject"))
231
            for row in core_rows
232
            if row_returncode(row) == 0
233
            and not is_excluded_sample_operation(str(row.get("operation") or ""))
234
        }
235
        missing = REQUIRED_TUNED_GIT_SUBJECTS - subjects
236
        if missing:
237
            failures.append(
238
                f"FAIL   tuned-git: core rows lack {sorted(missing)} — rerun with "
239
                "--git-modes untracked_cache,fsmonitor (verify fsmonitor--daemon health on darwin)"
240
            )
241
        else:
242
            notes.append("PASS   tuned-git: untracked_cache and fsmonitor rows present")
243
244
        bad_tuned_setup = [
245
            (row.get("subject"), row.get("operation"), row.get("returncode"))
246
            for row in core_rows
247
            if str(row.get("subject")) in TUNED_GIT_SUBJECTS
248
            and str(row.get("operation") or "").startswith(EXCLUDED_SAMPLE_OP_PREFIXES)
249
            and row_returncode(row) != 0
250
        ]
251
        if bad_tuned_setup:
252
            failures.append(
253
                "FAIL   tuned-git: tuned Git setup failed, so tuned rows are not publishable: "
254
                f"{bad_tuned_setup[:5]}"
255
            )
256
    else:
257
        notes.append("MANUAL tuned-git: no core-lane rows among inputs; check the core table separately")
258
259
    oak_provenance_failures = []
260
    for path, row in all_rows:
261
        if not is_oak_subject(row) or row.get("evidence_class") == "diagnostic_conformance":
262
            continue  # conformance rows carry binary identity, checked by the conformance gate
263
        source = row.get("source")
264
        source = source if isinstance(source, dict) else {}
265
        oak_hash = source.get("oak_hash")
266
        oak_status = source.get("oak_status")
267
        if not oak_hash or not source_status_is_clean(oak_status):
268
            oak_provenance_failures.append(
269
                (str(path), row.get("subject"), row.get("operation"), oak_hash, oak_status)
270
            )
271
    if oak_provenance_failures:
272
        failures.append(
273
            "FAIL   provenance: published Oak rows lack clean Oak source provenance: "
274
            f"{oak_provenance_failures[:5]}"
275
        )
276
    elif any(is_oak_subject(row) for _, row in all_rows):
277
        notes.append("PASS   provenance: Oak subject rows carry clean source hash/status")
278
279
    fake_provider_rows = [
280
        (str(path), row.get("platform"), row.get("driver"), row.get("scenario"), row.get("operation"))
281
        for path, row in all_rows
282
        if row.get("driver") == "fake-provider" or row.get("branch_triage_provider") == "fake"
283
    ]
284
    if fake_provider_rows:
285
        failures.append(
286
            "FAIL   fake-provider: simulated provider rows are not publishable: "
287
            f"{fake_provider_rows[:5]}"
288
        )
289
290
    public_core_rows = [
291
        (path, row)
292
        for path, row in all_rows
293
        if row.get("profile") in PUBLIC_CORE_PROFILES and not is_skip(row)
294
    ]
295
    public_failures = [
296
        (str(path), row.get("subject"), row.get("scenario"), row.get("operation"), row.get("returncode"))
297
        for path, row in all_rows
298
        if row.get("profile") in PUBLIC_CORE_PROFILES
299
        and not is_skip(row)
300
        and row_returncode(row) != 0
301
    ]
302
    if public_failures:
303
        failures.append(
304
            "FAIL   failures: public core rows contain failed operations: "
305
            f"{public_failures[:5]}"
306
        )
307
308
    workflow_evidence = workflow_evidence_failures([row for _, row in all_rows])
309
    workflow_evidence.extend(workflow_qualification_failures([row for _, row in all_rows], PUBLIC_CORE_MIN_SAMPLES))
310
    if workflow_evidence:
311
        failures.append("FAIL   workflow-evidence: " + "; ".join(workflow_evidence[:5]))
312
313
    unsupported_output = [
314
        (str(path), row.get("subject"), row.get("scenario"), row.get("operation"), reasons)
315
        for path, row in public_core_rows
316
        for reasons in [output_evidence_failures(row)]
317
        if reasons
318
    ]
319
    if unsupported_output:
320
        failures.append(
321
            "FAIL   output-evidence: public output usefulness claims lack exact evidence: "
322
            f"{unsupported_output[:5]}"
323
        )
324
325
    weak_content_trust = [
326
        (
327
            str(path),
328
            row.get("subject"),
329
            row.get("scenario"),
330
            row.get("operation"),
331
            row.get("content_integrity_source"),
332
            row.get("content_integrity_sources"),
333
        )
334
        for path, row in public_core_rows
335
        if row.get("content_integrity_check_passed") is True
336
        and not content_integrity_public_trust_passed(row)
337
    ]
338
    if weak_content_trust:
339
        failures.append(
340
            "FAIL   content-trust: public core rows claim passing content integrity "
341
            "from weak or mixed sources: "
342
            f"{weak_content_trust[:5]}"
343
        )
344
    elif any(row.get("content_integrity_check_passed") is True for _, row in public_core_rows):
345
        notes.append("PASS   content-trust: passing public content checks use strong sources")
346
347
    env_versions = {
348
        str(row.get("env_isolation_version"))
349
        for _, row in public_core_rows
350
        if isinstance(row.get("env_isolation_version"), str)
351
        and str(row.get("env_isolation_version")).strip()
352
    }
353
    missing_env_version = [
354
        (
355
            str(path),
356
            row.get("subject"),
357
            row.get("scenario"),
358
            row.get("operation"),
359
            row.get("run", row.get("run_index")),
360
        )
361
        for path, row in public_core_rows
362
        if not isinstance(row.get("env_isolation_version"), str)
363
        or not str(row.get("env_isolation_version")).strip()
364
    ]
365
    if missing_env_version:
366
        failures.append(
367
            "FAIL   env-isolation: public core rows missing env_isolation_version: "
368
            f"{missing_env_version[:5]}"
369
        )
370
    elif len(env_versions) > 1:
371
        failures.append(
372
            "FAIL   env-isolation: public core rows mix env_isolation_version values: "
373
            f"{sorted(env_versions)}"
374
        )
375
    elif env_versions:
376
        notes.append(f"PASS   env-isolation: public core rows use version {next(iter(env_versions))}")
377
378
    measured_groups: dict[tuple[str, str, str, str, str, str], set[str]] = {}
379
    sample_groups: dict[tuple[str, str, str, str, str, str], set[str]] = {}
380
    for index, (path, row) in enumerate(public_core_rows):
381
        operation = str(row.get("operation") or "")
382
        if is_excluded_sample_operation(operation):
383
            continue
384
        key = (
385
            str(path),
386
            str(row.get("profile")),
387
            str(row.get("benchmark_track", "")),
388
            str(row.get("scenario", "")),
389
            operation,
390
            str(row.get("subject", "")),
391
        )
392
        run_id = sample_run_id(row, index)
393
        measured_groups.setdefault(key, set()).add(run_id)
394
        if row_returncode(row) == 0:
395
            sample_groups.setdefault(key, set()).add(run_id)
396
    thin_groups = {
397
        "/".join(key[1:]): len(runs)
398
        for key, runs in measured_groups.items()
399
        if len(runs) < PUBLIC_CORE_MIN_SAMPLES
400
    }
401
    zero_success_groups = {
402
        "/".join(key[1:]): len(runs)
403
        for key, runs in measured_groups.items()
404
        if not sample_groups.get(key)
405
    }
406
    if thin_groups:
407
        failures.append(
408
            f"FAIL   samples: public core groups below n={PUBLIC_CORE_MIN_SAMPLES}: "
409
            f"{dict(list(thin_groups.items())[:5])}"
410
        )
411
    if zero_success_groups:
412
        failures.append(
413
            "FAIL   samples: public core groups with zero successful rows: "
414
            f"{dict(list(zero_success_groups.items())[:5])}"
415
        )
416
    if public_core_rows and not thin_groups and not zero_success_groups:
417
        notes.append(f"PASS   samples: public core groups have n>={PUBLIC_CORE_MIN_SAMPLES}")
418
419
    missing_randomization = [
420
        (
421
            str(path),
422
            row.get("subject"),
423
            row.get("scenario"),
424
            row.get("operation"),
425
            row.get("run", row.get("run_index")),
426
        )
427
        for path, row in public_core_rows
428
        if randomization_value(row) is None
429
    ]
430
    if missing_randomization:
431
        failures.append(
432
            "FAIL   randomization: public core rows missing randomization metadata: "
433
            f"{missing_randomization[:5]}"
434
        )
435
    else:
436
        not_randomized = [
437
            (
438
                str(path),
439
                row.get("subject"),
440
                row.get("scenario"),
441
                row.get("operation"),
442
                row.get("run", row.get("run_index")),
443
            )
444
            for path, row in public_core_rows
445
            if randomization_value(row) is not True
446
        ]
447
        randomized_without_order = []
448
        if not_randomized:
449
            failures.append(
450
                "FAIL   randomization: public core rows were not randomized: "
451
                f"{not_randomized[:5]}"
452
            )
453
        else:
454
            for path, row in public_core_rows:
455
                order = subject_order(row)
456
                if not isinstance(order, list) or not order or row.get("subject") not in order:
457
                    randomized_without_order.append(
458
                        (
459
                            str(path),
460
                            row.get("subject"),
461
                            row.get("scenario"),
462
                            row.get("operation"),
463
                            row.get("run", row.get("run_index")),
464
                        )
465
                    )
466
        if randomized_without_order:
467
            failures.append(
468
                "FAIL   randomization: randomized public core rows lack per-run subject order: "
469
                f"{randomized_without_order[:5]}"
470
            )
471
        elif public_core_rows:
472
            notes.append("PASS   randomization: public core rows state run-order metadata")
473
474
    tracks_by_file: dict[Path, set[str]] = {}
475
    for path, row in all_rows:
476
        track = row.get("benchmark_track")
477
        if track:
478
            tracks_by_file.setdefault(path, set()).add(str(track))
479
    mixed_tracks = {str(path): sorted(tracks) for path, tracks in tracks_by_file.items() if len(tracks) > 1}
480
    if mixed_tracks:
481
        failures.append(f"FAIL   tracks: files mix benchmark_track values: {mixed_tracks}")
482
    elif tracks_by_file:
483
        notes.append("PASS   tracks: each result file holds a single benchmark_track")
484
485
    # Transport hygiene: remote-touching rows must state their transport, and
486
    # one subject's operation must not mix transports. Side-by-side tables may
487
    # contain the same operation name for network and local_file subjects.
488
    missing_transport = [
489
        (str(path), row.get("subject"), row.get("operation"))
490
        for path, row in all_rows
491
        if (row.get("remote_repo") or row.get("remote_server"))
492
        and not (row.get("remote_transport") or row.get("workspace_transport"))
493
    ]
494
    if missing_transport:
495
        failures.append(f"FAIL   transports: remote rows without transport: {missing_transport[:5]}")
496
497
    op_transports: dict[tuple[str, str], set[str]] = {}
498
    for _, row in all_rows:
499
        transport = row.get("remote_transport") or row.get("workspace_transport")
500
        if transport:
501
            key = (str(row.get("subject")), str(row.get("operation")))
502
            op_transports.setdefault(key, set()).add(str(transport))
503
    mixed = {f"{subject} {op}": sorted(ts) for (subject, op), ts in op_transports.items() if len(ts) > 1}
504
    if mixed:
505
        failures.append(f"FAIL   transports: operations span multiple transports: {mixed}")
506
    elif op_transports:
507
        notes.append(f"PASS   transports: {len(op_transports)} remote-touching operations, one transport each")
508
509
    levels_by_file: dict[Path, set[str]] = {}
510
    for path, row in all_rows:
511
        level = row.get("instruction_level")
512
        if level is None and isinstance(row.get("agent"), dict):
513
            level = row["agent"].get("instruction_level")
514
        if level:
515
            levels_by_file.setdefault(path, set()).add(str(level))
516
    multi = {str(p): sorted(ls) for p, ls in levels_by_file.items() if len(ls) > 1}
517
    if multi:
518
        failures.append(f"FAIL   levels: files mix instruction levels (never aggregate): {multi}")
519
    elif levels_by_file:
520
        notes.append("PASS   levels: each agent result file holds a single instruction level")
521
522
    bad_skips = [
523
        (str(path), row.get("operation"))
524
        for path, row in all_rows
525
        if row_returncode(row) == 77 and not row.get("skip_reason")
526
    ]
527
    skip_count = sum(1 for _, row in all_rows if row_returncode(row) == 77)
528
    if bad_skips:
529
        failures.append(f"FAIL   skips: returncode-77 rows without skip_reason: {bad_skips[:5]}")
530
    else:
531
        notes.append(f"PASS   skips: {skip_count} skip rows, all carrying skip_reason (account for each in prose)")
532
533
    for path, row in all_rows:
534
        tail = (row.get("parallel_metrics") or {}).get("commit_latency_tail")
535
        if isinstance(tail, dict) and "samples" not in tail and "n" not in tail and "sample_count" not in tail:
536
            failures.append(f"FAIL   tails: {path.name} {row.get('operation')} tail summary lacks sample count")
537
            break
538
    else:
539
        notes.append("PASS   tails: tail summaries carry sample counts (or none present)")
540
541
    notes.append("MANUAL nulls: confirm summaries render null as 'unmeasured', never 0")
542
    notes.append("MANUAL prose: state transport asymmetries (oak network vs git local) next to every side-by-side")
543
544
    conformance_failures, conformance_notes = conformance_gate_lines(all_rows)
545
    failures += conformance_failures
546
    notes += conformance_notes
547
548
    for line in notes:
549
        print(line)
550
    for line in failures:
551
        print(line)
552
    print(f"\n{'GATE FAIL' if failures else 'GATE PASS'} ({len(failures)} failure(s), inputs: {len(paths)})")
553
    return 1 if failures else 0
554
555
556
if __name__ == "__main__":
557
    raise SystemExit(main(sys.argv[1:]))