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