Skip to content

feat(provenance): persist document structure as document_blocks - #105

Merged
mrsibe merged 1 commit into
mainfrom
feat/document-blocks
Sep 25, 2026
Merged

mrsibe merged 1 commit into
mainfrom
feat/document-blocks

Conversation

@mrsibe

@mrsibe mrsibe commented Sep 25, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

Persists the document structure the loaders already extract, so every character of a document can be traced back to a page, a heading and a bounding box. This is the data layer the citation work (#67, #69–#73) will build on.

Why?

addDocumentFromFile() consumed only parseResult.content/title/mimeType/metadata and discarded parseResult.structure entirely. There was nowhere to record which page or heading a span of text came from, so a future citation could only ever be document-level. See #66 and the epic #82.

Related issue

Fixes #66

What changed?

  • Schema + migration — new document_blocks table (additive migration 0014_naive_lifeguard.sql): id, document_id, kind, order, page, level, text, start_offset, end_offset, bbox, metadata; indexes on (document_id, order) and (document_id, page). No changes to chunks.
  • src/main/services/blocks/documentBlocks.ts — buildDocumentBlocks() is a pure function from a parse result to ordered block drafts. It never re-cleans text: every block's text is content.slice(startOffset, endOffset), so documents.content.slice(start_offset, end_offset) === text holds by construction.
    • Paged (PDF/PPT): one block per page, page matching structure.pages, with the page's normalized bbox.
    • Structured (Markdown/DOCX): heading blocks (kind: 'heading', level) plus paragraph blocks for the body between headings. It only trusts a heading offset when the title actually appears there, so WebLoader's all-zero offsets fall back to flat blocks instead of becoming bogus headings.
    • Flat (text/URL/notes, or unusable offsets): paragraph-level blocks split on blank lines.
  • KnowledgeService — persists blocks in both ingestion paths: addDocumentFromFile() (with structure) and addDocument() (text, URL and note, flat). Adds getDocumentBlocks(), and deletes a document's blocks explicitly rather than relying on a foreign-key cascade the connection does not enable.
  • PdfLoader — records a page's normalized text bbox, mapping its text items through the existing viewport transform so the renderer does not need to know the scale.

chunks are untouched, so retrieval and search results are unchanged — this issue only adds data.

How was this tested?

  • npm test — 100 tests pass (8 new in test/blocks.test.ts): the slice invariant for every block from every fixture, dense/ordered numbering, PDF page agreement, bbox normalization inside the unit square, DOCX/Markdown heading levels, the flat fallback, and the untrusted-offset fallback.
  • npm run typecheck — clean.
  • npm run build — clean; verified 0014_naive_lifeguard.sql and meta/0014_snapshot.json are copied into out/main/db/migrations.
  • npm run check:design — clean.
  • npx eslint on every changed file — clean.

Not run locally: build:unpack + smoke:packaged (no xvfb on this machine). CI runs both on all three platforms; runMigrations() there applies migration 0014 against a fresh database, which is the packaged check that matters for this change. A manual PDF import is the remaining manual step from the issue.

Screenshots / recordings

Not applicable — no UI change.

Checklist

  • I have reviewed my own changes.
  • npm run typecheck passes.
  • npm run build passes.
  • I have tested the affected user workflow. (manual PDF import not run here; covered by CI smoke + the new unit tests)
  • I have not included unrelated changes.
  • I have updated documentation when necessary.

Desktop / build changes

  • Not applicable
  • npm run build:unpack passes. (delegated to CI)
  • npm run smoke:packaged passes. (delegated to CI)

`addDocumentFromFile()` read `parseResult.content/title/mimeType/metadata`
and dropped `parseResult.structure`, so nothing downstream could tell which
page or heading a character belonged to. Add the missing persistent layer.

- `document_blocks` (schema + additive migration 0014): id, document_id,
  kind, order, page, level, text, start_offset, end_offset, bbox, metadata,
  with indexes on (document_id, order) and (document_id, page).
- `buildDocumentBlocks()` turns a parse result into blocks in document
  order. It is pure and never re-cleaned text: every block's text is
  `content.slice(startOffset, endOffset)`, so the invariant
  `documents.content.slice(start_offset, end_offset) === text` holds by
  construction. Paged documents get one block per page with a normalized
  bbox; Markdown/DOCX use their heading structure (heading + paragraph
  blocks); text/URL/notes and structures with unusable offsets fall back to
  paragraph-level flat blocks.
- `KnowledgeService` persists blocks in both ingestion paths
  (`addDocumentFromFile` and `addDocument`, which covers text, URL and
  note), exposes `getDocumentBlocks()`, and deletes a document's blocks
  explicitly rather than relying on a foreign-key cascade that the
  connection does not enable.
- `PdfLoader` now records a page's normalized text bounding box, computed
  from its text items through the existing viewport transform.

`chunks` are untouched, so retrieval and search results are unchanged;
this issue only adds data.

Verification: `test/blocks.test.ts` asserts the slice invariant, dense
ordering, PDF page agreement and bbox normalization, DOCX/Markdown heading
levels, and the flat fallback — 100 tests pass. `npm run typecheck`,
`npm run check:design` and `npm run build` are clean (the build copies
0014 into out/main/db/migrations).

Refs #66
@github-actions github-actions Bot added the enhancement New feature or request label Sep 25, 2026
@mrsibe
mrsibe merged commit e10cd66 into main Sep 25, 2026
4 checks passed
@mrsibe
mrsibe deleted the feat/document-blocks branch September 25, 2026 05:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Persist document structure as document_blocks (stop discarding parseResult.structure)

1 participant