#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

@ -92,18 +92,25 @@ def main(argv=None) -> int:
nli = ShadowNLI()
by_bucket: dict[str, dict] = defaultdict(lambda: {"n": 0, "would_demote": 0})
by_recomb_risk: dict[str, dict] = {"risk": {"n": 0, "would_demote": 0},
"no_risk": {"n": 0, "would_demote": 0}}
fp_probe = {"n": 0, "would_demote": 0}
rows = []
n_available = 0
n_candidate_clauses_total = 0
t0 = time.time()
for r in recs:
res = nli.check(r["claim"], r["source"])
if res.available:
n_available += 1
n_candidate_clauses_total += res.n_candidate_clauses
b = by_bucket[r["bucket"]]
b["n"] += 1
rr = by_recomb_risk["risk" if res.recombination_risk else "no_risk"]
rr["n"] += 1
if res.would_demote:
b["would_demote"] += 1
rr["would_demote"] += 1
if r["is_fp_probe"]:
fp_probe["n"] += 1
if res.would_demote:
@ -127,8 +134,12 @@ def main(argv=None) -> int:
"elapsed_seconds": round(elapsed, 1),
"would_demote_total": total_demote,
"would_demote_rate": round(total_demote / len(recs), 4) if recs else 0.0,
"mean_candidate_clauses": round(n_candidate_clauses_total / len(recs), 2) if recs else 0.0,
"max_candidate_clauses_cap": nli.max_candidate_clauses,
"by_bucket": {k: {**v, "rate": round(v["would_demote"] / v["n"], 4) if v["n"] else 0.0}
for k, v in sorted(by_bucket.items())},
"by_recombination_risk": {k: {**v, "rate": round(v["would_demote"] / v["n"], 4) if v["n"] else 0.0}
for k, v in by_recomb_risk.items()},
"false_positive_probe": {**fp_probe,
"rate": round(fp_probe["would_demote"] / fp_probe["n"], 4) if fp_probe["n"] else None,
"note": "would_demote on records labeled want=not_contradiction — these are shadow FALSE POSITIVES; this is §7 #12 gate item 4 when the input is a real legit-answer sample"},
@ -146,8 +157,13 @@ def main(argv=None) -> int:
print(" (install the runtime to get real numbers: pip install 'arborist[nli]')")
print(f" swept {len(recs)} records structurally; would-demote rate not measured.")
return 0
print(f" model {nli.model_version} · θc={nli.theta_contra} θe={nli.theta_entail} · {len(recs)} records · {elapsed:.1f}s")
print(f" model {nli.model_version} · θc={nli.theta_contra} θe={nli.theta_entail} · cand-clause cap {nli.max_candidate_clauses} · {len(recs)} records · {elapsed:.1f}s")
print(f" mean candidate clauses NLI'd / record: {report['mean_candidate_clauses']} (vs the whole context — the §7 #20 restriction)")
print(f" would_demote overall: {total_demote}/{len(recs)} = {report['would_demote_rate']:.3f}")
rr = report["by_recombination_risk"]
print(f" by recombination-risk: risk {rr['risk']['would_demote']}/{rr['risk']['n']}={rr['risk']['rate']:.3f} "
f"no_risk {rr['no_risk']['would_demote']}/{rr['no_risk']['n']}={rr['no_risk']['rate']:.3f} "
f"(if no_risk-rate is high too, the risk-gate alone isn't enough)")
print(f" {'bucket':<28} {'n':>5} {'would_demote':>13} {'rate':>7}")
print(" " + "-" * 56)
for k, v in report["by_bucket"].items():