Viewing files at commit f86d1109f5dfCommit detailsView latest

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