arborist/bench/batteries/b_5t.py
russell@unturf.com 2af28e66af
bench: land #000023 + #000024 + #000025 (Phase 1a/1b — Dav1DPrometheus suite)
Implements three coupled tickets in one push: complete the 5S battery,
align 5T to Dav1DPrometheus vocabulary + complete it, open 5F battery.
All runners deterministic, no LLM-as-judge anywhere.

#000023 — 5S Phase 1b (closed)
  run_syllogism + run_synthesis + run_semiotics replace stubs.
  30 deterministic fixtures each (90 total, all passing).
  Carrier metadata mandatory; unsupported carriers fail explicitly.
  Syllogism kernel handles categorical_transitivity, chain_3,
  invalid_converse, missing_premise. Order-agnostic over premise
  permutations.
  Synthesis uses content-token subset check (stopwords removed) —
  catches single-token entity swaps that lax overlap missed.
  Semiotics validates synonym-swap invariance via π* canonicalize +
  re-substitution. Hidden-channel work stays defensive only.

#000024 — 5T Phase 1b (closed)
  Vocabulary aligned: Transfer→transfer-learning, Truth→truthtables,
  Timing→time. Legacy transfer-v1.jsonl + run_transfer kept intact.
  run_transfer_learning, run_triangulation, run_truthtables,
  run_transitivity, run_time replace stubs.
  30 fixtures each × 5 = 150 total (155 with legacy transfer).
  Triangulation runs 4 strategies (substring, token_subset,
  token_overlap, entity_match) and gates on agreement threshold.
  Truthtables: deterministic propositional evaluator with
  recursive-descent parser; supports AND/OR/NOT/XOR/IMPL/IFF;
  capped at N=4 variables.
  Transitivity: typed-relation whitelist
  (implies, subset_of, ancestor_of, before, less_than). BFS path
  walk; mixed/unknown relations fail by construction.
  Time: synthetic memory-snapshot chains test preservation,
  stale-marking, current_root tracking. Phase 1b.2 will read real
  memory_records.

#000025 — 5F Phase 1a (in progress; Phase 1b expansion still open)
  New bench/batteries/b_5f.py with five sub-batteries:
  - Function (shape_match / pointer_set_match / threshold_on_metric)
  - Finetuning (parent→child SelfModel improvement check)
  - Falsification (planted-error detection rate, verifier_method_root pinned)
  - Formulate (structural lattice match: claim count + sorted-approx
    text + exact pointer-ID-set; not exact-string)
  - Feedback Loop (operation/observation chain → expected_delta in
    aggregated observation feed)
  10 deterministic seed fixtures per sub-battery (50 total).
  feedback_efficiency / adaptation_efficiency hooks stub the capital-
  ledger integration for v8 fork-choice.

Cross-cutting:
  - bench/batteries/base.py: PHASE_1_CARRIERS whitelist +
    validate_carrier helper. Backward-compatible with Phase 1a
    fixtures that lack carrier field (defaults to "text").
  - runner.py registers all 16 sub-batteries in _DEFAULT_FIXTURES
    so `--all` runs the entire Dav1DPrometheus suite.
  - Makefile: bench-5s / bench-5t / bench-5f / bench-5s5t5f
    targets. bench-5t-legacy preserves Phase 1a access.
  - tests/test_bench_batteries.py: 29 tests (was 17). Phase 1a
    digest stability checks, sub-battery smoke tests, carrier
    rejection tests, transitivity-whitelist test, truthtables
    N>4 cap test.

Bench summary across the full suite:
  5s syntax/semantics/syllogism/synthesis/semiotics  108/108 pass
  5t transfer/transfer-learning/triangulation/truthtables/transitivity/time  154/154 pass
  5f function/finetuning/falsification/formulate/feedback-loop  50/50 pass
  TOTAL: 312 fixtures across 16 sub-batteries — 100% pass.

Full test suite: 1103 passed, 36 skipped.

Source: Legally Unprecedented Dav1DPrometheus (BasementAGI host).
Honoring his framework. The complete state-space synthesis (SQD +
v7 + 5S/5T/5F) is now executable infrastructure, not metaphor.
2026-05-07 20:14:44 -04:00

652 lines
22 KiB
Python

"""5T battery — Transitivity / Transfer Learning / Triangulation /
Truthtables / Time.
Phase 1a (landed in ticket #000021): Transfer (legacy SQD-whitepaper
name). Stays intact for back-compat.
Phase 1b (ticket #000024, this file):
- Vocabulary aligned to Dav1DPrometheus: ``transfer-learning``,
``triangulation``, ``truthtables``, ``transitivity``, ``time``.
- ``transfer-v1`` digest stays pinned; new ``transfer-learning-v2``
is the canonical name going forward.
- Five non-stub runners over text / claim_lattice / propositional /
memory-root carriers.
No LLM-as-judge anywhere. All runners deterministic. Cross-modality
discipline: fixtures may carry future-modality hooks; v1 carriers
limited to the Phase-1 whitelist in ``base.PHASE_1_CARRIERS``.
"""
from __future__ import annotations
import json
import re
from itertools import permutations
from pathlib import Path
from typing import Iterable
from bench.batteries.base import (
BatteryResult,
TaskResult,
fixture_digest,
fixture_meta,
iter_tasks,
validate_carrier,
)
from bench.batteries.b_5s import _runtime_digest
def _carrier_check(task: dict) -> tuple[bool, str]:
reason = validate_carrier(task)
if reason is not None:
return False, reason
return True, ""
def _build_result(
sub_battery: str,
fixtures_path: Path,
per_task: list[TaskResult],
metrics: dict[str, float],
) -> BatteryResult:
pass_count = sum(1 for t in per_task if t.passed)
fail_count = len(per_task) - pass_count
return BatteryResult(
battery="5t",
sub_battery=sub_battery,
fixture_path=str(fixtures_path),
fixture_digest=fixture_digest(fixtures_path),
pass_count=pass_count,
fail_count=fail_count,
metrics=metrics,
per_task=per_task,
runtime_digest=_runtime_digest(),
)
# ---------------------------------------------------------------------
# Phase 1a: Transfer (legacy)
# ---------------------------------------------------------------------
def run_transfer(fixtures_path: Path) -> BatteryResult:
"""Legacy SQD-whitepaper Transfer runner.
Each task: π* + two surface variants → does canonicalize collapse
them as expected_equivalent says? Kept untouched for Phase 1a
digest stability.
"""
from arborist.pi_star import get
per_task: list[TaskResult] = []
for task in iter_tasks(fixtures_path):
task_id = task["id"]
ok, reason = _carrier_check(task)
if not ok:
per_task.append(
TaskResult(task_id=task_id, passed=False, detail={"reason": reason})
)
continue
detail: dict = {}
try:
pi_star = get(task["pi_star"])
ca = pi_star.canonicalize(task["input_a"].encode("utf-8"))
cb = pi_star.canonicalize(task["input_b"].encode("utf-8"))
equivalent = ca == cb
ok = equivalent == bool(task["expected_equivalent"])
if not ok:
detail["reason"] = (
f"observed_equivalent={equivalent} "
f"expected={task['expected_equivalent']}"
)
except Exception as exc: # noqa: BLE001
detail["reason"] = f"{type(exc).__name__}: {exc}"
ok = False
per_task.append(TaskResult(task_id=task_id, passed=ok, detail=detail))
total = len(per_task)
rate = sum(1 for t in per_task if t.passed) / total if total else 0.0
meta = fixture_meta(fixtures_path)
return _build_result(
meta.get("sub_battery", "transfer"),
fixtures_path,
per_task,
{"transfer_pass_rate": rate},
)
# ---------------------------------------------------------------------
# Phase 1b: Transfer Learning (Dav1DPrometheus canonical)
# ---------------------------------------------------------------------
def run_transfer_learning(fixtures_path: Path) -> BatteryResult:
"""Pattern transfer across tasks/domains.
Each task: a source-task pattern + a target-task pattern + an
expected_transfer flag. Runner asserts that the target task
can be expressed under the same canonical structure as the
source — for v1, this means the target's content tokens are a
superset of the source's content tokens at the *pattern* level
(not the example level).
"""
per_task: list[TaskResult] = []
for task in iter_tasks(fixtures_path):
task_id = task["id"]
ok, reason = _carrier_check(task)
if not ok:
per_task.append(
TaskResult(task_id=task_id, passed=False, detail={"reason": reason})
)
continue
try:
src_pattern = task["source_task"]["pattern"]
tgt_pattern = task["target_task"]["pattern"]
expected = bool(task["target_task"].get("expected_transfer", True))
structural_match = _patterns_share_structure(src_pattern, tgt_pattern)
passed = structural_match == expected
detail = {
"expected_transfer": expected,
"observed_transfer": structural_match,
}
except Exception as exc: # noqa: BLE001
passed = False
detail = {"reason": f"{type(exc).__name__}: {exc}"}
per_task.append(TaskResult(task_id=task_id, passed=passed, detail=detail))
total = len(per_task)
rate = sum(1 for t in per_task if t.passed) / total if total else 0.0
meta = fixture_meta(fixtures_path)
return _build_result(
meta.get("sub_battery", "transfer-learning"),
fixtures_path,
per_task,
{"transfer_learning_success_rate": rate},
)
def _patterns_share_structure(a: str, b: str) -> bool:
"""Two patterns share structure iff they share ≥ 50% of content
tokens AND have similar length (within 50% of each other)."""
from bench.batteries.b_5s import _content_tokens
a_tokens = _content_tokens(a)
b_tokens = _content_tokens(b)
if not a_tokens or not b_tokens:
return False
overlap = len(a_tokens & b_tokens) / max(len(a_tokens), len(b_tokens))
len_ratio = min(len(a), len(b)) / max(len(a), len(b))
return overlap >= 0.5 and len_ratio >= 0.5
# ---------------------------------------------------------------------
# Phase 1b: Triangulation
# ---------------------------------------------------------------------
# Independent verifier strategies that agree (or don't) on whether a
# claim is supported by evidence text. Each is deterministic.
def _strat_substring(claim: str, evidence: str) -> bool:
return claim.strip().lower() in evidence.lower()
def _strat_token_subset(claim: str, evidence: str) -> bool:
from bench.batteries.b_5s import _content_tokens
a = _content_tokens(claim)
b = _content_tokens(evidence)
return bool(a) and a.issubset(b)
def _strat_token_overlap(claim: str, evidence: str, threshold: float = 0.6) -> bool:
from bench.batteries.b_5s import _content_tokens
a = _content_tokens(claim)
b = _content_tokens(evidence)
if not a:
return False
return len(a & b) / len(a) >= threshold
def _strat_entity_match(claim: str, evidence: str) -> bool:
"""Capitalized tokens (proper nouns) in claim must appear in evidence."""
entities = {
tok.strip(".,;:!?")
for tok in claim.split()
if tok and tok[0].isupper() and tok.lower() not in {"a", "an", "the", "i"}
}
if not entities:
return False
e_low = evidence.lower()
return all(e.lower() in e_low for e in entities)
_TRIANGULATION_STRATEGIES = {
"substring": _strat_substring,
"token_subset": _strat_token_subset,
"token_overlap": _strat_token_overlap,
"entity_match": _strat_entity_match,
}
def run_triangulation(fixtures_path: Path) -> BatteryResult:
"""Independent-strategy agreement on (claim, evidence) pairs.
Each task names a list of strategies + an ``expected_agreement_min``.
Runner runs each strategy, computes pass-rate across them, and
accepts the task iff agreement rate ≥ threshold.
"""
per_task: list[TaskResult] = []
for task in iter_tasks(fixtures_path):
task_id = task["id"]
ok, reason = _carrier_check(task)
if not ok:
per_task.append(
TaskResult(task_id=task_id, passed=False, detail={"reason": reason})
)
continue
strategies = task.get("strategies", list(_TRIANGULATION_STRATEGIES))
claim = task["claim"]
evidence = task["evidence"]
results = {}
verdicts: list[bool] = []
for s in strategies:
fn = _TRIANGULATION_STRATEGIES.get(s)
if fn is None:
continue
v = fn(claim, evidence)
results[s] = v
verdicts.append(v)
if not verdicts:
per_task.append(
TaskResult(task_id=task_id, passed=False,
detail={"reason": "no recognized strategies"})
)
continue
agreement = sum(verdicts) / len(verdicts)
threshold = float(task.get("expected_agreement_min", 0.75))
expected = task.get("expected", "pass")
# expected="pass" → claim should triangulate; agreement>=threshold.
# expected="fail" → claim should NOT triangulate; agreement<threshold.
if expected == "pass":
passed = agreement >= threshold
else:
passed = agreement < threshold
per_task.append(
TaskResult(
task_id=task_id,
passed=passed,
detail={
"strategy_results": results,
"agreement": agreement,
"threshold": threshold,
"expected": expected,
},
)
)
total = len(per_task)
rate = sum(1 for t in per_task if t.passed) / total if total else 0.0
meta = fixture_meta(fixtures_path)
return _build_result(
meta.get("sub_battery", "triangulation"),
fixtures_path,
per_task,
{"triangulation_agreement_rate": rate},
)
# ---------------------------------------------------------------------
# Phase 1b: Truthtables (N=2..4)
# ---------------------------------------------------------------------
def _eval_propositional(expr: str, env: dict[str, bool]) -> bool:
"""Deterministic propositional evaluator for AND/OR/NOT/XOR.
Recognizes tokens A-Z (variable), AND, OR, NOT, XOR, IMPL, IFF,
parentheses. No LLM. Operator precedence: NOT > AND > OR/XOR/IMPL/IFF
(left-to-right within same precedence). Parens override.
Implementation: tokenize → recursive-descent parse → evaluate.
"""
tokens = _tokenize_propositional(expr)
pos = [0]
def expect(t: str) -> None:
if pos[0] >= len(tokens) or tokens[pos[0]] != t:
raise ValueError(f"expected {t!r} at {pos[0]}")
pos[0] += 1
def peek() -> str | None:
if pos[0] >= len(tokens):
return None
return tokens[pos[0]]
def parse_atom() -> bool:
t = peek()
if t is None:
raise ValueError("unexpected end of expression")
if t == "(":
pos[0] += 1
v = parse_or()
expect(")")
return v
if t == "NOT":
pos[0] += 1
return not parse_atom()
if t in ("TRUE", "FALSE"):
pos[0] += 1
return t == "TRUE"
if len(t) == 1 and t.isupper() and t.isalpha():
if t not in env:
raise ValueError(f"variable {t!r} not in env")
pos[0] += 1
return env[t]
raise ValueError(f"unexpected token {t!r}")
def parse_and() -> bool:
left = parse_atom()
while peek() == "AND":
pos[0] += 1
right = parse_atom()
left = left and right
return left
def parse_or() -> bool:
left = parse_and()
while peek() in ("OR", "XOR", "IMPL", "IFF"):
op = peek()
pos[0] += 1
right = parse_and()
if op == "OR":
left = left or right
elif op == "XOR":
left = left != right
elif op == "IMPL":
left = (not left) or right
else: # IFF
left = left == right
return left
val = parse_or()
if pos[0] != len(tokens):
raise ValueError(f"unconsumed tokens at {pos[0]}: {tokens[pos[0]:]!r}")
return val
def _tokenize_propositional(expr: str) -> list[str]:
"""Split an expression into tokens, treating AND/OR/NOT/XOR/IMPL/IFF as keywords."""
out: list[str] = []
i = 0
while i < len(expr):
c = expr[i]
if c.isspace():
i += 1
continue
if c in "()":
out.append(c)
i += 1
continue
if c.isalpha():
j = i
while j < len(expr) and expr[j].isalpha():
j += 1
tok = expr[i:j]
up = tok.upper()
if up in ("AND", "OR", "NOT", "XOR", "IMPL", "IFF", "TRUE", "FALSE"):
out.append(up)
elif len(tok) == 1:
out.append(tok.upper())
else:
raise ValueError(f"unrecognized token: {tok!r}")
i = j
continue
raise ValueError(f"unexpected char: {c!r}")
return out
def run_truthtables(fixtures_path: Path) -> BatteryResult:
"""Exhaustive propositional truth-table evaluation; N=2..4.
Each task lists `variables` (≤4), an `expression`, and a `rows`
array enumerating expected outputs. Runner evaluates the
expression on every row and fails the task if any row mismatches.
"""
per_task: list[TaskResult] = []
coverage_total = 0
coverage_correct = 0
for task in iter_tasks(fixtures_path):
task_id = task["id"]
ok, reason = _carrier_check(task)
if not ok:
per_task.append(
TaskResult(task_id=task_id, passed=False, detail={"reason": reason})
)
continue
variables = task["variables"]
if len(variables) > 4:
per_task.append(
TaskResult(task_id=task_id, passed=False,
detail={"reason": f"N={len(variables)} exceeds cap of 4"})
)
continue
rows = task["rows"]
coverage_total += len(rows)
all_correct = True
first_mismatch = None
for row in rows:
try:
got = _eval_propositional(task["expression"], row["inputs"])
except Exception as exc: # noqa: BLE001
all_correct = False
first_mismatch = {
"inputs": row["inputs"],
"error": f"{type(exc).__name__}: {exc}",
}
break
if got == bool(row["expected"]):
coverage_correct += 1
else:
all_correct = False
if first_mismatch is None:
first_mismatch = {
"inputs": row["inputs"],
"expected": row["expected"],
"got": got,
}
detail: dict = {"row_count": len(rows)}
if first_mismatch is not None:
detail["first_mismatch"] = first_mismatch
per_task.append(TaskResult(task_id=task_id, passed=all_correct, detail=detail))
total = len(per_task)
pass_rate = sum(1 for t in per_task if t.passed) / total if total else 0.0
row_acc = coverage_correct / coverage_total if coverage_total else 0.0
meta = fixture_meta(fixtures_path)
return _build_result(
meta.get("sub_battery", "truthtables"),
fixtures_path,
per_task,
{
"truth_table_coverage_rate": pass_rate,
"row_accuracy_rate": row_acc,
},
)
# ---------------------------------------------------------------------
# Phase 1b: Transitivity (typed-relation whitelist)
# ---------------------------------------------------------------------
# Per ticket #000024 §4.3: only relations on this whitelist license
# transitive closure. "related_to" / "causes" without domain
# constraints are NOT transitive.
_TRANSITIVE_RELATIONS = frozenset({
"implies",
"subset_of",
"ancestor_of",
"before",
"less_than",
})
def _walk_relation_path(
edges: list[dict], start: str, end: str, relation: str
) -> bool:
"""BFS over edges of the given relation only. Returns True iff a
path from start to end exists in the relation-restricted subgraph.
"""
if relation not in _TRANSITIVE_RELATIONS:
return False
adj: dict[str, set[str]] = {}
for e in edges:
if e.get("relation") != relation:
continue
adj.setdefault(e["from"], set()).add(e["to"])
if start == end:
return True
visited = {start}
queue = [start]
while queue:
node = queue.pop(0)
for nxt in adj.get(node, ()):
if nxt == end:
return True
if nxt not in visited:
visited.add(nxt)
queue.append(nxt)
return False
def run_transitivity(fixtures_path: Path) -> BatteryResult:
"""Typed-relation chain pass-rate.
Each task: typed edges + a query (from, to, relation). Runner
walks the edge subgraph for that relation and asserts whether a
path exists. Mixed-relation chains never pass — runner only
considers edges of the queried relation. Non-transitive
relations always fail by construction.
"""
per_task: list[TaskResult] = []
for task in iter_tasks(fixtures_path):
task_id = task["id"]
ok, reason = _carrier_check(task)
if not ok:
per_task.append(
TaskResult(task_id=task_id, passed=False, detail={"reason": reason})
)
continue
try:
q = task["query"]
edges = task["edges"]
path_exists = _walk_relation_path(edges, q["from"], q["to"], q["relation"])
observed = "pass" if path_exists else "fail"
expected = task.get("expected", "pass")
passed = observed == expected
detail = {
"expected": expected,
"observed": observed,
"relation": q["relation"],
"transitive": q["relation"] in _TRANSITIVE_RELATIONS,
}
except Exception as exc: # noqa: BLE001
passed = False
detail = {"reason": f"{type(exc).__name__}: {exc}"}
per_task.append(TaskResult(task_id=task_id, passed=passed, detail=detail))
total = len(per_task)
rate = sum(1 for t in per_task if t.passed) / total if total else 0.0
meta = fixture_meta(fixtures_path)
return _build_result(
meta.get("sub_battery", "transitivity"),
fixtures_path,
per_task,
{"full_chain_pass_rate": rate},
)
# ---------------------------------------------------------------------
# Phase 1b: Time (memory-root snapshots)
# ---------------------------------------------------------------------
def run_time(fixtures_path: Path) -> BatteryResult:
"""Temporal-context preservation across snapshots.
Each task: an ordered list of snapshots (each with a memory_root,
facts, optional stale-marker) + an `expected` block stating which
facts should preserve, which should mark stale, and what the
final root is. Runner deterministically validates these
properties from the fixture itself — no shard reads required for
Phase 1b. Real shard-driven Time runs ship in Phase 1b.2 once
arborist's memory_records is populated by a real workload.
"""
per_task: list[TaskResult] = []
for task in iter_tasks(fixtures_path):
task_id = task["id"]
ok, reason = _carrier_check(task)
if not ok:
per_task.append(
TaskResult(task_id=task_id, passed=False, detail={"reason": reason})
)
continue
try:
snapshots = task["snapshots"]
expected = task["expected"]
# All facts seen across the whole chain.
all_facts: set[str] = set()
for s in snapshots:
all_facts.update(s.get("facts", []))
all_facts.update(s.get("stale_facts", []))
# Preservation check: every fact named in expected.preserved_facts
# must appear in some snapshot.
preserved_ok = all(
f in all_facts
for f in expected.get("preserved_facts", [])
)
# Stale check: every fact in expected.marks_stale must appear
# at least once and the FINAL snapshot's stale_facts must
# include it.
final_stale = set(snapshots[-1].get("stale_facts", []))
stale_ok = all(
f in final_stale
for f in expected.get("marks_stale", [])
)
# Current root check.
current_root_ok = (
snapshots[-1].get("memory_root") == expected["current_root"]
)
passed = preserved_ok and stale_ok and current_root_ok
detail = {
"preserved_ok": preserved_ok,
"stale_ok": stale_ok,
"current_root_ok": current_root_ok,
}
except Exception as exc: # noqa: BLE001
passed = False
detail = {"reason": f"{type(exc).__name__}: {exc}"}
per_task.append(TaskResult(task_id=task_id, passed=passed, detail=detail))
total = len(per_task)
pres_rate = sum(1 for t in per_task if t.passed) / total if total else 0.0
meta = fixture_meta(fixtures_path)
return _build_result(
meta.get("sub_battery", "time"),
fixtures_path,
per_task,
{"temporal_context_preservation_rate": pres_rate},
)
# Triangulate / Timing alias names from Phase 1a stubs are removed —
# canonical Dav1DPrometheus names below.
SUB_BATTERIES = {
"transfer": run_transfer, # legacy SQD name (Phase 1a)
"transfer-learning": run_transfer_learning, # canonical Dav1DPrometheus
"triangulation": run_triangulation,
"truthtables": run_truthtables,
"transitivity": run_transitivity,
"time": run_time,
}