backdraft 0.7.0__tar.gz → 0.9.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.7.0 → backdraft-0.9.0}/.claude-plugin/plugin.json +1 -1
- {backdraft-0.7.0 → backdraft-0.9.0}/DESIGN.md +9 -3
- {backdraft-0.7.0 → backdraft-0.9.0}/PKG-INFO +72 -18
- {backdraft-0.7.0 → backdraft-0.9.0}/README.md +69 -17
- backdraft-0.9.0/ROADMAP.md +393 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/SPEC.md +32 -12
- {backdraft-0.7.0 → backdraft-0.9.0}/demo/memo.backdraft.html +172 -89
- {backdraft-0.7.0 → backdraft-0.9.0}/demo/memo.md +14 -1
- {backdraft-0.7.0 → backdraft-0.9.0}/demo/walkthrough.md +201 -36
- {backdraft-0.7.0 → backdraft-0.9.0}/pyproject.toml +4 -1
- {backdraft-0.7.0 → backdraft-0.9.0}/site/demo.html +172 -89
- {backdraft-0.7.0 → backdraft-0.9.0}/site/docs.html +57 -18
- {backdraft-0.7.0 → backdraft-0.9.0}/site/llms.txt +57 -11
- {backdraft-0.7.0 → backdraft-0.9.0}/skills/backdraft/SKILL.md +67 -11
- backdraft-0.9.0/skills/backdraft-artifact/SKILL.md +161 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/skills/backdraft-backfill/SKILL.md +9 -3
- {backdraft-0.7.0 → backdraft-0.9.0}/spec/artifact.md +24 -6
- backdraft-0.9.0/spec/registry.md +332 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/bind/binder.py +18 -24
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/bind/cli.py +66 -19
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/cli.py +123 -15
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/cli_context.py +42 -3
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/fetch.py +59 -6
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/gate/__init__.py +2 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/gate/cli.py +4 -1
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/gate/reader.py +52 -6
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/gate/searcher.py +27 -4
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/kernel/artifact.py +27 -7
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/registry/__init__.py +10 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/registry/store.py +184 -20
- backdraft-0.9.0/src/backdraft/render/cli.py +468 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/html/assets.py +5 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/html/components.py +18 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/html/page.py +17 -7
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/html/text.py +8 -1
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/markdown.py +41 -9
- backdraft-0.9.0/src/backdraft/render/math.py +145 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/sidecar.py +55 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/gate/fake_registry.py +18 -4
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/gate/test_gate_reader.py +89 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/gate/test_gate_searcher.py +81 -1
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/render/backfill.sidecar.json +5 -4
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/render/demo.sidecar.json +5 -4
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_bind_cli.py +129 -1
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_cli.py +189 -6
- backdraft-0.9.0/tests/test_docs.py +133 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_fetch.py +50 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_gate_integration.py +20 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_html.py +14 -1
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_ingest_url.py +95 -8
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_registry.py +72 -2
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_reingest.py +12 -2
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_render_cli.py +60 -1
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_render_markdown.py +33 -0
- backdraft-0.9.0/tests/test_render_math.py +217 -0
- backdraft-0.9.0/tests/test_spec_registry.py +324 -0
- backdraft-0.9.0/tests/test_verify_cli.py +564 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/uv.lock +15 -2
- backdraft-0.7.0/ROADMAP.md +0 -316
- backdraft-0.7.0/skills/backdraft-artifact/SKILL.md +0 -121
- backdraft-0.7.0/src/backdraft/render/cli.py +0 -184
- backdraft-0.7.0/tests/test_docs.py +0 -44
- {backdraft-0.7.0 → backdraft-0.9.0}/.claude-plugin/marketplace.json +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/.github/workflows/ci.yml +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/.github/workflows/release.yml +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/.github/workflows/site.yml +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/.gitignore +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/.python-version +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/LICENSE +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/demo/generate_sources.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/demo/sources/t12-summary.pdf +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/site/index.html +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/site/privacy.html +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/spec/chunking.md +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/spec/tokens.md +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/__init__.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/__main__.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/bind/evidence.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/bind/verify/entail.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/credentials.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/__init__.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/base.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/csv.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/docx.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/html.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/image.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/pdf_text.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/pptx.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/sheet.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/snapshots.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/vlm.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/vlm_settings.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/xls.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/extract/xlsx.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/kernel/chunking.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/kernel/model.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/registry/ledger.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/registry/schema.sql +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/__init__.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/_text.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/footnotes.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/html/__init__.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/html/fmt.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/html/fmt_vectors.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/theme.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/themes/default.toml +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/themes/press.toml +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/src/backdraft/render/themes/slate.toml +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/conftest.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/conftest_registry.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/conftest_render.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/css_util.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/fakes.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/gate/test_gate_cli.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/gate/test_gate_ledger.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/gate/test_reader_show.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/golden_util.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_bind_evidence.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_card_sizing.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_chunking.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_claims.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_credentials.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_extract.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_extract_html.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_fmt_parity.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_footnotes.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_hashing.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_invariants.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_model.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_placement.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_records_location.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_snapshot_settings.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_snapshots.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_theme.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_tokens.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.0}/tests/test_value_trace.py +0 -0
- {backdraft-0.7.0 → backdraft-0.9.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.9.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",
|
|
@@ -29,7 +29,7 @@ Four verbs:
|
|
|
29
29
|
- **`ingest`** — deterministically anchor source documents (PDF/xlsx/md/…) into the registry: extract, locate, snapshot snippets, hash.
|
|
30
30
|
- **`read` / `search`** — the gate. Emit token-marked context for a model. Search results are themselves mintable evidence (search that returns nothing citable forces a page read purely to obtain an anchor).
|
|
31
31
|
- **`bind`** — postprocess a written document: resolve tokens, run enabled verification switches, backfill surface forms (doc names, pages, quotes), emit sidecar claims file + binding report.
|
|
32
|
-
- **`render`** — produce the clickable artifact.
|
|
32
|
+
- **`render`** — produce the clickable artifact. Its reader half is `verify`: an artifact carries the checks it wants run in its own legend, so the tool that writes them ships the command that runs them.
|
|
33
33
|
|
|
34
34
|
### 3. Substrates (sequenced)
|
|
35
35
|
1. **Skill + CLI, together** (first release). The CLI is the system; the skill is ~a page instructing an agent to use it. Ships both the agent-native wedge and the zero-integration backfill demo.
|
|
@@ -143,6 +143,14 @@ Self-contained HTML (plus the machine-readable sidecar): **quotes + receipts + e
|
|
|
143
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
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
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. |
|
|
150
|
+
| 2026-08-24 | **`backdraft verify` — the recipient's check, and the drift bug it found in the spec.** The artifact's whole promise is that someone holding only the file can check it, and until now the instructions for doing so were prose: `skills/backdraft-artifact/SKILL.md` walked an agent through recomputing hashes by hand, which is a deterministic procedure re-implemented per reading — the way a recipient's audit ends up weaker than the producer's. It is now a command, in `render/cli.py` because that module already holds the format's reader half (`render.sidecar`), taking either the `.backdraft.html` or the `.backdraft.json`. Two tiers, and the output names which ran: the record against itself, which needs nothing but the file, and — only where a `.backdraft/` is discoverable — every token re-resolved against the sources. Discovery is from **cwd**, not from the artifact's own directory as `bind` does it from the document's: an artifact is a file people forward, so the folder it landed in is evidence about the recipient's machine and not about which registry produced it. Exit 2 joins `bind`'s, which was documented as "`bind`'s alone"; the shared meaning is "the run completed and the document is not clean", which is exactly what a Stop hook gates on, so a hook written for one now catches the other — the gate's `show` stays at 1, because a lookup answering "that token names nothing" is not a document failing. Read-only and minting nothing, which is the line between it and `show`: showing is minting, and an audit that made its own subject citable would manufacture the evidence it was sent to inspect. A record faithfully carrying an `unresolved` citation still passes tier one — a kept failure is the record working, so the same bytes exit 0 in an inbox and 2 in the project, answering different questions. Two consolidations came with it rather than a third copy of each: `registry.citation_for` is the token-to-status walk `bind` and `verify` both need and the dependency rule forbids either importing from the other, so it sits beside `current_at` for the same reason, with `not_shown` deliberately left out as a fact about a session the caller holds; and `cli_context.claim_words` is the line-item shape, since two commands reporting on the same claims must not print two layouts. The real find was in the spec. `spec/artifact.md` § Checking an artifact and the legend's `verify_this_record` both said the token's hash must prefix `anchor.snippet_sha256` — false for every `drifted` citation by construction, since the token names `drifted_from` (what the author cited) while `anchor` carries what stands there now, and the two differing *is* what drift means. Implemented as written, `verify` would have called every drifted artifact forged: the one status the format exists to carry honestly. Both statements are corrected, which moves the legend and therefore every artifact's bytes — goldens and the demo regenerated — and is worth the churn, because a known-wrong normative instruction shipped inside every artifact is a worse cost than a diff. |
|
|
151
|
+
| 2026-08-24 | **Math is rendered, not forbidden, and MathML is why that is affordable.** An agent writing a memo about coverage writes `$\mathrm{DSCR} = \frac{NOI}{D}$`, and the renderer handed the reader raw TeX — or worse, corrupted TeX, since `$x_1$ and $x_2$` paired its subscripts into emphasis and `\(a^2\)` lost its delimiters to the backslash-escape pass. The alternative on the table was to forbid math in the writing skill, and it was rejected twice over: this product's documents are exactly the finance and science memos that contain formulas, and a skill instruction cannot be enforced against an agent that did not read it carefully, so the corruption would have arrived anyway. Shipping *unrendered* TeX was rejected too — nobody wants to read `\frac{NOI}{D}`, and a document a reader cannot read is not a lesser artifact but a broken one. The target is MathML via `latex2mathml`, behind a `[math]` extra: it is static markup the browser lays out itself, so rules 1 and 2 of § The HTML artifact hold *by construction* rather than by care — no script, no font file, no request — where inlining KaTeX would have cost roughly a megabyte of fonts as `data:` URIs on every artifact, math or not. The tradeoff, stated because it is the one place the format's fixed-at-render promise is soft: MathML leans on whatever math font the reader's system has, so two readers of one artifact may see different glyph quality. That is a smaller and more honest cost than the alternative, and it is bounded — the markup, the spacing, and every character are identical; only the typeface is not. Dollars were the real hazard and the guard is Pandoc's: a memo saying `$250 per unit` and `$1.2M` twice in a paragraph would pair them into nonsense under a naive rule, so an opening `$` must be followed by a non-space and a closing `$` must be preceded by one and not followed by a digit — a currency amount is preceded by a space and therefore can never close a span. Code is skipped rather than scanned, so a shell snippet's `$PATH` and `$HOME` are safe, and the demo regenerates with zero math elements, which is the check that matters. Math is opt-in per call and **off for evidence**: a receipt's snippet must read as it was extracted, not as this renderer would prefer it. Conversion failure is data, at two different reliabilities — `latex2mathml` raises for some malformed input, which surfaces as a wavy mark at the formula and a note giving the reason, and silently degrades for the rest (`\notacommand{x}` becomes an identifier), which cannot be caught and is recorded here as a known limit rather than papered over. |
|
|
152
|
+
| 2026-08-25 | **The registry export is a specified format, or it is not a format.** `backdraft export` has written `"$format": "backdraft/registry-v1"` since v0 — a version string promising a specification that did not exist — while being the only complete, portable form a registry has, and therefore what a second implementation, a migration or an audit actually reads. The gap stopped being theoretical when URL sources added a conditional `meta` key to every exported document: a change to a format with nothing to change against. So spec/registry.md joins tokens, chunking and artifact as normative prose, and the pin is the spec itself rather than a golden file beside it — tests/test_spec_registry.py parses the spec's key tables, exports a registry built to exercise every branch, and fails in both directions, so an undocumented key and a documented-but-absent one are equally a test failure. Two boundaries were drawn rather than blurred. `bindings[].report` stays spec/artifact.md's, quoted opaquely here, because one format with two specifications is one too many and they would drift. And the export is content, not a backup: page images, sheet styling and the FTS index are deliberately outside it, so an export round-trips every citation and every receipt and does not round-trip a database — stated in the spec as a limit rather than left for a reader to discover by restoring one. |
|
|
153
|
+
| 2026-08-26 | **`search` reports the size of the answer, not the size of the page.** `--limit` defaults to 20 and the count line said `20 results` whether twenty matched or two hundred did, which is the silent cap this repo rejects everywhere else — `read` closes with `[Showing 0-464 of 3115 chars. Continue with: …]` for exactly this reason. Search is the surface an agent leans on hardest, since it is how a claim finds its evidence, and relevance order is not the same as the sentence the writer needed: a reader told it has twenty results picks the best of twenty and never learns the twenty-first existed. So `SearchResults` gains `total` beside `phrase_fallback` — the same shape for the same reason, an out-of-band fact riding on Addendum A's pinned `list[SearchHit]` so every consumer and every fake is untouched — and a capped run renders `2 of 56 results` plus a closing line naming the exact widening command, shell-quoted, because a hint carrying `$` or a space that has to be repaired before it runs is not a hint. The count is a `COUNT(*)` over the *same* joins the fetch runs, shared through `_match_sql` rather than written twice: a count that joined differently would report a total the caller can never reach. It is asked for only when the page came back full, since a page the limit did not fill is already the whole answer — so the common uncapped run still runs one query, and the output it prints is byte-identical to what it printed before, pinned as a test the way the artifact's bytes are. Two things were deliberately not built. No `--offset`: paging a relevance-ordered list is a different design question, and `--limit` already exists for the hint to name. And the total is not carried into anything durable — no token, anchor, receipt or artifact byte moves, because how many other sentences also matched is a fact about a query, not about a citation. |
|
|
146
154
|
|
|
147
155
|
## Open
|
|
148
156
|
|
|
@@ -150,8 +158,6 @@ Queued and parked work lives in ROADMAP.md; this list is questions still
|
|
|
150
158
|
awaiting a decision:
|
|
151
159
|
|
|
152
160
|
- Re-bind/orphan pass on re-ingest of changed docs (chunk ordinal drift)
|
|
153
|
-
- Slug assignment/collision rules (the trailing-hyphen truncation from the
|
|
154
|
-
first field trial lives here)
|
|
155
161
|
- Distribution: published to PyPI as `backdraft` 0.2.0 (2026-07-28); domain
|
|
156
162
|
backdraft.dev acquired; still open: skill registration, deploying site/
|
|
157
163
|
- First external user (design-partner validation layer vs eval substrate vs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: backdraft
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -18,6 +18,8 @@ Requires-Dist: python-pptx>=1.0
|
|
|
18
18
|
Requires-Dist: typer>=0.12
|
|
19
19
|
Provides-Extra: entail
|
|
20
20
|
Requires-Dist: anthropic>=0.40; extra == 'entail'
|
|
21
|
+
Provides-Extra: math
|
|
22
|
+
Requires-Dist: latex2mathml>=3.77; extra == 'math'
|
|
21
23
|
Provides-Extra: vlm
|
|
22
24
|
Provides-Extra: xls
|
|
23
25
|
Requires-Dist: python-calamine>=0.3; extra == 'xls'
|
|
@@ -67,11 +69,27 @@ documents: 0
|
|
|
67
69
|
… plus where settings live and what to do next
|
|
68
70
|
|
|
69
71
|
$ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
|
|
70
|
-
t12-summary t12-summary.pdf pdf 3 pages
|
|
71
|
-
underwriting-model underwriting-model.xlsx xlsx 2 sheets
|
|
72
|
+
t12-summary t12-summary.pdf pdf 3 pages 8865 chars
|
|
73
|
+
underwriting-model underwriting-model.xlsx xlsx 2 sheets 2357 chars
|
|
72
74
|
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.
|
|
75
|
+
|
|
76
|
+
$ backdraft ingest "https://en.wikipedia.org/w/index.php?title=Franklin_County,_Ohio&oldid=1367935775" --slug franklin-county
|
|
77
|
+
franklin-county https://en.wikipedia.org/w/index.php?title=Franklin_County,_Ohio&oldid=1367935775 html 1 page 34141 chars
|
|
73
78
|
```
|
|
74
79
|
|
|
80
|
+
The demo cites that page for market context. It is Wikipedia's *permanent
|
|
81
|
+
link* — `?oldid=` serves one revision's bytes forever — because a citation into
|
|
82
|
+
a live article reports `drifted` the moment somebody edits it, and the quoted
|
|
83
|
+
sentence would no longer be the sentence there.
|
|
84
|
+
|
|
85
|
+
Every ingest line ends with how much text came out, and says which of three
|
|
86
|
+
things happened: a document created, a **new generation** of one whose bytes
|
|
87
|
+
moved — which is when citations into the previous snapshot start reporting
|
|
88
|
+
`drifted` — or `unchanged`, a no-op because re-running would reproduce what is
|
|
89
|
+
already there. When almost no text came out, a note names the likely cause (a
|
|
90
|
+
scan with no text layer, a page behind a login) and what to do; it is a note at
|
|
91
|
+
exit 0, because a thin snapshot is still a real one.
|
|
92
|
+
|
|
75
93
|
**Read.** The page arrives with a citable name over each chunk. This is the whole
|
|
76
94
|
mechanism: what you can cite is exactly what you were shown.
|
|
77
95
|
|
|
@@ -112,6 +130,11 @@ $ backdraft search "24850000"
|
|
|
112
130
|
[Read the page: backdraft read underwriting-model p2]
|
|
113
131
|
```
|
|
114
132
|
|
|
133
|
+
Search shows twenty results by default. A run that hit the limit says so —
|
|
134
|
+
`20 of 56 results` — and closes with the command that shows the rest, because
|
|
135
|
+
"20 results" over an answer of 56 is how a reader concludes it has seen
|
|
136
|
+
everything when it has seen a third.
|
|
137
|
+
|
|
115
138
|
A cell you can already see mints directly:
|
|
116
139
|
|
|
117
140
|
```console
|
|
@@ -150,12 +173,13 @@ Multiple citations are `;`-separated in one href.
|
|
|
150
173
|
|
|
151
174
|
```console
|
|
152
175
|
$ backdraft bind memo.md --session s-bridgeview --check value-trace,overlap
|
|
153
|
-
bound
|
|
154
|
-
resolved:
|
|
176
|
+
bound 17 claim(s), 18 citation(s) [frontwalk]
|
|
177
|
+
resolved: 17
|
|
155
178
|
unresolved: 1
|
|
156
|
-
overlap: pass
|
|
157
|
-
|
|
158
|
-
|
|
179
|
+
overlap: pass 13, skip 4
|
|
180
|
+
skip 4 — wording overlap does not apply to a single cell
|
|
181
|
+
value-trace: pass 17
|
|
182
|
+
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @3629
|
|
159
183
|
wrote .backdraft/records/memo.backdraft.json
|
|
160
184
|
|
|
161
185
|
$ echo $?
|
|
@@ -177,6 +201,29 @@ memo.backdraft.html
|
|
|
177
201
|
That file is the whole deliverable: send it over email or Slack and the
|
|
178
202
|
recipient gets the full experience by double-clicking it.
|
|
179
203
|
|
|
204
|
+
**The recipient can check it.** `backdraft verify` takes the artifact or its
|
|
205
|
+
sidecar and does the checking that the file is built to make possible: every
|
|
206
|
+
snippet rehashed against the sha256 recorded beside it, every token checked
|
|
207
|
+
against the anchor it names, `summary` recounted from the claims.
|
|
208
|
+
|
|
209
|
+
```console
|
|
210
|
+
$ backdraft verify memo.backdraft.html
|
|
211
|
+
checked memo.backdraft.html [backdraft/artifact-v1]
|
|
212
|
+
receipts: 17 of 17 hold
|
|
213
|
+
record: 17 claim(s), 18 citation(s); the summary recount agrees
|
|
214
|
+
recorded: resolved 17, unresolved 1
|
|
215
|
+
sources: no .backdraft/ found from here — not re-checked
|
|
216
|
+
[Re-check against the sources: run this inside the project it was bound in.]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
That first tier needs nothing but the file, which is the point — it is the check
|
|
220
|
+
someone who was emailed the artifact can still make. Run it inside the project
|
|
221
|
+
the document was bound in and it also re-resolves every citation against the
|
|
222
|
+
registry, so a source that has changed since shows up as `drifted` rather than
|
|
223
|
+
going unnoticed. Exit 0 when everything it checked passed, 2 when something did
|
|
224
|
+
not, so a hook can gate on it. It opens no session and mints nothing: an audit
|
|
225
|
+
must not make its subject citable.
|
|
226
|
+
|
|
180
227
|
**Theming.** The artifact ships with a default look and two alternates,
|
|
181
228
|
`press` and `slate`:
|
|
182
229
|
|
|
@@ -244,18 +291,23 @@ address.
|
|
|
244
291
|
|
|
245
292
|
```console
|
|
246
293
|
$ backdraft ingest https://example.com/reports/q4-2025
|
|
247
|
-
q4-2025 q4-2025
|
|
294
|
+
q4-2025 https://example.com/reports/q4-2025 html 1 page 18402 chars
|
|
248
295
|
|
|
249
296
|
$ backdraft ls
|
|
250
|
-
q4-2025
|
|
297
|
+
q4-2025 https://example.com/reports/q4-2025 html 1 page
|
|
251
298
|
```
|
|
252
299
|
|
|
300
|
+
Every surface names a fetched source by its page. The filename you never see —
|
|
301
|
+
`q4-2025.html` — is the temporary file the fetch staged the bytes in, and it
|
|
302
|
+
exists on nobody's disk, so `ingest`, `ls`, `backdraft read` and the artifact
|
|
303
|
+
all show the URL in its place rather than beside it: two names for one thing
|
|
304
|
+
would let the invented one look authoritative.
|
|
305
|
+
|
|
253
306
|
The origin travels into the artifact too. A claim citing a fetched page shows
|
|
254
|
-
the URL as a link on its receipt, with the date the bytes were taken
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
says it.
|
|
307
|
+
the URL as a link on its receipt, with the date the bytes were taken. That
|
|
308
|
+
pointer is the half of citing a web page a frozen receipt cannot answer on its
|
|
309
|
+
own: the snippet says what the page said, the link is how a reader asks whether
|
|
310
|
+
it still says it.
|
|
259
311
|
|
|
260
312
|
What it does not do, stated rather than worked around: JavaScript-rendered
|
|
261
313
|
pages give you whatever the server sends to a plain GET, pages behind a login
|
|
@@ -299,12 +351,13 @@ the keyless text layer and says so — still storing each page's image, which it
|
|
|
299
351
|
renders locally. PDF page rendering needs poppler, installed separately; without
|
|
300
352
|
it ingest succeeds without the images and names `snapshot-pages` as the backfill.
|
|
301
353
|
|
|
302
|
-
|
|
354
|
+
Three extras:
|
|
303
355
|
|
|
304
356
|
| Extra | Adds | For |
|
|
305
357
|
|---|---|---|
|
|
306
358
|
| `[entail]` | `anthropic` | `bind --check entail`, the model-judge verifier |
|
|
307
359
|
| `[xls]` | `python-calamine` | legacy `.xls` workbooks, values only, rendered the same shape as xlsx |
|
|
360
|
+
| `[math]` | `latex2mathml` | LaTeX in a document becomes MathML in the artifact; without it, formulas render verbatim and `render` says so |
|
|
308
361
|
|
|
309
362
|
`backdraft[vlm]` still installs: it is an empty compat alias from when the
|
|
310
363
|
vision deps were an extra.
|
|
@@ -325,7 +378,7 @@ Three agent skills ship inside the package and in this repo:
|
|
|
325
378
|
|---|---|
|
|
326
379
|
| [`backdraft`](skills/backdraft/SKILL.md) | writing a new document from sources, citing as it goes |
|
|
327
380
|
| [`backdraft-backfill`](skills/backdraft-backfill/SKILL.md) | attributing a document that already exists |
|
|
328
|
-
| [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | reading someone else's artifact cold |
|
|
381
|
+
| [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | checking and reading someone else's artifact cold |
|
|
329
382
|
|
|
330
383
|
The CLI is the system; a skill is one page telling an agent to use it. Nothing in
|
|
331
384
|
the skills is required to use backdraft by hand. How the skills reach your agent
|
|
@@ -383,9 +436,10 @@ spans are citable. In a sandboxed session run every command as
|
|
|
383
436
|
| [spec/tokens.md](spec/tokens.md) | the citation token grammar, normatively |
|
|
384
437
|
| [spec/chunking.md](spec/chunking.md) | the deterministic chunker |
|
|
385
438
|
| [spec/artifact.md](spec/artifact.md) | the artifact format, sidecar payload, legend, HTML rules |
|
|
439
|
+
| [spec/registry.md](spec/registry.md) | the registry export format `backdraft export` writes |
|
|
386
440
|
| [demo/walkthrough.md](demo/walkthrough.md) | the whole thing end to end, with real output |
|
|
387
441
|
|
|
388
|
-
The
|
|
442
|
+
The four files under `spec/` are the portable specification: another
|
|
389
443
|
implementation reads them and nothing else.
|
|
390
444
|
|
|
391
445
|
## Status
|
|
@@ -42,11 +42,27 @@ documents: 0
|
|
|
42
42
|
… plus where settings live and what to do next
|
|
43
43
|
|
|
44
44
|
$ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
|
|
45
|
-
t12-summary t12-summary.pdf pdf 3 pages
|
|
46
|
-
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
|
|
47
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
|
|
48
51
|
```
|
|
49
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
|
+
|
|
50
66
|
**Read.** The page arrives with a citable name over each chunk. This is the whole
|
|
51
67
|
mechanism: what you can cite is exactly what you were shown.
|
|
52
68
|
|
|
@@ -87,6 +103,11 @@ $ backdraft search "24850000"
|
|
|
87
103
|
[Read the page: backdraft read underwriting-model p2]
|
|
88
104
|
```
|
|
89
105
|
|
|
106
|
+
Search shows twenty results by default. A run that hit the limit says so —
|
|
107
|
+
`20 of 56 results` — and closes with the command that shows the rest, because
|
|
108
|
+
"20 results" over an answer of 56 is how a reader concludes it has seen
|
|
109
|
+
everything when it has seen a third.
|
|
110
|
+
|
|
90
111
|
A cell you can already see mints directly:
|
|
91
112
|
|
|
92
113
|
```console
|
|
@@ -125,12 +146,13 @@ Multiple citations are `;`-separated in one href.
|
|
|
125
146
|
|
|
126
147
|
```console
|
|
127
148
|
$ backdraft bind memo.md --session s-bridgeview --check value-trace,overlap
|
|
128
|
-
bound
|
|
129
|
-
resolved:
|
|
149
|
+
bound 17 claim(s), 18 citation(s) [frontwalk]
|
|
150
|
+
resolved: 17
|
|
130
151
|
unresolved: 1
|
|
131
|
-
overlap: pass
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
overlap: pass 13, skip 4
|
|
153
|
+
skip 4 — wording overlap does not apply to a single cell
|
|
154
|
+
value-trace: pass 17
|
|
155
|
+
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @3629
|
|
134
156
|
wrote .backdraft/records/memo.backdraft.json
|
|
135
157
|
|
|
136
158
|
$ echo $?
|
|
@@ -152,6 +174,29 @@ memo.backdraft.html
|
|
|
152
174
|
That file is the whole deliverable: send it over email or Slack and the
|
|
153
175
|
recipient gets the full experience by double-clicking it.
|
|
154
176
|
|
|
177
|
+
**The recipient can check it.** `backdraft verify` takes the artifact or its
|
|
178
|
+
sidecar and does the checking that the file is built to make possible: every
|
|
179
|
+
snippet rehashed against the sha256 recorded beside it, every token checked
|
|
180
|
+
against the anchor it names, `summary` recounted from the claims.
|
|
181
|
+
|
|
182
|
+
```console
|
|
183
|
+
$ backdraft verify memo.backdraft.html
|
|
184
|
+
checked memo.backdraft.html [backdraft/artifact-v1]
|
|
185
|
+
receipts: 17 of 17 hold
|
|
186
|
+
record: 17 claim(s), 18 citation(s); the summary recount agrees
|
|
187
|
+
recorded: resolved 17, unresolved 1
|
|
188
|
+
sources: no .backdraft/ found from here — not re-checked
|
|
189
|
+
[Re-check against the sources: run this inside the project it was bound in.]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
That first tier needs nothing but the file, which is the point — it is the check
|
|
193
|
+
someone who was emailed the artifact can still make. Run it inside the project
|
|
194
|
+
the document was bound in and it also re-resolves every citation against the
|
|
195
|
+
registry, so a source that has changed since shows up as `drifted` rather than
|
|
196
|
+
going unnoticed. Exit 0 when everything it checked passed, 2 when something did
|
|
197
|
+
not, so a hook can gate on it. It opens no session and mints nothing: an audit
|
|
198
|
+
must not make its subject citable.
|
|
199
|
+
|
|
155
200
|
**Theming.** The artifact ships with a default look and two alternates,
|
|
156
201
|
`press` and `slate`:
|
|
157
202
|
|
|
@@ -219,18 +264,23 @@ address.
|
|
|
219
264
|
|
|
220
265
|
```console
|
|
221
266
|
$ backdraft ingest https://example.com/reports/q4-2025
|
|
222
|
-
q4-2025 q4-2025
|
|
267
|
+
q4-2025 https://example.com/reports/q4-2025 html 1 page 18402 chars
|
|
223
268
|
|
|
224
269
|
$ backdraft ls
|
|
225
|
-
q4-2025
|
|
270
|
+
q4-2025 https://example.com/reports/q4-2025 html 1 page
|
|
226
271
|
```
|
|
227
272
|
|
|
273
|
+
Every surface names a fetched source by its page. The filename you never see —
|
|
274
|
+
`q4-2025.html` — is the temporary file the fetch staged the bytes in, and it
|
|
275
|
+
exists on nobody's disk, so `ingest`, `ls`, `backdraft read` and the artifact
|
|
276
|
+
all show the URL in its place rather than beside it: two names for one thing
|
|
277
|
+
would let the invented one look authoritative.
|
|
278
|
+
|
|
228
279
|
The origin travels into the artifact too. A claim citing a fetched page shows
|
|
229
|
-
the URL as a link on its receipt, with the date the bytes were taken
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
says it.
|
|
280
|
+
the URL as a link on its receipt, with the date the bytes were taken. That
|
|
281
|
+
pointer is the half of citing a web page a frozen receipt cannot answer on its
|
|
282
|
+
own: the snippet says what the page said, the link is how a reader asks whether
|
|
283
|
+
it still says it.
|
|
234
284
|
|
|
235
285
|
What it does not do, stated rather than worked around: JavaScript-rendered
|
|
236
286
|
pages give you whatever the server sends to a plain GET, pages behind a login
|
|
@@ -274,12 +324,13 @@ the keyless text layer and says so — still storing each page's image, which it
|
|
|
274
324
|
renders locally. PDF page rendering needs poppler, installed separately; without
|
|
275
325
|
it ingest succeeds without the images and names `snapshot-pages` as the backfill.
|
|
276
326
|
|
|
277
|
-
|
|
327
|
+
Three extras:
|
|
278
328
|
|
|
279
329
|
| Extra | Adds | For |
|
|
280
330
|
|---|---|---|
|
|
281
331
|
| `[entail]` | `anthropic` | `bind --check entail`, the model-judge verifier |
|
|
282
332
|
| `[xls]` | `python-calamine` | legacy `.xls` workbooks, values only, rendered the same shape as xlsx |
|
|
333
|
+
| `[math]` | `latex2mathml` | LaTeX in a document becomes MathML in the artifact; without it, formulas render verbatim and `render` says so |
|
|
283
334
|
|
|
284
335
|
`backdraft[vlm]` still installs: it is an empty compat alias from when the
|
|
285
336
|
vision deps were an extra.
|
|
@@ -300,7 +351,7 @@ Three agent skills ship inside the package and in this repo:
|
|
|
300
351
|
|---|---|
|
|
301
352
|
| [`backdraft`](skills/backdraft/SKILL.md) | writing a new document from sources, citing as it goes |
|
|
302
353
|
| [`backdraft-backfill`](skills/backdraft-backfill/SKILL.md) | attributing a document that already exists |
|
|
303
|
-
| [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | reading someone else's artifact cold |
|
|
354
|
+
| [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | checking and reading someone else's artifact cold |
|
|
304
355
|
|
|
305
356
|
The CLI is the system; a skill is one page telling an agent to use it. Nothing in
|
|
306
357
|
the skills is required to use backdraft by hand. How the skills reach your agent
|
|
@@ -358,9 +409,10 @@ spans are citable. In a sandboxed session run every command as
|
|
|
358
409
|
| [spec/tokens.md](spec/tokens.md) | the citation token grammar, normatively |
|
|
359
410
|
| [spec/chunking.md](spec/chunking.md) | the deterministic chunker |
|
|
360
411
|
| [spec/artifact.md](spec/artifact.md) | the artifact format, sidecar payload, legend, HTML rules |
|
|
412
|
+
| [spec/registry.md](spec/registry.md) | the registry export format `backdraft export` writes |
|
|
361
413
|
| [demo/walkthrough.md](demo/walkthrough.md) | the whole thing end to end, with real output |
|
|
362
414
|
|
|
363
|
-
The
|
|
415
|
+
The four files under `spec/` are the portable specification: another
|
|
364
416
|
implementation reads them and nothing else.
|
|
365
417
|
|
|
366
418
|
## Status
|