Repository navigation
feat(knowledge): carry page provenance as metadata, not marker text (#13894) - #14239
Conversation
…13894) The KB upload path stored PDF text carrying '## Page N' markers, which went straight into the embedding. Those markers are structure, not meaning: they compete with the document's own words for similarity, and recovering a page number by parsing them back out of retrieved text breaks the moment a chunker splits between a marker and its page. Provenance now travels as character spans in fact metadata, with the stored text marker-free. render_plain() returns the text and the spans together from one pure call over the same pages, so the offsets always index the exact string stored - that invariant is what every citation rests on, and it is asserted directly. Adds page_for_offset(), pages_for_span() and chunk_page_map(). A span crossing a page break reports both pages rather than the first, since a chunk that straddles a break genuinely comes from two pages. chunk_page_map scans forward from the previous chunk's end: searching from zero attributes every recurring header, footer and table scaffold to page 1. An offset landing in the separator between pages resolves to None, and unpaginated formats get no page metadata at all - a DOCX has no pages, and 'page: 1' would be a fabricated citation. The marker-carrying renderer stays for callers that want it (the Drive/OneDrive connectors already emitted that shape).
|
Suite instability filed as #14240 — the failure-count comparison in this PR body is only meaningful because the identical command was re-run on base. |
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
Code reviewReviewed from a sweep of green-but-unreviewed PRs, not as the author. One blocking finding,
[{"page": 1, "start": 0, "end": 13}, {"page": 2, "start": 15, "end": 27}]
I ran the sanitizer's expression against that shape to confirm. That is the copy retrieval uses. So the invariant the PR calls "the whole design" — offsets that always index the exact stored Why the suite missed it: Simplest fix consistent with the existing sanitizer: Second, smaller point (already disclosed, flagging for tracking). The connectors still emit Not a finding: documents already stored with in-text markers read exactly as before, and no |
|
PR #14260 fixes the sanitizer itself (#14257), so Once it lands, Worth adding one test on this PR's side that crosses the storage seam rather than asserting on |
|
The blocking finding I raised is resolved by base, not by anything needed here. #14260 fixed So no producer-side workaround is needed here — update this branch onto current base and the offsets survive the write and come back parseable from the Chroma read at The smaller point stands and is worth a line before merge: The connector gap (Drive/OneDrive still emitting |
Thinking Path
The KB upload path stored PDF text carrying
## Page Nmarkers — markers Iintroduced in #13893 when unifying the page-marker convention — and that text
goes straight into the embedding.
Those markers are structure, not meaning. They compete with the document's own
words for similarity, and they are a poor provenance mechanism anyway: recovering
a page number by parsing markers back out of retrieved text breaks the moment a
chunker splits between a marker and the page it labels.
What Changed
render_plain(pages)returns the marker-free text and the page spans fromone pure call over the same pages. That pairing is the whole design: the offsets
always index the exact string that was stored, because both come from the same
render. Everything else rests on that invariant, so it is asserted directly rather
than assumed.
Resolution helpers —
page_for_offset(),pages_for_span(),chunk_page_map().Three decisions worth calling out:
break genuinely comes from two pages; reporting only the first would silently
mis-cite half its content.
chunk_page_mapscans forward from the previous chunk's end. Afind()from zero attributes every recurring header, footer and table scaffold to page 1
— precisely the content that repeats.
None, and unpaginatedformats get no page metadata at all. A DOCX has no pages, and
page: 1would bea fabricated citation.
Upload path stores marker-free content plus
page_count,page_spansandpages_with_textin fact metadata.Verification
Mutation-tested:
Mutation 2 initially passed, and that was a test defect, not a pass. The
boilerplate test used similar chunks (
"CONFIDENTIAL one"/"CONFIDENTIAL two"),which
find()from zero still resolves correctly. It now uses byte-identicalchunks across three pages — the only shape that actually distinguishes a forward
cursor from a naive search. Caught because the mutation was run, not because the
test looked right.
Startup imports:
352 passed.A note on the wider suite
The combined
knowledge/ tests/knowledge tests/apirun is order-unstable, soraw failure counts are not comparable between runs:
Dev_new_gui)Every failure on this branch also occurs on base, and each passes in isolation
(
tests/api/test_knowledge_grounding.py→31 passedalone, on both). This branchintroduces none. Filed separately as a finding — a suite that cannot distinguish a
new failure from a scheduling artefact cannot gate anything.
Model Used
Claude Opus 5 (1M context)
Closes #13894. Part of #13892 — Wave 2.