arborist/docs/benchmarks.md
russell@unturf.com 9d0015b4d4
feat(#000060): bench/jaggedness.py — deterministic retrieval jaggedness instrument
v1 of the same-model substrate-delta harness's non-jagged metric.
For one corpus title, surface-perturb its question (numeral / accent /
hyphen / honorific / amp / brit) preserving the referent, then ask
whether retrieval surfaces the SAME target for canonical vs perturbed
phrasing. J_norm = XOR disagreement rate @k (lower = less jagged);
graded mean |Δrank| catches rank instability the binary metric misses.

Pure query --dry-run: no LLM, no verifier, no judge, no n=3 noise, no
5pp floor — the recall_at_k discipline. Reuses recall_at_k.probe +
mine_questions._surface_variant. Feeds #000012 ForkScore
ΔJaggednessReduction. A-vs-C answer-quality arm already exists under
#000057 (control_ab/control_sweep) — not rebuilt. Curvature + LLM-arm
jaggedness delta remain open (ticket §8).

make bench-jaggedness JAGGED_LIMIT=40 JAGGED_K=8
2026-05-21 08:38:07 -04:00

375 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Benchmarks — orientation, harnesses, fixtures, discipline
Arborist ships two bench harnesses, four question fixtures, and a
signal-floor discipline pinned across the substrate. This doc is
the canonical entry point. Three companion docs cover specifics:
| File | Purpose |
|----------------------------------------|--------------------------------------------|
| `docs/bench-maxing.md` | Discipline rules (5pp signal floor, etc.) |
| `docs/qa-modes-bench.md` | Historical journal — running addendum log |
| `docs/bench-emergent-design.md` | Random-word stress-test design rationale |
| `docs/qa-modes-bench-2026-04-30.md` | Frozen point-in-time bench (Apr-30 baseline) |
Read those when you need the why or the long-form data. This file
covers the what, where, and how.
## 1. Two harnesses, two purposes
### 1.1 `bench/qa_sweep.py` — curated QA-quality bench
Runs a fixture file × answer modes × n samples. Persists a JSONL
of per-row results plus a markdown summary. Used for:
- **Regression checks** before merging substrate changes.
- **A/B cycles** on policy knobs (cap on/off, reminder on/off,
preflight enabled/disabled).
- **Per-mode strict-rate tracking** over time.
Run via `make bench-qa` or `bench/qa_sweep.py` directly. Bench
rows carry ~25 fields per cell, including the four-rung label
(`audit_mode`), the verifier-method, the violation kinds, the
preflight QuestionState projection (`preflight_logical_statuses`,
`preflight_question_shape`, etc.), the quantifier classifier
output (`quantifier_intensity`, `scope_bound_hint`,
`claim_cap_applied`), the preflight stage hash 12-char prefix
(#000009 §7.2), and a per-stage timing breakdown
(`preflight_ms`, `soft_preflight_ms`, `search_ms`, `context_ms`,
`llm_ms`, `persist_ms`, `total_ms`).
### 1.2 `scripts/bench_emergent.py` — random-word stress test
Picks three random words from `/usr/share/dict/words`, asks
Hermes (at temp 0.8) to weave them into a creative question
paragraph, then sends that paragraph to arborist. Logs every cycle
to `bench/emergent_log.jsonl` as one JSONL line. Designed for
**blue-moon cadence**, not every-commit benching — the
combinatoric word space surfaces failure shapes the curated bench
doesn't reach.
The teacher review step is intentionally **not automated.** Fox
brings interesting log entries to a teacher model (Claude Opus
4.7 in the current setup) and asks for guidance: did the answer
match the question? Novelty class
(known_truth_grounding / emergent_synthesis / novel_claim /
no_signal)? Which hyperparam to tune next?
Run via `make bench-emergent` (default `EMERGENT_N=10`) or
`make bench-emergent EMERGENT_N=100` for a longer cycle. See
`docs/bench-emergent-design.md` for the design rationale.
## 2. Four question fixtures
```
bench/qa_questions.txt — 75 questions
bench/qa_questions_smoke.txt — smoke test
bench/qa_questions_quantifier_subset.txt — 9 questions
bench/qa_questions_quantifier_baseline.txt — 1 question
bench/qa_questions_metacog_subset.txt — 28 questions
```
| Fixture | Use case | Cell size at n=3 × 3 modes |
|--------------------------------------|---------------------------------------------------------------------------------------------|----------------------------|
| `qa_questions.txt` | Full regression bench. Run before merging substrate changes. Wide coverage + corner cases. | 675 runs, ~3-3.5h |
| `qa_questions_smoke.txt` | Smoke test on 1-2 questions. Use to confirm a CLI flag wires through correctly. | 3-6 runs, ~30s |
| `qa_questions_quantifier_subset.txt` | 9 broad-quantifier questions (7 unbounded + 2 bounded). Use for A/B cycles on #000008 knobs. | 81 runs, ~7-15min |
| `qa_questions_quantifier_baseline.txt` | Single broad question for narrow n=3 baseline measurement. | 9 runs, ~2-4min |
| `qa_questions_metacog_subset.txt` | 28 questions targeting each #000010 detector (temporal, contradiction, false-premise, out-of-corpus) plus mixed multi-trigger + well-formed controls. | 252 runs, ~25-35min |
When choosing a fixture: ask whether you're measuring the
**substrate as a whole** (full bench), a **specific feature**
(broad-quantifier subset, metacog subset), or just **wiring
correctness** (smoke).
## 3. The signal floor — n=3 × 9 = 27 sample minimum, 5pp delta floor
Per `docs/bench-maxing.md`:
- **5pp floor** for STRICT-rate deltas at n=3 × any-fixture sample
size. Differences below this are Hermes nondeterminism, not
signal. Quote-mode often jitters ±10pp on 27-sample cells.
- **Mean-ratio + UNGROUNDED-rate** are tighter signals at the same
sample size — they aggregate per-row values rather than
counting discrete verdicts.
- **n=5 verification** when an A/B cell sits within the noise
band (e.g. 0.30 vs 0.33 STRICT-rate is 3pp — within noise; n=5
on the closer cell tightens the variance).
- **vLLM concurrency saturates at c=3-4.** Going higher slows
per-call latency without parallelizing harder. Default `c=4`.
Don't tune to a single bench cycle. Always have a baseline +
an A/B cell + (sometimes) a verification cell.
## 4. Make targets cheat sheet
```
# Curated QA bench (default: bench/qa_questions.txt × 3 modes × n=3)
make bench-qa
# Same but with limit / different file
.venv/bin/python bench/qa_sweep.py \
--questions bench/qa_questions_quantifier_subset.txt \
--shards-dir ~/.arborist/shards --n 3 --concurrency 4 \
--out-dir bench/qa_results
# A/B cycle: flip a policy knob for one bench cell
.venv/bin/python bench/qa_sweep.py \
--questions bench/qa_questions_quantifier_subset.txt \
--shards-dir ~/.arborist/shards --n 3 --concurrency 4 \
--policy quantifier_guard_apply_caps=true \
--policy quantifier_reminder_enabled=true
# Resume an interrupted bench (same --seed, append to existing JSONL)
.venv/bin/python bench/qa_sweep.py \
--resume bench/qa_results/2026-05-03T19-30-13Z.jsonl \
--seed 0
# Smoke test — quick sanity of the wiring
.venv/bin/python bench/qa_sweep.py \
--questions bench/qa_questions_smoke.txt --n 1 --concurrency 1
# Random-word stress test (blue-moon cadence)
make bench-emergent # n=10 default
make bench-emergent EMERGENT_N=100
```
## 5. The bench-row schema
Every row in `bench/qa_results/<stamp>.jsonl` carries (current
shape — fields are additive across versions):
**Identity & verdict:**
- `question`, `answer_mode`, `audit_mode`, `verifier_method`
- `n_quotes`, `n_verified`, `ratio`
- `cache_key` (12-char prefix)
- `preflight_hash` (12-char prefix; see #000009 §7.2)
- `model_profile_id`
**Diagnostics:**
- `format_collapsed` (pointer-mode only signal; #000008)
- `violation_kinds` (sorted list of unique kinds)
- `lazy_anchor_ratio`, `pointer_id_distribution`
- `answer_brackets`, `answer_pointer_count`,
`answer_chars_with_brackets`, `raw_meaningful_line_count`
- `deflection_kind`, `subject_anchor`, `subject_in_answer`
- `metaphor_deflection_kind`, `metaphor_cue_count`,
`metaphor_overlap_count`
**Preflight projection (#000010):**
- `preflight_logical_statuses` (list)
- `preflight_question_shape`, `preflight_result`
- `preflight_temporal_sensitivity`
- `preflight_has_false_premise`, `preflight_has_contradiction`
- `preflight_corpus_requirement`
**Quantifier classifier (#000008):**
- `quantifier_intensity`, `quantifier_matched_token`
- `scope_bound_hint`, `quantifier_explicit_count`
- `claim_cap_applied`
**Capacity:**
- `prompt_chars_total`, `prompt_chars_evidence`,
`prompt_chars_system`, `prompt_chars_question`
- `answer_chars`
**Directive compliance** (per-row pass/fail for the seven-point
program directives observable from a single bench row):
- `directive_compliance` dict keyed on D2/D3/D4/D6/D7
**Time:**
- `elapsed_s`, `iso_ts`, `error`
The markdown summary alongside the JSONL renders aggregate views
(per-mode strict-rate, format-collapse + violation kinds table,
strict-rate-by-prompt-size buckets, directive coverage).
## 6. The journal — where headlines live
`docs/qa-modes-bench.md` is the **rolling research log** for the
QA bench. Each substantial bench cycle adds a new addendum (or
refines an existing one). The 2026-05-02 baseline is the frozen
authoritative state of the substrate before #000008/9/10/11
landed; subsequent addenda compare against it.
Current addenda:
- **2026-05-02 baseline** — pre-preflight authoritative state.
- **Addendum 1** (broad-quantifier A/B from #000008 §12.6).
- **Addendum 2** (preflight on vs off, 9-question broad subset).
- **Addendum 3** (full 75-question regression check post-flip).
- **Addendum 4** (metacog-trigger detector validation, 6 → 28
question fixture).
When a new bench cycle produces a substantive finding, add an
addendum or refine an existing one. Bench artifacts (JSONL +
markdown) live in `bench/qa_results/` and are gitignored — only
headlines + cross-references go in the journal.
Per-ticket bench data lives in the relevant ticket file's `§12`
or `§13` (e.g. `docs/tickets/ticket-000008-...md` §12 carries the
four-cell A/B; #000010 §13 has three sub-validations; #000006 has
the rolling-amend pattern across multiple cycle counts).
## 7. The bench-emergent log
`bench/emergent_log.jsonl` is a long-running append-only log of
random-word stress cycles. Each entry has:
```json
{
"ts": <unix>, "iso_ts": "2026-05-04T...",
"words": ["dismally", "heats", "Jamaicans"],
"question": "<model-generated question paragraph>",
"answer": "<arborist's answer>",
"audit_mode": "STRICT|HYBRID|UNGROUNDED",
"verifier_method": "claim_lattice",
"n_quotes": 3, "n_verified": 0,
"violation_kinds": ["CITATION_MISMATCH", "TOO_MANY_EVIDENCE_IDS"],
"metaphor_deflection_kind": "no_signal",
"metaphor_cue_count": 2, "metaphor_overlap_count": 2,
"sources": [{"title": "...", "uri": "...", "used": false}, ...],
"answer_seconds": <float>, "total_seconds": <float>,
"teacher": <reserved for human review>
}
```
Findings live in `docs/tickets/ticket-000006-bench-emergent-findings.md`
as rolling amends — each new cycle batch produces a new amend with
the verdict shift, violation profile, and tuning candidates.
## 8. How to run a focused A/B
Pattern from #000008 §12 (broad-quantifier four-cell A/B):
1. **Pick a fixture** matching the feature under test
(broad-subset for quantifier work; metacog-subset for
metacognition work; smoke for wiring sanity).
2. **Run the baseline** — same fixture, default policy.
3. **Run the cell** — same fixture, one policy knob flipped via
`--policy KEY=VALUE`. Repeat for each independent knob.
4. **Compare** — STRICT-rate, mean-ratio, UNGROUNDED-rate,
FORMAT_COLLAPSED rate, dominant violation kinds. Apply the
5pp floor to STRICT-rate; mean-ratio and UNGROUNDED tend to
be tighter.
5. **Verify with n=5** if a cell sits within the noise band.
6. **Document** — add an addendum to `docs/qa-modes-bench.md` or
a new `§12.x` / `§13.x` to the relevant ticket file.
The four-cell A/B in #000008 (preflight off / reminder only / cap
only / cap+reminder) is the canonical pattern for measuring
multi-knob features.
## 9. How to interpret results
**STRICT-rate** = the fraction of cells where the verifier could
prove every claim grounded against cited evidence. Most direct
quality metric, but noisy at small sample sizes.
**HYBRID-rate** = mixed verdicts (some claims grounded, some
not). Often more informative than STRICT-rate when a feature's
job is to rescue UNGROUNDED → HYBRID rather than HYBRID → STRICT.
**UNGROUNDED-rate** = honest "the verifier could not ground."
Lower is generally better, but UNGROUNDED on questions the corpus
genuinely can't answer is the *correct* outcome — the substrate
preferring UNGROUNDED to fabricated STRICT is the architectural
win named in `docs/seven-point-program.md` D7.
**Mean ratio** = mean of `n_verified / n_quotes` across rows.
Aggregates per-row values rather than counting verdicts; tighter
at small sample sizes than STRICT-rate.
**FORMAT_COLLAPSED rate** = pointer-mode-specific gauge of
whether the model is following the `[E\d+]` citation protocol.
Zero is the goal; non-zero means the model emitted free prose
without pointer tags.
**Violation kind distribution** = per-mode tallies of which hard
checks fired most often. Gives the operator a per-failure-mode
view of the substrate's weaknesses.
**Directive compliance** = per-row pass/fail for the
seven-point-program directives (D2/D3/D4/D6/D7). Substrate-level
hygiene check; should be ~100% on lattice modes.
**Audit-line tails** (rendered by
`arborist/cli.py:_render_warrant_tail`):
- Hard tails: `· warrant missing`, `· title mismatch`,
`· format collapsed`, `· broad cap N`, `· broad rejected`,
`· broad unbounded`, `· broad runaway`, `· false premise`,
`· contradictory`, `· stale risk`, `· out of corpus`,
`· frame ambiguous`.
- Soft tails (advisory): `· soft: <label>` (#000011 sidecar
output; SOFT_DISABLED / SOFT_PARSE_FAIL / SOFT_WELL_FORMED
suppress).
A row with multiple tails composes them: `EVIDENCE-WARRANTED ·
via claim_lattice · false premise · soft: time sensitive` is a
verdict the substrate reached but with two distinct preflight
warnings worth the operator's attention.
## 10. Operator commands cheat sheet
```bash
# Inspect preflight stage payload from any cache row
arborist providence --show-preflight CACHE_KEY_PREFIX
# Single query with all preflight knobs flipped
arborist query "winners of all major sports?" \
--apply-quantifier-caps --reject-broad --soft-preflight
# Dry-run — assemble context but skip LLM
arborist query "..." --dry-run
# Force fresh inference (delete prior cache row first)
arborist query "..." --burn
# Show raw JSON record instead of human render
arborist query "..." --json
```
The `Makefile` exposes most of these via shorthand:
```
make query Q="..." BROAD=1 REJECT_BROAD=1 ALLOW_BROAD=1 \
BURN=1 JSON=1 ANSWER_MODE=claim_lattice
```
See the `Makefile` directly for the full set.
## 11. Deterministic retrieval instruments (no-LLM)
The bench-maxing rule (CLAUDE.md): when a lever's failure class is
below the n=3 / 5pp noise floor, fix the instrument — mine
ground-truth-carrying questions and grade by deterministic retrieval,
not by the LLM verifier. These instruments take `query --dry-run`
output only: no LLM, no verifier, no judge, no noise floor, scalable
to the corpus. They measure *retrieval surfacing*
necessary-not-sufficient for STRICT, and they **complement, never
replace**, the curated adversarial set (the verifier-honesty gate).
- **`bench/mine_questions.py`** — mines questions whose target article
is known by construction (surface-variant of a corpus title).
Classes: `numeral` (Roman↔ordinal), `accent`, `hyphen`,
`honorific` (Mt↔Mount), `amp` (&↔and), `brit` (US↔UK spelling),
`stale` (temporal fabrication-bait, control_ab only).
- **`bench/recall_at_k.py`** — recall@1/@3/@k of the known target on
a mined fixture. Returns rank, so recall at every k is free from one
retrieval (a too-lenient k hides a rank-only lift — report @1/@3/@k).
- **`bench/jaggedness.py`** (`make bench-jaggedness`, #000060) — does
retrieval surface the SAME target for a question's *canonical*
phrasing (title verbatim) AND its *surface-perturbed* phrasing? A
non-jagged retriever agrees; a jagged one finds "Henry VIII" but
misses "Henry the eighth". Reuses `recall_at_k.probe` +
`mine_questions._surface_variant`.
- `J_norm` (binary) = fraction of titles where canonical & perturbed
DISAGREE on surfacing the target @k (XOR). Lower = more non-jagged.
- graded mean `|Δrank|` over titles where both surfaced — rank
instability the binary metric can't see.
- Distinguishes a true recall-miss (both phrasings miss = not
jagged) from jaggedness (one surfaces, one doesn't). Feeds
#000012's ForkScore `ΔJaggednessReduction`: a retrieval fold that
lowers `J_norm` without lowering recall is a non-jagged win.
- The A-vs-C *answer-quality* jaggedness delta (does the substrate
make answers more consistent across variants than the bare model?)
is the LLM+judge version — rides `bench/control_sweep.py` over
variant groups, gated v2.