backdraft 0.5.0__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.
- {backdraft-0.5.0 → backdraft-0.6.0}/.claude-plugin/plugin.json +1 -1
- backdraft-0.6.0/.github/workflows/site.yml +36 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/DESIGN.md +5 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/PKG-INFO +64 -4
- {backdraft-0.5.0 → backdraft-0.6.0}/README.md +63 -3
- backdraft-0.6.0/ROADMAP.md +258 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/SPEC.md +25 -8
- {backdraft-0.5.0 → backdraft-0.6.0}/demo/memo.backdraft.html +38 -19
- {backdraft-0.5.0 → backdraft-0.6.0}/demo/walkthrough.md +20 -1
- {backdraft-0.5.0 → backdraft-0.6.0}/pyproject.toml +1 -1
- {backdraft-0.5.0 → backdraft-0.6.0}/site/demo.html +38 -19
- {backdraft-0.5.0 → backdraft-0.6.0}/site/docs.html +94 -7
- {backdraft-0.5.0 → backdraft-0.6.0}/site/index.html +7 -3
- {backdraft-0.5.0 → backdraft-0.6.0}/site/llms.txt +14 -1
- {backdraft-0.5.0 → backdraft-0.6.0}/skills/backdraft/SKILL.md +11 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/skills/backdraft-artifact/SKILL.md +7 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/skills/backdraft-backfill/SKILL.md +1 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/spec/artifact.md +29 -8
- backdraft-0.6.0/src/backdraft/__init__.py +38 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/bind/evidence.py +27 -8
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/cli.py +82 -36
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/base.py +2 -1
- backdraft-0.6.0/src/backdraft/extract/html.py +331 -0
- backdraft-0.6.0/src/backdraft/fetch.py +188 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/gate/__init__.py +2 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/gate/reader.py +11 -4
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/kernel/artifact.py +4 -1
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/kernel/model.py +12 -2
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/registry/schema.sql +6 -1
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/registry/store.py +117 -34
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/__init__.py +4 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/_text.py +18 -1
- backdraft-0.6.0/src/backdraft/render/cli.py +184 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/footnotes.py +27 -8
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/html/__init__.py +5 -2
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/html/assets.py +73 -16
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/html/components.py +32 -7
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/html/page.py +23 -29
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/html/text.py +27 -0
- backdraft-0.6.0/src/backdraft/render/theme.py +372 -0
- backdraft-0.6.0/src/backdraft/render/themes/default.toml +44 -0
- backdraft-0.6.0/src/backdraft/render/themes/press.toml +28 -0
- backdraft-0.6.0/src/backdraft/render/themes/slate.toml +32 -0
- backdraft-0.6.0/tests/conftest.py +25 -0
- backdraft-0.6.0/tests/css_util.py +66 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/render/backfill.sidecar.json +1 -1
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/render/demo.sidecar.json +1 -1
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_bind_evidence.py +52 -0
- backdraft-0.6.0/tests/test_card_sizing.py +107 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_cli.py +27 -2
- backdraft-0.6.0/tests/test_extract_html.py +249 -0
- backdraft-0.6.0/tests/test_fetch.py +188 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_footnotes.py +48 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_html.py +129 -6
- backdraft-0.6.0/tests/test_ingest_url.py +354 -0
- backdraft-0.6.0/tests/test_render_cli.py +330 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_snapshots.py +1 -1
- backdraft-0.6.0/tests/test_theme.py +283 -0
- backdraft-0.6.0/tests/test_version.py +46 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/uv.lock +1 -1
- backdraft-0.5.0/.github/workflows/site.yml +0 -26
- backdraft-0.5.0/ROADMAP.md +0 -177
- backdraft-0.5.0/src/backdraft/__init__.py +0 -13
- backdraft-0.5.0/src/backdraft/render/cli.py +0 -107
- backdraft-0.5.0/tests/conftest.py +0 -9
- backdraft-0.5.0/tests/test_render_cli.py +0 -161
- {backdraft-0.5.0 → backdraft-0.6.0}/.claude-plugin/marketplace.json +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/.github/workflows/ci.yml +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/.github/workflows/release.yml +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/.gitignore +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/.python-version +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/LICENSE +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/demo/generate_sources.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/demo/memo.md +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/demo/sources/t12-summary.pdf +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/site/privacy.html +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/spec/chunking.md +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/spec/tokens.md +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/__main__.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/bind/binder.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/bind/cli.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/bind/verify/entail.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/cli_context.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/credentials.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/__init__.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/csv.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/docx.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/image.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/pdf_text.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/pptx.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/sheet.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/snapshots.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/vlm.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/vlm_settings.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/xls.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/extract/xlsx.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/gate/cli.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/gate/searcher.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/kernel/chunking.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/registry/__init__.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/registry/ledger.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/html/fmt.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/html/fmt_vectors.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/markdown.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/src/backdraft/render/sidecar.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/conftest_registry.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/conftest_render.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/fakes.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/gate/fake_registry.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/gate/test_gate_cli.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/gate/test_gate_ledger.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/gate/test_gate_reader.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/gate/test_gate_searcher.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/golden_util.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_bind_cli.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_chunking.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_claims.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_credentials.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_extract.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_fmt_parity.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_gate_integration.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_hashing.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_invariants.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_model.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_placement.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_records_location.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_registry.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_reingest.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_render_markdown.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_snapshot_settings.py +0 -0
- {backdraft-0.5.0 → backdraft-0.6.0}/tests/test_tokens.py +0 -0
- {backdraft-0.5.0 → 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
|
+
"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
|
|
@@ -135,6 +135,11 @@ Self-contained HTML (plus the machine-readable sidecar): **quotes + receipts + e
|
|
|
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
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. |
|
|
138
143
|
|
|
139
144
|
## Open
|
|
140
145
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backdraft
|
|
3
|
-
Version: 0.
|
|
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,8 +54,9 @@ 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
|
|
@@ -65,7 +66,7 @@ documents: 0
|
|
|
65
66
|
|
|
66
67
|
$ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
|
|
67
68
|
t12-summary t12-summary.pdf pdf 3 pages
|
|
68
|
-
underwriting-model underwriting-model.xlsx xlsx 2
|
|
69
|
+
underwriting-model underwriting-model.xlsx xlsx 2 sheets
|
|
69
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.
|
|
70
71
|
```
|
|
71
72
|
|
|
@@ -155,6 +156,33 @@ memo.backdraft.html
|
|
|
155
156
|
That file is the whole deliverable: send it over email or Slack and the
|
|
156
157
|
recipient gets the full experience by double-clicking it.
|
|
157
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
|
+
|
|
158
186
|
The full version of this, with every command and every output:
|
|
159
187
|
[`demo/walkthrough.md`](demo/walkthrough.md). The artifact it produces is checked
|
|
160
188
|
in at [`demo/memo.backdraft.html`](demo/memo.backdraft.html).
|
|
@@ -183,6 +211,38 @@ Failures are data throughout: `unresolved`, `not_shown`, `drifted`, `malformed`
|
|
|
183
211
|
and `unmatched` are first-class records in the report and visible sections in the
|
|
184
212
|
artifact. Nothing is ever warned about and dropped.
|
|
185
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
|
+
|
|
186
246
|
## What lives where
|
|
187
247
|
|
|
188
248
|
A working directory stays clean: the authored document and its artifact are
|
|
@@ -29,8 +29,9 @@ 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
|
|
@@ -40,7 +41,7 @@ documents: 0
|
|
|
40
41
|
|
|
41
42
|
$ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
|
|
42
43
|
t12-summary t12-summary.pdf pdf 3 pages
|
|
43
|
-
underwriting-model underwriting-model.xlsx xlsx 2
|
|
44
|
+
underwriting-model underwriting-model.xlsx xlsx 2 sheets
|
|
44
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.
|
|
45
46
|
```
|
|
46
47
|
|
|
@@ -130,6 +131,33 @@ memo.backdraft.html
|
|
|
130
131
|
That file is the whole deliverable: send it over email or Slack and the
|
|
131
132
|
recipient gets the full experience by double-clicking it.
|
|
132
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
|
+
|
|
133
161
|
The full version of this, with every command and every output:
|
|
134
162
|
[`demo/walkthrough.md`](demo/walkthrough.md). The artifact it produces is checked
|
|
135
163
|
in at [`demo/memo.backdraft.html`](demo/memo.backdraft.html).
|
|
@@ -158,6 +186,38 @@ Failures are data throughout: `unresolved`, `not_shown`, `drifted`, `malformed`
|
|
|
158
186
|
and `unmatched` are first-class records in the report and visible sections in the
|
|
159
187
|
artifact. Nothing is ever warned about and dropped.
|
|
160
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
|
+
|
|
161
221
|
## What lives where
|
|
162
222
|
|
|
163
223
|
A working directory stays clean: the authored document and its artifact are
|
|
@@ -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.
|
|
@@ -8,7 +8,7 @@ Design stance: this is a small codebase built as a base platform. A pure kernel
|
|
|
8
8
|
|
|
9
9
|
| Concept | Is | Identity |
|
|
10
10
|
|---|---|---|
|
|
11
|
-
| **Document** | An ingested file | sha256 of bytes; human handle is a `slug
|
|
11
|
+
| **Document** | An ingested file, or a web page snapshotted at ingest | sha256 of bytes; human handle is a `slug`. A fetched page's origin URL is provenance metadata, never identity |
|
|
12
12
|
| **Extraction** | A snapshot of a document's content produced by one extractor run | (document, extractor, version, config); generations are kept, one is `current` |
|
|
13
13
|
| **Page** | Ordered unit within an extraction — a PDF page or a sheet | (extraction, number) |
|
|
14
14
|
| **Anchor** | An addressable location in an extraction, carrying its **receipt**: verbatim snippet + snippet hash | locator within extraction; named by its token |
|
|
@@ -52,6 +52,7 @@ backdraft/
|
|
|
52
52
|
pptx.py # python-pptx; one page per slide, text floor
|
|
53
53
|
text.py # md/txt passthrough
|
|
54
54
|
image.py # an image is a one-page document; VLM transcription
|
|
55
|
+
html.py # html/htm → one page of readable text; a parse, not a readability guess
|
|
55
56
|
vlm.py # pdf→images→VLM per page (deps ship by default; key gates use)
|
|
56
57
|
vlm_settings.py # provider/model/base-url resolution; stdlib-only
|
|
57
58
|
gate/
|
|
@@ -72,9 +73,12 @@ backdraft/
|
|
|
72
73
|
placement.py # locating each claim's span in the document render was handed
|
|
73
74
|
footnotes.py # markdown projection
|
|
74
75
|
sidecar.py # sidecar reader/writer over kernel/artifact.py's format
|
|
76
|
+
theme.py # the artifact's look: TOML in, one CSS override block out
|
|
77
|
+
themes/ # the bundled themes, as the same files a user would write
|
|
75
78
|
_text.py # presentation helpers both renderers share (status prose, elision)
|
|
76
79
|
cli.py # `render` — mounted by the top-level cli
|
|
77
|
-
|
|
80
|
+
fetch.py # the whole network surface: one bounded GET over http(s), stdlib only
|
|
81
|
+
cli.py # typer entry; owns discovery, sessions, the network, and the mounts
|
|
78
82
|
skills/
|
|
79
83
|
backdraft/SKILL.md # front-walk
|
|
80
84
|
backdraft-backfill/SKILL.md
|
|
@@ -134,10 +138,15 @@ CREATE TABLE documents (
|
|
|
134
138
|
sha256 TEXT NOT NULL UNIQUE,
|
|
135
139
|
path TEXT NOT NULL, -- as given at ingest; informational
|
|
136
140
|
filename TEXT NOT NULL,
|
|
137
|
-
media_type TEXT NOT NULL, -- 'pdf' | 'xlsx' | 'xls' | 'csv' | 'docx' | 'pptx' | 'image' | 'text'
|
|
141
|
+
media_type TEXT NOT NULL, -- 'pdf' | 'xlsx' | 'xls' | 'csv' | 'docx' | 'pptx' | 'image' | 'html' | 'text'
|
|
138
142
|
created_at TEXT NOT NULL -- ISO-8601 UTC, everywhere
|
|
139
143
|
);
|
|
140
144
|
|
|
145
|
+
CREATE TABLE document_meta (
|
|
146
|
+
document_id INTEGER PRIMARY KEY REFERENCES documents(id),
|
|
147
|
+
meta TEXT NOT NULL -- JSON provenance; a fetched page carries {url, fetched_at}. Never citation identity.
|
|
148
|
+
);
|
|
149
|
+
|
|
141
150
|
CREATE TABLE extractions (
|
|
142
151
|
id INTEGER PRIMARY KEY,
|
|
143
152
|
document_id INTEGER NOT NULL REFERENCES documents(id),
|
|
@@ -226,7 +235,7 @@ class Extractor(Protocol):
|
|
|
226
235
|
def extract(self, path: Path, config: dict) -> Iterator[ExtractedPage]: ...
|
|
227
236
|
```
|
|
228
237
|
|
|
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.
|
|
238
|
+
Registered in a plain dict; `ingest --extractor auto` picks the first `can_handle`, built-ins ordered `xlsx, xls, csv, docx, pptx, pdf-text, image, html, 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. `html` is one page for the whole document, named by its `<title>`: block elements end a block so the chunker's blank-line rule fires, `script`/`style`/`noscript`/`template`/`svg` are dropped whole, lists become `- `/`1. ` lines in a single block, and tables become pipe tables as `docx` renders them. It is a parse and not a readability heuristic — no boilerplate stripping, since a guess that changes between two versions of a site moves anchors. Decoding is a pure function of the bytes (BOM, then `<meta charset>`, then UTF-8 with replacement), so a saved page and a fetched one extract identically.
|
|
230
239
|
|
|
231
240
|
## Gate (gate/)
|
|
232
241
|
|
|
@@ -273,17 +282,20 @@ All switches **default off** (`--check` opts in). Verdicts are recorded evidence
|
|
|
273
282
|
|
|
274
283
|
- `render <authored.md> --to html` → single self-contained file: the bound doc, click/hover per claim → receipt card (quote, doc, locator, hashes, verdicts, drift diff if any), unresolved list rendered as a visible section, embedded JSON island with the full sidecar + `$format` string + legend (subtext pattern: the artifact teaches its own decoding). No external requests; no page images in v0.
|
|
275
284
|
- `--to footnotes` → plain markdown projection. `--to json` → sidecar alone.
|
|
285
|
+
- `--theme <name|file>` restyles the html artifact — a TOML file of colors, font stacks and heading treatment, resolved by `render/theme.py` into one CSS block emitted *after* the stylesheet (so an unthemed render is byte-identical to one from before themes existed). Precedence: `--theme` > project `.backdraft/theme.toml` > XDG `~/.config/backdraft/theme.toml` > built-in. `backdraft theme list` names the bundled themes and which file is in effect; `theme show <name|file>` prints one, validated, so redirecting `show default` bootstraps a commented starting file and `show ./mine.toml` lints one. Display only: no token, receipt or record moves, and layout is outside the allowlist.
|
|
276
286
|
- Artifact format string: `backdraft/artifact-v1` (prose spec in spec/artifact.md).
|
|
277
287
|
|
|
278
288
|
## CLI (cli.py — typer)
|
|
279
289
|
|
|
280
290
|
```
|
|
281
291
|
backdraft init # create .backdraft/, print status
|
|
282
|
-
backdraft ingest <
|
|
292
|
+
backdraft ingest <sources...> [--extractor auto] [--slug S] [--config k=v]
|
|
293
|
+
# a source is a path or an http(s) URL
|
|
283
294
|
backdraft ls | backdraft read ... # gate, above
|
|
284
295
|
backdraft search "<query>" [--in slug]
|
|
285
296
|
backdraft bind <doc.md> [--session S] [--check ...] [--mode ...]
|
|
286
|
-
backdraft render <doc.md> [--to html|footnotes|json] [-o out]
|
|
297
|
+
backdraft render <doc.md> [--to html|footnotes|json] [-o out] [--theme name|file]
|
|
298
|
+
backdraft theme [list|show <name|file>]
|
|
287
299
|
backdraft export [--out registry.json]
|
|
288
300
|
backdraft session [start|show]
|
|
289
301
|
```
|
|
@@ -323,10 +335,15 @@ class Registry:
|
|
|
323
335
|
|
|
324
336
|
# write side (W1)
|
|
325
337
|
def ingest(self, path: Path, *, extractor: str | None = None,
|
|
326
|
-
slug: str | None = None, config: dict | None = None
|
|
338
|
+
slug: str | None = None, config: dict | None = None,
|
|
339
|
+
url: str | None = None, fetched_at: str | None = None) -> Document: ...
|
|
327
340
|
# runs extractor → new extraction generation, pages, eager anchors, FTS rows.
|
|
328
341
|
# Token carry-over per re-ingest semantics; hash-collision extension steps TOKEN_HASH_LENGTHS.
|
|
329
342
|
# Sheet names sanitized to the sheetref charset here, at ingest.
|
|
343
|
+
# `url` marks `path` as a snapshot staged from the web: the document records the
|
|
344
|
+
# URL as its path, carries {url, fetched_at} as meta, and matches an earlier
|
|
345
|
+
# fetch by URL rather than by the temporary file. Identity stays the bytes.
|
|
346
|
+
# The registry never fetches; `fetch.py` does, and the CLI calls it.
|
|
330
347
|
|
|
331
348
|
# read side (W1 implements; W2/W3 consume)
|
|
332
349
|
def documents(self) -> list[Document]: ...
|
|
@@ -372,4 +389,4 @@ class Registry:
|
|
|
372
389
|
|
|
373
390
|
## Addendum B — CLI assembly
|
|
374
391
|
|
|
375
|
-
Top-level `cli.py` (W1) owns: typer app, registry discovery (nearest `.backdraft/` walking up from cwd; `BACKDRAFT_HOME` override), session resolution (`--session` flag > `BACKDRAFT_SESSION` env > default session), and the `init` / `ingest` / `ls` / `export` / `snapshot-pages` commands. After each ingest, `cli.py` captures page snapshots for any PDF whose extraction carries none — the text-layer path, since `vlm` stores its own — via `extract/snapshots.py` (poppler through pdf2image, `snapshot-pages`' internals). It sits outside the extractor so `pdf-text` stays deterministic regardless of whether poppler is installed, and it is best-effort: a `SnapshotError` leaves ingest at exit 0 with a one-line note naming `snapshot-pages` as the backfill. It mounts sub-apps `gate/cli.py` (W2: `read`, `search`, `session`), `bind/cli.py` (W3: `bind`), `render/cli.py` (W4: `render`) — each exposes `app = typer.Typer()`; the top level mounts each inside a try/except ImportError so partial merges still run. The `backdraft` console script is declared by W1.
|
|
392
|
+
Top-level `cli.py` (W1) owns: typer app, registry discovery (nearest `.backdraft/` walking up from cwd; `BACKDRAFT_HOME` override), session resolution (`--session` flag > `BACKDRAFT_SESSION` env > default session), and the `init` / `ingest` / `ls` / `export` / `snapshot-pages` commands. After each ingest, `cli.py` captures page snapshots for any PDF whose extraction carries none — the text-layer path, since `vlm` stores its own — via `extract/snapshots.py` (poppler through pdf2image, `snapshot-pages`' internals). It sits outside the extractor so `pdf-text` stays deterministic regardless of whether poppler is installed, and it is best-effort: a `SnapshotError` leaves ingest at exit 0 with a one-line note naming `snapshot-pages` as the backfill. `cli.py` also owns the network for the same reason: an `ingest` argument with a scheme is fetched through `fetch.py` (stdlib `urllib`, http and https only, redirects followed, 32 MiB cap) and staged in a temporary file named for the content type the server declared — which is what selects the extractor — so extractors stay pure functions of bytes and the registry opens no sockets. It mounts sub-apps `gate/cli.py` (W2: `read`, `search`, `session`), `bind/cli.py` (W3: `bind`), `render/cli.py` (W4: `render`) — each exposes `app = typer.Typer()`; the top level mounts each inside a try/except ImportError so partial merges still run. The `backdraft` console script is declared by W1.
|