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: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ All notable changes to vouch are documented here. Format follows
the same resolution now backs `capture answer`, which previously left
re-captured duplicates pending forever.

### Fixed
- `receipts.verify_receipt()` and `receipts.verify_evidence()` no longer
report `VERIFIED`/fall through past their guard for an empty-quote,
zero-length byte span. both guards only checked `quote is None`, not an
empty string, so `quote=""` with `byte_start == byte_end` decoded to
`""`, trivially equaled the empty quote, and `verify_receipt` returned
`VERIFIED` -- despite carrying no actual quoted text, contradicting the
function's own docstring. `locate_span` already refused to mint such a
receipt on the propose path; this closes the same gap on the verify
path, reachable via bundle import or sync.

## [1.4.0] — 2026-07-17

### Added
Expand Down
4 changes: 2 additions & 2 deletions src/vouch/receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def verify_receipt(evidence: Evidence, source_bytes: bytes) -> ReceiptResult:
when the decoded span equals the quote exactly.
"""
start, end, quote = evidence.byte_start, evidence.byte_end, evidence.quote
if start is None or end is None or quote is None:
if start is None or end is None or not quote:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return ReceiptResult(ReceiptStatus.NO_RECEIPT, "no byte-offset span")
if start > end or end > len(source_bytes):
return ReceiptResult(
Expand Down Expand Up @@ -184,7 +184,7 @@ def verify_evidence(store: KBStore, evidence: Evidence) -> ReceiptResult:
"""
from .storage import ArtifactNotFoundError

if evidence.byte_start is None or evidence.byte_end is None or evidence.quote is None:
if evidence.byte_start is None or evidence.byte_end is None or not evidence.quote:
return ReceiptResult(ReceiptStatus.NO_RECEIPT, "no byte-offset span")
try:
source_bytes = store.read_source_content(evidence.source_id)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ def test_no_receipt_when_quote_absent() -> None:
assert result.status is ReceiptStatus.NO_RECEIPT


def test_no_receipt_when_quote_is_empty_string() -> None:
# a zero-length span decodes to "" and trivially equals an empty quote --
# the docstring promises NO_RECEIPT for "no quote to compare", and
# locate_span already refuses to mint an empty-quote receipt for the same
# reason (see test_locate_span_returns_none_for_empty_quote); verify_receipt
# must reject one landing on disk some other way (bundle import, sync),
# not report it VERIFIED.
ev = _ev(quote="", byte_start=0, byte_end=0)
result = verify_receipt(ev, SOURCE)
assert result.status is ReceiptStatus.NO_RECEIPT
assert result.verified is False


def test_receipt_uses_byte_offsets_not_char_offsets() -> None:
# "café — au lait": 'é' is 2 bytes (0xc3 0xa9), '—' is 3 bytes (em dash).
# "au lait" starts at char index 7 but byte index 10. A char-offset
Expand Down Expand Up @@ -160,6 +173,23 @@ def test_verify_evidence_not_verified_when_source_missing(store: KBStore) -> Non
assert result.verified is False


def test_verify_evidence_no_receipt_for_empty_quote_and_missing_source(
store: KBStore,
) -> None:
# verify_evidence has its own pre-check ahead of the source-read (so a
# missing source doesn't mask a plain "nothing to compare" case) --
# it must apply the same falsy-quote rule verify_receipt does, or an
# empty quote paired with a missing source reports FORGED instead of
# NO_RECEIPT, same bug as verify_receipt's, one call site up.
ev = Evidence(
id="e4", source_id="does-not-exist",
locator="x", quote="", byte_start=0, byte_end=0,
)
result = verify_evidence(store, ev)
assert result.status is ReceiptStatus.NO_RECEIPT
assert result.verified is False


# ---- the quote step: locate a span, or drop what cannot be quoted ----


Expand Down
Loading