backdraft 0.11.0__tar.gz → 0.12.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.12.0}/.claude-plugin/plugin.json +1 -1
- {backdraft-0.11.0 → backdraft-0.12.0}/DESIGN.md +4 -1
- {backdraft-0.11.0 → backdraft-0.12.0}/PKG-INFO +75 -6
- {backdraft-0.11.0 → backdraft-0.12.0}/README.md +74 -5
- {backdraft-0.11.0 → backdraft-0.12.0}/ROADMAP.md +267 -196
- {backdraft-0.11.0 → backdraft-0.12.0}/SPEC.md +21 -7
- {backdraft-0.11.0 → backdraft-0.12.0}/demo/walkthrough.md +47 -3
- {backdraft-0.11.0 → backdraft-0.12.0}/pyproject.toml +1 -1
- {backdraft-0.11.0 → backdraft-0.12.0}/site/docs.html +16 -7
- {backdraft-0.11.0 → backdraft-0.12.0}/site/llms.txt +52 -8
- {backdraft-0.11.0 → backdraft-0.12.0}/skills/backdraft/SKILL.md +53 -12
- {backdraft-0.11.0 → backdraft-0.12.0}/skills/backdraft-artifact/SKILL.md +40 -5
- {backdraft-0.11.0 → backdraft-0.12.0}/skills/backdraft-backfill/SKILL.md +12 -1
- {backdraft-0.11.0 → backdraft-0.12.0}/spec/artifact.md +84 -7
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/bind/cli.py +54 -23
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/cli.py +215 -4
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/cli_context.py +81 -2
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/base.py +14 -2
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/gate/__init__.py +2 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/gate/cli.py +5 -2
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/gate/reader.py +230 -13
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/gate/searcher.py +44 -30
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/kernel/artifact.py +10 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/kernel/chunking.py +10 -2
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/registry/__init__.py +2 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/registry/schema.sql +1 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/registry/store.py +52 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/cli.py +272 -66
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/sidecar.py +18 -13
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/gate/fake_registry.py +38 -13
- backdraft-0.12.0/tests/gate/test_gate_adjoining.py +266 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/gate/test_gate_cli.py +9 -3
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/gate/test_gate_ledger.py +11 -2
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/gate/test_gate_reader.py +26 -7
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/gate/test_gate_searcher.py +41 -12
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_bind_cli.py +77 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_cli.py +79 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_extract.py +27 -1
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_gate_integration.py +137 -2
- backdraft-0.12.0/tests/test_locate.py +490 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_reingest.py +94 -1
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_render_cli.py +34 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_verify_cli.py +366 -1
- {backdraft-0.11.0 → backdraft-0.12.0}/uv.lock +1 -1
- {backdraft-0.11.0 → backdraft-0.12.0}/.claude-plugin/marketplace.json +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/.github/workflows/ci.yml +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/.github/workflows/release.yml +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/.github/workflows/site.yml +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/.gitignore +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/.python-version +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/LICENSE +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/demo/generate_sources.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/demo/memo.backdraft.html +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/demo/memo.md +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/demo/sources/t12-summary.pdf +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/site/demo.html +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/site/index.html +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/site/privacy.html +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/spec/chunking.md +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/spec/registry.md +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/spec/tokens.md +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/__main__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/bind/binder.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/bind/evidence.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/bind/verify/entail.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/credentials.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/csv.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/docx.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/html.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/image.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/pdf_text.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/pptx.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/sheet.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/snapshots.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/vlm.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/vlm_settings.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/xls.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/extract/xlsx.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/fetch.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/kernel/model.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/registry/ledger.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/_text.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/footnotes.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/html/__init__.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/html/assets.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/html/components.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/html/fmt.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/html/fmt_vectors.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/html/page.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/html/text.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/markdown.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/math.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/theme.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/themes/default.toml +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/themes/press.toml +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/src/backdraft/render/themes/slate.toml +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/conftest.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/conftest_registry.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/conftest_render.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/continuation_util.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/css_util.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/fakes.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/gate/test_reader_show.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/render/backfill.sidecar.json +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden/render/demo.sidecar.json +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/golden_util.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_bind_evidence.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_card_sizing.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_chunking.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_claims.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_credentials.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_docs.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_dry_run.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_extract_html.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_fetch.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_fmt_parity.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_footnotes.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_forget.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_hashing.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_html.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_ingest_url.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_invariants.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_model.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_placement.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_records_location.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_registry.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_render_markdown.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_render_math.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_snapshot_settings.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_snapshots.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_spec_registry.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_theme.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_tokens.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.0}/tests/test_value_trace.py +0 -0
- {backdraft-0.11.0 → backdraft-0.12.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.12.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,16 @@ 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. |
|
|
162
166
|
|
|
163
167
|
## Open
|
|
164
168
|
|
|
165
169
|
Queued and parked work lives in ROADMAP.md; this list is questions still
|
|
166
170
|
awaiting a decision:
|
|
167
171
|
|
|
168
|
-
- Re-bind/orphan pass on re-ingest of changed docs (chunk ordinal drift)
|
|
169
172
|
- Distribution: published to PyPI as `backdraft` 0.2.0 (2026-07-28); domain
|
|
170
173
|
backdraft.dev acquired; still open: skill registration, deploying site/
|
|
171
174
|
- 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.12.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
|
|
@@ -97,7 +97,8 @@ when the name it wants is taken.
|
|
|
97
97
|
Every ingest line ends with how much text came out, and says which of three
|
|
98
98
|
things happened: a document created, a **new generation** of one whose bytes
|
|
99
99
|
moved — which is when citations into the previous snapshot start reporting
|
|
100
|
-
`drifted
|
|
100
|
+
`drifted`, and [`backdraft locate`](#when-a-source-changes) says where their
|
|
101
|
+
text went — or `unchanged`, a no-op because re-running would reproduce what is
|
|
101
102
|
already there. When almost no text came out, a note names the likely cause (a
|
|
102
103
|
scan with no text layer, a page behind a login) and what to do; it is a note at
|
|
103
104
|
exit 0, because a thin snapshot is still a real one. That source keeps the mark
|
|
@@ -128,12 +129,21 @@ $2,972,160. The resulting economic occupancy of 90.3% reflects an average physic
|
|
|
128
129
|
by concessions, bad debt, and vacancy loss. Physical occupancy was 96.4% in the most recent month and has not
|
|
129
130
|
fallen below 92.0% in any month of the period.
|
|
130
131
|
|
|
131
|
-
… five more chunks, c3 through c7
|
|
132
|
+
… five more chunks, c3 through c7, and then:
|
|
133
|
+
|
|
134
|
+
next page begins: [bd:t12-summary:p2.c1:7bd3]
|
|
135
|
+
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...
|
|
136
|
+
|
|
137
|
+
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
138
|
```
|
|
133
139
|
|
|
134
140
|
Chunks fall on the document's own paragraphs. A PDF stores glyphs at
|
|
135
141
|
coordinates, not paragraphs, so the extractor rebuilds the breaks from the line
|
|
136
|
-
geometry before the chunker sees the page.
|
|
142
|
+
geometry before the chunker sees the page. Where a claim could still straddle
|
|
143
|
+
two chunks — a paragraph the chunker cut for length, or a page break, which
|
|
144
|
+
chunking never sees past — the gate names the chunk on the other side, minted
|
|
145
|
+
like the rest, so the sentence that runs on is cited by both tokens rather than
|
|
146
|
+
the one you happened to be reading.
|
|
137
147
|
|
|
138
148
|
`backdraft read <slug>` on its own is the table of contents: one line per page
|
|
139
149
|
for a source that paginates, and — for a web page, which has none — one line per
|
|
@@ -214,9 +224,9 @@ enough to write this yet?", asked before the draft exists rather than after
|
|
|
214
224
|
$ backdraft session show
|
|
215
225
|
session s-bridgeview (from BACKDRAFT_SESSION)
|
|
216
226
|
|
|
217
|
-
|
|
227
|
+
78 anchors shown across 3 documents
|
|
218
228
|
|
|
219
|
-
t12-summary
|
|
229
|
+
t12-summary 8
|
|
220
230
|
underwriting-model 68
|
|
221
231
|
franklin-county 2
|
|
222
232
|
|
|
@@ -293,6 +303,29 @@ going unnoticed. Exit 0 when everything it checked passed, 2 when something did
|
|
|
293
303
|
not, so a hook can gate on it. It opens no session and mints nothing: an audit
|
|
294
304
|
must not make its subject citable.
|
|
295
305
|
|
|
306
|
+
Holding a file that was sent to you beside a checkout of the project it came
|
|
307
|
+
out of, say so rather than moving the file:
|
|
308
|
+
|
|
309
|
+
```console
|
|
310
|
+
$ backdraft verify ~/Downloads/memo.backdraft.html --against ~/work/project
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
That runs the source check too, and its `sources:` line names the registry that
|
|
314
|
+
answered. `verify` never works out which project a file belongs to from where it
|
|
315
|
+
landed, so the link is yours to assert; a path with no registry in it is refused
|
|
316
|
+
rather than quietly checking the file alone.
|
|
317
|
+
|
|
318
|
+
**Scripts and agents read keys, not lines.** The reports above are worded for
|
|
319
|
+
people, and the wording changes. `backdraft bind memo.md --json` prints the
|
|
320
|
+
record in place of the report (the same bytes bind writes), and
|
|
321
|
+
`backdraft verify memo.backdraft.html --json` prints the check as one object whose
|
|
322
|
+
`findings` each carry a `kind`: `receipt` when the file was edited, `source`
|
|
323
|
+
when a citation does not resolve against the sources today (its status beside
|
|
324
|
+
the record's says whether they moved since binding), `recount` when the summary
|
|
325
|
+
disagrees with the claims. Exit codes are unchanged; the payload says which kind
|
|
326
|
+
of exit 2 you got. The format is in
|
|
327
|
+
[spec/artifact.md](spec/artifact.md#the-verify-report).
|
|
328
|
+
|
|
296
329
|
**Theming.** The artifact ships with a default look and two alternates,
|
|
297
330
|
`press` and `slate`:
|
|
298
331
|
|
|
@@ -388,6 +421,42 @@ are out of reach, and the extractor is a parse rather than a readability guess
|
|
|
388
421
|
its mind between two versions of a site would move anchors. Responses are
|
|
389
422
|
capped at 32 MiB.
|
|
390
423
|
|
|
424
|
+
## When a source changes
|
|
425
|
+
|
|
426
|
+
A re-ingested source re-chunks, so an edit near the top of a page moves every
|
|
427
|
+
citation below it: a paragraph inserted above the one you cited shifts its chunk
|
|
428
|
+
ordinal, and a citation whose words never changed comes back `drifted` because
|
|
429
|
+
its address did. `bind` can say that much and no more. `locate` finds where the
|
|
430
|
+
text went:
|
|
431
|
+
|
|
432
|
+
```console
|
|
433
|
+
$ backdraft locate memo.md
|
|
434
|
+
3 citation(s) in memo.md, 3 drifted
|
|
435
|
+
gone: 1
|
|
436
|
+
moved: 2
|
|
437
|
+
! moved: bd:q3-letter:p1.c1:afd2 — now at bd:q3-letter:p1.c2:afd2 — closed Q3 at 92.0% @24
|
|
438
|
+
! moved: bd:q3-letter:p1.c2:d140 — now at bd:q3-letter:p1.c3:d140 — 1.42x @84
|
|
439
|
+
! gone: bd:q3-letter:p1.c3:00ce — taxes of $412,300 @122
|
|
440
|
+
[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]
|
|
441
|
+
[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]
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
For each citation `bind` would call `drifted`, it looks for the exact text the
|
|
445
|
+
citation quotes — the normalized snippet hash — in the source's current
|
|
446
|
+
snapshot. `moved` is one place holding it, and names the token there.
|
|
447
|
+
`ambiguous` names the places when there are several, or when the citation is a
|
|
448
|
+
cell: an equal value at another address is as often a second figure that
|
|
449
|
+
happens to agree as the same cell after a row was inserted. `gone` is the text
|
|
450
|
+
edited or removed, here a tax figure revised from $412,300, and nothing is
|
|
451
|
+
proposed for it — matching is exact on purpose, because a near match offered as
|
|
452
|
+
a move would rewrite provenance.
|
|
453
|
+
|
|
454
|
+
It proposes and never rewrites, and it is read-only: the document, the registry
|
|
455
|
+
and the ledger are untouched. That includes minting — a token printed here was
|
|
456
|
+
never shown to your session, so swap the moved tokens in, run the `show` the
|
|
457
|
+
closing line names (and read that the text is what you cited), then re-bind.
|
|
458
|
+
Exit 0 whatever it finds: `bind` is the check.
|
|
459
|
+
|
|
391
460
|
## Forgetting a source
|
|
392
461
|
|
|
393
462
|
Ingest is otherwise one-way, and an unattended folder ingest picks up what it
|
|
@@ -70,7 +70,8 @@ when the name it wants is taken.
|
|
|
70
70
|
Every ingest line ends with how much text came out, and says which of three
|
|
71
71
|
things happened: a document created, a **new generation** of one whose bytes
|
|
72
72
|
moved — which is when citations into the previous snapshot start reporting
|
|
73
|
-
`drifted
|
|
73
|
+
`drifted`, and [`backdraft locate`](#when-a-source-changes) says where their
|
|
74
|
+
text went — or `unchanged`, a no-op because re-running would reproduce what is
|
|
74
75
|
already there. When almost no text came out, a note names the likely cause (a
|
|
75
76
|
scan with no text layer, a page behind a login) and what to do; it is a note at
|
|
76
77
|
exit 0, because a thin snapshot is still a real one. That source keeps the mark
|
|
@@ -101,12 +102,21 @@ $2,972,160. The resulting economic occupancy of 90.3% reflects an average physic
|
|
|
101
102
|
by concessions, bad debt, and vacancy loss. Physical occupancy was 96.4% in the most recent month and has not
|
|
102
103
|
fallen below 92.0% in any month of the period.
|
|
103
104
|
|
|
104
|
-
… five more chunks, c3 through c7
|
|
105
|
+
… five more chunks, c3 through c7, and then:
|
|
106
|
+
|
|
107
|
+
next page begins: [bd:t12-summary:p2.c1:7bd3]
|
|
108
|
+
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...
|
|
109
|
+
|
|
110
|
+
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
111
|
```
|
|
106
112
|
|
|
107
113
|
Chunks fall on the document's own paragraphs. A PDF stores glyphs at
|
|
108
114
|
coordinates, not paragraphs, so the extractor rebuilds the breaks from the line
|
|
109
|
-
geometry before the chunker sees the page.
|
|
115
|
+
geometry before the chunker sees the page. Where a claim could still straddle
|
|
116
|
+
two chunks — a paragraph the chunker cut for length, or a page break, which
|
|
117
|
+
chunking never sees past — the gate names the chunk on the other side, minted
|
|
118
|
+
like the rest, so the sentence that runs on is cited by both tokens rather than
|
|
119
|
+
the one you happened to be reading.
|
|
110
120
|
|
|
111
121
|
`backdraft read <slug>` on its own is the table of contents: one line per page
|
|
112
122
|
for a source that paginates, and — for a web page, which has none — one line per
|
|
@@ -187,9 +197,9 @@ enough to write this yet?", asked before the draft exists rather than after
|
|
|
187
197
|
$ backdraft session show
|
|
188
198
|
session s-bridgeview (from BACKDRAFT_SESSION)
|
|
189
199
|
|
|
190
|
-
|
|
200
|
+
78 anchors shown across 3 documents
|
|
191
201
|
|
|
192
|
-
t12-summary
|
|
202
|
+
t12-summary 8
|
|
193
203
|
underwriting-model 68
|
|
194
204
|
franklin-county 2
|
|
195
205
|
|
|
@@ -266,6 +276,29 @@ going unnoticed. Exit 0 when everything it checked passed, 2 when something did
|
|
|
266
276
|
not, so a hook can gate on it. It opens no session and mints nothing: an audit
|
|
267
277
|
must not make its subject citable.
|
|
268
278
|
|
|
279
|
+
Holding a file that was sent to you beside a checkout of the project it came
|
|
280
|
+
out of, say so rather than moving the file:
|
|
281
|
+
|
|
282
|
+
```console
|
|
283
|
+
$ backdraft verify ~/Downloads/memo.backdraft.html --against ~/work/project
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
That runs the source check too, and its `sources:` line names the registry that
|
|
287
|
+
answered. `verify` never works out which project a file belongs to from where it
|
|
288
|
+
landed, so the link is yours to assert; a path with no registry in it is refused
|
|
289
|
+
rather than quietly checking the file alone.
|
|
290
|
+
|
|
291
|
+
**Scripts and agents read keys, not lines.** The reports above are worded for
|
|
292
|
+
people, and the wording changes. `backdraft bind memo.md --json` prints the
|
|
293
|
+
record in place of the report (the same bytes bind writes), and
|
|
294
|
+
`backdraft verify memo.backdraft.html --json` prints the check as one object whose
|
|
295
|
+
`findings` each carry a `kind`: `receipt` when the file was edited, `source`
|
|
296
|
+
when a citation does not resolve against the sources today (its status beside
|
|
297
|
+
the record's says whether they moved since binding), `recount` when the summary
|
|
298
|
+
disagrees with the claims. Exit codes are unchanged; the payload says which kind
|
|
299
|
+
of exit 2 you got. The format is in
|
|
300
|
+
[spec/artifact.md](spec/artifact.md#the-verify-report).
|
|
301
|
+
|
|
269
302
|
**Theming.** The artifact ships with a default look and two alternates,
|
|
270
303
|
`press` and `slate`:
|
|
271
304
|
|
|
@@ -361,6 +394,42 @@ are out of reach, and the extractor is a parse rather than a readability guess
|
|
|
361
394
|
its mind between two versions of a site would move anchors. Responses are
|
|
362
395
|
capped at 32 MiB.
|
|
363
396
|
|
|
397
|
+
## When a source changes
|
|
398
|
+
|
|
399
|
+
A re-ingested source re-chunks, so an edit near the top of a page moves every
|
|
400
|
+
citation below it: a paragraph inserted above the one you cited shifts its chunk
|
|
401
|
+
ordinal, and a citation whose words never changed comes back `drifted` because
|
|
402
|
+
its address did. `bind` can say that much and no more. `locate` finds where the
|
|
403
|
+
text went:
|
|
404
|
+
|
|
405
|
+
```console
|
|
406
|
+
$ backdraft locate memo.md
|
|
407
|
+
3 citation(s) in memo.md, 3 drifted
|
|
408
|
+
gone: 1
|
|
409
|
+
moved: 2
|
|
410
|
+
! moved: bd:q3-letter:p1.c1:afd2 — now at bd:q3-letter:p1.c2:afd2 — closed Q3 at 92.0% @24
|
|
411
|
+
! moved: bd:q3-letter:p1.c2:d140 — now at bd:q3-letter:p1.c3:d140 — 1.42x @84
|
|
412
|
+
! gone: bd:q3-letter:p1.c3:00ce — taxes of $412,300 @122
|
|
413
|
+
[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]
|
|
414
|
+
[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]
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
For each citation `bind` would call `drifted`, it looks for the exact text the
|
|
418
|
+
citation quotes — the normalized snippet hash — in the source's current
|
|
419
|
+
snapshot. `moved` is one place holding it, and names the token there.
|
|
420
|
+
`ambiguous` names the places when there are several, or when the citation is a
|
|
421
|
+
cell: an equal value at another address is as often a second figure that
|
|
422
|
+
happens to agree as the same cell after a row was inserted. `gone` is the text
|
|
423
|
+
edited or removed, here a tax figure revised from $412,300, and nothing is
|
|
424
|
+
proposed for it — matching is exact on purpose, because a near match offered as
|
|
425
|
+
a move would rewrite provenance.
|
|
426
|
+
|
|
427
|
+
It proposes and never rewrites, and it is read-only: the document, the registry
|
|
428
|
+
and the ledger are untouched. That includes minting — a token printed here was
|
|
429
|
+
never shown to your session, so swap the moved tokens in, run the `show` the
|
|
430
|
+
closing line names (and read that the text is what you cited), then re-bind.
|
|
431
|
+
Exit 0 whatever it finds: `bind` is the check.
|
|
432
|
+
|
|
364
433
|
## Forgetting a source
|
|
365
434
|
|
|
366
435
|
Ingest is otherwise one-way, and an unattended folder ingest picks up what it
|