#000049 §7 #21: candidate-clause restriction in ShadowNLI.check — helps, doesn't close it

candidate_clauses() — NLI now runs only on the top-N source clauses by
content-token overlap with the answer claim (max_candidate_clauses=6),
not the whole context; records n_candidate_clauses / best_clause_overlap
/ recombination_risk. Synthetic sweep unchanged (28/28 recombination,
0/26 legit FP, mean 1.45 candidate clauses/record). Real-traffic smoke
re-run: STRICT would-demote 30% → 20%, overall 47% → 33% — better, not
fixed; recombination-risk split doesn't separate either. Residual STRICT
false-contras at ~0.83-0.92 → θc would need ≈ 0.90 (vs the clean-set
0.5); at θc=0.90 the data in hand gives 27/28 synthetic recall, 0/26
legit FP, 0/10 smoke STRICT FP — but n=10 is too small to set on.
Next: a fuller ARBORIST_NLI_SHADOW=1 bench-qa run → sweep θc on hundreds
of STRICT cells → confirm → set it. θc stays 0.5; runtime NLI demotion
stays off. Production verifier unchanged; falsification-hard stays 10/12.
This commit is contained in:
russell@unturf.com 2026-05-12 14:34:44 -04:00
parent f4de936ff9
commit 02f8dfec07
No known key found for this signature in database
8 changed files with 730 additions and 124 deletions

View file

@ -16,7 +16,7 @@ from pathlib import Path
import pytest
from arborist.qa.nli import ShadowNLI, ShadowResult, shadow_check, load_manifest
from arborist.qa.nli.shadow import clauses, _resolve_label_indices
from arborist.qa.nli.shadow import clauses, candidate_clauses, _content_tokens, _resolve_label_indices
# --- clause splitter -------------------------------------------------------
@ -42,6 +42,58 @@ def test_clauses_empty_or_blank():
assert clauses(None) == [] # type: ignore[arg-type]
# --- candidate-clause restriction (§7 #5 step 3 / §7 #20) ------------------
def test_content_tokens_filters_short_and_stopwords():
toks = _content_tokens("The Mercury is the largest planet in the Solar System.")
assert "mercury" in toks and "largest" in toks and "planet" in toks and "solar" in toks
assert "the" not in toks and "is" not in toks and "in" not in toks # stopword / too short
def test_candidate_clauses_ranks_by_overlap_and_caps():
claim = "Mercury is the largest planet in the Solar System."
source = ("Jupiter is the largest planet in the Solar System. "
"Mercury is the smallest planet in the Solar System. "
"The Roman god Mercury was the messenger of the gods. "
"Bananas are yellow.")
cand = candidate_clauses(claim, source, k=6)
# the banana clause shares no content tokens → dropped
assert all("Bananas" not in c for c, _ in cand)
# the two planet clauses are the highest-overlap ones
assert "Jupiter is the largest planet" in cand[0][0] or "Mercury is the smallest planet" in cand[0][0]
assert cand[0][1] > cand[-1][1] or len(cand) == 1
# cap respected
assert len(candidate_clauses(claim, source, k=1)) == 1
def test_candidate_clauses_empty_when_no_overlap():
assert candidate_clauses("Bananas are yellow.", "Mercury is the smallest planet.", k=6) == []
assert candidate_clauses("", "anything at all here", k=6) == []
def test_check_restricts_to_candidate_clauses_not_whole_haystack():
# a long source where only 2 clauses touch the claim — the result must
# report n_candidate_clauses << n_clauses (this is the §7 #20 fix).
haystack = " ".join(f"Unrelated fact number {i} about pottery." for i in range(40))
source = "Jupiter is the largest planet. " + haystack + " Mercury is the smallest planet."
res = shadow_check("Mercury is the largest planet.", source)
assert res.n_clauses >= 40
assert res.n_candidate_clauses <= 6
assert res.n_candidate_clauses < res.n_clauses
def test_check_no_candidate_clauses_is_no_demote():
res = shadow_check("Mercury is the largest planet.",
"Bananas are yellow. Pottery is ancient. The weather is fine.")
assert res.would_demote is False
if res.available:
assert res.reason == "no_candidate_clauses" and res.n_candidate_clauses == 0
def test_manifest_has_candidate_clause_cap():
assert load_manifest()["max_candidate_clauses"] == 6
# --- manifest --------------------------------------------------------------
def test_manifest_has_required_fields():