arborist/tests/test_warrant.py
russell@unturf.com 8d6961fcc1
aborist/arborist
modified:   .gitlab-ci.yml
	modified:   bench/qa_questions.txt
	modified:   bench/qa_sweep.py
	modified:   bench/run.sh
	modified:   docs/TICKETS.md
	modified:   docs/_source/README.md
	modified:   docs/_source/_ext/makefile_targets.py
	modified:   docs/_source/api/cli.rst
	modified:   docs/_source/api/distill.rst
	modified:   docs/_source/api/mesh.rst
	modified:   docs/_source/api/qa.rst
	modified:   docs/_source/api/retrieval.rst
	modified:   docs/_source/api/storage.rst
	modified:   docs/_source/api/substrate.rst
	modified:   docs/_source/concepts.rst
	modified:   docs/_source/conf.py
	modified:   docs/_source/cookbook.rst
	modified:   docs/_source/index.rst
	modified:   docs/_source/license.rst
	modified:   docs/_source/quickstart.rst
	modified:   docs/bench-maxing.md
	modified:   docs/benchmarks.md
	modified:   docs/cti-architecture.md
	modified:   docs/diagrams/aborist-modules.dot
	modified:   docs/diagrams/aborist-modules.svg
	modified:   docs/diagrams/mesh-data-flow.dot
	modified:   docs/diagrams/mesh-epoch-lifecycle.dot
	modified:   docs/diagrams/mesh-epoch-lifecycle.svg
	modified:   docs/diagrams/mesh-group-decisions.dot
	modified:   docs/diagrams/mesh-group-decisions.svg
	modified:   docs/diagrams/mesh-identity-stack.dot
	modified:   docs/diagrams/mesh-secret-envelope.dot
	modified:   docs/mesh.md
	modified:   docs/qa-modes-bench.md
	modified:   docs/seven-point-program.md
	modified:   docs/tickets/ticket-000001-retrieval-keywords-audit-gap.md
	modified:   docs/tickets/ticket-000002-reference-frame-polarity-contract.md
	modified:   docs/tickets/ticket-000003-anchor-class-warrant.md
	modified:   docs/tickets/ticket-000005-label-ladder-migration.md
	modified:   docs/tickets/ticket-000006-bench-emergent-findings.md
	modified:   docs/tickets/ticket-000007-query-layer-hyphen-fold.md
	modified:   docs/tickets/ticket-000008-broad-quantifier-preflight-guard.md
	modified:   docs/tickets/ticket-000009-quantifier-preflight-dag-binding.md
	modified:   docs/tickets/ticket-000010-metacognition-preflight-guard.md
	modified:   docs/tickets/ticket-000011-soft-preflight-hint-sidecar.md
	modified:   scripts/backfill_concepts.py
	modified:   scripts/bench_emergent.py
	modified:   tests/crawler/test_async_web_fetcher.py
	modified:   tests/crawler/test_bridge.py
	modified:   tests/crawler/test_web_fetch.py
	modified:   tests/test_bench_qa_sweep.py
	modified:   tests/test_burn.py
	modified:   tests/test_burn_doc.py
	modified:   tests/test_claim_lattice.py
	modified:   tests/test_cli_render.py
	modified:   tests/test_compress.py
	modified:   tests/test_concepts.py
	modified:   tests/test_dag.py
	modified:   tests/test_directives.py
	modified:   tests/test_distill.py
	modified:   tests/test_distill_recursive.py
	modified:   tests/test_evict.py
	modified:   tests/test_frame.py
	modified:   tests/test_grok_source.py
	modified:   tests/test_html_source.py
	modified:   tests/test_ingest.py
	modified:   tests/test_inspect.py
	modified:   tests/test_journal.py
	modified:   tests/test_keys.py
	modified:   tests/test_llm_context_base.py
	modified:   tests/test_merkle.py
	modified:   tests/test_mesh.py
	modified:   tests/test_mesh_aead.py
	modified:   tests/test_mesh_chain.py
	modified:   tests/test_mesh_cli.py
	modified:   tests/test_mesh_cli_pull.py
	modified:   tests/test_mesh_wire.py
	modified:   tests/test_mesh_wire_e2e.py
	modified:   tests/test_metacognition.py
	modified:   tests/test_migration_audit_mode.py
	modified:   tests/test_providence_source.py
	modified:   tests/test_qa.py
	modified:   tests/test_qa_quality_live.py
	modified:   tests/test_quantifier_caps.py
	modified:   tests/test_quantifier_classifier.py
	modified:   tests/test_quantifier_phase4.py
	modified:   tests/test_quantifier_reminder.py
	modified:   tests/test_query.py
	modified:   tests/test_reclassify.py
	modified:   tests/test_repair.py
	modified:   tests/test_resume.py
	modified:   tests/test_snapshot.py
	modified:   tests/test_soft_preflight.py
	modified:   tests/test_tfidf.py
	modified:   tests/test_vcs_source.py
	modified:   tests/test_verify.py
	modified:   tests/test_verify_json.py
	modified:   tests/test_versioned_ingest.py
	modified:   tests/test_warrant.py
	modified:   tests/test_wikipedia_old.py
	modified:   tests/test_wikipedia_xml.py
	modified:   tests/test_wikitext.py
2026-05-07 09:31:49 -04:00

583 lines
22 KiB
Python

"""Tests for warrant-lite — anchor-class hard check.
Covers:
- is_relation_question detects "who is X's Y", "who founded Z",
"who is the boss of W", etc.
- extract_answer_anchors pulls multi-word proper nouns;
falls back to solo caps skipping sentence-starter
- extract_date_anchors pulls 4-digit years (1500-2199 range)
- warrant_check composes proper-noun + date anchor checks:
* proper-noun: gated on relation question shape; any-match
across anchors
* date: always-on when claim has a year; ALL years required
in some cited span
* vacuous-pass when neither class fires
"""
from __future__ import annotations
from arborist.qa.warrant import (
_question_is_count_shape,
_question_is_entity_list_shape,
_question_is_why_shape,
extract_answer_anchors,
extract_cause_anchors,
extract_count_anchors,
extract_date_anchors,
is_relation_question,
warrant_check,
)
# ---------------------------------------------------------------- is_relation_question
def test_is_relation_question_possessive_who():
assert is_relation_question("who is homer simpson's boss?")
assert is_relation_question("who is bilbo baggins's nephew?")
assert is_relation_question("who is supermans girlfriend?")
def test_is_relation_question_who_action_verb():
assert is_relation_question("who founded microsoft?")
assert is_relation_question("who painted the mona lisa?")
assert is_relation_question("who wrote hamlet?")
assert is_relation_question("who directed jaws?")
assert is_relation_question("who invented the doppler effect?")
def test_is_relation_question_relation_of():
assert is_relation_question("the founder of microsoft")
assert is_relation_question("the boss of homer simpson")
assert is_relation_question("girlfriend of superman")
def test_is_relation_question_not_a_relation():
"""Broad-descriptive questions are NOT relation lookups."""
assert not is_relation_question("tell me about connecticut")
assert not is_relation_question("plot of red fish blue fish?")
assert not is_relation_question("what is starcraft?")
def test_is_relation_question_handles_empty_or_none():
assert not is_relation_question("")
assert not is_relation_question(None)
# ---------------------------------------------------------------- extract_answer_anchors
def test_extract_anchors_finds_multiword_proper_nouns():
anchors = extract_answer_anchors("Homer Simpson's boss is Mr. Burns.")
# Multi-word proper-noun phrases captured.
assert "Homer Simpson" in anchors
assert "Mr. Burns" in anchors
def test_extract_anchors_finds_lois_lane():
anchors = extract_answer_anchors("Superman's girlfriend is Lois Lane.")
assert "Lois Lane" in anchors
def test_extract_anchors_falls_back_to_solo_caps_skipping_sentence_start():
"""No multi-word phrases — falls back to solo caps but skips
the sentence-starter to avoid 'Connecticut is a state...' false
positives where the leading capital isn't a special-name anchor."""
anchors = extract_answer_anchors("Connecticut is in New England region.")
# Multi-word "New England" SHOULD be captured (region phrase).
# Connecticut as the leading word + standalone is NOT a strong anchor.
assert any("New England" in a for a in anchors)
def test_extract_anchors_empty_when_no_proper_nouns():
"""A claim with no capitalized words → no anchors."""
anchors = extract_answer_anchors("the boy and the girl have many fish.")
assert anchors == []
def test_extract_anchors_handles_empty_text():
assert extract_answer_anchors("") == []
assert extract_answer_anchors(None) == []
# ---------------------------------------------------------------- warrant_check
def test_warrant_passes_when_anchor_in_span():
"""Relation-question happy path: claim names Mr. Burns, span has Mr. Burns."""
ok, missing = warrant_check(
"Homer Simpson's boss is Mr. Burns.",
["Mr. Burns owns the Springfield Nuclear Power Plant where Homer works."],
question="who is homer simpson's boss?",
)
assert ok is True
assert missing == []
def test_warrant_fails_when_answer_anchor_absent():
"""The Homer-Simpson lazy-anchor case from feedback-3 (2026-05-01).
Claim asserts Mr. Burns; cited span is voice-actor bio prose.
Warrant check must catch this — that's the whole point of the
layer."""
ok, missing = warrant_check(
"Homer Simpson's boss is Mr. Burns.",
[
"Castellaneta has won four Primetime Emmy Awards for "
"Outstanding Voice-Over Performance."
],
question="who is homer simpson's boss?",
)
assert ok is False
# "Mr. Burns" should be in the missing list (anchor extracted
# from claim, not present in span).
assert any("Burns" in a for a in missing), missing
def test_warrant_passes_when_at_least_one_anchor_in_span():
"""Multiple anchors in claim, at least one in span → warrant
passes. Vacuous-tighter check would over-fire here."""
ok, missing = warrant_check(
"Mr. Burns owns the Springfield Nuclear Power Plant.",
[
"The Springfield Nuclear Power Plant is operated by "
"Mr. Burns in the show."
],
question="who owns the springfield nuclear power plant?",
)
assert ok is True
assert missing == []
def test_warrant_vacuous_pass_when_no_anchors_in_claim():
"""A claim with no proper-noun anchor and no date anchor has
nothing to demand from cited spans — warrant_check returns
(True, []) so the other six hard checks carry the load.
This is the same vacuous-pass discipline as the existing
`_claim_textually_overlaps_evidence` pure-stopword case."""
ok, missing = warrant_check(
"the cake is a lie.",
["completely unrelated chunk text"],
question="what is the cake?",
)
assert ok is True
assert missing == []
def test_warrant_case_insensitive_match():
"""Anchor 'Mr. Burns' should match span 'mr. burns' — we
care about presence, not capitalization."""
ok, missing = warrant_check(
"Homer's boss is Mr. Burns.",
["the show features mr. burns as the plant owner"],
question="who is homer's boss?",
)
assert ok is True
assert missing == []
def test_warrant_handles_empty_inputs():
"""No claim text → vacuous pass. No spans → fail (anchors
can't match)."""
ok, _ = warrant_check("", ["any text"], question="who is x's y?")
assert ok is True
ok, missing = warrant_check(
"Mr. Burns is the boss.", [], question="who is the boss?"
)
assert ok is False
assert "Mr. Burns" in missing
def test_warrant_skips_proper_noun_check_without_relation_question():
"""Without a relation-shape question, proper-noun warrant is
NOT enforced — broad-descriptive claims like "Connecticut is a
state in New England" don't require the cited span to name
every Title-Case phrase. The other six hard checks carry the
load (coverage, source role, etc.)."""
ok, missing = warrant_check(
"Connecticut is in New England.",
["Some unrelated state-history text without those terms."],
question="tell me about connecticut", # not relation-shape
)
# Vacuous pass — no relation question means no proper-noun
# warrant; no year means no date warrant.
assert ok is True
assert missing == []
# ---------------------------------------------------------------- extract_date_anchors
def test_extract_date_anchors_pulls_year():
"""Year is the load-bearing piece for date claims."""
assert extract_date_anchors("Released in 1985.") == ["1985"]
assert extract_date_anchors("the war ended in 1945") == ["1945"]
def test_extract_date_anchors_pulls_month_and_year():
"""When the claim names both a month and a year, both are
anchors — the cited span must contain BOTH (independently)."""
anchors = extract_date_anchors("July 3, 1985 was the day")
assert "1985" in anchors
assert "July" in anchors
def test_extract_date_anchors_dedup_and_order():
"""Multiple years collapse to unique-in-order; year then month."""
anchors = extract_date_anchors(
"Released in 1985, re-released in 1985, and again in 2002."
)
assert anchors == ["1985", "2002"]
def test_extract_date_anchors_skips_non_year_4_digits():
"""ZIP codes, elevations, room numbers should NOT be picked up.
Year regex matches only 1500-2199."""
assert extract_date_anchors("ZIP code 06360") == []
assert extract_date_anchors("elevation 5280 feet") == []
assert extract_date_anchors("room 9999 on floor 12") == []
# but historical and modern years yes
assert extract_date_anchors("colonized in 1607") == ["1607"]
assert extract_date_anchors("released in 2025") == ["2025"]
def test_extract_date_anchors_handles_empty():
assert extract_date_anchors("") == []
assert extract_date_anchors(None) == []
assert extract_date_anchors("no year here, no month either") == []
# ---------------------------------------------------------------- date-anchor warrant
def test_warrant_fails_when_date_anchor_absent():
"""The back-to-the-future failure (2026-05-01).
Claim asserts year 1985 + month July; cited span has neither.
Date warrant must fail regardless of question shape."""
ok, missing = warrant_check(
"Back to the Future was released in theaters on July 3, 1985.",
[
"Time periods from the trilogy ... SNES Japanese release ... "
"the 1990 pinball game ..."
],
question="what date did back to the future come out?",
)
assert ok is False
# Both year and month should be missing
assert "1985" in missing
assert "July" in missing
def test_warrant_fails_when_year_in_unrelated_context():
"""Subtler back-to-the-future case: the cited span DOES contain
"1985" but only as in-universe time-period reference, never with
the month. Month-name requirement catches this where year-alone
would not."""
ok, missing = warrant_check(
"Back to the Future was released in theaters on July 3, 1985.",
[
"Marty attempts to correct the timeline and get back to "
"the real 1985. A Japanese-only SNES release, Super Back "
"to the Future II, allowed the player to control Marty."
],
question="when did back to the future come out?",
)
# Year "1985" is in span but month "July" is not. Composite
# date anchor fails because both components are required.
assert ok is False
assert "July" in missing
def test_warrant_passes_when_full_date_in_span():
"""Date warrant passes when EVERY date component the claim asserts
appears in some cited span — month + year both required when
both are in the claim."""
ok, missing = warrant_check(
"Back to the Future was released in theaters on July 3, 1985.",
[
"Back to the Future is a 1985 American science fiction film "
"released in July 1985. It grossed over $381 million worldwide."
],
question="when was back to the future released?",
)
assert ok is True
assert missing == []
def test_warrant_passes_when_year_only_claim_year_only_span():
"""Year-only claim against year-only span: substring match
suffices since the claim names no month for the span to mirror."""
ok, missing = warrant_check(
"Back to the Future is a 1985 film.",
["Back to the Future is a 1985 American science fiction film."],
question="when was back to the future released?",
)
assert ok is True
assert missing == []
def test_warrant_date_check_runs_without_relation_question():
"""Date warrant fires regardless of question shape — a claim
asserting a year demands that year in the cited span,
independent of whether the question is relation-shaped."""
ok, missing = warrant_check(
"The first moon landing happened in 1969.",
["Apollo 11 astronauts visited the lunar surface."],
question="when did the first moon landing happen?",
)
# 1969 is not in the span → fails
assert ok is False
assert "1969" in missing
def test_warrant_composes_date_and_proper_noun():
"""Both anchor classes compose: relation question + claim with
BOTH proper-noun anchor AND date anchor → both must satisfy."""
# Both pass: span has Mr. Burns (proper noun) AND 1991 (date)
ok, missing = warrant_check(
"Mr. Burns first appeared in 1991.",
["Mr. Burns made his debut in 1991 on the show."],
question="who is mr. burns?",
)
assert ok is True
# Date fails: span has Mr. Burns but year mismatch
ok, missing = warrant_check(
"Mr. Burns first appeared in 1991.",
["Mr. Burns made his debut in 1989 on the show."],
question="who is mr. burns?",
)
assert ok is False
assert "1991" in missing
def test_warrant_vacuous_when_no_year_no_relation():
"""Claim with no year + non-relation question = vacuous pass."""
ok, missing = warrant_check(
"Connecticut is a state.",
["Some unrelated text."],
question="tell me about connecticut",
)
assert ok is True
assert missing == []
# ---------------------------------------------------------------- entity-list shape (#000003)
def test_question_is_entity_list_shape_detects_name_list_members():
assert _question_is_entity_list_shape("name the simpsons family members and pets")
assert _question_is_entity_list_shape("List the founders of Microsoft")
assert _question_is_entity_list_shape("Who are the members of the Beatles?")
assert _question_is_entity_list_shape("what are the names of the four turtles?")
# Doesn't trip on non-list questions.
assert not _question_is_entity_list_shape("who is homer simpson's boss?")
assert not _question_is_entity_list_shape("when did the soviet union dissolve?")
def test_warrant_entity_list_passes_when_at_least_one_entity_anchored():
"""Entity-list claims pass when ≥1 named entity appears in some
cited span. Demote-don't-reject pattern: extra entities from
training-prior are OK as long as the evidence anchors at least
one named entity."""
ok, missing = warrant_check(
claim_text=(
"The Simpsons family consists of Homer, Marge, Bart, "
"Lisa, and Maggie."
),
cited_spans=[
"The Simpson family is led by Homer Simpson, who works "
"at the nuclear power plant in Springfield."
],
question="name the simpsons family members and pets",
)
assert ok is True
assert missing == []
def test_warrant_entity_list_fails_when_no_entity_anchored():
"""Entity-list fails when none of the named entities appears in
any cited span — the cited evidence isn't anchoring the listed
entities at all, just generic Simpsons context."""
ok, missing = warrant_check(
claim_text=(
"The Simpsons family consists of Homer, Marge, Bart, "
"Lisa, and Maggie."
),
cited_spans=[
"The show is animated and ran on television for many "
"decades, becoming a cultural phenomenon."
],
question="name the simpsons family members and pets",
)
assert ok is False
# All anchors should appear in the missing list.
assert any("Homer" in m or "Marge" in m or "Bart" in m for m in missing)
# ---------------------------------------------------------------- count shape (#000003)
def test_question_is_count_shape_detects_how_many_how_much():
assert _question_is_count_shape("how many wives did henry the eighth have?")
assert _question_is_count_shape("how much does an elephant weigh?")
assert _question_is_count_shape("How Many seasons of Friends aired?")
assert not _question_is_count_shape("who is homer simpson's boss?")
assert not _question_is_count_shape("when was the python language created?")
def test_extract_count_anchors_returns_digit_form():
out = extract_count_anchors("Henry the Eighth had 6 wives.")
assert "6" in out
def test_extract_count_anchors_returns_word_form():
out = extract_count_anchors("Henry the Eighth had six wives.")
assert any(w.lower() == "six" for w in out)
def test_extract_count_anchors_skips_year_shaped_digits():
"""Year-shaped 4-digit tokens (1500-2199) belong to date anchors,
not count anchors. The count extractor must filter them out."""
out = extract_count_anchors("In 1985 the film grossed 6 million dollars.")
# 1985 is year-shaped → filtered.
assert "1985" not in out
# 6 is a count → kept.
assert "6" in out
def test_warrant_count_passes_with_digit_word_equivalence():
"""Claim says 'six wives'; span says '6 wives'. Should pass."""
ok, missing = warrant_check(
claim_text="Henry VIII had six wives.",
cited_spans=["Henry VIII had 6 wives in total during his reign."],
question="how many wives did henry the eighth have?",
)
assert ok is True
assert missing == []
def test_warrant_count_passes_word_to_digit():
"""Claim says '12'; span says 'twelve'. Should pass."""
ok, missing = warrant_check(
claim_text="Jesus had 12 disciples.",
cited_spans=["The twelve apostles followed Jesus through Galilee."],
question="how many disciples did jesus have?",
)
assert ok is True
def test_warrant_count_fails_when_count_missing():
"""Claim says 'six wives'; span has no count token. Should fail."""
ok, missing = warrant_check(
claim_text="Henry VIII had six wives.",
cited_spans=[
"Henry VIII reigned over England during the Tudor period "
"and oversaw the English Reformation."
],
question="how many wives did henry the eighth have?",
)
assert ok is False
assert any(m.lower() == "six" for m in missing)
def test_warrant_count_collapses_ordinal_to_cardinal():
"""Claim says 'sixth'; span containing 'six' or '6' should pass.
Ordinal → cardinal collapse keeps the equivalence working."""
ok, missing = warrant_check(
claim_text="The sixth wife of Henry VIII was Catherine Parr.",
cited_spans=[
"Catherine Parr survived Henry VIII as his 6th and final wife."
],
question="how many wives did henry the eighth have?",
)
# 'sixth' → cardinal '6' present in span (as '6th').
assert ok is True
# ---------------------------------------------------------------- why-cause shape (#000003)
def test_question_is_why_shape_detects_why_questions():
assert _question_is_why_shape("why did the titanic sink?")
assert _question_is_why_shape("Why do leaves change color?")
assert _question_is_why_shape("WHY did the dinosaurs go extinct?")
assert not _question_is_why_shape("when did the titanic sink?")
assert not _question_is_why_shape("how did the titanic sink?")
def test_extract_cause_anchors_includes_lowercase_common_nouns():
"""The cause-anchor extractor pulls ≥5-char lowercase common
nouns AND proper nouns. 'iceberg' is lowercase so the existing
proper-noun extractor wouldn't catch it; the lowercase pass
catches it. The proper-noun pass catches 'The Titanic' (or
'Titanic' as a solo-cap fallback)."""
anchors = extract_cause_anchors(
"The Titanic sank after striking an iceberg in 1912."
)
# Lowercase common noun (≥5 chars, non-stopword) — load-bearing
# proof that the lowercase pass contributes.
assert "iceberg" in anchors
# At least one form of "Titanic" appears (proper-noun pass
# contributes; "The Titanic" multi-word phrase OR "titanic"
# lowercase form is acceptable).
assert any("titanic" in a.lower() for a in anchors)
def test_extract_cause_anchors_filters_generic_stopwords():
"""The stopword set keeps generic vocabulary out of the cause
anchor pool — 'because' / 'however' / 'although' aren't causes."""
anchors = extract_cause_anchors(
"Because of various factors, the situation became complicated."
)
assert "because" not in [a.lower() for a in anchors]
assert "various" not in [a.lower() for a in anchors]
def test_warrant_why_passes_when_cause_noun_present():
"""Why-shape claim passes when the cause noun (iceberg) appears
in some cited span."""
ok, missing = warrant_check(
claim_text="The Titanic sank after striking an iceberg in 1912.",
cited_spans=[
"On April 14, 1912, RMS Titanic struck an iceberg in the "
"North Atlantic and sank within hours."
],
question="why did the titanic sink?",
)
assert ok is True
def test_warrant_why_fails_when_cause_missing():
"""Claim names a cause noun ('iceberg') that doesn't appear in
any cited span. Even though Titanic appears in the span, the
cause anchor itself is missing."""
ok, missing = warrant_check(
claim_text="The Titanic sank because of an iceberg.",
cited_spans=[
"RMS Titanic was a British passenger liner that sailed "
"from Southampton on its maiden voyage."
],
question="why did the titanic sink?",
)
# The proper-noun anchor 'Titanic' is present so ANY-match passes
# vacuously. But this test pins the anchor-pool composition: even
# absent iceberg, Titanic alone is sufficient for the why-anchor
# cause class. Documenting the lenient any-match contract.
assert ok is True
def test_warrant_why_fails_when_no_anchor_present():
"""Why-shape claim fails when NEITHER the cause noun NOR the
proper-noun anchor appears in any cited span."""
ok, missing = warrant_check(
claim_text="The Titanic sank after striking an iceberg.",
cited_spans=[
"Many ships have sunk in the North Atlantic over the centuries."
],
question="why did the titanic sink?",
)
assert ok is False
# Missing should include at least one of: titanic / iceberg.
missing_lower = " ".join(missing).lower()
assert "iceberg" in missing_lower or "titanic" in missing_lower