backdraft 0.3.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. backdraft-0.4.1/.claude-plugin/marketplace.json +16 -0
  2. backdraft-0.4.1/.claude-plugin/plugin.json +14 -0
  3. {backdraft-0.3.0 → backdraft-0.4.1}/DESIGN.md +8 -5
  4. {backdraft-0.3.0 → backdraft-0.4.1}/PKG-INFO +80 -26
  5. {backdraft-0.3.0 → backdraft-0.4.1}/README.md +73 -23
  6. backdraft-0.4.1/ROADMAP.md +63 -0
  7. {backdraft-0.3.0 → backdraft-0.4.1}/SPEC.md +14 -6
  8. {backdraft-0.3.0 → backdraft-0.4.1}/demo/memo.backdraft.html +352 -377
  9. {backdraft-0.3.0 → backdraft-0.4.1}/demo/walkthrough.md +3 -3
  10. {backdraft-0.3.0 → backdraft-0.4.1}/pyproject.toml +19 -8
  11. {backdraft-0.3.0 → backdraft-0.4.1}/site/demo.html +352 -377
  12. {backdraft-0.3.0 → backdraft-0.4.1}/site/docs.html +50 -19
  13. {backdraft-0.3.0 → backdraft-0.4.1}/site/index.html +2 -2
  14. {backdraft-0.3.0 → backdraft-0.4.1}/site/llms.txt +15 -6
  15. {backdraft-0.3.0 → backdraft-0.4.1}/skills/backdraft/SKILL.md +8 -3
  16. {backdraft-0.3.0 → backdraft-0.4.1}/skills/backdraft-artifact/SKILL.md +1 -1
  17. {backdraft-0.3.0 → backdraft-0.4.1}/skills/backdraft-backfill/SKILL.md +7 -1
  18. backdraft-0.4.1/src/backdraft/__main__.py +6 -0
  19. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/bind/evidence.py +2 -2
  20. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/cli.py +72 -36
  21. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/credentials.py +2 -1
  22. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/extract/__init__.py +1 -1
  23. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/extract/base.py +11 -8
  24. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/extract/csv.py +5 -5
  25. backdraft-0.4.1/src/backdraft/extract/docx.py +159 -0
  26. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/extract/image.py +8 -7
  27. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/extract/pdf_text.py +3 -3
  28. backdraft-0.4.1/src/backdraft/extract/pptx.py +121 -0
  29. backdraft-0.4.1/src/backdraft/extract/sheet.py +147 -0
  30. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/extract/vlm.py +10 -7
  31. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/extract/vlm_settings.py +34 -4
  32. backdraft-0.4.1/src/backdraft/extract/xls.py +79 -0
  33. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/extract/xlsx.py +11 -116
  34. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/kernel/model.py +8 -1
  35. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/registry/store.py +3 -0
  36. backdraft-0.4.1/src/backdraft/render/html/__init__.py +90 -0
  37. backdraft-0.4.1/src/backdraft/render/html/assets.py +633 -0
  38. backdraft-0.4.1/src/backdraft/render/html/components.py +301 -0
  39. backdraft-0.4.1/src/backdraft/render/html/fmt.py +80 -0
  40. backdraft-0.4.1/src/backdraft/render/html/fmt_vectors.py +65 -0
  41. backdraft-0.4.1/src/backdraft/render/html/page.py +206 -0
  42. backdraft-0.4.1/src/backdraft/render/html/text.py +244 -0
  43. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_bind_evidence.py +7 -0
  44. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_cli.py +113 -0
  45. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_extract.py +313 -2
  46. backdraft-0.4.1/tests/test_fmt_parity.py +60 -0
  47. backdraft-0.4.1/tests/test_snapshot_settings.py +113 -0
  48. {backdraft-0.3.0 → backdraft-0.4.1}/uv.lock +173 -7
  49. backdraft-0.3.0/ROADMAP.md +0 -78
  50. backdraft-0.3.0/src/backdraft/render/html.py +0 -1329
  51. {backdraft-0.3.0 → backdraft-0.4.1}/.github/workflows/ci.yml +0 -0
  52. {backdraft-0.3.0 → backdraft-0.4.1}/.github/workflows/release.yml +0 -0
  53. {backdraft-0.3.0 → backdraft-0.4.1}/.github/workflows/site.yml +0 -0
  54. {backdraft-0.3.0 → backdraft-0.4.1}/.gitignore +0 -0
  55. {backdraft-0.3.0 → backdraft-0.4.1}/.python-version +0 -0
  56. {backdraft-0.3.0 → backdraft-0.4.1}/LICENSE +0 -0
  57. {backdraft-0.3.0 → backdraft-0.4.1}/assets/backdraft-mark.svg +0 -0
  58. {backdraft-0.3.0 → backdraft-0.4.1}/demo/generate_sources.py +0 -0
  59. {backdraft-0.3.0 → backdraft-0.4.1}/demo/memo.md +0 -0
  60. {backdraft-0.3.0 → backdraft-0.4.1}/demo/sources/t12-summary.pdf +0 -0
  61. {backdraft-0.3.0 → backdraft-0.4.1}/demo/sources/underwriting-model.xlsx +0 -0
  62. {backdraft-0.3.0 → backdraft-0.4.1}/spec/artifact.md +0 -0
  63. {backdraft-0.3.0 → backdraft-0.4.1}/spec/chunking.md +0 -0
  64. {backdraft-0.3.0 → backdraft-0.4.1}/spec/tokens.md +0 -0
  65. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/__init__.py +0 -0
  66. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/bind/__init__.py +0 -0
  67. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/bind/binder.py +0 -0
  68. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/bind/cli.py +0 -0
  69. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/bind/verify/__init__.py +0 -0
  70. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/bind/verify/base.py +0 -0
  71. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/bind/verify/entail.py +0 -0
  72. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/bind/verify/overlap.py +0 -0
  73. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/bind/verify/recompute.py +0 -0
  74. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/bind/verify/value_trace.py +0 -0
  75. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/cli_context.py +0 -0
  76. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/extract/text.py +0 -0
  77. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/gate/__init__.py +0 -0
  78. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/gate/cli.py +0 -0
  79. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/gate/reader.py +0 -0
  80. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/gate/searcher.py +0 -0
  81. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/kernel/__init__.py +0 -0
  82. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/kernel/artifact.py +0 -0
  83. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/kernel/chunking.py +0 -0
  84. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/kernel/claims.py +0 -0
  85. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/kernel/errors.py +0 -0
  86. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/kernel/hashing.py +0 -0
  87. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/kernel/tokens.py +0 -0
  88. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/registry/__init__.py +0 -0
  89. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/registry/ledger.py +0 -0
  90. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/registry/schema.sql +0 -0
  91. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/render/__init__.py +0 -0
  92. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/render/_text.py +0 -0
  93. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/render/cli.py +0 -0
  94. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/render/footnotes.py +0 -0
  95. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/render/markdown.py +0 -0
  96. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/render/placement.py +0 -0
  97. {backdraft-0.3.0 → backdraft-0.4.1}/src/backdraft/render/sidecar.py +0 -0
  98. {backdraft-0.3.0 → backdraft-0.4.1}/tests/conftest.py +0 -0
  99. {backdraft-0.3.0 → backdraft-0.4.1}/tests/conftest_registry.py +0 -0
  100. {backdraft-0.3.0 → backdraft-0.4.1}/tests/conftest_render.py +0 -0
  101. {backdraft-0.3.0 → backdraft-0.4.1}/tests/fakes.py +0 -0
  102. {backdraft-0.3.0 → backdraft-0.4.1}/tests/gate/conftest.py +0 -0
  103. {backdraft-0.3.0 → backdraft-0.4.1}/tests/gate/fake_registry.py +0 -0
  104. {backdraft-0.3.0 → backdraft-0.4.1}/tests/gate/test_gate_cli.py +0 -0
  105. {backdraft-0.3.0 → backdraft-0.4.1}/tests/gate/test_gate_ledger.py +0 -0
  106. {backdraft-0.3.0 → backdraft-0.4.1}/tests/gate/test_gate_reader.py +0 -0
  107. {backdraft-0.3.0 → backdraft-0.4.1}/tests/gate/test_gate_searcher.py +0 -0
  108. {backdraft-0.3.0 → backdraft-0.4.1}/tests/gate/test_reader_cells.py +0 -0
  109. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/bind/backfill.bound.md +0 -0
  110. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/bind/frontwalk.bound.md +0 -0
  111. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/long_tail.json +0 -0
  112. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/long_tail.txt +0 -0
  113. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/many_small.json +0 -0
  114. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/many_small.txt +0 -0
  115. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/mixed.json +0 -0
  116. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/mixed.txt +0 -0
  117. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/one_giant.json +0 -0
  118. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/one_giant.txt +0 -0
  119. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/short_page.json +0 -0
  120. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/short_page.txt +0 -0
  121. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/unsplittable.json +0 -0
  122. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/chunking/unsplittable.txt +0 -0
  123. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/render/backfill.footnotes.md +0 -0
  124. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/render/backfill.sidecar.json +0 -0
  125. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/render/demo.footnotes.md +0 -0
  126. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden/render/demo.sidecar.json +0 -0
  127. {backdraft-0.3.0 → backdraft-0.4.1}/tests/golden_util.py +0 -0
  128. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_bind_binder.py +0 -0
  129. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_bind_cli.py +0 -0
  130. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_bind_verify.py +0 -0
  131. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_chunking.py +0 -0
  132. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_claims.py +0 -0
  133. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_credentials.py +0 -0
  134. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_footnotes.py +0 -0
  135. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_gate_integration.py +0 -0
  136. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_hashing.py +0 -0
  137. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_html.py +0 -0
  138. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_invariants.py +0 -0
  139. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_model.py +0 -0
  140. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_placement.py +0 -0
  141. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_records_location.py +0 -0
  142. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_registry.py +0 -0
  143. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_reingest.py +0 -0
  144. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_render_cli.py +0 -0
  145. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_render_markdown.py +0 -0
  146. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_sidecar.py +0 -0
  147. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_tokens.py +0 -0
  148. {backdraft-0.3.0 → backdraft-0.4.1}/tests/test_value_trace.py +0 -0
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "backdraft",
3
+ "owner": {
4
+ "name": "Spencer Braun",
5
+ "url": "https://github.com/spencerbraun"
6
+ },
7
+ "description": "Self-hosting marketplace for the backdraft plugin: agent skills for writing documents whose factual claims carry verifiable citations.",
8
+ "plugins": [
9
+ {
10
+ "name": "backdraft",
11
+ "source": "./",
12
+ "description": "Drop-in provenance for factual claims: one click from any claim to the evidence behind it. Ships the backdraft writing, backfill, and artifact-reading skills; the CLI installs separately (uv tool install backdraft).",
13
+ "category": "productivity"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "backdraft",
3
+ "displayName": "Backdraft",
4
+ "version": "0.4.1",
5
+ "description": "Drop-in provenance for factual claims: one click from any claim to the evidence behind it. Ships the backdraft writing, backfill, and artifact-reading skills; the CLI installs separately (uv tool install backdraft).",
6
+ "author": {
7
+ "name": "Spencer Braun",
8
+ "url": "https://github.com/spencerbraun"
9
+ },
10
+ "homepage": "https://backdraft.dev",
11
+ "repository": "https://github.com/spencerbraun/backdraft",
12
+ "license": "MIT",
13
+ "keywords": ["citations", "provenance", "sources", "evidence", "documents", "pdf", "xlsx", "writing"]
14
+ }
@@ -130,16 +130,19 @@ Self-contained HTML (plus the machine-readable sidecar): **quotes + receipts + e
130
130
  | 2026-07-27 | **Supersedes "VLM when key present":** credentials are explicit. Ambient provider keys (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`) are never read; a credential reaches backdraft only via `--config`, a `BACKDRAFT_*` variable, or `.backdraft/env` (`init` writes a template). `vlm_ready` — the `auto` gate — is therefore equivalent to consent. Decided after a real unapproved-spend incident during the first field trial: presence of a generic key in the environment is not consent to spend it or send documents to its provider. |
131
131
  | 2026-07-28 | **Reader doctrine (supersedes "no script"):** the artifact is designed for two readers — the writer verifying the AI's work, and a normal person the document is sent to — in three disclosure layers: the document (editorial typography, quiet marks), the footnote (click a claim → the source's own words, named and paged — no tokens, no hashes, no jargon), and the record (one deliberate step to the full machine-readable layer). **Success is silent; only failure speaks**: a fully-resolved artifact looks like a well-made document, while unresolved citations announce themselves before the reader invests belief. The load-bearing constraint is *no network, single file* — enforced by a CSP `meta` tag (`default-src 'none'`) — not *no script*: inline JS is allowed as progressive enhancement over a CSS-only baseline that survives script-stripping. Decided after field use: the v1 artifact rendered success and failure at equal volume and read as machinery, not a document. |
132
132
  | 2026-07-28 | **Evidence embeds in the artifact (supersedes "no page images in v0"):** the footnote layer carries the evidence itself, not just the quote — the cited page as an image, the cited cell in a mini-grid window (neighbors, row labels, column headers). Bounded by *what is cited*, never the corpus, so a memo citing ten pages costs ~2–3 MB; `--lean` skips images. Evidence is assembled at **bind** (the step that has the registry) and travels in the sidecar; render stays registry-blind and the artifact stays reproducible from the two files a reader was handed. For the VLM path, the page image the model was shown is stored at **ingest** — for a non-deterministic extractor the input pixels are the only reproducible snapshot, so the image completes the receipt chain (claim → quote → page). Reference shapes: WebP quality 85 for pages; sheet windows as compact JSON with typed cell values and column-letter headers. |
133
- | 2026-07-28 | **Exhibits: the evidence-first inversion.** The artifact carries a second reading mode: every source document with the data points cited from it, each with its evidence context and backlinks to the claims that lean on it — "this memo rests on N data points across M documents." Ranking is deterministic in v1 (an anchor cited by three claims outranks one cited once); model-judged importance is a later switch. This is the skeptical reader's entry point and the writer's spot-check checklist. |
133
+ | 2026-07-28 | **Exhibits: the evidence-first inversion.** The artifact carries a second reading mode: every source document with the data points cited from it, each with its evidence context and backlinks to the claims that lean on it — "this memo rests on N data points across M documents." Ranking is deterministic in v1 (an anchor cited by three claims outranks one cited once); model-judged importance is a later switch. This is the skeptical reader's entry point and the writer's spot-check checklist. *(2026-07-29: parked in ROADMAP — file-size and design-restraint concerns; needs a deliberate design pass and a real audience first.)* |
134
+ | 2026-07-29 | **Office formats extract natively, never through conversion.** DOCX and PPTX are read straight from their XML (python-docx / python-pptx, core deps). Converting to PDF was rejected on principle: conversion is rendering, so it needs a layout engine (LibreOffice, MS Office, or a cloud API — a 700MB dependency, a platform lock, or documents leaving the machine), and converter-dependent pagination would break content-addressed locator stability. DOCX pages are heading sections: the split level is the smallest outline level present among {Heading 1, Heading 2}; preamble is section 1; no headings → one section — locators stay `pN.cM`, no grammar change. PPTX pages are slides (slide N is slide N in every renderer). The text floor deliberately misses slide visuals; the ingest note says so and names the path that captures them — the user exporting the deck to PDF with the real renderer they already have, ingested through the vision extractor — so a calling agent can relay it. |
135
+ | 2026-07-29 | **The vision extractor ships by default (supersedes "[vlm] as extra").** `openai` + `pdf2image` moved into core dependencies; `[vlm]` remains as an empty compat alias. Rationale: the VLM path is the recommended path for real PDFs, and an extra made the recommended path opt-in. The consent boundary does not move — installing deps was never consent; the backdraft-scoped `BACKDRAFT_VLM_API_KEY` is, and ambient provider keys stay unread. There is no slim install: pip extras can only add, never subtract; a separate core distribution is possible later if anyone asks. poppler stays a runtime capability check. |
136
+ | 2026-07-29 | **Renderer decomposed; Python is the formatting authority.** `render/html.py` (1,300 lines) became the `render/html/` package (fmt, text, components, assets, page), byte-identical at the seam. The deliberate `fmt_cell` (Python) / `fmtCell` (JS) duplication is now pinned by one shared vector table driving both tests, with the JS run under node when available. The JS was found diverging on exact rounding ties (`toFixed` rounds half-up; Python rounds half-to-even on the double's value) and was rewritten to round exactly — Python defines correct, JS matches it. The artifact emits minified CSS/JS and unindented markup; the `$legend` and sidecar JSON stay readable in view-source (spec constraint). Snapshot budget knobs `BACKDRAFT_SNAPSHOT_QUALITY`/`BACKDRAFT_SNAPSHOT_MAX_HEIGHT` tune page-image weight at ingest — display only, tokens never derive from pixels. |
134
137
 
135
138
  ## Open
136
139
 
137
- Queued work lives in ROADMAP.md (exhibits, region maps, formats, `bd:calc`,
138
- substrates); this list is questions still awaiting a decision:
140
+ Queued and parked work lives in ROADMAP.md; this list is questions still
141
+ awaiting a decision:
139
142
 
140
143
  - Re-bind/orphan pass on re-ingest of changed docs (chunk ordinal drift)
141
- - Prose-file locator forms; slug assignment/collision rules (the trailing-hyphen
142
- truncation from the first field trial lives here)
144
+ - Slug assignment/collision rules (the trailing-hyphen truncation from the
145
+ first field trial lives here)
143
146
  - Distribution: published to PyPI as `backdraft` 0.2.0 (2026-07-28); domain
144
147
  backdraft.dev acquired; still open: skill registration, deploying site/
145
148
  - First external user (design-partner validation layer vs eval substrate vs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backdraft
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Drop-in provenance for factual claims: one click from any claim to the evidence behind it.
5
5
  Project-URL: Homepage, https://backdraft.dev
6
6
  Project-URL: Documentation, https://backdraft.dev/docs.html
@@ -9,14 +9,18 @@ Project-URL: Roadmap, https://github.com/spencerbraun/backdraft/blob/main/ROADMA
9
9
  License: MIT
10
10
  License-File: LICENSE
11
11
  Requires-Python: >=3.13
12
+ Requires-Dist: openai>=1.0
12
13
  Requires-Dist: openpyxl>=3.1
14
+ Requires-Dist: pdf2image>=1.17
13
15
  Requires-Dist: pdfplumber>=0.11
16
+ Requires-Dist: python-docx>=1.1
17
+ Requires-Dist: python-pptx>=1.0
14
18
  Requires-Dist: typer>=0.12
15
19
  Provides-Extra: entail
16
20
  Requires-Dist: anthropic>=0.40; extra == 'entail'
17
21
  Provides-Extra: vlm
18
- Requires-Dist: openai>=1.0; extra == 'vlm'
19
- Requires-Dist: pdf2image>=1.17; extra == 'vlm'
22
+ Provides-Extra: xls
23
+ Requires-Dist: python-calamine>=0.3; extra == 'xls'
20
24
  Description-Content-Type: text/markdown
21
25
 
22
26
  <img src="assets/backdraft-mark.svg" width="56" alt="">
@@ -44,13 +48,14 @@ Docs live at [backdraft.dev](https://backdraft.dev), one page for people, and
44
48
  ## 60 seconds
45
49
 
46
50
  ```bash
47
- uv tool install "backdraft[vlm]"
51
+ uv tool install backdraft
48
52
  cd demo
49
53
  ```
50
54
 
51
55
  **Ingest** the sources. Every anchor is minted here, chunks for PDF pages,
52
- cells for spreadsheet sheets. Formats: PDF, XLSX/XLSM, CSV/TSV, images
53
- (png, jpeg, tiff, through the vision model), plain text and Markdown.
56
+ cells for spreadsheet sheets. Formats: PDF, XLSX/XLSM, XLS, CSV/TSV, DOCX,
57
+ PPTX, images (png, jpeg, tiff, through the vision model), plain text and
58
+ Markdown.
54
59
 
55
60
  ```console
56
61
  $ backdraft init
@@ -60,7 +65,7 @@ documents: 0
60
65
  $ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
61
66
  t12-summary t12-summary.pdf pdf 3 pages
62
67
  underwriting-model underwriting-model.xlsx xlsx 2 pages
63
- note: extracted with pdf-text (the embedded text layer). Glossy or scanned PDFs extract better through a vision model: install backdraft[vlm] and set BACKDRAFT_VLM_API_KEY in .backdraft/env.
68
+ note: extracted with pdf-text (the embedded text layer). Glossy or scanned PDFs extract better through a vision model: set BACKDRAFT_VLM_API_KEY in .backdraft/env.
64
69
  ```
65
70
 
66
71
  **Read.** The page arrives with a citable name over each chunk. This is the whole
@@ -189,21 +194,33 @@ confidential corpora**.
189
194
  ## Install
190
195
 
191
196
  ```bash
192
- uv tool install "backdraft[vlm]" # CLI, recommended form
193
- uv add backdraft # as a dependency
194
- pip install backdraft # or plain pip
197
+ uv tool install backdraft # CLI, recommended form
198
+ uv add backdraft # as a dependency
199
+ pip install backdraft # or plain pip
195
200
  ```
196
201
 
197
- Python 3.13+. Two extras:
202
+ Python 3.13+. The vision extractor ships by default, and it is the
203
+ recommended path for real PDFs (glossy layouts, info boxes, scans): a vision
204
+ model reads each page and its clean representation becomes the receipt.
205
+ Installing it is not consent to use it. `--extractor auto` prefers it only on
206
+ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
207
+ `.backdraft/env`, `backdraft init` writes a template). Ambient
208
+ `OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic
209
+ key in the environment is not consent to send documents to its provider. The
210
+ default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
211
+ base_url + model, set explicitly. Without a scoped key, `auto` falls back to
212
+ the keyless text layer and says so. PDF page rendering needs poppler,
213
+ installed separately.
214
+
215
+ Two extras:
198
216
 
199
217
  | Extra | Adds | For |
200
218
  |---|---|---|
201
- | `[vlm]` | `openai`, `pdf2image` | the VLM extractor, the recommended path for real PDFs (glossy layouts, info boxes, scans): a vision model reads each page and its clean representation becomes the receipt. `--extractor auto` prefers it only on explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or `.backdraft/env`, `backdraft init` writes a template). Ambient `OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic key in the environment is not consent to send documents to its provider. The default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is base_url + model, set explicitly. Without a scoped key, `auto` falls back to the keyless text layer and says so. Needs poppler installed separately |
202
219
  | `[entail]` | `anthropic` | `bind --check entail`, the model-judge verifier |
220
+ | `[xls]` | `python-calamine` | legacy `.xls` workbooks, values only, rendered the same shape as xlsx |
203
221
 
204
- ```bash
205
- pip install "backdraft[vlm,entail]"
206
- ```
222
+ `backdraft[vlm]` still installs: it is an empty compat alias from when the
223
+ vision deps were an extra.
207
224
 
208
225
  From a checkout:
209
226
 
@@ -215,13 +232,7 @@ uv run pytest
215
232
 
216
233
  ## Skills
217
234
 
218
- Three agent skills ship inside the package, and the CLI installs them:
219
-
220
- ```bash
221
- backdraft skill install # the writing skill, into ~/.claude/skills/
222
- backdraft skill install --all # plus backfill and artifact-reading
223
- backdraft skill install --project # into this repo's .claude/skills/
224
- ```
235
+ Three agent skills ship inside the package and in this repo:
225
236
 
226
237
  | Skill | For |
227
238
  |---|---|
@@ -230,7 +241,51 @@ backdraft skill install --project # into this repo's .claude/skills/
230
241
  | [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | reading someone else's artifact cold |
231
242
 
232
243
  The CLI is the system; a skill is one page telling an agent to use it. Nothing in
233
- the skills is required to use backdraft by hand.
244
+ the skills is required to use backdraft by hand. How the skills reach your agent
245
+ depends on the harness:
246
+
247
+ **Claude Code.** The repo is its own plugin marketplace, so the plugin route
248
+ tracks releases:
249
+
250
+ ```text
251
+ /plugin marketplace add spencerbraun/backdraft
252
+ /plugin install backdraft@backdraft
253
+ ```
254
+
255
+ Or have the CLI copy the skills into your skills directory:
256
+
257
+ ```bash
258
+ backdraft skill install # the writing skill, into ~/.claude/skills/
259
+ backdraft skill install --all # plus backfill and artifact-reading
260
+ backdraft skill install --project # into this repo's .claude/skills/
261
+ ```
262
+
263
+ **Claude Cowork.** Once the plugin is listed in Anthropic's community
264
+ directory, it installs from Cowork's built-in skills directory. Until then,
265
+ zip a skill folder from `skills/` and upload it under Customize > Skills.
266
+ Inside a Cowork session the CLI itself runs per-command as
267
+ `uvx backdraft ...`, since installs do not persist between sessions.
268
+
269
+ **Codex, Cursor, Copilot.** Agents in this family read skills from
270
+ `~/.agents/skills`:
271
+
272
+ ```bash
273
+ backdraft skill install --agent codex # into ~/.agents/skills/
274
+ ```
275
+
276
+ or commit the skill folders to your repo under `.agents/skills/` so every
277
+ checkout carries them.
278
+
279
+ **Standing context, any harness.** Paste this into your repo's AGENTS.md:
280
+
281
+ ```markdown
282
+ ## backdraft (cited writing)
283
+ When a document must cite its sources, write it through the backdraft CLI:
284
+ it shows source text with a citation token over every span, and only shown
285
+ spans are citable. In a sandboxed session run every command as
286
+ `uvx backdraft ...` (no install, no PATH edits). Start from
287
+ `uvx backdraft --help`; ground truth is https://backdraft.dev/llms.txt.
288
+ ```
234
289
 
235
290
  ## Documentation
236
291
 
@@ -249,8 +304,7 @@ implementation reads them and nothing else.
249
304
  ## Status
250
305
 
251
306
  v1. The registry format, the token grammar and the artifact format are pinned
252
- by prose specs and golden-file tests. What is deliberately queued, exhibits,
253
- Excel region maps, more formats, `bd:calc` derivations, is written down in
254
- [ROADMAP.md](ROADMAP.md).
307
+ by prose specs and golden-file tests. What is queued, and what is deliberately
308
+ parked with the objection written down, lives in [ROADMAP.md](ROADMAP.md).
255
309
 
256
310
  MIT licensed.
@@ -23,13 +23,14 @@ Docs live at [backdraft.dev](https://backdraft.dev), one page for people, and
23
23
  ## 60 seconds
24
24
 
25
25
  ```bash
26
- uv tool install "backdraft[vlm]"
26
+ uv tool install backdraft
27
27
  cd demo
28
28
  ```
29
29
 
30
30
  **Ingest** the sources. Every anchor is minted here, chunks for PDF pages,
31
- cells for spreadsheet sheets. Formats: PDF, XLSX/XLSM, CSV/TSV, images
32
- (png, jpeg, tiff, through the vision model), plain text and Markdown.
31
+ cells for spreadsheet sheets. Formats: PDF, XLSX/XLSM, XLS, CSV/TSV, DOCX,
32
+ PPTX, images (png, jpeg, tiff, through the vision model), plain text and
33
+ Markdown.
33
34
 
34
35
  ```console
35
36
  $ backdraft init
@@ -39,7 +40,7 @@ documents: 0
39
40
  $ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
40
41
  t12-summary t12-summary.pdf pdf 3 pages
41
42
  underwriting-model underwriting-model.xlsx xlsx 2 pages
42
- note: extracted with pdf-text (the embedded text layer). Glossy or scanned PDFs extract better through a vision model: install backdraft[vlm] and set BACKDRAFT_VLM_API_KEY in .backdraft/env.
43
+ note: extracted with pdf-text (the embedded text layer). Glossy or scanned PDFs extract better through a vision model: set BACKDRAFT_VLM_API_KEY in .backdraft/env.
43
44
  ```
44
45
 
45
46
  **Read.** The page arrives with a citable name over each chunk. This is the whole
@@ -168,21 +169,33 @@ confidential corpora**.
168
169
  ## Install
169
170
 
170
171
  ```bash
171
- uv tool install "backdraft[vlm]" # CLI, recommended form
172
- uv add backdraft # as a dependency
173
- pip install backdraft # or plain pip
172
+ uv tool install backdraft # CLI, recommended form
173
+ uv add backdraft # as a dependency
174
+ pip install backdraft # or plain pip
174
175
  ```
175
176
 
176
- Python 3.13+. Two extras:
177
+ Python 3.13+. The vision extractor ships by default, and it is the
178
+ recommended path for real PDFs (glossy layouts, info boxes, scans): a vision
179
+ model reads each page and its clean representation becomes the receipt.
180
+ Installing it is not consent to use it. `--extractor auto` prefers it only on
181
+ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
182
+ `.backdraft/env`, `backdraft init` writes a template). Ambient
183
+ `OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic
184
+ key in the environment is not consent to send documents to its provider. The
185
+ default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
186
+ base_url + model, set explicitly. Without a scoped key, `auto` falls back to
187
+ the keyless text layer and says so. PDF page rendering needs poppler,
188
+ installed separately.
189
+
190
+ Two extras:
177
191
 
178
192
  | Extra | Adds | For |
179
193
  |---|---|---|
180
- | `[vlm]` | `openai`, `pdf2image` | the VLM extractor, the recommended path for real PDFs (glossy layouts, info boxes, scans): a vision model reads each page and its clean representation becomes the receipt. `--extractor auto` prefers it only on explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or `.backdraft/env`, `backdraft init` writes a template). Ambient `OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic key in the environment is not consent to send documents to its provider. The default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is base_url + model, set explicitly. Without a scoped key, `auto` falls back to the keyless text layer and says so. Needs poppler installed separately |
181
194
  | `[entail]` | `anthropic` | `bind --check entail`, the model-judge verifier |
195
+ | `[xls]` | `python-calamine` | legacy `.xls` workbooks, values only, rendered the same shape as xlsx |
182
196
 
183
- ```bash
184
- pip install "backdraft[vlm,entail]"
185
- ```
197
+ `backdraft[vlm]` still installs: it is an empty compat alias from when the
198
+ vision deps were an extra.
186
199
 
187
200
  From a checkout:
188
201
 
@@ -194,13 +207,7 @@ uv run pytest
194
207
 
195
208
  ## Skills
196
209
 
197
- Three agent skills ship inside the package, and the CLI installs them:
198
-
199
- ```bash
200
- backdraft skill install # the writing skill, into ~/.claude/skills/
201
- backdraft skill install --all # plus backfill and artifact-reading
202
- backdraft skill install --project # into this repo's .claude/skills/
203
- ```
210
+ Three agent skills ship inside the package and in this repo:
204
211
 
205
212
  | Skill | For |
206
213
  |---|---|
@@ -209,7 +216,51 @@ backdraft skill install --project # into this repo's .claude/skills/
209
216
  | [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | reading someone else's artifact cold |
210
217
 
211
218
  The CLI is the system; a skill is one page telling an agent to use it. Nothing in
212
- the skills is required to use backdraft by hand.
219
+ the skills is required to use backdraft by hand. How the skills reach your agent
220
+ depends on the harness:
221
+
222
+ **Claude Code.** The repo is its own plugin marketplace, so the plugin route
223
+ tracks releases:
224
+
225
+ ```text
226
+ /plugin marketplace add spencerbraun/backdraft
227
+ /plugin install backdraft@backdraft
228
+ ```
229
+
230
+ Or have the CLI copy the skills into your skills directory:
231
+
232
+ ```bash
233
+ backdraft skill install # the writing skill, into ~/.claude/skills/
234
+ backdraft skill install --all # plus backfill and artifact-reading
235
+ backdraft skill install --project # into this repo's .claude/skills/
236
+ ```
237
+
238
+ **Claude Cowork.** Once the plugin is listed in Anthropic's community
239
+ directory, it installs from Cowork's built-in skills directory. Until then,
240
+ zip a skill folder from `skills/` and upload it under Customize > Skills.
241
+ Inside a Cowork session the CLI itself runs per-command as
242
+ `uvx backdraft ...`, since installs do not persist between sessions.
243
+
244
+ **Codex, Cursor, Copilot.** Agents in this family read skills from
245
+ `~/.agents/skills`:
246
+
247
+ ```bash
248
+ backdraft skill install --agent codex # into ~/.agents/skills/
249
+ ```
250
+
251
+ or commit the skill folders to your repo under `.agents/skills/` so every
252
+ checkout carries them.
253
+
254
+ **Standing context, any harness.** Paste this into your repo's AGENTS.md:
255
+
256
+ ```markdown
257
+ ## backdraft (cited writing)
258
+ When a document must cite its sources, write it through the backdraft CLI:
259
+ it shows source text with a citation token over every span, and only shown
260
+ spans are citable. In a sandboxed session run every command as
261
+ `uvx backdraft ...` (no install, no PATH edits). Start from
262
+ `uvx backdraft --help`; ground truth is https://backdraft.dev/llms.txt.
263
+ ```
213
264
 
214
265
  ## Documentation
215
266
 
@@ -228,8 +279,7 @@ implementation reads them and nothing else.
228
279
  ## Status
229
280
 
230
281
  v1. The registry format, the token grammar and the artifact format are pinned
231
- by prose specs and golden-file tests. What is deliberately queued, exhibits,
232
- Excel region maps, more formats, `bd:calc` derivations, is written down in
233
- [ROADMAP.md](ROADMAP.md).
282
+ by prose specs and golden-file tests. What is queued, and what is deliberately
283
+ parked with the objection written down, lives in [ROADMAP.md](ROADMAP.md).
234
284
 
235
285
  MIT licensed.
@@ -0,0 +1,63 @@
1
+ # Roadmap
2
+
3
+ Sequenced intent. DESIGN.md records what was *decided*; this file records what
4
+ is *queued*, so tabling something is an act of writing it down here rather than
5
+ losing it. Items graduate by getting built and deleted.
6
+
7
+ ## Now
8
+
9
+ Empty, deliberately. The 0.4.0 wave graduated the whole section: DOCX, PPTX,
10
+ and XLS extractors, the shared sheet-extraction module, the renderer
11
+ decomposition with pinned Python/JS format parity, minified artifact
12
+ emission, and the snapshot budget knobs. The next item gets promoted out of
13
+ Parked or Later on purpose, not by momentum.
14
+
15
+ ## Parked
16
+
17
+ Deliberately not queued, each with the reason, so picking one up starts from
18
+ the objection rather than rediscovering it.
19
+
20
+ - **Exhibits view** — the evidence-first inversion. Risk: it grows the
21
+ artifact and pollutes a design whose strength is restraint. Needs a
22
+ deliberate design pass and a real audience before any code; not a
23
+ weekend-feature shape.
24
+ - **Excel region maps and range reads** — the research notes exist, but no
25
+ confidence the deterministic detection cascade lands well on the first
26
+ try. Revisit when large-sheet navigation hurts in a real corpus, with
27
+ test workbooks in hand.
28
+ - **`bd:calc` derivations** — reserved in the grammar and parsed as
29
+ malformed-on-purpose, which is enough for now. Full support complicates
30
+ binding and verification for one field-trial case.
31
+ - **Merged-cell and frozen-pane rendering** — ingest captures `merged` and
32
+ `frozen` in sheet meta and keeps them (cheap, and capture is the part you
33
+ cannot backfill). Frozen-pane rendering is cut; merged-cell rendering
34
+ waits for a real sheet that renders wrong without it.
35
+
36
+ ## Later
37
+
38
+ - **Living documents** — re-ingest, re-bind, and a drift-first report: "these
39
+ claims cite figures that changed; cited-then vs. now." The primitives
40
+ (generations, `drifted`, the word-diff) all exist; the missing piece is a
41
+ diff-shaped report and a demo.
42
+ - **Substrates beyond the CLI** — SDK middleware for pipelines (the exit-code
43
+ and `--to json` contracts are already shaped for it), and a client-side
44
+ drag-drop viewer page (no upload — the file never leaves the reader's
45
+ machine).
46
+ - **HTML pages and email (.eml)** — both arrive in diligence folders, both
47
+ have messy identity questions (what is "the source" of a web page?) that
48
+ deserve a decision row before code.
49
+ - **Distribution** — published on PyPI; the repo now ships
50
+ `.claude-plugin/plugin.json` and `marketplace.json`, so it installs as a
51
+ Claude Code plugin and self-hosts its marketplace, and the AGENTS.md
52
+ snippet is published in the README and site docs. Still queued: the
53
+ community-directory listing (submission form is a maintainer action,
54
+ pending), which is also what surfaces the skills in Cowork's directory.
55
+
56
+ ## Someday
57
+
58
+ - **Hosted team features** — upload, access control, shared registries.
59
+ Deliberately parked: the product's trust story is the self-contained file,
60
+ and a hosted viewer inverts it. Revisit only as a distinct teams product.
61
+ - **Entail at scale** — the model-judge verifier is wired (`[entail]` extra)
62
+ but has never been field-calibrated; needs a corpus and a rubric before it
63
+ is recommended anywhere.
@@ -44,10 +44,16 @@ backdraft/
44
44
  extract/
45
45
  base.py # Extractor protocol + registry of extractors
46
46
  pdf_text.py # pdfplumber text-layer; paragraph breaks rebuilt from line geometry
47
- xlsx.py # openpyxl → markdown table with [B10] refs
47
+ sheet.py # the shared tabular representation: bounds caps, [B10] row rendering
48
+ xlsx.py # openpyxl → sheet representation + styling meta
49
+ xls.py # [xls] extra: calamine → sheet representation, values only
50
+ csv.py # sniffed-dialect csv/tsv → sheet representation
51
+ docx.py # python-docx; heading sections become pages
52
+ pptx.py # python-pptx; one page per slide, text floor
48
53
  text.py # md/txt passthrough
49
- vlm.py # [vlm] extra: pdf→images→VLM per page
50
- vlm_settings.py # provider/model/base-url resolution; stdlib-only, imports without the extra
54
+ image.py # an image is a one-page document; VLM transcription
55
+ vlm.py # pdf→images→VLM per page (deps ship by default; key gates use)
56
+ vlm_settings.py # provider/model/base-url resolution; stdlib-only
51
57
  gate/
52
58
  reader.py # ls / toc / read page-range → token-marked context; records ledger
53
59
  searcher.py # FTS5 → minted snippet results; records ledger
@@ -59,7 +65,9 @@ backdraft/
59
65
  base.py # Verifier protocol
60
66
  value_trace.py overlap.py recompute.py entail.py # entail behind [entail]
61
67
  render/
62
- html.py # single-file artifact, embedded viewer + legend
68
+ html/ # single-file artifact, embedded viewer + legend
69
+ # fmt.py text.py components.py assets.py page.py
70
+ # fmt_vectors.py: one vector table drives fmt_cell (py) and fmtCell (js)
63
71
  markdown.py # the authored document's markdown → HTML, stdlib only
64
72
  placement.py # locating each claim's span in the document render was handed
65
73
  footnotes.py # markdown projection
@@ -126,7 +134,7 @@ CREATE TABLE documents (
126
134
  sha256 TEXT NOT NULL UNIQUE,
127
135
  path TEXT NOT NULL, -- as given at ingest; informational
128
136
  filename TEXT NOT NULL,
129
- media_type TEXT NOT NULL, -- 'pdf' | 'xlsx' | 'text'
137
+ media_type TEXT NOT NULL, -- 'pdf' | 'xlsx' | 'xls' | 'csv' | 'docx' | 'pptx' | 'image' | 'text'
130
138
  created_at TEXT NOT NULL -- ISO-8601 UTC, everywhere
131
139
  );
132
140
 
@@ -218,7 +226,7 @@ class Extractor(Protocol):
218
226
  def extract(self, path: Path, config: dict) -> Iterator[ExtractedPage]: ...
219
227
  ```
220
228
 
221
- Registered in a plain dict; `ingest --extractor auto` picks the first `can_handle`, built-ins ordered `xlsx, pdf-text, text`. `vlm` (extra) is pdf→images→VLM per page, OpenAI-compatible client, model configurable. **For PDFs, `auto` prefers `vlm` when it is installed and an API key is configured** (glossy layouts and info boxes extract badly from the text layer, and the snapshot is the receipt); otherwise it falls back to `pdf-text` and prints a one-line nudge naming the VLM option. An explicit `--extractor` always wins. xlsx's representation: `Row | A | B...` header, `[B10] value` prefixes, dimension caps, inflated-sheet placeholder pages; cell values are never rounded, since the snapshot is the receipt a claim is traced against. `pdf-text` reconstructs paragraph breaks from line geometry before handing the page over, so the chunker's blank-line rule has something to fire on.
229
+ Registered in a plain dict; `ingest --extractor auto` picks the first `can_handle`, built-ins ordered `xlsx, xls, csv, docx, pptx, pdf-text, image, text` (an extractor whose optional extra is missing is skipped, not fatal). `vlm` is pdf→images→VLM per page, OpenAI-compatible client, model configurable; its deps ship by default and the backdraft-scoped key gates use. **For PDFs, `auto` prefers `vlm` when it is ready (importable, key configured)** (glossy layouts and info boxes extract badly from the text layer, and the snapshot is the receipt); otherwise it falls back to `pdf-text` and prints a one-line nudge naming the VLM option. An explicit `--extractor` always wins. The sheet representation (shared by xlsx, xls, csv): `Row | A | B...` header, `[B10] value` prefixes, dimension caps, inflated-sheet placeholder pages; cell values are never rounded, since the snapshot is the receipt a claim is traced against. `docx` synthesizes pages from heading sections (the document's smallest present outline level among Heading 1–2 splits; no headings → one page), so locators stay `pN.cM`. `pptx` is one page per slide, text floor only; ingest notes that slide visuals need the PDF-export path. `pdf-text` reconstructs paragraph breaks from line geometry before handing the page over, so the chunker's blank-line rule has something to fire on.
222
230
 
223
231
  ## Gate (gate/)
224
232