backdraft 0.5.0__tar.gz → 0.7.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.7.0}/.claude-plugin/plugin.json +1 -1
- backdraft-0.7.0/.github/workflows/site.yml +36 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/DESIGN.md +8 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/PKG-INFO +92 -7
- {backdraft-0.5.0 → backdraft-0.7.0}/README.md +90 -5
- backdraft-0.7.0/ROADMAP.md +316 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/SPEC.md +30 -8
- {backdraft-0.5.0 → backdraft-0.7.0}/demo/memo.backdraft.html +38 -19
- {backdraft-0.5.0 → backdraft-0.7.0}/demo/walkthrough.md +71 -5
- {backdraft-0.5.0 → backdraft-0.7.0}/pyproject.toml +1 -1
- {backdraft-0.5.0 → backdraft-0.7.0}/site/demo.html +38 -19
- {backdraft-0.5.0 → backdraft-0.7.0}/site/docs.html +96 -7
- {backdraft-0.5.0 → backdraft-0.7.0}/site/index.html +7 -3
- {backdraft-0.5.0 → backdraft-0.7.0}/site/llms.txt +44 -10
- {backdraft-0.5.0 → backdraft-0.7.0}/skills/backdraft/SKILL.md +52 -12
- {backdraft-0.5.0 → backdraft-0.7.0}/skills/backdraft-artifact/SKILL.md +32 -3
- {backdraft-0.5.0 → backdraft-0.7.0}/skills/backdraft-backfill/SKILL.md +14 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/spec/artifact.md +29 -8
- backdraft-0.7.0/src/backdraft/__init__.py +38 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/bind/binder.py +6 -15
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/bind/cli.py +61 -3
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/bind/evidence.py +27 -8
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/cli.py +142 -37
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/cli_context.py +8 -1
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/base.py +55 -3
- backdraft-0.7.0/src/backdraft/extract/html.py +331 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/image.py +9 -2
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/pdf_text.py +6 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/snapshots.py +37 -2
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/vlm.py +15 -2
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/vlm_settings.py +17 -0
- backdraft-0.7.0/src/backdraft/fetch.py +188 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/gate/__init__.py +7 -5
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/gate/cli.py +35 -2
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/gate/reader.py +166 -6
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/kernel/artifact.py +4 -1
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/kernel/model.py +12 -2
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/registry/__init__.py +2 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/registry/schema.sql +6 -1
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/registry/store.py +154 -35
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/__init__.py +4 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/_text.py +18 -1
- backdraft-0.7.0/src/backdraft/render/cli.py +184 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/footnotes.py +27 -8
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/html/__init__.py +5 -2
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/html/assets.py +73 -16
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/html/components.py +32 -7
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/html/page.py +23 -29
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/html/text.py +27 -0
- backdraft-0.7.0/src/backdraft/render/theme.py +372 -0
- backdraft-0.7.0/src/backdraft/render/themes/default.toml +44 -0
- backdraft-0.7.0/src/backdraft/render/themes/press.toml +28 -0
- backdraft-0.7.0/src/backdraft/render/themes/slate.toml +32 -0
- backdraft-0.7.0/tests/conftest.py +25 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/conftest_registry.py +11 -1
- backdraft-0.7.0/tests/css_util.py +66 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/gate/test_gate_cli.py +42 -0
- backdraft-0.7.0/tests/gate/test_reader_show.py +157 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/render/backfill.sidecar.json +1 -1
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/render/demo.sidecar.json +1 -1
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_bind_cli.py +100 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_bind_evidence.py +52 -0
- backdraft-0.7.0/tests/test_card_sizing.py +107 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_cli.py +144 -5
- backdraft-0.7.0/tests/test_docs.py +44 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_extract.py +90 -1
- backdraft-0.7.0/tests/test_extract_html.py +249 -0
- backdraft-0.7.0/tests/test_fetch.py +188 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_footnotes.py +48 -0
- backdraft-0.7.0/tests/test_gate_integration.py +211 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_html.py +129 -6
- backdraft-0.7.0/tests/test_ingest_url.py +354 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_reingest.py +56 -4
- backdraft-0.7.0/tests/test_render_cli.py +330 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_snapshots.py +15 -1
- backdraft-0.7.0/tests/test_theme.py +283 -0
- backdraft-0.7.0/tests/test_version.py +46 -0
- {backdraft-0.5.0 → backdraft-0.7.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_gate_integration.py +0 -91
- backdraft-0.5.0/tests/test_render_cli.py +0 -161
- {backdraft-0.5.0 → backdraft-0.7.0}/.claude-plugin/marketplace.json +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/.github/workflows/ci.yml +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/.github/workflows/release.yml +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/.gitignore +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/.python-version +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/LICENSE +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/demo/generate_sources.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/demo/memo.md +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/demo/sources/t12-summary.pdf +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/site/privacy.html +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/spec/chunking.md +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/spec/tokens.md +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/__main__.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/bind/verify/entail.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/credentials.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/__init__.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/csv.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/docx.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/pptx.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/sheet.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/xls.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/extract/xlsx.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/gate/searcher.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/kernel/chunking.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/registry/ledger.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/html/fmt.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/html/fmt_vectors.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/markdown.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/src/backdraft/render/sidecar.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/conftest_render.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/fakes.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/gate/fake_registry.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/gate/test_gate_ledger.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/gate/test_gate_reader.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/gate/test_gate_searcher.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/golden_util.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_chunking.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_claims.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_credentials.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_fmt_parity.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_hashing.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_invariants.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_model.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_placement.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_records_location.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_registry.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_render_markdown.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_snapshot_settings.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.0}/tests/test_tokens.py +0 -0
- {backdraft-0.5.0 → backdraft-0.7.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.7.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,14 @@ 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. |
|
|
143
|
+
| 2026-08-11 | **An extractor declares the `--config` keys it reads, and an undeclared key is an error.** `--config dpy=300` used to exit 0, ingest, and change nothing: the config boundary was the one place this repo's "failures are data, never warn-and-drop" rule did not hold, and nothing — help, docs, `llms.txt` — said which keys existed, so learning them meant reading `extract/vlm.py` and `extract/snapshots.py`. The `Extractor` protocol gains an optional `config_keys: Mapping[str, str]` (key → one-line meaning) and `check_config` validates against it. Validation happens **after selection and inside `Registry.ingest`**, not in the CLI: `auto` picks per file, so only the chosen extractor can say whether `dpi` is a render setting or a typo, and putting it at the registry means every substrate is covered rather than only the one with a `--config` flag. It raises `ExtractionError` rather than the `UsageError` a CLI-side check would have used — `cli_context` imports the registry, so the registry cannot import `UsageError` back — and both are `BackdraftError`, so the guard maps either to exit 1 and the user sees the same line. Each key is declared once by the module that already owns its default and reads it: the page-render budget (`dpi`, `snapshot_quality`, `snapshot_max_height`) in `extract/snapshots.py`, the provider surface (`api_key`, `base_url`, `model`, `timeout`, `retries`) in `extract/vlm_settings.py`, and the extractors compose those by reference. `pdf-text` declares the page-render keys despite reading none of them itself — the snapshot capture that runs beside it at ingest does, and a user passing `dpi` to a text-layer ingest means the images; declaring by *what one flag on one ingest affects* beats declaring by which function happens to call `config.get`. Tradeoff accepted: declaring nothing means accepting nothing, so an out-of-tree extractor that reads config without declaring it starts rejecting keys it used to receive. That is the intended direction — a silently ignored key is the failure being fixed — and the fix is one attribute. The help text that lists the keys is pinned by a test against the declarations, so a new key cannot be added in code and left undocumented. |
|
|
144
|
+
| 2026-08-12 | **`show` is a gate verb, not a lookup tool.** `backdraft show <token>` answers what a token says — the inverse of minting, for a token that arrived out of an artifact, a draft, or somebody's message, which nothing in the tool could answer before without rebuilding the slug and selector by hand and re-reading a whole page. It therefore *mints what it shows*, exactly as `read` and `search` do: an agent that has seen a snippet through the gate has genuinely read it, so recording it is honest rather than a loophole, and `not_shown` stays true by construction — a status `show` can never produce. That makes a token laundering into a citable one by being looked at, which is the intended behavior and the reason this is in `gate/` and not somewhere neutral. Resolution is bind's path rather than a second reading of what a token means: `kernel.claims.parse_citation` decides `malformed` (so the reserved `bd:calc(...)` form lands identically in both) and `Registry.resolve` decides the rest, which is what makes a status printed here the status bind would print. Two smaller calls. `drifted` prints both snippets *and* mints the anchor standing at the locator now, since that anchor's token is the one worth citing and printing it unrecorded would be the under-minting the gate must never do. And drift does **not** fail the exit code: exit 1 covers `unresolved` and `malformed`, the two statuses where there was nothing to show, because whether a changed source still supports a sentence is bind's question about a document, not a lookup's about a token. Tradeoff accepted: auditing someone else's artifact against a registry now writes ledger rows into the auditor's session, so `skills/backdraft-artifact/SKILL.md` says so and keeps that check reported separately from the artifact's self-checks. |
|
|
145
|
+
| 2026-08-13 | **`ingest` finishes its list; an unreadable source is data, not the end of the run.** `backdraft ingest a.md broken.pdf c.md` used to ingest `a.md`, raise on `broken.pdf`, and never attempt `c.md` — leaving a half-built registry, a message naming only the first failure, and no way to tell which sources landed from which were never tried, so ingesting a folder meant re-running and inferring the state. The loop now catches `BackdraftError` per source and records `(source, reason)`, so every source is attempted, and prints one `!` line per failure under a count (`2 of 3 sources ingested; 1 failed`) whose two numbers add to the whole list — which is precisely the fact the old behavior could not state. This is principle 4 ("failures are data, never warn-and-drop") applied at the boundary it had not reached, and the same shape the page-snapshot note already uses one level down. Grouping differs deliberately: the snapshot note groups by *reason* because one missing poppler explains every document at once, while this lists by *source*, because the source is what the caller has to go fix. The exit code is unchanged at 1 and `guard` remains the only place a `BackdraftError` becomes one — nothing caught per source is re-raised, and the report exits through `fail` like any other error line, on stderr, so a clean run's stdout is byte-identical to before and that is pinned by a test. Tradeoff accepted: a run that fails partway now leaves *more* in the registry than it used to, so "the command failed" no longer implies "nothing happened". The closing line of the report answers that directly — re-running the same list is safe, because a source already ingested with unchanged bytes, extractor and config is a no-op — which is cheaper for a caller than reconstructing the failed half by diffing `ls` against its own arguments. |
|
|
138
146
|
|
|
139
147
|
## Open
|
|
140
148
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: backdraft
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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,11 @@ 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. A source that
|
|
60
|
+
cannot be read does not end the run: the rest of the list is ingested anyway,
|
|
61
|
+
and the command exits 1 naming each source that failed and why.
|
|
59
62
|
|
|
60
63
|
```console
|
|
61
64
|
$ backdraft init
|
|
@@ -65,7 +68,7 @@ documents: 0
|
|
|
65
68
|
|
|
66
69
|
$ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
|
|
67
70
|
t12-summary t12-summary.pdf pdf 3 pages
|
|
68
|
-
underwriting-model underwriting-model.xlsx xlsx 2
|
|
71
|
+
underwriting-model underwriting-model.xlsx xlsx 2 sheets
|
|
69
72
|
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
73
|
```
|
|
71
74
|
|
|
@@ -116,6 +119,25 @@ $ backdraft cell underwriting-model "assumptions!B10"
|
|
|
116
119
|
[bd:underwriting-model:assumptions!B10:964a] 24850000
|
|
117
120
|
```
|
|
118
121
|
|
|
122
|
+
And a token you were handed — out of an artifact, a draft, someone else's
|
|
123
|
+
message — runs backwards to what it actually says:
|
|
124
|
+
|
|
125
|
+
```console
|
|
126
|
+
$ backdraft show bd:t12-summary:p1.c1:c2e8
|
|
127
|
+
[bd:t12-summary:p1.c1:c2e8] resolved t12-summary p1.c1
|
|
128
|
+
Bridgeview Commons — Trailing Twelve Month Summary
|
|
129
|
+
|
|
130
|
+
Property: Bridgeview Commons, 4400 Halsted Avenue, Columbus, OH 43214. 128 units across four three-story
|
|
131
|
+
garden buildings on 6.2 acres, built in 1998 and substantially renovated between 2019 and 2021. …
|
|
132
|
+
|
|
133
|
+
[Read the page: backdraft read t12-summary p1]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`show` is the gate too, so what it shows is minted and citable. A token whose
|
|
137
|
+
source has changed since prints both snippets, the one that was cited and the
|
|
138
|
+
one standing there now; a token that names nothing exits 1 and says whether the
|
|
139
|
+
slug or the locator is the wrong half.
|
|
140
|
+
|
|
119
141
|
**Cite** by writing the token as the href of a markdown link on the claim span.
|
|
120
142
|
Multiple citations are `;`-separated in one href.
|
|
121
143
|
|
|
@@ -133,7 +155,7 @@ bound 15 claim(s), 16 citation(s) [frontwalk]
|
|
|
133
155
|
unresolved: 1
|
|
134
156
|
overlap: pass 11, skip 4
|
|
135
157
|
value-trace: pass 15
|
|
136
|
-
! unresolved: bd:t12-summary:p4.c1:1a2b
|
|
158
|
+
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @2900
|
|
137
159
|
wrote .backdraft/records/memo.backdraft.json
|
|
138
160
|
|
|
139
161
|
$ echo $?
|
|
@@ -155,6 +177,33 @@ memo.backdraft.html
|
|
|
155
177
|
That file is the whole deliverable: send it over email or Slack and the
|
|
156
178
|
recipient gets the full experience by double-clicking it.
|
|
157
179
|
|
|
180
|
+
**Theming.** The artifact ships with a default look and two alternates,
|
|
181
|
+
`press` and `slate`:
|
|
182
|
+
|
|
183
|
+
```console
|
|
184
|
+
$ backdraft render memo.md --theme slate
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A theme is a small TOML file naming colors, font stacks and heading treatment.
|
|
188
|
+
Drop one at `~/.config/backdraft/theme.toml` and every artifact you render, in
|
|
189
|
+
any project, honors it without a flag; `.backdraft/theme.toml` overrides that
|
|
190
|
+
for one project, and `--theme` overrides both. Themes are display only — they
|
|
191
|
+
never touch a token, a receipt or the record.
|
|
192
|
+
|
|
193
|
+
```console
|
|
194
|
+
$ backdraft theme list
|
|
195
|
+
default
|
|
196
|
+
press
|
|
197
|
+
slate
|
|
198
|
+
|
|
199
|
+
in effect here: the built-in look
|
|
200
|
+
[Start your own: backdraft theme show default > /Users/you/.config/backdraft/theme.toml]
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`theme show` prints a validated file, so redirecting it writes a fully
|
|
204
|
+
commented starting point with every key and what it paints — and pointing it
|
|
205
|
+
at a file of your own checks that file without rendering anything.
|
|
206
|
+
|
|
158
207
|
The full version of this, with every command and every output:
|
|
159
208
|
[`demo/walkthrough.md`](demo/walkthrough.md). The artifact it produces is checked
|
|
160
209
|
in at [`demo/memo.backdraft.html`](demo/memo.backdraft.html).
|
|
@@ -183,6 +232,38 @@ Failures are data throughout: `unresolved`, `not_shown`, `drifted`, `malformed`
|
|
|
183
232
|
and `unmatched` are first-class records in the report and visible sections in the
|
|
184
233
|
artifact. Nothing is ever warned about and dropped.
|
|
185
234
|
|
|
235
|
+
## Web sources
|
|
236
|
+
|
|
237
|
+
`backdraft ingest https://example.com/q4-results` fetches the page once and
|
|
238
|
+
snapshots it exactly as a file: the bytes at fetch time are the document's
|
|
239
|
+
identity, and the URL and fetch timestamp ride along as provenance. So a page
|
|
240
|
+
that has changed since you cited it comes back as a new generation of the same
|
|
241
|
+
document and the old citations report `drifted` — the same machinery as an
|
|
242
|
+
edited PDF, which is the point of hashing the snapshot rather than trusting the
|
|
243
|
+
address.
|
|
244
|
+
|
|
245
|
+
```console
|
|
246
|
+
$ backdraft ingest https://example.com/reports/q4-2025
|
|
247
|
+
q4-2025 q4-2025.html html 1 page
|
|
248
|
+
|
|
249
|
+
$ backdraft ls
|
|
250
|
+
q4-2025 q4-2025.html html 1 page https://example.com/reports/q4-2025
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The origin travels into the artifact too. A claim citing a fetched page shows
|
|
254
|
+
the URL as a link on its receipt, with the date the bytes were taken, and the
|
|
255
|
+
source list names the page rather than the staged filename. That pointer is the
|
|
256
|
+
half of citing a web page a frozen receipt cannot answer on its own: the
|
|
257
|
+
snippet says what the page said, the link is how a reader asks whether it still
|
|
258
|
+
says it.
|
|
259
|
+
|
|
260
|
+
What it does not do, stated rather than worked around: JavaScript-rendered
|
|
261
|
+
pages give you whatever the server sends to a plain GET, pages behind a login
|
|
262
|
+
are out of reach, and the extractor is a parse rather than a readability guess
|
|
263
|
+
— navigation and footers are part of the page, because a heuristic that changes
|
|
264
|
+
its mind between two versions of a site would move anchors. Responses are
|
|
265
|
+
capped at 32 MiB.
|
|
266
|
+
|
|
186
267
|
## What lives where
|
|
187
268
|
|
|
188
269
|
A working directory stays clean: the authored document and its artifact are
|
|
@@ -209,7 +290,11 @@ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
|
|
|
209
290
|
`OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic
|
|
210
291
|
key in the environment is not consent to send documents to its provider. The
|
|
211
292
|
default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
|
|
212
|
-
base_url + model, set explicitly
|
|
293
|
+
base_url + model, set explicitly (`--config base_url=...` and `--config
|
|
294
|
+
model=...`, one key per flag). Every `--config` key is declared by the extractor
|
|
295
|
+
that reads it and checked against the one `ingest` chose, so a misspelled key
|
|
296
|
+
fails and names the keys that apply rather than being silently ignored.
|
|
297
|
+
Without a scoped key, `auto` falls back to
|
|
213
298
|
the keyless text layer and says so — still storing each page's image, which it
|
|
214
299
|
renders locally. PDF page rendering needs poppler, installed separately; without
|
|
215
300
|
it ingest succeeds without the images and names `snapshot-pages` as the backfill.
|
|
@@ -29,8 +29,11 @@ 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. A source that
|
|
35
|
+
cannot be read does not end the run: the rest of the list is ingested anyway,
|
|
36
|
+
and the command exits 1 naming each source that failed and why.
|
|
34
37
|
|
|
35
38
|
```console
|
|
36
39
|
$ backdraft init
|
|
@@ -40,7 +43,7 @@ documents: 0
|
|
|
40
43
|
|
|
41
44
|
$ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
|
|
42
45
|
t12-summary t12-summary.pdf pdf 3 pages
|
|
43
|
-
underwriting-model underwriting-model.xlsx xlsx 2
|
|
46
|
+
underwriting-model underwriting-model.xlsx xlsx 2 sheets
|
|
44
47
|
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
48
|
```
|
|
46
49
|
|
|
@@ -91,6 +94,25 @@ $ backdraft cell underwriting-model "assumptions!B10"
|
|
|
91
94
|
[bd:underwriting-model:assumptions!B10:964a] 24850000
|
|
92
95
|
```
|
|
93
96
|
|
|
97
|
+
And a token you were handed — out of an artifact, a draft, someone else's
|
|
98
|
+
message — runs backwards to what it actually says:
|
|
99
|
+
|
|
100
|
+
```console
|
|
101
|
+
$ backdraft show bd:t12-summary:p1.c1:c2e8
|
|
102
|
+
[bd:t12-summary:p1.c1:c2e8] resolved t12-summary p1.c1
|
|
103
|
+
Bridgeview Commons — Trailing Twelve Month Summary
|
|
104
|
+
|
|
105
|
+
Property: Bridgeview Commons, 4400 Halsted Avenue, Columbus, OH 43214. 128 units across four three-story
|
|
106
|
+
garden buildings on 6.2 acres, built in 1998 and substantially renovated between 2019 and 2021. …
|
|
107
|
+
|
|
108
|
+
[Read the page: backdraft read t12-summary p1]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`show` is the gate too, so what it shows is minted and citable. A token whose
|
|
112
|
+
source has changed since prints both snippets, the one that was cited and the
|
|
113
|
+
one standing there now; a token that names nothing exits 1 and says whether the
|
|
114
|
+
slug or the locator is the wrong half.
|
|
115
|
+
|
|
94
116
|
**Cite** by writing the token as the href of a markdown link on the claim span.
|
|
95
117
|
Multiple citations are `;`-separated in one href.
|
|
96
118
|
|
|
@@ -108,7 +130,7 @@ bound 15 claim(s), 16 citation(s) [frontwalk]
|
|
|
108
130
|
unresolved: 1
|
|
109
131
|
overlap: pass 11, skip 4
|
|
110
132
|
value-trace: pass 15
|
|
111
|
-
! unresolved: bd:t12-summary:p4.c1:1a2b
|
|
133
|
+
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @2900
|
|
112
134
|
wrote .backdraft/records/memo.backdraft.json
|
|
113
135
|
|
|
114
136
|
$ echo $?
|
|
@@ -130,6 +152,33 @@ memo.backdraft.html
|
|
|
130
152
|
That file is the whole deliverable: send it over email or Slack and the
|
|
131
153
|
recipient gets the full experience by double-clicking it.
|
|
132
154
|
|
|
155
|
+
**Theming.** The artifact ships with a default look and two alternates,
|
|
156
|
+
`press` and `slate`:
|
|
157
|
+
|
|
158
|
+
```console
|
|
159
|
+
$ backdraft render memo.md --theme slate
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
A theme is a small TOML file naming colors, font stacks and heading treatment.
|
|
163
|
+
Drop one at `~/.config/backdraft/theme.toml` and every artifact you render, in
|
|
164
|
+
any project, honors it without a flag; `.backdraft/theme.toml` overrides that
|
|
165
|
+
for one project, and `--theme` overrides both. Themes are display only — they
|
|
166
|
+
never touch a token, a receipt or the record.
|
|
167
|
+
|
|
168
|
+
```console
|
|
169
|
+
$ backdraft theme list
|
|
170
|
+
default
|
|
171
|
+
press
|
|
172
|
+
slate
|
|
173
|
+
|
|
174
|
+
in effect here: the built-in look
|
|
175
|
+
[Start your own: backdraft theme show default > /Users/you/.config/backdraft/theme.toml]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`theme show` prints a validated file, so redirecting it writes a fully
|
|
179
|
+
commented starting point with every key and what it paints — and pointing it
|
|
180
|
+
at a file of your own checks that file without rendering anything.
|
|
181
|
+
|
|
133
182
|
The full version of this, with every command and every output:
|
|
134
183
|
[`demo/walkthrough.md`](demo/walkthrough.md). The artifact it produces is checked
|
|
135
184
|
in at [`demo/memo.backdraft.html`](demo/memo.backdraft.html).
|
|
@@ -158,6 +207,38 @@ Failures are data throughout: `unresolved`, `not_shown`, `drifted`, `malformed`
|
|
|
158
207
|
and `unmatched` are first-class records in the report and visible sections in the
|
|
159
208
|
artifact. Nothing is ever warned about and dropped.
|
|
160
209
|
|
|
210
|
+
## Web sources
|
|
211
|
+
|
|
212
|
+
`backdraft ingest https://example.com/q4-results` fetches the page once and
|
|
213
|
+
snapshots it exactly as a file: the bytes at fetch time are the document's
|
|
214
|
+
identity, and the URL and fetch timestamp ride along as provenance. So a page
|
|
215
|
+
that has changed since you cited it comes back as a new generation of the same
|
|
216
|
+
document and the old citations report `drifted` — the same machinery as an
|
|
217
|
+
edited PDF, which is the point of hashing the snapshot rather than trusting the
|
|
218
|
+
address.
|
|
219
|
+
|
|
220
|
+
```console
|
|
221
|
+
$ backdraft ingest https://example.com/reports/q4-2025
|
|
222
|
+
q4-2025 q4-2025.html html 1 page
|
|
223
|
+
|
|
224
|
+
$ backdraft ls
|
|
225
|
+
q4-2025 q4-2025.html html 1 page https://example.com/reports/q4-2025
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The origin travels into the artifact too. A claim citing a fetched page shows
|
|
229
|
+
the URL as a link on its receipt, with the date the bytes were taken, and the
|
|
230
|
+
source list names the page rather than the staged filename. That pointer is the
|
|
231
|
+
half of citing a web page a frozen receipt cannot answer on its own: the
|
|
232
|
+
snippet says what the page said, the link is how a reader asks whether it still
|
|
233
|
+
says it.
|
|
234
|
+
|
|
235
|
+
What it does not do, stated rather than worked around: JavaScript-rendered
|
|
236
|
+
pages give you whatever the server sends to a plain GET, pages behind a login
|
|
237
|
+
are out of reach, and the extractor is a parse rather than a readability guess
|
|
238
|
+
— navigation and footers are part of the page, because a heuristic that changes
|
|
239
|
+
its mind between two versions of a site would move anchors. Responses are
|
|
240
|
+
capped at 32 MiB.
|
|
241
|
+
|
|
161
242
|
## What lives where
|
|
162
243
|
|
|
163
244
|
A working directory stays clean: the authored document and its artifact are
|
|
@@ -184,7 +265,11 @@ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
|
|
|
184
265
|
`OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic
|
|
185
266
|
key in the environment is not consent to send documents to its provider. The
|
|
186
267
|
default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
|
|
187
|
-
base_url + model, set explicitly
|
|
268
|
+
base_url + model, set explicitly (`--config base_url=...` and `--config
|
|
269
|
+
model=...`, one key per flag). Every `--config` key is declared by the extractor
|
|
270
|
+
that reads it and checked against the one `ingest` chose, so a misspelled key
|
|
271
|
+
fails and names the keys that apply rather than being silently ignored.
|
|
272
|
+
Without a scoped key, `auto` falls back to
|
|
188
273
|
the keyless text layer and says so — still storing each page's image, which it
|
|
189
274
|
renders locally. PDF page rendering needs poppler, installed separately; without
|
|
190
275
|
it ingest succeeds without the images and names `snapshot-pages` as the backfill.
|