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
583 lines
22 KiB
Python
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
|