backdraft 0.6.0__tar.gz → 0.8.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.6.0 → backdraft-0.8.0}/.claude-plugin/plugin.json +1 -1
- {backdraft-0.6.0 → backdraft-0.8.0}/DESIGN.md +7 -2
- {backdraft-0.6.0 → backdraft-0.8.0}/PKG-INFO +64 -18
- {backdraft-0.6.0 → backdraft-0.8.0}/README.md +62 -16
- backdraft-0.8.0/ROADMAP.md +340 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/SPEC.md +13 -4
- {backdraft-0.6.0 → backdraft-0.8.0}/demo/memo.backdraft.html +162 -85
- {backdraft-0.6.0 → backdraft-0.8.0}/demo/memo.md +14 -1
- {backdraft-0.6.0 → backdraft-0.8.0}/demo/walkthrough.md +149 -30
- {backdraft-0.6.0 → backdraft-0.8.0}/pyproject.toml +1 -1
- {backdraft-0.6.0 → backdraft-0.8.0}/site/demo.html +162 -85
- {backdraft-0.6.0 → backdraft-0.8.0}/site/docs.html +35 -8
- {backdraft-0.6.0 → backdraft-0.8.0}/site/llms.txt +52 -16
- {backdraft-0.6.0 → backdraft-0.8.0}/skills/backdraft/SKILL.md +76 -22
- {backdraft-0.6.0 → backdraft-0.8.0}/skills/backdraft-artifact/SKILL.md +30 -5
- {backdraft-0.6.0 → backdraft-0.8.0}/skills/backdraft-backfill/SKILL.md +15 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/bind/binder.py +6 -15
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/bind/cli.py +61 -3
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/cli.py +201 -40
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/cli_context.py +8 -1
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/base.py +53 -2
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/image.py +9 -2
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/pdf_text.py +6 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/snapshots.py +37 -2
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/vlm.py +15 -2
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/vlm_settings.py +17 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/fetch.py +59 -6
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/gate/__init__.py +7 -5
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/gate/cli.py +35 -2
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/gate/reader.py +207 -8
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/registry/__init__.py +10 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/registry/store.py +110 -6
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/html/text.py +8 -1
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/conftest_registry.py +11 -1
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/gate/fake_registry.py +10 -1
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/gate/test_gate_cli.py +42 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/gate/test_gate_reader.py +89 -0
- backdraft-0.8.0/tests/gate/test_reader_show.py +157 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_bind_cli.py +100 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_cli.py +300 -3
- backdraft-0.8.0/tests/test_docs.py +133 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_extract.py +90 -1
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_fetch.py +50 -0
- backdraft-0.8.0/tests/test_gate_integration.py +211 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_html.py +14 -1
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_ingest_url.py +95 -8
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_registry.py +38 -2
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_reingest.py +67 -5
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_snapshots.py +14 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/uv.lock +1 -1
- backdraft-0.6.0/ROADMAP.md +0 -258
- backdraft-0.6.0/tests/test_gate_integration.py +0 -91
- {backdraft-0.6.0 → backdraft-0.8.0}/.claude-plugin/marketplace.json +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/.github/workflows/ci.yml +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/.github/workflows/release.yml +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/.github/workflows/site.yml +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/.gitignore +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/.python-version +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/LICENSE +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/demo/generate_sources.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/demo/sources/t12-summary.pdf +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/site/index.html +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/site/privacy.html +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/spec/artifact.md +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/spec/chunking.md +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/spec/tokens.md +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/__main__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/bind/evidence.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/bind/verify/entail.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/credentials.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/csv.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/docx.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/html.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/pptx.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/sheet.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/xls.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/extract/xlsx.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/gate/searcher.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/kernel/artifact.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/kernel/chunking.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/kernel/model.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/registry/ledger.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/registry/schema.sql +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/_text.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/cli.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/footnotes.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/html/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/html/assets.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/html/components.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/html/fmt.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/html/fmt_vectors.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/html/page.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/markdown.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/sidecar.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/theme.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/themes/default.toml +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/themes/press.toml +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/src/backdraft/render/themes/slate.toml +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/conftest.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/conftest_render.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/css_util.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/fakes.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/gate/test_gate_ledger.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/gate/test_gate_searcher.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/render/backfill.sidecar.json +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden/render/demo.sidecar.json +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/golden_util.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_bind_evidence.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_card_sizing.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_chunking.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_claims.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_credentials.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_extract_html.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_fmt_parity.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_footnotes.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_hashing.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_invariants.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_model.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_placement.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_records_location.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_render_cli.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_render_markdown.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_snapshot_settings.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_theme.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_tokens.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_value_trace.py +0 -0
- {backdraft-0.6.0 → backdraft-0.8.0}/tests/test_version.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backdraft",
|
|
3
3
|
"displayName": "Backdraft",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.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",
|
|
@@ -140,6 +140,13 @@ Self-contained HTML (plus the machine-readable sidecar): **quotes + receipts + e
|
|
|
140
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
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
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. |
|
|
146
|
+
| 2026-08-17 | **The demo cites a web page at a fixed revision, so the capability is visible and stays `resolved`.** URL sources shipped whole — fetch, snapshot, `url`/`fetched_at`, the artifact's live dated link — and none of it appeared at backdraft.dev/demo.html, because both demo sources were local files and the byte-identity test that proved the artifact change safe was the same fact that kept it invisible. The demo gains a third source: the Wikipedia article for Franklin County, Ohio — the county the fictional property sits in and the one its tax appeal is already pending before — cited for two market figures a T12 cannot supply, the county's population and its renter-occupied share. It is ingested at Wikipedia's **permanent link** (`?oldid=<rev>`) rather than the bare article URL. What that gives up is named: the demo no longer shows a live page changing under a citation, which is the better story and belongs in a `drifted` walkthrough rather than the shop window. What it buys is that the front page does not permanently display `drifted` within days of the next edit — a demo that always shows drift teaches that the tool is broken rather than that the source moved — and an immutable revision plus the rendered "as of" date is what a careful analyst cites anyway. The same reasoning is now advice rather than a one-off: the docs, `llms.txt` and the writing skill tell an agent to prefer a fixed-revision address (`?oldid=`, a DOI, an archived snapshot) and to pass `--slug` with it. `--slug` is load-bearing here, not decoration: a permanent link is served from `/w/index.php`, so the default stem would be `index` and every token in the memo would carry it. That is the queued slug-assignment item's problem and is fixed at the fetch layer there, not worked around here. One renderer change came with it. `source_title` took a source's display name from its filename's stem, which for a fetched page is the staging name `fetch.filename_for` invented — so the article titled itself "Index". Where a document carries a URL the slug is the name instead, which is the 2026-08-06 row's rule ("showing both would let the fictional one look authoritative") applied to the one surface that had kept the invented name. It changes nothing for a page ingested without `--slug`, since the slug is derived from that same stem; it defers to whoever named the document when someone did. |
|
|
147
|
+
| 2026-08-18 | **One rule names a source, and every surface that names one uses it.** A fetched page's `filename` is the temporary file `fetch.filename_for` staged the bytes in — `index.html` for a permanent link, a name on nobody's disk — and the 2026-08-06 row settled what to do about it ("showing both would give a reader two names for one thing and let the fictional one look authoritative") for the artifact alone. The four CLI surfaces that name a document kept the fiction: `ingest`'s line, the gate's document list, its table-of-contents headline, and `ls` — which showed *both*, filename in column two and URL in a fifth field, the exact shape that row rejected. `gate.source_name` is now the single owner, exported the way `gate.unit` is and imported downward by `cli.py`, so a writing agent learns a source's provenance from the surface it already lives on instead of being sent to `ls` by the skill. Display only: the slug, the token and everything a citation resolves through are untouched, which is the 2026-08-05 "provenance, never identity" rule. Two consequences named rather than discovered. `ls` loses a field — a fetched row is four columns, not five — so anything parsing column five stops seeing one; that is the point, since column two was a lie and one true name beats two names of which one is fictional. And in the gate's aligned list a URL *overflows* the name column rather than widening it: the column is sized on filenames alone, so one 80-character address pushes its own row's later columns right instead of pushing every row's past column 100. The cost is a ragged right on fetched rows, accepted because the file rows a mostly-file registry is made of stay byte-identical to what they printed before — pinned by a test, the way the artifact's byte-identity is. `bind`'s unmatched-claim report still names a source by filename: the fix cannot be shared into it without a sideways `bind` → `gate` import, which SPEC § Dependency rule forbids, and duplicating the rule there is the reinvention this row exists to end. |
|
|
148
|
+
| 2026-08-19 | **A slug is assigned by a rule, and the rule is written down.** The Open list carried "slug assignment/collision rules" from the first field trial, and URL sources sharpened it into a live failure: `fetch.filename_for` took the stem from a URL's last path segment, so `https://a.example/reports/2025/index.html` and `https://b.example/docs/index.html` both staged as `index.html` and became `index` and `index-2` — two handles that say which is which to nobody, carried by every token in every document written against them, permanently, since a slug is stable once assigned. The rule now has two clauses and they are stated together for files and URLs. A **file** is named by its filename's stem, kebabed and cut to 32 characters, which is unchanged and was never the problem. A **URL** is named by its last path segment, *except* when that segment names the site's plumbing rather than its page — a directory index or generic handler (`_GENERIC_STEMS`: `index`, `view`, `page`, `show`, and a dozen more), an all-digit id, or no segment at all — in which case the host goes in front of it, minus `www.` and minus the port, so the two pages above become `a-example-index` and `b-example-index`. The common case is deliberately untouched: `https://example.com/reports/q4-2025` is still `q4-2025`, because a rule that made every good name uglier to fix the bad ones would be the wrong trade. `_dedupe`'s `-2` stays, demoted from the normal outcome to the backstop it was meant to be — the host is a *better* stem, not a unique one, and two `index.html` pages on one site still collide. Truncation is trimmed on both cuts now (`slug_for` and `_dedupe`), which is the other half of the Open line: a 32-character cut lands on a hyphen often enough that `q4-report-` was a slug this repo could produce, legal in the grammar and reading like a bug. The compatibility statement, said out loud because nothing else will say it: two registries built on either side of this change disagree about a fetched page's slug, and an artifact's tokens are not portable between them. Existing registries are unaffected, and that is not luck — `_find_document` matches a re-fetch on the URL, never the slug, so re-ingesting a page keeps the slug it already has. `--slug` remains the way to overrule all of this and is still the advice for a fetched page worth citing, because `a-example-index` names a site rather than nothing, which is better and is not good. |
|
|
149
|
+
| 2026-08-20 | **`ingest` says which of its three outcomes happened, and how much text came out.** One line served three different events — a document created, a *new generation* of one whose bytes moved, and a no-op that wrote nothing — so an agent re-ingesting after a fix could not tell whether anything had changed, at exactly the moment it most needed to: a new generation is when citations into the previous snapshot begin resolving to a superseded one and `bind` starts calling them `drifted`. The registry already distinguished all three internally (`_find_document`, `_is_noop`, `_upsert_document`) and threw the answer away, so `ingest` now returns an `Ingested` — a `Document` subclass carrying `outcome`, the shape `SearchResults` established for `search`: Addendum A's pinned `-> Document` still holds, every `.slug` caller and every fake is untouched, and the one consumer that cares reads the extra field. Re-deriving the outcome in the CLI was the alternative and would have meant a second implementation of `_is_noop` living away from the first. The wart is named rather than papered over: a dataclass compares by class, so an `Ingested` never equals the plain `Document` the read side returns for the same row, and the two tests that compared them now compare what persists. The second half is a number: the extracted character count, beside the page count on every line. A scanned PDF with no text layer and a page behind a login both ingest cleanly, exit 0 and print a page count like any success, which is how a shell of a source gets cited — and the writing skill's instruction to notice "thin or empty" gave the agent nothing to notice it with. Under `THIN_SOURCE_CHARS` (200) a note names the likely cause by media type and what to do about it. That threshold is a heuristic, which this repo refuses elsewhere on purpose — the `html` extractor is a parse and not a readability guess, because a heuristic that changes its mind moves anchors. This one is admissible for the reason those are not: it is display only, at exit 0, and no token, anchor, status or byte of the artifact derives from it, so being wrong costs one line of advice and never a citation. `chars` counts sheets too, where `read`'s window unit is rows: this is the extraction's volume, not a window into it. Grouping follows the 2026-08-13 rule — thin sources group by cause, since one missing text layer explains every scan at once and each document's own count is already on its own line, while the failure report still lists by source. |
|
|
143
150
|
|
|
144
151
|
## Open
|
|
145
152
|
|
|
@@ -147,8 +154,6 @@ Queued and parked work lives in ROADMAP.md; this list is questions still
|
|
|
147
154
|
awaiting a decision:
|
|
148
155
|
|
|
149
156
|
- Re-bind/orphan pass on re-ingest of changed docs (chunk ordinal drift)
|
|
150
|
-
- Slug assignment/collision rules (the trailing-hyphen truncation from the
|
|
151
|
-
first field trial lives here)
|
|
152
157
|
- Distribution: published to PyPI as `backdraft` 0.2.0 (2026-07-28); domain
|
|
153
158
|
backdraft.dev acquired; still open: skill registration, deploying site/
|
|
154
159
|
- First external user (design-partner validation layer vs eval substrate vs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: backdraft
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -56,7 +56,9 @@ cd demo
|
|
|
56
56
|
cells for spreadsheet sheets. Formats: PDF, XLSX/XLSM, XLS, CSV/TSV, DOCX,
|
|
57
57
|
PPTX, HTML, images (png, jpeg, tiff, through the vision model), plain text and
|
|
58
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
|
+
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.
|
|
60
62
|
|
|
61
63
|
```console
|
|
62
64
|
$ backdraft init
|
|
@@ -65,11 +67,27 @@ documents: 0
|
|
|
65
67
|
… plus where settings live and what to do next
|
|
66
68
|
|
|
67
69
|
$ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
|
|
68
|
-
t12-summary t12-summary.pdf pdf 3 pages
|
|
69
|
-
underwriting-model underwriting-model.xlsx xlsx 2 sheets
|
|
70
|
+
t12-summary t12-summary.pdf pdf 3 pages 8865 chars
|
|
71
|
+
underwriting-model underwriting-model.xlsx xlsx 2 sheets 2357 chars
|
|
70
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.
|
|
73
|
+
|
|
74
|
+
$ backdraft ingest "https://en.wikipedia.org/w/index.php?title=Franklin_County,_Ohio&oldid=1367935775" --slug franklin-county
|
|
75
|
+
franklin-county https://en.wikipedia.org/w/index.php?title=Franklin_County,_Ohio&oldid=1367935775 html 1 page 34141 chars
|
|
71
76
|
```
|
|
72
77
|
|
|
78
|
+
The demo cites that page for market context. It is Wikipedia's *permanent
|
|
79
|
+
link* — `?oldid=` serves one revision's bytes forever — because a citation into
|
|
80
|
+
a live article reports `drifted` the moment somebody edits it, and the quoted
|
|
81
|
+
sentence would no longer be the sentence there.
|
|
82
|
+
|
|
83
|
+
Every ingest line ends with how much text came out, and says which of three
|
|
84
|
+
things happened: a document created, a **new generation** of one whose bytes
|
|
85
|
+
moved — which is when citations into the previous snapshot start reporting
|
|
86
|
+
`drifted` — or `unchanged`, a no-op because re-running would reproduce what is
|
|
87
|
+
already there. When almost no text came out, a note names the likely cause (a
|
|
88
|
+
scan with no text layer, a page behind a login) and what to do; it is a note at
|
|
89
|
+
exit 0, because a thin snapshot is still a real one.
|
|
90
|
+
|
|
73
91
|
**Read.** The page arrives with a citable name over each chunk. This is the whole
|
|
74
92
|
mechanism: what you can cite is exactly what you were shown.
|
|
75
93
|
|
|
@@ -117,6 +135,25 @@ $ backdraft cell underwriting-model "assumptions!B10"
|
|
|
117
135
|
[bd:underwriting-model:assumptions!B10:964a] 24850000
|
|
118
136
|
```
|
|
119
137
|
|
|
138
|
+
And a token you were handed — out of an artifact, a draft, someone else's
|
|
139
|
+
message — runs backwards to what it actually says:
|
|
140
|
+
|
|
141
|
+
```console
|
|
142
|
+
$ backdraft show bd:t12-summary:p1.c1:c2e8
|
|
143
|
+
[bd:t12-summary:p1.c1:c2e8] resolved t12-summary p1.c1
|
|
144
|
+
Bridgeview Commons — Trailing Twelve Month Summary
|
|
145
|
+
|
|
146
|
+
Property: Bridgeview Commons, 4400 Halsted Avenue, Columbus, OH 43214. 128 units across four three-story
|
|
147
|
+
garden buildings on 6.2 acres, built in 1998 and substantially renovated between 2019 and 2021. …
|
|
148
|
+
|
|
149
|
+
[Read the page: backdraft read t12-summary p1]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`show` is the gate too, so what it shows is minted and citable. A token whose
|
|
153
|
+
source has changed since prints both snippets, the one that was cited and the
|
|
154
|
+
one standing there now; a token that names nothing exits 1 and says whether the
|
|
155
|
+
slug or the locator is the wrong half.
|
|
156
|
+
|
|
120
157
|
**Cite** by writing the token as the href of a markdown link on the claim span.
|
|
121
158
|
Multiple citations are `;`-separated in one href.
|
|
122
159
|
|
|
@@ -129,12 +166,12 @@ Multiple citations are `;`-separated in one href.
|
|
|
129
166
|
|
|
130
167
|
```console
|
|
131
168
|
$ backdraft bind memo.md --session s-bridgeview --check value-trace,overlap
|
|
132
|
-
bound
|
|
133
|
-
resolved:
|
|
169
|
+
bound 17 claim(s), 18 citation(s) [frontwalk]
|
|
170
|
+
resolved: 17
|
|
134
171
|
unresolved: 1
|
|
135
|
-
overlap: pass
|
|
136
|
-
value-trace: pass
|
|
137
|
-
! unresolved: bd:t12-summary:p4.c1:1a2b
|
|
172
|
+
overlap: pass 13, skip 4
|
|
173
|
+
value-trace: pass 17
|
|
174
|
+
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @3629
|
|
138
175
|
wrote .backdraft/records/memo.backdraft.json
|
|
139
176
|
|
|
140
177
|
$ echo $?
|
|
@@ -223,18 +260,23 @@ address.
|
|
|
223
260
|
|
|
224
261
|
```console
|
|
225
262
|
$ backdraft ingest https://example.com/reports/q4-2025
|
|
226
|
-
q4-2025 q4-2025
|
|
263
|
+
q4-2025 https://example.com/reports/q4-2025 html 1 page 18402 chars
|
|
227
264
|
|
|
228
265
|
$ backdraft ls
|
|
229
|
-
q4-2025
|
|
266
|
+
q4-2025 https://example.com/reports/q4-2025 html 1 page
|
|
230
267
|
```
|
|
231
268
|
|
|
269
|
+
Every surface names a fetched source by its page. The filename you never see —
|
|
270
|
+
`q4-2025.html` — is the temporary file the fetch staged the bytes in, and it
|
|
271
|
+
exists on nobody's disk, so `ingest`, `ls`, `backdraft read` and the artifact
|
|
272
|
+
all show the URL in its place rather than beside it: two names for one thing
|
|
273
|
+
would let the invented one look authoritative.
|
|
274
|
+
|
|
232
275
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
says it.
|
|
276
|
+
the URL as a link on its receipt, with the date the bytes were taken. That
|
|
277
|
+
pointer is the half of citing a web page a frozen receipt cannot answer on its
|
|
278
|
+
own: the snippet says what the page said, the link is how a reader asks whether
|
|
279
|
+
it still says it.
|
|
238
280
|
|
|
239
281
|
What it does not do, stated rather than worked around: JavaScript-rendered
|
|
240
282
|
pages give you whatever the server sends to a plain GET, pages behind a login
|
|
@@ -269,7 +311,11 @@ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
|
|
|
269
311
|
`OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic
|
|
270
312
|
key in the environment is not consent to send documents to its provider. The
|
|
271
313
|
default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
|
|
272
|
-
base_url + model, set explicitly
|
|
314
|
+
base_url + model, set explicitly (`--config base_url=...` and `--config
|
|
315
|
+
model=...`, one key per flag). Every `--config` key is declared by the extractor
|
|
316
|
+
that reads it and checked against the one `ingest` chose, so a misspelled key
|
|
317
|
+
fails and names the keys that apply rather than being silently ignored.
|
|
318
|
+
Without a scoped key, `auto` falls back to
|
|
273
319
|
the keyless text layer and says so — still storing each page's image, which it
|
|
274
320
|
renders locally. PDF page rendering needs poppler, installed separately; without
|
|
275
321
|
it ingest succeeds without the images and names `snapshot-pages` as the backfill.
|
|
@@ -31,7 +31,9 @@ cd demo
|
|
|
31
31
|
cells for spreadsheet sheets. Formats: PDF, XLSX/XLSM, XLS, CSV/TSV, DOCX,
|
|
32
32
|
PPTX, HTML, images (png, jpeg, tiff, through the vision model), plain text and
|
|
33
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
|
+
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.
|
|
35
37
|
|
|
36
38
|
```console
|
|
37
39
|
$ backdraft init
|
|
@@ -40,11 +42,27 @@ documents: 0
|
|
|
40
42
|
… plus where settings live and what to do next
|
|
41
43
|
|
|
42
44
|
$ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
|
|
43
|
-
t12-summary t12-summary.pdf pdf 3 pages
|
|
44
|
-
underwriting-model underwriting-model.xlsx xlsx 2 sheets
|
|
45
|
+
t12-summary t12-summary.pdf pdf 3 pages 8865 chars
|
|
46
|
+
underwriting-model underwriting-model.xlsx xlsx 2 sheets 2357 chars
|
|
45
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.
|
|
48
|
+
|
|
49
|
+
$ backdraft ingest "https://en.wikipedia.org/w/index.php?title=Franklin_County,_Ohio&oldid=1367935775" --slug franklin-county
|
|
50
|
+
franklin-county https://en.wikipedia.org/w/index.php?title=Franklin_County,_Ohio&oldid=1367935775 html 1 page 34141 chars
|
|
46
51
|
```
|
|
47
52
|
|
|
53
|
+
The demo cites that page for market context. It is Wikipedia's *permanent
|
|
54
|
+
link* — `?oldid=` serves one revision's bytes forever — because a citation into
|
|
55
|
+
a live article reports `drifted` the moment somebody edits it, and the quoted
|
|
56
|
+
sentence would no longer be the sentence there.
|
|
57
|
+
|
|
58
|
+
Every ingest line ends with how much text came out, and says which of three
|
|
59
|
+
things happened: a document created, a **new generation** of one whose bytes
|
|
60
|
+
moved — which is when citations into the previous snapshot start reporting
|
|
61
|
+
`drifted` — or `unchanged`, a no-op because re-running would reproduce what is
|
|
62
|
+
already there. When almost no text came out, a note names the likely cause (a
|
|
63
|
+
scan with no text layer, a page behind a login) and what to do; it is a note at
|
|
64
|
+
exit 0, because a thin snapshot is still a real one.
|
|
65
|
+
|
|
48
66
|
**Read.** The page arrives with a citable name over each chunk. This is the whole
|
|
49
67
|
mechanism: what you can cite is exactly what you were shown.
|
|
50
68
|
|
|
@@ -92,6 +110,25 @@ $ backdraft cell underwriting-model "assumptions!B10"
|
|
|
92
110
|
[bd:underwriting-model:assumptions!B10:964a] 24850000
|
|
93
111
|
```
|
|
94
112
|
|
|
113
|
+
And a token you were handed — out of an artifact, a draft, someone else's
|
|
114
|
+
message — runs backwards to what it actually says:
|
|
115
|
+
|
|
116
|
+
```console
|
|
117
|
+
$ backdraft show bd:t12-summary:p1.c1:c2e8
|
|
118
|
+
[bd:t12-summary:p1.c1:c2e8] resolved t12-summary p1.c1
|
|
119
|
+
Bridgeview Commons — Trailing Twelve Month Summary
|
|
120
|
+
|
|
121
|
+
Property: Bridgeview Commons, 4400 Halsted Avenue, Columbus, OH 43214. 128 units across four three-story
|
|
122
|
+
garden buildings on 6.2 acres, built in 1998 and substantially renovated between 2019 and 2021. …
|
|
123
|
+
|
|
124
|
+
[Read the page: backdraft read t12-summary p1]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`show` is the gate too, so what it shows is minted and citable. A token whose
|
|
128
|
+
source has changed since prints both snippets, the one that was cited and the
|
|
129
|
+
one standing there now; a token that names nothing exits 1 and says whether the
|
|
130
|
+
slug or the locator is the wrong half.
|
|
131
|
+
|
|
95
132
|
**Cite** by writing the token as the href of a markdown link on the claim span.
|
|
96
133
|
Multiple citations are `;`-separated in one href.
|
|
97
134
|
|
|
@@ -104,12 +141,12 @@ Multiple citations are `;`-separated in one href.
|
|
|
104
141
|
|
|
105
142
|
```console
|
|
106
143
|
$ backdraft bind memo.md --session s-bridgeview --check value-trace,overlap
|
|
107
|
-
bound
|
|
108
|
-
resolved:
|
|
144
|
+
bound 17 claim(s), 18 citation(s) [frontwalk]
|
|
145
|
+
resolved: 17
|
|
109
146
|
unresolved: 1
|
|
110
|
-
overlap: pass
|
|
111
|
-
value-trace: pass
|
|
112
|
-
! unresolved: bd:t12-summary:p4.c1:1a2b
|
|
147
|
+
overlap: pass 13, skip 4
|
|
148
|
+
value-trace: pass 17
|
|
149
|
+
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @3629
|
|
113
150
|
wrote .backdraft/records/memo.backdraft.json
|
|
114
151
|
|
|
115
152
|
$ echo $?
|
|
@@ -198,18 +235,23 @@ address.
|
|
|
198
235
|
|
|
199
236
|
```console
|
|
200
237
|
$ backdraft ingest https://example.com/reports/q4-2025
|
|
201
|
-
q4-2025 q4-2025
|
|
238
|
+
q4-2025 https://example.com/reports/q4-2025 html 1 page 18402 chars
|
|
202
239
|
|
|
203
240
|
$ backdraft ls
|
|
204
|
-
q4-2025
|
|
241
|
+
q4-2025 https://example.com/reports/q4-2025 html 1 page
|
|
205
242
|
```
|
|
206
243
|
|
|
244
|
+
Every surface names a fetched source by its page. The filename you never see —
|
|
245
|
+
`q4-2025.html` — is the temporary file the fetch staged the bytes in, and it
|
|
246
|
+
exists on nobody's disk, so `ingest`, `ls`, `backdraft read` and the artifact
|
|
247
|
+
all show the URL in its place rather than beside it: two names for one thing
|
|
248
|
+
would let the invented one look authoritative.
|
|
249
|
+
|
|
207
250
|
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
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
says it.
|
|
251
|
+
the URL as a link on its receipt, with the date the bytes were taken. That
|
|
252
|
+
pointer is the half of citing a web page a frozen receipt cannot answer on its
|
|
253
|
+
own: the snippet says what the page said, the link is how a reader asks whether
|
|
254
|
+
it still says it.
|
|
213
255
|
|
|
214
256
|
What it does not do, stated rather than worked around: JavaScript-rendered
|
|
215
257
|
pages give you whatever the server sends to a plain GET, pages behind a login
|
|
@@ -244,7 +286,11 @@ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
|
|
|
244
286
|
`OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic
|
|
245
287
|
key in the environment is not consent to send documents to its provider. The
|
|
246
288
|
default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
|
|
247
|
-
base_url + model, set explicitly
|
|
289
|
+
base_url + model, set explicitly (`--config base_url=...` and `--config
|
|
290
|
+
model=...`, one key per flag). Every `--config` key is declared by the extractor
|
|
291
|
+
that reads it and checked against the one `ingest` chose, so a misspelled key
|
|
292
|
+
fails and names the keys that apply rather than being silently ignored.
|
|
293
|
+
Without a scoped key, `auto` falls back to
|
|
248
294
|
the keyless text layer and says so — still storing each page's image, which it
|
|
249
295
|
renders locally. PDF page rendering needs poppler, installed separately; without
|
|
250
296
|
it ingest succeeds without the images and names `snapshot-pages` as the backfill.
|