arborist/tests/test_claim_lattice.py
russell@unturf.com 7bb11ed62f
#000048 step 2.4 — parse_pointer_claims clause segmentation
Closes the 8 mis-segments #000046 left in formulate-hard-v1.jsonl.
The parser was line/bullet-only — one line ⇒ one claim — so a line
that crammed several pointered claims onto one row ("Water is wet
[E1]; fire is hot [E2]", "X happened [E1]. Y followed [E2]") became
one monolithic claim with all the pointers, and a wrapped bullet
became two.

arborist/qa/parse_claims.py: _SEGMENT_SEP_RE splits a line on ';',
sentence boundaries ('. '/'! '/'? ' then a Capital), spaced dashes
(' - '/' — '/' – '), ' and '/' or '/' because '/' although '/' since
'/' while ', inline '(N)' enumeration markers, and commas — with
'(?![^\[]*\])' so a comma inside a [E1, E2] bracket never splits it.
_segment_line keeps the split ONLY IF every resulting non-empty
segment is a well-pointered claim — a legit single claim ("The cat
is black and white [E1].", "The cast: A, B, C [E1].") is never
broken because splitting it would manufacture pointer-less prose
fragments → guard rejects; a leading colon-terminated header with no
pointer ("Two facts:", "Key points:") is allowed and dropped. Plus a
wrapped-bullet join: a continuation line (leading whitespace then a
lowercase letter, no bullet glyph) folds its text + pointers into the
previous claim.

Effect: formulate-hard rate 4/12 → 12/12 (the pack is now at ceiling
— a harder Formulate tier would re-open below-ceiling headroom; a
#000046 follow-up). Remaining #000048 headroom: 2 STRICT_PARAPHRASE
recombinations in falsification-hard (Mercury, Einstein — step 2.2).

Bench gate: make bench-qa (n=3 × 75 × 3 = 675 cells; parse_pointer_claims
feeds the 450 claim_lattice_pointer + claim_lattice cells) after
(bench/qa_results/2026-05-11T20-26-37Z) vs the pre-step-2.4 baseline
(...T17-12-41Z = HEAD's parse_claims.py). STRICT-rate quote 0.54→0.55,
pointer 0.22→0.22, lattice 0.43→0.45 — all within the 5-pp noise
floor. Per-row diff: the segmenter changed the parsed-claim count on
the SAME answer text for 7 of the 450 lattice cells (0 in
claim_lattice, 7 in claim_lattice_pointer); of those, 2 caused an
audit_mode change — both correct: a wrap-join recovered an answer's
intended structure (4 claims, 2 pointer-less wrap-fragments → HYBRID)
into 2 well-pointered claims → STRICT; and a crammed-one-line blob (1
monolithic claim, all pointers → STRICT) split into 8 claims, some
not individually verifying → HYBRID (the honest verdict — false-
positive STRICT was the corruption). Every other lattice/quote delta
is LLM re-answer variance. No regression — the segmenter's only
visible effects on real traffic are honest improvements. Summarized
in qa-modes-bench.md Addendum 7 + ticket-000048 §5 step 2.4.

Tests: 8 new in test_claim_lattice.py (semicolon/sentence/conjunction
splits; pointerless-fragment + cast-list guards; leading-colon-header
drop; wrapped-bullet join; pointer-order/multi-pointer); existing
parse_pointer_claims tests pass untouched; test_5f_formulate_hard_pack
re-pinned 4/12 → 12/12. make test 2358 passed, 28 skipped.

#000048 → steps 2.1 + 2.4 landed; #000046 / #000012 §8 / TICKETS.md /
Makefile / fixture _meta + notes updated.
2026-05-11 17:09:06 -04:00

1292 lines
48 KiB
Python

"""G0 / CTI — claim-lattice-pointer mode (quote-by-pointer).
Covers the pointer-line protocol Hermes emits, plus the runtime
two-layer id mapping (prompt-facing E1/E2/…, content-addressed
E########). Tests the deterministic verifier + 9-stage run-DAG +
schema migration round-trip.
"""
from __future__ import annotations
import sqlite3
from pathlib import Path
import pytest
from arborist.qa.dag import build_run_dag, verify_run_dag
from arborist.qa.evidence import (
build_evidence_map,
evidence_map_by_evidence_id,
evidence_map_by_pointer_id,
evidence_map_root,
render_claim_lattice,
render_evidence_map,
)
from arborist.qa.keys import governance_policy_hash
from arborist.qa.parse_claims import parse_pointer_claims
from arborist.qa.runner import DEFAULT_POLICY
from arborist.qa.query import DEFAULT_QUERY_POLICY
from arborist.qa.verify import (
ANSWER_MODES,
DEFAULT_ANSWER_MODE,
verify_claim_lattice,
)
# ---------- evidence map ----------------------------------------------------
def _sample_chunks() -> list[dict]:
"""Three chunks across two sources, two source roles."""
return [
{
"source_root": "a" * 64,
"document_uri": "https://example.org/jp",
"title": "Jurassic Park (film)",
"chunk_idx": 0,
"chunk_root": "11" * 32,
"span": "Tyrannosaurus rex appears in the climactic scene.",
"source_role": "primary_answer_source",
},
{
"source_root": "a" * 64,
"document_uri": "https://example.org/jp",
"title": "Jurassic Park (film)",
"chunk_idx": 1,
"chunk_root": "22" * 32,
"span": "Velociraptors are shown attacking workers.",
"source_role": "primary_answer_source",
},
{
"source_root": "b" * 64,
"document_uri": "https://example.org/jp-game",
"title": "Jurassic Park (video game)",
"chunk_idx": 0,
"chunk_root": "33" * 32,
"span": "The Sega Genesis adaptation was released in 1993.",
"source_role": "noisy_background_source",
},
]
def test_pointer_id_is_sequential():
em = build_evidence_map(_sample_chunks())
assert [e.pointer_id for e in em] == ["E1", "E2", "E3"]
def test_evidence_id_is_content_addressed_and_stable():
chunks = _sample_chunks()
map1 = build_evidence_map(chunks)
map2 = build_evidence_map(chunks)
# Content-addressed evidence_ids stay stable across runs.
assert [e.evidence_id for e in map1] == [e.evidence_id for e in map2]
assert all(e.evidence_id.startswith("E") and len(e.evidence_id) == 9
for e in map1)
# Pointer ids are deterministic too (position-based).
assert [e.pointer_id for e in map1] == [e.pointer_id for e in map2]
def test_pointer_id_is_run_dependent_when_chunks_reorder():
chunks = _sample_chunks()
map1 = build_evidence_map(chunks)
map2 = build_evidence_map(list(reversed(chunks)))
# Chunk B's content-addressed evidence_id stays stable...
assert {e.chunk_root: e.evidence_id for e in map1} == \
{e.chunk_root: e.evidence_id for e in map2}
# ...but its pointer_id moves with position.
assert {e.chunk_root: e.pointer_id for e in map1} != \
{e.chunk_root: e.pointer_id for e in map2}
def test_evidence_map_root_order_independent():
chunks = _sample_chunks()
r1 = evidence_map_root(build_evidence_map(chunks))
r2 = evidence_map_root(build_evidence_map(list(reversed(chunks))))
assert r1 == r2
assert r1 != "00" * 32
def test_render_evidence_map_uses_pointer_ids():
em = build_evidence_map(_sample_chunks())
text = render_evidence_map(em)
for e in em:
assert f"=== {e.pointer_id} " in text
# Hex evidence_id must NOT leak into the prompt.
assert e.evidence_id not in text
assert e.span in text
# ---------- parser ---------------------------------------------------------
def test_parse_pointer_claims_basic():
out = parse_pointer_claims(
"Trex appears in the film. [E1]\n"
"Velociraptors stalk workers. [E2,E3]\n"
)
assert len(out) == 2
assert out[0].claim_text == "Trex appears in the film."
assert out[0].pointer_ids == ["E1"]
assert out[0].parse_status == "PARSED"
assert out[1].pointer_ids == ["E2", "E3"]
def test_parse_handles_bullet_markers():
out = parse_pointer_claims("- Trex appears. [E1]\n")
assert out[0].claim_text == "Trex appears."
def test_parse_handles_whitespace_inside_brackets():
out = parse_pointer_claims("Trex appears. [E1, E2 ,E3]\n")
assert out[0].pointer_ids == ["E1", "E2", "E3"]
def test_parse_skips_blank_lines():
out = parse_pointer_claims("\n \nTrex appears. [E1]\n\n")
assert len(out) == 1
def test_parse_no_evidence_pointer_status():
out = parse_pointer_claims("Trex appears in the film.\n")
assert out[0].parse_status == "NO_EVIDENCE_POINTER"
assert out[0].pointer_ids == []
assert out[0].claim_text == "Trex appears in the film."
def test_parse_inline_tag_preserves_prose():
out = parse_pointer_claims("Trex [E1] appears in the film.\n")
# Tag stripped; prose around it stays.
assert "Trex" in out[0].claim_text
assert "appears in the film" in out[0].claim_text
assert out[0].pointer_ids == ["E1"]
# ---------- #000048 step 2.4 — clause segmentation ----------------------
def test_parse_splits_semicolon_pointered_claims():
out = parse_pointer_claims("Water is wet [E1]; fire is hot [E2]; ice is cold [E3].\n")
assert len(out) == 3
assert [c.pointer_ids for c in out] == [["E1"], ["E2"], ["E3"]]
assert all(c.parse_status == "PARSED" for c in out)
def test_parse_splits_sentence_boundary():
out = parse_pointer_claims("Water boils at 100 C [E1]. It freezes at 0 C [E2].\n")
assert len(out) == 2
assert [c.pointer_ids for c in out] == [["E1"], ["E2"]]
def test_parse_splits_conjunction_when_both_sides_pointered():
out = parse_pointer_claims("Water is wet [E1] and fire is hot [E2].\n")
assert len(out) == 2
assert [c.pointer_ids for c in out] == [["E1"], ["E2"]]
def test_parse_does_not_split_pointerless_fragments():
# "The cat is black and white" is ONE claim — splitting on "and"
# would manufacture a pointer-less "The cat is black" fragment, so
# the guard rejects the split.
out = parse_pointer_claims("The cat is black and white. [E1]\n")
assert len(out) == 1
assert out[0].pointer_ids == ["E1"]
assert "black and white" in out[0].claim_text
def test_parse_does_not_split_cast_list_comma():
out = parse_pointer_claims("The cast: Keanu Reeves, Laurence Fishburne, Carrie-Anne Moss [E1].\n")
assert len(out) == 1
assert out[0].pointer_ids == ["E1"]
def test_parse_drops_leading_colon_header_on_split():
out = parse_pointer_claims("Two facts: (1) water is wet [E1] (2) fire is hot [E2].\n")
assert len(out) == 2
assert [c.pointer_ids for c in out] == [["E1"], ["E2"]]
# The "Two facts:" header is dropped, not emitted as a claim.
assert all("Two facts" not in c.claim_text for c in out)
def test_parse_joins_wrapped_bullet_continuation():
out = parse_pointer_claims(
"- The cell respires aerobically\n"
" to produce ATP efficiently. [E1]\n"
)
assert len(out) == 1
assert out[0].pointer_ids == ["E1"]
assert out[0].parse_status == "PARSED"
assert "respires aerobically" in out[0].claim_text
assert "produce ATP" in out[0].claim_text
def test_parse_split_preserves_pointer_order_and_multi_pointer():
out = parse_pointer_claims("Alpha holds [E1]; beta holds [E2, E3]\n")
assert len(out) == 2
assert out[0].pointer_ids == ["E1"]
assert out[1].pointer_ids == ["E2", "E3"]
# ---------- verifier: happy path ------------------------------------------
def test_verify_strict_when_all_resolve():
em = build_evidence_map(_sample_chunks())
# Claim text must textually overlap the cited span (G+: 6th hard
# check — claim content tokens must appear in the cited evidence).
# AND at least one cited source's title must share a content
# token with the claim (Rule 8, post-2026-05-02): so we keep
# "film" in each claim to anchor against the
# `Jurassic Park (film)` title.
# E1 span = "Tyrannosaurus rex appears in the climactic scene."
# E2 span = "Velociraptors are shown attacking workers."
answer = (
"Tyrannosaurus rex appears in the film. [E1]\n"
"Velociraptors are shown attacking workers in the film. [E2]\n"
)
v = verify_claim_lattice(answer, em)
assert v["audit_mode"] == "STRICT"
assert v["verifier_method"] == "claim_lattice"
assert v["n_quotes"] == 2
assert v["n_verified"] == 2
assert v["violations"] == []
# Evidence_id pairs use the content-addressed form for run-DAG.
pairs = v["evidence_id_pairs"]
assert pairs[0] == [em[0].evidence_id]
assert pairs[1] == [em[1].evidence_id]
def test_verify_renders_with_literal_spans_from_runtime():
em = build_evidence_map(_sample_chunks())
answer = "Tyrannosaurus rex is in the film. [E1]\n"
v = verify_claim_lattice(answer, em)
# Renderer pulls literal span from runtime, not from model output.
assert em[0].span in v["rendered_text"]
# New pointer format: [E1 | <title> | <chunk_prefix>: "<excerpt>"]
# Closes the visual provenance gap where E# could be confused
# with the source-list 1-indexed rank.
assert "[E1 |" in v["rendered_text"]
assert "Jurassic Park (film)" in v["rendered_text"]
def test_verify_passes_multi_pointer_claims():
em = build_evidence_map(_sample_chunks())
# Title is "Jurassic Park (film)" — claim must share a content
# token (Rule 8). Add "film" so the claim anchors against the
# source title.
answer = "Tyrannosaurus and velociraptors appear in the film. [E1,E2]\n"
v = verify_claim_lattice(answer, em)
assert v["audit_mode"] == "STRICT"
assert v["n_quotes"] == 2
assert v["n_verified"] == 2
# ---------- verifier: failure modes ---------------------------------------
def test_unknown_pointer_id_is_violation():
em = build_evidence_map(_sample_chunks())
answer = "Imaginary claim. [E99]\n"
v = verify_claim_lattice(answer, em)
assert v["audit_mode"] == "UNGROUNDED"
assert v["n_verified"] == 0
assert any(viol["kind"] == "UNKNOWN_EVIDENCE_ID" for viol in v["violations"])
assert v["claim_statuses"][0]["status"] == "UNKNOWN_EVIDENCE_ID"
def test_source_role_blocked_violation():
em = build_evidence_map(_sample_chunks())
# E3 → noisy_background_source, disallowed by default.
answer = "Sega adaptation exists. [E3]\n"
v = verify_claim_lattice(answer, em)
assert v["audit_mode"] == "UNGROUNDED"
assert any(viol["kind"] == "SOURCE_ROLE_BLOCKED" for viol in v["violations"])
assert v["claim_statuses"][0]["status"] == "SOURCE_ROLE_BLOCKED"
def test_double_quote_in_claim_text_no_longer_blocks_verification():
"""Pre-2026-04-30: any `"` in claim text was a hard MANUAL_QUOTE_VIOLATION
that blocked every pointer on the claim — even when the claim was
factually correct and source-grounded. Hermes-3-8B paraphrases prose
but copies named-quoted phrases verbatim from source (e.g.
`"Constitution State"` from a Connecticut chunk), so the rule was
rejecting good claims for cosmetic punctuation. Removed in favor
of the coverage-threshold check (Rule 5) and pointer cap (Rule 6).
The claim below would previously have failed with MANUAL_QUOTE_VIOLATION;
now it stands or falls on whether the cited evidence actually supports
it — same as any quote-free claim.
"""
em = build_evidence_map(_sample_chunks())
answer = 'The "T-rex" appears. [E1]\n'
v = verify_claim_lattice(answer, em)
# No MANUAL_QUOTE_VIOLATION emitted anywhere.
assert not any(viol["kind"] == "MANUAL_QUOTE_VIOLATION"
for viol in v["violations"])
assert v["claim_statuses"][0]["status"] != "MANUAL_QUOTE_VIOLATION"
def test_curly_quotes_also_no_longer_block():
"""Mirrors the ASCII-quote case for curly typographic quotes."""
em = build_evidence_map(_sample_chunks())
answer = "The “T-rex” appears. [E1]\n"
v = verify_claim_lattice(answer, em)
assert not any(viol["kind"] == "MANUAL_QUOTE_VIOLATION"
for viol in v["violations"])
def test_no_evidence_pointer_downgrades():
em = build_evidence_map(_sample_chunks())
answer = (
"Trex appears in the film.\n" # no pointer
"Velociraptors are shown in the film. [E2]\n" # 'film' overlaps title
)
# Opt out of bare-name check (Velociraptors stems to 1 content token
# after spotlight stopword strip); test exercises NO_EVIDENCE_POINTER.
v = verify_claim_lattice(answer, em, min_claim_content_tokens=0)
# Mixed: one verified, one NO_EVIDENCE_POINTER → HYBRID.
assert v["audit_mode"] == "HYBRID"
assert any(viol["kind"] == "NO_EVIDENCE_POINTER" for viol in v["violations"])
statuses = {c["status"] for c in v["claim_statuses"]}
assert "NO_EVIDENCE_POINTER" in statuses
assert "EVIDENCE_LINKED" in statuses
def test_partial_resolution_yields_hybrid():
em = build_evidence_map(_sample_chunks())
answer = (
"Tyrannosaurus rex appears in the film. [E1]\n" # 'film' overlaps title
"Made-up. [E99]\n"
)
# Test exercises mixed-pointer-resolution behavior; bare-name check
# is orthogonal so opt out.
v = verify_claim_lattice(answer, em, min_claim_content_tokens=0)
assert v["audit_mode"] == "HYBRID"
assert v["n_verified"] == 1
assert v["n_quotes"] == 2
def test_partial_pointer_within_claim_yields_partial():
em = build_evidence_map(_sample_chunks())
answer = "Tyrannosaurus rex appears in the film. [E1,E99]\n" # 'film' overlaps title
v = verify_claim_lattice(answer, em, min_claim_content_tokens=0)
assert v["audit_mode"] == "HYBRID"
assert v["claim_statuses"][0]["status"] == "EVIDENCE_LINKED_PARTIAL"
# Renderer keeps only resolved ids — new pointer format
# `[E1 | <title> | <chunk_prefix>: ...]` since 2026-05-01.
assert "[E1 |" in v["rendered_text"]
assert "E99" not in v["rendered_text"]
def test_empty_response_yields_ungrounded():
em = build_evidence_map(_sample_chunks())
v = verify_claim_lattice("", em)
assert v["audit_mode"] == "UNGROUNDED"
assert v["n_verified"] == 0
assert v["claim_statuses"] == []
assert v["evidence_id_pairs"] == []
def test_non_pointer_response_yields_ungrounded():
em = build_evidence_map(_sample_chunks())
# Model wrote prose without any pointer tags.
v = verify_claim_lattice(
"I don't know based on the provided sources.", em
)
assert v["audit_mode"] == "UNGROUNDED"
assert v["n_verified"] == 0
# Each non-empty line without a tag becomes a NO_EVIDENCE_POINTER claim.
assert v["claim_statuses"][0]["status"] == "NO_EVIDENCE_POINTER"
def test_orphan_tag_with_no_text_yields_schema_invalid():
em = build_evidence_map(_sample_chunks())
answer = "[E1]\n"
v = verify_claim_lattice(answer, em)
assert v["audit_mode"] == "UNGROUNDED"
assert v["claim_statuses"][0]["status"] == "SCHEMA_INVALID"
# ---------- policy & cache aliasing ---------------------------------------
def test_default_answer_mode_is_quote():
assert DEFAULT_POLICY["answer_mode"] == "quote"
assert DEFAULT_QUERY_POLICY["answer_mode"] == "quote"
assert DEFAULT_ANSWER_MODE == "quote"
assert "claim_lattice_pointer" in ANSWER_MODES
assert "quote" in ANSWER_MODES
def test_governance_hash_differs_between_modes():
p_quote = dict(DEFAULT_POLICY, answer_mode="quote")
p_lattice = dict(DEFAULT_POLICY, answer_mode="claim_lattice_pointer")
assert governance_policy_hash(p_quote) != governance_policy_hash(p_lattice)
# ---------- run-DAG -------------------------------------------------------
def test_run_dag_quote_mode_is_seven_stages():
common = dict(
question_hash="11" * 32,
sources=[{"document_root": "aa" * 32, "source_role": "primary_answer_source"}],
context_root="bb" * 32,
conversation_hash="cc" * 32,
answer_text="hello",
audit_mode="STRICT",
verifier_method="quote",
n_quotes=1,
n_verified=1,
claim_statuses=[],
lookup_path="miss",
)
dag = build_run_dag(**common)
stages = [n["stage"] for n in dag["nodes"]]
assert stages == [
"question", "retrieval", "context", "prompt",
"answer", "verify", "final_label",
]
assert verify_run_dag(dag) is True
def test_run_dag_pointer_mode_is_nine_stages():
common = dict(
question_hash="11" * 32,
sources=[{"document_root": "aa" * 32, "source_role": "primary_answer_source"}],
context_root="bb" * 32,
conversation_hash="cc" * 32,
answer_text="rendered prose",
audit_mode="STRICT",
verifier_method="claim_lattice",
n_quotes=1,
n_verified=1,
claim_statuses=[],
lookup_path="miss",
evidence_map_root="dd" * 32,
answer_mode="claim_lattice_pointer",
violations=[],
raw_answer_text="Claim. [E1]",
parsed_lattice=[{"claim_text": "Claim.", "evidence_ids": ["E12345678"]}],
rendered_text="rendered prose",
)
dag = build_run_dag(**common)
stages = [n["stage"] for n in dag["nodes"]]
assert stages == [
"question", "retrieval", "evidence_map", "prompt",
"raw_answer", "parsed_claim_lattice", "verify", "render",
"final_label",
]
# No 'context' or single 'answer' nodes in pointer mode.
assert "context" not in stages
assert "answer" not in stages
assert verify_run_dag(dag) is True
def test_run_dag_root_changes_when_evidence_map_changes():
base = dict(
question_hash="11" * 32,
sources=[{"document_root": "aa" * 32, "source_role": "primary_answer_source"}],
context_root="bb" * 32,
conversation_hash="cc" * 32,
answer_text="hello",
audit_mode="STRICT",
verifier_method="claim_lattice",
n_quotes=1,
n_verified=1,
claim_statuses=[],
lookup_path="miss",
answer_mode="claim_lattice_pointer",
violations=[],
raw_answer_text="raw",
parsed_lattice=[],
rendered_text="hello",
)
a = build_run_dag(**base, evidence_map_root="dd" * 32)
b = build_run_dag(**base, evidence_map_root="ee" * 32)
assert a["root"] != b["root"]
def test_run_dag_root_changes_when_parsed_lattice_changes():
base = dict(
question_hash="11" * 32,
sources=[{"document_root": "aa" * 32, "source_role": "primary_answer_source"}],
context_root="bb" * 32,
conversation_hash="cc" * 32,
answer_text="hello",
audit_mode="STRICT",
verifier_method="claim_lattice",
n_quotes=1,
n_verified=1,
claim_statuses=[],
lookup_path="miss",
evidence_map_root="dd" * 32,
answer_mode="claim_lattice_pointer",
violations=[],
raw_answer_text="raw",
rendered_text="hello",
)
a = build_run_dag(**base, parsed_lattice=[{"claim_text": "x", "evidence_ids": ["E12345678"]}])
b = build_run_dag(**base, parsed_lattice=[{"claim_text": "y", "evidence_ids": ["E12345678"]}])
assert a["root"] != b["root"]
# ---------- schema / migration --------------------------------------------
def test_verifier_method_check_accepts_claim_lattice(tmp_path: Path):
"""Fresh shards include 'claim_lattice' in the verifier_method CHECK."""
from arborist.store import connect
db = tmp_path / "fresh.db"
conn = connect(db)
try:
ddl = conn.execute(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='providence_cache'"
).fetchone()[0]
assert "'claim_lattice'" in ddl
finally:
conn.close()
def test_legacy_shard_migrates_in_place(tmp_path: Path):
"""An old shard with the pre-claim_lattice CHECK gets rebuilt on open
AND keeps run_dag_root / run_dag_blob populated."""
from arborist.store import connect
db = tmp_path / "legacy.db"
c = sqlite3.connect(db)
c.executescript(
"""
PRAGMA journal_mode=WAL;
CREATE TABLE providence_cache (
cache_key TEXT PRIMARY KEY,
source_root TEXT NOT NULL,
document_uri TEXT NOT NULL,
question_hash TEXT NOT NULL,
question_text TEXT NOT NULL,
answer_text TEXT NOT NULL,
merkle_proof TEXT NOT NULL,
model_profile_hash TEXT NOT NULL,
conversation_hash TEXT NOT NULL,
governance_policy_hash TEXT NOT NULL,
schema_version TEXT NOT NULL,
canonicalization_version TEXT NOT NULL,
chunking_version TEXT NOT NULL,
falsification_state TEXT NOT NULL DEFAULT 'live'
CHECK (falsification_state IN ('live','failed','stale','quarantined')),
chain TEXT NOT NULL DEFAULT 'private'
CHECK (chain IN ('private','public')),
audit_event_hash TEXT,
created_at INTEGER NOT NULL,
last_hit_at INTEGER,
hit_count INTEGER NOT NULL DEFAULT 0,
audit_mode TEXT NOT NULL DEFAULT 'UNGROUNDED'
CHECK (audit_mode IN ('STRICT','HYBRID','UNGROUNDED')),
n_quotes INTEGER NOT NULL DEFAULT 0,
n_verified INTEGER NOT NULL DEFAULT 0,
unverified_quotes TEXT,
verifier_method TEXT NOT NULL DEFAULT 'none'
CHECK (verifier_method IN ('quote','span','entity','paraphrase','none')),
run_dag_root TEXT,
run_dag_blob TEXT
);
INSERT INTO providence_cache
(cache_key, source_root, document_uri, question_hash, question_text,
answer_text, merkle_proof, model_profile_hash, conversation_hash,
governance_policy_hash, schema_version, canonicalization_version,
chunking_version, audit_event_hash, created_at,
run_dag_root, run_dag_blob)
VALUES
('ck1','sr1','u','qh','q','a','{}','mh','ch','gh','v','c','c1','eh',
1, 'rdr', '{"root":"rdr","nodes":[]}');
"""
)
c.commit()
c.close()
conn = connect(db)
try:
ddl = conn.execute(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='providence_cache'"
).fetchone()[0]
assert "'claim_lattice'" in ddl
row = conn.execute(
"SELECT run_dag_root, run_dag_blob FROM providence_cache WHERE cache_key=?",
("ck1",),
).fetchone()
assert row[0] == "rdr"
assert row[1] == '{"root":"rdr","nodes":[]}'
finally:
conn.close()
# ---------- soft-signal isolation -----------------------------------------
def test_no_entailment_field_in_verify_output():
"""Hard verifier must not assert semantic claims."""
em = build_evidence_map(_sample_chunks())
answer = "Trex appears. [E1]\n"
v = verify_claim_lattice(answer, em)
forbidden = {"entailment", "entailed", "completeness",
"predicate_compatibility", "scope_match"}
assert not (set(v) & forbidden)
for c in v["claim_statuses"]:
assert not (set(c) & forbidden)
# ---------- evidence_map index helpers ------------------------------------
def test_evidence_map_by_pointer_id_index():
em = build_evidence_map(_sample_chunks())
by_pid = evidence_map_by_pointer_id(em)
assert set(by_pid) == {"E1", "E2", "E3"}
assert by_pid["E1"] is em[0]
def test_evidence_map_by_evidence_id_index():
em = build_evidence_map(_sample_chunks())
by_eid = evidence_map_by_evidence_id(em)
assert set(by_eid) == {e.evidence_id for e in em}
# Ids are 9 chars: "E" + 8 hex.
assert all(len(eid) == 9 for eid in by_eid)
def test_render_claim_lattice_with_pointer_ids():
em = build_evidence_map(_sample_chunks())
by_pid = evidence_map_by_pointer_id(em)
text = render_claim_lattice(
[
{"text": "trex appears", "pointer_ids": ["E1"]},
{"text": "raptors attack", "pointer_ids": ["E2"]},
],
by_pid,
)
assert "Tyrannosaurus rex appears" in text
assert "Velociraptors are shown" in text
# New format includes source title + chunk_root prefix to avoid
# E# / source-rank visual confusion (2026-05-01).
assert "[E1 |" in text
assert "[E2 |" in text
assert "Jurassic Park (film)" in text
def test_render_claim_lattice_marks_unknown():
em = build_evidence_map(_sample_chunks())
by_pid = evidence_map_by_pointer_id(em)
text = render_claim_lattice(
[{"text": "fake", "pointer_ids": ["E99"]}],
by_pid,
)
assert "E99: ?" in text
def test_render_pointer_format_includes_source_title_and_chunk_prefix():
"""Pointer format: `[E# | <title> | <chunk_prefix>: "<excerpt>"]`.
Closes the 2026-05-01 visual provenance gap on the Orwell run
where `[E5: ...]` paired with a source list whose `[5]` was an
unrelated document — operator could mis-attribute the citation.
Now title + chunk_root prefix are inline so each pointer is
self-identifying."""
em = build_evidence_map(_sample_chunks())
by_pid = evidence_map_by_pointer_id(em)
text = render_claim_lattice(
[{"text": "trex appears", "pointer_ids": ["E1"]}],
by_pid,
)
# Title appears inline with the pointer.
assert "[E1 | Jurassic Park (film) | " in text
# chunk_root prefix is the first 8 hex chars of em[0].chunk_root.
chunk_prefix = em[0].chunk_root[:8]
assert chunk_prefix in text
# Excerpt still rendered after the prefix.
assert "Tyrannosaurus rex appears" in text
def test_render_pointer_format_falls_back_to_uri_tail_when_title_none():
"""If EvidenceObject.title is None (rare — source had no title
metadata), the renderer falls back to the URI tail. Defensive,
keeps the pointer self-identifying even when title indexing
fails for a chunk."""
chunks = [{
"source_root": "f" * 64,
"document_uri": "https://example.org/some-article-slug",
"title": None, # explicit absence
"chunk_idx": 0,
"chunk_root": "ab" * 32,
"span": "Some content here.",
"source_role": "primary_answer_source",
}]
em = build_evidence_map(chunks)
by_pid = evidence_map_by_pointer_id(em)
text = render_claim_lattice(
[{"text": "Some content", "pointer_ids": ["E1"]}],
by_pid,
)
# URI tail used as label.
assert "[E1 | some-article-slug | " in text
def test_render_pointer_format_no_visual_collision_with_source_rank():
"""Regression for the Orwell case: the substring `[E5:` (which
would visually collide with a source-rank `[5]` in older
renders) does not appear in the new output. The format change
isn't optional — it's enforced."""
em = build_evidence_map(_sample_chunks())
by_pid = evidence_map_by_pointer_id(em)
text = render_claim_lattice(
[{"text": "trex appears", "pointer_ids": ["E1"]}],
by_pid,
)
# Old format was `[E1: "..."]`; new format is `[E1 | ...: "..."]`.
# The colon-after-id must be gone.
assert "[E1:" not in text
# New format always has the pipe separator.
assert "[E1 |" in text
# ---------- spotlight excerpt (G0.2) --------------------------------------
def _long_chunk_with_buried_term() -> list[dict]:
"""Single chunk much longer than the spotlight window with the
target term ('Brachiosaurus') buried mid-span."""
intro = "Jurassic Park is a 1993 American science fiction thriller film. " * 30
middle = (
"Among the dinosaurs depicted on screen, Brachiosaurus is shown "
"lifting its long neck to eat from tall trees in an early scene."
)
tail = "Production took place across multiple studios. " * 30
long_span = intro + middle + tail
return [{
"source_root": "a" * 64,
"document_uri": "https://example.org/jp",
"title": "Jurassic Park (film)",
"chunk_idx": 0,
"chunk_root": "11" * 32,
"span": long_span,
"source_role": "primary_answer_source",
}]
def test_spotlight_finds_buried_topic_token():
em = build_evidence_map(_long_chunk_with_buried_term())
by_pid = evidence_map_by_pointer_id(em)
text = render_claim_lattice(
[{"text": "Brachiosaurus appears in the film.",
"pointer_ids": ["E1"]}],
by_pid,
)
# The displayed excerpt must contain the actual brachiosaurus
# mention from mid-span, not the leading article-intro sentence.
assert "Brachiosaurus is shown lifting" in text
def test_spotlight_falls_back_when_no_token_matches():
em = build_evidence_map(_long_chunk_with_buried_term())
by_pid = evidence_map_by_pointer_id(em)
# Claim contains no content tokens that appear in the span.
text = render_claim_lattice(
[{"text": "Quokka spotted yesterday morning.",
"pointer_ids": ["E1"]}],
by_pid,
)
# Falls back to leading window with trailing ellipsis.
assert text.rstrip().endswith('..."]')
# And the leading content of the span is shown.
assert "Jurassic Park is a 1993" in text
def test_spotlight_no_truncation_for_short_spans():
em = build_evidence_map(_sample_chunks())
by_pid = evidence_map_by_pointer_id(em)
text = render_claim_lattice(
[{"text": "trex", "pointer_ids": ["E1"]}],
by_pid,
)
# Sample span is <100 chars, fits in window; rendered intact.
assert em[0].span in text
def test_spotlight_window_size_overridable():
em = build_evidence_map(_long_chunk_with_buried_term())
by_pid = evidence_map_by_pointer_id(em)
narrow = render_claim_lattice(
[{"text": "Brachiosaurus appears in the film.",
"pointer_ids": ["E1"]}],
by_pid,
window=80,
)
wide = render_claim_lattice(
[{"text": "Brachiosaurus appears in the film.",
"pointer_ids": ["E1"]}],
by_pid,
window=400,
)
# Both windows hit the buried mention.
assert "Brachiosaurus" in narrow
assert "Brachiosaurus" in wide
# Wider window carries more surrounding prose.
assert len(wide) > len(narrow)
def test_spotlight_picks_longest_token_for_specificity():
"""Claim has both a generic ('film') and a specific ('Brachiosaurus')
content token. Spotlight should pick the specific one — sorted by
length desc — so the displayed excerpt anchors on what the claim
is actually *about*."""
em = build_evidence_map(_long_chunk_with_buried_term())
by_pid = evidence_map_by_pointer_id(em)
text = render_claim_lattice(
[{"text": "Brachiosaurus appears in the film.",
"pointer_ids": ["E1"]}],
by_pid,
)
# Window is centered on Brachiosaurus, not on the first occurrence
# of "film" (which is in the intro at offset 0).
assert "Brachiosaurus is shown" in text
def test_spotlight_deterministic():
em = build_evidence_map(_long_chunk_with_buried_term())
by_pid = evidence_map_by_pointer_id(em)
args = (
[{"text": "Brachiosaurus appears in the film.",
"pointer_ids": ["E1"]}],
by_pid,
)
a = render_claim_lattice(*args)
b = render_claim_lattice(*args)
assert a == b
# ---------- G0.1: per-chunk evidence granularity --------------------------
def test_per_chunk_evidence_map_query_path(tmp_path):
"""A single source with three chunks should produce three evidence
objects in the prompt-facing map — not one whole-doc entry."""
from typing import Iterator
from arborist.document import Document
from arborist.ingest import ingest_source
from arborist.qa.client import StubClient
from arborist.qa.query import DEFAULT_QUERY_POLICY, query
from arborist.source import Source
from arborist.store import connect
class FakeSource(Source):
source_type = "test"
def __init__(self, docs): self.docs = docs
def iter_documents(self) -> Iterator[Document]:
yield from self.docs
# Long enough to chunk into multiple pieces under tok-512-v1.
txt = (
"Paragraph one introduces Brachiosaurus as a long-necked sauropod. "
* 80
+ "Paragraph two introduces Velociraptor as a small carnivorous theropod. "
* 80
+ "Paragraph three introduces Tyrannosaurus rex as the apex predator. "
* 80
)
shard = tmp_path / "shard.db"
qa_db = tmp_path / "qa.db"
src_conn = connect(shard)
try:
ingest_source(src_conn, FakeSource([
Document(uri="t://chunked", content=txt, source_type="test", title="Dinosaurs")
]))
finally:
src_conn.close()
# Stub answer that hits all three pointer ids — proves the
# evidence map exposed >1 chunk to the model. Each claim names
# "dinosaur" so Rule 8 (title-relevance) anchors against the
# source title "Dinosaurs".
stub = StubClient(answer=(
"Brachiosaurus is a sauropod dinosaur. [E1]\n"
"Velociraptor is a theropod dinosaur. [E2]\n"
"T-rex is the apex dinosaur. [E3]\n"
))
# Override the per-source chunk cap so all three chunks of this
# single test source surface as E1/E2/E3. Production default caps
# at 2 chunks per source to keep the evidence catalog small for
# broad-descriptive questions; this test is asserting the per-chunk
# mapping itself, so it opts into the unbounded path.
policy = dict(
DEFAULT_QUERY_POLICY,
answer_mode="claim_lattice_pointer",
claim_lattice_max_chunks_per_source=8,
)
result = query(
question="dinosaur paragraphs",
qa_db=qa_db,
chat_client=stub,
model_id="stub",
single_db=shard,
top_k=4,
over_fetch=8,
max_context_chars=60000,
policy=policy,
)
# All three pointers resolved → STRICT.
assert result["audit_mode"] == "STRICT"
assert result["n_quotes"] == 3
assert result["n_verified"] == 3
def test_chunk_query_relevance_scores_by_overlap():
from arborist.qa.query import _chunk_query_relevance
qstem = {"brachiosaurus", "film"}
# Many distinct + many mentions wins.
a = _chunk_query_relevance(
"Brachiosaurus appears in the film. The film features Brachiosaurus.",
qstem,
)
# Only one of the two query tokens present.
b = _chunk_query_relevance(
"The film opens with credits and a logo.", qstem,
)
# Neither token present.
c = _chunk_query_relevance(
"Production took place across multiple studios.", qstem,
)
assert a > b > c
assert a[0] == 2 and a[1] >= 3
assert b[0] == 1
assert c == (0, 0)
def test_chunk_query_relevance_stem_aware():
from arborist.qa.query import _chunk_query_relevance
# Query has the plural "dinosaurs"; chunk has the singular "dinosaur".
# The stem strip in _body_count_with_stem should still match.
qstem = {"dinosaur"} # already stemmed
score = _chunk_query_relevance(
"Each dinosaur was modeled with animatronics.", qstem
)
assert score[0] == 1
def test_query_orders_chunks_by_relevance_g03(tmp_path):
"""End-to-end: a source whose query-relevant chunk is in the MIDDLE
of the document — G0.3 must promote it to E1 so a model that
lazy-anchors on early pointers still cites the relevant chunk."""
from typing import Iterator
from arborist.document import Document
from arborist.ingest import ingest_source
from arborist.qa.client import StubClient
from arborist.qa.evidence import build_evidence_map
from arborist.qa.query import (
DEFAULT_QUERY_POLICY, _load_doc_chunks, _chunk_query_relevance,
)
from arborist.qa.query import _stem_token_for_match, _title_query_tokens
from arborist.source import Source
from arborist.store import connect
class FakeSource(Source):
source_type = "test"
def __init__(self, docs): self.docs = docs
def iter_documents(self) -> Iterator[Document]:
yield from self.docs
# Three chunks worth of content; the middle one mentions the
# rare term. tok-512-v1 chunks ~500 tokens (~2000 chars) each.
irrelevant = "Production notes and scheduling details. " * 80
relevant = "The Brachiosaurus appears in a key scene. " * 80
more_irrelevant = "Cast and crew biographies follow. " * 80
txt = irrelevant + relevant + more_irrelevant
shard = tmp_path / "shard.db"
qa_db = tmp_path / "qa.db"
src_conn = connect(shard)
try:
ingest_source(src_conn, FakeSource([
Document(uri="t://reorder", content=txt, source_type="test", title="r")
]))
doc_root = src_conn.execute(
"SELECT document_root FROM documents WHERE document_uri='t://reorder'"
).fetchone()["document_root"]
finally:
src_conn.close()
chunks = _load_doc_chunks(str(shard), doc_root)
assert chunks is not None
assert len(chunks) >= 3
# Mirror what query() does: rank chunks by query-relevance.
qstem = {
_stem_token_for_match(t.lower())
for t in _title_query_tokens("Brachiosaurus appearance")
}
scored = []
for idx, leaf, span in chunks:
d, t = _chunk_query_relevance(span, qstem)
scored.append((idx, leaf, span, d, t))
scored.sort(key=lambda r: (-r[3], -r[4], r[0]))
# The first chunk after sorting MUST be the one containing
# Brachiosaurus — even though it's not chunk_idx=0 in doc order.
assert "Brachiosaurus" in scored[0][2]
# Document-order first chunk should NOT be the top-ranked one
# (otherwise the test fixture isn't exercising the reorder).
assert scored[0][0] != 0
def test_lazy_anchor_ratio_one_when_all_share_pointer():
em = build_evidence_map(_sample_chunks())
# All three claims must textually overlap E1's span
# ("Tyrannosaurus rex appears in the climactic scene.").
answer = (
"Tyrannosaurus rex appears. [E1]\n"
"Tyrannosaurus rex roars. [E1]\n"
"Tyrannosaurus rex broke out. [E1]\n"
)
# Test exercises the smell ratio counter; bare-name check is
# orthogonal so opt out so all three claims reach EVIDENCE_LINKED
# and populate pointer_distribution.
v = verify_claim_lattice(answer, em, min_claim_content_tokens=0)
assert v["lazy_anchor_ratio"] == 1.0
assert v["pointer_id_distribution"] == {"E1": 3}
def test_lazy_anchor_ratio_one_third_when_diversified():
em = build_evidence_map(_sample_chunks())
# Each claim's content tokens must overlap its cited span.
answer = (
"Tyrannosaurus rex appears. [E1]\n"
"Velociraptors stalk workers. [E2]\n"
"The climactic Tyrannosaurus moment. [E1]\n"
)
v = verify_claim_lattice(answer, em, min_claim_content_tokens=0)
# 2 cite E1, 1 cites E2 → max share 2/3.
assert v["pointer_id_distribution"] == {"E1": 2, "E2": 1}
assert abs(v["lazy_anchor_ratio"] - 2 / 3) < 1e-9
def test_lazy_anchor_ratio_zero_when_no_verified_pairs():
em = build_evidence_map(_sample_chunks())
# Unknown pointer → 0 verified pairs.
answer = "Imaginary claim. [E99]\n"
v = verify_claim_lattice(answer, em)
assert v["lazy_anchor_ratio"] == 0.0
assert v["pointer_id_distribution"] == {}
def test_citation_mismatch_when_claim_token_absent_from_evidence():
"""6th hard check: cited evidence span must textually contain at
least one content token from the claim text."""
em = build_evidence_map(_sample_chunks())
# E1's span is "Tyrannosaurus rex appears in the climactic scene."
# — the claim says "Brachiosaurus" (not present in the span).
answer = "Brachiosaurus appears in the film. [E1]\n"
v = verify_claim_lattice(answer, em)
assert v["audit_mode"] == "UNGROUNDED"
assert any(viol["kind"] == "CITATION_MISMATCH" for viol in v["violations"])
assert v["claim_statuses"][0]["status"] == "CITATION_MISMATCH"
assert v["n_verified"] == 0
def test_citation_mismatch_partial_when_one_pointer_overlaps():
em = build_evidence_map(_sample_chunks())
# E1 contains "Tyrannosaurus rex"; E2 contains "Velociraptors".
# Claim mentions Tyrannosaurus → E1 overlaps, E2 does not.
answer = "Tyrannosaurus rex appears in the film. [E1,E2]\n" # 'film' overlaps title
# Test exercises CITATION_MISMATCH on a single pointer; bare-name
# check is orthogonal so opt out.
v = verify_claim_lattice(answer, em, min_claim_content_tokens=0)
assert v["audit_mode"] == "HYBRID"
assert v["n_verified"] == 1
assert v["claim_statuses"][0]["status"] == "EVIDENCE_LINKED_PARTIAL"
# E1 resolved, E2 mismatched.
assert any(
viol["kind"] == "CITATION_MISMATCH" and viol["pointer_id"] == "E2"
for viol in v["violations"]
)
def test_pure_stopword_claim_passes_overlap_check_vacuously():
"""A claim composed entirely of stopwords has no content tokens to
check; the overlap rule returns True vacuously and other hard
checks own that case."""
em = build_evidence_map(_sample_chunks())
# Tyrannosaurus rex appears... is in E1's span; the claim text
# below has effectively no content tokens after stopword filter
# (just function words), so the overlap check shouldn't fail it.
answer = "It was so. [E1]\n"
# Bare-name check would reject this for 0 content tokens; opt out
# since this test is specifically about the overlap rule's
# vacuous-True branch for claims with no content tokens.
v = verify_claim_lattice(answer, em, min_claim_content_tokens=0)
# Overlap check passed; claim is short but valid.
assert v["claim_statuses"][0]["status"] == "EVIDENCE_LINKED"
def test_lazy_anchor_signals_not_in_run_dag_payload():
"""Sidecar invariant: pointer_id_distribution and lazy_anchor_ratio
must NOT enter build_run_dag's verify_payload — they're soft
signals recoverable from claim_statuses, and threading them in
would make run_dag_root depend on the model's anchoring habits."""
import inspect as _inspect
from arborist.qa.dag import build_run_dag
sig = _inspect.signature(build_run_dag)
params = set(sig.parameters)
assert "pointer_id_distribution" not in params
assert "lazy_anchor_ratio" not in params
def test_per_chunk_evidence_map_uses_distinct_chunk_roots(tmp_path):
"""Chunk-level evidence_ids must be content-addressed per chunk —
different chunks of the same source produce different evidence_ids
even though they share source_root."""
from typing import Iterator
from arborist.document import Document
from arborist.ingest import ingest_source
from arborist.qa.query import _load_doc_chunks
from arborist.source import Source
from arborist.store import connect
class FakeSource(Source):
source_type = "test"
def __init__(self, docs): self.docs = docs
def iter_documents(self) -> Iterator[Document]:
yield from self.docs
txt = "para alpha. " * 200 + "para beta. " * 200 + "para gamma. " * 200
shard = tmp_path / "shard.db"
conn = connect(shard)
try:
ingest_source(conn, FakeSource([
Document(uri="t://multi", content=txt, source_type="test", title="m")
]))
doc_root = conn.execute(
"SELECT document_root FROM documents WHERE document_uri='t://multi'"
).fetchone()["document_root"]
finally:
conn.close()
chunks = _load_doc_chunks(str(shard), doc_root)
assert chunks is not None
assert len(chunks) > 1, "test fixture must produce multiple chunks"
# Build a chunks_for_map dict like query() does, then verify each
# chunk gets a distinct content-addressed evidence_id.
cfm = [
{
"source_root": doc_root,
"document_uri": "t://multi",
"title": "m",
"chunk_idx": idx,
"chunk_root": leaf_hash,
"span": span,
"source_role": "primary_answer_source",
}
for idx, leaf_hash, span in chunks
]
em = build_evidence_map(cfm)
eids = [e.evidence_id for e in em]
pids = [e.pointer_id for e in em]
assert len(set(eids)) == len(eids), "evidence_ids must be unique per chunk"
# Pointer ids are sequential.
assert pids == [f"E{i + 1}" for i in range(len(em))]
# All share the same source_root but different chunk_roots.
assert len({e.source_root for e in em}) == 1
assert len({e.chunk_root for e in em}) == len(em)
# ---------- claim-count ceiling --------------------------------------------
def test_too_many_claims_demotes_pointer_mode_to_hybrid():
"""Pointer-mode mirror of test_verify_json_too_many_claims_demotes_to_hybrid.
13 well-formed pointer claims (default cap 12) trips TOO_MANY_CLAIMS
even when each individually verifies. Demotes STRICT to HYBRID so
'tell me all there is to know about X' runaway is operator-visible.
"""
chunks = [
{
"source_root": "f" * 64,
"document_uri": "https://example.org/runaway",
"title": "Runaway Source",
"chunk_idx": i,
"chunk_root": f"{i:02x}" * 32,
"span": f"Fact {i} appears in the runaway source span.",
"source_role": "primary_answer_source",
}
for i in range(13)
]
em = build_evidence_map(chunks)
answer = "\n".join(
f"Fact {i} appears in the runaway source span. [E{i + 1}]"
for i in range(13)
) + "\n"
v = verify_claim_lattice(answer, em)
assert v["audit_mode"] == "HYBRID", \
f"expected HYBRID (TOO_MANY_CLAIMS demote), got {v['audit_mode']}"
assert any(vio["kind"] == "TOO_MANY_CLAIMS" for vio in v["violations"])
# The cap doesn't truncate — every claim still verifies.
assert v["n_verified"] == 13, \
f"all 13 should still verify; got {v['n_verified']}"
def test_format_collapsed_fires_on_bracketless_multi_line_prose():
"""The 'winners of all major sports?' case (2026-05-02): Hermes
melted under an under-specified broad question, dumped 50+ free-form
prose claims with ZERO `[E\\d+]` pointer tags. Parser found a couple
of fragments, both ungrounded → UNGROUNDED. Verifier was honest, but
operators couldn't tell from the audit line whether UNGROUNDED meant
'tried to ground & failed' vs 'abandoned the protocol entirely.'
FORMAT_COLLAPSED separates the two failure shapes.
"""
em = build_evidence_map(_sample_chunks())
answer = (
"The 1979 FINA Men's Water Polo World Cup was won by Hungary.\n"
"The 1979 FINA Women's Water Polo World Cup was won by the USA.\n"
"The 8th Pan American Games were won by Cuba.\n"
"The 8th Mediterranean Games were won by Italy.\n"
"The Tenth Summer Universiade was won by the Soviet Union.\n"
)
v = verify_claim_lattice(answer, em)
assert v["format_collapsed"] is True
assert any(vio["kind"] == "FORMAT_COLLAPSED" for vio in v["violations"])
def test_format_collapsed_does_not_fire_when_pointer_tags_present():
"""A well-formed pointer-line answer with at least one `[E\\d+]`
bracket is a graceful protocol-following attempt — even if every
pointer is wrong, that's a per-claim verification failure, not a
format collapse. FORMAT_COLLAPSED must stay off."""
em = build_evidence_map(_sample_chunks())
answer = (
"Tyrannosaurus rex appears. [E1]\n"
"Velociraptors stalk workers. [E2]\n"
)
v = verify_claim_lattice(answer, em, min_claim_content_tokens=0)
assert v["format_collapsed"] is False
assert not any(vio["kind"] == "FORMAT_COLLAPSED" for vio in v["violations"])
def test_format_collapsed_skips_short_answers():
"""Below the meaningful-line threshold (5 lines >20 chars), absence
of pointer tags is more likely a one-line refusal than a runaway
prose dump. FORMAT_COLLAPSED stays off."""
em = build_evidence_map(_sample_chunks())
answer = "I don't know.\n"
v = verify_claim_lattice(answer, em)
assert v["format_collapsed"] is False
def test_format_collapse_check_disabled_by_policy():
"""Operators can opt out via policy. With the check off, even a
50-line bracket-free dump returns format_collapsed=False."""
em = build_evidence_map(_sample_chunks())
answer = "\n".join(
f"Some bracket-free prose claim number {i} with enough length."
for i in range(10)
) + "\n"
v = verify_claim_lattice(answer, em, format_collapse_check_enabled=False)
assert v["format_collapsed"] is False
assert not any(vio["kind"] == "FORMAT_COLLAPSED" for vio in v["violations"])