From 182bb67996d3637e473252b0d602d64650e24c4b Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Fri, 1 May 2026 13:32:05 -0400 Subject: [PATCH] =?UTF-8?q?qa:=20warrant-lite=20=E2=80=94=20wire=20JSON=20?= =?UTF-8?q?variant=20of=20claim=5Flattice=20verifier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initial Ticket H commit (38d95dd) only plumbed warrant into verify_claim_lattice (pointer mode). The JSON variant verify_claim_lattice_json was missed — and the CLI's --answer-mode claim_lattice (the JSON path) was producing EVIDENCE-LINKED on relation-question records without warrant firing. This adds the same warrant block to verify_claim_lattice_json: question + warrant_check_enabled params, identical WARRANT_MISSING violations, identical demote-to-HYBRID semantics, same warrant_missing_claim_idxs field on the verdict. Plumbs question= and warrant_check_enabled= through call sites in runner.py and query.py for the JSON path. Live verified: warrant correctly checks the FULL cited span (not the spotlight excerpt). On the Homer-Simpson "boss is Mr. Burns" case, the chunk text DOES contain "Mr. Burns" elsewhere in the paragraph — warrant rightly passes. The misleading spotlight excerpt that fox flagged is a separate render-layer issue (failure to spotlight the load-bearing slice), not a warrant failure. Note: cache-hit path does not currently restore violations into the result dict (providence_cache schema has no violations column; run_dag_blob stores only hashes). On cache hits the warrant_tail in CLI render won't surface even when warrant fired at write-time. Follow-up needs a schema migration to persist violations alongside the cached record. --- aborist/qa/query.py | 4 ++++ aborist/qa/runner.py | 4 ++++ aborist/qa/verify.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/aborist/qa/query.py b/aborist/qa/query.py index 2f7cbde..7081242 100644 --- a/aborist/qa/query.py +++ b/aborist/qa/query.py @@ -1835,6 +1835,10 @@ def query( max_claims_per_answer=int(policy.get( "claim_lattice_max_claims_per_answer", 12 )), + question=question, + warrant_check_enabled=bool(policy.get( + "claim_lattice_warrant_check_enabled", True + )), ) rendered = verdict["rendered_text"] answer_text = rendered if rendered else raw_answer diff --git a/aborist/qa/runner.py b/aborist/qa/runner.py index 5701f95..81b3350 100644 --- a/aborist/qa/runner.py +++ b/aborist/qa/runner.py @@ -652,6 +652,10 @@ def ask( max_claims_per_answer=int(policy.get( "claim_lattice_max_claims_per_answer", 12 )), + question=question, + warrant_check_enabled=bool(policy.get( + "claim_lattice_warrant_check_enabled", True + )), ) rendered = verdict["rendered_text"] answer_text = rendered if rendered else raw_answer diff --git a/aborist/qa/verify.py b/aborist/qa/verify.py index 86195fa..f070eb5 100644 --- a/aborist/qa/verify.py +++ b/aborist/qa/verify.py @@ -1467,6 +1467,8 @@ def verify_claim_lattice_json( max_evidence_per_claim: int = DEFAULT_MAX_POINTERS_PER_CLAIM, min_citation_coverage: float = DEFAULT_MIN_CITATION_COVERAGE, max_claims_per_answer: int = DEFAULT_MAX_CLAIMS_PER_ANSWER, + question: str | None = None, + warrant_check_enabled: bool = True, ) -> dict: """Deterministic verifier for ``answer_mode="claim_lattice"`` (JSON). @@ -1710,6 +1712,38 @@ def verify_claim_lattice_json( else: audit_mode = "UNGROUNDED" + # Warrant-lite — same relation-question hard check as the pointer + # variant. See verify_claim_lattice for the rationale (Ticket H, + # 2026-05-01). Identical demote-to-HYBRID semantics; the JSON + # variant carries the same WARRANT_MISSING violations & the same + # warrant_missing_claim_idxs field on the verdict. + warrant_missing_claims: list[int] = [] + if warrant_check_enabled and question and is_relation_question(question): + for cs in claim_statuses: + if cs.get("status") not in ("EVIDENCE_LINKED", "EVIDENCE_LINKED_PARTIAL"): + continue + cited_eids = cs.get("evidence_ids") or [] + cited_spans = [ + obj.span + for eid in cited_eids + for obj in [evidence_map_by_evidence_id_local(evidence_map, eid)] + if obj is not None + ] + ok, missing = warrant_check(cs.get("text") or "", cited_spans) + if not ok: + warrant_missing_claims.append(cs.get("claim_idx")) + violations.append({ + "kind": "WARRANT_MISSING", + "claim_idx": cs.get("claim_idx"), + "missing_anchors": missing, + "rationale": ( + "claim names answer entity not present in any " + "cited span — pointer-linked but warrant missing" + ), + }) + if warrant_missing_claims and audit_mode == "STRICT": + audit_mode = "HYBRID" + # Same `verifier_method` as the pointer variant ("claim_lattice") # so the providence_cache CHECK constraint accepts both. The mode # is disambiguated downstream via `answer_mode` on the run-DAG & @@ -1725,4 +1759,5 @@ def verify_claim_lattice_json( "rendered_text": rendered_text, "evidence_id_pairs": evidence_id_pairs, "json_fixups": json_fixups, + "warrant_missing_claim_idxs": warrant_missing_claims, }