88e86b3faccd
Rebuild benchmarks as a clean single-root reposi
2 months ago
| 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
|
e9b767a51662
Preserve measured agent failures and validate ca
9 days ago
| 49 | from oakbench.public_output import output_evidence_failures, workflow_evidence_failures
|
88e86b3faccd
Rebuild benchmarks as a clean single-root reposi
2 months ago
| 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 |
|
e9b767a51662
Preserve measured agent failures and validate ca
9 days ago
| 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 |
|
88e86b3faccd
Rebuild benchmarks as a clean single-root reposi
2 months ago
| 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:]))
|