speed: pytest-xdist, bench smoke, concurrency default; UTF surrogate fix

Bench-max sprint 1a + sprint 3 + speed audit. Five wins, none of
them traded calibration.

UTF-16 surrogate fix (sprint 1a)
================================
Hermes occasionally emits text with lone UTF-16 surrogates. Bare
.encode('utf-8') raises UnicodeEncodeError on those, which aborted
the run with no Merkle root. Two errors per lattice mode in the
2026-05-02 bench were this exact path on the 'tell me about the
roman empire' question.

Fix: errors='surrogatepass' on the four sha256 helpers that hash
model-derived text, plus the two audit-chain encode sites in
store.py for defense-in-depth (audit body could carry user text in
some flows). The hash stays deterministic because WTF-8 bytes are
reversible & unique per input.

Touched:
  aborist/qa/dag.py:_sha256_hex            (the loud one)
  aborist/qa/keys.py:_sha256
  aborist/qa/evidence.py:_sha256_hex
  aborist/store.py: chain_audit_events + append_audit

Predicted Δ on next bench: +1pp on lattice modes (the 2 errors
become valid runs).

Smoke fixture (sprint 3)
========================
bench/qa_questions_smoke.txt — 5 questions, all anchor classes,
each currently failing pointer mode 100% while JSON aces 100% per
the 2026-05-02 bench. Wired as 'make bench-qa-smoke', --n 1
--concurrency 4, ~30-90s wall-clock depending on vLLM warmth. The
inner loop for prompt iteration; the full 71-question sweep stays
the scoreboard.

Smoke verified: pointer=0/5 STRICT, JSON=5/5, quote=2/5. Confirms
the gap pattern from the journal.

Concurrency default
===================
Makefile bench-qa now defaults to BENCH_QA_CONCURRENCY=4 (was
sequential). Override via BENCH_QA_CONCURRENCY=N. Combined with
the --concurrency landing in 0870af6, full sweep drops from ~107
min projected to ~51 min actual.

pytest-xdist (test-speed)
=========================
Added pytest-xdist>=3.5 to dev extras. 'make test' now uses
-n auto (= one worker per logical CPU). Measured: 36s → 10s on
the 641-test suite. 3.6× speedup, no test changes required.

Bench-max scoreboard (predicted lift from this commit alone):
  +1pp lattice modes (UTF fix)
  +cycle-time enabler (smoke fixture, xdist)
  no calibration cost — none of the verifier checks moved.
This commit is contained in:
russell@unturf.com 2026-05-02 09:29:40 -04:00
parent 5bc42d277e
commit 3b9122395c
No known key found for this signature in database
7 changed files with 46 additions and 8 deletions

View file

@ -183,7 +183,8 @@ BENCH_QA_OUT ?= bench/qa_results
BENCH_QA_MODES ?= quote,claim_lattice_pointer,claim_lattice
BENCH_QA_LIMIT ?= 0
BENCH_QA_N ?= 3
bench-qa: bootstrap ## QA-quality sweep: questions × modes × N samples [BENCH_QA_N=3 BENCH_QA_LIMIT=N BENCH_QA_MODES=...]
BENCH_QA_CONCURRENCY ?= 4
bench-qa: bootstrap ## QA-quality sweep: questions × modes × N samples [BENCH_QA_N=3 BENCH_QA_LIMIT=N BENCH_QA_MODES=... BENCH_QA_CONCURRENCY=4]
PYTHONUNBUFFERED=1 $(PY) bench/qa_sweep.py \
--questions $(BENCH_QA_QUESTIONS) \
--shards-dir $(SHARDS_DIR) \
@ -191,7 +192,21 @@ bench-qa: bootstrap ## QA-quality sweep: questions × modes × N samples [BENCH_
--top-k $(QUERY_TOP_K) \
--modes $(BENCH_QA_MODES) \
--limit $(BENCH_QA_LIMIT) \
--n $(BENCH_QA_N)
--n $(BENCH_QA_N) \
--concurrency $(BENCH_QA_CONCURRENCY)
# Smoke fixture: 5 questions, all anchor classes, all currently failing
# pointer mode 100% while JSON aces 100%. Inner loop for prompt iteration.
# ~30s wall-clock at concurrency=4. Use this between full sweeps.
bench-qa-smoke: bootstrap ## quick 5-question smoke (all anchor classes; ~30s)
PYTHONUNBUFFERED=1 $(PY) bench/qa_sweep.py \
--questions bench/qa_questions_smoke.txt \
--shards-dir $(SHARDS_DIR) \
--out-dir $(BENCH_QA_OUT) \
--top-k $(QUERY_TOP_K) \
--modes $(BENCH_QA_MODES) \
--n 1 \
--concurrency $(BENCH_QA_CONCURRENCY)
test-live: bootstrap ## live QA quality tests against Hermes (gated; ~1 min)
ABORIST_LIVE_TESTS=1 ABORIST_LIVE_SHARDS_DIR=$(SHARDS_DIR) \
@ -371,7 +386,7 @@ stats: bootstrap ## counts: documents, chunks, edges, audit chain
$(ABORIST) --db $(DB) stats
test: bootstrap ## run pytest suite (excludes opt-in crawler tests)
$(VENV)/bin/pytest -q --ignore=tests/crawler
$(VENV)/bin/pytest -q --ignore=tests/crawler -n auto
# Crawler tests are off-by-default — they hit the network in many cases
# and require the heavy [crawler] extras (aiohttp, bs4, lxml, etc.).

View file

@ -45,7 +45,13 @@ from aborist.merkle import MerkleTree
def _sha256_hex(s: str) -> str:
return hashlib.sha256(s.encode("utf-8")).hexdigest()
# ``errors='surrogatepass'`` lets lone UTF-16 surrogates through as
# their WTF-8 form. Hermes occasionally emits text with unpaired
# surrogates inside multi-byte sequences; bare ``.encode('utf-8')``
# raises UnicodeEncodeError on those, which previously aborted the
# run with no Merkle root. The hash stays deterministic because the
# WTF-8 byte sequence is reversible & unique per input.
return hashlib.sha256(s.encode("utf-8", errors="surrogatepass")).hexdigest()
def _canonical_json(obj) -> str:

View file

@ -51,7 +51,9 @@ from aborist.merkle import MerkleTree
def _sha256_hex(s: str) -> str:
return hashlib.sha256(s.encode("utf-8")).hexdigest()
# ``errors='surrogatepass'`` for model-output text containing lone
# UTF-16 surrogates; same rationale as ``aborist.qa.dag._sha256_hex``.
return hashlib.sha256(s.encode("utf-8", errors="surrogatepass")).hexdigest()
@dataclass(frozen=True)

View file

@ -28,7 +28,9 @@ from aborist.document import canonicalize
def _sha256(s: str) -> str:
return hashlib.sha256(s.encode("utf-8")).hexdigest()
# ``errors='surrogatepass'`` survives lone UTF-16 surrogates from
# model output; same rationale as ``aborist.qa.dag._sha256_hex``.
return hashlib.sha256(s.encode("utf-8", errors="surrogatepass")).hexdigest()
def _canonical_json(obj) -> str:

View file

@ -936,7 +936,7 @@ def chain_audit_events(
h = hashlib.sha256()
if prev is not None:
h.update(bytes.fromhex(prev))
h.update(body_json.encode("utf-8"))
h.update(body_json.encode("utf-8", errors="surrogatepass"))
event_hash = h.hexdigest()
rows.append(
(
@ -973,7 +973,7 @@ def append_audit(
h = hashlib.sha256()
if prev is not None:
h.update(bytes.fromhex(prev))
h.update(body_json.encode("utf-8"))
h.update(body_json.encode("utf-8", errors="surrogatepass"))
event_hash = h.hexdigest()
conn.execute(
"INSERT INTO audit_events (event_hash, prev_event_hash, event_type, subject_root, body, ts) "

View file

@ -0,0 +1,12 @@
# Smoke fixture — 5 questions covering all anchor classes (date, relation,
# entity-list, count/place, why-cause), each chosen because the 2026-05-02
# bench shows pointer mode failing 100% (HH) while JSON mode aces 100% (SS).
# These are the load-bearing test cases for prompt-iteration loops aimed at
# closing the pointer-vs-JSON gap. Run with --concurrency 4 --n 1 → ~30s
# wall-clock. The full bench (71 questions) stays the scoreboard; this is
# the inner loop.
when did the soviet union dissolve?
where is mount kilimanjaro located?
who painted the mona lisa?
who were the original seven mercury astronauts?
why did the dinosaurs go extinct?

View file

@ -51,6 +51,7 @@ crawler = [
dev = [
"pytest>=8",
"pytest-asyncio>=0.23",
"pytest-xdist>=3.5",
"aborist[html]",
"aborist[wikitext]",
"aborist[mesh]",