fan-out: witness audit + 5F extractor + function-sampled demo + docs

Three small streams in one commit:

#000028 follow-up — witness divergence → 5F fixtures
=====================================================

Witness fan-out now writes a `providence_canonical_witness` audit
event when it fires (next to the capital-ledger record landed in
708aa45). Body carries pi_star_ref, question_text, agreement_label,
canonical_answer_text, llm_raw_text, llm_canonical_bytes,
cache_status. Best-effort write — chain failure never fails the
query.

New extractor `bench/scripts/witness_to_5f.py` reads those events
from a qa.db and writes them out as 5F-Falsification fixtures
matching the existing `falsification-live-v1` schema. Filtering
includes only divergence labels (LLM-DIVERGED / KERNEL-LLM-DIVERGED
/ CACHE-DRIFT); skips KERNEL-LLM-AGREE / STRICT-WITNESSED (no
calibration signal) and KERNEL-ONLY (LLM unparseable, not a
supervised-correction sample).

Idempotent: sorted by audit-event seq, so re-running against the
same qa.db produces byte-equal fixture files. The existing
fixture-digest discipline stays valid.

Makefile: `make bench-witness-divergence` (override default
qa.db / output path via WITNESS_QA_DB / WITNESS_OUT env-vars).

Closes the divergence → calibration data loop the witness ticket
imagined: every LLM hallucination on a canonical-shape question
becomes a supervised-correction fixture downstream prompt
improvements can grade against.

#000030 Phase 7 demo — function-sampled@v1 end-to-end
======================================================

`bench/scripts/demo_plot.py` — closes the loop on opencompletion's
activity24-math-plot.yaml. SymPy expression → quantized
integer-vector signature (canonical bytes) → optional matplotlib
PNG. Canonical bytes are the proof; PNG is just a downstream view
of the same evidence.

  $ make demo-plot Q='sin(x)' PNG=/tmp/sin.png

Output JSON contains canonical_bytes_sha256 + canonical_bytes_preview
+ canonical_bytes_total_chars + grid metadata + the optional png_path.
matplotlib is gated — when absent, --png prints a warning to stderr
and skips the render; the canonical bytes still print. Tests skip
the PNG-presence assertion via `pytest.importorskip("matplotlib")`.

Public docs polish (#7)
========================

- docs/_source/bench.rst: updated fixture-count narrative (~660 →
  662 default tasks + ~110 math π* fixtures); `make` quick-reference
  now lists all per-π* 5S targets (tabular, calculus-limit/series,
  linear-algebra, function-sampled) plus bench-real-shard,
  bench-fork-baseline/score, bench-witness-divergence.
- docs/_source/v8-fork-score.rst: CLI section gained --out flag
  documentation + a Make-harness sub-section covering
  bench-fork-baseline / bench-fork-score / FORK_PARENT/CHILD/REPORT
  env-vars.

Tests
=====

- tests/test_witness_to_5f.py — 8 new tests covering the audit-event
  write (3) + extractor logic (5).
- tests/test_demo_plot.py — 6 new tests covering canonical-bytes
  determinism + equivalence-class collapse + matplotlib gating.
Full suite: 1624 passed, 37 skipped (was 1568; +56).
This commit is contained in:
russell@unturf.com 2026-05-09 13:19:30 -04:00
parent abe5988bef
commit 70ffc01ce4
No known key found for this signature in database
8 changed files with 779 additions and 14 deletions

160
bench/scripts/demo_plot.py Normal file
View file

@ -0,0 +1,160 @@
"""End-to-end demo for ``function-sampled@v1`` (#000030 Phase 7).
Closes the loop on opencompletion's ``activity24-math-plot.yaml``:
SymPy expression quantized integer-vector signature (canonical
bytes) optional matplotlib PNG (a downstream view of the
canonical evidence).
The canonical bytes are the proof: two function-sampled outputs
that match byte-for-byte represent the same function on the same
quantization grid. The PNG is just a human-readable rendering of
those same bytes different DPIs / colormaps / axis spans don't
change identity.
Usage::
python -m bench.scripts.demo_plot \\
--expr 'sin(x)' --x-min 0 --x-max 6.283 --n-samples 200 --dv 0.01 \\
--png /tmp/sin.png
# Or via Make (PNG optional):
make demo-plot Q='sin(x)'
make demo-plot Q='x**2 + 2*x + 1' PNG=/tmp/parabola.png
The matplotlib import is gated the canonical bytes always print
to stdout; the PNG is written only when ``--png`` is provided AND
matplotlib is importable.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from pathlib import Path
def _build_payload(
expr: str, x: str, x_min: float, x_max: float,
n_samples: int, dv: float,
) -> bytes:
return json.dumps({
"f": expr, "x": x,
"x_min": x_min, "x_max": x_max,
"n_samples": n_samples, "dv": dv,
}).encode("utf-8")
def _maybe_render_png(
*, expr: str, x_var: str, x_min: float, x_max: float,
n_samples: int, png_path: Path,
) -> None:
"""Render a PNG via matplotlib for human-readable inspection.
No-op if matplotlib isn't installed."""
try:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import sympy as sp
except ImportError:
print(
"warning: matplotlib + sympy required for PNG render; "
"skipping --png",
file=sys.stderr,
)
return
x_sym = sp.Symbol(x_var)
f_expr = sp.sympify(expr, locals={x_var: x_sym})
f = sp.lambdify(x_sym, f_expr, "math")
step = (x_max - x_min) / (n_samples - 1)
xs = [x_min + i * step for i in range(n_samples)]
ys = []
for xi in xs:
try:
yi = f(xi)
if isinstance(yi, complex):
yi = yi.real
ys.append(float(yi))
except (ValueError, ZeroDivisionError, OverflowError):
ys.append(float("nan"))
plt.figure(figsize=(8, 5))
plt.plot(xs, ys, linewidth=2, color="#3366cc")
plt.title(f"y = {expr}")
plt.xlabel(x_var)
plt.ylabel("y")
plt.grid(True, alpha=0.3)
png_path.parent.mkdir(parents=True, exist_ok=True)
plt.tight_layout()
plt.savefig(png_path, dpi=120)
plt.close()
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
p.add_argument("--expr", required=True, help="SymPy expression in the variable --x")
p.add_argument("--x", default="x", help="independent-variable name (default 'x')")
p.add_argument("--x-min", dest="x_min", type=float, default=-3.14159)
p.add_argument("--x-max", dest="x_max", type=float, default=3.14159)
p.add_argument("--n-samples", dest="n_samples", type=int, default=200)
p.add_argument("--dv", type=float, default=0.01,
help="value-quantization step (smaller = tighter)")
p.add_argument("--png", type=Path, default=None,
help="optional PNG output path (requires matplotlib)")
args = p.parse_args(argv)
from arborist.pi_star import PiStarError, get
try:
ps = get("function-sampled@v1")
except KeyError:
print(
"error: function-sampled@v1 not registered (sympy missing?); "
"install with `pip install arborist[math]`",
file=sys.stderr,
)
return 2
payload = _build_payload(
args.expr, args.x, args.x_min, args.x_max,
args.n_samples, args.dv,
)
try:
canonical_bytes = ps.canonicalize(payload)
except PiStarError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
digest = hashlib.sha256(canonical_bytes).hexdigest()
out = {
"expr": args.expr,
"x": args.x,
"grid": {
"x_min": args.x_min,
"x_max": args.x_max,
"n_samples": args.n_samples,
"dv": args.dv,
},
"canonical_bytes_sha256": digest,
"canonical_bytes_preview": canonical_bytes.decode("utf-8")[:200] + (
"" if len(canonical_bytes) > 200 else ""
),
"canonical_bytes_total_chars": len(canonical_bytes),
"pi_star_ref": "function-sampled@v1",
}
if args.png is not None:
_maybe_render_png(
expr=args.expr, x_var=args.x,
x_min=args.x_min, x_max=args.x_max,
n_samples=args.n_samples,
png_path=args.png,
)
out["png_path"] = str(args.png)
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,192 @@
"""Witness-divergence → 5F Falsification fixture extractor.
Pulls ``providence_canonical_witness`` audit events from a qa.db
and writes them out as 5F Falsification fixtures (matching the
existing ``falsification-live-v1`` schema). Each diverging witness
event becomes a supervised-correction sample the LLM can be
calibrated against the kernel's canonical bytes are ground
truth, so an LLM raw answer that doesn't fold to those bytes is
falsifiable evidence of an arithmetic / logic / symbolic
hallucination.
#000028 follow-up — closes the divergence → calibration data loop.
The witness path landed in commit `656b573`; the audit-event write
landed in this same commit. The extractor here is the missing
piece that converts audit-chain entries into 5F-runner-compatible
fixtures.
Usage::
python -m bench.scripts.witness_to_5f \\
--qa-db ~/.arborist/shards/qa.db \\
--out bench/fixtures/5f/falsification-witness-v1.jsonl
Or via Makefile::
make bench-witness-divergence
Filtering rules
---------------
Includes events whose ``agreement_label`` indicates LLM divergence:
- ``LLM-DIVERGED`` (kernel + cache agree; LLM differs)
- ``KERNEL-LLM-DIVERGED`` (no cache; LLM differs from kernel)
- ``CACHE-DRIFT`` (kernel + LLM agree; cache differs flagged
as a calibration-data point too since the cache row is wrong)
Skips:
- ``STRICT-WITNESSED`` / ``KERNEL-LLM-AGREE`` /
``KERNEL-CACHE-AGREE`` / ``KERNEL-ONLY`` agreement, no signal.
- Events without ``llm_raw_text`` LLM was unparseable; not a
useful supervised-correction sample.
Idempotency
-----------
Output is sorted by audit-event seq for deterministic byte-equality
across runs against the same qa.db. Re-running with no new
divergence events writes the same bytes. The fixture-digest
discipline (#000021 §3) stays valid.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from arborist.store import connect
_DIVERGENCE_LABELS = {
"LLM-DIVERGED",
"KERNEL-LLM-DIVERGED",
"CACHE-DRIFT",
}
def extract_divergence_fixtures(
qa_db: Path,
*,
fixture_id_prefix: str = "5f-fal-witness",
verifier_method_root: str = "verify_quotes-v1",
) -> list[dict]:
"""Pull divergence events from `qa_db`'s audit chain. Returns a
list of 5F-Falsification fixture dicts (sorted by audit-event
seq for determinism)."""
conn = connect(qa_db)
try:
rows = conn.execute(
"SELECT seq, body FROM audit_events "
"WHERE event_type = 'providence_canonical_witness' "
"ORDER BY seq ASC"
).fetchall()
finally:
conn.close()
fixtures: list[dict] = []
seen_ids: set[str] = set()
for r in rows:
try:
body = json.loads(r["body"])
except (json.JSONDecodeError, TypeError):
continue
agreement = body.get("agreement_label")
if agreement not in _DIVERGENCE_LABELS:
continue
llm_raw = body.get("llm_raw_text")
if not llm_raw:
# Unparseable LLM output isn't a calibration sample.
continue
canonical_text = body.get("canonical_answer_text") or ""
question_text = body.get("question_text") or ""
pi_star_ref = body.get("pi_star_ref") or "arithmetic@v1"
# Stable id from seq — re-running against the same qa.db
# produces the same fixture bytes (idempotency contract).
fixture_id = f"{fixture_id_prefix}-{int(r['seq']):06d}"
if fixture_id in seen_ids:
continue
seen_ids.add(fixture_id)
fixtures.append({
"id": fixture_id,
"battery": "5f",
"sub_battery": "falsification",
"version": "v1",
"carrier": "providence_record",
"domain": "claim_lattice",
"pi_star_ref": pi_star_ref,
"answer_text": llm_raw,
"context": (
f"canonical_kernel_answer={canonical_text}\n"
f"question={question_text}"
),
"expected_reason": "UNGROUNDED",
"verifier_method_root": verifier_method_root,
"expected": "pass",
"_witness_meta": {
"agreement_label": agreement,
"audit_seq": int(r["seq"]),
"pi_star_ref": pi_star_ref,
},
})
return fixtures
def write_fixtures(out_path: Path, fixtures: list[dict]) -> None:
out_path.parent.mkdir(parents=True, exist_ok=True)
meta = {
"_meta": {
"battery": "5f",
"sub_battery": "falsification",
"version": "v1",
"task_count": len(fixtures),
"notes": (
"Witness-divergence-extracted falsification fixtures. "
"Auto-generated by `make bench-witness-divergence` "
"from providence_canonical_witness audit events. "
"Each row pairs an LLM raw answer with the kernel's "
"canonical answer; verify_quotes is expected to "
"return UNGROUNDED because the LLM prose doesn't "
"substring-match the kernel's terse canonical form. "
"_witness_meta carries the original agreement_label "
"+ audit_seq for traceability."
),
},
}
with out_path.open("w", encoding="utf-8") as fh:
fh.write(json.dumps(meta, ensure_ascii=False) + "\n")
for fx in fixtures:
fh.write(json.dumps(fx, ensure_ascii=False) + "\n")
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
p.add_argument(
"--qa-db", type=Path, required=True,
help="path to qa.db (the providence_cache + audit_events store)",
)
p.add_argument(
"--out", type=Path,
default=Path("bench/fixtures/5f/falsification-witness-v1.jsonl"),
help="output JSONL path",
)
args = p.parse_args(argv)
if not args.qa_db.is_file():
print(f"error: qa-db not found: {args.qa_db}", file=sys.stderr)
return 2
fixtures = extract_divergence_fixtures(args.qa_db)
write_fixtures(args.out, fixtures)
print(
f"wrote {len(fixtures)} divergence fixtures to {args.out}",
file=sys.stderr,
)
print(json.dumps({"divergence_count": len(fixtures)}))
return 0
if __name__ == "__main__":
sys.exit(main())