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).
210 lines
7.5 KiB
Python
210 lines
7.5 KiB
Python
"""Tests for the witness-divergence → 5F Falsification extractor.
|
|
|
|
Two streams: the extractor logic itself + the audit-event write
|
|
in query.py that feeds it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from arborist.qa.client import StubClient
|
|
from arborist.qa.query import DEFAULT_QUERY_POLICY, query
|
|
from arborist.store import connect
|
|
|
|
|
|
# ----- audit-event write (#000028 follow-up) -----------------------------
|
|
|
|
|
|
def test_witness_event_written_to_audit_chain(tmp_path: Path):
|
|
"""When witness fires, query() appends a
|
|
providence_canonical_witness audit event with the question +
|
|
canonical + LLM raw answer + agreement label."""
|
|
qa_db = tmp_path / "qa.db"
|
|
policy = dict(DEFAULT_QUERY_POLICY)
|
|
policy["canonical_witness_enabled"] = True
|
|
client = StubClient(answer="3/10")
|
|
query(
|
|
question="0.1 + 0.2", qa_db=qa_db,
|
|
chat_client=client, model_id="stub", policy=policy,
|
|
)
|
|
conn = connect(qa_db)
|
|
try:
|
|
rows = conn.execute(
|
|
"SELECT body FROM audit_events "
|
|
"WHERE event_type = 'providence_canonical_witness'"
|
|
).fetchall()
|
|
finally:
|
|
conn.close()
|
|
assert len(rows) == 1
|
|
body = json.loads(rows[0]["body"])
|
|
assert body["pi_star_ref"] == "arithmetic@v1"
|
|
assert body["question_text"] == "0.1 + 0.2"
|
|
assert body["canonical_answer_text"] == "3/10"
|
|
assert body["llm_raw_text"] == "3/10"
|
|
# Agreement: KERNEL-LLM-AGREE (no cache yet on first call).
|
|
assert body["agreement_label"] == "KERNEL-LLM-AGREE"
|
|
|
|
|
|
def test_witness_event_records_divergence(tmp_path: Path):
|
|
"""LLM emits a wrong canonical-shape answer; the audit event
|
|
captures the divergence label + raw text for the extractor."""
|
|
qa_db = tmp_path / "qa.db"
|
|
policy = dict(DEFAULT_QUERY_POLICY)
|
|
policy["canonical_witness_enabled"] = True
|
|
client = StubClient(answer="0.4") # 0.4 → 2/5 ≠ kernel's 3/10
|
|
query(
|
|
question="0.1 + 0.2", qa_db=qa_db,
|
|
chat_client=client, model_id="stub", policy=policy,
|
|
)
|
|
conn = connect(qa_db)
|
|
try:
|
|
body = json.loads(conn.execute(
|
|
"SELECT body FROM audit_events "
|
|
"WHERE event_type = 'providence_canonical_witness' "
|
|
"ORDER BY seq DESC LIMIT 1"
|
|
).fetchone()["body"])
|
|
finally:
|
|
conn.close()
|
|
assert body["agreement_label"] == "KERNEL-LLM-DIVERGED"
|
|
assert body["llm_raw_text"] == "0.4"
|
|
assert body["canonical_answer_text"] == "3/10"
|
|
|
|
|
|
def test_witness_no_event_when_disabled(tmp_path: Path):
|
|
"""Default-off witness path → no providence_canonical_witness
|
|
event landed."""
|
|
qa_db = tmp_path / "qa.db"
|
|
client = StubClient(answer="<should-not-be-called>")
|
|
query(
|
|
question="0.1 + 0.2", qa_db=qa_db,
|
|
chat_client=client, model_id="stub",
|
|
)
|
|
conn = connect(qa_db)
|
|
try:
|
|
n = conn.execute(
|
|
"SELECT COUNT(*) FROM audit_events "
|
|
"WHERE event_type = 'providence_canonical_witness'"
|
|
).fetchone()[0]
|
|
finally:
|
|
conn.close()
|
|
assert n == 0
|
|
|
|
|
|
# ----- extractor (witness_to_5f.py) --------------------------------------
|
|
|
|
|
|
def _seed_witness_events(qa_db: Path, events: list[dict]) -> None:
|
|
"""Helper: fire `query` with synthetic LLM answers to build a
|
|
qa.db carrying a known set of witness audit events."""
|
|
policy = dict(DEFAULT_QUERY_POLICY)
|
|
policy["canonical_witness_enabled"] = True
|
|
for ev in events:
|
|
client = StubClient(answer=ev["llm_answer"])
|
|
query(
|
|
question=ev["question"], qa_db=qa_db,
|
|
chat_client=client, model_id="stub", policy=policy,
|
|
)
|
|
|
|
|
|
def test_extract_divergence_fixtures_basic(tmp_path: Path):
|
|
from bench.scripts.witness_to_5f import extract_divergence_fixtures
|
|
qa_db = tmp_path / "qa.db"
|
|
# Seed: 1 agreement, 2 divergences. Only the divergences come back.
|
|
_seed_witness_events(qa_db, [
|
|
{"question": "0.1 + 0.2", "llm_answer": "3/10"}, # agree
|
|
{"question": "1 + 1", "llm_answer": "3"}, # diverged
|
|
{"question": "2 + 2", "llm_answer": "5"}, # diverged
|
|
])
|
|
fixtures = extract_divergence_fixtures(qa_db)
|
|
assert len(fixtures) == 2
|
|
questions = sorted(
|
|
f["context"].split("\nquestion=")[1] for f in fixtures
|
|
)
|
|
assert questions == ["1 + 1", "2 + 2"]
|
|
|
|
|
|
def test_extract_skips_unparseable_llm_text(tmp_path: Path):
|
|
"""LLM emits prose rather than canonical-shape answer; that's
|
|
KERNEL-ONLY (LLM modality absent), not divergence."""
|
|
from bench.scripts.witness_to_5f import extract_divergence_fixtures
|
|
qa_db = tmp_path / "qa.db"
|
|
_seed_witness_events(qa_db, [
|
|
{"question": "0.1 + 0.2", "llm_answer": "the answer is around 0.3"},
|
|
])
|
|
fixtures = extract_divergence_fixtures(qa_db)
|
|
# KERNEL-ONLY isn't in _DIVERGENCE_LABELS — skipped.
|
|
assert len(fixtures) == 0
|
|
|
|
|
|
def test_extract_fixture_schema_matches_5f_falsification(tmp_path: Path):
|
|
"""The emitted fixture dict has the same fields the existing
|
|
5F-falsification-live runner expects."""
|
|
from bench.scripts.witness_to_5f import extract_divergence_fixtures
|
|
qa_db = tmp_path / "qa.db"
|
|
_seed_witness_events(qa_db, [
|
|
{"question": "1 + 1", "llm_answer": "3"},
|
|
])
|
|
fixtures = extract_divergence_fixtures(qa_db)
|
|
assert len(fixtures) == 1
|
|
fx = fixtures[0]
|
|
# Required by the 5F-falsification schema.
|
|
for required in (
|
|
"id", "battery", "sub_battery", "version", "carrier",
|
|
"domain", "pi_star_ref", "answer_text", "context",
|
|
"expected_reason", "verifier_method_root", "expected",
|
|
):
|
|
assert required in fx, f"missing field {required!r}"
|
|
assert fx["battery"] == "5f"
|
|
assert fx["sub_battery"] == "falsification"
|
|
assert fx["version"] == "v1"
|
|
assert fx["carrier"] == "providence_record"
|
|
assert fx["expected_reason"] == "UNGROUNDED"
|
|
assert fx["expected"] == "pass"
|
|
# Witness traceability metadata stays out of the runner's path
|
|
# (underscored field) but available for inspection.
|
|
assert "_witness_meta" in fx
|
|
assert fx["_witness_meta"]["agreement_label"] == "KERNEL-LLM-DIVERGED"
|
|
|
|
|
|
def test_extract_idempotent_on_same_db(tmp_path: Path):
|
|
"""Running the extractor twice against the same qa.db produces
|
|
byte-equal fixtures (sorted by audit-event seq)."""
|
|
from bench.scripts.witness_to_5f import (
|
|
extract_divergence_fixtures,
|
|
)
|
|
qa_db = tmp_path / "qa.db"
|
|
_seed_witness_events(qa_db, [
|
|
{"question": "1 + 1", "llm_answer": "3"},
|
|
{"question": "2 + 2", "llm_answer": "5"},
|
|
])
|
|
a = extract_divergence_fixtures(qa_db)
|
|
b = extract_divergence_fixtures(qa_db)
|
|
assert a == b
|
|
|
|
|
|
def test_write_fixtures_creates_5f_jsonl_meta(tmp_path: Path):
|
|
"""The output JSONL starts with a meta header naming the
|
|
battery + sub_battery + task_count, matching the rest of the
|
|
bench/fixtures/5f/*-v1.jsonl files."""
|
|
from bench.scripts.witness_to_5f import (
|
|
extract_divergence_fixtures, write_fixtures,
|
|
)
|
|
qa_db = tmp_path / "qa.db"
|
|
out = tmp_path / "out.jsonl"
|
|
_seed_witness_events(qa_db, [
|
|
{"question": "1 + 1", "llm_answer": "3"},
|
|
])
|
|
fixtures = extract_divergence_fixtures(qa_db)
|
|
write_fixtures(out, fixtures)
|
|
lines = out.read_text(encoding="utf-8").splitlines()
|
|
meta = json.loads(lines[0])["_meta"]
|
|
assert meta["battery"] == "5f"
|
|
assert meta["sub_battery"] == "falsification"
|
|
assert meta["task_count"] == 1
|
|
# Subsequent lines are valid fixture JSONs.
|
|
fx = json.loads(lines[1])
|
|
assert fx["id"].startswith("5f-fal-witness-")
|