Viewing files at commit 21316fe59317Commit detailsView latest

Browse files
scripts/publish_gate.py 533 lines · 22.3 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 conformance_gate_lines(all_rows: list[tuple[Path, dict]]) -> tuple[list[str], list[str]]:
149
    from oakbench import conformance_gate
150
    from oakbench.conformance import SCENARIOS
151
    from oakbench.conformance_matrix import BEHAVIOURS
152
    conformance_rows = [row for _, row in all_rows if row.get("evidence_class") == "diagnostic_conformance"]
153
    oak_rows = [row for _, row in all_rows
154
                if is_oak_subject(row) and row.get("evidence_class") != "diagnostic_conformance"]
155
    if not conformance_rows:
156
        if oak_rows:
157
            return ["FAIL   conformance: Oak rows published without diagnostic conformance rows; run "
158
                    "scripts/vcs_conformance.py --output <file> with the published Oak binary and pass it here"], []
159
        return [], ["PASS   conformance: no Oak rows among inputs"]
160
    try:
161
        expected = conformance_gate.load_expected()
162
    except (OSError, ValueError) as exc:
163
        return [f"FAIL   conformance: expected-failures list unreadable: {exc}"], []
164
    required = list(SCENARIOS) + [behaviour.id for behaviour in BEHAVIOURS]
165
    result = conformance_gate.evaluate(conformance_rows, expected, required)
166
    failures = []
167
    if result["verdict"] != "PASS":
168
        failures.append(f"FAIL   conformance: {result['verdict']}: "
169
                        f"{(result['regressions'] + result['unmeasured'])[:5]}")
170
    if result["fixed"]:
171
        failures.append(f"FAIL   conformance: expected-failures list is stale (now passing): {result['fixed'][:5]}")
172
    published = {row.get("binary_sha256") for row in oak_rows if row.get("binary_sha256")}
173
    covered = {row.get("binary_sha256") for row in conformance_rows
174
               if row.get("subject") == expected["subject"] and row.get("binary_sha256")}
175
    if published - covered:
176
        failures.append(f"FAIL   conformance: published Oak binaries lack conformance rows: {sorted(published - covered)[:3]}")
177
    if failures:
178
        return failures, []
179
    return [], ["PASS   " + conformance_gate.summary_lines(result)[0]]
180
181
182
def main(paths: list[str]) -> int:
183
    failures: list[str] = []
184
    notes: list[str] = []
185
186
    if CALIBRATION_DOC.exists():
187
        notes.append(f"PASS   calibration: {CALIBRATION_DOC.name} present")
188
    else:
189
        failures.append(
190
            "FAIL   calibration: docs/token-calibration-factors.md missing — run "
191
            "token_calibration.py per output style before publishing token deltas"
192
        )
193
194
    all_rows: list[tuple[Path, dict]] = []
195
    for raw in paths:
196
        path = Path(raw)
197
        if not path.exists():
198
            failures.append(f"FAIL   missing result file: {path}")
199
            continue
200
        for row in load_rows(path):
201
            all_rows.append((path, row))
202
203
    core_rows = [row for _, row in all_rows if row.get("profile") in CORE_PROFILES]
204
    if core_rows:
205
        subjects = {
206
            str(row.get("subject"))
207
            for row in core_rows
208
            if row_returncode(row) == 0
209
            and not is_excluded_sample_operation(str(row.get("operation") or ""))
210
        }
211
        missing = REQUIRED_TUNED_GIT_SUBJECTS - subjects
212
        if missing:
213
            failures.append(
214
                f"FAIL   tuned-git: core rows lack {sorted(missing)} — rerun with "
215
                "--git-modes untracked_cache,fsmonitor (verify fsmonitor--daemon health on darwin)"
216
            )
217
        else:
218
            notes.append("PASS   tuned-git: untracked_cache and fsmonitor rows present")
219
220
        bad_tuned_setup = [
221
            (row.get("subject"), row.get("operation"), row.get("returncode"))
222
            for row in core_rows
223
            if str(row.get("subject")) in TUNED_GIT_SUBJECTS
224
            and str(row.get("operation") or "").startswith(EXCLUDED_SAMPLE_OP_PREFIXES)
225
            and row_returncode(row) != 0
226
        ]
227
        if bad_tuned_setup:
228
            failures.append(
229
                "FAIL   tuned-git: tuned Git setup failed, so tuned rows are not publishable: "
230
                f"{bad_tuned_setup[:5]}"
231
            )
232
    else:
233
        notes.append("MANUAL tuned-git: no core-lane rows among inputs; check the core table separately")
234
235
    oak_provenance_failures = []
236
    for path, row in all_rows:
237
        if not is_oak_subject(row) or row.get("evidence_class") == "diagnostic_conformance":
238
            continue  # conformance rows carry binary identity, checked by the conformance gate
239
        source = row.get("source")
240
        source = source if isinstance(source, dict) else {}
241
        oak_hash = source.get("oak_hash")
242
        oak_status = source.get("oak_status")
243
        if not oak_hash or not source_status_is_clean(oak_status):
244
            oak_provenance_failures.append(
245
                (str(path), row.get("subject"), row.get("operation"), oak_hash, oak_status)
246
            )
247
    if oak_provenance_failures:
248
        failures.append(
249
            "FAIL   provenance: published Oak rows lack clean Oak source provenance: "
250
            f"{oak_provenance_failures[:5]}"
251
        )
252
    elif any(is_oak_subject(row) for _, row in all_rows):
253
        notes.append("PASS   provenance: Oak subject rows carry clean source hash/status")
254
255
    fake_provider_rows = [
256
        (str(path), row.get("platform"), row.get("driver"), row.get("scenario"), row.get("operation"))
257
        for path, row in all_rows
258
        if row.get("driver") == "fake-provider" or row.get("branch_triage_provider") == "fake"
259
    ]
260
    if fake_provider_rows:
261
        failures.append(
262
            "FAIL   fake-provider: simulated provider rows are not publishable: "
263
            f"{fake_provider_rows[:5]}"
264
        )
265
266
    public_core_rows = [
267
        (path, row)
268
        for path, row in all_rows
269
        if row.get("profile") in PUBLIC_CORE_PROFILES and not is_skip(row)
270
    ]
271
    public_failures = [
272
        (str(path), row.get("subject"), row.get("scenario"), row.get("operation"), row.get("returncode"))
273
        for path, row in all_rows
274
        if row.get("profile") in PUBLIC_CORE_PROFILES
275
        and not is_skip(row)
276
        and row_returncode(row) != 0
277
    ]
278
    if public_failures:
279
        failures.append(
280
            "FAIL   failures: public core rows contain failed operations: "
281
            f"{public_failures[:5]}"
282
        )
283
284
    workflow_evidence = workflow_evidence_failures([row for _, row in all_rows])
285
    workflow_evidence.extend(workflow_qualification_failures([row for _, row in all_rows], PUBLIC_CORE_MIN_SAMPLES))
286
    if workflow_evidence:
287
        failures.append("FAIL   workflow-evidence: " + "; ".join(workflow_evidence[:5]))
288
289
    unsupported_output = [
290
        (str(path), row.get("subject"), row.get("scenario"), row.get("operation"), reasons)
291
        for path, row in public_core_rows
292
        for reasons in [output_evidence_failures(row)]
293
        if reasons
294
    ]
295
    if unsupported_output:
296
        failures.append(
297
            "FAIL   output-evidence: public output usefulness claims lack exact evidence: "
298
            f"{unsupported_output[:5]}"
299
        )
300
301
    weak_content_trust = [
302
        (
303
            str(path),
304
            row.get("subject"),
305
            row.get("scenario"),
306
            row.get("operation"),
307
            row.get("content_integrity_source"),
308
            row.get("content_integrity_sources"),
309
        )
310
        for path, row in public_core_rows
311
        if row.get("content_integrity_check_passed") is True
312
        and not content_integrity_public_trust_passed(row)
313
    ]
314
    if weak_content_trust:
315
        failures.append(
316
            "FAIL   content-trust: public core rows claim passing content integrity "
317
            "from weak or mixed sources: "
318
            f"{weak_content_trust[:5]}"
319
        )
320
    elif any(row.get("content_integrity_check_passed") is True for _, row in public_core_rows):
321
        notes.append("PASS   content-trust: passing public content checks use strong sources")
322
323
    env_versions = {
324
        str(row.get("env_isolation_version"))
325
        for _, row in public_core_rows
326
        if isinstance(row.get("env_isolation_version"), str)
327
        and str(row.get("env_isolation_version")).strip()
328
    }
329
    missing_env_version = [
330
        (
331
            str(path),
332
            row.get("subject"),
333
            row.get("scenario"),
334
            row.get("operation"),
335
            row.get("run", row.get("run_index")),
336
        )
337
        for path, row in public_core_rows
338
        if not isinstance(row.get("env_isolation_version"), str)
339
        or not str(row.get("env_isolation_version")).strip()
340
    ]
341
    if missing_env_version:
342
        failures.append(
343
            "FAIL   env-isolation: public core rows missing env_isolation_version: "
344
            f"{missing_env_version[:5]}"
345
        )
346
    elif len(env_versions) > 1:
347
        failures.append(
348
            "FAIL   env-isolation: public core rows mix env_isolation_version values: "
349
            f"{sorted(env_versions)}"
350
        )
351
    elif env_versions:
352
        notes.append(f"PASS   env-isolation: public core rows use version {next(iter(env_versions))}")
353
354
    measured_groups: dict[tuple[str, str, str, str, str, str], set[str]] = {}
355
    sample_groups: dict[tuple[str, str, str, str, str, str], set[str]] = {}
356
    for index, (path, row) in enumerate(public_core_rows):
357
        operation = str(row.get("operation") or "")
358
        if is_excluded_sample_operation(operation):
359
            continue
360
        key = (
361
            str(path),
362
            str(row.get("profile")),
363
            str(row.get("benchmark_track", "")),
364
            str(row.get("scenario", "")),
365
            operation,
366
            str(row.get("subject", "")),
367
        )
368
        run_id = sample_run_id(row, index)
369
        measured_groups.setdefault(key, set()).add(run_id)
370
        if row_returncode(row) == 0:
371
            sample_groups.setdefault(key, set()).add(run_id)
372
    thin_groups = {
373
        "/".join(key[1:]): len(runs)
374
        for key, runs in measured_groups.items()
375
        if len(runs) < PUBLIC_CORE_MIN_SAMPLES
376
    }
377
    zero_success_groups = {
378
        "/".join(key[1:]): len(runs)
379
        for key, runs in measured_groups.items()
380
        if not sample_groups.get(key)
381
    }
382
    if thin_groups:
383
        failures.append(
384
            f"FAIL   samples: public core groups below n={PUBLIC_CORE_MIN_SAMPLES}: "
385
            f"{dict(list(thin_groups.items())[:5])}"
386
        )
387
    if zero_success_groups:
388
        failures.append(
389
            "FAIL   samples: public core groups with zero successful rows: "
390
            f"{dict(list(zero_success_groups.items())[:5])}"
391
        )
392
    if public_core_rows and not thin_groups and not zero_success_groups:
393
        notes.append(f"PASS   samples: public core groups have n>={PUBLIC_CORE_MIN_SAMPLES}")
394
395
    missing_randomization = [
396
        (
397
            str(path),
398
            row.get("subject"),
399
            row.get("scenario"),
400
            row.get("operation"),
401
            row.get("run", row.get("run_index")),
402
        )
403
        for path, row in public_core_rows
404
        if randomization_value(row) is None
405
    ]
406
    if missing_randomization:
407
        failures.append(
408
            "FAIL   randomization: public core rows missing randomization metadata: "
409
            f"{missing_randomization[:5]}"
410
        )
411
    else:
412
        not_randomized = [
413
            (
414
                str(path),
415
                row.get("subject"),
416
                row.get("scenario"),
417
                row.get("operation"),
418
                row.get("run", row.get("run_index")),
419
            )
420
            for path, row in public_core_rows
421
            if randomization_value(row) is not True
422
        ]
423
        randomized_without_order = []
424
        if not_randomized:
425
            failures.append(
426
                "FAIL   randomization: public core rows were not randomized: "
427
                f"{not_randomized[:5]}"
428
            )
429
        else:
430
            for path, row in public_core_rows:
431
                order = subject_order(row)
432
                if not isinstance(order, list) or not order or row.get("subject") not in order:
433
                    randomized_without_order.append(
434
                        (
435
                            str(path),
436
                            row.get("subject"),
437
                            row.get("scenario"),
438
                            row.get("operation"),
439
                            row.get("run", row.get("run_index")),
440
                        )
441
                    )
442
        if randomized_without_order:
443
            failures.append(
444
                "FAIL   randomization: randomized public core rows lack per-run subject order: "
445
                f"{randomized_without_order[:5]}"
446
            )
447
        elif public_core_rows:
448
            notes.append("PASS   randomization: public core rows state run-order metadata")
449
450
    tracks_by_file: dict[Path, set[str]] = {}
451
    for path, row in all_rows:
452
        track = row.get("benchmark_track")
453
        if track:
454
            tracks_by_file.setdefault(path, set()).add(str(track))
455
    mixed_tracks = {str(path): sorted(tracks) for path, tracks in tracks_by_file.items() if len(tracks) > 1}
456
    if mixed_tracks:
457
        failures.append(f"FAIL   tracks: files mix benchmark_track values: {mixed_tracks}")
458
    elif tracks_by_file:
459
        notes.append("PASS   tracks: each result file holds a single benchmark_track")
460
461
    # Transport hygiene: remote-touching rows must state their transport, and
462
    # one subject's operation must not mix transports. Side-by-side tables may
463
    # contain the same operation name for network and local_file subjects.
464
    missing_transport = [
465
        (str(path), row.get("subject"), row.get("operation"))
466
        for path, row in all_rows
467
        if (row.get("remote_repo") or row.get("remote_server"))
468
        and not (row.get("remote_transport") or row.get("workspace_transport"))
469
    ]
470
    if missing_transport:
471
        failures.append(f"FAIL   transports: remote rows without transport: {missing_transport[:5]}")
472
473
    op_transports: dict[tuple[str, str], set[str]] = {}
474
    for _, row in all_rows:
475
        transport = row.get("remote_transport") or row.get("workspace_transport")
476
        if transport:
477
            key = (str(row.get("subject")), str(row.get("operation")))
478
            op_transports.setdefault(key, set()).add(str(transport))
479
    mixed = {f"{subject} {op}": sorted(ts) for (subject, op), ts in op_transports.items() if len(ts) > 1}
480
    if mixed:
481
        failures.append(f"FAIL   transports: operations span multiple transports: {mixed}")
482
    elif op_transports:
483
        notes.append(f"PASS   transports: {len(op_transports)} remote-touching operations, one transport each")
484
485
    levels_by_file: dict[Path, set[str]] = {}
486
    for path, row in all_rows:
487
        level = row.get("instruction_level")
488
        if level is None and isinstance(row.get("agent"), dict):
489
            level = row["agent"].get("instruction_level")
490
        if level:
491
            levels_by_file.setdefault(path, set()).add(str(level))
492
    multi = {str(p): sorted(ls) for p, ls in levels_by_file.items() if len(ls) > 1}
493
    if multi:
494
        failures.append(f"FAIL   levels: files mix instruction levels (never aggregate): {multi}")
495
    elif levels_by_file:
496
        notes.append("PASS   levels: each agent result file holds a single instruction level")
497
498
    bad_skips = [
499
        (str(path), row.get("operation"))
500
        for path, row in all_rows
501
        if row_returncode(row) == 77 and not row.get("skip_reason")
502
    ]
503
    skip_count = sum(1 for _, row in all_rows if row_returncode(row) == 77)
504
    if bad_skips:
505
        failures.append(f"FAIL   skips: returncode-77 rows without skip_reason: {bad_skips[:5]}")
506
    else:
507
        notes.append(f"PASS   skips: {skip_count} skip rows, all carrying skip_reason (account for each in prose)")
508
509
    for path, row in all_rows:
510
        tail = (row.get("parallel_metrics") or {}).get("commit_latency_tail")
511
        if isinstance(tail, dict) and "samples" not in tail and "n" not in tail and "sample_count" not in tail:
512
            failures.append(f"FAIL   tails: {path.name} {row.get('operation')} tail summary lacks sample count")
513
            break
514
    else:
515
        notes.append("PASS   tails: tail summaries carry sample counts (or none present)")
516
517
    notes.append("MANUAL nulls: confirm summaries render null as 'unmeasured', never 0")
518
    notes.append("MANUAL prose: state transport asymmetries (oak network vs git local) next to every side-by-side")
519
520
    conformance_failures, conformance_notes = conformance_gate_lines(all_rows)
521
    failures += conformance_failures
522
    notes += conformance_notes
523
524
    for line in notes:
525
        print(line)
526
    for line in failures:
527
        print(line)
528
    print(f"\n{'GATE FAIL' if failures else 'GATE PASS'} ({len(failures)} failure(s), inputs: {len(paths)})")
529
    return 1 if failures else 0
530
531
532
if __name__ == "__main__":
533
    raise SystemExit(main(sys.argv[1:]))