research-sdk: structure runs as falsifiable tests — hypothesis, notes, verdict

Add the scientific-method frame to the experiment handle: state a hypothesis
+ prediction up front, .note() the running log, and .conclude(verdict ∈
proven|refuted|inconclusive, because=...) to seal it. A refutation is
recorded as clearly and durably as a proof. finish() defaults a stated
hypothesis to inconclusive — a finished run never silently reads as proof.
All fields serialize into the record's meta alongside the auto-captured
provenance (git sha + lib versions), so a verdict is queryable evidence.
Backward-compatible: every new param is optional.
This commit is contained in:
2026-07-22 21:56:07 -07:00
parent a0599c1fee
commit 53cc3979d5
2 changed files with 64 additions and 10 deletions
@@ -37,9 +37,12 @@ def client():
return _default
def experiment(kind, subject, task, metric="accuracy", n_total=0, note=""):
"""Get/create an experiment handle on the default client."""
return client().experiment(kind, subject, task, metric=metric, n_total=n_total, note=note)
def experiment(kind, subject, task, metric="accuracy", n_total=0, note="",
hypothesis="", predict=""):
"""Get/create an experiment handle on the default client. Pass `hypothesis` + `predict`
to structure it as a falsifiable test that conclude(verdict) can prove or refute."""
return client().experiment(kind, subject, task, metric=metric, n_total=n_total, note=note,
hypothesis=hypothesis, predict=predict)
def query(project=None, kind=None, canonical=True):
+58 -7
View File
@@ -5,12 +5,22 @@ ZERO-CONFIG surface so hand-rolling is obviously the worse choice:
import hanzo_research as research
# A benchmark run — accumulate attempts, seal with the score.
exp = research.experiment("benchmark", "grok-4.5", "gpqa_diamond") # get/create → handle
exp.record("q1", "grok-4.5", {"answer": "A", "correct": True}) # an attempt, idempotent
exp.snapshot(png_bytes) # a board snapshot artifact
exp.report(markdown_text) # a generated report artifact
exp.finish(value=94.3) # seal the run (or use `with`)
# A kernel-perf run structured as a falsifiable test — state the claim, log what you
# see, then PROVE or REFUTE it. A refutation is a first-class, durable result.
k = research.experiment("kernel-perf", "matvec_q4k_f32_blk", "vulkan/6144x2048",
metric="ratio_vs_hand",
hypothesis="the DSL f32-direct matvec beats the hand kernel",
predict="DSL/hand >= 1.0 cold in-engine at the dominant FFN shape")
k.note("cold in-engine A/B, evo gfx1151, quiet window, 3 runs, bit-exact 2.3e-6")
k.conclude("proven", because="1.022x at 6144 rows (loses small shapes → gate >=4096)",
value=1.022) # git sha + lib versions auto-stamped
rows = research.query(project="enso-bench", kind="benchmark") # read canonical
Auto-instrumentation (the caller supplies NO provenance): on experiment()/finish() the
@@ -82,11 +92,18 @@ class Research:
return self._req("POST", "/v1/research/grants", g)
# ── ergonomic surface ───────────────────────────────────────────────────────
def experiment(self, kind, subject, task, metric="accuracy", n_total=0, note=""):
def experiment(self, kind, subject, task, metric="accuracy", n_total=0, note="",
hypothesis="", predict=""):
"""Get/create the experiment for (kind, subject, task) and return a handle. Auto-
captures provenance + the self-documenting narrative (the caller's docstring, the
commit messages since this experiment's last run, and an optional note)."""
return Experiment(self, kind, subject, task, metric=metric, n_total=n_total, note=note)
commit messages since this experiment's last run, and an optional note).
Structure it as a falsifiable test: `hypothesis` is the claim under test and
`predict` is the observation that would confirm it — stated up front so a later
conclude(verdict) can PROVE or REFUTE it unambiguously. A refutation is a
first-class result, recorded as clearly as a proof."""
return Experiment(self, kind, subject, task, metric=metric, n_total=n_total,
note=note, hypothesis=hypothesis, predict=predict)
def query(self, project=None, kind=None, canonical=True):
"""Read canonical experiments (the answered latest-run view). canonical=False is
@@ -122,13 +139,21 @@ class Experiment:
snapshot()/report() file diary artifacts; finish() seals the run with its final
number + the auto-captured narrative. Usable as a context manager (finish on exit)."""
def __init__(self, client, kind, subject, task, metric="accuracy", n_total=0, note=""):
def __init__(self, client, kind, subject, task, metric="accuracy", n_total=0, note="",
hypothesis="", predict=""):
self.c = client
self.kind, self.subject, self.task, self.metric = kind, subject, task, metric
self.id = f"{kind}:{subject}:{task}"
self.n_total = n_total
self._ok = 0
self._n = 0
# The falsifiable frame, stated up front so the verdict is unambiguous: the claim
# under test, and the observation that would confirm it.
self.hypothesis = hypothesis
self.predict = predict
self.verdict = "" # proven | refuted | inconclusive — set by conclude()
self.because = "" # the reasoning that earns the verdict
self._log = [] # the running notes/logs that travel with the run
# Zero-config, self-documenting provenance. THREE narrative sources, captured with
# nothing asked of the caller: (a) the calling code's docstring — what this
# experiment IS; (b) the commit messages since this experiment's last recorded run
@@ -143,13 +168,36 @@ class Experiment:
# Post the run as in-flight so the ops board sees it immediately.
self._post(status="running", value=0.0)
def note(self, text):
"""Append to the running log — the 'what I saw / thought' trail that travels with
the run into the record. Chainable."""
self._log.append(text)
return self
def conclude(self, verdict, because="", value=None):
"""Seal the experiment with its epistemic VERDICT ∈ {proven, refuted, inconclusive}
(distinct from execution status) and the reasoning that earns it, then finish the
run. A refutation is recorded as clearly and durably as a proof — the whole point of
an evidentiary layer. Returns the sealed record."""
if verdict not in ("proven", "refuted", "inconclusive"):
raise ValueError(f"verdict must be proven|refuted|inconclusive, got {verdict!r}")
self.verdict = verdict
self.because = because
return self.finish(value=value)
def _post(self, status, value):
e = {
"id": self.id, "kind": self.kind, "subject": self.subject, "task": self.task,
"metric": self.metric, "value": value, "n": self._n, "n_total": self.n_total,
"status": status, "git_sha": self.prov["git_sha"], "git_branch": self.prov["git_branch"],
"git_dirty": self.prov["git_dirty"], "lib_versions": self.libs,
"meta": {"doc": self.doc, "commits": self.commits, "note": self.note, "host": self.host},
# meta carries the scientific frame + the self-documenting narrative. hypothesis/
# predict/verdict/because make the run a falsifiable claim with a clear result;
# verdict is queryable evidence (e.g. every refuted lever) — promoting it to a
# first-class column is the follow-up, the shape is fixed here.
"meta": {"doc": self.doc, "commits": self.commits, "note": self.note, "host": self.host,
"hypothesis": self.hypothesis, "predict": self.predict,
"verdict": self.verdict, "because": self.because, "log": self._log},
}
return self.c.ingest(experiments=[e])
@@ -193,9 +241,12 @@ class Experiment:
def finish(self, value=None):
"""Seal the run: post the final number (computed from recorded attempts if not
given) + status complete. A new version supersedes the in-flight one; the prior
is retained."""
is retained. If a hypothesis was stated but no verdict declared, it defaults to
`inconclusive` — a finished run never silently reads as a proof."""
if value is None:
value = round(100.0 * self._ok / self._n, 2) if self._n else 0.0
if self.hypothesis and not self.verdict:
self.verdict = "inconclusive"
return self._post(status="complete", value=value)
def __enter__(self):