Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/vouch/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,6 @@ def build_context_pack(
budget_clipped = 0
budget_omitted = 0

if require_citations:
for it in items:
if it.type == "claim" and not it.citations:
uncited.append(it.id)

if max_chars is not None:
total = sum(len(i.summary) for i in items)
if total > max_chars:
Expand All @@ -124,6 +119,12 @@ def build_context_pack(
items.pop()
budget_omitted += 1

# Compute the citation gate over the items actually returned — AFTER the
# max_chars budget has dropped tail items — so the gate never fails on (or
# reports in uncited_items) claims the consumer did not receive.
if require_citations:
uncited = [it.id for it in items if it.type == "claim" and not it.citations]

if len(items) < min_items:
warnings.append(f"only {len(items)} items, minimum {min_items}")
failed.append("min_items")
Expand Down
20 changes: 20 additions & 0 deletions tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@ def test_context_pack_has_quality_metadata(store: KBStore) -> None:
assert pack["quality"]["ok"] is True


def test_require_citations_only_considers_returned_items(store: KBStore) -> None:
# Uncited claims (no evidence). With max_chars dropping tail items, the
# require_citations gate and uncited_items must reference only items actually
# returned — never claims the budget already dropped. Before the fix, uncited
# was computed pre-budget, so uncited_items could name absent items.
for i in range(20):
store.put_claim(Claim(
id=f"u{i}",
text=f"uncited padding claim number {i} with extra text",
evidence=[],
))
health.rebuild_index(store)
pack = context.build_context_pack(
store, query="padding", max_chars=80, require_citations=True,
)
returned = {it["id"] for it in pack["items"]}
assert pack["quality"]["uncited_items"], "expected uncited claims to be flagged"
assert all(uid in returned for uid in pack["quality"]["uncited_items"])


def test_context_pack_max_chars_omits_items(store: KBStore) -> None:
src = store.put_source(b"e")
# Many short claims — total summary length > 100 chars.
Expand Down