arborist/docs/modules/qa.md
russell@unturf.com 326badf6d8
docs: README label refresh + per-module reference + Graphviz diagrams
Three things in one commit because they're tightly coupled (README
points at the diagrams; diagrams index in modules/index.md points
back at README; module pages embed the diagrams).

(1) README — label refresh:
    - Quickstart label changed from STRICT/HYBRID/UNGROUNDED to the
      four-rung ladder POINTER-LINKED → ANCHOR-WARRANTED →
      EVIDENCE-WARRANTED → UNGROUNDED with -PARTIAL suffix on HYBRID.
    - Verifier section spells out both layers (schema trichotomy +
      display ladder), the seven hard checks of claim_lattice, and
      the five anchor classes of warrant.
    - Architecture tree updated: concepts/ package added, qa/
      sub-modules expanded (warrant, evidence, parse_claims, dag),
      verify.py described as quote/span/entity/paraphrase + claim_lattice.
    - Concept overlay description updated for corpus-derived layer
      (concept_relations table, link_reciprocity extractor, 1.6%
      tax cite).
    - Test count: 326+ → 641+.

(2) docs/diagrams/ — Graphviz dot sources:
    - aborist-modules.dot — top-level package graph (substrate /
      storage / sources / retrieval / qa / mesh / cli)
    - query-pipeline.dot — question → cache → retrieval → LLM →
      verify → render → cache write, with phase budgets
    - ingest-pipeline.dot — source doc → canonicalize → chunk →
      Merkle → upsert (+ optional distill)
    - verifier-ladder.dot — (audit_mode, violations) → display rung
      decision tree
    Existing mesh-*.dot kept as-is. Makefile `make docs` target
    extended to also emit .svg alongside the existing .png so the
    diagrams render in markdown viewers.

(3) docs/modules/ — per-module reference pages:
    - index.md (links to every diagram + every module page)
    - merkle.md, document.md, store.md, ingest.md, evict.md,
      sources.md, search.md, concepts.md, qa.md, distill.md,
      wikitext.md
    Each page is a one-screenful concise reference: what the
    module is for, public API, key invariants, embedded diagrams
    where useful, link to source. Mesh stays at the existing
    docs/mesh.md + docs/mesh-deploy.md (already comprehensive).

Tests: 641 passed (no code change).
2026-05-01 23:19:01 -04:00

185 lines
6.9 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.

# `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