backdraft 0.2.1__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.
Files changed (150) hide show
  1. backdraft-0.4.0/.github/workflows/ci.yml +14 -0
  2. backdraft-0.4.0/.github/workflows/release.yml +23 -0
  3. backdraft-0.4.0/.github/workflows/site.yml +26 -0
  4. {backdraft-0.2.1 → backdraft-0.4.0}/.gitignore +1 -0
  5. {backdraft-0.2.1 → backdraft-0.4.0}/DESIGN.md +24 -21
  6. {backdraft-0.2.1 → backdraft-0.4.0}/PKG-INFO +42 -21
  7. {backdraft-0.2.1 → backdraft-0.4.0}/README.md +31 -18
  8. backdraft-0.4.0/ROADMAP.md +59 -0
  9. {backdraft-0.2.1 → backdraft-0.4.0}/SPEC.md +15 -7
  10. {backdraft-0.2.1 → backdraft-0.4.0}/demo/generate_sources.py +31 -2
  11. {backdraft-0.2.1 → backdraft-0.4.0}/demo/memo.backdraft.html +681 -327
  12. {backdraft-0.2.1 → backdraft-0.4.0}/demo/sources/t12-summary.pdf +2 -2
  13. backdraft-0.4.0/demo/sources/underwriting-model.xlsx +0 -0
  14. {backdraft-0.2.1 → backdraft-0.4.0}/demo/walkthrough.md +3 -3
  15. backdraft-0.4.0/pyproject.toml +61 -0
  16. {backdraft-0.2.1 → backdraft-0.4.0}/site/demo.html +681 -327
  17. {backdraft-0.2.1 → backdraft-0.4.0}/site/docs.html +32 -15
  18. {backdraft-0.2.1 → backdraft-0.4.0}/site/index.html +39 -14
  19. {backdraft-0.2.1 → backdraft-0.4.0}/site/llms.txt +8 -3
  20. {backdraft-0.2.1 → backdraft-0.4.0}/skills/backdraft/SKILL.md +1 -2
  21. {backdraft-0.2.1 → backdraft-0.4.0}/spec/artifact.md +12 -4
  22. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/evidence.py +45 -8
  23. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/cli.py +39 -21
  24. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/credentials.py +2 -1
  25. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/__init__.py +1 -1
  26. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/base.py +26 -7
  27. backdraft-0.4.0/src/backdraft/extract/csv.py +97 -0
  28. backdraft-0.4.0/src/backdraft/extract/docx.py +159 -0
  29. backdraft-0.4.0/src/backdraft/extract/image.py +89 -0
  30. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/pdf_text.py +3 -3
  31. backdraft-0.4.0/src/backdraft/extract/pptx.py +121 -0
  32. backdraft-0.4.0/src/backdraft/extract/sheet.py +147 -0
  33. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/vlm.py +13 -11
  34. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/vlm_settings.py +43 -13
  35. backdraft-0.4.0/src/backdraft/extract/xls.py +79 -0
  36. backdraft-0.4.0/src/backdraft/extract/xlsx.py +283 -0
  37. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/gate/searcher.py +3 -3
  38. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/artifact.py +7 -2
  39. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/model.py +15 -2
  40. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/registry/ledger.py +1 -1
  41. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/registry/schema.sql +8 -1
  42. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/registry/store.py +40 -5
  43. backdraft-0.4.0/src/backdraft/render/html/__init__.py +90 -0
  44. backdraft-0.4.0/src/backdraft/render/html/assets.py +633 -0
  45. backdraft-0.4.0/src/backdraft/render/html/components.py +301 -0
  46. backdraft-0.4.0/src/backdraft/render/html/fmt.py +80 -0
  47. backdraft-0.4.0/src/backdraft/render/html/fmt_vectors.py +65 -0
  48. backdraft-0.4.0/src/backdraft/render/html/page.py +206 -0
  49. backdraft-0.4.0/src/backdraft/render/html/text.py +244 -0
  50. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/render/backfill.sidecar.json +1 -1
  51. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/render/demo.sidecar.json +1 -1
  52. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_bind_evidence.py +42 -0
  53. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_cli.py +41 -0
  54. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_extract.py +476 -6
  55. backdraft-0.4.0/tests/test_fmt_parity.py +60 -0
  56. backdraft-0.4.0/tests/test_snapshot_settings.py +113 -0
  57. {backdraft-0.2.1 → backdraft-0.4.0}/uv.lock +173 -7
  58. backdraft-0.2.1/ROADMAP.md +0 -79
  59. backdraft-0.2.1/demo/sources/underwriting-model.xlsx +0 -0
  60. backdraft-0.2.1/pyproject.toml +0 -44
  61. backdraft-0.2.1/research/20260727-excel-region-detection.md +0 -53
  62. backdraft-0.2.1/research/20260727-research-findings.md +0 -182
  63. backdraft-0.2.1/src/backdraft/extract/xlsx.py +0 -244
  64. backdraft-0.2.1/src/backdraft/render/html.py +0 -1211
  65. {backdraft-0.2.1 → backdraft-0.4.0}/.python-version +0 -0
  66. {backdraft-0.2.1 → backdraft-0.4.0}/LICENSE +0 -0
  67. {backdraft-0.2.1 → backdraft-0.4.0}/assets/backdraft-mark.svg +0 -0
  68. {backdraft-0.2.1 → backdraft-0.4.0}/demo/memo.md +0 -0
  69. {backdraft-0.2.1 → backdraft-0.4.0}/skills/backdraft-artifact/SKILL.md +0 -0
  70. {backdraft-0.2.1 → backdraft-0.4.0}/skills/backdraft-backfill/SKILL.md +0 -0
  71. {backdraft-0.2.1 → backdraft-0.4.0}/spec/chunking.md +0 -0
  72. {backdraft-0.2.1 → backdraft-0.4.0}/spec/tokens.md +0 -0
  73. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/__init__.py +0 -0
  74. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/__init__.py +0 -0
  75. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/binder.py +0 -0
  76. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/cli.py +0 -0
  77. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/__init__.py +0 -0
  78. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/base.py +0 -0
  79. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/entail.py +0 -0
  80. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/overlap.py +0 -0
  81. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/recompute.py +0 -0
  82. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/value_trace.py +0 -0
  83. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/cli_context.py +0 -0
  84. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/text.py +0 -0
  85. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/gate/__init__.py +0 -0
  86. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/gate/cli.py +0 -0
  87. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/gate/reader.py +0 -0
  88. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/__init__.py +0 -0
  89. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/chunking.py +0 -0
  90. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/claims.py +0 -0
  91. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/errors.py +0 -0
  92. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/hashing.py +0 -0
  93. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/tokens.py +0 -0
  94. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/registry/__init__.py +0 -0
  95. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/__init__.py +0 -0
  96. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/_text.py +0 -0
  97. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/cli.py +0 -0
  98. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/footnotes.py +0 -0
  99. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/markdown.py +0 -0
  100. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/placement.py +0 -0
  101. {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/sidecar.py +0 -0
  102. {backdraft-0.2.1 → backdraft-0.4.0}/tests/conftest.py +0 -0
  103. {backdraft-0.2.1 → backdraft-0.4.0}/tests/conftest_registry.py +0 -0
  104. {backdraft-0.2.1 → backdraft-0.4.0}/tests/conftest_render.py +0 -0
  105. {backdraft-0.2.1 → backdraft-0.4.0}/tests/fakes.py +0 -0
  106. {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/conftest.py +0 -0
  107. {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/fake_registry.py +0 -0
  108. {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/test_gate_cli.py +0 -0
  109. {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/test_gate_ledger.py +0 -0
  110. {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/test_gate_reader.py +0 -0
  111. {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/test_gate_searcher.py +0 -0
  112. {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/test_reader_cells.py +0 -0
  113. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/bind/backfill.bound.md +0 -0
  114. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/bind/frontwalk.bound.md +0 -0
  115. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/long_tail.json +0 -0
  116. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/long_tail.txt +0 -0
  117. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/many_small.json +0 -0
  118. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/many_small.txt +0 -0
  119. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/mixed.json +0 -0
  120. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/mixed.txt +0 -0
  121. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/one_giant.json +0 -0
  122. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/one_giant.txt +0 -0
  123. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/short_page.json +0 -0
  124. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/short_page.txt +0 -0
  125. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/unsplittable.json +0 -0
  126. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/unsplittable.txt +0 -0
  127. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/render/backfill.footnotes.md +0 -0
  128. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/render/demo.footnotes.md +0 -0
  129. {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden_util.py +0 -0
  130. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_bind_binder.py +0 -0
  131. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_bind_cli.py +0 -0
  132. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_bind_verify.py +0 -0
  133. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_chunking.py +0 -0
  134. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_claims.py +0 -0
  135. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_credentials.py +0 -0
  136. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_footnotes.py +0 -0
  137. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_gate_integration.py +0 -0
  138. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_hashing.py +0 -0
  139. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_html.py +0 -0
  140. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_invariants.py +0 -0
  141. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_model.py +0 -0
  142. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_placement.py +0 -0
  143. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_records_location.py +0 -0
  144. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_registry.py +0 -0
  145. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_reingest.py +0 -0
  146. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_render_cli.py +0 -0
  147. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_render_markdown.py +0 -0
  148. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_sidecar.py +0 -0
  149. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_tokens.py +0 -0
  150. {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_value_trace.py +0 -0
@@ -0,0 +1,14 @@
1
+ name: ci
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ jobs:
7
+ test:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: astral-sh/setup-uv@v5
12
+ - run: uv sync --all-extras
13
+ - run: uv run pytest
14
+ - run: uv build
@@ -0,0 +1,23 @@
1
+ name: release
2
+ on:
3
+ release:
4
+ types: [published]
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - uses: astral-sh/setup-uv@v5
11
+ - run: uv sync --all-extras
12
+ - run: uv run pytest
13
+ publish:
14
+ needs: test
15
+ runs-on: ubuntu-latest
16
+ environment: pypi
17
+ permissions:
18
+ id-token: write # trusted publishing: OIDC, no stored token
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: astral-sh/setup-uv@v5
22
+ - run: uv build
23
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,26 @@
1
+ name: site
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ paths: ["site/**"]
6
+ workflow_dispatch:
7
+ permissions:
8
+ contents: read
9
+ pages: write
10
+ id-token: write
11
+ concurrency:
12
+ group: pages
13
+ cancel-in-progress: true
14
+ jobs:
15
+ deploy:
16
+ runs-on: ubuntu-latest
17
+ environment:
18
+ name: github-pages
19
+ url: ${{ steps.deployment.outputs.page_url }}
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/upload-pages-artifact@v3
23
+ with:
24
+ path: site/
25
+ - id: deployment
26
+ uses: actions/deploy-pages@v4
@@ -22,3 +22,4 @@ uv.lock.bak
22
22
 
23
23
  # Claude Code session state and agent worktrees
24
24
  .claude/
25
+ .private/
@@ -1,12 +1,12 @@
1
1
  # Backdraft — Design
2
2
 
3
- Status: v0 design, 2026-07-27. Decisions below are made unless marked open. Research backing this is in [research/20260727-research-findings.md](research/20260727-research-findings.md).
3
+ Status: v0 design, 2026-07-27. Decisions below are made unless marked open.
4
4
 
5
5
  ## The point
6
6
 
7
- Drop-in provenance for factual claims: one click from any claim to the evidence behind it. Extracted from what was built inside the HighRoad app, redesigned from scratch, released standalone. The working problem statement is **verification cost** — an analyst who can't show where a number came from either re-reads the document or doesn't use the output, and both outcomes destroy the value of extraction.
7
+ Drop-in provenance for factual claims: one click from any claim to the evidence behind it. The working problem statement is **verification cost** — an analyst who can't show where a number came from either re-reads the document or doesn't use the output, and both outcomes destroy the value of extraction.
8
8
 
9
- Not a port. The app proved the product (citations worked great from the frontend) but its mechanism was path-dependent: random UUIDs, four citation grammars, four resolver copies, silent failure everywhere, anchors that die on reprocessing. We port the ideas, not the code.
9
+ Designed from first principles against the failure modes that kill citation systems in practice: random anchor ids, competing citation grammars, duplicated resolvers, silent failure, anchors that die on reprocessing.
10
10
 
11
11
  ## Principles
12
12
 
@@ -14,7 +14,7 @@ Not a port. The app proved the product (citations worked great from the frontend
14
14
  2. **Content-addressed everything.** Anchors derive their identity from document content + location, not from a run. Reprocessing the same bytes yields the same anchors; citations survive.
15
15
  3. **The receipt travels with the claim.** An anchor is not a pointer — it is a pointer plus captured evidence: verbatim snippet, content hash, location. Defensible without reopening the source.
16
16
  4. **Failures are data.** Unresolvable tokens, drifted sources, unmatched claims are first-class records in reports and artifacts — never warn-and-drop.
17
- 5. **Coordinates live in-band.** The app's only working sub-page attribution was Excel, because `[B10]` markers sat in the text the model read. Human-readable location in the token; side-channel geometry rots (bbox was plumbed through six layers and read by nothing).
17
+ 5. **Coordinates live in-band.** Sub-page attribution works when markers like `[B10]` sit in the text the model reads. Human-readable location in the token; side-channel geometry rots (a bbox plumbed through six layers ends up read by nothing).
18
18
  6. **The format is the product.** Substrates (skill, CLI, SDK, viewer) are thin shells over one spec + one library. Artifacts are self-describing and outlive any substrate (subtext's `$format` + embedded legend pattern).
19
19
 
20
20
  ## Architecture: three layers
@@ -27,13 +27,13 @@ Not a port. The app proved the product (citations worked great from the frontend
27
27
  ### 2. The library / CLI (the system)
28
28
  Four verbs:
29
29
  - **`ingest`** — deterministically anchor source documents (PDF/xlsx/md/…) into the registry: extract, locate, snapshot snippets, hash.
30
- - **`read` / `search`** — the gate. Emit token-marked context for a model. Search results are themselves mintable evidence (fixing pipeline-v2's gap where search returned nothing citable and forced a page read).
30
+ - **`read` / `search`** — the gate. Emit token-marked context for a model. Search results are themselves mintable evidence (search that returns nothing citable forces a page read purely to obtain an anchor).
31
31
  - **`bind`** — postprocess a written document: resolve tokens, run enabled verification switches, backfill surface forms (doc names, pages, quotes), emit sidecar claims file + binding report.
32
32
  - **`render`** — produce the clickable artifact.
33
33
 
34
34
  ### 3. Substrates (sequenced)
35
35
  1. **Skill + CLI, together** (first release). The CLI is the system; the skill is ~a page instructing an agent to use it. Ships both the agent-native wedge and the zero-integration backfill demo.
36
- 2. **SDK middleware** — wrap a model call: minted context in, bound citations out. The app-developer drop-in (LoanCore validation-layer shape).
36
+ 2. **SDK middleware** — wrap a model call: minted context in, bound citations out. The drop-in for application developers.
37
37
  3. **Viewer** — the artifact carries its own single-file viewer; a component version for embedding comes later.
38
38
 
39
39
  ## The registry
@@ -43,7 +43,7 @@ Four verbs:
43
43
  Tables (shape, not final DDL):
44
44
  - `documents` — path, display name, **slug**, content sha256, media type, ingest metadata.
45
45
  - `anchors` — token, document, locator, verbatim snippet, snippet sha256, created-by (ingest run).
46
- - `ledger` — session id, token, when shown. Records every token minted into context. Bind distinguishes "cited what you saw" from "cited a valid token you were never shown" — a caught hallucination class the app couldn't express.
46
+ - `ledger` — session id, token, when shown. Records every token minted into context. Bind distinguishes "cited what you saw" from "cited a valid token you were never shown" — a caught hallucination class most systems cannot express.
47
47
  - `claims` (written at bind) — claim span text, source doc, citation tokens, verification records.
48
48
 
49
49
  FTS5 backs `search`. No server, no tenancy — single-analyst/single-project scope; multi-user is a substrate concern, not a registry concern.
@@ -54,7 +54,7 @@ FTS5 backs `search`. No server, no tenancy — single-analyst/single-project sco
54
54
 
55
55
  - `slug` — short kebab doc identifier assigned at ingest, unique per registry (`t12-audit`, `rent-roll-2025`).
56
56
  - `locator` — media-native:
57
- - PDF/text: `p8` (page), `p8.c3` (chunk ordinal within page; chunking is deterministic with an explicit within-page ordinal — the column the app never had)
57
+ - PDF/text: `p8` (page), `p8.c3` (chunk ordinal within page; chunking is deterministic with an explicit within-page ordinal)
58
58
  - Excel: `Rent!B10`, `Rent!B10:C12` (sheet + cell/range, spreadsheet-native)
59
59
  - Prose files: heading-path or line-range forms (open detail)
60
60
  - `hash` — first 4+ hex of sha256 of the normalized snippet. Catches transcription typos, detects drift after re-ingest, and makes the token content-addressed. Registry extends length on collision.
@@ -65,11 +65,11 @@ Reserved extension — **declared derivations**: a claim whose value appears in
65
65
 
66
66
  ## The gate (context construction)
67
67
 
68
- Ported ideas from pipeline-v2's read tool, rebuilt local:
68
+ The read tool's shape:
69
69
  - Unified dispatch: list docs → per-doc TOC of page/sheet summaries → page/range read → batch read. Short slugs everywhere.
70
70
  - Progressive disclosure with continuation, plus search hints.
71
- - **Excel**: sheet-per-page markdown table with `Row | A | B | …` headers and `[B10] value` cell prefixes — the representation that made Excel the app's only working sub-page attribution. Hardening kept: dimension caps, inflated-sheet placeholders, trimming.
72
- - Fixes over the app: search mints citable snippet tokens; Excel long-context gets structural navigation (detected regions/ranges) instead of char-offset slicing (open design area).
71
+ - **Excel**: sheet-per-page markdown table with `Row | A | B | …` headers and `[B10] value` cell prefixes — the representation that makes cell-level attribution work. Hardening: dimension caps, inflated-sheet placeholders, trimming.
72
+ - Search mints citable snippet tokens; Excel long-context gets structural navigation (detected regions/ranges) rather than char-offset slicing (open design area).
73
73
 
74
74
  The skill's core instruction is a substitution: for source documents, use `backdraft read`/`search`, not raw Read/Grep.
75
75
 
@@ -97,11 +97,11 @@ Independent checks per citation, recorded as graded evidence, never gating gener
97
97
  | 3 | `entail` | Model judge: does snippet support claim — tiny closed question, batchable, async | qualitative |
98
98
  | 4 | `recompute` | Deterministic re-execution of declared derivations | derived values |
99
99
 
100
- Rationale for default-off: out of the box this is pure provenance (one click to the receipt); verification is opt-in per claim class. Rung-pass rates aggregated per run/model double as an eval substrate (the Clarion use).
100
+ Rationale for default-off: out of the box this is pure provenance (one click to the receipt); verification is opt-in per claim class. Rung-pass rates aggregated per run/model double as an eval substrate.
101
101
 
102
102
  ## Modes
103
103
 
104
- - **Front-walk** — citations minted during generation; bind resolves against the session ledger. The high-quality integrated path (what the app did).
104
+ - **Front-walk** — citations minted during generation; bind resolves against the session ledger. The high-quality integrated path.
105
105
  - **Backfill** — an existing document + ingested sources; for each claim: search → propose anchors → bind via the fuzzier strategies (overlap, value-trace, structured lookup). Unmatched claims land in an explicit unresolved list, not silent non-attribution. Zero integration required — the demo wedge.
106
106
 
107
107
  Same anchors, same bind, same artifact out.
@@ -127,20 +127,23 @@ Self-contained HTML (plus the machine-readable sidecar): **quotes + receipts + e
127
127
  | 2026-07-27 | CLI unified on the gate's patterns: shared `cli_context` (constants, discovery, sessions, one error guard mapping `BackdraftError` → exit 1), `Annotated` params, relative imports. Test fakes renamed so the real and fake registries cannot be confused. |
128
128
  | 2026-07-27 | File naming is part of the artifact format, owned by `kernel/artifact.py`: `<stem>.bound.md`, `<stem>.backdraft.json`, `<stem>.backdraft.html`, `<stem>.footnotes.md`. |
129
129
  | 2026-07-27 | `bind --check entail` without the extra records `skip` verdicts carrying the reason — verifiers never gate, including at the CLI layer. |
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 Darnell trial: presence of a generic key in the environment is not consent to spend it or send documents to its provider. |
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
- | 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 ported from embed-ses `generate-preview`: 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. |
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. *(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 Darnell 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
- - First external user (LoanCore validation layer vs Clarion eval substrate vs
148
+ - First external user (design-partner validation layer vs eval substrate vs
146
149
  open release feedback)
@@ -1,18 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backdraft
3
- Version: 0.2.1
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
+ Project-URL: Homepage, https://backdraft.dev
6
+ Project-URL: Documentation, https://backdraft.dev/docs.html
7
+ Project-URL: Repository, https://github.com/spencerbraun/backdraft
8
+ Project-URL: Roadmap, https://github.com/spencerbraun/backdraft/blob/main/ROADMAP.md
5
9
  License: MIT
6
10
  License-File: LICENSE
7
11
  Requires-Python: >=3.13
12
+ Requires-Dist: openai>=1.0
8
13
  Requires-Dist: openpyxl>=3.1
14
+ Requires-Dist: pdf2image>=1.17
9
15
  Requires-Dist: pdfplumber>=0.11
16
+ Requires-Dist: python-docx>=1.1
17
+ Requires-Dist: python-pptx>=1.0
10
18
  Requires-Dist: typer>=0.12
11
19
  Provides-Extra: entail
12
20
  Requires-Dist: anthropic>=0.40; extra == 'entail'
13
21
  Provides-Extra: vlm
14
- Requires-Dist: openai>=1.0; extra == 'vlm'
15
- Requires-Dist: pdf2image>=1.17; extra == 'vlm'
22
+ Provides-Extra: xls
23
+ Requires-Dist: python-calamine>=0.3; extra == 'xls'
16
24
  Description-Content-Type: text/markdown
17
25
 
18
26
  <img src="assets/backdraft-mark.svg" width="56" alt="">
@@ -40,12 +48,14 @@ Docs live at [backdraft.dev](https://backdraft.dev), one page for people, and
40
48
  ## 60 seconds
41
49
 
42
50
  ```bash
43
- uv tool install "backdraft[vlm]"
51
+ uv tool install backdraft
44
52
  cd demo
45
53
  ```
46
54
 
47
55
  **Ingest** the sources. Every anchor is minted here, chunks for PDF pages,
48
- cells for spreadsheet sheets.
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.
49
59
 
50
60
  ```console
51
61
  $ backdraft init
@@ -55,7 +65,7 @@ documents: 0
55
65
  $ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
56
66
  t12-summary t12-summary.pdf pdf 3 pages
57
67
  underwriting-model underwriting-model.xlsx xlsx 2 pages
58
- 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.
59
69
  ```
60
70
 
61
71
  **Read.** The page arrives with a citable name over each chunk. This is the whole
@@ -184,21 +194,33 @@ confidential corpora**.
184
194
  ## Install
185
195
 
186
196
  ```bash
187
- uv tool install "backdraft[vlm]" # CLI, recommended form
188
- uv add backdraft # as a dependency
189
- 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
190
200
  ```
191
201
 
192
- 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:
193
216
 
194
217
  | Extra | Adds | For |
195
218
  |---|---|---|
196
- | `[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 |
197
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 |
198
221
 
199
- ```bash
200
- pip install "backdraft[vlm,entail]"
201
- ```
222
+ `backdraft[vlm]` still installs: it is an empty compat alias from when the
223
+ vision deps were an extra.
202
224
 
203
225
  From a checkout:
204
226
 
@@ -210,12 +232,12 @@ uv run pytest
210
232
 
211
233
  ## Skills
212
234
 
213
- Three agent skills ship in [`skills/`](skills). Copy the ones you want into your
214
- agent's skills directory, for Claude Code, `~/.claude/skills/` (personal) or
215
- `.claude/skills/` (per project):
235
+ Three agent skills ship inside the package, and the CLI installs them:
216
236
 
217
237
  ```bash
218
- cp -r skills/backdraft skills/backdraft-backfill skills/backdraft-artifact ~/.claude/skills/
238
+ backdraft skill install # the writing skill, into ~/.claude/skills/
239
+ backdraft skill install --all # plus backfill and artifact-reading
240
+ backdraft skill install --project # into this repo's .claude/skills/
219
241
  ```
220
242
 
221
243
  | Skill | For |
@@ -244,8 +266,7 @@ implementation reads them and nothing else.
244
266
  ## Status
245
267
 
246
268
  v1. The registry format, the token grammar and the artifact format are pinned
247
- by prose specs and golden-file tests. What is deliberately queued, exhibits,
248
- Excel region maps, more formats, `bd:calc` derivations, is written down in
249
- [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).
250
271
 
251
272
  MIT licensed.
@@ -23,12 +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.
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.
32
34
 
33
35
  ```console
34
36
  $ backdraft init
@@ -38,7 +40,7 @@ documents: 0
38
40
  $ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
39
41
  t12-summary t12-summary.pdf pdf 3 pages
40
42
  underwriting-model underwriting-model.xlsx xlsx 2 pages
41
- 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.
42
44
  ```
43
45
 
44
46
  **Read.** The page arrives with a citable name over each chunk. This is the whole
@@ -167,21 +169,33 @@ confidential corpora**.
167
169
  ## Install
168
170
 
169
171
  ```bash
170
- uv tool install "backdraft[vlm]" # CLI, recommended form
171
- uv add backdraft # as a dependency
172
- 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
173
175
  ```
174
176
 
175
- 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:
176
191
 
177
192
  | Extra | Adds | For |
178
193
  |---|---|---|
179
- | `[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 |
180
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 |
181
196
 
182
- ```bash
183
- pip install "backdraft[vlm,entail]"
184
- ```
197
+ `backdraft[vlm]` still installs: it is an empty compat alias from when the
198
+ vision deps were an extra.
185
199
 
186
200
  From a checkout:
187
201
 
@@ -193,12 +207,12 @@ uv run pytest
193
207
 
194
208
  ## Skills
195
209
 
196
- Three agent skills ship in [`skills/`](skills). Copy the ones you want into your
197
- agent's skills directory, for Claude Code, `~/.claude/skills/` (personal) or
198
- `.claude/skills/` (per project):
210
+ Three agent skills ship inside the package, and the CLI installs them:
199
211
 
200
212
  ```bash
201
- cp -r skills/backdraft skills/backdraft-backfill skills/backdraft-artifact ~/.claude/skills/
213
+ backdraft skill install # the writing skill, into ~/.claude/skills/
214
+ backdraft skill install --all # plus backfill and artifact-reading
215
+ backdraft skill install --project # into this repo's .claude/skills/
202
216
  ```
203
217
 
204
218
  | Skill | For |
@@ -227,8 +241,7 @@ implementation reads them and nothing else.
227
241
  ## Status
228
242
 
229
243
  v1. The registry format, the token grammar and the artifact format are pinned
230
- by prose specs and golden-file tests. What is deliberately queued, exhibits,
231
- Excel region maps, more formats, `bd:calc` derivations, is written down in
232
- [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).
233
246
 
234
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
- xlsx.py # calamine/openpyxl → markdown table with [B10] refs (port the app's representation)
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: zerox-minimal pattern (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
@@ -113,7 +121,7 @@ Deterministic, pure: `chunk(page_text) -> [Chunk(ordinal, text, start, end)]`.
113
121
  1. Split on blank lines: regex `\n\s*\n`.
114
122
  2. Merge forward: any segment < 200 chars merges into the following segment (the last merges backward).
115
123
  3. Split long: any segment > 2400 chars splits at the sentence boundary nearest each 1200-char multiple (sentence = terminal `.!?` + space + uppercase/digit start; no abbreviation table in v0 — splitting slightly wrong is fine, it only shifts chunk boundaries deterministically). A final piece under 200 chars merges backward, since merging ran before splitting and could not see it; a chunk is therefore under 200 chars only when it is the only chunk on its page.
116
- 4. Ordinals `c1..cN` in order. No per-page rebalancing, no chunk-count caps (the app's balancing caused tail-loss bugs; deliberately dropped).
124
+ 4. Ordinals `c1..cN` in order. No per-page rebalancing, no chunk-count caps (rebalancing causes tail-loss bugs; deliberately avoided).
117
125
 
118
126
  Anchor identity for a chunk: (extraction, page number, ordinal). `start`/`end` are char offsets into the page text — stored, enabling future span/region features.
119
127
 
@@ -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) follows the zerox-minimal pattern — 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 ports the app'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
 
@@ -17,7 +17,7 @@ from __future__ import annotations
17
17
  from pathlib import Path
18
18
 
19
19
  from openpyxl import Workbook
20
- from openpyxl.styles import Alignment, Font
20
+ from openpyxl.styles import Alignment, Font, PatternFill
21
21
  from reportlab.lib.enums import TA_JUSTIFY
22
22
  from reportlab.lib.pagesizes import LETTER
23
23
  from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
@@ -306,13 +306,42 @@ def build_xlsx(path: Path) -> None:
306
306
  for row in ASSUMPTIONS:
307
307
  assumptions.append(list(row))
308
308
 
309
+ # Styling, the way a sponsor's model actually looks: a dark header band,
310
+ # inputs in modeling blue, currency and percent number formats, a frozen
311
+ # header row. Values are untouched — the snapshot text and every token
312
+ # are identical with or without this block.
313
+ header_fill = PatternFill("solid", fgColor="FF1F4E79")
309
314
  for sheet in (rent, assumptions):
310
315
  for cell in sheet[1]:
311
- cell.font = Font(bold=True)
316
+ cell.font = Font(bold=True, color="FFFFFFFF")
317
+ cell.fill = header_fill
312
318
  cell.alignment = Alignment(horizontal="left")
313
319
  widths = {"A": 30, "B": 14, "C": 14, "D": 16, "E": 16, "F": 28}
314
320
  for column, width in widths.items():
315
321
  sheet.column_dimensions[column].width = width
322
+ sheet.freeze_panes = "A2"
323
+
324
+ for row in range(2, 6): # unit-mix money and occupancy columns
325
+ for column, fmt in (("C", "#,##0"), ("D", '"$"#,##0'), ("E", '"$"#,##0')):
326
+ rent[f"{column}{row}"].number_format = fmt
327
+ rent[f"F{row}"].number_format = "0.0%"
328
+ for cell in rent[7]:
329
+ cell.font = Font(bold=True)
330
+ rent["F7"].number_format = "0.0%"
331
+ rent["B9"].number_format = '"$"#,##0'
332
+ rent["B10"].number_format = "0.0%"
333
+ rent["B11"].number_format = '"$"#,##0'
334
+
335
+ input_font = Font(color="FF0000CC") # modeling convention: inputs in blue
336
+ percent_rows = (2, 3, 5, 6, 7, 8, 12, 16)
337
+ money_rows = (9, 10, 11, 14)
338
+ for row in range(2, 17):
339
+ cell = assumptions[f"B{row}"]
340
+ cell.font = input_font
341
+ if row in percent_rows:
342
+ cell.number_format = "0.00%"
343
+ elif row in money_rows:
344
+ cell.number_format = '"$"#,##0'
316
345
 
317
346
  book.save(path)
318
347