Viewing files at commit e70815b9044aCommit detailsView latest

Browse files
scripts/publish_gate.py 490 lines · 19.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
"""
41
42
from __future__ import annotations
43
44
import json
45
import sys
46
from pathlib import Path
47
48
from oakbench.integrity import content_integrity_public_trust_passed
49
from oakbench.public_output import output_evidence_failures, workflow_evidence_failures, workflow_qualification_failures
50
from oakbench.rows import row_returncode
51
52
ROOT = Path(__file__).resolve().parents[1]
53
CALIBRATION_DOC = ROOT / "docs" / "token-calibration-factors.md"
54
# Tuned subjects whose mode.setup.* rows must all succeed before any of their
55
# rows are publishable (a failed setup means stock git measured under a tuned
56
# label).
57
TUNED_GIT_SUBJECTS = {"git_untracked_cache", "git_fsmonitor", "git_lfs"}
58
# Tuned subjects every core publish must include. git_lfs is deliberately not
59
# required here: it only applies to the binary-fixture scenarios listed in
60
# config/git_modes.json and needs git-lfs installed, so its absence is an
61
# honest coverage gap (skip rows), while a present-but-failed lfs setup still
62
# fails the gate via TUNED_GIT_SUBJECTS above.
63
REQUIRED_TUNED_GIT_SUBJECTS = {"git_untracked_cache", "git_fsmonitor"}
64
CORE_PROFILES = {"smoke", "standard", "large", "micro"}
65
PUBLIC_CORE_PROFILES = {"standard", "large", "micro"}
66
PUBLIC_CORE_MIN_SAMPLES = 30
67
EXCLUDED_SAMPLE_OP_SUFFIXES = (".add", ".commit", ".determinism", ".inforecall")
68
EXCLUDED_SAMPLE_OP_PREFIXES = ("mode.setup",)
69
RANDOMIZATION_FIELDS = ("randomized_subject_order", "randomize_subject_order", "subject_order_randomized")
70
SUBJECT_ORDER_FIELDS = ("subject_order", "run_subject_order")
71
72
73
def load_rows(path: Path) -> list[dict]:
74
    rows = []
75
    for line in path.read_text().splitlines():
76
        line = line.strip()
77
        if line:
78
            rows.append(json.loads(line))
79
    return rows
80
81
82
def is_oak_subject(row: dict) -> bool:
83
    subject = str(row.get("subject", ""))
84
    if subject.startswith("oak"):
85
        return True
86
    if row.get("subject_kind") == "oak":
87
        return True
88
    details = row.get("subject_details")
89
    if isinstance(details, dict):
90
        subject_detail = details.get(subject)
91
        if isinstance(subject_detail, dict):
92
            return subject_detail.get("kind") == "oak" or str(
93
                subject_detail.get("role", "")
94
            ).startswith("oak_")
95
    return False
96
97
98
def source_status_is_clean(status: object) -> bool:
99
    if not isinstance(status, str):
100
        return False
101
    normalized = " ".join(status.strip().lower().split())
102
    return normalized in {
103
        "",
104
        "clean",
105
        "no changes",
106
        "nothing to commit",
107
        "nothing to commit, working tree clean",
108
    }
109
110
111
def randomization_value(row: dict) -> object:
112
    for field in RANDOMIZATION_FIELDS:
113
        if field in row:
114
            return row[field]
115
    return None
116
117
118
def subject_order(row: dict) -> object:
119
    for field in SUBJECT_ORDER_FIELDS:
120
        if field in row:
121
            return row[field]
122
    return None
123
124
125
def is_skip(row: dict) -> bool:
126
    return row_returncode(row) == 77 or bool(row.get("skipped"))
127
128
129
def is_excluded_sample_operation(operation: str) -> bool:
130
    return operation.endswith(EXCLUDED_SAMPLE_OP_SUFFIXES) or operation.startswith(
131
        EXCLUDED_SAMPLE_OP_PREFIXES
132
    )
133
134
135
def sample_run_id(row: dict, fallback: int) -> str:
136
    for field in ("run", "run_index"):
137
        value = row.get(field)
138
        if value is not None:
139
            return str(value)
140
    return f"row:{fallback}"
141
142
143
def main(paths: list[str]) -> int:
144
    failures: list[str] = []
145
    notes: list[str] = []
146
147
    if CALIBRATION_DOC.exists():
148
        notes.append(f"PASS   calibration: {CALIBRATION_DOC.name} present")
149
    else:
150
        failures.append(
151
            "FAIL   calibration: docs/token-calibration-factors.md missing — run "
152
            "token_calibration.py per output style before publishing token deltas"
153
        )
154
155
    all_rows: list[tuple[Path, dict]] = []
156
    for raw in paths:
157
        path = Path(raw)
158
        if not path.exists():
159
            failures.append(f"FAIL   missing result file: {path}")
160
            continue
161
        for row in load_rows(path):
162
            all_rows.append((path, row))
163
164
    core_rows = [row for _, row in all_rows if row.get("profile") in CORE_PROFILES]
165
    if core_rows:
166
        subjects = {
167
            str(row.get("subject"))
168
            for row in core_rows
169
            if row_returncode(row) == 0
170
            and not is_excluded_sample_operation(str(row.get("operation") or ""))
171
        }
172
        missing = REQUIRED_TUNED_GIT_SUBJECTS - subjects
173
        if missing:
174
            failures.append(
175
                f"FAIL   tuned-git: core rows lack {sorted(missing)} — rerun with "
176
                "--git-modes untracked_cache,fsmonitor (verify fsmonitor--daemon health on darwin)"
177
            )
178
        else:
179
            notes.append("PASS   tuned-git: untracked_cache and fsmonitor rows present")
180
181
        bad_tuned_setup = [
182
            (row.get("subject"), row.get("operation"), row.get("returncode"))
183
            for row in core_rows
184
            if str(row.get("subject")) in TUNED_GIT_SUBJECTS
185
            and str(row.get("operation") or "").startswith(EXCLUDED_SAMPLE_OP_PREFIXES)
186
            and row_returncode(row) != 0
187
        ]
188
        if bad_tuned_setup:
189
            failures.append(
190
                "FAIL   tuned-git: tuned Git setup failed, so tuned rows are not publishable: "
191
                f"{bad_tuned_setup[:5]}"
192
            )
193
    else:
194
        notes.append("MANUAL tuned-git: no core-lane rows among inputs; check the core table separately")
195
196
    oak_provenance_failures = []
197
    for path, row in all_rows:
198
        if not is_oak_subject(row):
199
            continue
200
        source = row.get("source")
201
        source = source if isinstance(source, dict) else {}
202
        oak_hash = source.get("oak_hash")
203
        oak_status = source.get("oak_status")
204
        if not oak_hash or not source_status_is_clean(oak_status):
205
            oak_provenance_failures.append(
206
                (str(path), row.get("subject"), row.get("operation"), oak_hash, oak_status)
207
            )
208
    if oak_provenance_failures:
209
        failures.append(
210
            "FAIL   provenance: published Oak rows lack clean Oak source provenance: "
211
            f"{oak_provenance_failures[:5]}"
212
        )
213
    elif any(is_oak_subject(row) for _, row in all_rows):
214
        notes.append("PASS   provenance: Oak subject rows carry clean source hash/status")
215
216
    fake_provider_rows = [
217
        (str(path), row.get("platform"), row.get("driver"), row.get("scenario"), row.get("operation"))
218
        for path, row in all_rows
219
        if row.get("driver") == "fake-provider" or row.get("branch_triage_provider") == "fake"
220
    ]
221
    if fake_provider_rows:
222
        failures.append(
223
            "FAIL   fake-provider: simulated provider rows are not publishable: "
224
            f"{fake_provider_rows[:5]}"
225
        )
226
227
    public_core_rows = [
228
        (path, row)
229
        for path, row in all_rows
230
        if row.get("profile") in PUBLIC_CORE_PROFILES and not is_skip(row)
231
    ]
232
    public_failures = [
233
        (str(path), row.get("subject"), row.get("scenario"), row.get("operation"), row.get("returncode"))
234
        for path, row in all_rows
235
        if row.get("profile") in PUBLIC_CORE_PROFILES
236
        and not is_skip(row)
237
        and row_returncode(row) != 0
238
    ]
239
    if public_failures:
240
        failures.append(
241
            "FAIL   failures: public core rows contain failed operations: "
242
            f"{public_failures[:5]}"
243
        )
244
245
    workflow_evidence = workflow_evidence_failures([row for _, row in all_rows])
246
    workflow_evidence.extend(workflow_qualification_failures([row for _, row in all_rows], PUBLIC_CORE_MIN_SAMPLES))
247
    if workflow_evidence:
248
        failures.append("FAIL   workflow-evidence: " + "; ".join(workflow_evidence[:5]))
249
250
    unsupported_output = [
251
        (str(path), row.get("subject"), row.get("scenario"), row.get("operation"), reasons)
252
        for path, row in public_core_rows
253
        for reasons in [output_evidence_failures(row)]
254
        if reasons
255
    ]
256
    if unsupported_output:
257
        failures.append(
258
            "FAIL   output-evidence: public output usefulness claims lack exact evidence: "
259
            f"{unsupported_output[:5]}"
260
        )
261
262
    weak_content_trust = [
263
        (
264
            str(path),
265
            row.get("subject"),
266
            row.get("scenario"),
267
            row.get("operation"),
268
            row.get("content_integrity_source"),
269
            row.get("content_integrity_sources"),
270
        )
271
        for path, row in public_core_rows
272
        if row.get("content_integrity_check_passed") is True
273
        and not content_integrity_public_trust_passed(row)
274
    ]
275
    if weak_content_trust:
276
        failures.append(
277
            "FAIL   content-trust: public core rows claim passing content integrity "
278
            "from weak or mixed sources: "
279
            f"{weak_content_trust[:5]}"
280
        )
281
    elif any(row.get("content_integrity_check_passed") is True for _, row in public_core_rows):
282
        notes.append("PASS   content-trust: passing public content checks use strong sources")
283
284
    env_versions = {
285
        str(row.get("env_isolation_version"))
286
        for _, row in public_core_rows
287
        if isinstance(row.get("env_isolation_version"), str)
288
        and str(row.get("env_isolation_version")).strip()
289
    }
290
    missing_env_version = [
291
        (
292
            str(path),
293
            row.get("subject"),
294
            row.get("scenario"),
295
            row.get("operation"),
296
            row.get("run", row.get("run_index")),
297
        )
298
        for path, row in public_core_rows
299
        if not isinstance(row.get("env_isolation_version"), str)
300
        or not str(row.get("env_isolation_version")).strip()
301
    ]
302
    if missing_env_version:
303
        failures.append(
304
            "FAIL   env-isolation: public core rows missing env_isolation_version: "
305
            f"{missing_env_version[:5]}"
306
        )
307
    elif len(env_versions) > 1:
308
        failures.append(
309
            "FAIL   env-isolation: public core rows mix env_isolation_version values: "
310
            f"{sorted(env_versions)}"
311
        )
312
    elif env_versions:
313
        notes.append(f"PASS   env-isolation: public core rows use version {next(iter(env_versions))}")
314
315
    measured_groups: dict[tuple[str, str, str, str, str, str], set[str]] = {}
316
    sample_groups: dict[tuple[str, str, str, str, str, str], set[str]] = {}
317
    for index, (path, row) in enumerate(public_core_rows):
318
        operation = str(row.get("operation") or "")
319
        if is_excluded_sample_operation(operation):
320
            continue
321
        key = (
322
            str(path),
323
            str(row.get("profile")),
324
            str(row.get("benchmark_track", "")),
325
            str(row.get("scenario", "")),
326
            operation,
327
            str(row.get("subject", "")),
328
        )
329
        run_id = sample_run_id(row, index)
330
        measured_groups.setdefault(key, set()).add(run_id)
331
        if row_returncode(row) == 0:
332
            sample_groups.setdefault(key, set()).add(run_id)
333
    thin_groups = {
334
        "/".join(key[1:]): len(runs)
335
        for key, runs in measured_groups.items()
336
        if len(runs) < PUBLIC_CORE_MIN_SAMPLES
337
    }
338
    zero_success_groups = {
339
        "/".join(key[1:]): len(runs)
340
        for key, runs in measured_groups.items()
341
        if not sample_groups.get(key)
342
    }
343
    if thin_groups:
344
        failures.append(
345
            f"FAIL   samples: public core groups below n={PUBLIC_CORE_MIN_SAMPLES}: "
346
            f"{dict(list(thin_groups.items())[:5])}"
347
        )
348
    if zero_success_groups:
349
        failures.append(
350
            "FAIL   samples: public core groups with zero successful rows: "
351
            f"{dict(list(zero_success_groups.items())[:5])}"
352
        )
353
    if public_core_rows and not thin_groups and not zero_success_groups:
354
        notes.append(f"PASS   samples: public core groups have n>={PUBLIC_CORE_MIN_SAMPLES}")
355
356
    missing_randomization = [
357
        (
358
            str(path),
359
            row.get("subject"),
360
            row.get("scenario"),
361
            row.get("operation"),
362
            row.get("run", row.get("run_index")),
363
        )
364
        for path, row in public_core_rows
365
        if randomization_value(row) is None
366
    ]
367
    if missing_randomization:
368
        failures.append(
369
            "FAIL   randomization: public core rows missing randomization metadata: "
370
            f"{missing_randomization[:5]}"
371
        )
372
    else:
373
        not_randomized = [
374
            (
375
                str(path),
376
                row.get("subject"),
377
                row.get("scenario"),
378
                row.get("operation"),
379
                row.get("run", row.get("run_index")),
380
            )
381
            for path, row in public_core_rows
382
            if randomization_value(row) is not True
383
        ]
384
        randomized_without_order = []
385
        if not_randomized:
386
            failures.append(
387
                "FAIL   randomization: public core rows were not randomized: "
388
                f"{not_randomized[:5]}"
389
            )
390
        else:
391
            for path, row in public_core_rows:
392
                order = subject_order(row)
393
                if not isinstance(order, list) or not order or row.get("subject") not in order:
394
                    randomized_without_order.append(
395
                        (
396
                            str(path),
397
                            row.get("subject"),
398
                            row.get("scenario"),
399
                            row.get("operation"),
400
                            row.get("run", row.get("run_index")),
401
                        )
402
                    )
403
        if randomized_without_order:
404
            failures.append(
405
                "FAIL   randomization: randomized public core rows lack per-run subject order: "
406
                f"{randomized_without_order[:5]}"
407
            )
408
        elif public_core_rows:
409
            notes.append("PASS   randomization: public core rows state run-order metadata")
410
411
    tracks_by_file: dict[Path, set[str]] = {}
412
    for path, row in all_rows:
413
        track = row.get("benchmark_track")
414
        if track:
415
            tracks_by_file.setdefault(path, set()).add(str(track))
416
    mixed_tracks = {str(path): sorted(tracks) for path, tracks in tracks_by_file.items() if len(tracks) > 1}
417
    if mixed_tracks:
418
        failures.append(f"FAIL   tracks: files mix benchmark_track values: {mixed_tracks}")
419
    elif tracks_by_file:
420
        notes.append("PASS   tracks: each result file holds a single benchmark_track")
421
422
    # Transport hygiene: remote-touching rows must state their transport, and
423
    # one subject's operation must not mix transports. Side-by-side tables may
424
    # contain the same operation name for network and local_file subjects.
425
    missing_transport = [
426
        (str(path), row.get("subject"), row.get("operation"))
427
        for path, row in all_rows
428
        if (row.get("remote_repo") or row.get("remote_server"))
429
        and not (row.get("remote_transport") or row.get("workspace_transport"))
430
    ]
431
    if missing_transport:
432
        failures.append(f"FAIL   transports: remote rows without transport: {missing_transport[:5]}")
433
434
    op_transports: dict[tuple[str, str], set[str]] = {}
435
    for _, row in all_rows:
436
        transport = row.get("remote_transport") or row.get("workspace_transport")
437
        if transport:
438
            key = (str(row.get("subject")), str(row.get("operation")))
439
            op_transports.setdefault(key, set()).add(str(transport))
440
    mixed = {f"{subject} {op}": sorted(ts) for (subject, op), ts in op_transports.items() if len(ts) > 1}
441
    if mixed:
442
        failures.append(f"FAIL   transports: operations span multiple transports: {mixed}")
443
    elif op_transports:
444
        notes.append(f"PASS   transports: {len(op_transports)} remote-touching operations, one transport each")
445
446
    levels_by_file: dict[Path, set[str]] = {}
447
    for path, row in all_rows:
448
        level = row.get("instruction_level")
449
        if level is None and isinstance(row.get("agent"), dict):
450
            level = row["agent"].get("instruction_level")
451
        if level:
452
            levels_by_file.setdefault(path, set()).add(str(level))
453
    multi = {str(p): sorted(ls) for p, ls in levels_by_file.items() if len(ls) > 1}
454
    if multi:
455
        failures.append(f"FAIL   levels: files mix instruction levels (never aggregate): {multi}")
456
    elif levels_by_file:
457
        notes.append("PASS   levels: each agent result file holds a single instruction level")
458
459
    bad_skips = [
460
        (str(path), row.get("operation"))
461
        for path, row in all_rows
462
        if row_returncode(row) == 77 and not row.get("skip_reason")
463
    ]
464
    skip_count = sum(1 for _, row in all_rows if row_returncode(row) == 77)
465
    if bad_skips:
466
        failures.append(f"FAIL   skips: returncode-77 rows without skip_reason: {bad_skips[:5]}")
467
    else:
468
        notes.append(f"PASS   skips: {skip_count} skip rows, all carrying skip_reason (account for each in prose)")
469
470
    for path, row in all_rows:
471
        tail = (row.get("parallel_metrics") or {}).get("commit_latency_tail")
472
        if isinstance(tail, dict) and "samples" not in tail and "n" not in tail and "sample_count" not in tail:
473
            failures.append(f"FAIL   tails: {path.name} {row.get('operation')} tail summary lacks sample count")
474
            break
475
    else:
476
        notes.append("PASS   tails: tail summaries carry sample counts (or none present)")
477
478
    notes.append("MANUAL nulls: confirm summaries render null as 'unmeasured', never 0")
479
    notes.append("MANUAL prose: state transport asymmetries (oak network vs git local) next to every side-by-side")
480
481
    for line in notes:
482
        print(line)
483
    for line in failures:
484
        print(line)
485
    print(f"\n{'GATE FAIL' if failures else 'GATE PASS'} ({len(failures)} failure(s), inputs: {len(paths)})")
486
    return 1 if failures else 0
487
488
489
if __name__ == "__main__":
490
    raise SystemExit(main(sys.argv[1:]))