# `aborist.qa`

The Q&A pipeline. Question → cache → retrieval → LLM → verify →
render → cache write. Lives in 9 sub-modules; this page is the
map.

![query pipeline](../diagrams/query-pipeline.svg)

## Sub-modules

### `qa.client` — LLM transport

`ChatClient` ABC with three concrete implementations:

- `StubClient` — deterministic test fixture; returns canned
  responses keyed on the input. Used in unit tests to avoid network.
- `OpenAICompatibleClient` — talks to any OpenAI-shape `/v1/chat/completions`
  endpoint (Hermes-3 on vLLM by default). Includes HTTP retry layer
  (3× exponential backoff on 5xx).
- (Future) `AnthropicClient` — Claude API direct.

[`aborist/qa/client.py`](../../aborist/qa/client.py)

### `qa.keys` — the 8-dim cache_key

```
cache_key = sha256(
    source_root | question_hash | model_profile_hash |
    conversation_hash | governance_policy_hash |
    schema_version | canonicalization_version | chunking_version
)
```

Two question-hash modes (`strict` vs `equivalence_class`) live here.
Bumping any of these eight dimensions invalidates prior records on
lookup. The `verifier_policy_hash` (v9.9 9th dim) is also
implemented here.

[`aborist/qa/keys.py`](../../aborist/qa/keys.py)

### `qa.runner` — `ask()` for single-doc Q&A

The simplest entry point. Take one document, ask one question, get
back an answer + audit_mode + cache record. Used by the CLI for
focused queries against one URI.

[`aborist/qa/runner.py`](../../aborist/qa/runner.py)

### `qa.query` — `query()` for multi-source RAG

The main retrieval entry point. Walks shards, runs FTS5 BM25 with
AND→OR fallback (with synonym-pool injection in OR mode), filters
by title relevance with 4 accept paths, reranks by body coverage +
title boost + source role + title purity, assembles a 60 KB context
budget, calls the LLM, runs the verifier, persists to
`providence_cache`.

[`aborist/qa/query.py`](../../aborist/qa/query.py)

### `qa.verify` — the layered verifier

Five strategies run in sequence; first to find evidence classifies:
1. `quote` — `"..."`-wrapped claims tested verbatim
2. `span` — bullet/sentence units substring-tested
3. `entity` — multi-word proper nouns with proximity gating
4. `paraphrase` — token coverage on prose-shaped spans (≥85%)
5. `claim_lattice` — pointer-line `[E1,E2]` or JSON; runs **seven
   deterministic hard checks**:
   1. parser succeeded
   2. evidence_id resolves
   3. source_role allowed
   4. claim text non-empty
   5. citation coverage threshold
   6. pointer count cap (trim-and-verify)
   7. anchor-class warrant (see `qa.warrant`)

The classifier output rolls up into the v9.8 trichotomy
`audit_mode ∈ {STRICT, HYBRID, UNGROUNDED}`. Display layer (in
`cli.py`) maps `(audit_mode, violations) → four-rung ladder`.

![verifier ladder](../diagrams/verifier-ladder.svg)

[`aborist/qa/verify.py`](../../aborist/qa/verify.py)

### `qa.warrant` — anchor-class warrant

Five lexical anchor classes the verifier composes:

- **Proper-noun** — relation-question shape; at least one
  Title-Case anchor must appear in some cited span
- **Date** — claim has a 4-digit year + month name; ALL components
  required in some cited span
- **Entity-list** — entity-list-shape question; ≥1 named entity
  must anchor (demote-don't-reject)
- **Count** — count-shape question; count token must appear in
  word OR digit form (digit↔word equivalence)
- **Cause** — why-shape question; ≥1 cause anchor (proper noun OR
  ≥5-char common noun outside stopword pool)

The warrant layer earns proof-path entry by staying **lexical** —
no NLI, no embeddings. Substring tests over already-canonicalized
spans. See `docs/concept-relations-design.md` (sibling section)
for the relationship to retrieval-time synonym expansion.

[`aborist/qa/warrant.py`](../../aborist/qa/warrant.py)

### `qa.evidence` — EvidenceObject + spotlight

Builds the runtime evidence map for claim-lattice modes. Each
chunk becomes one `EvidenceObject` carrying TWO ids:

- `pointer_id` — short prompt-facing tag (`E1`, `E2`, …)
- `evidence_id` — content-addressed `E########` (sha256-derived)

The model sees only `pointer_id`s in the prompt; the runtime maps
to `evidence_id` for the cache & run-DAG (run-stable identity).

The spotlight excerpt picks the load-bearing slice via **density
rank** — find ALL match positions for ALL claim content tokens,
pick the position with maximum distinct-token cluster within
±half-window. Replaces the older first-match-of-longest-token
approach which lost the load-bearing slice on noisy chunks.

[`aborist/qa/evidence.py`](../../aborist/qa/evidence.py)

### `qa.parse_claims` — pointer-line parser

Walks lines of the model output, pulls every `[E\d+]` and
`[E\d+,E\d+,…]` bracket payload, returns
`(claim_text, pointer_ids[])` per line. Lines without a tag get
`parse_status='NO_EVIDENCE_POINTER'` & count toward the denominator
so unsourced prose can't smuggle past the verifier.

[`aborist/qa/parse_claims.py`](../../aborist/qa/parse_claims.py)

### `qa.dag` — per-run Merkle DAG

Commits each provenance step independently as a stage hash. Two
shapes:

- **7-stage (quote mode):** question / retrieval / context /
  prompt / answer / verify / final_label
- **9-stage (claim-lattice / CTI):** question / retrieval /
  evidence_map / prompt / raw_answer / parsed_claim_lattice /
  verify / render / final_label

The `run_dag_root` is persisted alongside every providence record;
`run_dag_blob` carries the full `{root, nodes}` JSON so an auditor
can recompute & verify any step.

[`aborist/qa/dag.py`](../../aborist/qa/dag.py)

### `qa.inspect` — read-only sidecar

Pulls source chunks for a given cache_key & classifies each
unverified span: `verbatim_in_base` / `verbatim_in_raw_only` /
`trailing_artifact` / `paraphrase` / `partial_paraphrase` /
`no_overlap`. Also includes the deflection-detection sidecar
(subject-anchor heuristic for adversarial-premise topic shift).

**Sidecars never write to `providence_cache` or `audit_events`** —
they're diagnostic only. That invariant is what keeps `audit_mode`
a binary classification rather than a soft score.

[`aborist/qa/inspect.py`](../../aborist/qa/inspect.py)

### `qa.concepts` — backwards-compat shim

Delegates to `aborist.concepts` (the corpus-derived synonym/rivalry
layer). Pre-2026-05-01 the data lived as hand-curated frozensets in
this file; now it's a per-shard SQLite table. The shim preserves
the legacy public API (`synonym_expand`, `rivalry_excluded`,
`has_compare_phrasing`) so call sites in `qa/query.py` didn't have
to change.

[`aborist/qa/concepts.py`](../../aborist/qa/concepts.py) →
[`aborist/concepts/`](../../aborist/concepts/)

## Source papers

- Whitepaper §13.8 covers the layered verifier in depth
- Whitepaper §13.9 covers claim-lattice / CTI mode
- `docs/cti-architecture.md` is the architecture reference
- `docs/seven-point-program.md` enumerates the seven hard checks
- `docs/concept-relations-design.md` covers the synonym layer
