File size: 7,019 Bytes
e2d54c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Recover raw per-round tables from orx run logs.

This project runs in orx LOCAL mode, where `orx artifacts` does not exist and the run
log is the only evidence channel. The expensive stages therefore print every per-round
metric to stdout, and this script parses those lines back into CSV so the numbers can
be published as downloadable raw data next to the claims that rest on them.

Theory-stage artifacts are NOT recovered this way: those stages are deterministic and
run in seconds, so they are regenerated by rerunning them, which is a stronger
guarantee than parsing text.

Usage:
    python tools/extract_evidence.py <out_dir> <label>=<run_id> [...]
"""

from __future__ import annotations

import csv
import hashlib
import json
import re
import subprocess
import sys
from pathlib import Path

# Numbers may be negative ("H=-0.0000" for a fully collapsed round), so every
# numeric group allows a leading minus -- without it those rows are silently dropped,
# which loses exactly the collapsed rounds the comparison depends on.
# "    round  7                                       H(L)=1.9312  mean_len= 18.4  distinct= 31  nearA=0.14  nearB=0.09  loss=3.201  74s"
TEXT_ROUND = re.compile(
    r"round\s+(\d+)\s+H\(L\)=(-?[\d.]+)\s+mean_len=\s*(-?[\d.]+)\s+distinct=\s*(\d+)\s+"
    r"nearA=(-?[\d.nan]+)\s+nearB=(-?[\d.nan]+)\s+loss=(-?[\d.]+)\s+(\d+)s"
)
# "    round  3                                       H=1.9312  KL=0.0821  featVar=0.412  intraVar=0.377  classes=10  91s"
FLOW_ROUND = re.compile(
    r"round\s+(\d+)\s+H=(-?[\d.]+)\s+KL=(-?[\d.]+)\s+featVar=(-?[\d.]+)\s+"
    r"intraVar=(-?[\d.]+)\s+classes=(\d+)\s+(\d+)s"
)
KV = re.compile(r"^\s{4}(\S.*?)\s{2,}(.+?)\s*$")

EMIT_BEGIN = re.compile(
    r"^<<<ORX-ARTIFACT path=(\S+) bytes=(\d+) sha256=([0-9a-f]{64}) "
    r"encoding=(\S+) status=(\S+)"
)


def recover_artifacts(log: str, dest: Path) -> list[dict]:
    """Rebuild the artifact files a job wrote, from the framed blocks in its log.

    Each block declares its own length and SHA-256, so a truncated or interleaved
    log is rejected rather than silently yielding a corrupt file.
    """
    import base64

    recovered, lines, i = [], log.splitlines(), 0
    while i < len(lines):
        m = EMIT_BEGIN.match(lines[i])
        if not m:
            i += 1
            continue
        rel, size, digest, encoding, status = m.groups()
        i += 1
        payload = []
        while i < len(lines) and not lines[i].startswith(">>>ORX-ARTIFACT-END"):
            payload.append(lines[i].strip())
            i += 1
        i += 1
        rec = {"path": rel, "declared_bytes": int(size), "declared_sha256": digest,
               "status": status, "ok": False}
        if encoding == "base64" and status == "OK":
            try:
                raw = base64.b64decode("".join(payload), validate=True)
            except Exception as exc:  # noqa: BLE001 - report, never guess
                rec["error"] = f"base64 decode failed: {exc}"
            else:
                actual = hashlib.sha256(raw).hexdigest()
                rec["actual_bytes"], rec["actual_sha256"] = len(raw), actual
                if actual == digest and len(raw) == int(size):
                    out = dest / rel
                    out.parent.mkdir(parents=True, exist_ok=True)
                    out.write_bytes(raw)
                    rec["ok"] = True
                else:
                    rec["error"] = "hash/length mismatch — log is truncated or interleaved"
        else:
            rec["error"] = f"not recoverable (encoding={encoding} status={status})"
        recovered.append(rec)
    return recovered


def fetch(run_id: str) -> str:
    """Fetch the WHOLE log.

    `orx logs` tails by default, so a byte budget smaller than the log silently
    drops the beginning — which, with artifact frames spread through the run,
    means silently losing evidence. Ask for far more than any run produces.
    """
    return subprocess.run(
        ["orx", "logs", run_id, "--bytes", "100000000"],
        capture_output=True, text=True, check=True,
    ).stdout


def parse(log: str) -> dict:
    text_rows, flow_rows = [], []
    for m in TEXT_ROUND.finditer(log):
        text_rows.append({
            "round": int(m.group(1)), "H_L": float(m.group(2)),
            "mean_len": float(m.group(3)), "n_distinct_lengths": int(m.group(4)),
            "frac_near_T_A": float(m.group(5)), "frac_near_T_B": float(m.group(6)),
            "train_loss": float(m.group(7)), "seconds": int(m.group(8)),
        })
    for m in FLOW_ROUND.finditer(log):
        flow_rows.append({
            "round": int(m.group(1)), "class_entropy": float(m.group(2)),
            "kl_to_uniform": float(m.group(3)), "feature_variance": float(m.group(4)),
            "intra_class_variance": float(m.group(5)),
            "n_classes_present": int(m.group(6)), "seconds": int(m.group(7)),
        })
    meta = {}
    for line in log.splitlines():
        m = KV.match(line)
        if m and len(m.group(1)) < 60:
            meta.setdefault(m.group(1).strip(), m.group(2).strip())
    verdicts = re.findall(r"VERDICT (\S+): (VERIFIED|FALSIFIED|BLOCKED)", log)
    gates = re.findall(r"GATE: (PASS|FAIL)", log)
    return {
        "text_rounds": text_rows, "flow_rounds": flow_rows, "meta": meta,
        "verdicts": [{"claim": c, "status": s} for c, s in verdicts],
        "gates": gates,
        "all_gates_pass": bool(gates) and all(g == "PASS" for g in gates),
        "exit_ok": "OK: every recorded verdict is supported by its own evidence." in log,
    }


def main() -> int:
    if len(sys.argv) < 3:
        print(__doc__)
        return 2
    out = Path(sys.argv[1])
    out.mkdir(parents=True, exist_ok=True)
    index = {}
    for spec in sys.argv[2:]:
        label, run_id = spec.split("=", 1)
        log = fetch(run_id)
        (out / f"log_{label}.txt").write_text(log)
        d = parse(log)
        d["run_id"] = run_id
        d["artifacts"] = recover_artifacts(log, out / "artifacts" / label)
        bad = [a for a in d["artifacts"] if not a["ok"]]
        print(f"{label}: recovered {len(d['artifacts']) - len(bad)}/{len(d['artifacts'])} artifacts"
              + (f"  ({len(bad)} FAILED)" if bad else ""))
        for a in bad:
            print(f"  !! {a['path']}: {a.get('error')}")
        for key, rows in (("text_rounds", d["text_rounds"]), ("flow_rounds", d["flow_rounds"])):
            if rows:
                p = out / f"rounds_{label}.csv"
                with p.open("w", newline="") as fh:
                    w = csv.DictWriter(fh, fieldnames=list(rows[0]))
                    w.writeheader()
                    w.writerows(rows)
                print(f"{label}: {len(rows)} rounds -> {p}")
        index[label] = d
        (out / f"meta_{label}.json").write_text(json.dumps(d, indent=2))
    (out / "index.json").write_text(json.dumps(index, indent=2))
    print(f"wrote {out/'index.json'}")
    return 0


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