The phrase-pattern retrieval route (commit 1b8677d) closed the
RETRIEVAL side of reference-frame failure; this ticket closes
the ANSWER side.
New module aborist/qa/frame.py:
FrameDetection dataclass (frame_kind, reference_title,
reference_uri, confidence). Sidecar — never enters cache_key
or governance_policy_hash.
detect_frame(question, sources, phrase_match_roots) — heuristic
detector. Reference-frame classification fires when:
(a) phrase route surfaced at least one source, AND
(b) at least one phrase-matched source is a reference work,
determined by:
- title parenthetical disambig (`(novel)` / `(film)` /
`(play)` / `(franchise)` / etc.), OR
- body sample contains ≥3 DISTINCT fiction markers
(novel / published / protagonist / plot / ...).
Distinct-marker count keeps the heuristic robust against a
history article saying "novel approach" twice.
aborist/qa/query.py:
Calls detect_frame for lattice modes only. Body sample uses the
ARTICLE LEAD (chunk_idx=0, post-wikitext-strip) — fiction
markers cluster in the lead on Wikipedia, not in plot chunks
that may have been query-relevance-ranked higher.
New policy field claim_lattice_polarity_preamble injected as a
user-role message before the grounding_reminder when
frame_kind == "reference". Format-string with
{reference_title} placeholder.
Result dict carries frame_detection (kind / title / uri /
confidence) for renderer + bench consumption.
aborist/cli.py:
Renderer adds a `reference frame: <title>` line when
frame_detection.kind == "reference". Skipped for literal /
no-phrase-route / ambiguous rows.
Live verification — Orwell case:
PRE : "The text does not directly state that Oceania has always
been at war with East Asia."
POST : "In George Orwell's dystopian novel Nineteen Eighty-Four,
the nation of Oceania is always at war with Eastasia,
but this is a result of propaganda and doublethink, not
actual historical continuity. The war with Eastasia is
a fabricated conflict to maintain control..."
Multi-frame compilation: distinguishes propaganda claim from
fictional-actual continuity, exactly the polarity contract from
the ticket §2 abstraction.
Literal queries (capital of france) unchanged — polarity preamble
only injects when frame_kind classifies as "reference".
22 new tests (19 in test_frame.py + 3 in test_cli_render.py).
Full suite: 734 passed (was 712, +22).
Directive D3 status: ½ → ✓. Ticket #000002 closed.
All seven structural directives now ✓:
D1, D2, D5 (were ✓);
D3 #000002, D4 #000001, D6 #000003, D7 #000005 (closed in this
series).
367 lines
13 KiB
Python
367 lines
13 KiB
Python
"""Pretty-print + ensure_ascii=False on `aborist query` output.
|
|
|
|
The CLI default emits a human-readable render of the query result;
|
|
``--json`` switches to the raw record. Both paths must:
|
|
- render unicode literals (no \\uXXXX escape sequences)
|
|
- present the answer prominently; metadata is supporting info
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from aborist.cli import _render_query_human
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _render_query_human — direct unit tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _result(**overrides) -> dict:
|
|
base = {
|
|
"status": "cache_miss_then_written",
|
|
"audit_mode": "HYBRID",
|
|
"cache_key": "35ab7d3355c723b759145d6d446cb1d9010abe0dce9992ca0e8b87837b596610",
|
|
"context_root": "ab" * 32,
|
|
"answer_text": "Pikachu is a species of Pokémon creatures.",
|
|
"sources": [
|
|
{
|
|
"document_root": "cd" * 32,
|
|
"document_uri": "https://en.wikipedia.org/wiki/Pikachu",
|
|
"title": "Pikachu",
|
|
"score": 36.6,
|
|
"chunk_idx": 6,
|
|
"shard": "002.db",
|
|
}
|
|
],
|
|
"n_quotes": 2,
|
|
"n_verified": 1,
|
|
"verifier_method": "quote",
|
|
"unverified_quotes": ["something the model said but didn't ground"],
|
|
"timings": {"total_ms": 9234.1},
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
def test_render_includes_question_at_top():
|
|
out = _render_query_human(_result(), "who is pikachu?")
|
|
assert out.splitlines()[0] == "who is pikachu?"
|
|
|
|
|
|
def test_render_summary_line_has_audit_n_verified_method_elapsed():
|
|
"""Summary line carries audit_label · n_verified/n_quotes · elapsed.
|
|
The label format updated 2026-05-01 to honesty-spell verifier
|
|
method (e.g. "EVIDENCE-LINKED · via claim_lattice" instead of
|
|
"STRICT") so user-facing output doesn't overclaim semantic truth.
|
|
Quote / span / entity / paraphrase modes keep the original
|
|
audit_mode token plus a "via {method}" tail."""
|
|
out = _render_query_human(_result(), "q")
|
|
second = out.splitlines()[1]
|
|
# Quote-mode keeps the audit_mode token (HYBRID) — it's the
|
|
# claim_lattice modes that get the EVIDENCE-LINKED relabel.
|
|
assert "HYBRID" in second
|
|
# Counts and method still in the line; "verified" word dropped
|
|
# to give the slimmer "1/2" form room for the longer label tail.
|
|
assert "1/2" in second
|
|
assert "via quote" in second
|
|
assert "9.2s" in second
|
|
|
|
|
|
def test_render_marks_cache_hit_distinctly():
|
|
out_hit = _render_query_human(_result(status="cache_hit"), "q")
|
|
out_fresh = _render_query_human(_result(status="cache_miss_then_written"), "q")
|
|
assert "(cached)" in out_hit
|
|
assert "(fresh)" in out_fresh
|
|
|
|
|
|
def test_render_emits_unicode_literals_not_escapes():
|
|
"""Pokémon, not Pok\\u00e9mon — the user's terminal should see real é."""
|
|
out = _render_query_human(_result(), "who is pikachu?")
|
|
assert "Pokémon" in out
|
|
assert "\\u00e9" not in out
|
|
|
|
|
|
def test_render_lists_sources_with_clean_host_path():
|
|
out = _render_query_human(_result(), "q")
|
|
# "[1] Pikachu — en.wikipedia.org/wiki/Pikachu (002.db)"
|
|
assert "[1] Pikachu" in out
|
|
assert "en.wikipedia.org/wiki/Pikachu" in out
|
|
# scheme stripped
|
|
assert "https://" not in out.split("sources (")[1] if "sources (" in out else True
|
|
assert "(002.db)" in out
|
|
|
|
|
|
def test_render_truncates_long_unverified_quotes():
|
|
long = "x" * 200
|
|
out = _render_query_human(_result(unverified_quotes=[long]), "q")
|
|
# Truncated form ends in ellipsis
|
|
assert "..." in out
|
|
# Original doesn't fully appear
|
|
assert long not in out
|
|
|
|
|
|
def test_render_omits_unverified_section_when_empty():
|
|
out = _render_query_human(_result(unverified_quotes=[]), "q")
|
|
assert "unverified" not in out.lower()
|
|
|
|
|
|
def test_render_omits_sources_section_when_empty():
|
|
out = _render_query_human(_result(sources=[]), "q")
|
|
assert "sources (" not in out
|
|
|
|
|
|
def test_render_falls_back_for_error_status():
|
|
"""no_sources / unknown_document / etc. produce a one-line status."""
|
|
out = _render_query_human(
|
|
{"status": "no_sources", "msg": "FTS5 returned no hits"},
|
|
"q",
|
|
)
|
|
assert "no_sources" in out
|
|
assert "FTS5" in out
|
|
|
|
|
|
def test_render_includes_short_cache_key_with_pointer_to_json():
|
|
out = _render_query_human(_result(), "q")
|
|
# Short prefix only — full key requires --json
|
|
assert "cache_key: 35ab7d33" in out
|
|
assert "--json" in out
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# json.dumps everywhere uses ensure_ascii=False (regression on the global pass)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_unicode_round_trips_through_print_path(capsys):
|
|
"""Pin: a result with a Pokémon-style answer prints é, not \\u00e9.
|
|
Uses argparse via build_parser to exercise the real CLI dispatch."""
|
|
from aborist.cli import build_parser
|
|
|
|
parser = build_parser()
|
|
# Build a fake result and call _cmd_query's render path indirectly.
|
|
out = _render_query_human(_result(), "who is pikachu?")
|
|
print(out)
|
|
captured = capsys.readouterr().out
|
|
assert "Pokémon" in captured
|
|
assert "\\u00e9" not in captured
|
|
|
|
|
|
def test_json_mode_also_uses_unicode():
|
|
"""The --json path should also emit unicode literals, not escapes."""
|
|
payload = {"answer": "Pokémon"}
|
|
s = json.dumps(payload, ensure_ascii=False)
|
|
assert "Pokémon" in s
|
|
assert "\\u00e9" not in s
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# capacity metrics — human render surfaces prompt_chars breakdown
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_render_shows_capacity_line_when_prompt_chars_present():
|
|
"""When the result carries `prompt_chars`, the human render
|
|
emits a one-liner summarizing prompt size + answer size so an
|
|
operator can tell at a glance whether STRICT came from a small
|
|
prompt or a context-stuffed one."""
|
|
r = _result(
|
|
prompt_chars={
|
|
"system_prompt": 1292,
|
|
"grounding_reminder": 342,
|
|
"user_question": 30,
|
|
"evidence_or_context": 20304,
|
|
"messages_total": 21996,
|
|
},
|
|
answer_chars=247,
|
|
)
|
|
out = _render_query_human(r, "what is the capital of france?")
|
|
assert "capacity:" in out
|
|
assert "21,996 chars" in out # messages_total
|
|
assert "sys 1,292" in out
|
|
assert "evidence 20,304" in out
|
|
assert "answer 247 chars" in out
|
|
|
|
|
|
def test_render_omits_capacity_line_on_legacy_results_without_prompt_chars():
|
|
"""Backwards-compat: results that pre-date capacity metrics (no
|
|
prompt_chars key) render without the capacity line — no KeyError,
|
|
no awkward 'capacity: 0 chars' noise."""
|
|
r = _result() # no prompt_chars
|
|
out = _render_query_human(r, "q")
|
|
assert "capacity:" not in out
|
|
|
|
|
|
def test_render_capacity_thousand_separators():
|
|
"""Large prompts must format with thousand-separator commas so
|
|
a 60K prompt reads as '60,000' not '60000' — operator legibility
|
|
on the daily render."""
|
|
r = _result(
|
|
prompt_chars={
|
|
"system_prompt": 1500,
|
|
"grounding_reminder": 0,
|
|
"user_question": 50,
|
|
"evidence_or_context": 60000,
|
|
"messages_total": 61550,
|
|
},
|
|
answer_chars=1234,
|
|
)
|
|
out = _render_query_human(r, "q")
|
|
assert "61,550 chars" in out
|
|
assert "60,000" in out
|
|
assert "1,234 chars" in out
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# label discipline — claim_lattice* modes get EVIDENCE-LINKED relabel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_render_label_strict_lattice_no_violations_is_evidence_warranted():
|
|
"""claim-lattice STRICT with no soft demotes lands at the top
|
|
ladder rung (EVIDENCE-WARRANTED) — pointer verified, warrant
|
|
passed where it ran, no other demotes pulled the verdict back."""
|
|
r = _result(
|
|
audit_mode="STRICT",
|
|
verifier_method="claim_lattice",
|
|
n_quotes=1, n_verified=1,
|
|
violations=[],
|
|
)
|
|
out = _render_query_human(r, "q")
|
|
assert "EVIDENCE-WARRANTED" in out
|
|
assert "via claim_lattice" in out
|
|
# The bare STRICT token should NOT appear on the summary line.
|
|
summary_line = out.splitlines()[1]
|
|
assert "STRICT" not in summary_line
|
|
|
|
|
|
def test_render_label_strict_lattice_warrant_missing_is_pointer_linked():
|
|
"""claim-lattice STRICT (or HYBRID) with WARRANT_MISSING in
|
|
violations drops to POINTER-LINKED — the pointer resolved but
|
|
the cited evidence didn't anchor the claim."""
|
|
r = _result(
|
|
audit_mode="HYBRID",
|
|
verifier_method="claim_lattice_pointer",
|
|
n_quotes=2, n_verified=1,
|
|
violations=[{"kind": "WARRANT_MISSING", "claim_idx": 0}],
|
|
)
|
|
out = _render_query_human(r, "q")
|
|
assert "POINTER-LINKED-PARTIAL" in out
|
|
assert "via claim_lattice_pointer" in out
|
|
|
|
|
|
def test_render_label_strict_lattice_soft_demote_is_anchor_warranted():
|
|
"""claim-lattice STRICT/HYBRID with a soft-demote violation
|
|
(LAZY_ANCHOR_DEMOTED, POINTER_OVERFLOW_TRIMMED, TOO_MANY_CLAIMS)
|
|
but no WARRANT_MISSING lands at ANCHOR-WARRANTED — warrant
|
|
passed where it ran, but other demotes apply."""
|
|
r = _result(
|
|
audit_mode="STRICT",
|
|
verifier_method="claim_lattice",
|
|
n_quotes=3, n_verified=3,
|
|
violations=[{"kind": "LAZY_ANCHOR_DEMOTED"}],
|
|
)
|
|
out = _render_query_human(r, "q")
|
|
assert "ANCHOR-WARRANTED" in out
|
|
assert "via claim_lattice" in out
|
|
|
|
|
|
def test_render_label_hybrid_lattice_evidence_warranted_partial():
|
|
"""HYBRID + no violations + lattice mode → EVIDENCE-WARRANTED-PARTIAL.
|
|
The -PARTIAL suffix shows the audit didn't reach STRICT but the
|
|
claim that did verify reached the top rung."""
|
|
r = _result(
|
|
audit_mode="HYBRID",
|
|
verifier_method="claim_lattice",
|
|
n_quotes=3, n_verified=1,
|
|
violations=[],
|
|
)
|
|
out = _render_query_human(r, "q")
|
|
assert "EVIDENCE-WARRANTED-PARTIAL" in out
|
|
|
|
|
|
def test_render_label_ungrounded_in_claim_lattice_stays_ungrounded():
|
|
"""UNGROUNDED already names what it means — no rung-relabel."""
|
|
r = _result(
|
|
audit_mode="UNGROUNDED",
|
|
verifier_method="claim_lattice",
|
|
n_quotes=2, n_verified=0,
|
|
violations=[],
|
|
)
|
|
out = _render_query_human(r, "q")
|
|
assert "UNGROUNDED" in out
|
|
assert "via claim_lattice" in out
|
|
|
|
|
|
def test_render_shows_reference_frame_notes_when_detected():
|
|
"""When frame_detection.kind == 'reference', the renderer
|
|
surfaces the named reference work so an operator can see the
|
|
substrate routed to a fictional / reference source."""
|
|
r = _result(
|
|
audit_mode="STRICT",
|
|
verifier_method="claim_lattice",
|
|
frame_detection={
|
|
"kind": "reference",
|
|
"reference_title": "Nineteen Eighty-Four",
|
|
"reference_uri": "https://en.wikipedia.org/wiki/Nineteen_Eighty-Four",
|
|
"confidence": 0.8,
|
|
},
|
|
)
|
|
out = _render_query_human(r, "has oceania always been at war with east asia")
|
|
assert "reference frame: Nineteen Eighty-Four" in out
|
|
|
|
|
|
def test_render_omits_reference_frame_notes_for_literal_query():
|
|
"""Literal-frame queries (no allusion) don't render the frame
|
|
notes line — keeps the daily render clean for the common case."""
|
|
r = _result(
|
|
audit_mode="STRICT",
|
|
verifier_method="claim_lattice",
|
|
frame_detection={
|
|
"kind": "no_phrase_route",
|
|
"reference_title": None,
|
|
"reference_uri": None,
|
|
"confidence": 0.0,
|
|
},
|
|
)
|
|
out = _render_query_human(r, "what is the capital of france?")
|
|
assert "reference frame:" not in out
|
|
|
|
|
|
def test_render_omits_reference_frame_notes_for_ambiguous():
|
|
"""Ambiguous frame (phrase route fired but no clear reference
|
|
work) → no frame notes line. The signal isn't strong enough
|
|
to claim a named reference."""
|
|
r = _result(
|
|
audit_mode="STRICT",
|
|
verifier_method="claim_lattice",
|
|
frame_detection={
|
|
"kind": "ambiguous",
|
|
"reference_title": None,
|
|
"reference_uri": None,
|
|
"confidence": 0.4,
|
|
},
|
|
)
|
|
out = _render_query_human(r, "some allusion-shape query")
|
|
assert "reference frame:" not in out
|
|
|
|
|
|
def test_render_label_quote_mode_keeps_audit_mode_token():
|
|
"""Quote / span / entity / paraphrase modes verify against
|
|
pinned spans, not synthesis. STRICT in quote mode IS a strong
|
|
claim about evidence units — keep the label as-is."""
|
|
r = _result(
|
|
audit_mode="STRICT",
|
|
verifier_method="quote",
|
|
n_quotes=2, n_verified=2,
|
|
)
|
|
out = _render_query_human(r, "q")
|
|
assert "STRICT" in out
|
|
assert "via quote" in out
|
|
# Don't relabel quote-mode STRICT to either ladder rung.
|
|
assert "EVIDENCE-WARRANTED" not in out
|
|
assert "POINTER-LINKED" not in out
|
|
assert "ANCHOR-WARRANTED" not in out
|