Log in
scripts/publish_gate.py 489 lines · 19.6 KB · python Blame
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
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
    if workflow_evidence:
247
        failures.append("FAIL   workflow-evidence: " + "; ".join(workflow_evidence[:5]))
248
249
    unsupported_output = [
250
        (str(path), row.get("subject"), row.get("scenario"), row.get("operation"), reasons)
251
        for path, row in public_core_rows
252
        for reasons in [output_evidence_failures(row)]
253
        if reasons
254
    ]
255
    if unsupported_output:
256
        failures.append(
257
            "FAIL   output-evidence: public output usefulness claims lack exact evidence: "
258
            f"{unsupported_output[:5]}"
259
        )
260
261
    weak_content_trust = [
262
        (
263
            str(path),
264
            row.get("subject"),
265
            row.get("scenario"),
266
            row.get("operation"),
267
            row.get("content_integrity_source"),
268
            row.get("content_integrity_sources"),
269
        )
270
        for path, row in public_core_rows
271
        if row.get("content_integrity_check_passed") is True
272
        and not content_integrity_public_trust_passed(row)
273
    ]
274
    if weak_content_trust:
275
        failures.append(
276
            "FAIL   content-trust: public core rows claim passing content integrity "
277
            "from weak or mixed sources: "
278
            f"{weak_content_trust[:5]}"
279
        )
280
    elif any(row.get("content_integrity_check_passed") is True for _, row in public_core_rows):
281
        notes.append("PASS   content-trust: passing public content checks use strong sources")
282
283
    env_versions = {
284
        str(row.get("env_isolation_version"))
285
        for _, row in public_core_rows
286
        if isinstance(row.get("env_isolation_version"), str)
287
        and str(row.get("env_isolation_version")).strip()
288
    }
289
    missing_env_version = [
290
        (
291
            str(path),
292
            row.get("subject"),
293
            row.get("scenario"),
294
            row.get("operation"),
295
            row.get("run", row.get("run_index")),
296
        )
297
        for path, row in public_core_rows
298
        if not isinstance(row.get("env_isolation_version"), str)
299
        or not str(row.get("env_isolation_version")).strip()
300
    ]
301
    if missing_env_version:
302
        failures.append(
303
            "FAIL   env-isolation: public core rows missing env_isolation_version: "
304
            f"{missing_env_version[:5]}"
305
        )
306
    elif len(env_versions) > 1:
307
        failures.append(
308
            "FAIL   env-isolation: public core rows mix env_isolation_version values: "
309
            f"{sorted(env_versions)}"
310
        )
311
    elif env_versions:
312
        notes.append(f"PASS   env-isolation: public core rows use version {next(iter(env_versions))}")
313
314
    measured_groups: dict[tuple[str, str, str, str, str, str], set[str]] = {}
315
    sample_groups: dict[tuple[str, str, str, str, str, str], set[str]] = {}
316
    for index, (path, row) in enumerate(public_core_rows):
317
        operation = str(row.get("operation") or "")
318
        if is_excluded_sample_operation(operation):
319
            continue
320
        key = (
321
            str(path),
322
            str(row.get("profile")),
323
            str(row.get("benchmark_track", "")),
324
            str(row.get("scenario", "")),
325
            operation,
326
            str(row.get("subject", "")),
327
        )
328
        run_id = sample_run_id(row, index)
329
        measured_groups.setdefault(key, set()).add(run_id)
330
        if row_returncode(row) == 0:
331
            sample_groups.setdefault(key, set()).add(run_id)
332
    thin_groups = {
333
        "/".join(key[1:]): len(runs)
334
        for key, runs in measured_groups.items()
335
        if len(runs) < PUBLIC_CORE_MIN_SAMPLES
336
    }
337
    zero_success_groups = {
338
        "/".join(key[1:]): len(runs)
339
        for key, runs in measured_groups.items()
340
        if not sample_groups.get(key)
341
    }
342
    if thin_groups:
343
        failures.append(
344
            f"FAIL   samples: public core groups below n={PUBLIC_CORE_MIN_SAMPLES}: "
345
            f"{dict(list(thin_groups.items())[:5])}"
346
        )
347
    if zero_success_groups:
348
        failures.append(
349
            "FAIL   samples: public core groups with zero successful rows: "
350
            f"{dict(list(zero_success_groups.items())[:5])}"
351
        )
352
    if public_core_rows and not thin_groups and not zero_success_groups:
353
        notes.append(f"PASS   samples: public core groups have n>={PUBLIC_CORE_MIN_SAMPLES}")
354
355
    missing_randomization = [
356
        (
357
            str(path),
358
            row.get("subject"),
359
            row.get("scenario"),
360
            row.get("operation"),
361
            row.get("run", row.get("run_index")),
362
        )
363
        for path, row in public_core_rows
364
        if randomization_value(row) is None
365
    ]
366
    if missing_randomization:
367
        failures.append(
368
            "FAIL   randomization: public core rows missing randomization metadata: "
369
            f"{missing_randomization[:5]}"
370
        )
371
    else:
372
        not_randomized = [
373
            (
374
                str(path),
375
                row.get("subject"),
376
                row.get("scenario"),
377
                row.get("operation"),
378
                row.get("run", row.get("run_index")),
379
            )
380
            for path, row in public_core_rows
381
            if randomization_value(row) is not True
382
        ]
383
        randomized_without_order = []
384
        if not_randomized:
385
            failures.append(
386
                "FAIL   randomization: public core rows were not randomized: "
387
                f"{not_randomized[:5]}"
388
            )
389
        else:
390
            for path, row in public_core_rows:
391
                order = subject_order(row)
392
                if not isinstance(order, list) or not order or row.get("subject") not in order:
393
                    randomized_without_order.append(
394
                        (
395
                            str(path),
396
                            row.get("subject"),
397
                            row.get("scenario"),
398
                            row.get("operation"),
399
                            row.get("run", row.get("run_index")),
400
                        )
401
                    )
402
        if randomized_without_order:
403
            failures.append(
404
                "FAIL   randomization: randomized public core rows lack per-run subject order: "
405
                f"{randomized_without_order[:5]}"
406
            )
407
        elif public_core_rows:
408
            notes.append("PASS   randomization: public core rows state run-order metadata")
409
410
    tracks_by_file: dict[Path, set[str]] = {}
411
    for path, row in all_rows:
412
        track = row.get("benchmark_track")
413
        if track:
414
            tracks_by_file.setdefault(path, set()).add(str(track))
415
    mixed_tracks = {str(path): sorted(tracks) for path, tracks in tracks_by_file.items() if len(tracks) > 1}
416
    if mixed_tracks:
417
        failures.append(f"FAIL   tracks: files mix benchmark_track values: {mixed_tracks}")
418
    elif tracks_by_file:
419
        notes.append("PASS   tracks: each result file holds a single benchmark_track")
420
421
    # Transport hygiene: remote-touching rows must state their transport, and
422
    # one subject's operation must not mix transports. Side-by-side tables may
423
    # contain the same operation name for network and local_file subjects.
424
    missing_transport = [
425
        (str(path), row.get("subject"), row.get("operation"))
426
        for path, row in all_rows
427
        if (row.get("remote_repo") or row.get("remote_server"))
428
        and not (row.get("remote_transport") or row.get("workspace_transport"))
429
    ]
430
    if missing_transport:
431
        failures.append(f"FAIL   transports: remote rows without transport: {missing_transport[:5]}")
432
433
    op_transports: dict[tuple[str, str], set[str]] = {}
434
    for _, row in all_rows:
435
        transport = row.get("remote_transport") or row.get("workspace_transport")
436
        if transport:
437
            key = (str(row.get("subject")), str(row.get("operation")))
438
            op_transports.setdefault(key, set()).add(str(transport))
439
    mixed = {f"{subject} {op}": sorted(ts) for (subject, op), ts in op_transports.items() if len(ts) > 1}
440
    if mixed:
441
        failures.append(f"FAIL   transports: operations span multiple transports: {mixed}")
442
    elif op_transports:
443
        notes.append(f"PASS   transports: {len(op_transports)} remote-touching operations, one transport each")
444
445
    levels_by_file: dict[Path, set[str]] = {}
446
    for path, row in all_rows:
447
        level = row.get("instruction_level")
448
        if level is None and isinstance(row.get("agent"), dict):
449
            level = row["agent"].get("instruction_level")
450
        if level:
451
            levels_by_file.setdefault(path, set()).add(str(level))
452
    multi = {str(p): sorted(ls) for p, ls in levels_by_file.items() if len(ls) > 1}
453
    if multi:
454
        failures.append(f"FAIL   levels: files mix instruction levels (never aggregate): {multi}")
455
    elif levels_by_file:
456
        notes.append("PASS   levels: each agent result file holds a single instruction level")
457
458
    bad_skips = [
459
        (str(path), row.get("operation"))
460
        for path, row in all_rows
461
        if row_returncode(row) == 77 and not row.get("skip_reason")
462
    ]
463
    skip_count = sum(1 for _, row in all_rows if row_returncode(row) == 77)
464
    if bad_skips:
465
        failures.append(f"FAIL   skips: returncode-77 rows without skip_reason: {bad_skips[:5]}")
466
    else:
467
        notes.append(f"PASS   skips: {skip_count} skip rows, all carrying skip_reason (account for each in prose)")
468
469
    for path, row in all_rows:
470
        tail = (row.get("parallel_metrics") or {}).get("commit_latency_tail")
471
        if isinstance(tail, dict) and "samples" not in tail and "n" not in tail and "sample_count" not in tail:
472
            failures.append(f"FAIL   tails: {path.name} {row.get('operation')} tail summary lacks sample count")
473
            break
474
    else:
475
        notes.append("PASS   tails: tail summaries carry sample counts (or none present)")
476
477
    notes.append("MANUAL nulls: confirm summaries render null as 'unmeasured', never 0")
478
    notes.append("MANUAL prose: state transport asymmetries (oak network vs git local) next to every side-by-side")
479
480
    for line in notes:
481
        print(line)
482
    for line in failures:
483
        print(line)
484
    print(f"\n{'GATE FAIL' if failures else 'GATE PASS'} ({len(failures)} failure(s), inputs: {len(paths)})")
485
    return 1 if failures else 0
486
487
488
if __name__ == "__main__":
489
    raise SystemExit(main(sys.argv[1:]))