File size: 8,472 Bytes
4ae3b85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#!/usr/bin/env python3
"""Offline verification reconciler / trigger (§5.7).

Run by an admin whose HF token can write the central bucket, read the private
audit bucket, and launch org jobs (the same token the Space holds):

    HF_TOKEN=hf_... python scripts/verify_submissions.py reconcile [--dry-run]
    HF_TOKEN=hf_... python scripts/verify_submissions.py trigger [--dry-run] [--limit N]

``reconcile`` is the restart-safety net for the in-Space verifier: its watcher
threads are in-memory (DESIGN §1), so a Space restart mid-verification loses
the verdict even though the job finishes and writes ``summary.json``. This
mode scans ``verification_runs/*`` in the audit bucket for completed runs
whose index entry is still ``pending``/absent, then computes the verdict,
writes it through the SAME compare-and-set (human verdicts win), and posts the
same announcement — all via ``app.verifier``, one canonical implementation, so
online and offline behavior cannot drift. Idempotent: a run already recorded
(or human-decided) is skipped, so it is safe to re-run or schedule.

``trigger`` is the backfill for results that were posted before the trigger
existed (or whose launch failed): it walks ``agent-run`` results from the
highest claimed TPS down and fires the same ``maybe_trigger`` the POST hook
uses. Launches run SYNCHRONOUSLY (the script polls the job to terminal state,
~15–40 min each), and each recorded ``valid`` raises the champion bar for the
candidates after it — champion-search semantics. This mode SPENDS org credits;
use ``--dry-run`` first.
"""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from app.config import Settings  # noqa: E402
from app.hub import HubClient  # noqa: E402
from app.jobs import JobRunner  # noqa: E402
from app.naming import agent_from_filename  # noqa: E402
from app.read_model import ReadModel  # noqa: E402
from app.verification import PENDING, VerificationStatusStore  # noqa: E402
from app.verifier import Verifier, compute_verdict  # noqa: E402


def build_verifier(settings: Settings) -> tuple[Verifier, HubClient, ReadModel]:
    hub = HubClient(settings)
    read_model = ReadModel(hub, settings)
    verification = VerificationStatusStore(
        hub, runs_prefix=settings.verification_runs_prefix
    )
    runner = JobRunner(settings, hub)
    # Inline spawn: watchers run synchronously so the script doesn't exit
    # while a job is still being supervised.
    verifier = Verifier(
        settings, hub, read_model, verification, runner,
        spawn=lambda _name, fn: fn(),
    )
    return verifier, hub, read_model


def _run_dirs(hub: HubClient, settings: Settings) -> dict[str, set[str]]:
    """{result filename: {leaf files in its verification run dir}}."""
    prefix = settings.verification_runs_prefix
    out: dict[str, set[str]] = {}
    for e in hub.list_bucket_dir(settings.audit_bucket, prefix):
        rel = e.rel_path[len(prefix) + 1 :]
        if "/" not in rel:
            continue
        filename, leaf = rel.split("/", 1)
        out.setdefault(filename, set()).add(leaf)
    return out


def _reported_tps(
    hub: HubClient,
    read_model: ReadModel,
    settings: Settings,
    filename: str,
) -> float | None:
    """The TPS the result claimed: from verification_request.json (written at
    launch), falling back to the promoted result's frontmatter."""
    raw = hub.read_audit_bytes(
        f"{settings.verification_runs_prefix}/{filename}/verification_request.json"
    )
    if raw is not None:
        try:
            tps = json.loads(raw.decode("utf-8")).get("reported_tps")
            if isinstance(tps, (int, float)) and not isinstance(tps, bool) and tps > 0:
                return float(tps)
        except (json.JSONDecodeError, UnicodeDecodeError):
            pass
    rec = read_model.record("results", filename)
    if rec is not None:
        tps = rec.frontmatter.get("tps")
        if isinstance(tps, (int, float)) and not isinstance(tps, bool) and tps > 0:
            return float(tps)
    return None


def reconcile(settings: Settings, dry_run: bool) -> int:
    verifier, hub, read_model = build_verifier(settings)
    index = read_model.verification_index()
    runs = _run_dirs(hub, settings)
    print(f"run_dirs={len(runs)} indexed={len(index)}")

    healed = skipped = undecided = 0
    for filename, leaves in sorted(runs.items()):
        state = index.get(filename, PENDING)
        if state != PENDING:
            skipped += 1
            continue
        if "summary.json" not in leaves:
            print(f"  {filename}: no summary.json yet (job running/failed); skipping")
            skipped += 1
            continue
        raw = hub.read_audit_bytes(
            f"{settings.verification_runs_prefix}/{filename}/summary.json"
        )
        if raw is None:
            skipped += 1
            continue
        try:
            summary = json.loads(raw.decode("utf-8"))
        except (json.JSONDecodeError, UnicodeDecodeError) as exc:
            print(f"  {filename}: unparseable summary.json ({exc}); skipping")
            skipped += 1
            continue
        reported_tps = _reported_tps(hub, read_model, settings, filename)
        if reported_tps is None:
            print(f"  {filename}: no reported TPS on record; skipping")
            skipped += 1
            continue
        owner = agent_from_filename(filename) or ""
        if dry_run:
            verdict, details = compute_verdict(
                reported_tps, summary, tps_tol=settings.tps_tol, ppl_cap=settings.ppl_cap
            )
            print(f"  {filename}: would record {verdict} ({details})")
            healed += verdict is not None
            undecided += verdict is None
            continue
        verdict = verifier.record_verdict(
            filename,
            owner=owner,
            reported_tps=reported_tps,
            summary=summary,
            job_id=summary.get("job_id"),
        )
        print(f"  {filename}: {verdict or 'left pending / deferred'}")
        healed += verdict is not None
        undecided += verdict is None
    print(f"healed={healed} undecided={undecided} skipped={skipped}")
    return 0


def _claimed_tps(record) -> float:
    tps = record.frontmatter.get("tps")
    if isinstance(tps, bool) or not isinstance(tps, (int, float)):
        return 0.0
    return float(tps)


def trigger(settings: Settings, dry_run: bool, limit: int | None) -> int:
    verifier, _hub, read_model = build_verifier(settings)
    records = read_model.records("results")
    candidates = sorted(
        (r for r in records if not r.parse_error), key=_claimed_tps, reverse=True
    )
    launched = 0
    for r in candidates:
        if limit is not None and launched >= limit:
            break
        # Re-check per candidate: each recorded `valid` raises the bar (T*)
        # for everything after it — champion search from the top claim down.
        if not verifier.should_verify(r.filename, r.frontmatter):
            continue
        if dry_run:
            print(f"  would verify {r.filename} (tps={r.frontmatter.get('tps')})")
            launched += 1
            continue
        print(f"  verifying {r.filename} (tps={r.frontmatter.get('tps')}) ...")
        if verifier.maybe_trigger(r.filename, r.frontmatter):
            launched += 1
    print(f"{'would launch' if dry_run else 'launched'}={launched}")
    return 0


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    sub = ap.add_subparsers(dest="mode", required=True)
    p_rec = sub.add_parser("reconcile", help="record verdicts for completed runs")
    p_rec.add_argument("--dry-run", action="store_true", help="report only, write nothing")
    p_trg = sub.add_parser("trigger", help="launch verification for unverified SOTA claims")
    p_trg.add_argument("--dry-run", action="store_true", help="report only, launch nothing")
    p_trg.add_argument("--limit", type=int, default=None, help="max launches this run")
    args = ap.parse_args()

    # The offline tool is always 'enabled' — the env flag gates only the
    # in-Space trigger.
    settings = Settings(VERIFIER_ENABLED=True)
    if args.mode == "reconcile":
        return reconcile(settings, args.dry_run)
    return trigger(settings, args.dry_run, args.limit)


if __name__ == "__main__":
    raise SystemExit(main())