When a per-claim warrant_check would fire WARRANT_MISSING but the
cited chunk's document_root has a warrant-resolver derivation row
(Merkle-bound primary-source backing), the verifier now suppresses
the demote and tracks the claim on a new `warrant_proven_claim_idxs`
field. The render layer surfaces this as `· warrant proven via
chain ×N` in the audit-line tail so operators see when a claim got
through on the chain rather than on lexical anchors.
Mechanism (additive, fail-closed):
1. New `arborist/qa/warrant_chain.py` — read-only helper that
loads the frozenset of `core_root` values having a derivation
row with `process_id LIKE 'warrant-resolver-v1%'`. One sqlite
query per Q&A run, walks main shards + sibling crawl/ shards
(skipping ad-hoc crawl_russell_/qa./snapshots. prefixes).
Tolerates missing tables.
2. `verify_claim_lattice` + `verify_claim_lattice_json` accept
a new optional `warrant_chain_roots: frozenset[str]` parameter
(default empty = backward-compatible). When the lexical
warrant_check fails for a claim AND any cited evidence's
source_root is in the set, the WARRANT_MISSING violation is
suppressed and the claim_idx flows to a new
`warrant_proven_claim_idxs` field on the verdict.
3. `runner.ask` computes the warrant_chain_roots set once from
the conn's main DB directory before invoking the verifier.
Failure-mode fallback: empty set, behavior identical to
pre-Phase-3.
4. `query.py` threads `warrant_proven_claim_idxs` from the
verdict into the result dict.
5. `cli.py:_render_warrant_tail` adds a `warrant proven via chain
×N` segment when `warrant_proven_claim_idxs` is non-empty.
Distinct from the pre-existing `_render_warrant_chain_tail`
which counts cited SOURCES with chains; this counts CLAIMS
that survived because of a chain.
Tests:
- tests/test_warrant_chain.py — 9 new tests covering
warrant_chain_lookup (basic / unrelated process_id / missing
table / +alias variant / empty path) + has_warrant_chain
short-circuit + verifier suppression behavior + verdict-field
presence guarantee.
Live smoke: warrant_chain_lookup(~/.arborist/shards) returns
exactly 92 core_roots (matches the 92/92 claim-pack records
resolved earlier today).
Total: 1652 tests pass (was 1643). Honest layering preserved:
- soft signal (positive warrant_proven) lives on a separate verdict
field, not in `violations` (which stays a hard-failure list)
- audit_mode (STRICT/HYBRID/UNGROUNDED) unchanged when chain
suppresses WARRANT_MISSING — the claim was going to land at
HYBRID without the suppression; the suppression keeps it at
STRICT, which is now defensible because the warrant chain IS the
warrant
- four-rung ladder rung promotes naturally: no WARRANT_MISSING in
violations + no soft demotes -> EVIDENCE-WARRANTED via the
existing _ladder_rung_for_lattice logic. No render-layer ladder
change needed.
Phase 3 follow-ups still open under #000031:
- via_citation_alias process_id attribution (~15 LOC)
- source-side title-from-author backfill in HTML/textbook_tex
ingest (~30 LOC)