backdraft 0.4.1__tar.gz → 0.6.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 (168) hide show
  1. {backdraft-0.4.1 → backdraft-0.6.0}/.claude-plugin/plugin.json +1 -1
  2. backdraft-0.6.0/.github/workflows/site.yml +36 -0
  3. {backdraft-0.4.1 → backdraft-0.6.0}/DESIGN.md +6 -0
  4. {backdraft-0.4.1 → backdraft-0.6.0}/PKG-INFO +69 -7
  5. {backdraft-0.4.1 → backdraft-0.6.0}/README.md +68 -6
  6. backdraft-0.6.0/ROADMAP.md +258 -0
  7. {backdraft-0.4.1 → backdraft-0.6.0}/SPEC.md +29 -10
  8. {backdraft-0.4.1 → backdraft-0.6.0}/demo/memo.backdraft.html +38 -19
  9. {backdraft-0.4.1 → backdraft-0.6.0}/demo/walkthrough.md +40 -22
  10. {backdraft-0.4.1 → backdraft-0.6.0}/pyproject.toml +1 -1
  11. {backdraft-0.4.1 → backdraft-0.6.0}/site/demo.html +38 -19
  12. {backdraft-0.4.1 → backdraft-0.6.0}/site/docs.html +114 -14
  13. {backdraft-0.4.1 → backdraft-0.6.0}/site/index.html +7 -3
  14. {backdraft-0.4.1 → backdraft-0.6.0}/site/llms.txt +21 -1
  15. backdraft-0.6.0/site/privacy.html +126 -0
  16. {backdraft-0.4.1 → backdraft-0.6.0}/skills/backdraft/SKILL.md +23 -5
  17. {backdraft-0.4.1 → backdraft-0.6.0}/skills/backdraft-artifact/SKILL.md +7 -0
  18. {backdraft-0.4.1 → backdraft-0.6.0}/skills/backdraft-backfill/SKILL.md +1 -0
  19. {backdraft-0.4.1 → backdraft-0.6.0}/spec/artifact.md +29 -8
  20. backdraft-0.6.0/src/backdraft/__init__.py +38 -0
  21. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/bind/cli.py +25 -3
  22. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/bind/evidence.py +27 -8
  23. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/cli.py +147 -89
  24. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/base.py +2 -1
  25. backdraft-0.6.0/src/backdraft/extract/html.py +331 -0
  26. backdraft-0.6.0/src/backdraft/extract/snapshots.py +170 -0
  27. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/vlm.py +20 -18
  28. backdraft-0.6.0/src/backdraft/fetch.py +188 -0
  29. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/gate/__init__.py +2 -0
  30. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/gate/reader.py +34 -7
  31. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/gate/searcher.py +2 -2
  32. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/kernel/artifact.py +4 -1
  33. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/kernel/model.py +12 -2
  34. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/registry/schema.sql +6 -1
  35. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/registry/store.py +117 -34
  36. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/__init__.py +4 -0
  37. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/_text.py +18 -1
  38. backdraft-0.6.0/src/backdraft/render/cli.py +184 -0
  39. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/footnotes.py +27 -8
  40. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/html/__init__.py +5 -2
  41. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/html/assets.py +73 -16
  42. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/html/components.py +32 -7
  43. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/html/page.py +23 -29
  44. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/html/text.py +27 -0
  45. backdraft-0.6.0/src/backdraft/render/theme.py +372 -0
  46. backdraft-0.6.0/src/backdraft/render/themes/default.toml +44 -0
  47. backdraft-0.6.0/src/backdraft/render/themes/press.toml +28 -0
  48. backdraft-0.6.0/src/backdraft/render/themes/slate.toml +32 -0
  49. backdraft-0.6.0/tests/conftest.py +25 -0
  50. backdraft-0.6.0/tests/css_util.py +66 -0
  51. {backdraft-0.4.1 → backdraft-0.6.0}/tests/gate/test_gate_cli.py +13 -3
  52. {backdraft-0.4.1 → backdraft-0.6.0}/tests/gate/test_gate_reader.py +17 -0
  53. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/render/backfill.sidecar.json +1 -1
  54. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/render/demo.sidecar.json +1 -1
  55. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_bind_cli.py +26 -0
  56. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_bind_evidence.py +52 -0
  57. backdraft-0.6.0/tests/test_card_sizing.py +107 -0
  58. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_cli.py +51 -2
  59. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_extract.py +20 -24
  60. backdraft-0.6.0/tests/test_extract_html.py +249 -0
  61. backdraft-0.6.0/tests/test_fetch.py +188 -0
  62. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_footnotes.py +48 -0
  63. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_html.py +129 -6
  64. backdraft-0.6.0/tests/test_ingest_url.py +354 -0
  65. backdraft-0.6.0/tests/test_render_cli.py +330 -0
  66. backdraft-0.6.0/tests/test_snapshots.py +509 -0
  67. backdraft-0.6.0/tests/test_theme.py +283 -0
  68. backdraft-0.6.0/tests/test_version.py +46 -0
  69. {backdraft-0.4.1 → backdraft-0.6.0}/uv.lock +1 -1
  70. backdraft-0.4.1/.github/workflows/site.yml +0 -26
  71. backdraft-0.4.1/ROADMAP.md +0 -63
  72. backdraft-0.4.1/src/backdraft/__init__.py +0 -13
  73. backdraft-0.4.1/src/backdraft/render/cli.py +0 -107
  74. backdraft-0.4.1/tests/conftest.py +0 -9
  75. backdraft-0.4.1/tests/test_render_cli.py +0 -161
  76. {backdraft-0.4.1 → backdraft-0.6.0}/.claude-plugin/marketplace.json +0 -0
  77. {backdraft-0.4.1 → backdraft-0.6.0}/.github/workflows/ci.yml +0 -0
  78. {backdraft-0.4.1 → backdraft-0.6.0}/.github/workflows/release.yml +0 -0
  79. {backdraft-0.4.1 → backdraft-0.6.0}/.gitignore +0 -0
  80. {backdraft-0.4.1 → backdraft-0.6.0}/.python-version +0 -0
  81. {backdraft-0.4.1 → backdraft-0.6.0}/LICENSE +0 -0
  82. {backdraft-0.4.1 → backdraft-0.6.0}/assets/backdraft-mark.svg +0 -0
  83. {backdraft-0.4.1 → backdraft-0.6.0}/demo/generate_sources.py +0 -0
  84. {backdraft-0.4.1 → backdraft-0.6.0}/demo/memo.md +0 -0
  85. {backdraft-0.4.1 → backdraft-0.6.0}/demo/sources/t12-summary.pdf +0 -0
  86. {backdraft-0.4.1 → backdraft-0.6.0}/demo/sources/underwriting-model.xlsx +0 -0
  87. {backdraft-0.4.1 → backdraft-0.6.0}/spec/chunking.md +0 -0
  88. {backdraft-0.4.1 → backdraft-0.6.0}/spec/tokens.md +0 -0
  89. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/__main__.py +0 -0
  90. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/bind/__init__.py +0 -0
  91. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/bind/binder.py +0 -0
  92. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/bind/verify/__init__.py +0 -0
  93. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/bind/verify/base.py +0 -0
  94. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/bind/verify/entail.py +0 -0
  95. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/bind/verify/overlap.py +0 -0
  96. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/bind/verify/recompute.py +0 -0
  97. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/bind/verify/value_trace.py +0 -0
  98. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/cli_context.py +0 -0
  99. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/credentials.py +0 -0
  100. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/__init__.py +0 -0
  101. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/csv.py +0 -0
  102. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/docx.py +0 -0
  103. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/image.py +0 -0
  104. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/pdf_text.py +0 -0
  105. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/pptx.py +0 -0
  106. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/sheet.py +0 -0
  107. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/text.py +0 -0
  108. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/vlm_settings.py +0 -0
  109. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/xls.py +0 -0
  110. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/extract/xlsx.py +0 -0
  111. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/gate/cli.py +0 -0
  112. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/kernel/__init__.py +0 -0
  113. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/kernel/chunking.py +0 -0
  114. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/kernel/claims.py +0 -0
  115. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/kernel/errors.py +0 -0
  116. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/kernel/hashing.py +0 -0
  117. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/kernel/tokens.py +0 -0
  118. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/registry/__init__.py +0 -0
  119. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/registry/ledger.py +0 -0
  120. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/html/fmt.py +0 -0
  121. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/html/fmt_vectors.py +0 -0
  122. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/markdown.py +0 -0
  123. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/placement.py +0 -0
  124. {backdraft-0.4.1 → backdraft-0.6.0}/src/backdraft/render/sidecar.py +0 -0
  125. {backdraft-0.4.1 → backdraft-0.6.0}/tests/conftest_registry.py +0 -0
  126. {backdraft-0.4.1 → backdraft-0.6.0}/tests/conftest_render.py +0 -0
  127. {backdraft-0.4.1 → backdraft-0.6.0}/tests/fakes.py +0 -0
  128. {backdraft-0.4.1 → backdraft-0.6.0}/tests/gate/conftest.py +0 -0
  129. {backdraft-0.4.1 → backdraft-0.6.0}/tests/gate/fake_registry.py +0 -0
  130. {backdraft-0.4.1 → backdraft-0.6.0}/tests/gate/test_gate_ledger.py +0 -0
  131. {backdraft-0.4.1 → backdraft-0.6.0}/tests/gate/test_gate_searcher.py +0 -0
  132. {backdraft-0.4.1 → backdraft-0.6.0}/tests/gate/test_reader_cells.py +0 -0
  133. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/bind/backfill.bound.md +0 -0
  134. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/bind/frontwalk.bound.md +0 -0
  135. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/long_tail.json +0 -0
  136. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/long_tail.txt +0 -0
  137. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/many_small.json +0 -0
  138. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/many_small.txt +0 -0
  139. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/mixed.json +0 -0
  140. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/mixed.txt +0 -0
  141. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/one_giant.json +0 -0
  142. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/one_giant.txt +0 -0
  143. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/short_page.json +0 -0
  144. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/short_page.txt +0 -0
  145. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/unsplittable.json +0 -0
  146. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/chunking/unsplittable.txt +0 -0
  147. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/render/backfill.footnotes.md +0 -0
  148. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden/render/demo.footnotes.md +0 -0
  149. {backdraft-0.4.1 → backdraft-0.6.0}/tests/golden_util.py +0 -0
  150. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_bind_binder.py +0 -0
  151. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_bind_verify.py +0 -0
  152. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_chunking.py +0 -0
  153. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_claims.py +0 -0
  154. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_credentials.py +0 -0
  155. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_fmt_parity.py +0 -0
  156. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_gate_integration.py +0 -0
  157. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_hashing.py +0 -0
  158. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_invariants.py +0 -0
  159. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_model.py +0 -0
  160. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_placement.py +0 -0
  161. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_records_location.py +0 -0
  162. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_registry.py +0 -0
  163. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_reingest.py +0 -0
  164. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_render_markdown.py +0 -0
  165. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_sidecar.py +0 -0
  166. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_snapshot_settings.py +0 -0
  167. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_tokens.py +0 -0
  168. {backdraft-0.4.1 → backdraft-0.6.0}/tests/test_value_trace.py +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "backdraft",
3
3
  "displayName": "Backdraft",
4
- "version": "0.4.1",
4
+ "version": "0.6.0",
5
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
6
  "author": {
7
7
  "name": "Spencer Braun",
@@ -0,0 +1,36 @@
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
+ # One deployment at a time, and a publish in flight is never cancelled: a
12
+ # cancelled Pages deploy can leave the backend with a deployment that never
13
+ # completes, which is a stuck site rather than a lost run.
14
+ concurrency:
15
+ group: pages
16
+ cancel-in-progress: false
17
+ jobs:
18
+ # Build and deploy are separate so that re-running the failed job re-runs
19
+ # only the deploy. Bundled, a retry uploaded a second `github-pages`
20
+ # artifact and `deploy-pages` refused the run for finding two of them.
21
+ build:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: actions/upload-pages-artifact@v3
26
+ with:
27
+ path: site/
28
+ deploy:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment:
32
+ name: github-pages
33
+ url: ${{ steps.deployment.outputs.page_url }}
34
+ steps:
35
+ - id: deployment
36
+ uses: actions/deploy-pages@v4
@@ -134,6 +134,12 @@ Self-contained HTML (plus the machine-readable sidecar): **quotes + receipts + e
134
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
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
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. |
137
+ | 2026-07-30 | **Every PDF ingest captures page images, not just the VLM path's.** The text-layer route stored none, so the keyless path produced visually poorer artifacts for no principled reason — the pages render locally through poppler, no model calls, and the capability already existed as the `snapshot-pages` backfill. The capture is a step *after* extraction, owned by `cli.py` over `extract/snapshots.py`, deliberately **not** inside `pdf-text`: an extractor whose output varied with whether poppler happened to be installed would not be `deterministic = True`, and the same bytes must extract identically on every machine. Because snapshots only ever write `page_images` — a table no token, chunk, or receipt path reads — capture is safe to make best-effort: no poppler means ingest still exits 0, with a one-line note naming `snapshot-pages` as the backfill, rather than failing over a display concern. Tradeoff accepted: PDF ingest is now slower by a local render per page (one poppler call per page rather than one per document, to bound peak memory on long PDFs), and a library caller using `Registry.ingest` directly gets no snapshots — the policy lives at the CLI, which is the substrate that has a user to notify. |
138
+ | 2026-08-03 | **The card divides its own height; nothing inside it is pinned.** The receipt card is user-resizable, so it is a flex column: header, source selector, source line and tabs are fixed rows that never scroll away, and the evidence region takes whatever is left — dragging the card taller grows the evidence rather than the whitespace under it. The caps that survive are relative (the card at `82vh` of the window; the quote, drift diff and record at `40%` of the card), and a `5rem` floor keeps the evidence from vanishing at the card's minimum, with the citation scrolling instead. Resizing means *more*, not *smaller*: the page image keeps its scale and the pane scrolls over it, because a page shrunk to fit a short card is a page you cannot read, while the sheet window — which has no scale to lose — grows to fill, matching the full-sheet overlay it opens into. Both sheet views now stick their headers from one rule. Display only, as ever: no token, receipt or record moves, and the phone card keeps `resize:none` and the fixed caps it always had, since there is no drag there for the evidence to follow. |
139
+ | 2026-08-04 | **A theme is an override block, not a stylesheet.** The artifact's look is themeable, and a theme is a small TOML file naming values for the CSS custom properties the stylesheet already exposes — the sixteen colors and the three font roles — plus four bounded heading knobs (family, case, weight, tracking) that the theme module compiles into one rule over the document's own headings (`HEADING_SELECTOR`) that it owns. The built-in stylesheet is not edited and not replaced: the resolved theme is emitted as a block *after* it, so with nothing configured the artifact is byte-identical to an untheme'd build and theming is provably inert by default. A theme may not touch layout, structure, or any verification affordance, and never touches a token, receipt or record — `--rail-w` and `color-scheme` are deliberately outside the allowlist for that reason, and so is the artifact's refusal to follow the reader's system theme. Keys and values are both validated against declared allowlists, so `--colour` or `#GGG` fails by name before anything is written rather than producing a half-styled file; `url(...)` is rejected with the reason, since a font the file would have to fetch is exactly what the CSP forbids. Precedence is `--theme` > project `.backdraft/theme.toml` > XDG `~/.config/backdraft/theme.toml` > built-in, the user-wide file being what makes a preference stick across projects. The bundled `default.toml` restates today's values and is pinned by a test against the stylesheet's own `:root`, which makes it the audit as well as the reference sample. Tradeoff accepted: `--serif`/`--sans`/`--mono` name roles (body, UI, code) rather than classifications, so a sans-bodied theme sets `serif`. *(2026-08-04, follow-up: `--hl` was declared in `:root` and read by no rule, so it is deleted rather than left as a themeable key that paints nothing, and the `#fff` behind page images now tracks `--paper` — white there was coincidence, and every stored image is opaque RGB, so it was dead either way. The six constants that remain hardcoded — the card and rail shadows, the overlay scrim, the PDF tab accent, the `partial` verdict amber, sheet-header grey — were considered and deliberately left: each is near-neutral at any sane palette distance, and promoting them is a wider audit for churn rather than effect. Separately, `.doc h3` through `h6` had no rule at all — the markdown renderer emits every level and only two were styled, so a memo with `### ` fell to the browser's bold-Times default mid-page — and they are now a two-step scale that *brackets* the body rather than continuing down from `h2`, since the sheet sizes in `rem` while `body` sets 17px and a plain `1rem` heading lands under the text it heads. The theme's heading selector covers the whole scale for the same reason it exists: small-caps section heads above a body-face subsection read as a mistake, not a choice.)* |
140
+ | 2026-08-04 | **Failure speaks at the claim, not in the masthead (amends the 2026-07-28 reader doctrine).** The artifact no longer prints a failure summary under the title. "Success is silent; only failure speaks" stands, and so does the principle under it — failures are data, never warn-and-drop — but *where* it speaks moves: the wavy alarm-coloured mark on the affected claim, and that claim's note with the reason, both of which a reader meets in context. The line that went was a bare count, and a count is the one thing about a failed citation a reader cannot act on: whether `1 of 16 citations could not be traced` matters depends entirely on which claim it was, which the headline does not say and the mark and the note do. It was also aimed at the wrong reader — the writer verifying the work learns about failures from `bind`, which exits 2 and lists them, while the recipient got a number out of context before the first sentence. Nothing loses its last surface: an unmatched claim with no citations ranks `unresolved` and is marked in the body like any other failure, and a claim that is not in the document at all was already the Notes' job under artifact rule 4. This is a spec change — `spec/artifact.md` rule 6 is rewritten, and the summary is now explicitly *not* required of a conforming producer rather than merely unmentioned, so another implementation reads the same intent. |
141
+ | 2026-08-05 | **A web page's identity is the sha256 of the snapshot fetched at ingest time; the URL is provenance, not identity.** `ingest` accepts an http(s) URL alongside files (graduating the "HTML pages" question from Later). The fetched bytes are hashed and snapshotted exactly as a file's are, so everything downstream — anchors, the ledger, `bind`, generations — is unchanged, and a page that changed since the last fetch is a *new generation of the same document*, which is the machinery drift already describes. Continuity across a re-fetch follows the URL rather than the path, because the path is a temporary staging file; `documents.sha256` is UNIQUE, so bytes still win over origin when the same content arrives twice under two URLs. The URL and `fetched_at` live in a new `document_meta` table — a JSON sibling of `page_meta`, chosen over columns on `documents` so an existing registry needs no migration and so the next provenance question (`.eml` headers) needs no second one; `fetched_at` moves on a re-fetch that changed nothing, since "when was this last confirmed to say this" is the question a reader of an old citation actually has. Fetching is stdlib `urllib` and lives in `fetch.py`, *outside* `extract/`: an extractor stays a pure function of a file's bytes, which is what keeps `deterministic` honest and lets a saved `.html` file and a fetched page extract identically. The CLI owns the network the way it already owns page-snapshot capture. The new `html` extractor is a parse, not a readability heuristic — no boilerplate stripping, no main-content guess, because a heuristic that changes its mind moves anchors, which is the failure mode this system exists to prevent; navigation and footers are part of the page. Its representation follows `docx`: tables as markdown pipe tables, lists as `- `/`1. ` lines kept in one block, `<pre>` verbatim. Decoding is BOM, then the document's own `<meta charset>`, then UTF-8 with replacement — the server's `Content-Type` charset is deliberately *not* consulted, so the bytes alone determine the text. Known limitations, accepted and documented rather than worked around: JavaScript-rendered pages yield whatever the server sends, pages behind a login are out of reach, and a non-UTF-8 page that declares its encoding only in the HTTP header will render as mojibake in the snapshot — visibly, rather than silently. |
142
+ | 2026-08-06 | **A fetched source's origin travels into the artifact as a live link, dated.** The capture half of the row above stored `url` and `fetched_at` and stopped at the registry, so an artifact citing a web page named `q4-2025.html` — a staging filename that never existed on anyone's disk — and a reader holding the file had the frozen receipt with no way back to the page. `bind`'s evidence `documents` map now carries `url` and `fetched_at` through to the record, and both renderers show them: the receipt card, the script-free note, the source list and the rail all print the URL as a real `<a href>` with the fetch date beside it, and the markdown projection carries it as an autolink. This is an artifact-format change, so `spec/artifact.md` and the `$legend` name the two keys together, both OPTIONAL. The keys are emitted *only* where a document has a URL, which is what keeps an artifact built from files byte-identical to one built before this existed — the conditional is the compatibility guarantee, and it is a test rather than an intention. Linking does not weaken the self-containment rule: the CSP (`default-src 'none'`) forbids *fetching*, and an anchor fetches nothing until a reader chooses to follow it, so the file still renders identically offline. The href is allowlisted to `http`/`https` rather than merely screened for `javascript:`, since artifact rule 3 is a conformance requirement and an allowlist cannot be outflanked by a scheme nobody thought of; a URL under any other scheme is printed as plain text rather than dropped, because failures are data and provenance the reader must paste by hand still beats provenance withheld. In the source list the URL *replaces* the filename rather than joining it — showing both would give a reader two names for one thing and let the fictional one look authoritative. The date is the day, not the timestamp: "as of" is what makes a frozen quote from a live page defensible, and no reader needs the seconds. |
137
143
 
138
144
  ## Open
139
145
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backdraft
3
- Version: 0.4.1
3
+ Version: 0.6.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
@@ -54,17 +54,19 @@ cd demo
54
54
 
55
55
  **Ingest** the sources. Every anchor is minted here, chunks for PDF pages,
56
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.
57
+ PPTX, HTML, images (png, jpeg, tiff, through the vision model), plain text and
58
+ Markdown. A source can also be an `http(s)` URL — the page is fetched once and
59
+ snapshotted like any other source, and the URL travels with it.
59
60
 
60
61
  ```console
61
62
  $ backdraft init
62
63
  registry: /…/demo/.backdraft
63
64
  documents: 0
65
+ … plus where settings live and what to do next
64
66
 
65
67
  $ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
66
68
  t12-summary t12-summary.pdf pdf 3 pages
67
- underwriting-model underwriting-model.xlsx xlsx 2 pages
69
+ underwriting-model underwriting-model.xlsx xlsx 2 sheets
68
70
  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.
69
71
  ```
70
72
 
@@ -130,7 +132,7 @@ $ backdraft bind memo.md --session s-bridgeview --check value-trace,overlap
130
132
  bound 15 claim(s), 16 citation(s) [frontwalk]
131
133
  resolved: 15
132
134
  unresolved: 1
133
- overlap: partial 4, pass 11
135
+ overlap: pass 11, skip 4
134
136
  value-trace: pass 15
135
137
  ! unresolved: bd:t12-summary:p4.c1:1a2b
136
138
  wrote .backdraft/records/memo.backdraft.json
@@ -154,6 +156,33 @@ memo.backdraft.html
154
156
  That file is the whole deliverable: send it over email or Slack and the
155
157
  recipient gets the full experience by double-clicking it.
156
158
 
159
+ **Theming.** The artifact ships with a default look and two alternates,
160
+ `press` and `slate`:
161
+
162
+ ```console
163
+ $ backdraft render memo.md --theme slate
164
+ ```
165
+
166
+ A theme is a small TOML file naming colors, font stacks and heading treatment.
167
+ Drop one at `~/.config/backdraft/theme.toml` and every artifact you render, in
168
+ any project, honors it without a flag; `.backdraft/theme.toml` overrides that
169
+ for one project, and `--theme` overrides both. Themes are display only — they
170
+ never touch a token, a receipt or the record.
171
+
172
+ ```console
173
+ $ backdraft theme list
174
+ default
175
+ press
176
+ slate
177
+
178
+ in effect here: the built-in look
179
+ [Start your own: backdraft theme show default > /Users/you/.config/backdraft/theme.toml]
180
+ ```
181
+
182
+ `theme show` prints a validated file, so redirecting it writes a fully
183
+ commented starting point with every key and what it paints — and pointing it
184
+ at a file of your own checks that file without rendering anything.
185
+
157
186
  The full version of this, with every command and every output:
158
187
  [`demo/walkthrough.md`](demo/walkthrough.md). The artifact it produces is checked
159
188
  in at [`demo/memo.backdraft.html`](demo/memo.backdraft.html).
@@ -182,6 +211,38 @@ Failures are data throughout: `unresolved`, `not_shown`, `drifted`, `malformed`
182
211
  and `unmatched` are first-class records in the report and visible sections in the
183
212
  artifact. Nothing is ever warned about and dropped.
184
213
 
214
+ ## Web sources
215
+
216
+ `backdraft ingest https://example.com/q4-results` fetches the page once and
217
+ snapshots it exactly as a file: the bytes at fetch time are the document's
218
+ identity, and the URL and fetch timestamp ride along as provenance. So a page
219
+ that has changed since you cited it comes back as a new generation of the same
220
+ document and the old citations report `drifted` — the same machinery as an
221
+ edited PDF, which is the point of hashing the snapshot rather than trusting the
222
+ address.
223
+
224
+ ```console
225
+ $ backdraft ingest https://example.com/reports/q4-2025
226
+ q4-2025 q4-2025.html html 1 page
227
+
228
+ $ backdraft ls
229
+ q4-2025 q4-2025.html html 1 page https://example.com/reports/q4-2025
230
+ ```
231
+
232
+ The origin travels into the artifact too. A claim citing a fetched page shows
233
+ the URL as a link on its receipt, with the date the bytes were taken, and the
234
+ source list names the page rather than the staged filename. That pointer is the
235
+ half of citing a web page a frozen receipt cannot answer on its own: the
236
+ snippet says what the page said, the link is how a reader asks whether it still
237
+ says it.
238
+
239
+ What it does not do, stated rather than worked around: JavaScript-rendered
240
+ pages give you whatever the server sends to a plain GET, pages behind a login
241
+ are out of reach, and the extractor is a parse rather than a readability guess
242
+ — navigation and footers are part of the page, because a heuristic that changes
243
+ its mind between two versions of a site would move anchors. Responses are
244
+ capped at 32 MiB.
245
+
185
246
  ## What lives where
186
247
 
187
248
  A working directory stays clean: the authored document and its artifact are
@@ -209,8 +270,9 @@ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
209
270
  key in the environment is not consent to send documents to its provider. The
210
271
  default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
211
272
  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.
273
+ the keyless text layer and says so — still storing each page's image, which it
274
+ renders locally. PDF page rendering needs poppler, installed separately; without
275
+ it ingest succeeds without the images and names `snapshot-pages` as the backfill.
214
276
 
215
277
  Two extras:
216
278
 
@@ -29,17 +29,19 @@ cd demo
29
29
 
30
30
  **Ingest** the sources. Every anchor is minted here, chunks for PDF pages,
31
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
+ PPTX, HTML, images (png, jpeg, tiff, through the vision model), plain text and
33
+ Markdown. A source can also be an `http(s)` URL — the page is fetched once and
34
+ snapshotted like any other source, and the URL travels with it.
34
35
 
35
36
  ```console
36
37
  $ backdraft init
37
38
  registry: /…/demo/.backdraft
38
39
  documents: 0
40
+ … plus where settings live and what to do next
39
41
 
40
42
  $ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
41
43
  t12-summary t12-summary.pdf pdf 3 pages
42
- underwriting-model underwriting-model.xlsx xlsx 2 pages
44
+ underwriting-model underwriting-model.xlsx xlsx 2 sheets
43
45
  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.
44
46
  ```
45
47
 
@@ -105,7 +107,7 @@ $ backdraft bind memo.md --session s-bridgeview --check value-trace,overlap
105
107
  bound 15 claim(s), 16 citation(s) [frontwalk]
106
108
  resolved: 15
107
109
  unresolved: 1
108
- overlap: partial 4, pass 11
110
+ overlap: pass 11, skip 4
109
111
  value-trace: pass 15
110
112
  ! unresolved: bd:t12-summary:p4.c1:1a2b
111
113
  wrote .backdraft/records/memo.backdraft.json
@@ -129,6 +131,33 @@ memo.backdraft.html
129
131
  That file is the whole deliverable: send it over email or Slack and the
130
132
  recipient gets the full experience by double-clicking it.
131
133
 
134
+ **Theming.** The artifact ships with a default look and two alternates,
135
+ `press` and `slate`:
136
+
137
+ ```console
138
+ $ backdraft render memo.md --theme slate
139
+ ```
140
+
141
+ A theme is a small TOML file naming colors, font stacks and heading treatment.
142
+ Drop one at `~/.config/backdraft/theme.toml` and every artifact you render, in
143
+ any project, honors it without a flag; `.backdraft/theme.toml` overrides that
144
+ for one project, and `--theme` overrides both. Themes are display only — they
145
+ never touch a token, a receipt or the record.
146
+
147
+ ```console
148
+ $ backdraft theme list
149
+ default
150
+ press
151
+ slate
152
+
153
+ in effect here: the built-in look
154
+ [Start your own: backdraft theme show default > /Users/you/.config/backdraft/theme.toml]
155
+ ```
156
+
157
+ `theme show` prints a validated file, so redirecting it writes a fully
158
+ commented starting point with every key and what it paints — and pointing it
159
+ at a file of your own checks that file without rendering anything.
160
+
132
161
  The full version of this, with every command and every output:
133
162
  [`demo/walkthrough.md`](demo/walkthrough.md). The artifact it produces is checked
134
163
  in at [`demo/memo.backdraft.html`](demo/memo.backdraft.html).
@@ -157,6 +186,38 @@ Failures are data throughout: `unresolved`, `not_shown`, `drifted`, `malformed`
157
186
  and `unmatched` are first-class records in the report and visible sections in the
158
187
  artifact. Nothing is ever warned about and dropped.
159
188
 
189
+ ## Web sources
190
+
191
+ `backdraft ingest https://example.com/q4-results` fetches the page once and
192
+ snapshots it exactly as a file: the bytes at fetch time are the document's
193
+ identity, and the URL and fetch timestamp ride along as provenance. So a page
194
+ that has changed since you cited it comes back as a new generation of the same
195
+ document and the old citations report `drifted` — the same machinery as an
196
+ edited PDF, which is the point of hashing the snapshot rather than trusting the
197
+ address.
198
+
199
+ ```console
200
+ $ backdraft ingest https://example.com/reports/q4-2025
201
+ q4-2025 q4-2025.html html 1 page
202
+
203
+ $ backdraft ls
204
+ q4-2025 q4-2025.html html 1 page https://example.com/reports/q4-2025
205
+ ```
206
+
207
+ The origin travels into the artifact too. A claim citing a fetched page shows
208
+ the URL as a link on its receipt, with the date the bytes were taken, and the
209
+ source list names the page rather than the staged filename. That pointer is the
210
+ half of citing a web page a frozen receipt cannot answer on its own: the
211
+ snippet says what the page said, the link is how a reader asks whether it still
212
+ says it.
213
+
214
+ What it does not do, stated rather than worked around: JavaScript-rendered
215
+ pages give you whatever the server sends to a plain GET, pages behind a login
216
+ are out of reach, and the extractor is a parse rather than a readability guess
217
+ — navigation and footers are part of the page, because a heuristic that changes
218
+ its mind between two versions of a site would move anchors. Responses are
219
+ capped at 32 MiB.
220
+
160
221
  ## What lives where
161
222
 
162
223
  A working directory stays clean: the authored document and its artifact are
@@ -184,8 +245,9 @@ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
184
245
  key in the environment is not consent to send documents to its provider. The
185
246
  default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
186
247
  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.
248
+ the keyless text layer and says so — still storing each page's image, which it
249
+ renders locally. PDF page rendering needs poppler, installed separately; without
250
+ it ingest succeeds without the images and names `snapshot-pages` as the backfill.
189
251
 
190
252
  Two extras:
191
253
 
@@ -0,0 +1,258 @@
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
+ ## How the queue runs
8
+
9
+ The Now section is an ordered queue written for an implementing agent with no
10
+ session context: each item carries its intent, its shape, and its acceptance
11
+ test. The working agreement is one item at a time, top first, shipped complete
12
+ — code, tests, docs, and a DESIGN.md decision row where the item calls for one
13
+ — then deleted from this file in the same commit. Releases cut Friday, so an
14
+ item that lands mid-week waits on PyPI until the release; nothing ships
15
+ half-done to make a release. An item that turns out bigger than its sizing
16
+ gets split here, not stretched silently.
17
+
18
+ The cadence is daily and the runner is an agent: one item each morning Monday
19
+ through Thursday, landing as a direct commit to `main` — this repo does not
20
+ use pull requests, so a week of commits is the only review surface there is.
21
+
22
+ Friday builds nothing new. It reads that week's diff instead — duplication,
23
+ drift from the repo's own idioms, branches that landed untested, help text and
24
+ error messages that confuse, commands the docs describe wrongly — and lands
25
+ the fixes. Then the release, and only if something landed since the last tag:
26
+ the version moves in both `pyproject.toml` and `.claude-plugin/plugin.json`,
27
+ and publishing the GitHub release is what ships to PyPI. Then this section is
28
+ refilled with five items for the week ahead, drawn against the core — the four
29
+ verbs, the registry, the grammar, the artifact — and against where an agent
30
+ using backdraft actually stumbles. What the maintenance pass just found is the
31
+ best evidence available for what those five should be.
32
+
33
+ ## Now
34
+
35
+ ### 1. Bind's failure lines name the claim, not just the token
36
+
37
+ **Intent.** Exit 2 tells a calling agent that a citation did not resolve and
38
+ prints the token: `! unresolved: bd:t12-summary:p4.c1:1a2b`. It does not say
39
+ which sentence that token was on. `skills/backdraft/SKILL.md` then instructs
40
+ the agent to "tell the user which claim it belongs to" — the one fact the
41
+ report withheld — so the agent's next move is to grep its own document for a
42
+ token string. Meanwhile the `! unmatched:` line, three lines below, already
43
+ prints its claim text. The two line-items should carry the same weight.
44
+
45
+ **Shape.** `_print_report` in `bind/cli.py`, CLI output only — the record JSON
46
+ is a pinned format and already carries the claim/citation nesting, so nothing
47
+ in `spec/artifact.md` moves. `BindReport.claims` holds each claim's `text`,
48
+ `start` and `end` alongside its citations, so the mapping from a reported
49
+ citation back to its claim exists in memory; report the citation line as the
50
+ token, then the claim text truncated the way the `unmatched` line already
51
+ truncates it (80 chars), then the offset. Repeats matter here: the same token
52
+ cited on four claims currently prints four identical lines, which reads as four
53
+ problems — print one line per (token, claim) pair, which is what the offset
54
+ makes legible. Applies to every non-resolved status, `not_shown` and `drifted`
55
+ included, not only `unresolved`.
56
+
57
+ **Acceptance.** In `demo/`, `backdraft bind memo.md --session s-bridgeview
58
+ --check value-trace,overlap` prints the unresolved line with the replacement-
59
+ reserve claim text and its offset on it. A bind whose session was never read
60
+ into prints one line per distinct claim rather than one per citation
61
+ occurrence. Tests cover `unresolved`, `not_shown`, `drifted` and `malformed`.
62
+ Every doc that shows a bind report — `README.md`, `demo/walkthrough.md`,
63
+ `site/llms.txt`, `skills/backdraft/SKILL.md`,
64
+ `skills/backdraft-backfill/SKILL.md` — is updated to the real new output.
65
+
66
+ **Size.** One day.
67
+
68
+ ### 2. `--config` keys are declared, validated, and listed
69
+
70
+ **Intent.** `backdraft ingest x.pdf --config dpy=300` exits 0, ingests, and
71
+ does nothing about the typo. No output names the mistake, and nothing anywhere
72
+ — help, docs, `llms.txt` — lists what keys exist; today the only way to learn
73
+ `dpi`, `model`, `base_url`, `api_key`, `snapshot_quality` or
74
+ `snapshot_max_height` is to read `extract/vlm.py` and `extract/snapshots.py`.
75
+ This repo's rule is that failures are data and never warn-and-drop, and the
76
+ config boundary is where that rule does not hold.
77
+
78
+ **Shape.** Give the `Extractor` protocol in `extract/base.py` a
79
+ `config_keys: Mapping[str, str]` (key → one-line meaning), defaulting to empty
80
+ so no extractor is forced to declare. `vlm` declares its provider and retry
81
+ keys; the page-render keys (`dpi`, `snapshot_quality`, `snapshot_max_height`)
82
+ are declared once by `extract/snapshots.py`, which already owns them, and are
83
+ accepted for every PDF path since both rasterize. `ingest` validates the parsed
84
+ config against the selected extractor's declared set *after* selection, since
85
+ `auto` picks per file, and raises `UsageError` in the shape the unknown-name
86
+ errors already use: `unknown config key 'dpy' for pdf-text; known: dpi,
87
+ snapshot_max_height, snapshot_quality`. A key that is valid for one extractor
88
+ and not another must fail only where it does not apply.
89
+
90
+ **Acceptance.** The typo above exits 1 and names both the key and the valid
91
+ ones; `--config dpi=300` still works on both the `vlm` and the text-layer path;
92
+ `--config model=...` fails on `pdf-text` and passes on `vlm`. Tests for each
93
+ branch, including a declaration-free extractor rejecting any key. The docs
94
+ page's `ingest` row and `site/llms.txt` list the keys.
95
+
96
+ **Size.** Two days.
97
+
98
+ ### 3. `backdraft show <token>`: the inverse of minting
99
+
100
+ **Intent.** An agent handed a token — out of someone's artifact, a half-written
101
+ draft, a colleague's message — cannot ask what it says. The gate mints tokens
102
+ and `bind` resolves them, but only as part of binding a whole document, so
103
+ there is no way to answer "what is `bd:t12-summary:p1.c4:410d`?" without
104
+ reconstructing the slug and selector by hand and re-reading a whole page.
105
+ `skills/backdraft-artifact/SKILL.md` names exactly this gap when it says only
106
+ one check needs the outside world.
107
+
108
+ **Shape.** A new command in `gate/cli.py` — `show <token> [<token> ...]` —
109
+ because this is the gate showing source text and must mint what it shows,
110
+ exactly as `read` and `search` do; an anchor shown here is an anchor the writer
111
+ may cite, and `not_shown` stays true by construction. Parse with
112
+ `kernel.tokens`, resolve through the same path `bind` uses rather than a second
113
+ implementation of what a token means, and print the closed status set bind
114
+ reports: `resolved` (locator + verbatim snippet), `drifted` (the snippet then
115
+ and the snippet now), `unresolved`, `malformed`. Output follows the gate's
116
+ line-oriented shape and ends with the usual bracketed next-step hint.
117
+
118
+ **Acceptance.** In `demo/`, `backdraft show bd:t12-summary:p1.c1:c2e8` prints
119
+ the snippet, and a subsequent `bind` of a document citing it reports `resolved`
120
+ rather than `not_shown` — that is the test that proves it minted. A
121
+ well-formed token naming nothing exits 1 saying so; a malformed one names the
122
+ grammar. Several tokens in one invocation print in argument order. `README.md`,
123
+ the docs page's command table, `site/llms.txt` and
124
+ `skills/backdraft-artifact/SKILL.md` gain it.
125
+
126
+ **Size.** Two days.
127
+
128
+ ### 4. Ingest finishes the list, then reports what it could not read
129
+
130
+ **Intent.** `backdraft ingest a.md broken.pdf c.md` ingests `a.md`, fails on
131
+ `broken.pdf`, and never attempts `c.md`. The registry is left half-built, the
132
+ message names only the first failure, and nothing says which files landed and
133
+ which were never tried — so an agent ingesting a folder must re-run and infer
134
+ the state. This is the same warn-and-drop shape the page-snapshot capture
135
+ already avoids: it collects failures, names them, and lets the rest of the run
136
+ stand.
137
+
138
+ **Shape.** `ingest` in `cli.py`. Move the per-file work inside a try that
139
+ catches `BackdraftError` and records `(path, reason)` instead of propagating,
140
+ so the loop always reaches the end of the list. After the loop, print one line
141
+ per failure naming the file and the reason, grouped the way the snapshot note
142
+ already groups by reason, and exit 1 if anything failed — the exit code is
143
+ unchanged, only the coverage and the reporting improve. Re-ingest is idempotent
144
+ for deterministic extractors, so re-running after a fix costs nothing, and the
145
+ message should say that rather than leaving the agent to guess. Keep the guard
146
+ in `cli_context` as the only place `BackdraftError` becomes an exit code:
147
+ ingest catches per file and re-raises nothing, printing failures as data.
148
+
149
+ **Acceptance.** The three-file example above ingests `a.md` and `c.md`, prints
150
+ one failure line for `broken.pdf` naming why, and exits 1; `backdraft ls`
151
+ afterwards shows both good documents. A run where every file fails still exits
152
+ 1 and names each. A run where none fail is byte-identical to today's output.
153
+ Tests for all three.
154
+
155
+ **Size.** One day.
156
+
157
+ ### 5. The demo cites a web page, so the feature is visible
158
+
159
+ **Intent.** URL sources ship complete — `ingest` fetches and snapshots, the
160
+ registry keeps `url` and `fetched_at`, and as of 2026-08-06 the artifact links
161
+ back to the page with its fetch date. None of it appears at
162
+ `backdraft.dev/demo.html`, because both demo sources are local files
163
+ (`t12-summary.pdf`, `underwriting-model.xlsx`). The one artifact anybody
164
+ actually looks at does not show the capability, and the byte-identity test that
165
+ proved the change was safe is the same fact that keeps it invisible. A reader
166
+ evaluating backdraft cannot see that a web citation is a citation like any
167
+ other.
168
+
169
+ **Shape.** Add a third source to `demo/`: the Wikipedia article for the county
170
+ the property sits in, cited for market context — population, median household
171
+ income, the kind of external figure a screening memo genuinely leans on and
172
+ cannot get from a T12. Bridgeview Commons is fictional and says so in its own
173
+ first line, so give it a real county and let the memo name it; one new
174
+ paragraph under "The asset" or a short "Market" section, with one or two
175
+ claims. Cite Wikipedia's **permanent link** (`?oldid=<rev>`), not the bare
176
+ article URL: an article is edited constantly, a citation into the live URL
177
+ would report `drifted` within days, and a front-page demo that permanently
178
+ shows drift teaches that the tool is broken rather than that the source moved.
179
+ An immutable revision plus the rendered "as of" date is also what a careful
180
+ analyst cites. Write the tradeoff as a DESIGN row, naming the thing given up —
181
+ the demo no longer shows a live page changing under a citation, which is the
182
+ better story and belongs in a `drifted` walkthrough, not the shop window.
183
+
184
+ Practical notes for whoever builds this: `demo/.backdraft/` is gitignored, so
185
+ the fetch happens once on the implementer's machine and the snapshot then
186
+ travels in the local registry; `demo/generate_sources.py` builds the two
187
+ fixture files and is *not* where this goes, since the page is fetched rather
188
+ than generated. Re-bind and re-render (`bind memo.md --session s-bridgeview
189
+ --check value-trace,overlap`, then `render memo.md --to html`), and copy the
190
+ result over `site/demo.html` — the two are byte-identical by convention.
191
+ `demo/walkthrough.md` shows real CLI output and its `ls` and `bind` blocks both
192
+ move.
193
+
194
+ **Acceptance.** `backdraft.dev/demo.html` shows three sources, one of them a
195
+ clickable Wikipedia link with a fetch date, on both the resting rail and the
196
+ end-matter list; the claim citing it opens a card whose source line carries the
197
+ same link. `demo/walkthrough.md` matches what the commands now print. A re-bind
198
+ some weeks later still reports `resolved`, not `drifted` — that is the check
199
+ that the permanent link did its job.
200
+
201
+ **Size.** One day.
202
+
203
+ ## Parked
204
+
205
+ Deliberately not queued, each with the reason, so picking one up starts from
206
+ the objection rather than rediscovering it.
207
+
208
+ - **Exhibits view** — the evidence-first inversion. Risk: it grows the
209
+ artifact and pollutes a design whose strength is restraint. Needs a
210
+ deliberate design pass and a real audience before any code; not a
211
+ weekend-feature shape.
212
+ - **Excel region maps and range reads** — the research notes exist, but no
213
+ confidence the deterministic detection cascade lands well on the first
214
+ try. Revisit when large-sheet navigation hurts in a real corpus, with
215
+ test workbooks in hand.
216
+ - **`bd:calc` derivations** — reserved in the grammar and parsed as
217
+ malformed-on-purpose, which is enough for now. Full support complicates
218
+ binding and verification for one field-trial case.
219
+ - **Merged-cell and frozen-pane rendering** — ingest captures `merged` and
220
+ `frozen` in sheet meta and keeps them (cheap, and capture is the part you
221
+ cannot backfill). Frozen-pane rendering is cut; merged-cell rendering
222
+ waits for a real sheet that renders wrong without it.
223
+
224
+ ## Later
225
+
226
+ - **Living documents** — re-ingest, re-bind, and a drift-first report: "these
227
+ claims cite figures that changed; cited-then vs. now." The primitives
228
+ (generations, `drifted`, the word-diff) all exist; the missing piece is a
229
+ diff-shaped report and a demo.
230
+ - **Substrates beyond the CLI** — SDK middleware for pipelines (the exit-code
231
+ and `--to json` contracts are already shaped for it), and a client-side
232
+ drag-drop viewer page (no upload — the file never leaves the reader's
233
+ machine).
234
+ - **Email (.eml)** — arrives in diligence folders alongside everything else;
235
+ identity questions (headers vs. body, attachments as child sources) deserve
236
+ a decision row before code. The web-page half of this item shipped
237
+ 2026-08-05; `document_meta` is where an email's provenance would land.
238
+ - **Anthropic-API provider for the VLM extractor** — Cowork's sandbox
239
+ egress allowlist includes api.anthropic.com but not the OpenAI-compatible
240
+ providers, so an Anthropic-native option would let sandboxed sessions
241
+ ingest at full fidelity. Needs a client abstraction the vlm module
242
+ currently doesn't have; the documented workaround (ingest locally, the
243
+ registry travels with the folder) covers it meanwhile.
244
+ - **Distribution** — published on PyPI; the repo ships
245
+ `.claude-plugin/plugin.json` and `marketplace.json`, so it installs as a
246
+ Claude Code plugin and self-hosts its marketplace, and the AGENTS.md
247
+ snippet is published in the README and site docs. The community-directory
248
+ submission went in 2026-07-29 and is awaiting review — approval is what
249
+ surfaces the skills in Cowork's directory.
250
+
251
+ ## Someday
252
+
253
+ - **Hosted team features** — upload, access control, shared registries.
254
+ Deliberately parked: the product's trust story is the self-contained file,
255
+ and a hosted viewer inverts it. Revisit only as a distinct teams product.
256
+ - **Entail at scale** — the model-judge verifier is wired (`[entail]` extra)
257
+ but has never been field-calibrated; needs a corpus and a rubric before it
258
+ is recommended anywhere.