arborist/docs/qa-modes-bench.md
russell@unturf.com 7bb11ed62f
#000048 step 2.4 — parse_pointer_claims clause segmentation
Closes the 8 mis-segments #000046 left in formulate-hard-v1.jsonl.
The parser was line/bullet-only — one line ⇒ one claim — so a line
that crammed several pointered claims onto one row ("Water is wet
[E1]; fire is hot [E2]", "X happened [E1]. Y followed [E2]") became
one monolithic claim with all the pointers, and a wrapped bullet
became two.

arborist/qa/parse_claims.py: _SEGMENT_SEP_RE splits a line on ';',
sentence boundaries ('. '/'! '/'? ' then a Capital), spaced dashes
(' - '/' — '/' – '), ' and '/' or '/' because '/' although '/' since
'/' while ', inline '(N)' enumeration markers, and commas — with
'(?![^\[]*\])' so a comma inside a [E1, E2] bracket never splits it.
_segment_line keeps the split ONLY IF every resulting non-empty
segment is a well-pointered claim — a legit single claim ("The cat
is black and white [E1].", "The cast: A, B, C [E1].") is never
broken because splitting it would manufacture pointer-less prose
fragments → guard rejects; a leading colon-terminated header with no
pointer ("Two facts:", "Key points:") is allowed and dropped. Plus a
wrapped-bullet join: a continuation line (leading whitespace then a
lowercase letter, no bullet glyph) folds its text + pointers into the
previous claim.

Effect: formulate-hard rate 4/12 → 12/12 (the pack is now at ceiling
— a harder Formulate tier would re-open below-ceiling headroom; a
#000046 follow-up). Remaining #000048 headroom: 2 STRICT_PARAPHRASE
recombinations in falsification-hard (Mercury, Einstein — step 2.2).

Bench gate: make bench-qa (n=3 × 75 × 3 = 675 cells; parse_pointer_claims
feeds the 450 claim_lattice_pointer + claim_lattice cells) after
(bench/qa_results/2026-05-11T20-26-37Z) vs the pre-step-2.4 baseline
(...T17-12-41Z = HEAD's parse_claims.py). STRICT-rate quote 0.54→0.55,
pointer 0.22→0.22, lattice 0.43→0.45 — all within the 5-pp noise
floor. Per-row diff: the segmenter changed the parsed-claim count on
the SAME answer text for 7 of the 450 lattice cells (0 in
claim_lattice, 7 in claim_lattice_pointer); of those, 2 caused an
audit_mode change — both correct: a wrap-join recovered an answer's
intended structure (4 claims, 2 pointer-less wrap-fragments → HYBRID)
into 2 well-pointered claims → STRICT; and a crammed-one-line blob (1
monolithic claim, all pointers → STRICT) split into 8 claims, some
not individually verifying → HYBRID (the honest verdict — false-
positive STRICT was the corruption). Every other lattice/quote delta
is LLM re-answer variance. No regression — the segmenter's only
visible effects on real traffic are honest improvements. Summarized
in qa-modes-bench.md Addendum 7 + ticket-000048 §5 step 2.4.

Tests: 8 new in test_claim_lattice.py (semicolon/sentence/conjunction
splits; pointerless-fragment + cast-list guards; leading-colon-header
drop; wrapped-bullet join; pointer-order/multi-pointer); existing
parse_pointer_claims tests pass untouched; test_5f_formulate_hard_pack
re-pinned 4/12 → 12/12. make test 2358 passed, 28 skipped.

#000048 → steps 2.1 + 2.4 landed; #000046 / #000012 §8 / TICKETS.md /
Makefile / fixture _meta + notes updated.
2026-05-11 17:09:06 -04:00

531 lines
30 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.

# QA-modes bench — 2026-05-02
**Date:** 2026-05-02
**Endpoint:** `https://hermes.ai.unturf.com/v1` (Hermes-3-Llama-3.1-8B-FP8-Dynamic, vLLM, 82K ctx)
**Corpus:** Wikipedia 2003-05-16 cur snapshot, sharded under `~/.arborist/shards`
Two sweeps landed today, each 71 questions × 3 modes:
| stamp | samples | concurrency | scheduling | runs | wall-clock |
|-------|---------|-------------|------------|------|------------|
| `11:31Z` | n=2 | c=4 | cell-grouped | 426 | 51 min |
| `15:07Z` | n=3 | c=4 | sample-shuffled, `--seed 0` | 639 | **48 min** |
The 15:07Z sweep is the **authoritative state of the substrate** at end-of-day; the 11:31Z sweep is the intermediate witnessed before the second wave of hardening landed. Headlines below show both.
## Hardening since the prior bench (2026-04-30 post-retry)
Substrate-side (between `2026-04-30T18:46Z` and `2026-05-02T11:31Z`):
- Rule 8 — title-relevance promoted to hard verifier check (`TITLE_MISMATCH` violation demotes `STRICT → HYBRID` when no cited evidence's source title shares a stemmed token with the claim).
- Anchor-class warrant generalized to entity-list / count / why-cause shapes (Ticket #000003).
- Retrieval-plan hash bound into the run-DAG retrieval stage (Ticket #000001).
- Reference-frame polarity contract (Ticket #000002).
- Four-rung ladder display (Ticket #000005) — `POINTER-LINKED → ANCHOR-WARRANTED → EVIDENCE-WARRANTED → (ENTAILMENT-VERIFIED reserved)` mapped from the v9.8 audit_mode trichotomy at render time.
- Bench harness extended with directive coverage + log-scale buckets to 1M (Ticket #000004).
Substrate-side (between `2026-05-02T11:31Z` and `2026-05-02T15:07Z`):
- **Sprint 1b** — per-mode `max_context_chars`. Bench's recommended-context-budget table flows back into `DEFAULT_QUERY_POLICY["max_context_chars_by_mode"]`: quote 24 KB, pointer 24 KB, JSON 48 KB. Folds into `governance_policy_hash`.
- **Sprint 2** — pointer Rule 9: chunk-specificity instruction added to the lattice-pointer system prompt.
- **DRY collapse** — the four lattice prompts (system + grounding × pointer + JSON) lifted to `arborist/qa/prompts.py` as a single source of truth, imported by both `runner.DEFAULT_POLICY` and `query.DEFAULT_QUERY_POLICY`.
- **httpx persistent client** — the chat-completion path used to construct a fresh `httpx.Client` per call, paying a TLS handshake every request. Move to `__init__`; HTTP/1.1 keep-alive across calls. Save 1-2 min on a 426-call bench.
- **Sample-level shuffled bench scheduling** — every `(question, mode, sample_idx)` is a task, shuffled with `--seed`, dispatched concurrently. Per-cell `Lock` dict serializes burn-then-write on the shared cache_key. True i.i.d. n=3 variance; vLLM batcher fed a diverse request stream.
- **Bench `--resume`** — read existing JSONL, skip done tasks, append fresh rows. Stop/start-able.
- **Concurrency sweep on the smoke fixture.** vLLM peaks at c=3-4; saturates badly past c=4 (see Wall-clock & throughput below).
- **UTF-16 surrogate fix v2** (`41d1d9b`) — corpus chunks with lone surrogates broke httpx's outbound JSON encode. Scrub at the client boundary by WTF-8 → UTF-8-with-replace roundtrip. (The earlier v1 fix `3b91223` hardened the OUTPUT side — SHA-256 hashers — but missed the INPUT side. Both paths now safe.)
## Aggregate (15:07Z, authoritative)
| mode | runs | STRICT | HYBRID | UNGROUNDED | err | strict-rate | mean ratio | mean latency |
|------|------|--------|--------|------------|-----|-------------|------------|--------------|
| `quote` | 213 | 116 | 51 | 46 | 0 | **0.54** | 0.699 | **16.9 s** |
| `claim_lattice_pointer` | 213 | 43 | 146 | 21 | 3 | **0.20** | 0.642 | 18.5 s |
| `claim_lattice` (JSON) | 213 | 89 | 82 | 39 | 3 | **0.42** | 0.698 | 18.3 s |
## Δ across the day
| mode | 2026-04-30 (post-retry) | 2026-05-02T11:31Z | 2026-05-02T15:07Z | net Δ |
|------|------------------------|-------------------|-------------------|-------|
| `quote` | 0.47 | 0.50 (+3pp) | **0.54** (+4pp) | **+7pp** |
| `claim_lattice_pointer` | 0.24 | 0.23 (1pp) | 0.20 (3pp) | 4pp |
| `claim_lattice` (JSON) | 0.50 | 0.44 (6pp) | 0.42 (2pp) | 8pp |
**Quote climbed the most across the day**, ending at 0.54. The Sprint 1b 24 KB cap surfaces tighter retrievals that quote can ground verbatim (the bucket data confirms — quote's peak migrated to 8-16 KB at 0.58 strict-rate).
**Pointer & JSON each took an honesty cost.** Rule 8 promotion + warrant-class generalization demote more cases that *would* have classified STRICT under the looser pre-2026-05-02 verifier. The architectural win — 99% directive coverage on lattice modes — is the price for those drops; false-positive STRICT was corruption, and we converted it to honest HYBRID.
## Per-bucket strict-rate (15:07Z)
| mode | bucket | runs | strict-rate | note |
|------|--------|------|-------------|------|
| `quote` | 8-16 KB | 105 | **0.58** | peak |
| `quote` | 16-32 KB | 105 | 0.52 | |
| `claim_lattice_pointer` | 16-32 KB | 210 | **0.20** | peak (only bucket) |
| `claim_lattice` (JSON) | 32-64 KB | 99 | **0.48** | peak |
| `claim_lattice` (JSON) | 16-32 KB | 108 | 0.38 | |
**Sprint 1b's intent confirmed.** JSON's peak at 32-64 KB justifies the 48 KB default. Quote's peak migrated to 8-16 KB after the 24 KB cap; the cap may be tighter than optimal — quote could plausibly be cut to 16 KB (mid of 8-16 KB bucket) for another small lift in a follow-up sprint.
`claim_lattice` (JSON) **peaks at LARGER context** than the other modes. The structured per-claim evidence linkage that JSON enforces benefits from more evidence per claim. Pointer & quote degrade past 16-32 KB.
## Recommended context budget (per-mode)
5-run minimum to reduce noise; from the 15:07Z bucket data:
| mode | peak bucket | strict-rate | n |
|------|-------------|-------------|---|
| `quote` | 8-16 KB | 0.58 | 105 |
| `claim_lattice_pointer` | 16-32 KB | 0.20 | 210 |
| `claim_lattice` (JSON) | **32-64 KB** | **0.48** | 99 |
These flow into `DEFAULT_QUERY_POLICY["max_context_chars_by_mode"]` (currently 24 / 24 / 48 KB — mid of each peak bucket). When the bench shifts those peaks, retune the policy and let `governance_policy_hash` partition the cache.
## Directive coverage (seven-point program)
| mode | D2 pointer | D3 cti-ready | D4 ev-map bound | D6 warrant | D7 honest label |
|------|-----------|--------------|-----------------|-----------|----------------|
| `quote` | 0/213 (0%) | 0/213 (0%) | 213/213 (100%) | 0/213 (0%) | 213/213 (100%) |
| `claim_lattice_pointer` | 210/213 (99%) | 210/213 (99%) | 210/213 (99%) | 210/213 (99%) | 210/213 (99%) |
| `claim_lattice` | 210/213 (99%) | 210/213 (99%) | 210/213 (99%) | 210/213 (99%) | 210/213 (99%) |
Lattice modes hit **99% on every observable directive**; the 1% gap is the 6 surrogate errors (already fixed in `41d1d9b`, next bench will hit 100%). Quote shows 0% on D2 / D3 / D6 by construction (those are lattice-only directives) and 100% on D4 / D7.
## Pointer-mode signal — where the gap lives
Pointer mode's strict-rate did not lift on Sprint 2's chunk-specificity prompt nudge. The load-bearing failure pattern is **lazy-anchoring**: the model cites a topic-overview chunk for every claim instead of the specific chunk that supports each individual claim. Lazy-anchor ratio histogram (11:31Z bench, 142 pointer rows):
| `lazy_anchor_ratio` bucket | rows |
|----------------------------|------|
| `0.00` | 25 |
| `<0.25` | 9 |
| `<0.5` | 11 |
| `<0.75` | 46 |
| `>=0.75` | **49** (35%) |
35% of pointer rows anchor ≥75% of claims to a topic chunk. **This is the failure pattern Rule 8 was promoted to catch** — when the lazy-anchor target's source title shares zero stemmed content tokens with the claim, the run demotes `STRICT → HYBRID` via `TITLE_MISMATCH`. The honesty surfaces; the structural lift requires a stronger intervention (evidence-map ranking change, model fine-tune, or a different prompt frame).
19 of 71 questions show JSON ≥ 50pp above pointer, including:
```
pointer=HH json=SS did napoleon really die on saint helena?
pointer=HH json=SS name simpsons family members including pets?
pointer=HH json=SS what are the planets of our solar system?
pointer=HH json=SS what country is the city of prague in?
pointer=HH json=SS what is the boltzmann constant?
pointer=UU json=SS what is the difference between http and ftp?
pointer=HH json=SS when did the soviet union dissolve?
pointer=HH json=SS where does the nile river begin?
pointer=HH json=SS where is mount kilimanjaro located?
pointer=HH json=SS who is supermans girlfriend?
pointer=HH json=SS who painted the mona lisa?
pointer=HH json=SS who said may the force be with you?
pointer=HH json=SS who were the original seven mercury astronauts?
pointer=HH json=SS why did the dinosaurs go extinct?
```
(plus 5 more.) These are factual single-fact questions where JSON's structured per-claim evidence linkage forces the model to pick the specific chunk; pointer's prose-with-tags lets the model lazy-anchor to the topic article and Rule 8 catches it.
`subject_in_answer` is high across all rungs (STRICT 100%, HYBRID 92%, UNGROUNDED 84%) — the subject is in the answer; the answer just doesn't anchor cleanly to a single chunk. This is **structural-honesty signal**, not deflection.
## Wall-clock & throughput evolution
| stamp | concurrency | scheduling | n | tasks | wall-clock | tasks/min | mean latency (mode) |
|-------|-------------|-----------|---|-------|------------|-----------|---------------------|
| 11:31Z | 4 | cell-grouped | 2 | 426 | 51 min | 8.4 | 26-29 s |
| 15:07Z | 4 | sample-shuffled | 3 | 639 | **48 min** | **13.3** | **17-19 s** |
Sample-level shuffled scheduling at the same concurrency delivers **+58% throughput**. n=3 (50% more total work) ran in 6% LESS wall-clock than n=2 cell-grouped. Per-call mean latency dropped 35-42% across all modes (quote 29.1 s → 16.9 s, pointer 26.7 s → 18.5 s, JSON 28.2 s → 18.3 s) because vLLM's continuous batcher fills better when fed a diverse, uncorrelated request stream.
### Concurrency sweep (smoke fixture, 15-task)
| c | wall-clock | tasks/min | normalized |
|---|------------|-----------|------------|
| 3 | 102 s | 8.8 | **1.00** (peak) |
| 4 | 106 s | 8.5 | 0.96 (chosen — within 4% of peak, more forgiving on single-call hiccups) |
| 5 | 119 s | 7.6 | 0.86 |
| 6 | 185 s | 4.9 | 0.55 (vLLM batching ceiling, brutal) |
vLLM saturates at c=3-4 on this endpoint. More concurrent requests fill the batch better, but only up to the point where per-call latency growth outpaces parallelism gain. Re-sweep when the endpoint is upgraded, when corpus shape changes the average context size, or when other tenants change the queue.
## Errors
Across both benches: **2 errors at 11:31Z, 6 errors at 15:07Z** — all on the same question (`tell me about the roman empire`). Lone UTF-16 surrogates in Wikipedia chunk content, two distinct paths:
- v1 (`3b91223`) — hardened SHA-256 hashers on the OUTPUT side (`arborist/qa/dag.py`, `keys.py`, `evidence.py`, `store.py` audit chain) with `errors='surrogatepass'` so the run-DAG roots survive surrogate-bearing model output.
- v2 (`41d1d9b`) — scrubs message content INSIDE `OpenAICompatibleClient.chat_completion` before httpx's outbound JSON encode. The corpus chunk text was the path; httpx's `.encode('utf-8')` on the request body raised before the call left the client.
Verified: `tell me about the roman empire` under `claim_lattice` now classifies HYBRID 5/7 instead of erroring. Next bench will land 0 errors.
## Verdict & per-mode recommendation
**Quote leads on raw lexical grounding (0.54 strict-rate).** Best when the four-rung ladder isn't needed and the answer is short-and-cite-able. Doesn't surface anchor-class warrants.
**JSON leads among lattice modes (0.42).** Recommended default for cache-grade provenance. Structured per-claim evidence linkage; peaks at larger context (32-64 KB) so it scales with evidence growth.
**Pointer (0.20) stays useful for low-context-budget scenarios** (16-32 KB peak) and prose-distribution models that struggle with JSON grammar; the lazy-anchor honesty cost is the trade.
**The directive-coverage table (99% across every observable D2-D7 on lattice modes) is the architectural win.** False-positive STRICT was corruption; converting it to honest HYBRID was the price of calibration. Strict-rate slipped on lattice modes; the substrate is more honest.
## Outputs
- `bench/qa_results/2026-05-02T11-31-55Z.{jsonl,md}` — 426 rows
- `bench/qa_results/2026-05-02T15-07-24Z.{jsonl,md}` — 639 rows
(Both gitignored under `bench/qa_results/`. Headlines & analysis live in this journal.)
---
## Addendum — 2026-05-03 broad-quantifier A/B (Ticket #000008)
This journal froze the 2026-05-02 substrate baseline. On 2026-05-03
Ticket #000008 (broad-quantifier preflight guard) landed Phases 04
and ran a four-cell A/B on a 9-question broad subset
(`bench/qa_questions_quantifier_subset.txt`, 81 runs per cell).
Findings relevant to this journal's per-mode recommendations:
- **JSON-mode strict-rate moves under cap-only**: 0.19 baseline →
0.33 with `quantifier_guard_apply_caps=true` (+14pp on the broad
subset). The recommendation here that "JSON leads among lattice
modes" still holds, but the *headroom above 0.42 globally* is
partly dependent on broad-vs-narrow question mix.
- **Pointer-mode stays at 0/27 STRICT** on broad questions across
all four A/B cells. The 0.20 strict-rate above is *across the
full 71-question set*; the broad subset alone is structurally
unfavorable to pointer mode regardless of cap or reminder.
- **Reminder eliminates FORMAT_COLLAPSED**: 2/27 → 0/27 on pointer
mode under `quantifier_reminder_enabled=true`.
- **Cap and reminder help DIFFERENT failure modes**: reminder
rescues UNGROUNDED → HYBRID (restates citation rule); cap
rescues HYBRID → STRICT (forces fewer-but-better claims).
Compound effect on pointer mean-ratio is best at 0.684 (vs
0.473 baseline).
See `docs/tickets/ticket-000008-broad-quantifier-preflight-guard.md`
§12 for the four-cell data and the §10.8 decision-tree verdict.
Bench artifacts: `bench/qa_results/2026-05-03T12-{29-53,38-53,47-23,54-11}Z.{jsonl,md}`.
### Addendum 2 — preflight ON vs OFF validation (2026-05-03T23-06-21Z)
After ticket #000010 flipped `quantifier_reminder_enabled=True` and
shipped `metacognition_enabled=True` defaults, ran a preflight-OFF
cell to validate the flip didn't regress baseline behavior. Same
9-question broad subset (`bench/qa_questions_quantifier_subset.txt`),
n=3, policy override
`--policy metacognition_enabled=false --policy quantifier_reminder_enabled=false`.
Compared against §12.6 reminder-only baseline (cleanest single-knob
on-cell). On this 9-question subset, none of the metacognition
detectors fire (no temporal / contradiction / false-premise / out-
of-corpus shapes), so the comparison effectively isolates the
reminder contribution.
| Metric | OFF | ON | Δ |
|------------------------------|-----------|-----------|----------|
| `quote` strict-rate | 0.63 | 0.52 | 11pp (noise band on 27 samples) |
| `pointer` strict-rate | 0.00 | 0.00 | 0 |
| `JSON` strict-rate | 0.26 | 0.22 | 4pp |
| **`pointer` mean ratio** | 0.483 | **0.643** | **+16pp** |
| **`JSON` mean ratio** | 0.570 | **0.735** | **+17pp** |
| **`JSON` UNGROUNDED rate** | 7/27 (26%) | **1/27 (4%)** | **22pp** |
| **`pointer` FORMAT_COLLAPSED** | 2/27 | **0/27** | **100%** |
| `pointer` NO_EVIDENCE_POINTER | 8/27 | 6/27 | 7pp |
**Interpretation:**
- **Mean-ratio improvement is robust** (+16-17pp on lattice modes).
Grounded rows are MORE thoroughly grounded under preflight ON.
- **JSON UNGROUNDED collapse is dramatic** (22pp). The "didn't
ground" pool reclassifies into "partially grounded" — operator-
visible win.
- **FORMAT_COLLAPSED elimination on pointer mode** (2 → 0). The
reminder restates the [E\d+] citation rule and Hermes follows
it.
- **STRICT-rate moves are within noise.** Quote-mode dipped 11pp,
but quote is mode-gated off the guard so this is pure Hermes
nondeterminism — the §10.8 5pp floor exists exactly to filter
this. JSON SR moved 4pp (within floor).
**Verdict:** the #000010 default-on flip is doing what was
claimed. Mean-ratio + UNGROUNDED + FORMAT_COLLAPSED metrics all
improve by ≥5pp on lattice modes; STRICT-rate is within noise.
Defaults stay on.
Bench artifact: `bench/qa_results/2026-05-03T23-06-21Z.{jsonl,md}`.
### Addendum 3 — full-bench regression check (2026-05-03T23-30-12Z)
Validates that the #000010 default flip (reminder default-on for
lattice modes) doesn't regress narrow-question performance. The
prior validations in Addendum 1 + Addendum 2 used the 9-question
broad subset only; this run sweeps the full 75-question
`bench/qa_questions.txt` (~10% broad, ~89% narrow factoid /
descriptive / list shapes).
Same harness, n=3 × 3 modes × 75 questions = 225 runs per mode.
Compared against the frozen `15:07Z` baseline (the authoritative
pre-#000008/9/10 state of the substrate, 71 questions × n=3 = 213
runs per mode):
| Mode | Pre-flip (15:07Z) | **Post-flip (23:30Z)** | Δ SR | Δ mean ratio |
|-------------------------|-------------------|------------------------|------|--------------|
| `quote` | 116S/51H/46U · SR 0.54 · ratio 0.699 | 116S/60H/49U · **SR 0.52** · **ratio 0.692** | 2pp | 1pp |
| `claim_lattice_pointer` | 43S/146H/21U · SR 0.20 · ratio 0.642 | 48S/145H/32U · **SR 0.21** · **ratio 0.663** | +1pp | +2pp |
| `claim_lattice` (JSON) | 89S/82H/39U · SR 0.42 · ratio 0.698 | 99S/80H/46U · **SR 0.44** · **ratio 0.730** | +2pp | +3pp |
All STRICT-rate deltas within the 5pp signal floor. Same question
set ±2 (3 added: `winners of all major sports?`, `name all members
of the beatles`, `list all planets in the solar system`).
**Substrate-level findings:**
- **Pointer-mode `FORMAT_COLLAPSED`: 0/225** across the full sweep.
The default-on reminder eliminates the collapse mode globally,
not just on broad questions where it was originally measured.
- **Pointer-mode `NO_EVIDENCE_POINTER`: 29/225** (13%) — lower than
the 33% we saw on broad-only (9/27) when reminder was off.
Reminder discipline propagates to non-broad questions even though
the reminder text only fires on broad shapes (the broader signal
is that the reminder text reinforces the citation rule across
the model's session attention).
- **Quote-mode is essentially unchanged** (-2pp SR, -1pp ratio).
Quote opts out of the guard (`quantifier_guard_modes` default
excludes quote), so this is pure Hermes nondeterminism on a
225-sample cell.
- **JSON-mode mean ratio +3pp** — small but positive, consistent
with the broad-subset finding that reminder boosts per-claim
grounding density.
**Latency note.** Wall-clock per call jumped from ~17-19s (pre-flip
baseline) to ~33-35s on this run. This is **endpoint contention**
on Hermes-3 at the time of measurement, not a feature regression
(the new substrate adds zero LLM calls — preflight is pure regex).
The throughput numbers in §6 of the journal pre-date this evening's
network conditions; future bench cycles should normalize.
**Verdict — no regression.** The #000010 default flip ships clean
across the full corpus, not just the broad-quantifier subset where
it was originally validated. Defaults stay on. The substrate is
strictly more honest under preflight (FORMAT_COLLAPSED → 0,
NO_EVIDENCE_POINTER ↓) without sacrificing throughput on
non-broad questions.
Bench artifact: `bench/qa_results/2026-05-03T23-30-12Z.{jsonl,md}`.
### Addendum 4 — metacog-trigger detector validation (2026-05-04T02-18-42Z)
The validations above (Addendum 1-3) covered the broad-quantifier
subset and the full corpus, neither of which contains questions
that fire the #000010 metacog detectors (temporal sensitivity,
contradiction, false-premise-lite, out-of-corpus). This addendum
closes that empirical gap.
Fixture: `bench/qa_questions_metacog_subset.txt` — 6 questions,
one per detector kind plus a control:
```
who is the current CEO of OpenAI? # stale_risk
which character is alive and dead? # contradictory
when did George Washington stop being president # false_premise +
of France? # stale_risk (multi)
what does my uploaded contract say about # out_of_corpus
clause 9?
when did the current CEO of Twitter stop # stale_risk +
being CEO of Tesla? # false_premise
who painted the mona lisa? # control (well_formed)
```
Run: n=3 × 6 questions × 3 modes = 54 rows.
**Per-question verdict matrix:**
| question (detector) | quote | pointer | JSON |
|---------------------------|---------|----------|---------|
| Current CEO (stale_risk) | 0/0/3 U | 0/0/3 U | 0/0/3 U |
| Alive+dead (contradictory)| 0/0/3 U | 3/0/0 S | 3/0/0 S |
| Wash. stop France (FP) | 0/0/3 U | 0/3/0 H | 3/0/0 S |
| Uploaded contract (OOC) | 0/3/0 H | 0/1/2 U | 3/0/0 S |
| Twitter→Tesla (multi) | 0/0/3 U | 3/0/0 S | 0/0/3 U |
| Mona Lisa (control) | 3/0/0 S | 1/2/0 S | 3/0/0 S |
(S/H/U = STRICT/HYBRID/UNGROUNDED; n=3 each cell.)
**Findings:**
1. **Detector accuracy is 6/6.** All trigger questions fire the
expected `logical_statuses` value during preflight (verified
programmatically before the bench: `stale_risk`,
`contradictory_question`, `false_premise_suspected`,
`out_of_corpus_risk` — matched 1:1 with fixture intent). The
detectors are doing what their unit tests claim.
2. **Quote mode is the most honest fallback.** 4 of 5 trigger
questions land all-UNGROUNDED on quote mode. The paraphrase
verifier won't substring-match across the corpus when the
question's premise has no anchor. Quote mode's mode-gated-off
guard works in our favor here.
3. **Lattice modes accidentally ground 2 trigger questions to
STRICT.** Schrödinger's cat (alive+dead) JSON STRICT is
*defensible* — the corpus contains quantum-mechanics articles
that legitimately discuss the state. But:
- JSON STRICT on **"when did George Washington stop being
president of France?"** is **NOT defensible**. False premise;
the model invented an answer that lexically grounded against
some chunk. The metacog detector correctly flagged
`false_premise_suspected`; the audit-line tail surfaced
`· false premise`; but the verdict still lands STRICT.
- JSON STRICT on **"what does my uploaded contract say about
clause 9?"** is the same shape: out-of-corpus reference,
model fabricates a grounding.
4. **Audit-line tails are doing operator-warning duty correctly.**
The bench rows persist `preflight_logical_statuses` and the
render layer tails (`· stale risk`, `· false premise`, etc.)
even on STRICT verdicts — operator sees the warning. But the
substrate doesn't refuse execution by default for these shapes.
**Implication for #000011 (SOFT_PREFLIGHT_HINT).** This bench
validates the design rationale for the soft-sidecar ticket: the
deterministic metacog detectors flag these shapes correctly, but
the corpus accidentally grounds 2/5 of them to STRICT. A model-
assisted soft preflight could add independent semantic skepticism
("does George Washington being president of France match
historical reality?") that the lexical detectors can't supply.
Soft sidecar output would surface as `· soft: false_premise_*`
on the audit-line, distinct from the hard `· false premise` tail,
giving operators a stronger warning when both signals fire.
**Implication for default policy.** Keep
`metacognition_block_on_contradiction=False` as the default.
Schrödinger's cat (alive+dead) would have been rejected
unnecessarily under a hard-block, and that's a real-world
question with a legitimate answer. The label-only default is
correct; operators wanting strictness opt in via
`--block-on-contradiction`.
Bench artifact: `bench/qa_results/2026-05-04T02-18-42Z.{jsonl,md}`.
### Addendum 5 — #000046 Phase 3 verifier numeric-gate regression check (2026-05-11)
`arborist/qa/verify.py` gained a paraphrase numeric-agreement gate
(`_numeric_signature` + a check in `_check_each_with_paraphrase`):
a span that token-covers the source ≥ `paraphrase_coverage` but
asserts a digit-number the source lacks (modulo thousands-comma) is
no longer paraphrase-grounded — it goes to `unverified`. Closes
#000046 by lifting the `falsification-hard-v1.jsonl` rate 4/12 → 6/12
(`5f-fal-hard-004` 50-vs-100 and `-007` 300-vs-300,000 → UNGROUNDED).
Before/after `make bench-qa` (n=3 × 75 questions × 3 modes = 675
cells):
| mode | STRICT-rate before → after | Δ |
|------|----------------------------|---|
| quote | 0.53 → 0.50 | 3pp |
| claim_lattice_pointer | 0.23 → 0.25 | +2pp |
| claim_lattice | 0.44 → 0.45 | +1pp |
All within the 5-pp noise floor. Per-row diff (675 common cells, 74
changed audit_mode): the *only* clearly gate-attributable QA shift
was the fictional "our cold fusion breakthrough" year-claim demoting
STRICT → HYBRID across all 3 samples — a **correct** demotion (the
year isn't grounded). Every other transition was `quote→quote` /
`claim_lattice→claim_lattice` LLM re-answer variance — the answer
text changed on the re-run, not the verifier (the gate touches only
the paraphrase fallback, never the verbatim/span/entity/claim-lattice
paths). No regression on legitimate answers; the gate ships.
Bench artifacts: `bench/qa_results/2026-05-11T13-42-38Z.{jsonl,md}`
(before) · `bench/qa_results/2026-05-11T14-19-51Z.{jsonl,md}` (after).
Full per-ticket detail: `docs/tickets/ticket-000046-harder-5sf-fixture-tier.md`
§5 Phase 3.
### Addendum 6 — #000048 step 2.1 verifier entity-gate regression check (2026-05-11)
`arborist/qa/verify.py` gained an entity salient-token-disagreement
gate (`_entity_salient_disagrees` + `_is_single_sentence`): in
`verify_quotes`' entity branch (proximity policy), the *weakest*
grounding — `not cluster AND len(verified) <= 1 AND single sentence`
is declined (→ UNGROUNDED) when the answer asserts a > 4-char
capitalized content token (stopword-filtered) or a digit-number the
source lacks. Catches "Insulin was discovered by Alexander Fleming"
against "Penicillin was discovered by Alexander Fleming" (shared
"Alexander Fleming" matched; swapped subject "Insulin" trips the
gate). Lifts the `falsification-hard-v1.jsonl` rate 6/12 → 10/12 (the
4 HYBRID_ENTITY over-grounds: Insulin / Berlin / 1889 / Pacific).
Before/after `make bench-qa` (n=3 × 75 questions × 3 modes = 675
cells):
| mode | STRICT-rate before → after | Δ |
|------|----------------------------|---|
| quote | 0.50 → 0.54 | +4pp |
| claim_lattice_pointer | 0.25 → 0.22 | 3pp |
| claim_lattice | 0.45 → 0.43 | 2pp |
All within the 5-pp noise floor. Per-row diff (675 common cells, 30
quote-mode rows changed audit_mode): **0 quote-mode rows demoted to
UNGROUNDED from the entity path** — the gate didn't fire on a single
legitimate QA answer in the whole bench. Every quote-mode transition
was LLM re-answer variance (`verifier quote→quote` with the verdict
flipping = a different answer text); the pointer/lattice deltas are
noise too (the gate is in `verify_quotes` / quote mode, not the
claim-lattice verifier). The gate is provably narrow on real traffic;
it ships.
Bench artifacts: `bench/qa_results/2026-05-11T14-19-51Z.{jsonl,md}`
(before — HEAD's verify.py) · `bench/qa_results/2026-05-11T17-12-41Z.{jsonl,md}`
(after). Full per-ticket detail: `docs/tickets/ticket-000048-verifier-upgrade-recombination-segmentation.md`
§5 step 2.1.
### Addendum 7 — #000048 step 2.4 parse_pointer_claims clause-segmentation regression check (2026-05-11)
`arborist/qa/parse_claims.py` gained a clause segmenter: a line that
crams several well-pointered claims onto one row is split into one
claim per clause (split on `;`, sentence boundaries, spaced dashes,
` and `/` or `/` because `/` although `/` since `/` while `, inline
`(N)` enumeration markers, commas — with `(?![^\[]*\])` so a comma
inside a `[E1, E2]` bracket never splits it). `_segment_line` keeps
the split *only if every resulting segment is well-pointered* (a legit
single claim like "The cat is black and white [E1]." or "The cast: A,
B, C [E1]." is never broken — splitting would create pointer-less
fragments → guard rejects; a leading colon-terminated header — "Two
facts:", "Key points:" — is dropped). Plus a wrapped-bullet join: a
continuation line (leading whitespace then a lowercase letter, no
bullet) folds into the previous claim. Closes the 8 mis-segments in
`formulate-hard-v1.jsonl` → rate 4/12 → 12/12 (that pack now at
ceiling).
Before/after `make bench-qa` (n=3 × 75 questions × 3 modes = 675
cells; `parse_pointer_claims` feeds the 450 `claim_lattice_pointer` +
`claim_lattice` cells):
| mode | STRICT-rate before → after | Δ |
|------|----------------------------|---|
| quote | 0.54 → 0.55 | +1pp |
| claim_lattice_pointer | 0.22 → 0.22 | 0pp |
| claim_lattice | 0.43 → 0.45 | +2pp |
All within the 5-pp noise floor. Per-row diff (675 common cells): the
segmenter changed the parsed-claim count *on the same answer text*
for **7 of the 450 lattice cells** (0 in `claim_lattice`, 7 in
`claim_lattice_pointer`); of those, 2 caused an `audit_mode` change —
both **correct**: (a) a wrap-join recovered an answer's intended
structure (4 claims, 2 of which were pointer-less wrap-fragments →
HYBRID) into 2 well-pointered claims → STRICT; (b) a crammed-one-line
blob (1 monolithic claim, all pointers → STRICT) split into 8 claims,
some of which don't individually verify → HYBRID — the honest verdict
(false-positive STRICT was the corruption). Every other lattice/quote
delta is LLM re-answer variance (`answer_chars` changed, often
drastically). No regression — the segmenter's only visible effects on
real traffic are honest improvements.
Bench artifacts: `bench/qa_results/2026-05-11T17-12-41Z.{jsonl,md}`
(before — HEAD's parse_claims.py) · `bench/qa_results/2026-05-11T20-26-37Z.{jsonl,md}`
(after). Full per-ticket detail: `docs/tickets/ticket-000048-verifier-upgrade-recombination-segmentation.md`
§5 step 2.4.