textmachine/eval/door_to_file/collect.py

310 lines
15 KiB
Python

#!/usr/bin/env python3
"""Trace collector for the cold run «door to file».
It reads the three places a run leaves a trace and prints them as facts an outsider can check:
the platform's Postgres, the engine's own disk (project DB + sidecars + the frame stream), and the
HTTP surface plus the downloaded bytes.
Two rules it follows everywhere, because both were paid for:
* every zero is printed next to a CONTROL number, so «nothing happened» and «nothing was read» are
told apart on the page rather than in the reader's head;
* the three traces are NOT independent — the platform's rows are materialised from the engine's
event stream and its text comes from the same `tmctl` verbs — so the report says so rather than
presenting agreement between them as corroboration.
Usage: collect.py --stand DIR --dsn DSN [--book ID] [--run ID] [--label NAME] [--json OUT]
"""
import argparse
import json
import os
import re
import sqlite3
import subprocess
import sys
from collections import Counter
MICRO = 1_000_000
def psql(dsn, sql):
"""One query, rows as lists of strings. Separator is a tab; psql -A -t gives raw fields."""
out = subprocess.run(
[os.path.expanduser("~/.local/pgsql/bin/psql"), dsn, "-At", "-F", "\t", "-c", sql],
capture_output=True, text=True)
if out.returncode != 0:
raise RuntimeError(f"psql failed: {out.stderr.strip()}\nSQL: {sql}")
return [line.split("\t") for line in out.stdout.strip().splitlines() if line != ""]
def one(dsn, sql, default="0"):
rows = psql(dsn, sql)
return rows[0][0] if rows and rows[0] and rows[0][0] != "" else default
def section(title):
print()
print("=" * 78)
print(title)
print("=" * 78)
def stamp_of(binary):
"""The VCS stamp of a Go binary, as three answers: revision, modified, and how many vcs.* lines
were there at all. The third is the one that matters: a binary built in a linked worktree
carries NONE, and a gate that only refuses `vcs.modified=true` passes it every time."""
out = subprocess.run(["go", "version", "-m", binary], capture_output=True, text=True).stdout
lines = [l.strip() for l in out.splitlines()]
vcs = [l for l in lines if "\tvcs." in l or l.startswith("build\tvcs.")]
rev = mod = ""
for l in vcs:
if "vcs.revision=" in l:
rev = l.split("vcs.revision=")[1].strip()
if "vcs.modified=" in l:
mod = l.split("vcs.modified=")[1].strip()
return {"binary": binary, "revision": rev, "modified": mod,
"vcs_lines": len(vcs), "build_lines": len(lines)}
def freeze_gate(stand, sha):
section("A. FREEZE GATE — every binary, three conditions, not one")
ok = True
for name in ("tmctl", "tmplatformd", "tmplatformctl"):
path = os.path.join(stand, "bin", name)
if not os.path.exists(path):
print(f" {name}: ABSENT at {path}")
ok = False
continue
s = stamp_of(path)
verdict = []
if s["vcs_lines"] == 0:
verdict.append("NO vcs.* LINES AT ALL — the gate would be vacuous")
if s["revision"] != sha:
verdict.append(f"revision is not the frozen one ({sha[:12]})")
if s["modified"] != "false":
verdict.append(f"vcs.modified={s['modified']!r}")
print(f" {name}: revision={s['revision'][:12] or '(none)'} modified={s['modified'] or '(none)'} "
f"vcs_lines={s['vcs_lines']} (control: build lines {s['build_lines']}) "
f"{'ok' if not verdict else '' + '; '.join(verdict)}")
ok = ok and not verdict
print(f" VERDICT: {'all three binaries are from the freeze' if ok else '⛔ NOT ALL BINARIES ARE FROM THE FREEZE'}")
return ok
def platform_trace(dsn, book, run):
section("B. PLATFORM (Postgres) — the rows, each zero beside its control")
tables = ["users", "books", "chapters", "units", "runs", "run_attempts", "reservations",
"credit_ledger", "book_events", "exports", "bank_terms"]
for t in tables:
n = one(dsn, f"select count(*) from {t}")
print(f" {t}: {n}")
if book:
print(f"\n -- this book ({book}) --")
print(" books row:", psql(dsn, f"""
select status, chapter_count, source_chars, expected_micro_usd, book_once_micro_usd,
step_max_micro_usd, ordered_at is not null
from books where id = '{book}'"""))
units = psql(dsn, f"""
select u.state, count(*) from units u
join chapters c on c.id = u.chapter_id
where c.book_id = '{book}' group by 1 order by 1""")
total = one(dsn, f"select count(*) from units u join chapters c on c.id=u.chapter_id where c.book_id='{book}'")
print(f" units by state: {units} (control: units of this book = {total})")
ev = psql(dsn, f"select event, count(*) from book_events where book_id='{book}' group by 1 order by 2 desc")
evtotal = one(dsn, f"select count(*) from book_events where book_id='{book}'")
print(f" book_events by event: {ev} (control: events of this book = {evtotal})")
bank_all = one(dsn, f"select count(*) from bank_terms where book_id='{book}'")
bank_ok = one(dsn, f"select count(*) from bank_terms where book_id='{book}' and status='approved'")
allbooks = one(dsn, "select count(*) from bank_terms")
print(f" bank_terms: {bank_all} rows, {bank_ok} approved (control: bank_terms of ALL books = {allbooks})")
ex = psql(dsn, f"""select id, format, state, complete, size_bytes, path
from exports where book_id='{book}' order by requested_at""")
print(f" exports: {ex or 'none'} (control: exports of ALL books = {one(dsn, 'select count(*) from exports')})")
if run:
print(f"\n -- this run ({run}) --")
print(" runs row:", psql(dsn, f"""
select status, verify_bank, bond_funded, coalesce(paused_reason,''),
coalesce(failure_reason,''), ceiling_chapters, finished_at is not null
from runs where id='{run}'"""))
att = psql(dsn, f"""
select attempt_no, exit_code, exit_result, last_seq, spend_micro_usd,
ceiling_arg_micro_usd, engine_binary
from run_attempts where run_id='{run}' order by attempt_no""")
print(f" run_attempts ({len(att)}): ")
for a in att:
print(" ", a)
print(" ⚠ attempts, not runs: a resume after a bank stop is a SECOND attempt under the same run id")
def money(dsn, book, run, projectdb):
section("C. MONEY — micro-USD, as a CHECK between paths and never as an identity")
led = psql(dsn, "select kind, count(*), coalesce(sum(amount_micro_usd),0) from credit_ledger group by 1 order by 1")
print(f" credit_ledger by kind: {led}")
grants = int(one(dsn, "select coalesce(sum(amount_micro_usd),0) from credit_ledger where kind='grant'"))
settle = int(one(dsn, "select coalesce(sum(amount_micro_usd),0) from credit_ledger where kind='settlement'"))
balance = int(one(dsn, "select coalesce(sum(amount_micro_usd),0) from credit_ledger"))
openres = one(dsn, "select count(*) from reservations where state='open'")
allres = one(dsn, "select count(*) from reservations")
print(f" grants: {grants:>12} µUSD (${grants/MICRO:.6f})")
print(f" settlements: {settle:>12} µUSD (${settle/MICRO:.6f})")
print(f" balance: {balance:>12} µUSD (${balance/MICRO:.6f}) = sum of ALL ledger rows")
print(f" open reservations: {openres} (control: reservations of every state = {allres})")
spend_att = one(dsn, f"select coalesce(sum(spend_micro_usd),0) from run_attempts where run_id='{run}'") if run else "0"
print(f" Σ run_attempts.spend_micro_usd for this run: {spend_att}")
if projectdb and os.path.exists(projectdb):
rows, total, hits, models, bad = request_log(projectdb)
print(f" engine request_log: {rows} rows, Σ cost_usd = ${total:.6f}{round(total*MICRO)} µUSD")
print(f" tm_hit=1 (answer taken from a checkpoint, bought nothing): {hits} (control: rows = {rows})")
print(f" distinct model_actual: {models}")
print(f" rows with err or finish_reason != stop: {len(bad)} (control: rows = {rows})")
for b in bad:
print(" ", b)
print(f" ⚠ request_log is TELEMETRY, not the source of money (the engine's money lives in spend/checkpoints);")
print(f" a redrive deletes checkpoints, so a difference here is a named class, not an error to hide.")
sp = engine_spend(projectdb)
print(f" engine spend table: {sp}")
else:
print(f" engine request_log: NOT READ — no project db at {projectdb}")
def request_log(db):
c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
rows = c.execute("select count(*) from request_log").fetchone()[0]
total = c.execute("select coalesce(sum(cost_usd),0) from request_log").fetchone()[0]
hits = c.execute("select count(*) from request_log where tm_hit=1").fetchone()[0]
models = [r[0] for r in c.execute("select distinct model_actual from request_log order by 1")]
bad = c.execute("""select id, stage, role, model_actual, finish_reason, err
from request_log
where coalesce(err,'') != '' or coalesce(finish_reason,'') not in ('stop','')""").fetchall()
return rows, total, hits, models, bad
def engine_spend(db):
c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
return c.execute("select book_id, date, committed_usd, reserved_usd from spend").fetchall()
def engine_disk(bookdir):
section("D. ENGINE DISK — frames, sidecars, checkpoints")
if not os.path.isdir(bookdir):
print(f" no book directory at {bookdir}")
return
files = sorted(os.listdir(bookdir))
print(f" book directory holds {len(files)} entries: {files}")
ev = os.path.join(bookdir, "events.jsonl")
if os.path.exists(ev):
kinds = Counter()
n = 0
with open(ev, encoding="utf-8") as f:
for line in f:
n += 1
try:
kinds[json.loads(line).get("type", "?")] += 1
except json.JSONDecodeError:
kinds["<unparsable>"] += 1
print(f" events.jsonl: {n} frames, by type: {dict(kinds)}")
else:
print(f" events.jsonl: ABSENT (control: {len(files)} files were listed in this directory)")
for name in sorted(f for f in files if ".bank" in f or "signature" in f or "mined" in f):
p = os.path.join(bookdir, name)
print(f" sidecar {name}: {os.path.getsize(p)} bytes sha256={sha256(p)}")
db = os.path.join(bookdir, "project.db")
if os.path.exists(db):
c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
for t in ("checkpoints", "chunk_status", "glossary", "bank_stop_presented", "snapshots"):
try:
print(f" {t}: {c.execute(f'select count(*) from {t}').fetchone()[0]} rows")
except sqlite3.Error as e:
print(f" {t}: unreadable ({e})")
def sha256(path):
import hashlib
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def build_report(stand, bookdir, out):
section("E. BUILD REPORT — the seven counters the API does NOT carry")
cfg = os.path.join(bookdir, "book.yaml")
if not os.path.exists(cfg):
print(f" no book.yaml at {cfg}")
return
# --out names the FILE, not a directory, and an explicit path is never replaced — so each call
# writes its own copy rather than asking the engine to overwrite one.
os.makedirs(os.path.dirname(out), exist_ok=True)
# The verb first, its flags after, and no --json: the report is JSON regardless (the flag is
# refused by name). Order and flags are the platform's own BuildArgs (internal/runner/build.go).
cmd = [os.path.join(stand, "bin", "tmctl"), "build", "--config", cfg,
"--format", "txt", "--out", out, "--partial"]
res = subprocess.run(cmd, capture_output=True, text=True)
print(f" $ {' '.join(cmd)}")
print(f" exit {res.returncode}")
# The report is the WHOLE of stdout, pretty-printed over many lines: a per-line scan finds
# nothing and reports "no document", which looks exactly like a build that said nothing.
doc = None
try:
doc = json.loads(res.stdout)
except json.JSONDecodeError:
start = res.stdout.find("{")
if start >= 0:
try:
doc = json.loads(res.stdout[start:])
except json.JSONDecodeError:
doc = None
if doc is None:
print(" stdout carried no report document:")
print(" ", res.stdout.strip()[:600])
print(" stderr:", res.stderr.strip()[:600])
return
keys = ["build_version", "total_units", "pending_units", "withheld_units", "incomplete_units",
"stale_units", "stale_unknown", "ghost_rows", "config_drift", "complete"]
for k in keys:
print(f" {k}: {doc.get(k)}")
print(f" removed_files: {doc.get('removed_files')} stale_copies: {doc.get('stale_copies')}")
clean = (doc.get("complete") is True and doc.get("stale_unknown") is False
and all(doc.get(k, 0) == 0 for k in
("pending_units", "withheld_units", "incomplete_units", "stale_units", "ghost_rows")))
print(f" VERDICT: {'a WHOLE book — all five counters zero, stale_unknown false, complete true' if clean else '⛔ NOT whole by this report'}")
print(" ⚠ stale_units == 0 proves nothing on its own: under config drift it is mechanically zero,")
print(" which is why stale_unknown is printed beside it rather than folded into the same count.")
return doc
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--stand", required=True)
ap.add_argument("--dsn", required=True)
ap.add_argument("--book", default="")
ap.add_argument("--run", default="")
ap.add_argument("--sha", default=os.environ.get("FREEZE_SHA", ""))
ap.add_argument("--label", default="collect")
ap.add_argument("--skip-build", action="store_true")
a = ap.parse_args()
print(f"COLD RUN «DOOR TO FILE» — trace bundle «{a.label}»")
print(f"stand={a.stand} book={a.book or '(none)'} run={a.run or '(none)'}")
if a.sha:
freeze_gate(a.stand, a.sha)
bookdir = os.path.join(a.stand, "stand", "books", a.book) if a.book else ""
platform_trace(a.dsn, a.book, a.run)
money(a.dsn, a.book, a.run, os.path.join(bookdir, "project.db") if bookdir else "")
if bookdir:
engine_disk(bookdir)
if not a.skip_build:
import time as _t
build_report(a.stand, bookdir, os.path.join(
a.stand, "evidence", "builds", f"{a.label}-{_t.strftime('%Y%m%dT%H%M%S')}.txt"))
section("F. WHAT THIS BUNDLE DOES NOT PROVE")
print(" The platform's rows are MATERIALISED from the engine's event stream, and its text, its")
print(" manifest and its file come from the same `tmctl` verbs. So B, D and E agreeing is ONE")
print(" database agreeing with itself, not three witnesses. Independent of them are only the")
print(" bytes downloaded over HTTP and their sha256. There is no provider-side billing here.")
if __name__ == "__main__":
main()