Two cleanup operations bundled (separate scopes, single commit
since they share the doc-tree settle):
1. Move ticket-NNNNNN-<slug>.md files into docs/tickets/. The
directory makes browsing the design log easier; the index
stays at docs/TICKETS.md (top-level pointer). Convention text
in TICKETS.md updated to spell the new path.
2. Delete three docs whose load-bearing content has either been
absorbed into the codebase or distilled into closed tickets:
- docs/naming-deferral.md (147 lines) — explained why we
don't rename claim_lattice → CTI/PROMETHEUS-Σ. Decision
stays in place; the rationale is no longer worth a
dedicated doc. Inline citations removed from
cti-architecture.md (4 refs), warrant.py, ticket-000003
(closed-ticket internal ref).
- docs/reference-frame-failure-class.md (169 lines) — Orwell
case journal that motivated the phrase-pattern retrieval
route. The route shipped; the analysis is now duplicate
with the closed Ticket #000002. Inline citation removed
from CLAUDE.md retrieval pipeline section + frame.py.
- docs/test-coverage-audit-2026-05-01.md (46 lines) —
point-in-time audit checking 16/16 of fox's §11 list. Tests
themselves live in tests/; the audit was a one-shot
checkmark exercise.
References updated:
CLAUDE.md, aborist/qa/frame.py, aborist/qa/retrieval_plan.py,
aborist/qa/warrant.py, docs/cti-architecture.md, docs/TICKETS.md,
docs/tickets/ticket-000003 + ticket-000004 (internal links).
Net: -362 lines + tickets/ subdir. 751/34 tests still pass.
196 lines
8.1 KiB
Python
196 lines
8.1 KiB
Python
"""Reference-frame detection for claim-lattice queries.
|
|
|
|
Closes the answer-side gap left open by the phrase-pattern retrieval
|
|
route (commit `1b8677d`). The
|
|
phrase route surfaces the right article for allusion-shape queries
|
|
(e.g. Nineteen Eighty-Four for "has oceania always been at war with
|
|
east asia"); this module then classifies the answer's intended frame
|
|
so the prompt + renderer can produce a multi-frame answer that
|
|
distinguishes literal from reference from in-universe-propaganda.
|
|
|
|
See `docs/tickets/ticket-000002-reference-frame-polarity-contract.md` for the
|
|
full scope. This module is the *substrate* — it returns a
|
|
`FrameDetection` per call. Prompt augmentation + renderer extension
|
|
live in their respective layers and consume this output.
|
|
|
|
Hard or soft? **Soft.** This is a sidecar-class signal that informs
|
|
prompt augmentation + renderer presentation; it never enters the
|
|
proof path or `cache_key`. Per CLAUDE.md "Soft hash vs hard hash":
|
|
heuristics, scores, and judgment calls live in the soft channel.
|
|
The hard verifier never consults `FrameDetection` to accept or
|
|
reject claims.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from typing import Literal
|
|
|
|
|
|
# Parenthetical disambiguation suffixes Wikipedia uses for fictional /
|
|
# reference works. Case-insensitive; matches at the open-paren without
|
|
# a leading word-boundary (`\b` doesn't match between space and `(`
|
|
# since both are non-word chars).
|
|
_REFERENCE_TITLE_SUFFIX_RE = re.compile(
|
|
r"\("
|
|
r"(?:novel|novella|short\s+story|story|film|movie|play|"
|
|
r"video\s+game|game|comic|comics|graphic\s+novel|book|"
|
|
r"franchise|series|tv\s+series|television\s+series|"
|
|
r"manga|anime|fictional\s+character|character|episode|"
|
|
r"album|song|musical|opera|poem)"
|
|
r"\)",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
# Body-side fiction markers. When a retrieved source's body sample
|
|
# contains a high density of these tokens, the source is a reference
|
|
# work even when the title lacks a disambiguation suffix
|
|
# (Nineteen_Eighty-Four article being the canonical case — its title
|
|
# is the bare work-name with no parenthetical, but the body opens
|
|
# with "is a dystopian social science fiction novel by ...").
|
|
_FICTION_BODY_MARKERS = frozenset({
|
|
"novel", "novels", "novella", "novellas", "fiction", "fictional",
|
|
"fantasy", "dystopian", "satire", "satirical", "allegory",
|
|
"allegorical", "characters", "protagonist", "antagonist",
|
|
"plot", "narrator", "narrative", "chapter", "chapters",
|
|
"published", "publishing", "publisher", "screenplay", "directed",
|
|
"starring", "cast", "filmed", "broadcast", "aired", "premiered",
|
|
"written", "writer", "author", "playwright", "screenwriter",
|
|
"adaptation", "adapted", "sequel", "prequel", "trilogy",
|
|
"saga", "franchise", "manga", "anime",
|
|
})
|
|
|
|
# Distinct-marker threshold: a body sample must contain at least
|
|
# this many DISTINCT fiction markers to classify as reference work.
|
|
# Counting distinct markers (not total occurrences) defends against
|
|
# a single common-vocabulary use repeated through the sample (e.g.
|
|
# "novel approach" + "novel approach" doesn't count as two markers,
|
|
# only one distinct).
|
|
_FICTION_DISTINCT_MARKER_THRESHOLD = 3
|
|
|
|
|
|
FrameKind = Literal["literal", "reference", "ambiguous", "no_phrase_route"]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FrameDetection:
|
|
"""Reference-frame classification for a claim-lattice query.
|
|
|
|
Returned by ``detect_frame``. Consumed by:
|
|
- prompt augmentation (when ``frame_kind == "reference"``,
|
|
inject a polarity instruction into the system prompt)
|
|
- renderer (when ``frame_kind == "reference"``, render a
|
|
Frame notes section under the answer)
|
|
|
|
Never persisted in `providence_cache`, never enters
|
|
`governance_policy_hash`, never folds into the run-DAG. Soft
|
|
sidecar signal only.
|
|
"""
|
|
frame_kind: FrameKind
|
|
reference_title: str | None = None # e.g. "Nineteen Eighty-Four"
|
|
reference_uri: str | None = None # e.g. wiki URI tail
|
|
confidence: float = 0.0 # 0.0-1.0, heuristic-based
|
|
|
|
|
|
def _title_indicates_reference_work(title: str | None) -> bool:
|
|
"""Title carries a Wikipedia-style disambiguation suffix for a
|
|
fictional / reference work."""
|
|
if not title:
|
|
return False
|
|
return bool(_REFERENCE_TITLE_SUFFIX_RE.search(title))
|
|
|
|
|
|
def _body_indicates_reference_work(body_sample: str | None) -> bool:
|
|
"""Body sample contains at least
|
|
``_FICTION_DISTINCT_MARKER_THRESHOLD`` distinct fiction markers.
|
|
`body_sample` should be a short prefix (~1-2 KB) of the source's
|
|
first chunk; we don't scan the whole article.
|
|
|
|
Counting DISTINCT markers (not total occurrences) keeps the
|
|
heuristic robust against single-marker repetition: a history
|
|
article saying "novel approach" twice doesn't trip the detector
|
|
because only one marker (`novel`) fired. A reference-work lead
|
|
typically clusters several distinct markers (`novel` + `published`
|
|
+ `characters` + `plot` + `protagonist` etc.).
|
|
"""
|
|
if not body_sample:
|
|
return False
|
|
text_lower = body_sample.lower()
|
|
distinct_hits = 0
|
|
for marker in _FICTION_BODY_MARKERS:
|
|
pattern = r"\b" + re.escape(marker) + r"\b"
|
|
if re.search(pattern, text_lower):
|
|
distinct_hits += 1
|
|
if distinct_hits >= _FICTION_DISTINCT_MARKER_THRESHOLD:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _source_is_reference_work(source: dict) -> bool:
|
|
"""A source is a reference work when its title carries a
|
|
disambiguation suffix OR its body opens with high fiction-marker
|
|
density. Either signal alone suffices.
|
|
"""
|
|
if _title_indicates_reference_work(source.get("title")):
|
|
return True
|
|
return _body_indicates_reference_work(source.get("body_sample"))
|
|
|
|
|
|
def detect_frame(
|
|
question: str,
|
|
sources: list[dict],
|
|
phrase_match_roots: set[str] | None = None,
|
|
) -> FrameDetection:
|
|
"""Classify the query's intended frame.
|
|
|
|
Inputs:
|
|
- ``question`` — the user's question text (currently unused
|
|
directly; kept for future shape-detection extensions).
|
|
- ``sources`` — the retrieved sources for the query. Each
|
|
dict should carry ``document_root``, ``title``, and
|
|
``body_sample`` (a short prefix of the first chunk).
|
|
- ``phrase_match_roots`` — set of document_roots that were
|
|
surfaced by the phrase-pattern retrieval route (allusion
|
|
/ verbatim multi-token sequence match). When non-empty,
|
|
the query is allusion-shape and the corresponding sources
|
|
are candidate reference works.
|
|
|
|
Algorithm:
|
|
1. If no phrase route fired (``phrase_match_roots`` empty),
|
|
the query is literal — no allusion signal at retrieval.
|
|
2. If phrase route fired AND any phrase-matched source is a
|
|
reference work (title suffix or body fiction-density), the
|
|
query is reference-frame; pick the first such source as
|
|
the named reference.
|
|
3. If phrase route fired but no matched source is a reference
|
|
work, classify ``ambiguous`` — the verbatim phrase appeared
|
|
somewhere in the corpus but not in a fictional context.
|
|
|
|
Returns:
|
|
FrameDetection with the classified ``frame_kind``,
|
|
``reference_title`` / ``reference_uri`` populated for
|
|
reference-frame queries, and a confidence score that
|
|
consumers can threshold if they want a more conservative
|
|
gate.
|
|
"""
|
|
if not phrase_match_roots:
|
|
return FrameDetection(frame_kind="no_phrase_route", confidence=0.0)
|
|
candidate: dict | None = None
|
|
for src in sources:
|
|
if src.get("document_root") not in phrase_match_roots:
|
|
continue
|
|
if _source_is_reference_work(src):
|
|
candidate = src
|
|
break
|
|
if candidate is not None:
|
|
return FrameDetection(
|
|
frame_kind="reference",
|
|
reference_title=candidate.get("title"),
|
|
reference_uri=candidate.get("document_uri"),
|
|
confidence=0.8,
|
|
)
|
|
# Phrase route fired but no clear reference-work source — the
|
|
# verbatim phrase appeared in non-fiction context. Could be a
|
|
# quotation in a history article, a proverbs page, etc.
|
|
return FrameDetection(frame_kind="ambiguous", confidence=0.4)
|