backdraft 0.3.0__tar.gz → 0.4.0__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.
- {backdraft-0.3.0 → backdraft-0.4.0}/DESIGN.md +8 -5
- {backdraft-0.3.0 → backdraft-0.4.0}/PKG-INFO +34 -18
- {backdraft-0.3.0 → backdraft-0.4.0}/README.md +27 -15
- backdraft-0.4.0/ROADMAP.md +59 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/SPEC.md +14 -6
- {backdraft-0.3.0 → backdraft-0.4.0}/demo/memo.backdraft.html +352 -377
- {backdraft-0.3.0 → backdraft-0.4.0}/demo/walkthrough.md +3 -3
- {backdraft-0.3.0 → backdraft-0.4.0}/pyproject.toml +19 -8
- {backdraft-0.3.0 → backdraft-0.4.0}/site/demo.html +352 -377
- {backdraft-0.3.0 → backdraft-0.4.0}/site/docs.html +17 -13
- {backdraft-0.3.0 → backdraft-0.4.0}/site/index.html +2 -2
- {backdraft-0.3.0 → backdraft-0.4.0}/site/llms.txt +7 -5
- {backdraft-0.3.0 → backdraft-0.4.0}/skills/backdraft/SKILL.md +1 -2
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/bind/evidence.py +2 -2
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/cli.py +39 -21
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/credentials.py +2 -1
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/extract/__init__.py +1 -1
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/extract/base.py +11 -8
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/extract/csv.py +5 -5
- backdraft-0.4.0/src/backdraft/extract/docx.py +159 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/extract/image.py +8 -7
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/extract/pdf_text.py +3 -3
- backdraft-0.4.0/src/backdraft/extract/pptx.py +121 -0
- backdraft-0.4.0/src/backdraft/extract/sheet.py +147 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/extract/vlm.py +10 -7
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/extract/vlm_settings.py +34 -4
- backdraft-0.4.0/src/backdraft/extract/xls.py +79 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/extract/xlsx.py +11 -116
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/kernel/model.py +8 -1
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/registry/store.py +3 -0
- backdraft-0.4.0/src/backdraft/render/html/__init__.py +90 -0
- backdraft-0.4.0/src/backdraft/render/html/assets.py +633 -0
- backdraft-0.4.0/src/backdraft/render/html/components.py +301 -0
- backdraft-0.4.0/src/backdraft/render/html/fmt.py +80 -0
- backdraft-0.4.0/src/backdraft/render/html/fmt_vectors.py +65 -0
- backdraft-0.4.0/src/backdraft/render/html/page.py +206 -0
- backdraft-0.4.0/src/backdraft/render/html/text.py +244 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_bind_evidence.py +7 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_cli.py +24 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_extract.py +313 -2
- backdraft-0.4.0/tests/test_fmt_parity.py +60 -0
- backdraft-0.4.0/tests/test_snapshot_settings.py +113 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/uv.lock +173 -7
- backdraft-0.3.0/ROADMAP.md +0 -78
- backdraft-0.3.0/src/backdraft/render/html.py +0 -1329
- {backdraft-0.3.0 → backdraft-0.4.0}/.github/workflows/ci.yml +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/.github/workflows/release.yml +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/.github/workflows/site.yml +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/.gitignore +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/.python-version +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/LICENSE +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/demo/generate_sources.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/demo/memo.md +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/demo/sources/t12-summary.pdf +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/skills/backdraft-artifact/SKILL.md +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/skills/backdraft-backfill/SKILL.md +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/spec/artifact.md +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/spec/chunking.md +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/spec/tokens.md +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/__init__.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/bind/binder.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/bind/cli.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/bind/verify/entail.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/cli_context.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/gate/__init__.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/gate/cli.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/gate/reader.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/gate/searcher.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/kernel/artifact.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/kernel/chunking.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/registry/__init__.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/registry/ledger.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/registry/schema.sql +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/render/__init__.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/render/_text.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/render/cli.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/render/footnotes.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/render/markdown.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/src/backdraft/render/sidecar.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/conftest.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/conftest_registry.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/conftest_render.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/fakes.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/gate/fake_registry.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/gate/test_gate_cli.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/gate/test_gate_ledger.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/gate/test_gate_reader.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/gate/test_gate_searcher.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/render/backfill.sidecar.json +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden/render/demo.sidecar.json +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/golden_util.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_bind_cli.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_chunking.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_claims.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_credentials.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_footnotes.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_gate_integration.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_hashing.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_html.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_invariants.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_model.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_placement.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_records_location.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_registry.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_reingest.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_render_cli.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_render_markdown.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_tokens.py +0 -0
- {backdraft-0.3.0 → backdraft-0.4.0}/tests/test_value_trace.py +0 -0
|
@@ -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
|
|
138
|
-
|
|
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
|
-
-
|
|
142
|
-
|
|
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
|
+
Version: 0.4.0
|
|
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
|
-
|
|
19
|
-
Requires-Dist:
|
|
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
|
|
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,
|
|
53
|
-
(png, jpeg, tiff, through the vision model), plain text and
|
|
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:
|
|
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
|
|
193
|
-
uv add backdraft
|
|
194
|
-
pip install backdraft
|
|
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+.
|
|
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
|
-
|
|
205
|
-
|
|
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
|
|
|
@@ -249,8 +266,7 @@ implementation reads them and nothing else.
|
|
|
249
266
|
## Status
|
|
250
267
|
|
|
251
268
|
v1. The registry format, the token grammar and the artifact format are pinned
|
|
252
|
-
by prose specs and golden-file tests. What is
|
|
253
|
-
|
|
254
|
-
[ROADMAP.md](ROADMAP.md).
|
|
269
|
+
by prose specs and golden-file tests. What is queued, and what is deliberately
|
|
270
|
+
parked with the objection written down, lives in [ROADMAP.md](ROADMAP.md).
|
|
255
271
|
|
|
256
272
|
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
|
|
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,
|
|
32
|
-
(png, jpeg, tiff, through the vision model), plain text and
|
|
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:
|
|
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
|
|
172
|
-
uv add backdraft
|
|
173
|
-
pip install backdraft
|
|
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+.
|
|
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
|
-
|
|
184
|
-
|
|
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
|
|
|
@@ -228,8 +241,7 @@ implementation reads them and nothing else.
|
|
|
228
241
|
## Status
|
|
229
242
|
|
|
230
243
|
v1. The registry format, the token grammar and the artifact format are pinned
|
|
231
|
-
by prose specs and golden-file tests. What is
|
|
232
|
-
|
|
233
|
-
[ROADMAP.md](ROADMAP.md).
|
|
244
|
+
by prose specs and golden-file tests. What is queued, and what is deliberately
|
|
245
|
+
parked with the objection written down, lives in [ROADMAP.md](ROADMAP.md).
|
|
234
246
|
|
|
235
247
|
MIT licensed.
|
|
@@ -0,0 +1,59 @@
|
|
|
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; still queued: a skills-registry
|
|
50
|
+
listing when one becomes the standard channel.
|
|
51
|
+
|
|
52
|
+
## Someday
|
|
53
|
+
|
|
54
|
+
- **Hosted team features** — upload, access control, shared registries.
|
|
55
|
+
Deliberately parked: the product's trust story is the self-contained file,
|
|
56
|
+
and a hosted viewer inverts it. Revisit only as a distinct teams product.
|
|
57
|
+
- **Entail at scale** — the model-judge verifier is wired (`[entail]` extra)
|
|
58
|
+
but has never been field-calibrated; needs a corpus and a rubric before it
|
|
59
|
+
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
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
|
|
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
|
|