bench: ticket #000021 Phase 1a — 5S/5T harness skeleton + seed fixtures

Per fox's "partial punt on larger ones" — ships the bench/ skeleton +
small seed fixture sets so future v8/v7-W/SelfModel work can cite a
real fitness target. Full Phase 1 (50-200 fixtures per sub-battery)
and Phases 2-3 stay open in the ticket.

Phase 1a delivers:

- bench/batteries/{base,b_5s,b_5t,runner}.py — Battery protocol,
  BatteryResult, fixture-digest helpers, CLI runner.
- Seed fixtures:
  - bench/fixtures/5s/syntax-v1.jsonl — 10 tasks against
    wikitext-base@v1 and claim-lattice@v1
  - bench/fixtures/5s/semantics-v1.jsonl — 8 equivalence tasks
  - bench/fixtures/5t/transfer-v1.jsonl — 4 paraphrase-invariance
    tasks
- Runners for 5S Syntax, 5S Semantics, 5T Transfer. Other 5S/5T
  sub-batteries are stubs returning zero-task results.
- Makefile targets: bench-5s, bench-5t, bench-5s5t.
- runtime_digest field captures the active π* registry fingerprint
  so a registry change surfaces in bench results.

Tests: tests/test_bench_batteries.py (17 cases). Full suite:
1076 passed, 36 skipped. `make bench-5s5t` runs end-to-end and
emits JSON results.

Ticket #000021 status: in progress · Phase 1a landed; Phase 1b/2/3
remain open.
This commit is contained in:
russell@unturf.com 2026-05-07 16:57:45 -04:00
parent 5cbcda41b9
commit a64d941528
No known key found for this signature in database
12 changed files with 726 additions and 10 deletions

View file

@ -242,6 +242,22 @@ bench-emergent-pending: bootstrap ## print log entries awaiting teacher review
$(PY) scripts/bench_emergent.py --print-pending
bench-5s: bootstrap ## 5S battery (Syntax + Semantics) on Phase-1a seed fixtures
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner \
--battery 5s --sub syntax \
--fixtures bench/fixtures/5s/syntax-v1.jsonl
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner \
--battery 5s --sub semantics \
--fixtures bench/fixtures/5s/semantics-v1.jsonl
bench-5t: bootstrap ## 5T battery (Transfer) on Phase-1a seed fixture
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner \
--battery 5t --sub transfer \
--fixtures bench/fixtures/5t/transfer-v1.jsonl
bench-5s5t: bench-5s bench-5t ## both Phase-1a batteries (alias)
verify-shards: bootstrap ## cross-shard Merkle round-trip on a random sample
$(ARBORIST) --shards-dir $(SHARDS_DIR) verify -n $(VERIFY_N)

View file

@ -0,0 +1,13 @@
"""5S/5T/5R battery harness (ticket #000021).
Phase 1a (this landing): 5S Syntax + 5S Semantics + 5T Transfer
runners with seed fixtures. Phase 1b (full fixture authoring) and
Phase 2 (5R battery, depends on tickets #000014 + #000017) land
in follow-up tickets.
"""
from __future__ import annotations
from bench.batteries.base import Battery, BatteryResult, TaskResult
__all__ = ["Battery", "BatteryResult", "TaskResult"]

163
bench/batteries/b_5s.py Normal file
View file

@ -0,0 +1,163 @@
"""5S battery — Syntax / Semantics / Synthesis / Syllogism / Semiotics.
Phase 1a implements Syntax and Semantics against arborist's π*
registry. The other three sub-batteries are reserved as stubs that
return zero-task BatteryResults; full fixtures + runners land in
follow-up tickets.
"""
from __future__ import annotations
from pathlib import Path
from bench.batteries.base import (
BatteryResult,
TaskResult,
fixture_digest,
fixture_meta,
iter_tasks,
)
def _runtime_digest() -> str:
"""SHA-256 fingerprint of the active π* registry.
Includes name@version of every registered π* so a registry change
surfaces in the runtime_digest.
"""
import hashlib
from arborist.pi_star import list_keys
payload = "\n".join(list_keys()).encode("utf-8")
return hashlib.sha256(payload).hexdigest()
def run_syntax(fixtures_path: Path) -> BatteryResult:
"""Each task asserts that the named π* canonicalizes the input
without raising. expected="pass" is the only supported expected;
other values fail loudly."""
from arborist.pi_star import get
per_task: list[TaskResult] = []
pass_count = 0
fail_count = 0
for task in iter_tasks(fixtures_path):
task_id = task["id"]
ok = False
detail: dict = {}
try:
pi_star = get(task["pi_star"])
pi_star.canonicalize(task["input"].encode("utf-8"))
ok = task.get("expected", "pass") == "pass"
if not ok:
detail["reason"] = "canonicalize succeeded but expected != pass"
except NotImplementedError:
detail["reason"] = "pi_star is a stub"
ok = False
except Exception as exc: # noqa: BLE001
detail["reason"] = f"{type(exc).__name__}: {exc}"
ok = False
if ok:
pass_count += 1
else:
fail_count += 1
per_task.append(TaskResult(task_id=task_id, passed=ok, detail=detail))
total = pass_count + fail_count
pass_rate = pass_count / total if total else 0.0
meta = fixture_meta(fixtures_path)
return BatteryResult(
battery="5s",
sub_battery=meta.get("sub_battery", "syntax"),
fixture_path=str(fixtures_path),
fixture_digest=fixture_digest(fixtures_path),
pass_count=pass_count,
fail_count=fail_count,
metrics={"parse_pass_rate": pass_rate},
per_task=per_task,
runtime_digest=_runtime_digest(),
)
def run_semantics(fixtures_path: Path) -> BatteryResult:
"""Each task: two inputs + a π*; passes if canonicalize agrees with
expected_equivalent."""
from arborist.pi_star import get
per_task: list[TaskResult] = []
pass_count = 0
fail_count = 0
for task in iter_tasks(fixtures_path):
task_id = task["id"]
ok = False
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
if ok:
pass_count += 1
else:
fail_count += 1
per_task.append(TaskResult(task_id=task_id, passed=ok, detail=detail))
total = pass_count + fail_count
rate = pass_count / total if total else 0.0
meta = fixture_meta(fixtures_path)
return BatteryResult(
battery="5s",
sub_battery=meta.get("sub_battery", "semantics"),
fixture_path=str(fixtures_path),
fixture_digest=fixture_digest(fixtures_path),
pass_count=pass_count,
fail_count=fail_count,
metrics={"equivalence_recovery_rate": rate},
per_task=per_task,
runtime_digest=_runtime_digest(),
)
def _stub_result(name: str, fixtures_path: Path) -> BatteryResult:
return BatteryResult(
battery="5s",
sub_battery=name,
fixture_path=str(fixtures_path),
fixture_digest="",
pass_count=0,
fail_count=0,
metrics={},
per_task=[],
runtime_digest="",
)
def run_synthesis(fixtures_path: Path) -> BatteryResult:
return _stub_result("synthesis", fixtures_path)
def run_syllogism(fixtures_path: Path) -> BatteryResult:
return _stub_result("syllogism", fixtures_path)
def run_semiotics(fixtures_path: Path) -> BatteryResult:
return _stub_result("semiotics", fixtures_path)
SUB_BATTERIES = {
"syntax": run_syntax,
"semantics": run_semantics,
"synthesis": run_synthesis,
"syllogism": run_syllogism,
"semiotics": run_semiotics,
}

105
bench/batteries/b_5t.py Normal file
View file

@ -0,0 +1,105 @@
"""5T battery — Transfer / Triangulate / Timing / Transitivity / Truth.
Phase 1a implements Transfer (paraphrase-invariance under a π*).
The other four sub-batteries are reserved as stubs.
"""
from __future__ import annotations
from pathlib import Path
from bench.batteries.base import (
BatteryResult,
TaskResult,
fixture_digest,
fixture_meta,
iter_tasks,
)
from bench.batteries.b_5s import _runtime_digest
def run_transfer(fixtures_path: Path) -> BatteryResult:
"""Each task: π* + two surface variants → does canonicalize
collapse them as expected_equivalent says it should?"""
from arborist.pi_star import get
per_task: list[TaskResult] = []
pass_count = 0
fail_count = 0
for task in iter_tasks(fixtures_path):
task_id = task["id"]
ok = False
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
if ok:
pass_count += 1
else:
fail_count += 1
per_task.append(TaskResult(task_id=task_id, passed=ok, detail=detail))
total = pass_count + fail_count
rate = pass_count / total if total else 0.0
meta = fixture_meta(fixtures_path)
return BatteryResult(
battery="5t",
sub_battery=meta.get("sub_battery", "transfer"),
fixture_path=str(fixtures_path),
fixture_digest=fixture_digest(fixtures_path),
pass_count=pass_count,
fail_count=fail_count,
metrics={"transfer_pass_rate": rate},
per_task=per_task,
runtime_digest=_runtime_digest(),
)
def _stub_result(name: str, fixtures_path: Path) -> BatteryResult:
return BatteryResult(
battery="5t",
sub_battery=name,
fixture_path=str(fixtures_path),
fixture_digest="",
pass_count=0,
fail_count=0,
metrics={},
per_task=[],
runtime_digest="",
)
def run_triangulate(fixtures_path: Path) -> BatteryResult:
return _stub_result("triangulate", fixtures_path)
def run_timing(fixtures_path: Path) -> BatteryResult:
return _stub_result("timing", fixtures_path)
def run_transitivity(fixtures_path: Path) -> BatteryResult:
return _stub_result("transitivity", fixtures_path)
def run_truth(fixtures_path: Path) -> BatteryResult:
return _stub_result("truth", fixtures_path)
SUB_BATTERIES = {
"transfer": run_transfer,
"triangulate": run_triangulate,
"timing": run_timing,
"transitivity": run_transitivity,
"truth": run_truth,
}

92
bench/batteries/base.py Normal file
View file

@ -0,0 +1,92 @@
"""Battery base classes.
A Battery runs a fixture file end-to-end against a specified runtime
(usually arborist's existing surface) and emits a deterministic
:class:`BatteryResult`. Determinism: same fixture digest + same code
digest identical result (modulo wall-clock fields).
"""
from __future__ import annotations
import hashlib
import json
import time
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any, Iterable, Optional, Protocol
@dataclass
class TaskResult:
"""Per-task outcome inside a battery run."""
task_id: str
passed: bool
detail: dict[str, Any] = field(default_factory=dict)
@dataclass
class BatteryResult:
"""End-to-end battery result; deterministic given fixed inputs."""
battery: str
sub_battery: str
fixture_path: str
fixture_digest: str
pass_count: int
fail_count: int
metrics: dict[str, float] = field(default_factory=dict)
per_task: list[TaskResult] = field(default_factory=list)
runtime_digest: str = ""
timestamp: int = field(default_factory=lambda: int(time.time()))
def to_dict(self) -> dict:
d = asdict(self)
return d
class Battery(Protocol):
"""One sub-battery (e.g., 5S Syntax)."""
name: str
def run(self, fixtures_path: Path) -> BatteryResult:
...
def fixture_digest(fixtures_path: Path) -> str:
"""SHA-256 over the raw fixture-file bytes.
Used as the canonical identity of a battery run; bench results
cite this digest so cross-run comparisons are unambiguous.
"""
return hashlib.sha256(
Path(fixtures_path).read_bytes()
).hexdigest()
def iter_tasks(fixtures_path: Path) -> Iterable[dict]:
"""Yield non-meta task dicts from a JSONL fixture file."""
with open(fixtures_path, "r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line:
continue
obj = json.loads(line)
if "_meta" in obj:
continue
yield obj
def fixture_meta(fixtures_path: Path) -> dict:
"""Return the ``_meta`` dict if present at the top of the JSONL."""
with open(fixtures_path, "r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line:
continue
obj = json.loads(line)
if "_meta" in obj:
return obj["_meta"]
return {}
return {}

114
bench/batteries/runner.py Normal file
View file

@ -0,0 +1,114 @@
"""CLI entry: ``python -m bench.batteries.runner``.
Runs one or more battery sub-batteries against their fixture files
and emits a JSON result on stdout (or to ``--out`` if provided).
Usage examples
--------------
::
# Run 5S Syntax + Semantics with the seed fixtures.
python -m bench.batteries.runner \\
--battery 5s --sub syntax --fixtures bench/fixtures/5s/syntax-v1.jsonl
python -m bench.batteries.runner \\
--battery 5s --sub semantics \\
--fixtures bench/fixtures/5s/semantics-v1.jsonl
# Run 5T Transfer.
python -m bench.batteries.runner \\
--battery 5t --sub transfer \\
--fixtures bench/fixtures/5t/transfer-v1.jsonl
# All Phase-1a fixtures, written to a results file.
python -m bench.batteries.runner --all --out bench/results/run.json
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import asdict
from pathlib import Path
from typing import Optional
from bench.batteries import b_5s, b_5t
from bench.batteries.base import BatteryResult
_BATTERIES = {
"5s": b_5s.SUB_BATTERIES,
"5t": b_5t.SUB_BATTERIES,
}
_DEFAULT_FIXTURES = {
("5s", "syntax"): "bench/fixtures/5s/syntax-v1.jsonl",
("5s", "semantics"): "bench/fixtures/5s/semantics-v1.jsonl",
("5t", "transfer"): "bench/fixtures/5t/transfer-v1.jsonl",
}
def _run_one(battery: str, sub: str, fixtures: Path) -> BatteryResult:
if battery not in _BATTERIES:
raise SystemExit(f"unknown battery {battery!r}")
if sub not in _BATTERIES[battery]:
raise SystemExit(f"unknown sub-battery {sub!r} for {battery}")
return _BATTERIES[battery][sub](fixtures)
def _emit(results: list[BatteryResult], out: Optional[str]) -> None:
payload = {
"schema_version": "bench-result-v1",
"results": [asdict(r) for r in results],
}
encoded = json.dumps(payload, indent=2, ensure_ascii=False, default=str)
if out:
Path(out).parent.mkdir(parents=True, exist_ok=True)
Path(out).write_text(encoded + "\n", encoding="utf-8")
print(f"wrote {out}")
else:
print(encoded)
def main(argv: Optional[list[str]] = None) -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--battery", choices=sorted(_BATTERIES), default=None)
p.add_argument("--sub", default=None, help="sub-battery name")
p.add_argument("--fixtures", default=None, help="fixture JSONL path")
p.add_argument(
"--all",
dest="run_all",
action="store_true",
help="run all Phase-1a sub-batteries with default fixtures",
)
p.add_argument("--out", default=None, help="write JSON to this path")
args = p.parse_args(argv)
results: list[BatteryResult] = []
if args.run_all:
for (battery, sub), fixture_path in _DEFAULT_FIXTURES.items():
results.append(_run_one(battery, sub, Path(fixture_path)))
else:
if not args.battery or not args.sub:
p.error("--battery and --sub are required unless --all is given")
if args.fixtures:
fixtures = Path(args.fixtures)
else:
key = (args.battery, args.sub)
if key not in _DEFAULT_FIXTURES:
p.error(
f"no default fixture for ({args.battery},{args.sub}); "
"pass --fixtures explicitly"
)
fixtures = Path(_DEFAULT_FIXTURES[key])
results.append(_run_one(args.battery, args.sub, fixtures))
_emit(results, args.out)
failed = sum(r.fail_count for r in results)
return 1 if failed else 0
if __name__ == "__main__": # pragma: no cover
sys.exit(main())

View file

@ -0,0 +1,9 @@
{"_meta":{"battery":"5s","sub_battery":"semantics","version":"v1","task_count":8}}
{"id":"5s-sem-001","kind":"equivalence","pi_star":"wikitext-base@v1","input_a":"hello world","input_b":"hello world","expected_equivalent":true}
{"id":"5s-sem-002","kind":"equivalence","pi_star":"wikitext-base@v1","input_a":"hello world","input_b":"hello world","expected_equivalent":true}
{"id":"5s-sem-003","kind":"equivalence","pi_star":"wikitext-base@v1","input_a":"hello world","input_b":"goodbye world","expected_equivalent":false}
{"id":"5s-sem-004","kind":"equivalence","pi_star":"wikitext-base@v1","input_a":"[[link|word]]","input_b":"word","expected_equivalent":true}
{"id":"5s-sem-005","kind":"equivalence","pi_star":"wikitext-base@v1","input_a":"<ref>cite</ref>text","input_b":"text","expected_equivalent":true}
{"id":"5s-sem-006","kind":"equivalence","pi_star":"claim-lattice@v1","input_a":"- A claim. [E1]","input_b":"- A claim. [E1]","expected_equivalent":true}
{"id":"5s-sem-007","kind":"equivalence","pi_star":"claim-lattice@v1","input_a":"- A claim. [E1]","input_b":"- A different claim. [E1]","expected_equivalent":false}
{"id":"5s-sem-008","kind":"equivalence","pi_star":"claim-lattice@v1","input_a":"- A claim. [E1]","input_b":"- A claim. [E2]","expected_equivalent":false}

View file

@ -0,0 +1,11 @@
{"_meta":{"battery":"5s","sub_battery":"syntax","version":"v1","task_count":10}}
{"id":"5s-syn-001","kind":"parse-pass","pi_star":"wikitext-base@v1","input":"Plain text.","expected":"pass"}
{"id":"5s-syn-002","kind":"parse-pass","pi_star":"wikitext-base@v1","input":"[[Wikilink]]","expected":"pass"}
{"id":"5s-syn-003","kind":"parse-pass","pi_star":"wikitext-base@v1","input":"''italic''","expected":"pass"}
{"id":"5s-syn-004","kind":"parse-pass","pi_star":"wikitext-base@v1","input":"'''bold'''","expected":"pass"}
{"id":"5s-syn-005","kind":"parse-pass","pi_star":"wikitext-base@v1","input":"== Header ==","expected":"pass"}
{"id":"5s-syn-006","kind":"parse-pass","pi_star":"claim-lattice@v1","input":"- A claim. [E1]","expected":"pass"}
{"id":"5s-syn-007","kind":"parse-pass","pi_star":"claim-lattice@v1","input":"- Another claim. [E2]","expected":"pass"}
{"id":"5s-syn-008","kind":"parse-pass","pi_star":"claim-lattice@v1","input":"","expected":"pass"}
{"id":"5s-syn-009","kind":"parse-pass","pi_star":"wikitext-base@v1","input":"<ref>citation</ref> dropped","expected":"pass"}
{"id":"5s-syn-010","kind":"parse-pass","pi_star":"wikitext-base@v1","input":"[[File:img.jpg|thumb]]","expected":"pass"}

View file

@ -0,0 +1,5 @@
{"_meta":{"battery":"5t","sub_battery":"transfer","version":"v1","task_count":4,"note":"Phase 1 seed; full fixtures land in follow-up. Each task names a π* and asserts that canonicalize-then-canonicalize equivalence is preserved across paraphrase shapes drawn from a different surface form."}}
{"id":"5t-xfer-001","kind":"paraphrase-invariance","pi_star":"wikitext-base@v1","input_a":"hello [[world]]","input_b":"hello [[world|world]]","expected_equivalent":true}
{"id":"5t-xfer-002","kind":"paraphrase-invariance","pi_star":"wikitext-base@v1","input_a":"text\n\n\n\nmore","input_b":"text\n\nmore","expected_equivalent":true}
{"id":"5t-xfer-003","kind":"paraphrase-invariance","pi_star":"claim-lattice@v1","input_a":"- A. [E1]\n- B. [E2]","input_b":"- A. [E1]\n- B. [E2]\n","expected_equivalent":true}
{"id":"5t-xfer-004","kind":"paraphrase-invariance","pi_star":"claim-lattice@v1","input_a":"- A. [E1]","input_b":"- A. [E1, E2]","expected_equivalent":false}

View file

@ -58,7 +58,7 @@ Newest first. Update on every open/close.
| ID | Title | Status | Opened | Directive |
|----------|------------------------------------------------|-----------------------|------------|-----------|
| #000022 | Adapter LossReport (PRD I9 analogue) | open · awaiting go/no-go | 2026-05-07 | — |
| #000021 | 5S/5T/5R benchmark fixtures + harness | open · awaiting go/no-go | 2026-05-07 | — |
| #000021 | 5S/5T/5R benchmark fixtures + harness | in progress · Phase 1a landed 2026-05-07 | 2026-05-07 | — |
| #000020 | Capital-cost ledger (8-capital queues) | closed · landed 2026-05-07 | 2026-05-07 | — |
| #000019 | Specification methodology for π* and V | closed · landed 2026-05-07 | 2026-05-07 | — |
| #000018 | Adversarial soft-hash covert-channel analysis | open · awaiting go/no-go | 2026-05-07 | — |

View file

@ -1,6 +1,6 @@
# Ticket #000021 — 5S/5T/5R benchmark fixtures + harness extension
**Status:** open · awaiting go/no-go
**Status:** in progress · Phase 1a landed 2026-05-07; Phase 1b + 2 + 3 open
**Opened:** 2026-05-07
**Scope:** Implement the SQD whitepaper's named bench batteries
(5S Syntax/Semantics/Synthesis/Syllogism/Semiotics, 5T Transfer/
@ -290,12 +290,44 @@ bench-5s5t: bench-5s bench-5t
## 7. Status
**Open · awaiting go/no-go.** Mid-sized implementation surface
(~600 lines code + 200-1000 fixtures). Recommended Phase 1 scope
keeps it shippable in a single session for a focused operator;
Phase 2/3 are future tickets.
**In progress · Phase 1a landed 2026-05-07.** Skeleton + seed
fixtures ship; full Phase 1 fixture authoring + Phase 2 (5R) +
Phase 3 (external corpora) deferred to follow-up tickets.
Closure criterion: `bench/fixtures/5s/`, `bench/fixtures/5t/`,
`bench/batteries/{b_5s,b_5t,runner}.py` land; `make bench-5s5t`
runs end-to-end against the existing shards and produces a JSON
result file with all 10 battery slots populated.
### Phase 1a (landed)
- `bench/batteries/{base,b_5s,b_5t,runner}.py` — Battery protocol,
BatteryResult, fixture digest helpers, runner CLI.
- `bench/fixtures/5s/syntax-v1.jsonl` — 10 seed tasks against
`wikitext-base@v1` and `claim-lattice@v1`. All pass.
- `bench/fixtures/5s/semantics-v1.jsonl` — 8 seed tasks
(equivalence assertions). All pass.
- `bench/fixtures/5t/transfer-v1.jsonl` — 4 seed tasks
(paraphrase-invariance). All pass.
- Battery runners for **5S Syntax**, **5S Semantics**, and **5T
Transfer** are functional. Other 5S/5T sub-batteries are stubs
returning zero-task BatteryResults.
- Makefile targets `bench-5s`, `bench-5t`, `bench-5s5t`.
- Tests: `tests/test_bench_batteries.py` — 17 cases. Full suite:
1076 passed, 36 skipped.
### Phase 1b (deferred)
- Expand seed fixtures to 50-200 per sub-battery so signal floor
is meaningful (5pp tolerance over n≥3 runs).
- Implement Synthesis, Syllogism, Semiotics under 5S; Triangulate,
Timing, Transitivity, Truth under 5T.
### Phase 2 (deferred — depends on #000014 + #000017)
- 5R battery (React/Rearrange/Restore/Replicate/Resonate) over a
workspace abstraction. Depends on SelfModel + memory_root being
the workspace surface.
### Phase 3 (deferred)
- External-corpus expansion. Cross-substrate transfer testing.
Closure criterion (final): all three phases landed; full battery
suite runs as `make bench-5s5t5r`. Phase 1a alone is not closure;
ticket stays in-progress until Phase 1b lands.

View file

@ -0,0 +1,156 @@
"""5S/5T battery harness tests (ticket #000021 Phase 1a).
Covers:
- Fixture digest is stable across reads
- 5S Syntax produces a non-empty BatteryResult on seed fixtures
- 5S Semantics passes/fails as expected on seed fixtures
- 5T Transfer passes/fails as expected on seed fixtures
- Stub sub-batteries return zero-task results without error
- Runner produces JSON-serializable output
"""
from __future__ import annotations
import json
from dataclasses import asdict
from pathlib import Path
import pytest
from bench.batteries import b_5s, b_5t
from bench.batteries.base import (
BatteryResult,
fixture_digest,
fixture_meta,
iter_tasks,
)
REPO_ROOT = Path(__file__).resolve().parent.parent
SYNTAX_FX = REPO_ROOT / "bench" / "fixtures" / "5s" / "syntax-v1.jsonl"
SEM_FX = REPO_ROOT / "bench" / "fixtures" / "5s" / "semantics-v1.jsonl"
XFER_FX = REPO_ROOT / "bench" / "fixtures" / "5t" / "transfer-v1.jsonl"
def test_fixture_digest_stable():
a = fixture_digest(SYNTAX_FX)
b = fixture_digest(SYNTAX_FX)
assert a == b
assert len(a) == 64
def test_fixture_meta_round_trip():
m = fixture_meta(SYNTAX_FX)
assert m["battery"] == "5s"
assert m["sub_battery"] == "syntax"
assert m["version"] == "v1"
def test_iter_tasks_skips_meta():
tasks = list(iter_tasks(SYNTAX_FX))
assert all("_meta" not in t for t in tasks)
assert len(tasks) == 10
def test_5s_syntax_runs():
res = b_5s.run_syntax(SYNTAX_FX)
assert isinstance(res, BatteryResult)
assert res.battery == "5s"
assert res.sub_battery == "syntax"
assert res.pass_count + res.fail_count == 10
# All seed fixtures should pass — they're chosen for active π*'s.
assert res.pass_count == 10
assert res.metrics["parse_pass_rate"] == 1.0
assert len(res.runtime_digest) == 64
def test_5s_semantics_runs():
res = b_5s.run_semantics(SEM_FX)
assert res.battery == "5s"
assert res.sub_battery == "semantics"
# Seed fixtures are hand-curated to pass under current π*'s.
assert res.pass_count == res.pass_count + res.fail_count
assert res.metrics["equivalence_recovery_rate"] == 1.0
def test_5t_transfer_runs():
res = b_5t.run_transfer(XFER_FX)
assert res.battery == "5t"
assert res.sub_battery == "transfer"
assert res.pass_count + res.fail_count >= 1
@pytest.mark.parametrize(
"fn",
[
b_5s.run_synthesis,
b_5s.run_syllogism,
b_5s.run_semiotics,
b_5t.run_triangulate,
b_5t.run_timing,
b_5t.run_transitivity,
b_5t.run_truth,
],
)
def test_stub_sub_batteries_return_zero(fn, tmp_path):
# Stub takes any path; doesn't read it.
res = fn(tmp_path / "missing.jsonl")
assert res.pass_count == 0
assert res.fail_count == 0
def test_battery_result_is_json_serializable():
res = b_5s.run_syntax(SYNTAX_FX)
payload = json.dumps(asdict(res), default=str)
assert "syntax" in payload
def test_runner_main_smoke(tmp_path, capsys):
"""Runner produces JSON on stdout when --out is omitted."""
from bench.batteries.runner import main
rc = main(
[
"--battery",
"5s",
"--sub",
"syntax",
"--fixtures",
str(SYNTAX_FX),
]
)
out = capsys.readouterr().out
payload = json.loads(out)
assert payload["schema_version"] == "bench-result-v1"
assert payload["results"][0]["battery"] == "5s"
assert rc == 0
def test_runner_writes_to_file(tmp_path):
from bench.batteries.runner import main
out_path = tmp_path / "result.json"
rc = main(
[
"--battery",
"5s",
"--sub",
"semantics",
"--fixtures",
str(SEM_FX),
"--out",
str(out_path),
]
)
assert rc == 0
payload = json.loads(out_path.read_text())
assert payload["results"][0]["sub_battery"] == "semantics"
def test_runner_all_runs_phase_1a():
"""--all runs all three default fixtures and exits 0 when all pass."""
from bench.batteries.runner import main
rc = main(["--all"])
# All seed fixtures are constructed to pass; rc=0 expected.
assert rc == 0