backdraft 0.6.0__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {backdraft-0.6.0 → backdraft-0.7.0}/.claude-plugin/plugin.json +1 -1
- {backdraft-0.6.0 → backdraft-0.7.0}/DESIGN.md +3 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/PKG-INFO +30 -5
- {backdraft-0.6.0 → backdraft-0.7.0}/README.md +28 -3
- backdraft-0.7.0/ROADMAP.md +316 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/SPEC.md +7 -2
- {backdraft-0.6.0 → backdraft-0.7.0}/demo/walkthrough.md +51 -4
- {backdraft-0.6.0 → backdraft-0.7.0}/pyproject.toml +1 -1
- {backdraft-0.6.0 → backdraft-0.7.0}/site/docs.html +3 -1
- {backdraft-0.6.0 → backdraft-0.7.0}/site/llms.txt +30 -9
- {backdraft-0.6.0 → backdraft-0.7.0}/skills/backdraft/SKILL.md +41 -12
- {backdraft-0.6.0 → backdraft-0.7.0}/skills/backdraft-artifact/SKILL.md +25 -3
- {backdraft-0.6.0 → backdraft-0.7.0}/skills/backdraft-backfill/SKILL.md +13 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/bind/binder.py +6 -15
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/bind/cli.py +61 -3
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/cli.py +87 -28
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/cli_context.py +8 -1
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/base.py +53 -2
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/image.py +9 -2
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/pdf_text.py +6 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/snapshots.py +37 -2
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/vlm.py +15 -2
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/vlm_settings.py +17 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/gate/__init__.py +5 -5
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/gate/cli.py +35 -2
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/gate/reader.py +155 -2
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/registry/__init__.py +2 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/registry/store.py +37 -1
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/conftest_registry.py +11 -1
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/gate/test_gate_cli.py +42 -0
- backdraft-0.7.0/tests/gate/test_reader_show.py +157 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_bind_cli.py +100 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_cli.py +117 -3
- backdraft-0.7.0/tests/test_docs.py +44 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_extract.py +90 -1
- backdraft-0.7.0/tests/test_gate_integration.py +211 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_reingest.py +56 -4
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_snapshots.py +14 -0
- {backdraft-0.6.0 → backdraft-0.7.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.7.0}/.claude-plugin/marketplace.json +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/.github/workflows/ci.yml +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/.github/workflows/release.yml +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/.github/workflows/site.yml +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/.gitignore +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/.python-version +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/LICENSE +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/demo/generate_sources.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/demo/memo.backdraft.html +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/demo/memo.md +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/demo/sources/t12-summary.pdf +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/site/demo.html +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/site/index.html +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/site/privacy.html +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/spec/artifact.md +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/spec/chunking.md +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/spec/tokens.md +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/__main__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/bind/evidence.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/bind/verify/entail.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/credentials.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/csv.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/docx.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/html.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/pptx.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/sheet.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/xls.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/extract/xlsx.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/fetch.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/gate/searcher.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/kernel/artifact.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/kernel/chunking.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/kernel/model.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/registry/ledger.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/registry/schema.sql +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/_text.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/cli.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/footnotes.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/html/__init__.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/html/assets.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/html/components.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/html/fmt.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/html/fmt_vectors.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/html/page.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/html/text.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/markdown.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/sidecar.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/theme.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/themes/default.toml +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/themes/press.toml +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/src/backdraft/render/themes/slate.toml +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/conftest.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/conftest_render.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/css_util.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/fakes.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/gate/fake_registry.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/gate/test_gate_ledger.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/gate/test_gate_reader.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/gate/test_gate_searcher.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/render/backfill.sidecar.json +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden/render/demo.sidecar.json +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/golden_util.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_bind_evidence.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_card_sizing.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_chunking.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_claims.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_credentials.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_extract_html.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_fetch.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_fmt_parity.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_footnotes.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_hashing.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_html.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_ingest_url.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_invariants.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_model.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_placement.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_records_location.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_registry.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_render_cli.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_render_markdown.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_snapshot_settings.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_theme.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_tokens.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.0}/tests/test_value_trace.py +0 -0
- {backdraft-0.6.0 → backdraft-0.7.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.7.0",
|
|
5
5
|
"description": "Drop-in provenance for factual claims: one click from any claim to the evidence behind it. Ships the backdraft writing, backfill, and artifact-reading skills; the CLI installs separately (uv tool install backdraft).",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Spencer Braun",
|
|
@@ -140,6 +140,9 @@ 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. |
|
|
143
146
|
|
|
144
147
|
## Open
|
|
145
148
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: backdraft
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Drop-in provenance for factual claims: one click from any claim to the evidence behind it.
|
|
5
5
|
Project-URL: Homepage, https://backdraft.dev
|
|
6
6
|
Project-URL: Documentation, https://backdraft.dev/docs.html
|
|
@@ -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
|
|
@@ -117,6 +119,25 @@ $ backdraft cell underwriting-model "assumptions!B10"
|
|
|
117
119
|
[bd:underwriting-model:assumptions!B10:964a] 24850000
|
|
118
120
|
```
|
|
119
121
|
|
|
122
|
+
And a token you were handed — out of an artifact, a draft, someone else's
|
|
123
|
+
message — runs backwards to what it actually says:
|
|
124
|
+
|
|
125
|
+
```console
|
|
126
|
+
$ backdraft show bd:t12-summary:p1.c1:c2e8
|
|
127
|
+
[bd:t12-summary:p1.c1:c2e8] resolved t12-summary p1.c1
|
|
128
|
+
Bridgeview Commons — Trailing Twelve Month Summary
|
|
129
|
+
|
|
130
|
+
Property: Bridgeview Commons, 4400 Halsted Avenue, Columbus, OH 43214. 128 units across four three-story
|
|
131
|
+
garden buildings on 6.2 acres, built in 1998 and substantially renovated between 2019 and 2021. …
|
|
132
|
+
|
|
133
|
+
[Read the page: backdraft read t12-summary p1]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`show` is the gate too, so what it shows is minted and citable. A token whose
|
|
137
|
+
source has changed since prints both snippets, the one that was cited and the
|
|
138
|
+
one standing there now; a token that names nothing exits 1 and says whether the
|
|
139
|
+
slug or the locator is the wrong half.
|
|
140
|
+
|
|
120
141
|
**Cite** by writing the token as the href of a markdown link on the claim span.
|
|
121
142
|
Multiple citations are `;`-separated in one href.
|
|
122
143
|
|
|
@@ -134,7 +155,7 @@ bound 15 claim(s), 16 citation(s) [frontwalk]
|
|
|
134
155
|
unresolved: 1
|
|
135
156
|
overlap: pass 11, skip 4
|
|
136
157
|
value-trace: pass 15
|
|
137
|
-
! unresolved: bd:t12-summary:p4.c1:1a2b
|
|
158
|
+
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @2900
|
|
138
159
|
wrote .backdraft/records/memo.backdraft.json
|
|
139
160
|
|
|
140
161
|
$ echo $?
|
|
@@ -269,7 +290,11 @@ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
|
|
|
269
290
|
`OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic
|
|
270
291
|
key in the environment is not consent to send documents to its provider. The
|
|
271
292
|
default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
|
|
272
|
-
base_url + model, set explicitly
|
|
293
|
+
base_url + model, set explicitly (`--config base_url=...` and `--config
|
|
294
|
+
model=...`, one key per flag). Every `--config` key is declared by the extractor
|
|
295
|
+
that reads it and checked against the one `ingest` chose, so a misspelled key
|
|
296
|
+
fails and names the keys that apply rather than being silently ignored.
|
|
297
|
+
Without a scoped key, `auto` falls back to
|
|
273
298
|
the keyless text layer and says so — still storing each page's image, which it
|
|
274
299
|
renders locally. PDF page rendering needs poppler, installed separately; without
|
|
275
300
|
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
|
|
@@ -92,6 +94,25 @@ $ backdraft cell underwriting-model "assumptions!B10"
|
|
|
92
94
|
[bd:underwriting-model:assumptions!B10:964a] 24850000
|
|
93
95
|
```
|
|
94
96
|
|
|
97
|
+
And a token you were handed — out of an artifact, a draft, someone else's
|
|
98
|
+
message — runs backwards to what it actually says:
|
|
99
|
+
|
|
100
|
+
```console
|
|
101
|
+
$ backdraft show bd:t12-summary:p1.c1:c2e8
|
|
102
|
+
[bd:t12-summary:p1.c1:c2e8] resolved t12-summary p1.c1
|
|
103
|
+
Bridgeview Commons — Trailing Twelve Month Summary
|
|
104
|
+
|
|
105
|
+
Property: Bridgeview Commons, 4400 Halsted Avenue, Columbus, OH 43214. 128 units across four three-story
|
|
106
|
+
garden buildings on 6.2 acres, built in 1998 and substantially renovated between 2019 and 2021. …
|
|
107
|
+
|
|
108
|
+
[Read the page: backdraft read t12-summary p1]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`show` is the gate too, so what it shows is minted and citable. A token whose
|
|
112
|
+
source has changed since prints both snippets, the one that was cited and the
|
|
113
|
+
one standing there now; a token that names nothing exits 1 and says whether the
|
|
114
|
+
slug or the locator is the wrong half.
|
|
115
|
+
|
|
95
116
|
**Cite** by writing the token as the href of a markdown link on the claim span.
|
|
96
117
|
Multiple citations are `;`-separated in one href.
|
|
97
118
|
|
|
@@ -109,7 +130,7 @@ bound 15 claim(s), 16 citation(s) [frontwalk]
|
|
|
109
130
|
unresolved: 1
|
|
110
131
|
overlap: pass 11, skip 4
|
|
111
132
|
value-trace: pass 15
|
|
112
|
-
! unresolved: bd:t12-summary:p4.c1:1a2b
|
|
133
|
+
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @2900
|
|
113
134
|
wrote .backdraft/records/memo.backdraft.json
|
|
114
135
|
|
|
115
136
|
$ echo $?
|
|
@@ -244,7 +265,11 @@ explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
|
|
|
244
265
|
`OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic
|
|
245
266
|
key in the environment is not consent to send documents to its provider. The
|
|
246
267
|
default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
|
|
247
|
-
base_url + model, set explicitly
|
|
268
|
+
base_url + model, set explicitly (`--config base_url=...` and `--config
|
|
269
|
+
model=...`, one key per flag). Every `--config` key is declared by the extractor
|
|
270
|
+
that reads it and checked against the one `ingest` chose, so a misspelled key
|
|
271
|
+
fails and names the keys that apply rather than being silently ignored.
|
|
272
|
+
Without a scoped key, `auto` falls back to
|
|
248
273
|
the keyless text layer and says so — still storing each page's image, which it
|
|
249
274
|
renders locally. PDF page rendering needs poppler, installed separately; without
|
|
250
275
|
it ingest succeeds without the images and names `snapshot-pages` as the backfill.
|
|
@@ -0,0 +1,316 @@
|
|
|
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. The demo cites a web page, so the feature is visible
|
|
36
|
+
|
|
37
|
+
**Intent.** URL sources ship complete — `ingest` fetches and snapshots, the
|
|
38
|
+
registry keeps `url` and `fetched_at`, and as of 2026-08-06 the artifact links
|
|
39
|
+
back to the page with its fetch date. None of it appears at
|
|
40
|
+
`backdraft.dev/demo.html`, because both demo sources are local files
|
|
41
|
+
(`t12-summary.pdf`, `underwriting-model.xlsx`). The one artifact anybody
|
|
42
|
+
actually looks at does not show the capability, and the byte-identity test that
|
|
43
|
+
proved the change was safe is the same fact that keeps it invisible. A reader
|
|
44
|
+
evaluating backdraft cannot see that a web citation is a citation like any
|
|
45
|
+
other.
|
|
46
|
+
|
|
47
|
+
**Shape.** Add a third source to `demo/`: the Wikipedia article for the county
|
|
48
|
+
the property sits in, cited for market context — population, median household
|
|
49
|
+
income, the kind of external figure a screening memo genuinely leans on and
|
|
50
|
+
cannot get from a T12. Bridgeview Commons is fictional and says so in its own
|
|
51
|
+
first line, so give it a real county and let the memo name it; one new
|
|
52
|
+
paragraph under "The asset" or a short "Market" section, with one or two
|
|
53
|
+
claims. Cite Wikipedia's **permanent link** (`?oldid=<rev>`), not the bare
|
|
54
|
+
article URL: an article is edited constantly, a citation into the live URL
|
|
55
|
+
would report `drifted` within days, and a front-page demo that permanently
|
|
56
|
+
shows drift teaches that the tool is broken rather than that the source moved.
|
|
57
|
+
An immutable revision plus the rendered "as of" date is also what a careful
|
|
58
|
+
analyst cites. Write the tradeoff as a DESIGN row, naming the thing given up —
|
|
59
|
+
the demo no longer shows a live page changing under a citation, which is the
|
|
60
|
+
better story and belongs in a `drifted` walkthrough, not the shop window.
|
|
61
|
+
|
|
62
|
+
Practical notes for whoever builds this: `demo/.backdraft/` is gitignored, so
|
|
63
|
+
the fetch happens once on the implementer's machine and the snapshot then
|
|
64
|
+
travels in the local registry; `demo/generate_sources.py` builds the two
|
|
65
|
+
fixture files and is *not* where this goes, since the page is fetched rather
|
|
66
|
+
than generated. Re-bind and re-render (`bind memo.md --session s-bridgeview
|
|
67
|
+
--check value-trace,overlap`, then `render memo.md --to html`), and copy the
|
|
68
|
+
result over `site/demo.html` — the two are byte-identical by convention.
|
|
69
|
+
`demo/walkthrough.md` shows real CLI output and its `ls` and `bind` blocks both
|
|
70
|
+
move.
|
|
71
|
+
|
|
72
|
+
**Acceptance.** `backdraft.dev/demo.html` shows three sources, one of them a
|
|
73
|
+
clickable Wikipedia link with a fetch date, on both the resting rail and the
|
|
74
|
+
end-matter list; the claim citing it opens a card whose source line carries the
|
|
75
|
+
same link. `demo/walkthrough.md` matches what the commands now print. A re-bind
|
|
76
|
+
some weeks later still reports `resolved`, not `drifted` — that is the check
|
|
77
|
+
that the permanent link did its job.
|
|
78
|
+
|
|
79
|
+
**Size.** One day.
|
|
80
|
+
|
|
81
|
+
### 2. The gate names a fetched source by its page, not by its staging filename
|
|
82
|
+
|
|
83
|
+
**Intent.** A fetched page's `filename` is `q4-2025.html`, a name invented by
|
|
84
|
+
`fetch.filename_for` for a temporary file that no longer exists. The
|
|
85
|
+
2026-08-06 decision row ruled on this for the artifact — the URL *replaces* the
|
|
86
|
+
filename there, because "showing both would give a reader two names for one
|
|
87
|
+
thing and let the fictional one look authoritative" — but the same fictional
|
|
88
|
+
name is still what `backdraft read` prints in its document list, what its table
|
|
89
|
+
of contents headline prints, and what `ls` prints in column two. The gate is
|
|
90
|
+
where a writing agent actually lives, and there it learns nothing about
|
|
91
|
+
provenance at all: `skills/backdraft/SKILL.md` has to send it to `ls` for a
|
|
92
|
+
fact the surface it is already using could tell it. One rule, applied in one
|
|
93
|
+
place, is the fix.
|
|
94
|
+
|
|
95
|
+
**Shape.** `gate/reader.py`'s `render_documents` and `_document_headline`, plus
|
|
96
|
+
`cli.py`'s `ls` (which now shares the gate's page-count vocabulary through
|
|
97
|
+
`gate.unit` and should share this too). For a document whose `meta` carries a
|
|
98
|
+
`url`, show the origin in place of the filename, exactly as the artifact's
|
|
99
|
+
source list does. The gate's output is a contract — its module docstring says
|
|
100
|
+
"stable enough to diff" — so keep it line-oriented and keep the column
|
|
101
|
+
alignment: a URL is long, so put it on the row rather than in the aligned
|
|
102
|
+
filename column if alignment would collapse, and say which choice you made in
|
|
103
|
+
the docstring. `Document.meta` is already carried by `Registry.documents()` and
|
|
104
|
+
`Registry.document()`, so no registry change is needed. Do not touch the slug,
|
|
105
|
+
the token, or anything a citation resolves through — this is display, and the
|
|
106
|
+
2026-08-05 row's "provenance, never identity" governs.
|
|
107
|
+
|
|
108
|
+
**Acceptance.** In a registry holding one file and one fetched page, `backdraft
|
|
109
|
+
read` shows the file by its filename and the page by its URL; `backdraft read
|
|
110
|
+
<slug>` for the fetched page carries the URL in its headline; `backdraft ls`
|
|
111
|
+
matches. A registry of files prints exactly what it printed before — pin that
|
|
112
|
+
as a test, the way the artifact's byte-identity is pinned. `README.md`, the
|
|
113
|
+
docs page and `skills/backdraft/SKILL.md` drop the detour through `ls` for a
|
|
114
|
+
fact `read` now carries.
|
|
115
|
+
|
|
116
|
+
**Size.** Two days.
|
|
117
|
+
|
|
118
|
+
### 3. A fetched page's slug is decided, not defaulted
|
|
119
|
+
|
|
120
|
+
**Intent.** DESIGN.md's Open list has carried "slug assignment/collision rules"
|
|
121
|
+
since the first field trial, and URL sources sharpened it into a real failure:
|
|
122
|
+
`fetch.filename_for` takes the URL's last path segment, so
|
|
123
|
+
`https://a.example/reports/2025/index.html` and
|
|
124
|
+
`https://b.example/docs/index.html` both stage as `index.html` and become slugs
|
|
125
|
+
`index` and `index-2`. An agent then cites `bd:index-2:p1.c3:...` with no way to
|
|
126
|
+
tell which site that is, and a slug is the handle every token in every authored
|
|
127
|
+
document carries — it is stable once assigned, so a bad one is permanent. The
|
|
128
|
+
same collapse hits any site whose pages are `index.html`, `page`, `view`, or a
|
|
129
|
+
bare numeric id.
|
|
130
|
+
|
|
131
|
+
**Shape.** `fetch.filename_for`, and a decision row that closes the Open
|
|
132
|
+
question for files and URLs together. For a URL, build the stem from enough of
|
|
133
|
+
the address to distinguish it — the host plus the last meaningful path segment
|
|
134
|
+
is the obvious candidate, with the registry's existing `slug_for` slugification
|
|
135
|
+
doing the rest — and keep `_dedupe`'s `-2` as the collision *backstop* rather
|
|
136
|
+
than the normal outcome. `store._assign_slug` and `_dedupe` do not need to
|
|
137
|
+
change; this is about giving them a stem worth deduping. Note the constraint
|
|
138
|
+
that makes this delicate: continuity across a re-fetch is matched on the URL
|
|
139
|
+
(see `_find_document`), not on the slug, so changing the stem rule does not
|
|
140
|
+
break an existing registry's documents — but it does mean two registries built
|
|
141
|
+
on either side of this change disagree about a page's slug, which is a
|
|
142
|
+
compatibility statement the decision row has to make out loud.
|
|
143
|
+
|
|
144
|
+
**Acceptance.** The two `index.html` URLs above ingest as two slugs that each
|
|
145
|
+
name their site. `https://example.com/reports/q4-2025` still ingests as
|
|
146
|
+
`q4-2025` — the common case does not get uglier to fix the collision case. A
|
|
147
|
+
URL with an empty path still falls back to the host. Tests for each, plus one
|
|
148
|
+
that two pages colliding even after the new rule still dedupe rather than
|
|
149
|
+
error. DESIGN.md gains the decision row and the Open list loses the line.
|
|
150
|
+
|
|
151
|
+
**Size.** Two days.
|
|
152
|
+
|
|
153
|
+
### 4. Ingest says what it did and what it got
|
|
154
|
+
|
|
155
|
+
**Intent.** Two things `ingest` knows and does not say. First, it prints the
|
|
156
|
+
same line whether it created a document, created a *new generation* of one
|
|
157
|
+
whose bytes changed, or did nothing at all because the bytes and config were
|
|
158
|
+
identical — so an agent re-ingesting after a fix cannot tell whether anything
|
|
159
|
+
happened, and the moment a new generation appears is exactly the moment
|
|
160
|
+
existing bindings may start reporting `drifted`. Second, it never says how much
|
|
161
|
+
text came out. `skills/backdraft/SKILL.md` instructs the agent that a
|
|
162
|
+
JavaScript-rendered page or one behind a login "will come back thin or empty,
|
|
163
|
+
and if it does, say so to the user rather than citing the shell of it" — and
|
|
164
|
+
gives it no signal to notice that with. A login wall extracts as a few dozen
|
|
165
|
+
characters, exits 0, and prints `1 page` like any success. The same silence
|
|
166
|
+
covers a scanned PDF with no text layer.
|
|
167
|
+
|
|
168
|
+
**Shape.** `ingest` in `cli.py`, output only — no registry or format change.
|
|
169
|
+
`Registry.ingest` already distinguishes the three outcomes internally
|
|
170
|
+
(`_find_document`, `_is_noop`, `_upsert_document`); surface which one happened
|
|
171
|
+
rather than re-deriving it in the CLI, which may mean returning it alongside
|
|
172
|
+
the `Document` or exposing it on the returned value. Print the extracted
|
|
173
|
+
character count beside the page count, and when it falls under a threshold the
|
|
174
|
+
module declares as a named constant, add one line naming the likely cause and
|
|
175
|
+
what to do — the shape the poppler and `pdf-text` notes already use, which is a
|
|
176
|
+
note at exit 0 and never a failure, because a thin page is still a real
|
|
177
|
+
snapshot and failures here are data. A new generation gets its own line saying
|
|
178
|
+
that citations into the previous one may now report `drifted`, and that `bind`
|
|
179
|
+
is what will say so.
|
|
180
|
+
|
|
181
|
+
**Acceptance.** Ingesting a file twice unchanged says so the second time;
|
|
182
|
+
editing it and re-ingesting says a new generation was made and names drift;
|
|
183
|
+
both still exit 0. A near-empty HTML file ingests, exits 0, and prints the thin
|
|
184
|
+
-source note; a normal source does not. `backdraft ingest` on a fresh document
|
|
185
|
+
prints what it prints today plus the character count. Tests for all five
|
|
186
|
+
branches. `demo/walkthrough.md`, `README.md` and `site/docs.html` show the real
|
|
187
|
+
new output, and `skills/backdraft/SKILL.md` points at the note instead of
|
|
188
|
+
asking the agent to judge thinness unaided.
|
|
189
|
+
|
|
190
|
+
**Size.** Two days.
|
|
191
|
+
|
|
192
|
+
### 5. `backdraft verify <artifact>`: checking a receipt without the registry
|
|
193
|
+
|
|
194
|
+
**Intent.** `skills/backdraft-artifact/SKILL.md` exists because there is no
|
|
195
|
+
command: it walks an agent through opening a `.backdraft.json` or
|
|
196
|
+
`.backdraft.html`, recomputing each `snippet_sha256`, and reporting claim
|
|
197
|
+
statuses by hand. That is a deterministic check over a self-describing file,
|
|
198
|
+
which is precisely the kind of thing that should not be a prose procedure an
|
|
199
|
+
agent re-implements every time — and doing it by hand is how a recipient's
|
|
200
|
+
check ends up weaker than the producer's. The artifact is the product; nothing in the
|
|
201
|
+
tool reads one back.
|
|
202
|
+
|
|
203
|
+
**Shape.** A new command in `render/cli.py` (which already owns the artifact's
|
|
204
|
+
reader half through `sidecar.read`), taking the artifact or its sidecar.
|
|
205
|
+
Two tiers, and the output must be explicit about which ran. Tier one needs
|
|
206
|
+
nothing but the file: parse it against `kernel/artifact.py`'s format, recompute
|
|
207
|
+
every `snippet_sha256` from the snippet the file carries, and confirm the
|
|
208
|
+
`$format` and the claim/citation counts — this is the check a recipient with
|
|
209
|
+
only the file can make, and it catches an edited artifact. Tier two runs only
|
|
210
|
+
when a registry is discoverable: re-resolve each token through the same path
|
|
211
|
+
`bind` uses and report the closed status set. Exit 0 when everything checked
|
|
212
|
+
passed, 1 on a usage error, and — matching `bind` — 2 when something did not
|
|
213
|
+
verify, so a hook can gate on it. Read-only: it opens no session and mints
|
|
214
|
+
nothing, which is what distinguishes it from `read` and from `backdraft show`
|
|
215
|
+
(landed 2026-08-12), whose whole point is that showing mints — an audit must not
|
|
216
|
+
make its subject citable.
|
|
217
|
+
|
|
218
|
+
**Acceptance.** `backdraft verify demo/memo.backdraft.html` from outside
|
|
219
|
+
`demo/` reports tier one passing and says plainly that no registry was found,
|
|
220
|
+
exiting 0. From inside `demo/` it also re-resolves and reports the one
|
|
221
|
+
unresolved citation, exiting 2. A file with one snippet byte changed fails by
|
|
222
|
+
naming the claim and the hash that did not match. A file that is not an
|
|
223
|
+
artifact exits 1 saying so. `skills/backdraft-artifact/SKILL.md` is rewritten
|
|
224
|
+
around the command, keeping the prose procedure only as the fallback for an
|
|
225
|
+
agent that has no backdraft install.
|
|
226
|
+
|
|
227
|
+
**Size.** Three days.
|
|
228
|
+
|
|
229
|
+
### 6. `spec/registry.md`: the export format is normative, or it is not a format
|
|
230
|
+
|
|
231
|
+
**Intent.** `backdraft export` writes `"$format": "backdraft/registry-v1"`, a
|
|
232
|
+
version string that promises a specification and does not have one. `spec/`
|
|
233
|
+
holds `artifact.md`, `tokens.md` and `chunking.md`, and the repo's own rule is
|
|
234
|
+
that another implementation reads those and nothing else — the export is the
|
|
235
|
+
only declared format outside that set. It is also the only complete, portable
|
|
236
|
+
representation of a registry, so it is what a second implementation, a
|
|
237
|
+
migration, or an audit would read. The gap is not theoretical: the 2026-08-05
|
|
238
|
+
work added a conditional `meta` key to every exported document, a format change
|
|
239
|
+
to a format with nothing to change.
|
|
240
|
+
|
|
241
|
+
**Shape.** A new `spec/registry.md` in the voice of `spec/artifact.md`:
|
|
242
|
+
normative prose, MUST/OPTIONAL where it means them, and written to be readable
|
|
243
|
+
without the code. Cover the top-level shape, the document entry including the
|
|
244
|
+
`meta` key and its `url`/`fetched_at` (OPTIONAL, present only for a fetched
|
|
245
|
+
source — the same conditional the artifact spec already states, for the same
|
|
246
|
+
compatibility reason), every extraction generation with `is_current`, pages
|
|
247
|
+
and cells, and anchors with their receipts. State what identity is and what it
|
|
248
|
+
is not, matching the concept table in `SPEC.md`. Say what a conforming reader
|
|
249
|
+
must do with a key it does not recognize. Pin it the way the artifact format is
|
|
250
|
+
pinned: a golden-file test over the demo registry's export, so the next
|
|
251
|
+
conditional key is a test failure rather than an undocumented change.
|
|
252
|
+
|
|
253
|
+
**Acceptance.** `spec/registry.md` describes every key `Registry.export`
|
|
254
|
+
actually emits — check that mechanically, with a test that walks the exported
|
|
255
|
+
JSON and fails on a key the spec does not name. `SPEC.md`'s file map and
|
|
256
|
+
`README.md`'s spec list gain it. Exporting the demo registry and reading only
|
|
257
|
+
the spec is enough to say what each field means.
|
|
258
|
+
|
|
259
|
+
**Size.** Two days.
|
|
260
|
+
|
|
261
|
+
## Parked
|
|
262
|
+
|
|
263
|
+
Deliberately not queued, each with the reason, so picking one up starts from
|
|
264
|
+
the objection rather than rediscovering it.
|
|
265
|
+
|
|
266
|
+
- **Exhibits view** — the evidence-first inversion. Risk: it grows the
|
|
267
|
+
artifact and pollutes a design whose strength is restraint. Needs a
|
|
268
|
+
deliberate design pass and a real audience before any code; not a
|
|
269
|
+
weekend-feature shape.
|
|
270
|
+
- **Excel region maps and range reads** — the research notes exist, but no
|
|
271
|
+
confidence the deterministic detection cascade lands well on the first
|
|
272
|
+
try. Revisit when large-sheet navigation hurts in a real corpus, with
|
|
273
|
+
test workbooks in hand.
|
|
274
|
+
- **`bd:calc` derivations** — reserved in the grammar and parsed as
|
|
275
|
+
malformed-on-purpose, which is enough for now. Full support complicates
|
|
276
|
+
binding and verification for one field-trial case.
|
|
277
|
+
- **Merged-cell and frozen-pane rendering** — ingest captures `merged` and
|
|
278
|
+
`frozen` in sheet meta and keeps them (cheap, and capture is the part you
|
|
279
|
+
cannot backfill). Frozen-pane rendering is cut; merged-cell rendering
|
|
280
|
+
waits for a real sheet that renders wrong without it.
|
|
281
|
+
|
|
282
|
+
## Later
|
|
283
|
+
|
|
284
|
+
- **Living documents** — re-ingest, re-bind, and a drift-first report: "these
|
|
285
|
+
claims cite figures that changed; cited-then vs. now." The primitives
|
|
286
|
+
(generations, `drifted`, the word-diff) all exist; the missing piece is a
|
|
287
|
+
diff-shaped report and a demo.
|
|
288
|
+
- **Substrates beyond the CLI** — SDK middleware for pipelines (the exit-code
|
|
289
|
+
and `--to json` contracts are already shaped for it), and a client-side
|
|
290
|
+
drag-drop viewer page (no upload — the file never leaves the reader's
|
|
291
|
+
machine).
|
|
292
|
+
- **Email (.eml)** — arrives in diligence folders alongside everything else;
|
|
293
|
+
identity questions (headers vs. body, attachments as child sources) deserve
|
|
294
|
+
a decision row before code. The web-page half of this item shipped
|
|
295
|
+
2026-08-05; `document_meta` is where an email's provenance would land.
|
|
296
|
+
- **Anthropic-API provider for the VLM extractor** — Cowork's sandbox
|
|
297
|
+
egress allowlist includes api.anthropic.com but not the OpenAI-compatible
|
|
298
|
+
providers, so an Anthropic-native option would let sandboxed sessions
|
|
299
|
+
ingest at full fidelity. Needs a client abstraction the vlm module
|
|
300
|
+
currently doesn't have; the documented workaround (ingest locally, the
|
|
301
|
+
registry travels with the folder) covers it meanwhile.
|
|
302
|
+
- **Distribution** — published on PyPI; the repo ships
|
|
303
|
+
`.claude-plugin/plugin.json` and `marketplace.json`, so it installs as a
|
|
304
|
+
Claude Code plugin and self-hosts its marketplace, and the AGENTS.md
|
|
305
|
+
snippet is published in the README and site docs. The community-directory
|
|
306
|
+
submission went in 2026-07-29 and is awaiting review — approval is what
|
|
307
|
+
surfaces the skills in Cowork's directory.
|
|
308
|
+
|
|
309
|
+
## Someday
|
|
310
|
+
|
|
311
|
+
- **Hosted team features** — upload, access control, shared registries.
|
|
312
|
+
Deliberately parked: the product's trust story is the self-contained file,
|
|
313
|
+
and a hosted viewer inverts it. Revisit only as a distinct teams product.
|
|
314
|
+
- **Entail at scale** — the model-judge verifier is wired (`[entail]` extra)
|
|
315
|
+
but has never been field-calibrated; needs a corpus and a rubric before it
|
|
316
|
+
is recommended anywhere.
|
|
@@ -231,11 +231,12 @@ class Extractor(Protocol):
|
|
|
231
231
|
name: str
|
|
232
232
|
version: str
|
|
233
233
|
deterministic: bool
|
|
234
|
+
config_keys: Mapping[str, str] # optional; --config surface, key -> meaning
|
|
234
235
|
def can_handle(self, path: Path, media_type: str) -> bool: ...
|
|
235
236
|
def extract(self, path: Path, config: dict) -> Iterator[ExtractedPage]: ...
|
|
236
237
|
```
|
|
237
238
|
|
|
238
|
-
Registered in a plain dict; `ingest --extractor auto` picks the first `can_handle`, built-ins ordered `xlsx, xls, csv, docx, pptx, pdf-text, image, html, text` (an extractor whose optional extra is missing is skipped, not fatal). `vlm` is pdf→images→VLM per page, OpenAI-compatible client, model configurable; its deps ship by default and the backdraft-scoped key gates use. **For PDFs, `auto` prefers `vlm` when it is ready (importable, key configured)** (glossy layouts and info boxes extract badly from the text layer, and the snapshot is the receipt); otherwise it falls back to `pdf-text` and prints a one-line nudge naming the VLM option. An explicit `--extractor` always wins. The sheet representation (shared by xlsx, xls, csv): `Row | A | B...` header, `[B10] value` prefixes, dimension caps, inflated-sheet placeholder pages; cell values are never rounded, since the snapshot is the receipt a claim is traced against. `docx` synthesizes pages from heading sections (the document's smallest present outline level among Heading 1–2 splits; no headings → one page), so locators stay `pN.cM`. `pptx` is one page per slide, text floor only; ingest notes that slide visuals need the PDF-export path. `pdf-text` reconstructs paragraph breaks from line geometry before handing the page over, so the chunker's blank-line rule has something to fire on. `html` is one page for the whole document, named by its `<title>`: block elements end a block so the chunker's blank-line rule fires, `script`/`style`/`noscript`/`template`/`svg` are dropped whole, lists become `- `/`1. ` lines in a single block, and tables become pipe tables as `docx` renders them. It is a parse and not a readability heuristic — no boilerplate stripping, since a guess that changes between two versions of a site moves anchors. Decoding is a pure function of the bytes (BOM, then `<meta charset>`, then UTF-8 with replacement), so a saved page and a fetched one extract identically.
|
|
239
|
+
Registered in a plain dict; `ingest --extractor auto` picks the first `can_handle`, built-ins ordered `xlsx, xls, csv, docx, pptx, pdf-text, image, html, text` (an extractor whose optional extra is missing is skipped, not fatal). `config_keys` declares the `--config` keys an extractor reads; `Registry.ingest` validates the parsed config against the *chosen* extractor (after selection, since `auto` picks per file) and raises on an undeclared key, naming it and the ones that apply. Declaring nothing is the default and means accepting nothing. The page-render keys (`dpi`, `snapshot_quality`, `snapshot_max_height`) are declared once in `extract/snapshots.py` and taken by both PDF paths, since both rasterize; the provider keys (`api_key`, `base_url`, `model`, `timeout`, `retries`) are declared in `extract/vlm_settings.py` and taken by both vision paths; `concurrency` is `vlm`'s alone. `vlm` is pdf→images→VLM per page, OpenAI-compatible client, model configurable; its deps ship by default and the backdraft-scoped key gates use. **For PDFs, `auto` prefers `vlm` when it is ready (importable, key configured)** (glossy layouts and info boxes extract badly from the text layer, and the snapshot is the receipt); otherwise it falls back to `pdf-text` and prints a one-line nudge naming the VLM option. An explicit `--extractor` always wins. The sheet representation (shared by xlsx, xls, csv): `Row | A | B...` header, `[B10] value` prefixes, dimension caps, inflated-sheet placeholder pages; cell values are never rounded, since the snapshot is the receipt a claim is traced against. `docx` synthesizes pages from heading sections (the document's smallest present outline level among Heading 1–2 splits; no headings → one page), so locators stay `pN.cM`. `pptx` is one page per slide, text floor only; ingest notes that slide visuals need the PDF-export path. `pdf-text` reconstructs paragraph breaks from line geometry before handing the page over, so the chunker's blank-line rule has something to fire on. `html` is one page for the whole document, named by its `<title>`: block elements end a block so the chunker's blank-line rule fires, `script`/`style`/`noscript`/`template`/`svg` are dropped whole, lists become `- `/`1. ` lines in a single block, and tables become pipe tables as `docx` renders them. It is a parse and not a readability heuristic — no boilerplate stripping, since a guess that changes between two versions of a site moves anchors. Decoding is a pure function of the bytes (BOM, then `<meta charset>`, then UTF-8 with replacement), so a saved page and a fetched one extract identically.
|
|
239
240
|
|
|
240
241
|
## Gate (gate/)
|
|
241
242
|
|
|
@@ -243,6 +244,7 @@ CLI-facing behavior contract:
|
|
|
243
244
|
- `read` with no args → document list (slug, filename, media, pages). `read <slug>` → TOC (page/sheet, name, summary-or-first-120-chars). `read <slug> p3` / `p3-5` / `rent-roll` → token-marked content.
|
|
244
245
|
- PDF pages render as chunks: `[bd:slug:p3.c1:a7f3]` on its own line above each chunk's text. Sheets render the markdown table as-is with a header line carrying the page token; cell tokens are NOT inlined per-cell (the `[B10]` refs are already in-band; bind composes `bd:slug:sheet!B10:hash` from the registry).
|
|
245
246
|
- `search "<query>"` → FTS5 over anchors; each result: token, slug, page, snippet. Results are minted (ledger-recorded) — a searched snippet is citable without a page read.
|
|
247
|
+
- `show <token>...` → the inverse of minting: per token, its bind status, its slug and locator, and the verbatim snippet, in argument order. `resolved` prints the snippet; `drifted` prints both the cited snippet and what stands at the locator now (and mints that anchor too, since it is the one worth citing); `unresolved` says whether the slug or the locator is what named nothing; `malformed` carries the kernel's reason and the grammar. Statuses and resolution are bind's, reached through `kernel.claims.parse_citation` and `Registry.resolve` rather than a second reading of what a token means; `not_shown` cannot occur, because showing mints. Exit 1 if any token was `unresolved` or `malformed`, with the block still on stdout.
|
|
246
248
|
- Every emitted token is recorded to the ledger under the session (`--session` flag or `BACKDRAFT_SESSION` env; auto-created default session otherwise).
|
|
247
249
|
- Long pages: `--offset/--limit` on chars for PDFs; sheets paginate by rows, never mid-row, header row repeated.
|
|
248
250
|
|
|
@@ -293,6 +295,7 @@ backdraft ingest <sources...> [--extractor auto] [--slug S] [--config k=v]
|
|
|
293
295
|
# a source is a path or an http(s) URL
|
|
294
296
|
backdraft ls | backdraft read ... # gate, above
|
|
295
297
|
backdraft search "<query>" [--in slug]
|
|
298
|
+
backdraft show <token>... # gate: what does this token say?
|
|
296
299
|
backdraft bind <doc.md> [--session S] [--check ...] [--mode ...]
|
|
297
300
|
backdraft render <doc.md> [--to html|footnotes|json] [-o out] [--theme name|file]
|
|
298
301
|
backdraft theme [list|show <name|file>]
|
|
@@ -327,6 +330,8 @@ Integration invariants (every workstream's tests must respect): any token the ga
|
|
|
327
330
|
|
|
328
331
|
`registry/store.py` exposes exactly this surface. W1 implements it; W2/W3 consume it and MUST NOT implement anything under `registry/`; their tests use a lightweight fake implementing these names.
|
|
329
332
|
|
|
333
|
+
Beside the class, `registry` exports one module function, `current_at(registry, anchor) -> Anchor | None`: the current generation's anchor at `anchor`'s locator, or None if the locator is gone. It is the other half of drift — `resolve` finds what the writer saw, this finds what stands there now — and it lives here because both `bind` and the gate's `show` need it and the dependency rule forbids either importing the other. A function rather than a method so it composes the surface above without widening it, and so the fakes keep working unchanged.
|
|
334
|
+
|
|
330
335
|
```python
|
|
331
336
|
class Registry:
|
|
332
337
|
@classmethod
|
|
@@ -389,4 +394,4 @@ class Registry:
|
|
|
389
394
|
|
|
390
395
|
## Addendum B — CLI assembly
|
|
391
396
|
|
|
392
|
-
Top-level `cli.py` (W1) owns: typer app, registry discovery (nearest `.backdraft/` walking up from cwd; `BACKDRAFT_HOME` override), session resolution (`--session` flag > `BACKDRAFT_SESSION` env > default session), and the `init` / `ingest` / `ls` / `export` / `snapshot-pages` commands. After each ingest, `cli.py` captures page snapshots for any PDF whose extraction carries none — the text-layer path, since `vlm` stores its own — via `extract/snapshots.py` (poppler through pdf2image, `snapshot-pages`' internals). It sits outside the extractor so `pdf-text` stays deterministic regardless of whether poppler is installed, and it is best-effort: a `SnapshotError` leaves ingest at exit 0 with a one-line note naming `snapshot-pages` as the backfill. `cli.py` also owns the network for the same reason: an `ingest` argument with a scheme is fetched through `fetch.py` (stdlib `urllib`, http and https only, redirects followed, 32 MiB cap) and staged in a temporary file named for the content type the server declared — which is what selects the extractor — so extractors stay pure functions of bytes and the registry opens no sockets. It mounts sub-apps `gate/cli.py` (W2: `read`, `search`, `session`), `bind/cli.py` (W3: `bind`), `render/cli.py` (W4: `render`) — each exposes `app = typer.Typer()`; the top level mounts each inside a try/except ImportError so partial merges still run. The `backdraft` console script is declared by W1.
|
|
397
|
+
Top-level `cli.py` (W1) owns: typer app, registry discovery (nearest `.backdraft/` walking up from cwd; `BACKDRAFT_HOME` override), session resolution (`--session` flag > `BACKDRAFT_SESSION` env > default session), and the `init` / `ingest` / `ls` / `export` / `snapshot-pages` commands. `ingest` runs its whole source list: a `BackdraftError` from one source is caught, recorded against that source, and the loop continues, so a folder ingest is never half-attempted. Every failure is printed at the end as one `!` line naming the source and the reason, under a count of what landed, and the command exits 1 — the guard in `cli_context` stays the only place a `BackdraftError` becomes an exit code, since nothing caught per source is re-raised. After each ingest, `cli.py` captures page snapshots for any PDF whose extraction carries none — the text-layer path, since `vlm` stores its own — via `extract/snapshots.py` (poppler through pdf2image, `snapshot-pages`' internals). It sits outside the extractor so `pdf-text` stays deterministic regardless of whether poppler is installed, and it is best-effort: a `SnapshotError` leaves ingest at exit 0 with a one-line note naming `snapshot-pages` as the backfill. `cli.py` also owns the network for the same reason: an `ingest` argument with a scheme is fetched through `fetch.py` (stdlib `urllib`, http and https only, redirects followed, 32 MiB cap) and staged in a temporary file named for the content type the server declared — which is what selects the extractor — so extractors stay pure functions of bytes and the registry opens no sockets. It mounts sub-apps `gate/cli.py` (W2: `read`, `search`, `session`), `bind/cli.py` (W3: `bind`), `render/cli.py` (W4: `render`) — each exposes `app = typer.Typer()`; the top level mounts each inside a try/except ImportError so partial merges still run. The `backdraft` console script is declared by W1.
|
|
@@ -339,7 +339,7 @@ bound 15 claim(s), 16 citation(s) [frontwalk]
|
|
|
339
339
|
unresolved: 1
|
|
340
340
|
overlap: pass 11, skip 4
|
|
341
341
|
value-trace: pass 15
|
|
342
|
-
! unresolved: bd:t12-summary:p4.c1:1a2b
|
|
342
|
+
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @2900
|
|
343
343
|
wrote .backdraft/records/memo.backdraft.json
|
|
344
344
|
exit: 2
|
|
345
345
|
```
|
|
@@ -351,6 +351,13 @@ the current extraction of their source. One did not, and it is named. Bind did
|
|
|
351
351
|
not delete it, did not warn-and-continue, and did not quietly attach it to
|
|
352
352
|
something nearby.
|
|
353
353
|
|
|
354
|
+
**The line item names the sentence, not just the token.** Exit 2 sends you back
|
|
355
|
+
into `memo.md` to fix something, and `bd:t12-summary:p4.c1:1a2b` is the one
|
|
356
|
+
thing you already had. The claim's own words follow it, then `@2900` — the
|
|
357
|
+
claim's character offset into the authored document, which is what tells two
|
|
358
|
+
line items carrying the same token apart when one token is cited from several
|
|
359
|
+
sentences.
|
|
360
|
+
|
|
354
361
|
**Exit code 2 means unresolved citations.** Not a failed check — a citation that
|
|
355
362
|
does not point at anything. It is the code a `Stop` hook or a CI job gates on.
|
|
356
363
|
Exit 1 is a usage error; exit 0 means every citation resolved.
|
|
@@ -492,7 +499,29 @@ backdraft theme show default > ~/.config/backdraft/theme.toml
|
|
|
492
499
|
|
|
493
500
|
## 10. Fixing the failure
|
|
494
501
|
|
|
495
|
-
|
|
502
|
+
Start by asking the token itself. `show` is the inverse of minting: hand it a
|
|
503
|
+
token from anywhere — a bind report, an artifact, a draft — and it says what
|
|
504
|
+
that token names.
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
backdraft show bd:t12-summary:p4.c1:1a2b
|
|
508
|
+
echo "exit: $?"
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
```
|
|
512
|
+
[bd:t12-summary:p4.c1:1a2b] unresolved
|
|
513
|
+
t12-summary carries no anchor named by this token, in any extraction; the locator or the hash is wrong
|
|
514
|
+
|
|
515
|
+
[Table of contents: backdraft read t12-summary]
|
|
516
|
+
exit: 1
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
The slug is real — that document was ingested — so what was invented is the page
|
|
520
|
+
or the hash. (A token whose source had *changed* rather than never existed would
|
|
521
|
+
come back `drifted`, printing both the snippet that was cited and the snippet
|
|
522
|
+
standing at that locator now.)
|
|
523
|
+
|
|
524
|
+
So the broken token was a typo for a real anchor. Find it the same way you
|
|
496
525
|
found everything else:
|
|
497
526
|
|
|
498
527
|
```bash
|
|
@@ -512,8 +541,26 @@ backdraft search "replacement reserve"
|
|
|
512
541
|
[Read the page: backdraft read t12-summary p3]
|
|
513
542
|
```
|
|
514
543
|
|
|
515
|
-
`bd:t12-summary:p3.c4:6f0f` is the snippet that actually says it
|
|
516
|
-
|
|
544
|
+
`bd:t12-summary:p3.c4:6f0f` is the snippet that actually says it — and `show`
|
|
545
|
+
confirms that before the edit, rather than after the re-bind:
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
backdraft show bd:t12-summary:p3.c4:6f0f
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
```
|
|
552
|
+
[bd:t12-summary:p3.c4:6f0f] resolved t12-summary p3.c4
|
|
553
|
+
Capital expenditures of $317,000 were incurred outside the operating statement and are not deducted in arriving at
|
|
554
|
+
net operating income. The largest items were roof replacement on the A and B buildings ($186,000) and parking
|
|
555
|
+
lot resurfacing ($64,500). A replacement reserve of $250 per unit per year, or $32,000 annually, would be
|
|
556
|
+
customary for an asset of this vintage and is not reflected in the figures above.
|
|
557
|
+
|
|
558
|
+
[Read the page: backdraft read t12-summary p3]
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
`show` is the gate like `read` and `search` are, so that snippet is now minted
|
|
562
|
+
into the session and the token is citable. Swapping it in and re-binding would
|
|
563
|
+
take the run to `resolved: 16` and exit 0.
|
|
517
564
|
|
|
518
565
|
**This demo deliberately does not make that fix.** The checked-in `memo.md`
|
|
519
566
|
and `memo.backdraft.html` both carry the unresolved citation, because an
|