Viewing files at commit 3354584a5d91Commit detailsView latest

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