qa/deflection: shape suppression — kill subject-anchor false positives

Bench dig (2026-05-01, 65-question sweep): 4 of 7 deflecting
questions were false positives from verb-shape subject anchors.

  what year did the berlin wall fall?  → answer "1989"  (anchor "fall")
  when did the soviet union dissolve?  → answer "1991"  (anchor "dissolve")
  how many wives did henry the eighth? → answer "six"   (anchor "eighth")
  what year does our cold fusion ...?  → answer "1989"  (anchor "happen")
  why did the titanic sink?            → "...sank in 1912" (anchor "sink" vs "sank")
  why did the dinosaurs go extinct?    → "asteroid impact"  (anchor "extinct" vs "died")

These shapes ask for a year/count/cause; the answer is structurally
not expected to echo the question's subject. Pre-suppression, all
three modes landed deflection on these — the apparent
"pointer-only" deflection rate (10.3% vs 6.2%) was almost entirely
heuristic noise, not a real substrate failure.

New ``shape_suppressed`` flag returned by diagnose_deflection. When
the question starts with one of:
  when, what year, what date, what time,
  how many, how much, how long, how old, why
the subject-anchor check is skipped — overlap-ratio takes over.
Mars-BDFL (who-shape) still fires correctly.

Field exposed so the operator can filter the signal: a
shape_suppressed=True row that still classifies as deflection on
overlap-ratio means "answer was a bare year/count/cause with no
topic words" — known noisy, not real topic shift.

5 new unit tests; full suite 582 passed (was 578).
This commit is contained in:
russell@unturf.com 2026-05-01 06:37:17 -04:00
parent 2f60b6533c
commit 8506a7068f
No known key found for this signature in database
2 changed files with 111 additions and 12 deletions

View file

@ -390,6 +390,40 @@ def _content_tokens_in_order(text: str) -> list[str]:
return out
# Question-shape leaders that *structurally* don't expect the answer
# to echo the question's subject — date/count/cause shapes where the
# answer is a year, a number, or a cause-explanation that may or may
# not re-mention the topic. Bench evidence (2026-05-01, 65-question
# sweep, n=3): subject-anchor heuristic produced false-positive
# deflections on 4 of 7 deflecting questions, all of these shapes:
# 'when did the soviet union dissolve?' (answer '1991'),
# 'what year did the berlin wall fall?' ('1989'),
# 'how many wives did henry the eighth have?' ('six'),
# 'what year does our cold fusion breakthrough happen?' ('1989').
# For these shapes, suppress the subject-anchor check entirely —
# fall back to overlap-ratio and accept that some short numeric
# answers will read as zero-overlap deflection (a known noisy
# signal we won't pretend is more reliable than it is).
_DEFLECTION_NUMERIC_OR_CAUSE_LEADERS = (
"when ",
"what year ",
"what date ",
"what time ",
"how many ",
"how much ",
"how long ",
"how old ",
"why ",
)
def _question_is_numeric_or_cause_shape(question_text: str) -> bool:
"""Returns True for shapes whose answer doesn't structurally
echo the question's subject (date/count/cause)."""
q = (question_text or "").strip().lower()
return any(q.startswith(lead) for lead in _DEFLECTION_NUMERIC_OR_CAUSE_LEADERS)
def diagnose_deflection(question_text: str, answer_text: str) -> dict[str, Any]:
"""Soft signal: did the answer change topic?
@ -402,18 +436,24 @@ def diagnose_deflection(question_text: str, answer_text: str) -> dict[str, Any]:
Detection rule:
1. **Subject anchor**: the LAST content token in the question
(after stopword strip) is a strong heuristic for the question's
primary subject ('mars' in the BDFL case, 'beatles' in 'who
are the members of the beatles?'). If the subject anchor
doesn't appear in the answer → ``deflection``.
2. Otherwise, fall back to the overlap-ratio heuristic (1.0 →
``on_topic``, partial → ``partial_overlap``, 0 → ``deflection``).
1. **Numeric/cause-shape suppression**: questions starting with
``when``/``what year``/``how many``/``how much``/``why``/etc.
have answers that don't structurally echo the subject (a year,
a count, a cause-explanation). For these, skip the subject-
anchor check and use overlap-ratio only — bench finding
2026-05-01: subject-anchor produced 4 false-positive
deflections out of 7 across these shapes.
2. **Subject anchor (default)**: the LAST content token in the
question (after stopword strip) is a strong heuristic for the
question's primary subject ('mars' in the BDFL case, 'beatles'
in 'who are the members of the beatles?'). If the subject
anchor doesn't appear in the answer → ``deflection``.
3. Otherwise, fall back to overlap-ratio: 1.0 → ``on_topic``,
partial → ``partial_overlap``, 0 → ``deflection``.
Subject-anchor is more robust than zero-overlap because the
BDFL-Mars case has 3/4 generic-vocabulary overlap (benevolent,
dictator, life) while still being a clear topic shift. The signal
we want is "the *subject* is missing", not "all tokens are missing."
Subject-anchor catches BDFL-Mars (3/4 generic vocab overlap with
missing subject) but mis-fires on numeric/cause shapes — the
leader-suppression closes that gap.
Returns:
{
@ -424,6 +464,8 @@ def diagnose_deflection(question_text: str, answer_text: str) -> dict[str, Any]:
"overlap_ratio": float, # |overlap| / |question_tokens|
"subject_anchor": str | None,
"subject_in_answer": bool,
"shape_suppressed": bool, # True if subject-anchor was
# skipped due to numeric/cause shape
}
Sidecar-only — never enters the binary verifier output."""
@ -439,11 +481,13 @@ def diagnose_deflection(question_text: str, answer_text: str) -> dict[str, Any]:
"overlap_ratio": 0.0,
"subject_anchor": None,
"subject_in_answer": False,
"shape_suppressed": False,
}
shape_suppressed = _question_is_numeric_or_cause_shape(question_text)
subject_anchor = q_in_order[-1] if q_in_order else None
subject_in_answer = subject_anchor in atok if subject_anchor else False
overlap = qtok & atok
if subject_anchor and not subject_in_answer:
if not shape_suppressed and subject_anchor and not subject_in_answer:
# Subject token missing dominates — even if 3/4 generic vocab
# overlapped, the actual topic isn't in the answer.
kind = "deflection"
@ -461,6 +505,7 @@ def diagnose_deflection(question_text: str, answer_text: str) -> dict[str, Any]:
"overlap_ratio": len(overlap) / len(qtok),
"subject_anchor": subject_anchor,
"subject_in_answer": subject_in_answer,
"shape_suppressed": shape_suppressed,
}

View file

@ -484,3 +484,57 @@ def test_deflection_strips_possessive_s_for_overlap():
)
assert "mars" in d["overlap"]
assert "atmosphere" in d["overlap"]
def test_deflection_suppressed_for_when_year_questions():
"""Bench finding 2026-05-01: 'what year did the berlin wall fall?'
answer '1989' has zero subject overlap (subject anchor 'fall' is
a verb the answer doesn't echo). Pre-suppression, classified as
deflection across all three modes (false positive)."""
d = diagnose_deflection(
"what year did the berlin wall fall?",
"1989.",
)
assert d["shape_suppressed"] is True
# Even with shape_suppressed, zero overlap on a date answer falls
# back to overlap-ratio = 0 → deflection. The shape_suppressed
# field tells the operator the signal was a known false-positive
# shape, not an actual topic shift.
assert d["kind"] == "deflection" # overlap-ratio path still fires
# But the operator can filter on shape_suppressed when scoring.
def test_deflection_suppressed_when_numeric_answer_overlaps():
"""When the date answer DOES include topic words ('Berlin Wall
fell in 1989'), shape suppression doesn't matter — overlap is
high enough for partial_overlap or on_topic."""
d = diagnose_deflection(
"what year did the berlin wall fall?",
"The Berlin Wall fell in 1989, ending the Cold War.",
)
assert d["shape_suppressed"] is True
assert d["kind"] in ("on_topic", "partial_overlap")
def test_deflection_suppressed_for_why_questions():
"""WHY-shape questions: answer discusses causes, may or may not
echo the question's verb. Bench: pointer mode deflected 3/3 on
'why did the titanic sink?' but answer was a valid cause."""
d = diagnose_deflection(
"why did the titanic sink?",
"The Titanic struck an iceberg and sank in 1912.",
)
assert d["shape_suppressed"] is True
# Has overlap on titanic+sink, classifies as on_topic or partial.
assert d["kind"] in ("on_topic", "partial_overlap")
def test_deflection_not_suppressed_for_who_questions():
"""Mars-BDFL pattern still fires on who-shape questions."""
d = diagnose_deflection(
"who is a benevolent dictator for life for mars?",
"Guido van Rossum is a Benevolent Dictator For Life for Python.",
)
assert d["shape_suppressed"] is False
assert d["kind"] == "deflection"
assert d["subject_anchor"] == "mars"