backdraft 0.11.0__tar.gz → 0.13.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.11.0 → backdraft-0.13.0}/.claude-plugin/plugin.json +1 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/DESIGN.md +7 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/PKG-INFO +115 -10
- {backdraft-0.11.0 → backdraft-0.13.0}/README.md +114 -9
- backdraft-0.13.0/ROADMAP.md +620 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/SPEC.md +26 -8
- {backdraft-0.11.0 → backdraft-0.13.0}/demo/memo.backdraft.html +2 -2
- {backdraft-0.11.0 → backdraft-0.13.0}/demo/walkthrough.md +49 -5
- {backdraft-0.11.0 → backdraft-0.13.0}/pyproject.toml +1 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/site/demo.html +2 -2
- {backdraft-0.11.0 → backdraft-0.13.0}/site/docs.html +30 -11
- {backdraft-0.11.0 → backdraft-0.13.0}/site/llms.txt +74 -13
- {backdraft-0.11.0 → backdraft-0.13.0}/skills/backdraft/SKILL.md +78 -17
- {backdraft-0.11.0 → backdraft-0.13.0}/skills/backdraft-artifact/SKILL.md +41 -6
- {backdraft-0.11.0 → backdraft-0.13.0}/skills/backdraft-backfill/SKILL.md +12 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/spec/artifact.md +89 -9
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/bind/cli.py +74 -25
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/bind/verify/entail.py +27 -10
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/cli.py +478 -40
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/cli_context.py +85 -4
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/__init__.py +2 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/base.py +37 -6
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/snapshots.py +43 -4
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/gate/__init__.py +2 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/gate/cli.py +5 -2
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/gate/reader.py +230 -13
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/gate/searcher.py +44 -30
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/kernel/artifact.py +40 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/kernel/chunking.py +10 -2
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/registry/__init__.py +2 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/registry/schema.sql +1 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/registry/store.py +72 -5
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/cli.py +289 -68
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/math.py +5 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/sidecar.py +18 -13
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/gate/fake_registry.py +38 -13
- backdraft-0.13.0/tests/gate/test_gate_adjoining.py +266 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/gate/test_gate_cli.py +9 -3
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/gate/test_gate_ledger.py +11 -2
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/gate/test_gate_reader.py +26 -7
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/gate/test_gate_searcher.py +41 -12
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/render/backfill.sidecar.json +1 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/render/demo.sidecar.json +1 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_bind_cli.py +166 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_cli.py +160 -0
- backdraft-0.13.0/tests/test_doctor.py +321 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_extract.py +28 -2
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_forget.py +87 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_gate_integration.py +137 -2
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_ingest_url.py +14 -0
- backdraft-0.13.0/tests/test_locate.py +490 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_reingest.py +94 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_render_cli.py +34 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_verify_cli.py +414 -1
- {backdraft-0.11.0 → backdraft-0.13.0}/uv.lock +1 -1
- backdraft-0.11.0/ROADMAP.md +0 -483
- {backdraft-0.11.0 → backdraft-0.13.0}/.claude-plugin/marketplace.json +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/.github/workflows/ci.yml +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/.github/workflows/release.yml +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/.github/workflows/site.yml +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/.gitignore +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/.python-version +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/LICENSE +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/demo/generate_sources.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/demo/memo.md +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/demo/sources/t12-summary.pdf +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/site/index.html +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/site/privacy.html +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/spec/chunking.md +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/spec/registry.md +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/spec/tokens.md +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/__main__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/bind/binder.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/bind/evidence.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/credentials.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/csv.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/docx.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/html.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/image.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/pdf_text.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/pptx.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/sheet.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/vlm.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/vlm_settings.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/xls.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/extract/xlsx.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/fetch.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/kernel/model.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/registry/ledger.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/_text.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/footnotes.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/html/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/html/assets.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/html/components.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/html/fmt.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/html/fmt_vectors.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/html/page.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/html/text.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/markdown.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/theme.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/themes/default.toml +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/themes/press.toml +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/src/backdraft/render/themes/slate.toml +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/conftest.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/conftest_registry.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/conftest_render.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/continuation_util.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/css_util.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/fakes.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/gate/test_reader_show.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/golden_util.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_bind_evidence.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_card_sizing.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_chunking.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_claims.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_credentials.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_docs.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_dry_run.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_extract_html.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_fetch.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_fmt_parity.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_footnotes.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_hashing.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_html.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_invariants.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_model.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_placement.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_records_location.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_registry.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_render_markdown.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_render_math.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_snapshot_settings.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_snapshots.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_spec_registry.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_theme.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_tokens.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.0}/tests/test_value_trace.py +0 -0
- {backdraft-0.11.0 → backdraft-0.13.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.13.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",
|
|
@@ -159,13 +159,19 @@ Self-contained HTML (plus the machine-readable sidecar): **quotes + receipts + e
|
|
|
159
159
|
| 2026-09-08 | **A page read has a budget, and the line that closes a short read is a command that runs.** `backdraft read franklin-county p1` printed 36,442 characters and stopped on `49 languages Add topic`, which reads exactly like a truncation and was not one: `--limit` defaulted to *all of them*, so the continuation line this repo is proud of — `[Showing 0-464 of 3115 chars. Continue with: …]` — only ever appeared to a caller who already knew to pass `--limit`. The gate is the one surface an agent is required to use, which makes the default the case that matters, and unbounded meant a single read could spend a large fraction of a context window with nothing in the output to say so — the same silent-cap family as the 2026-08-26 `search` row, from the other end. The choice was a default budget against staying unbounded and *always* closing with the size, and byte-identity settled it: always-closing puts a new line on every read of every small page, and a small page is both the common case and a contract, so `DEFAULT_BUDGET` in `gate/reader.py` is 12,000 characters and 200 sheet rows — roughly three full pages of prose, and about the same spend counted the way a sheet is — while the demo's `read t12-summary p1`, `p1-3` and `assumptions` come back byte for byte what they were. The budget is **soft**, which is the half that keeps the format honest: `_Window.take` was already offering whole chunks and whole rows, so a chunk longer than the whole budget is still shown whole rather than cut under its token, and no token ever stands above text the reader received only part of — now pinned by a test that walks every window at five limits and compares each printed block against the anchor's own snippet, rather than left as a property the code happens to have. What a read shows is unchanged; only how much of it arrives at once. The closing line then had to become the thing it claimed to be, because a budget makes it the normal path rather than the expert one, and it was wrong in three ways nobody had hit: it dropped the caller's `--limit`, so continuing a deliberately narrow read silently widened it to the default; it dropped `--session`, so continuing a read performed under `--session s-deal` minted the rest of the page into the default ledger and the writer's own reading came back `not_shown` — the one failure this mechanism exists to catch, manufactured by the hint telling the truth badly; and it did not quote the selector, so a page read by its name (`Franklin County, Ohio - Wikipedia`) named a command no shell could run, which `searcher._widen_hint` had already learned to avoid. The session flag is keyed on the **rule** that supplied the id and not on the id, which is the opposite of the 2026-08-31 note and right for the opposite reason: that note warns about a shared ledger however you reached it, while this asks whether the id survives the command — an exported `BACKDRAFT_SESSION` does and needs no flag, a typed `--session` does not. `gate.cli` makes that call and the reader owns the wording, the same split `session show` makes for its note, so the gate's library half still does not import the CLI's constants. The default is not echoed back as a `--limit`: a command carrying 12000 forward would go on meaning 12000 after the number moved. Two costs stated. There is no `--all`; the way to read a long page in one call is the `--limit` the closing line already names, which is why that line reports the total and not just the remainder. And `demo/walkthrough.md` quotes the closing line rather than adding the page read that produces it — the walkthrough's `session show` block is the ledger a clean run leaves behind, a new minting command would move those counts, and the numbers cannot be regenerated without re-fetching a source whose bytes carry a render date and would break every token in the demo. |
|
|
160
160
|
| 2026-09-09 | **A source that came back a shell says so on every surface that lists it, not only in the `ingest` that printed the note.** The 2026-08-20 row gave `ingest` a character count and a `note: little text extracted` naming the cause, and both were printed once and then gone — nothing stored the signal and no other surface carried it, so off `backdraft read`'s list a 34,000-character article and a 27-character login wall were the same row. The asymmetry is the point: the skill says to ingest everything up front and then write, a registry travels with the project folder, and the agent choosing evidence is usually not the process that ingested — so the one signal that matters was the one it was least likely to be present for. `gate.thin_mark` now closes the row of a thin source in the gate's document list, in its table-of-contents headline (inside the parenthesis, where everything else describing the source already lives) and in `ls`, reading `little text: 27 chars`; the count is there because 0 characters and 190 are different problems. Reporting, not storage: the count is `sum(len(page.text))` over the current extraction, so nothing was added to the registry and no migration exists to have gone wrong. The owner is `gate/reader.py` beside `unit` rather than `kernel.model` beside `source_name`, which is the other precedent and the wrong one here: the kernel is the spec's vocabulary, `source_name` is a pure rule about a `Document` that bind also needs, and this is a **heuristic** about how a surface describes an extraction — putting a tunable threshold down there would read as normative to the next implementation, and only the gate and the CLI (which imports downward from it, as it already does for `unit`) ever ask. `cli.THIN_SOURCE_CHARS` is gone; `ingest`'s own line, its grouped note and all three list surfaces now derive the same number from `gate.extracted_chars`, which is the whole of the item — a second implementation of the count would have defeated it. Only thin rows are marked, so a registry of ordinary sources gains no column and prints byte for byte what it always did — the gate's count column is padded and `_block`'s existing rstrip takes the padding back off an unmarked row, and `ls` adds a fifth tab-separated field only where there is one to add. That fifth field is the same slot the 2026-08-18 row deliberately emptied when `ls` stopped printing a fetched source's URL beside its invented filename, and the difference is worth stating: that field was a second name for a thing that already had one, this one appears only on the rows nothing else says anything about. Two costs. The heuristic now lives on a persistent surface rather than in a note that scrolled away, so a genuinely tiny real source carries the mark for as long as it is in the registry — accepted on the 2026-08-20 terms, which still hold exactly: display only, exit 0, and no token, anchor, status or byte of the artifact derives from it. And the gate's own fixtures had to grow: the miniature standing in for a 34,000-character Wikipedia article held 112 characters, which would have pinned `little text` into the golden strings that document the list format — a fixture that small was already lying about what it represented, and the marks in `tests/gate` are now on sources that really are shells. |
|
|
161
161
|
| 2026-09-10 | **What a source will be called is a question `ingest` answers with `--dry-run`, not a sibling command.** Three docs tell an agent to pass `--slug` when it ingests a URL, because the default may name a site rather than a page and a slug is permanent the moment a token carries it — and none of them let the agent find out what the default would be. The fix is that the question is cheap: `fetch.filename_for` settles the stem from the address alone (the 2026-08-19 row's own words), so the only way to ask it was to ingest and live with the answer, and getting it wrong costs a re-ingest under a new slug plus a rewritten draft. The shape considered against a flag was a sibling command — `backdraft name <source>` — and the objection to a flag is real: a verb that does not do its verb reads badly, and `ingest` is a writing verb. `--dry-run` answers it, and that is the whole reason it is spelled that way rather than `--plan` or `--name`: the convention is universal and says exactly the thing the objection worries about, that the verb did not run. Two things settled it beyond the wording. The question arises *while composing the ingest command*, so the answer belongs on that command's own help, next to the `--slug` it exists to inform — a separate noun is discoverable only by reading the top-level help of a CLI whose surface is deliberately four verbs. And the prediction has to be the outcome, which means taking `ingest`'s arguments and walking `ingest`'s path: `Registry.naming` matches identity through the same `_find_document` and assigns through the same `_assign_slug`, so it cannot drift from the thing it predicts. Where the two must differ is stated rather than hidden. A file's bytes are read, because identity is the bytes and a copy under another name is the same document keeping its first slug. A URL's are not, so continuity rests on the URL — which is what a re-fetch matches on anyway — and the honest gap, printed as a note rather than guessed at, is the media type: a slug comes from the address and is settled here, a media type comes from the content type the server sends and is not. A source already ingested reports its recorded media type instead, because that one *was* settled by a server. The registry's part of the answer is the part a name alone cannot give: already ingested (and withdrawn, since `ls` denies those exist), the numbered slug a taken name would land on, and that `--slug` will not rename a document that has one. Read-only throughout — no socket, no document, no anchor, no ledger row — and the test that pins it blocks `fetch.fetch` outright rather than checking the output for absence. |
|
|
162
|
+
| 2026-09-14 | **`bind` and `verify` answer a caller that parses with `--json`, and the exit codes stay exactly where they are.** Both commands' output is the product and an agent is their main caller, which made every wording fix this repo lands — and it lands them weekly — a silent break for anything that scraped the last wording. The sharper problem was `verify`'s exit 2, which means both "a receipt did not hold" (the file was edited) and "a citation does not resolve against the sources" (the file is honest and the sources do not stand behind it): opposite conclusions, one code, and the prose the only place the difference lived. A third exit code was the obvious fix and was rejected, because the codes are the contract every Stop hook is already written against, and `bind` and `verify` sharing 2 is what lets one hook catch both (2026-08-24). The distinction goes in a payload instead. `bind --json` needed no new format: it prints the record, byte for byte the sidecar that run wrote, so there is no second serialization to keep in step with `spec/artifact.md`. `verify --json` did need one, `backdraft/verify-v1`, specified beside the checks it reports in § Checking an artifact and matched exactly under the artifact's no-guessing rule: each tier with a `ran` flag, and a `findings` list whose `kind` — `receipt`, `recount`, `source` — is the field a caller branches on, empty exactly when the run exits 0. A `receipt` finding also names the failed step in `check` (`snippet_sha256`, `token_parses`, `token_hash`, `token_slug`, `token_locator`), so a rehashed snippet and a re-pointed token are told apart without the sentence, and a `source` finding carries today's status beside the record's, so drift since binding is two values differing rather than a clause to find. `detail` and `error` stay prose and are declared rewordable; that line between keys and sentences is the point of the format. The same rule reached the implementation: `verify` now computes its findings once and hands them to two presenters, and the exit code reads the findings rather than either rendering, so the plain report is byte-identical to before and cannot drift from the object. Tradeoff accepted, and named in every skill that mentions the flag: `bind --json` is the whole record, evidence included, which for the demo is some 450 KB of mostly page images — the price of refusing to invent a second, slimmer bind format. The skills therefore tell an agent to pipe it into a parser (or read the record file bind already wrote) rather than print it into context, and to relay the plain report, not the JSON, to the user. |
|
|
163
|
+
| 2026-09-15 | **`verify --against <project>` checks a file you were sent against a registry you have, and discovery still infers nothing.** The 2026-08-24 row put `verify`'s source tier behind discovery from cwd, because an artifact is a file people forward and the folder it landed in says nothing about which registry produced it. That rule is kept exactly, and it had left no way to say what the recipient often does know — this file came out of *that* project — so the only route to a source check was to copy the artifact into the project first: a filesystem move performed to change a discovery result, repeated per artifact by an agent auditing several, and impossible for a reviewer without write access to the checkout. The flag answers the objection rather than overriding it. Discovery stays refusal-by-default and never looks at the artifact's location; `--against` is the caller asserting a link the tool must not infer, the same shape as `--slug` overruling a derived name — the tool derives nothing it is not told, and what it is told it takes as said. Three consequences follow from treating it as an assertion rather than a hint. It bypasses discovery instead of joining it, so it outranks `BACKDRAFT_HOME` as well as the walk: a name typed on one command is more specific than one exported for a whole shell, and a test pins flag, then environment, then walk. A path holding no registry is a usage error (exit 1) naming what was expected, never a fall back to tier one — a caller who named a registry and got the file-only check would read `sources: no .backdraft/ found from here` as a fact about the file rather than a typo — and it is refused *before* anything opens, because `Registry.open` creates what it does not find and a read-only audit must not leave a registry in whatever directory was typed. And the report names the registry that answered either way, on the `sources:` line and in `--json`'s `sources.registry`, resolved to an absolute root so the line from an inbox is the line from inside the project, which is what makes a wrong `--against` visible rather than just a run of `unresolved`. Both forms `BACKDRAFT_HOME` accepts — the project root or the `.backdraft` directory — are accepted through one rule (`cli_context.named_root` shares `find_root`'s), so the two ways of naming a registry cannot drift. Without the flag nothing moved: seven scenarios, in and out of the demo project, with and without `--json` and `BACKDRAFT_HOME`, diff byte-identical against the previous CLI, including the closing `[Re-check against the sources: …]` hint, which still names only the in-project route; pointing that hint at the flag is a wording change the acceptance ruled out of this item. |
|
|
164
|
+
| 2026-09-16 | **A chunk whose text may run on names the chunk it runs on into, decided by the chunks' own edges and never by the prose — so every page break counts.** The writing skill told an agent that "a claim that spans two chunks needs both tokens, not the nearest one", which is right and gave it nothing to act on: the gate handed back chunks, and whether the sentence being cited ended inside one was for the agent to notice by eye, mid-draft. When it did not, nothing complained — one token resolved, the receipt was real, the artifact rendered clean, and the half of the claim in the next chunk was uncited while looking cited, the one failure worse than `unresolved`, because `unresolved` says so. `gate.reader.adjoining` now answers it off the registry's own rows. On one page, two chunks adjoin when the whitespace between their stored offsets holds no blank line, tested with the chunker's own separator — `kernel.chunking.PARAGRAPH_BREAK`, made public so the question is asked of one pattern rather than a copy — because the chunker splits on blank lines before anything else, so two chunks with none between them are a paragraph rule 3 cut for length. A search hit carries that neighbour indented under its excerpt, labelled `same paragraph, before` or `after`, over the edge that touches the hit: the tail of the chunk before, the head of the chunk after. The page break was the decision, and it went the uncomfortable way on purpose. Each page is chunked alone and `pdf-text` writes no whitespace at a page's edges, so nothing a chunk carries tells a paragraph that crosses the break from one that ended at it; the only way to tell would be a sentence test at the join, and the obvious one — rule 3's own terminal-punctuation definition — was rejected twice over. It would decide what gets minted, which a guess must not. And its miss is exactly this row's failure: a paragraph that crosses the break at a full stop, under a claim spanning those two sentences. So a page break always adjoins, labelled `previous page ends` or `next page begins` rather than `same paragraph`, since that is all that is known, and the excerpt shows the reader in one line which case it is. The page read takes the same rule where it adds something: every chunk inside a read already sits beside the ones it adjoins, so only a read that shows the last chunk of its last page names anything — the next page's first chunk — and a read that stopped short already names the command that goes on. The costs, named rather than discovered. Emitting is minting, so the adjoining chunk is recorded as shown though only an excerpt of it was: it now binds `resolved` for a claim drawn from text the writer saw 160 characters of, the trade `search` already makes for every hit, and `ADJOINING_NOTE` says so once per run. It is noisy on clean documents: a read of any page of a paginated source but the last gains three lines and the note, and a hit on a page's first or last chunk gains its neighbour. The demo's own `read t12-summary p1`, a page ending on a full stop before a heading, now names `p2.c1`, which moves the walkthrough's `session show` from 77 anchors to 78; the gate tests' toy fixtures, one sentence a page, show it hardest. Display and ledger only everywhere else: no chunk boundary, token, anchor or receipt moves, and a hit or a read with nothing adjoining prints byte for byte what it did — pinned over the fakes, which now carry offsets the way the store does, and over a real ingest of a PDF whose paragraph the typesetter carried onto the next page. `EXCERPT_CHARS` and the excerpt cut moved from `searcher.py` to `reader.py` for the reason `LIST_HINT` lives there: both surfaces print the neighbour in one shape, and the searcher imports the reader, never the reverse. |
|
|
165
|
+
| 2026-09-17 | **A drifted citation whose words did not change is found where they stand now, by exact text and never by resemblance, and printing its new token is not minting it.** A re-ingested source re-chunks, so a paragraph inserted near the top of a page moves every chunk ordinal below it, and a citation whose words are untouched comes back `drifted` because a chunk's ordinal is its address — the Open list's oldest line, sharpened when `ingest` learned to announce a new generation (2026-08-20) and still left the agent a list of failures and a manual hunt per line. `backdraft locate <doc.md>` does the hunt: for each citation `citation_for` calls `drifted`, it looks the cited anchor's snippet hash up in the document's current generation (`Registry.anchors_with_snippet`, one query on the new `idx_anchors_snippet`, which the idempotent DDL adds to an existing registry on its next open) through `current_with`, `current_at`'s other half — that one keeps the locator and asks what text stands there, this keeps the text and asks which locators hold it. Matching is the normalized hash, same anchor kind, and nothing looser, because a near match proposed as a move rewrites provenance; the price is named rather than discovered — a sentence edited by one figure is `gone`, and so is a chunk that an insertion under 200 characters merged into (chunking rule 2), since its text did change. Three outcomes, and only one proposes: `moved` is exactly one place holding the words of a chunk or a page. Several places are `ambiguous` and named, because choosing among identical text is a guess. So is a cell with one match, which was the real decision: a cell's receipt is its value, and an equal value at another address is as often a second figure that agrees — two rates of 6.25% — as the same cell after a row went in, so a workbook gets lines naming the right cell for a person to confirm rather than tokens to paste. Read-only is the queued shape and the gate's doctrine argued against it, since every gate surface that hands a writer a token to cite mints it; it holds anyway, and the reason is that printing is not what makes a token citable — `bind` is. A moved token binds `not_shown` until the session is shown it, which is the ledger being right about names even where the words were seen, so the queued acceptance ("applying those tokens by hand and re-binding gives a clean run") holds one step later than written: the closing line names the `show` that records exactly the moved tokens `--session` has not seen (read, never created, and carried when typed through `gate.session_argument`), or the re-bind itself once none remain. Minting here was the alternative and was rejected, because a command that proposes should not write the ledger it proposes against, and `verify` already prints tokens without minting them. Two more departures from the queued shape, stated. It walks the document's own tokens (`kernel.claims.parse_claims`) rather than the stored `bindings`, which go stale the moment one token is swapped by hand and key on the path as it was typed. And it lives in `cli.py` beside the `ingest` whose note now names it, not in `bind/cli.py`, since that closing line needs the gate's session wording and bind may not import the gate. Exit 0 whatever it finds: `bind` is the check and this is the advice. Not Later's living documents, which is a cited-then-versus-now presentation; this is the lookup that presentation would stand on. |
|
|
166
|
+
| 2026-10-06 | **`bind` names the session it judged `not_shown` against, and a record bound in the default one says what that costs wherever it travels.** The 2026-08-31 row found that the default session accumulates every read any run in a registry ever made and fixed it at `session show`, the command that reports the ledger, leaving `bind`, the command that acts on it, to resolve the session in silence: `backdraft bind memo.md` with nothing exported judged against the shared ledger and its report never said so — in this repo's own demo that run binds clean with no `not_shown` at all, because development runs have read everything into `default`, which is exactly the false comfort the row described. The front-walk header now ends `against session <id>` and, where the id is `default`, the report closes with `gate.reader.DEFAULT_SESSION_NOTE` itself, imported rather than re-worded, since two wordings of one cost is how the two commands' warnings would drift; keyed on the id as `session show` keys it, so an explicit `--session default` is the same run byte for byte. Backfill names no session and gets no note, because it never consults the ledger and a warning that `bind` judges `not_shown` against all of it would be false there. The CLI does not travel and the record does, so the format says it too: `not_shown`'s legend entry and `spec/artifact.md`'s `session_id` row state that `default` means a ledger every run shares, and `verify`'s first tier prints a `session: default` line from the file alone, with no registry, so the recipient — the reader least able to learn it otherwise — is told. That sentence is `kernel.artifact.DEFAULT_SESSION_MEANING`, one string the legend embeds and `verify` prints, and the id itself moved into the kernel as `DEFAULT_SESSION` (re-exported by `cli_context` unchanged) because a session id the format assigns meaning to is format, not CLI vocabulary. The writer's note and the reader's sentence stay two strings on purpose: one says what to do before binding, the other what to conclude from a record already bound, and neither audience can act on the other's. The cost is the 2026-08-24 trade taken again — a legend change regenerates the golden sidecars and the demo artifact — and `verify --json` gained nothing, since the record it checks already carries `session_id` and the line is a reading of the record, not a finding. |
|
|
167
|
+
| 2026-10-07 | **What `forget` took out is listed by `ls --withdrawn`, a flag rather than a block under `ls`.** Withdrawal took a source off every surface that offers one, which is right, and left none that lists what went: `ls` in a registry holding two withdrawn documents said `no documents ingested`, and the only places a withdrawn document still appeared — `session show`'s mark, the JSON export, the unknown-slug error — are not where anybody looks. That mattered most for the undo, since re-ingesting is the way back and the path to re-ingest lives on the document, which is exactly what drops out of the list. The choice was a flag or a block printed under `ls` whenever the registry holds a withdrawal, and the block lost on the objection the item named: `ls` is the readable set, and anything that widens it risks making a withdrawn source look available — a footer under the live rows is one scroll from being read as more of them, and an agent parsing tab rows would have to learn to stop at it. So `ls --withdrawn` is a view of its own that lists only the withdrawn documents, never a marked row sorted among live ones, and default `ls` holds the byte-identity rule the source-naming work set: a registry with a withdrawal beside live sources prints exactly what the live sources alone print. The one place default `ls` does change is the place it was false: with nothing on offer and something withdrawn it says `no documents on offer: N withdrawn` and names the flag, instead of claiming an empty registry. Each withdrawn row is `ls`'s four fields then `withdrawn_reason` and `WITHDRAWN_HINT` verbatim, so the date and the way back keep their single owners rather than gaining a fourth wording, and it drops the `little text` mark, which advises a citation and nothing here is on offer to cite. The gate's own document list is untouched and pinned equal: the gate serves sources, and a withdrawn source is not one. The cost is discoverability — a flag is found by reading `--help` or the docs, not by running `ls` — which the `unresolved — withdrawn` guidance in the skill and `llms.txt` now pays by naming it at the moment a caller meets a withdrawal. |
|
|
168
|
+
| 2026-10-08 | **`ingest` says when `--slug` was not applied, and names the cost of a rename rather than a route to one, because there is no route.** `Registry.ingest` honours a slug only for a new document, a rule stated in its docstring and nowhere a caller reads: `backdraft ingest report.pdf --slug q4-report` against a file already ingested printed the old slug at exit 0 and never mentioned the flag, which is worst on a URL re-fetch, where the host-built fallback slug is exactly what a late `--slug` is trying to replace. The source's line now ends `--slug not applied` and one note names the slug the document keeps, in the shape `restored` set — a mark beside the outcome and a grouped note, not a third reporting shape — with no registry or schema change, since the CLI holds the requested slug and the returned `Ingested` carries the assigned one. The item as queued asked the note to offer `forget` and a re-ingest under the new slug as the costly way to re-slug; that path does not exist, because `_find_document` matches a withdrawn document by bytes and then origin and never by slug, so the re-ingest restores the same document under the same slug — the 2026-09-02 rule that makes re-ingest `forget`'s undo. Printing it would have been advice that cannot be taken, so the note says a rename would strand every token carrying the slug and that nothing renames a document, `forget` included, and a test pins that the forget-then-ingest run reports `restored --slug not applied`. Exit 0 throughout: getting back the document you already had is not a failure. A `--slug` on a new document and every ingest without one are byte-identical, pinned and checked against `demo/`'s sources. The dry run's own advice for a source not yet ingested — that changing a slug later means re-ingesting — was untouched here and was then the one surface that still implied a re-ingest renames; the release cleanup gave it this note's wording. |
|
|
162
169
|
|
|
163
170
|
## Open
|
|
164
171
|
|
|
165
172
|
Queued and parked work lives in ROADMAP.md; this list is questions still
|
|
166
173
|
awaiting a decision:
|
|
167
174
|
|
|
168
|
-
- Re-bind/orphan pass on re-ingest of changed docs (chunk ordinal drift)
|
|
169
175
|
- Distribution: published to PyPI as `backdraft` 0.2.0 (2026-07-28); domain
|
|
170
176
|
backdraft.dev acquired; still open: skill registration, deploying site/
|
|
171
177
|
- 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.13.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
|
|
@@ -92,12 +92,16 @@ than a page, so the host stands in — and a slug is permanent once tokens carry
|
|
|
92
92
|
it. `backdraft ingest <source> --dry-run` is how to see that before choosing:
|
|
93
93
|
it prints the slug and media type each source would take and stops, fetching
|
|
94
94
|
nothing and writing nothing, and it says when a source is already ingested or
|
|
95
|
-
when the name it wants is taken.
|
|
95
|
+
when the name it wants is taken. Permanent means `--slug` only ever names a new
|
|
96
|
+
document: on a source already ingested the line says `--slug not applied` and a
|
|
97
|
+
note names the slug it keeps — and `forget` then a re-ingest brings back that
|
|
98
|
+
same slug rather than a new one, since identity is the bytes, not the name.
|
|
96
99
|
|
|
97
100
|
Every ingest line ends with how much text came out, and says which of three
|
|
98
101
|
things happened: a document created, a **new generation** of one whose bytes
|
|
99
102
|
moved — which is when citations into the previous snapshot start reporting
|
|
100
|
-
`drifted
|
|
103
|
+
`drifted`, and [`backdraft locate`](#when-a-source-changes) says where their
|
|
104
|
+
text went — or `unchanged`, a no-op because re-running would reproduce what is
|
|
101
105
|
already there. When almost no text came out, a note names the likely cause (a
|
|
102
106
|
scan with no text layer, a page behind a login) and what to do; it is a note at
|
|
103
107
|
exit 0, because a thin snapshot is still a real one. That source keeps the mark
|
|
@@ -128,12 +132,21 @@ $2,972,160. The resulting economic occupancy of 90.3% reflects an average physic
|
|
|
128
132
|
by concessions, bad debt, and vacancy loss. Physical occupancy was 96.4% in the most recent month and has not
|
|
129
133
|
fallen below 92.0% in any month of the period.
|
|
130
134
|
|
|
131
|
-
… five more chunks, c3 through c7
|
|
135
|
+
… five more chunks, c3 through c7, and then:
|
|
136
|
+
|
|
137
|
+
next page begins: [bd:t12-summary:p2.c1:7bd3]
|
|
138
|
+
Revenue Detail Rental income of $2,548,900 accounts for 94.9% of effective gross income. Other income of $135,500 comprises pet rent, parking, utility reimburse...
|
|
139
|
+
|
|
140
|
+
note: text can run on across a paragraph split in two or a page break, so the chunk on the other side is named and recorded as shown. A claim whose words come from both sides cites both tokens.
|
|
132
141
|
```
|
|
133
142
|
|
|
134
143
|
Chunks fall on the document's own paragraphs. A PDF stores glyphs at
|
|
135
144
|
coordinates, not paragraphs, so the extractor rebuilds the breaks from the line
|
|
136
|
-
geometry before the chunker sees the page.
|
|
145
|
+
geometry before the chunker sees the page. Where a claim could still straddle
|
|
146
|
+
two chunks — a paragraph the chunker cut for length, or a page break, which
|
|
147
|
+
chunking never sees past — the gate names the chunk on the other side, minted
|
|
148
|
+
like the rest, so the sentence that runs on is cited by both tokens rather than
|
|
149
|
+
the one you happened to be reading.
|
|
137
150
|
|
|
138
151
|
`backdraft read <slug>` on its own is the table of contents: one line per page
|
|
139
152
|
for a source that paginates, and — for a web page, which has none — one line per
|
|
@@ -214,9 +227,9 @@ enough to write this yet?", asked before the draft exists rather than after
|
|
|
214
227
|
$ backdraft session show
|
|
215
228
|
session s-bridgeview (from BACKDRAFT_SESSION)
|
|
216
229
|
|
|
217
|
-
|
|
230
|
+
78 anchors shown across 3 documents
|
|
218
231
|
|
|
219
|
-
t12-summary
|
|
232
|
+
t12-summary 8
|
|
220
233
|
underwriting-model 68
|
|
221
234
|
franklin-county 2
|
|
222
235
|
|
|
@@ -227,8 +240,9 @@ Anything counted there binds `resolved`; anything else in the registry binds
|
|
|
227
240
|
`not_shown`. Without an exported session, reads land in one default ledger every
|
|
228
241
|
run in the project shares and nothing ever resets — so `not_shown` weakens from
|
|
229
242
|
"this writer never saw it" to "nothing here ever has". `session show` says so at
|
|
230
|
-
exit 0 when you are in it,
|
|
231
|
-
|
|
243
|
+
exit 0 when you are in it, `bind` names the session on its first line and
|
|
244
|
+
closes with the same note when it judged against the default, and `backdraft
|
|
245
|
+
session start --id s-<name>` is the one command that fixes it.
|
|
232
246
|
|
|
233
247
|
**Cite** by writing the token as the href of a markdown link on the claim span.
|
|
234
248
|
Multiple citations are `;`-separated in one href.
|
|
@@ -242,7 +256,7 @@ Multiple citations are `;`-separated in one href.
|
|
|
242
256
|
|
|
243
257
|
```console
|
|
244
258
|
$ backdraft bind memo.md --session s-bridgeview --check value-trace,overlap
|
|
245
|
-
bound 17 claim(s), 18 citation(s) [frontwalk]
|
|
259
|
+
bound 17 claim(s), 18 citation(s) [frontwalk] against session s-bridgeview
|
|
246
260
|
resolved: 17
|
|
247
261
|
unresolved: 1
|
|
248
262
|
overlap: pass 13, skip 4
|
|
@@ -293,6 +307,29 @@ going unnoticed. Exit 0 when everything it checked passed, 2 when something did
|
|
|
293
307
|
not, so a hook can gate on it. It opens no session and mints nothing: an audit
|
|
294
308
|
must not make its subject citable.
|
|
295
309
|
|
|
310
|
+
Holding a file that was sent to you beside a checkout of the project it came
|
|
311
|
+
out of, say so rather than moving the file:
|
|
312
|
+
|
|
313
|
+
```console
|
|
314
|
+
$ backdraft verify ~/Downloads/memo.backdraft.html --against ~/work/project
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
That runs the source check too, and its `sources:` line names the registry that
|
|
318
|
+
answered. `verify` never works out which project a file belongs to from where it
|
|
319
|
+
landed, so the link is yours to assert; a path with no registry in it is refused
|
|
320
|
+
rather than quietly checking the file alone.
|
|
321
|
+
|
|
322
|
+
**Scripts and agents read keys, not lines.** The reports above are worded for
|
|
323
|
+
people, and the wording changes. `backdraft bind memo.md --json` prints the
|
|
324
|
+
record in place of the report (the same bytes bind writes), and
|
|
325
|
+
`backdraft verify memo.backdraft.html --json` prints the check as one object whose
|
|
326
|
+
`findings` each carry a `kind`: `receipt` when the file was edited, `source`
|
|
327
|
+
when a citation does not resolve against the sources today (its status beside
|
|
328
|
+
the record's says whether they moved since binding), `recount` when the summary
|
|
329
|
+
disagrees with the claims. Exit codes are unchanged; the payload says which kind
|
|
330
|
+
of exit 2 you got. The format is in
|
|
331
|
+
[spec/artifact.md](spec/artifact.md#the-verify-report).
|
|
332
|
+
|
|
296
333
|
**Theming.** The artifact ships with a default look and two alternates,
|
|
297
334
|
`press` and `slate`:
|
|
298
335
|
|
|
@@ -388,6 +425,42 @@ are out of reach, and the extractor is a parse rather than a readability guess
|
|
|
388
425
|
its mind between two versions of a site would move anchors. Responses are
|
|
389
426
|
capped at 32 MiB.
|
|
390
427
|
|
|
428
|
+
## When a source changes
|
|
429
|
+
|
|
430
|
+
A re-ingested source re-chunks, so an edit near the top of a page moves every
|
|
431
|
+
citation below it: a paragraph inserted above the one you cited shifts its chunk
|
|
432
|
+
ordinal, and a citation whose words never changed comes back `drifted` because
|
|
433
|
+
its address did. `bind` can say that much and no more. `locate` finds where the
|
|
434
|
+
text went:
|
|
435
|
+
|
|
436
|
+
```console
|
|
437
|
+
$ backdraft locate memo.md
|
|
438
|
+
3 citation(s) in memo.md, 3 drifted
|
|
439
|
+
gone: 1
|
|
440
|
+
moved: 2
|
|
441
|
+
! moved: bd:q3-letter:p1.c1:afd2 — now at bd:q3-letter:p1.c2:afd2 — closed Q3 at 92.0% @24
|
|
442
|
+
! moved: bd:q3-letter:p1.c2:d140 — now at bd:q3-letter:p1.c3:d140 — 1.42x @84
|
|
443
|
+
! gone: bd:q3-letter:p1.c3:00ce — taxes of $412,300 @122
|
|
444
|
+
[Nothing was rewritten. Put each moved token in place of the old one in memo.md, then show the new ones, which this session has not seen, and re-bind: backdraft show bd:q3-letter:p1.c2:afd2 bd:q3-letter:p1.c3:d140]
|
|
445
|
+
[A gone citation's text was edited or removed, so nothing is proposed. Read what it cited, then search for the new wording: backdraft show bd:q3-letter:p1.c3:00ce]
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
For each citation `bind` would call `drifted`, it looks for the exact text the
|
|
449
|
+
citation quotes — the normalized snippet hash — in the source's current
|
|
450
|
+
snapshot. `moved` is one place holding it, and names the token there.
|
|
451
|
+
`ambiguous` names the places when there are several, or when the citation is a
|
|
452
|
+
cell: an equal value at another address is as often a second figure that
|
|
453
|
+
happens to agree as the same cell after a row was inserted. `gone` is the text
|
|
454
|
+
edited or removed, here a tax figure revised from $412,300, and nothing is
|
|
455
|
+
proposed for it — matching is exact on purpose, because a near match offered as
|
|
456
|
+
a move would rewrite provenance.
|
|
457
|
+
|
|
458
|
+
It proposes and never rewrites, and it is read-only: the document, the registry
|
|
459
|
+
and the ledger are untouched. That includes minting — a token printed here was
|
|
460
|
+
never shown to your session, so swap the moved tokens in, run the `show` the
|
|
461
|
+
closing line names (and read that the text is what you cited), then re-bind.
|
|
462
|
+
Exit 0 whatever it finds: `bind` is the check.
|
|
463
|
+
|
|
391
464
|
## Forgetting a source
|
|
392
465
|
|
|
393
466
|
Ingest is otherwise one-way, and an unattended folder ingest picks up what it
|
|
@@ -417,6 +490,19 @@ slug, not a second one beside it, so every token it ever minted resolves again.
|
|
|
417
490
|
The price of never actually removing anything is that a registry only grows; a
|
|
418
491
|
corpus you must be able to shrink is a new registry, not a `forget`.
|
|
419
492
|
|
|
493
|
+
`ls` lists what is on offer, so a withdrawn source leaves it too, and a registry
|
|
494
|
+
with nothing withdrawn prints exactly what it always did. `ls --withdrawn` is
|
|
495
|
+
where the rest are listed — each with the date it went and the ingest that
|
|
496
|
+
brings it back, which is the one place that path is still written down:
|
|
497
|
+
|
|
498
|
+
```console
|
|
499
|
+
$ backdraft ls --withdrawn
|
|
500
|
+
scratch-copy scratch-copy.md text 1 page withdrawn from the registry on 2026-10-07T13:03:11.922792Z Re-ingest it to bring it back: backdraft ingest scratch-copy.md
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
A registry whose every source has been withdrawn says so — `no documents on
|
|
504
|
+
offer: 2 withdrawn` — rather than calling itself empty.
|
|
505
|
+
|
|
420
506
|
## What lives where
|
|
421
507
|
|
|
422
508
|
A working directory stays clean: the authored document and its artifact are
|
|
@@ -463,6 +549,25 @@ Three extras:
|
|
|
463
549
|
`backdraft[vlm]` still installs: it is an empty compat alias from when the
|
|
464
550
|
vision deps were an extra.
|
|
465
551
|
|
|
552
|
+
**In an unfamiliar environment, run `backdraft doctor` first.** Everything
|
|
553
|
+
above degrades rather than fails, so without it each gap is discovered at the
|
|
554
|
+
moment it is missed. `doctor` reports all of them up front — the registry,
|
|
555
|
+
poppler, the vision model, `[xls]`, `[entail]` and `[math]` — each as `ready`
|
|
556
|
+
or `missing`, with what its absence costs the verbs and the command that fixes
|
|
557
|
+
it, in the same words the verb itself would print. Keys are named `set` or
|
|
558
|
+
`absent` and their values never printed. Read-only, and always exit 0: a
|
|
559
|
+
missing optional capability is not an error.
|
|
560
|
+
|
|
561
|
+
```console
|
|
562
|
+
$ backdraft doctor
|
|
563
|
+
registry ready /…/demo/.backdraft, 3 document(s)
|
|
564
|
+
page images ready poppler renders PDF pages for the artifact's cited-page view
|
|
565
|
+
vision model missing Glossy or scanned PDFs extract better through a vision model: set BACKDRAFT_VLM_API_KEY in .backdraft/env.
|
|
566
|
+
costs ingest: `auto` reads a PDF's text layer, so a scan comes back thin, and an image cannot be ingested at all. A PDF with a text layer, and every other format, is unaffected.
|
|
567
|
+
key BACKDRAFT_VLM_API_KEY absent
|
|
568
|
+
…
|
|
569
|
+
```
|
|
570
|
+
|
|
466
571
|
From a checkout:
|
|
467
572
|
|
|
468
573
|
```bash
|
|
@@ -65,12 +65,16 @@ than a page, so the host stands in — and a slug is permanent once tokens carry
|
|
|
65
65
|
it. `backdraft ingest <source> --dry-run` is how to see that before choosing:
|
|
66
66
|
it prints the slug and media type each source would take and stops, fetching
|
|
67
67
|
nothing and writing nothing, and it says when a source is already ingested or
|
|
68
|
-
when the name it wants is taken.
|
|
68
|
+
when the name it wants is taken. Permanent means `--slug` only ever names a new
|
|
69
|
+
document: on a source already ingested the line says `--slug not applied` and a
|
|
70
|
+
note names the slug it keeps — and `forget` then a re-ingest brings back that
|
|
71
|
+
same slug rather than a new one, since identity is the bytes, not the name.
|
|
69
72
|
|
|
70
73
|
Every ingest line ends with how much text came out, and says which of three
|
|
71
74
|
things happened: a document created, a **new generation** of one whose bytes
|
|
72
75
|
moved — which is when citations into the previous snapshot start reporting
|
|
73
|
-
`drifted
|
|
76
|
+
`drifted`, and [`backdraft locate`](#when-a-source-changes) says where their
|
|
77
|
+
text went — or `unchanged`, a no-op because re-running would reproduce what is
|
|
74
78
|
already there. When almost no text came out, a note names the likely cause (a
|
|
75
79
|
scan with no text layer, a page behind a login) and what to do; it is a note at
|
|
76
80
|
exit 0, because a thin snapshot is still a real one. That source keeps the mark
|
|
@@ -101,12 +105,21 @@ $2,972,160. The resulting economic occupancy of 90.3% reflects an average physic
|
|
|
101
105
|
by concessions, bad debt, and vacancy loss. Physical occupancy was 96.4% in the most recent month and has not
|
|
102
106
|
fallen below 92.0% in any month of the period.
|
|
103
107
|
|
|
104
|
-
… five more chunks, c3 through c7
|
|
108
|
+
… five more chunks, c3 through c7, and then:
|
|
109
|
+
|
|
110
|
+
next page begins: [bd:t12-summary:p2.c1:7bd3]
|
|
111
|
+
Revenue Detail Rental income of $2,548,900 accounts for 94.9% of effective gross income. Other income of $135,500 comprises pet rent, parking, utility reimburse...
|
|
112
|
+
|
|
113
|
+
note: text can run on across a paragraph split in two or a page break, so the chunk on the other side is named and recorded as shown. A claim whose words come from both sides cites both tokens.
|
|
105
114
|
```
|
|
106
115
|
|
|
107
116
|
Chunks fall on the document's own paragraphs. A PDF stores glyphs at
|
|
108
117
|
coordinates, not paragraphs, so the extractor rebuilds the breaks from the line
|
|
109
|
-
geometry before the chunker sees the page.
|
|
118
|
+
geometry before the chunker sees the page. Where a claim could still straddle
|
|
119
|
+
two chunks — a paragraph the chunker cut for length, or a page break, which
|
|
120
|
+
chunking never sees past — the gate names the chunk on the other side, minted
|
|
121
|
+
like the rest, so the sentence that runs on is cited by both tokens rather than
|
|
122
|
+
the one you happened to be reading.
|
|
110
123
|
|
|
111
124
|
`backdraft read <slug>` on its own is the table of contents: one line per page
|
|
112
125
|
for a source that paginates, and — for a web page, which has none — one line per
|
|
@@ -187,9 +200,9 @@ enough to write this yet?", asked before the draft exists rather than after
|
|
|
187
200
|
$ backdraft session show
|
|
188
201
|
session s-bridgeview (from BACKDRAFT_SESSION)
|
|
189
202
|
|
|
190
|
-
|
|
203
|
+
78 anchors shown across 3 documents
|
|
191
204
|
|
|
192
|
-
t12-summary
|
|
205
|
+
t12-summary 8
|
|
193
206
|
underwriting-model 68
|
|
194
207
|
franklin-county 2
|
|
195
208
|
|
|
@@ -200,8 +213,9 @@ Anything counted there binds `resolved`; anything else in the registry binds
|
|
|
200
213
|
`not_shown`. Without an exported session, reads land in one default ledger every
|
|
201
214
|
run in the project shares and nothing ever resets — so `not_shown` weakens from
|
|
202
215
|
"this writer never saw it" to "nothing here ever has". `session show` says so at
|
|
203
|
-
exit 0 when you are in it,
|
|
204
|
-
|
|
216
|
+
exit 0 when you are in it, `bind` names the session on its first line and
|
|
217
|
+
closes with the same note when it judged against the default, and `backdraft
|
|
218
|
+
session start --id s-<name>` is the one command that fixes it.
|
|
205
219
|
|
|
206
220
|
**Cite** by writing the token as the href of a markdown link on the claim span.
|
|
207
221
|
Multiple citations are `;`-separated in one href.
|
|
@@ -215,7 +229,7 @@ Multiple citations are `;`-separated in one href.
|
|
|
215
229
|
|
|
216
230
|
```console
|
|
217
231
|
$ backdraft bind memo.md --session s-bridgeview --check value-trace,overlap
|
|
218
|
-
bound 17 claim(s), 18 citation(s) [frontwalk]
|
|
232
|
+
bound 17 claim(s), 18 citation(s) [frontwalk] against session s-bridgeview
|
|
219
233
|
resolved: 17
|
|
220
234
|
unresolved: 1
|
|
221
235
|
overlap: pass 13, skip 4
|
|
@@ -266,6 +280,29 @@ going unnoticed. Exit 0 when everything it checked passed, 2 when something did
|
|
|
266
280
|
not, so a hook can gate on it. It opens no session and mints nothing: an audit
|
|
267
281
|
must not make its subject citable.
|
|
268
282
|
|
|
283
|
+
Holding a file that was sent to you beside a checkout of the project it came
|
|
284
|
+
out of, say so rather than moving the file:
|
|
285
|
+
|
|
286
|
+
```console
|
|
287
|
+
$ backdraft verify ~/Downloads/memo.backdraft.html --against ~/work/project
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
That runs the source check too, and its `sources:` line names the registry that
|
|
291
|
+
answered. `verify` never works out which project a file belongs to from where it
|
|
292
|
+
landed, so the link is yours to assert; a path with no registry in it is refused
|
|
293
|
+
rather than quietly checking the file alone.
|
|
294
|
+
|
|
295
|
+
**Scripts and agents read keys, not lines.** The reports above are worded for
|
|
296
|
+
people, and the wording changes. `backdraft bind memo.md --json` prints the
|
|
297
|
+
record in place of the report (the same bytes bind writes), and
|
|
298
|
+
`backdraft verify memo.backdraft.html --json` prints the check as one object whose
|
|
299
|
+
`findings` each carry a `kind`: `receipt` when the file was edited, `source`
|
|
300
|
+
when a citation does not resolve against the sources today (its status beside
|
|
301
|
+
the record's says whether they moved since binding), `recount` when the summary
|
|
302
|
+
disagrees with the claims. Exit codes are unchanged; the payload says which kind
|
|
303
|
+
of exit 2 you got. The format is in
|
|
304
|
+
[spec/artifact.md](spec/artifact.md#the-verify-report).
|
|
305
|
+
|
|
269
306
|
**Theming.** The artifact ships with a default look and two alternates,
|
|
270
307
|
`press` and `slate`:
|
|
271
308
|
|
|
@@ -361,6 +398,42 @@ are out of reach, and the extractor is a parse rather than a readability guess
|
|
|
361
398
|
its mind between two versions of a site would move anchors. Responses are
|
|
362
399
|
capped at 32 MiB.
|
|
363
400
|
|
|
401
|
+
## When a source changes
|
|
402
|
+
|
|
403
|
+
A re-ingested source re-chunks, so an edit near the top of a page moves every
|
|
404
|
+
citation below it: a paragraph inserted above the one you cited shifts its chunk
|
|
405
|
+
ordinal, and a citation whose words never changed comes back `drifted` because
|
|
406
|
+
its address did. `bind` can say that much and no more. `locate` finds where the
|
|
407
|
+
text went:
|
|
408
|
+
|
|
409
|
+
```console
|
|
410
|
+
$ backdraft locate memo.md
|
|
411
|
+
3 citation(s) in memo.md, 3 drifted
|
|
412
|
+
gone: 1
|
|
413
|
+
moved: 2
|
|
414
|
+
! moved: bd:q3-letter:p1.c1:afd2 — now at bd:q3-letter:p1.c2:afd2 — closed Q3 at 92.0% @24
|
|
415
|
+
! moved: bd:q3-letter:p1.c2:d140 — now at bd:q3-letter:p1.c3:d140 — 1.42x @84
|
|
416
|
+
! gone: bd:q3-letter:p1.c3:00ce — taxes of $412,300 @122
|
|
417
|
+
[Nothing was rewritten. Put each moved token in place of the old one in memo.md, then show the new ones, which this session has not seen, and re-bind: backdraft show bd:q3-letter:p1.c2:afd2 bd:q3-letter:p1.c3:d140]
|
|
418
|
+
[A gone citation's text was edited or removed, so nothing is proposed. Read what it cited, then search for the new wording: backdraft show bd:q3-letter:p1.c3:00ce]
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
For each citation `bind` would call `drifted`, it looks for the exact text the
|
|
422
|
+
citation quotes — the normalized snippet hash — in the source's current
|
|
423
|
+
snapshot. `moved` is one place holding it, and names the token there.
|
|
424
|
+
`ambiguous` names the places when there are several, or when the citation is a
|
|
425
|
+
cell: an equal value at another address is as often a second figure that
|
|
426
|
+
happens to agree as the same cell after a row was inserted. `gone` is the text
|
|
427
|
+
edited or removed, here a tax figure revised from $412,300, and nothing is
|
|
428
|
+
proposed for it — matching is exact on purpose, because a near match offered as
|
|
429
|
+
a move would rewrite provenance.
|
|
430
|
+
|
|
431
|
+
It proposes and never rewrites, and it is read-only: the document, the registry
|
|
432
|
+
and the ledger are untouched. That includes minting — a token printed here was
|
|
433
|
+
never shown to your session, so swap the moved tokens in, run the `show` the
|
|
434
|
+
closing line names (and read that the text is what you cited), then re-bind.
|
|
435
|
+
Exit 0 whatever it finds: `bind` is the check.
|
|
436
|
+
|
|
364
437
|
## Forgetting a source
|
|
365
438
|
|
|
366
439
|
Ingest is otherwise one-way, and an unattended folder ingest picks up what it
|
|
@@ -390,6 +463,19 @@ slug, not a second one beside it, so every token it ever minted resolves again.
|
|
|
390
463
|
The price of never actually removing anything is that a registry only grows; a
|
|
391
464
|
corpus you must be able to shrink is a new registry, not a `forget`.
|
|
392
465
|
|
|
466
|
+
`ls` lists what is on offer, so a withdrawn source leaves it too, and a registry
|
|
467
|
+
with nothing withdrawn prints exactly what it always did. `ls --withdrawn` is
|
|
468
|
+
where the rest are listed — each with the date it went and the ingest that
|
|
469
|
+
brings it back, which is the one place that path is still written down:
|
|
470
|
+
|
|
471
|
+
```console
|
|
472
|
+
$ backdraft ls --withdrawn
|
|
473
|
+
scratch-copy scratch-copy.md text 1 page withdrawn from the registry on 2026-10-07T13:03:11.922792Z Re-ingest it to bring it back: backdraft ingest scratch-copy.md
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
A registry whose every source has been withdrawn says so — `no documents on
|
|
477
|
+
offer: 2 withdrawn` — rather than calling itself empty.
|
|
478
|
+
|
|
393
479
|
## What lives where
|
|
394
480
|
|
|
395
481
|
A working directory stays clean: the authored document and its artifact are
|
|
@@ -436,6 +522,25 @@ Three extras:
|
|
|
436
522
|
`backdraft[vlm]` still installs: it is an empty compat alias from when the
|
|
437
523
|
vision deps were an extra.
|
|
438
524
|
|
|
525
|
+
**In an unfamiliar environment, run `backdraft doctor` first.** Everything
|
|
526
|
+
above degrades rather than fails, so without it each gap is discovered at the
|
|
527
|
+
moment it is missed. `doctor` reports all of them up front — the registry,
|
|
528
|
+
poppler, the vision model, `[xls]`, `[entail]` and `[math]` — each as `ready`
|
|
529
|
+
or `missing`, with what its absence costs the verbs and the command that fixes
|
|
530
|
+
it, in the same words the verb itself would print. Keys are named `set` or
|
|
531
|
+
`absent` and their values never printed. Read-only, and always exit 0: a
|
|
532
|
+
missing optional capability is not an error.
|
|
533
|
+
|
|
534
|
+
```console
|
|
535
|
+
$ backdraft doctor
|
|
536
|
+
registry ready /…/demo/.backdraft, 3 document(s)
|
|
537
|
+
page images ready poppler renders PDF pages for the artifact's cited-page view
|
|
538
|
+
vision model missing Glossy or scanned PDFs extract better through a vision model: set BACKDRAFT_VLM_API_KEY in .backdraft/env.
|
|
539
|
+
costs ingest: `auto` reads a PDF's text layer, so a scan comes back thin, and an image cannot be ingested at all. A PDF with a text layer, and every other format, is unaffected.
|
|
540
|
+
key BACKDRAFT_VLM_API_KEY absent
|
|
541
|
+
…
|
|
542
|
+
```
|
|
543
|
+
|
|
439
544
|
From a checkout:
|
|
440
545
|
|
|
441
546
|
```bash
|