backdraft 0.13.0__tar.gz → 0.14.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.13.0 → backdraft-0.14.0}/.claude-plugin/plugin.json +1 -1
- {backdraft-0.13.0 → backdraft-0.14.0}/DESIGN.md +4 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/PKG-INFO +14 -6
- {backdraft-0.13.0 → backdraft-0.14.0}/README.md +13 -5
- {backdraft-0.13.0 → backdraft-0.14.0}/ROADMAP.md +173 -219
- {backdraft-0.13.0 → backdraft-0.14.0}/SPEC.md +10 -4
- {backdraft-0.13.0 → backdraft-0.14.0}/demo/walkthrough.md +30 -5
- {backdraft-0.13.0 → backdraft-0.14.0}/pyproject.toml +1 -1
- {backdraft-0.13.0 → backdraft-0.14.0}/site/docs.html +10 -6
- {backdraft-0.13.0 → backdraft-0.14.0}/site/llms.txt +25 -7
- {backdraft-0.13.0 → backdraft-0.14.0}/skills/backdraft/SKILL.md +27 -9
- {backdraft-0.13.0 → backdraft-0.14.0}/skills/backdraft-artifact/SKILL.md +15 -3
- {backdraft-0.13.0 → backdraft-0.14.0}/skills/backdraft-backfill/SKILL.md +3 -1
- {backdraft-0.13.0 → backdraft-0.14.0}/spec/artifact.md +22 -5
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/bind/binder.py +7 -15
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/bind/cli.py +21 -5
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/cli.py +17 -2
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/cli_context.py +86 -1
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/gate/cli.py +6 -2
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/gate/reader.py +158 -15
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/gate/searcher.py +1 -1
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/registry/__init__.py +6 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/registry/ledger.py +15 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/registry/store.py +102 -15
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/cli.py +118 -16
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/gate/fake_registry.py +7 -2
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/gate/test_gate_cli.py +6 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/gate/test_gate_reader.py +130 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/gate/test_gate_searcher.py +22 -1
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_bind_cli.py +90 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_cli.py +52 -0
- backdraft-0.14.0/tests/test_next_moves.py +75 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_registry.py +40 -0
- backdraft-0.14.0/tests/test_search_window.py +135 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_verify_cli.py +265 -1
- {backdraft-0.13.0 → backdraft-0.14.0}/uv.lock +1 -1
- {backdraft-0.13.0 → backdraft-0.14.0}/.claude-plugin/marketplace.json +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/.github/workflows/ci.yml +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/.github/workflows/release.yml +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/.github/workflows/site.yml +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/.gitignore +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/.python-version +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/LICENSE +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/demo/generate_sources.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/demo/memo.backdraft.html +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/demo/memo.md +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/demo/sources/t12-summary.pdf +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/site/demo.html +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/site/index.html +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/site/privacy.html +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/spec/chunking.md +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/spec/registry.md +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/spec/tokens.md +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/__init__.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/__main__.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/bind/evidence.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/bind/verify/entail.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/credentials.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/__init__.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/base.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/csv.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/docx.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/html.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/image.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/pdf_text.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/pptx.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/sheet.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/snapshots.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/vlm.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/vlm_settings.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/xls.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/extract/xlsx.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/fetch.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/gate/__init__.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/kernel/artifact.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/kernel/chunking.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/kernel/model.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/registry/schema.sql +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/__init__.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/_text.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/footnotes.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/html/__init__.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/html/assets.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/html/components.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/html/fmt.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/html/fmt_vectors.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/html/page.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/html/text.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/markdown.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/math.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/sidecar.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/theme.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/themes/default.toml +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/themes/press.toml +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/src/backdraft/render/themes/slate.toml +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/conftest.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/conftest_registry.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/conftest_render.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/continuation_util.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/css_util.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/fakes.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/gate/test_gate_adjoining.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/gate/test_gate_ledger.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/gate/test_reader_show.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/render/backfill.sidecar.json +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden/render/demo.sidecar.json +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/golden_util.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_bind_evidence.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_card_sizing.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_chunking.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_claims.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_credentials.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_docs.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_doctor.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_dry_run.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_extract.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_extract_html.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_fetch.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_fmt_parity.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_footnotes.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_forget.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_gate_integration.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_hashing.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_html.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_ingest_url.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_invariants.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_locate.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_model.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_placement.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_records_location.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_reingest.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_render_cli.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_render_markdown.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_render_math.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_snapshot_settings.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_snapshots.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_spec_registry.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_theme.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_tokens.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.0}/tests/test_value_trace.py +0 -0
- {backdraft-0.13.0 → backdraft-0.14.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.14.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",
|
|
@@ -166,6 +166,10 @@ Self-contained HTML (plus the machine-readable sidecar): **quotes + receipts + e
|
|
|
166
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
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
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. |
|
|
169
|
+
| 2026-10-12 | **`verify` re-checks `not_shown` against the ledger session the record names, reading it and never creating it.** The source tier claimed to report statuses "as `bind` would" with one status it could not reach, so a record saying a claim cited something its author never read came back `resolved` from the tier meant to be the stronger check — and the claim this product rests on, that the citable set is exactly the set the gate emitted, was the one a recipient holding the registry could not re-make. The standing objection was the docstring's: verify is read-only, opens no session and mints nothing, which is what separates it from `show`. It is answered rather than overridden by asking only what the record names: the session is the record's `session_id`, never one verify resolves from flags or the environment, and it is found with a new `Registry.has_session` instead of `ensure_session`, because a session id this registry has never seen is a fact about where the record came from and creating it would both widen the ledger and turn "absent" into "empty", which reads as every citation `not_shown`. Absent, `null` and backfill each leave `not_shown` unchecked and say which on a `ledger:` line; a ledger that agrees with every citation prints no line, so every report it stands behind is byte-identical to before, `demo/`'s included. The `was_shown` rule moved into `registry.citation_for` behind an optional `session_id` rather than being restated beside bind's copy — the walk already lived there so the two commands could not drift, and the one status left outside it was the one that just gained a second caller. A `not_shown` today is a `source` finding and exits 2 like a kept `unresolved`, since both are citations the sources and the ledger do not stand behind; the ledger is read as it stands, so a session that has read the passage since binding verifies clean. The cost is `verify --json`: `sources` gains a `ledger` key (`read`, `absent`, `no_session`, `backfill`, or `null` when the tier did not run), additive under verify-v1's unknown-keys rule but not byte-identical, taken because a caller branching on `findings` otherwise could not tell a `resolved` the ledger confirmed from one it was never asked about — the plain report can say that, and one set of findings with two presenters (2026-09-14) means the object must too. |
|
|
170
|
+
| 2026-10-13 | **`bind` and `verify` close with one `next:` line per kind of failure they printed, naming the command that addresses it, from one table both import.** The 2026-09-01 row made `ingest`'s failures say the move as well as the diagnosis; the two commands whose output is the product still said only the diagnosis, and the move differs sharply per status — `show` clears a `not_shown`, `search` (or an honest uncited claim) an `unresolved`, `locate` a `drifted`, a syntax fix a `malformed`, and a `receipt` finding means the file was edited, the opposite of every source finding — so it lived only in the skill, where a hook's log, a CI job or a person never sees it. Grouped per kind rather than appended to each line item, because one move answers every item of its kind and eighteen identical hints would bury the items. `cli_context.NEXT_MOVES` is the one owner, keyed by the closed `CitationStatus` set plus `unmatched`, `receipt` and `recount`, and a test holds it total and holds every command it names to one `backdraft --help` lists; the lines carry the document and session the run used, shell-quoted, so the command is the one to type. Display only: no exit code, status or record field moves, `--json` carries none of it (2026-09-14 serves the caller that parses, this the one that relays), and a clean run's report is byte-identical. The tradeoff is a long line on a failing run and a second wording of the skill's per-status guidance; the skill keeps the nuance (withdrawn sources, `ambiguous` and `gone` from `locate`) and the line keeps only the command. In `bind` the lines sit above the default-session note, which keeps the last word it already had. |
|
|
171
|
+
| 2026-10-15 | **A search hit's excerpt opens on what FTS5 matched when the chunk's opening does not show it, and the registry, not the gate, says what matched.** `search` printed every hit's first 160 characters, on purpose: `excerpt`'s NOTE said FTS5 decides what matched — stemming, a phrase run as one — and the gate re-derives nothing the registry owns. The cost was that a long chunk's excerpt was its opening rather than its evidence; the walkthrough's own `search "replacement reserve"` returned a hit whose 160 characters never contained the phrase, and an agent picking among twenty hits does not `show` each one. The NOTE is answered rather than overridden: `Registry.search` asks FTS5 itself, through `highlight()` with two control-character markers no extracted snippet carries, and `SearchHit` gains `matches` — the marked spans as offsets into the receipt snippet, checked back against that snippet and dropped whole if the marked text disagrees, since no span is better than a wrong one. `highlight()` over `snippet()` because `snippet()` cuts by FTS5 tokens and writes its own ellipses, which would put a second length rule beside `EXCERPT_CHARS`; the offsets leave the cut where it was and hand the gate only the fact it could not know. The gate decides where to cut and nothing else: a hit whose first match ends inside the first `EXCERPT_CHARS` prints exactly what it did, so every documented `search` block over cells and short chunks is byte-identical and only the excerpts that hid their evidence move; otherwise the window opens `MATCH_LEAD_CHARS` ahead of whichever match lets it hold the most matches whole, snapped to a word, kept inside the text, and elided at whichever end it cut. The spans travel through `normalize` as markers rather than as offset arithmetic, so the whitespace collapse and NFC move them with the text, and both ends read markers through the one `registry.unmark`, so the registry and the gate cannot disagree about where a span ends. A phrase-fallback retry needs nothing extra — `highlight()` marks the phrase that actually ran. `matches` defaults to empty, so Addendum A's three-field `SearchHit` still constructs and a registry that reports nothing renders the old cut. Display only: no token, anchor, receipt, ledger row or `registry-v1` field changes, and the FTS index stays outside the export. The cost, named: a windowed excerpt can begin mid-sentence, and a multi-term query's densest cluster is not always the sentence the caller meant — the token is still the whole chunk and `show` still prints it. |
|
|
172
|
+
| 2026-10-15 | **A sheet is thin when no cell holds a value, not when its text is short.** The 2026-08-20 threshold is a fair proxy for prose — a login wall and a scan both extract almost nothing — and a wrong one for a table: a three-row `rates.csv` holding a going-in and an exit cap rate ingests at well under 200 characters, so `ingest` told the agent to "tell the user the source came back thin", `ls` and the gate marked the row, and the writing skill said not to cite it. The heuristic built to stop an agent citing an empty page stopped it citing a correct, complete rate table and had it say something false about the source, and small and complete is normal for a sheet: an assumptions tab, a rate card, a cap table. `gate.thin_mark` now decides by page kind: an extraction whose pages are all sheets is thin exactly when no cell holds a value, read off the `cells` `Registry.pages` already returns, because a workbook's shell is charts, images or pivot caches with nothing in its grid — and the rendered table's pipes and `[B10]` prefixes make a character count meaningless there anyway. Prose, and the zero-page extraction, keep the character threshold exactly. Page kind rather than extension, so a CSV forced through `--extractor text` is judged as the prose it was extracted as. This amends 2026-08-20 rather than contradicting it: still display only, still exit 0, still no token, anchor, status or byte of the artifact derived from it, and still one owner — the 2026-09-09 single owner is what made this a change in one function that `ingest`'s note and all three list surfaces follow. The mark keeps its wording, `little text: N chars`, on a marked sheet too: the count is still true, an agent that learned the one string keeps reading it, and `cli._THIN_CAUSE` gains a sheet cause shared by `xlsx`, `xls` and `csv` that says what the rule is, so the mark on an empty workbook is not read as a verdict on short tables. A registry's output changes only on the table rows this exists to unmark and on sheet sources that hold no values at all; the demo's three sources print byte for byte what they did, list and tables of contents both. The cost taken: a sheet whose only values are a stray title cell and nothing else is no longer marked, because one value is a value — the rule trades that miss for never again marking a complete table, which is the case that happens. |
|
|
169
173
|
|
|
170
174
|
## Open
|
|
171
175
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: backdraft
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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
|
|
@@ -109,7 +109,10 @@ afterwards — `backdraft read`'s document list, its table of contents and
|
|
|
109
109
|
`backdraft ls` end its row with `little text: N chars` — because a registry
|
|
110
110
|
travels with the project folder and whoever writes against it is usually not
|
|
111
111
|
whoever ingested it. Ordinary sources gain nothing: only the rows worth doubting
|
|
112
|
-
say anything.
|
|
112
|
+
say anything. A table is judged by its cells rather than its length — a
|
|
113
|
+
three-row rate card is small and complete, so a spreadsheet or CSV is marked
|
|
114
|
+
only when none of its cells holds a value, which is what a workbook of charts or
|
|
115
|
+
images extracts to.
|
|
113
116
|
|
|
114
117
|
**Read.** The page arrives with a citable name over each chunk. This is the whole
|
|
115
118
|
mechanism: what you can cite is exactly what you were shown.
|
|
@@ -227,11 +230,11 @@ enough to write this yet?", asked before the draft exists rather than after
|
|
|
227
230
|
$ backdraft session show
|
|
228
231
|
session s-bridgeview (from BACKDRAFT_SESSION)
|
|
229
232
|
|
|
230
|
-
|
|
233
|
+
83 anchors shown across 3 documents
|
|
231
234
|
|
|
232
235
|
t12-summary 8
|
|
233
236
|
underwriting-model 68
|
|
234
|
-
franklin-county
|
|
237
|
+
franklin-county 7
|
|
235
238
|
|
|
236
239
|
[Read more: backdraft read <slug> <page>]
|
|
237
240
|
```
|
|
@@ -264,13 +267,15 @@ bound 17 claim(s), 18 citation(s) [frontwalk] against session s-bridgeview
|
|
|
264
267
|
value-trace: pass 17
|
|
265
268
|
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @3629
|
|
266
269
|
wrote .backdraft/records/memo.backdraft.json
|
|
270
|
+
next: unresolved — the token names nothing. `backdraft show <token>` says which half is wrong, and `backdraft search "<the fact>" --session s-bridgeview` finds the real one. A claim no source supports says so in its text; a withdrawn source is replaced by one still ingested, not re-ingested. Never clear one by deleting the token.
|
|
267
271
|
|
|
268
272
|
$ echo $?
|
|
269
273
|
2
|
|
270
274
|
```
|
|
271
275
|
|
|
272
276
|
Exit 2 means a citation did not resolve, the code a `Stop` hook or a CI job
|
|
273
|
-
gates on.
|
|
277
|
+
gates on. The closing `next:` line names the command that fixes each kind of
|
|
278
|
+
failure the report printed.
|
|
274
279
|
|
|
275
280
|
**Render.** One self-contained HTML file: the document, the receipts, the
|
|
276
281
|
evidence, cited page images, spreadsheet cells in place, the machine-readable
|
|
@@ -303,7 +308,10 @@ That first tier needs nothing but the file, which is the point — it is the che
|
|
|
303
308
|
someone who was emailed the artifact can still make. Run it inside the project
|
|
304
309
|
the document was bound in and it also re-resolves every citation against the
|
|
305
310
|
registry, so a source that has changed since shows up as `drifted` rather than
|
|
306
|
-
going unnoticed
|
|
311
|
+
going unnoticed — and asks the ledger session the record names whether it was
|
|
312
|
+
ever shown each citation, so a `not_shown` is re-checked rather than taken on
|
|
313
|
+
the record's word. That session is read, never created: when this registry does
|
|
314
|
+
not have it, a `ledger:` line says so. Exit 0 when everything it checked passed, 2 when something did
|
|
307
315
|
not, so a hook can gate on it. It opens no session and mints nothing: an audit
|
|
308
316
|
must not make its subject citable.
|
|
309
317
|
|
|
@@ -82,7 +82,10 @@ afterwards — `backdraft read`'s document list, its table of contents and
|
|
|
82
82
|
`backdraft ls` end its row with `little text: N chars` — because a registry
|
|
83
83
|
travels with the project folder and whoever writes against it is usually not
|
|
84
84
|
whoever ingested it. Ordinary sources gain nothing: only the rows worth doubting
|
|
85
|
-
say anything.
|
|
85
|
+
say anything. A table is judged by its cells rather than its length — a
|
|
86
|
+
three-row rate card is small and complete, so a spreadsheet or CSV is marked
|
|
87
|
+
only when none of its cells holds a value, which is what a workbook of charts or
|
|
88
|
+
images extracts to.
|
|
86
89
|
|
|
87
90
|
**Read.** The page arrives with a citable name over each chunk. This is the whole
|
|
88
91
|
mechanism: what you can cite is exactly what you were shown.
|
|
@@ -200,11 +203,11 @@ enough to write this yet?", asked before the draft exists rather than after
|
|
|
200
203
|
$ backdraft session show
|
|
201
204
|
session s-bridgeview (from BACKDRAFT_SESSION)
|
|
202
205
|
|
|
203
|
-
|
|
206
|
+
83 anchors shown across 3 documents
|
|
204
207
|
|
|
205
208
|
t12-summary 8
|
|
206
209
|
underwriting-model 68
|
|
207
|
-
franklin-county
|
|
210
|
+
franklin-county 7
|
|
208
211
|
|
|
209
212
|
[Read more: backdraft read <slug> <page>]
|
|
210
213
|
```
|
|
@@ -237,13 +240,15 @@ bound 17 claim(s), 18 citation(s) [frontwalk] against session s-bridgeview
|
|
|
237
240
|
value-trace: pass 17
|
|
238
241
|
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @3629
|
|
239
242
|
wrote .backdraft/records/memo.backdraft.json
|
|
243
|
+
next: unresolved — the token names nothing. `backdraft show <token>` says which half is wrong, and `backdraft search "<the fact>" --session s-bridgeview` finds the real one. A claim no source supports says so in its text; a withdrawn source is replaced by one still ingested, not re-ingested. Never clear one by deleting the token.
|
|
240
244
|
|
|
241
245
|
$ echo $?
|
|
242
246
|
2
|
|
243
247
|
```
|
|
244
248
|
|
|
245
249
|
Exit 2 means a citation did not resolve, the code a `Stop` hook or a CI job
|
|
246
|
-
gates on.
|
|
250
|
+
gates on. The closing `next:` line names the command that fixes each kind of
|
|
251
|
+
failure the report printed.
|
|
247
252
|
|
|
248
253
|
**Render.** One self-contained HTML file: the document, the receipts, the
|
|
249
254
|
evidence, cited page images, spreadsheet cells in place, the machine-readable
|
|
@@ -276,7 +281,10 @@ That first tier needs nothing but the file, which is the point — it is the che
|
|
|
276
281
|
someone who was emailed the artifact can still make. Run it inside the project
|
|
277
282
|
the document was bound in and it also re-resolves every citation against the
|
|
278
283
|
registry, so a source that has changed since shows up as `drifted` rather than
|
|
279
|
-
going unnoticed
|
|
284
|
+
going unnoticed — and asks the ledger session the record names whether it was
|
|
285
|
+
ever shown each citation, so a `not_shown` is re-checked rather than taken on
|
|
286
|
+
the record's word. That session is read, never created: when this registry does
|
|
287
|
+
not have it, a `ledger:` line says so. Exit 0 when everything it checked passed, 2 when something did
|
|
280
288
|
not, so a hook can gate on it. It opens no session and mints nothing: an audit
|
|
281
289
|
must not make its subject citable.
|
|
282
290
|
|
|
@@ -32,219 +32,7 @@ best evidence available for what those five should be.
|
|
|
32
32
|
|
|
33
33
|
## Now
|
|
34
34
|
|
|
35
|
-
### 1. `
|
|
36
|
-
|
|
37
|
-
**Intent.** `verify`'s source tier re-resolves every token and reports the
|
|
38
|
-
statuses "as `bind` would", with one gap the code names out loud: `not_shown`
|
|
39
|
-
cannot appear, because it is a fact about a ledger session and verify opens none
|
|
40
|
-
(`render/cli.py`'s `_against_sources`). So a record that says a claim cited
|
|
41
|
-
something its author never read comes back `resolved` from the tier that is
|
|
42
|
-
supposed to be the stronger check, and the strongest claim this product makes —
|
|
43
|
-
that the set of citable tokens is exactly the set the gate emitted — is the one
|
|
44
|
-
claim a recipient holding the registry cannot re-check. That recipient is the
|
|
45
|
-
`backdraft-artifact` skill's whole case: they have the artifact, they have the
|
|
46
|
-
project, and the check they most want is the one that says the author read it.
|
|
47
|
-
|
|
48
|
-
**Shape.** `render/cli.py`'s source tier plus one registry read. The record
|
|
49
|
-
already carries `session_id`, and `Registry.was_shown(session_id, token)`
|
|
50
|
-
already answers the question one token at a time — that is the call `bind`
|
|
51
|
-
makes, so a status printed here stays the status a re-bind would print. The
|
|
52
|
-
standing objection is in the docstring and must be answered rather than
|
|
53
|
-
overridden: "read-only, it opens no session and mints nothing, which is what
|
|
54
|
-
separates it from `backdraft show`". Reading a ledger is not minting into one,
|
|
55
|
-
and the session read is the one the record names rather than one verify chose —
|
|
56
|
-
so `ensure_session` must not be called, a session id absent from the registry is
|
|
57
|
-
reported as absent rather than created, and a `session_id` of `null` is reported
|
|
58
|
-
as a run that had none. Nothing about `show`'s minting changes. The `sources:`
|
|
59
|
-
line gains the status, and so do `--json`'s `sources.by_status` and `findings` —
|
|
60
|
-
one set of findings, two presenters (2026-09-14); the existing counts must keep
|
|
61
|
-
meaning what they mean.
|
|
62
|
-
|
|
63
|
-
**Acceptance.** In `demo/`, bind a memo under a session that was never shown one
|
|
64
|
-
of its citations, render, then `backdraft verify` in the project: the source tier
|
|
65
|
-
reports that citation `not_shown` and says which session it read. Verified from
|
|
66
|
-
outside the project, the tier does not run and the output is byte-identical to
|
|
67
|
-
today. A record whose `session_id` names no session in this registry says so and
|
|
68
|
-
does not create it — assert the registry's session count before and after. A
|
|
69
|
-
record with `session_id: null` says the run had no session. Every existing verify
|
|
70
|
-
output where the ledger agrees with the record is byte-identical, pinned,
|
|
71
|
-
including `demo/`'s. `spec/artifact.md` § Checking an artifact,
|
|
72
|
-
`skills/backdraft-artifact/SKILL.md`, `site/llms.txt` and `README.md` follow.
|
|
73
|
-
DESIGN row.
|
|
74
|
-
|
|
75
|
-
**Size.** Two to three days.
|
|
76
|
-
|
|
77
|
-
### 2. `bind` and `verify` name the failure and not the move
|
|
78
|
-
|
|
79
|
-
**Intent.** 2026-09-01 made `ingest`'s failures say what to do as well as what
|
|
80
|
-
went wrong, on the argument that a calling agent reads the reason and acts on it.
|
|
81
|
-
`bind` and `verify` are the two commands whose output *is* the product, and
|
|
82
|
-
neither says a next step anywhere: `! not_shown: <token> — <claim> @<offset>` is
|
|
83
|
-
a complete diagnosis and a blank instruction. The move differs sharply per status
|
|
84
|
-
and is not guessable — `not_shown` clears by showing the token, `unresolved`
|
|
85
|
-
needs a search and a new token or an uncited claim, `drifted` needs `locate`
|
|
86
|
-
and, where the text did not simply move, both snippets read and a judgement,
|
|
87
|
-
`malformed` is an href to fix, and a `receipt:` finding means the artifact was
|
|
88
|
-
edited rather than the sources moved. Today that mapping
|
|
89
|
-
lives only in `skills/backdraft/SKILL.md`, which means it works for an agent
|
|
90
|
-
running the skill and for nobody else — a hook, a CI job, a person, or any agent
|
|
91
|
-
that reached the command another way.
|
|
92
|
-
|
|
93
|
-
**Shape.** A closing block on both commands, one line per status actually
|
|
94
|
-
present, naming the exact command that addresses it — the shape `ingest`'s
|
|
95
|
-
grouped notes already use, and never a hint appended to each of eighteen line
|
|
96
|
-
items. One owner for the mapping, imported by `bind/cli.py` and `render/cli.py`
|
|
97
|
-
rather than written twice; the status set is the artifact format's and closed,
|
|
98
|
-
so the mapping is total and a test asserts every `CitationStatus` has a line.
|
|
99
|
-
`verify` adds the `receipt` case, which is not a citation status and is the
|
|
100
|
-
finding that means something categorically different. Display only: no exit code
|
|
101
|
-
moves, no status moves, no record field is added — a run that came out clean
|
|
102
|
-
prints exactly what it prints today. Not `--json` (2026-09-14), which gave the
|
|
103
|
-
same two commands a machine-readable payload; that serves a caller that parses,
|
|
104
|
-
this one serves the caller that reads, and both are wanted because the human
|
|
105
|
-
report is what gets relayed to the user.
|
|
106
|
-
|
|
107
|
-
**Acceptance.** In `demo/`, `backdraft bind memo.md --session s-bridgeview
|
|
108
|
-
--check value-trace,overlap` closes by naming `backdraft search` for its one
|
|
109
|
-
`unresolved`, and says nothing about the statuses it did not produce. A bind that
|
|
110
|
-
comes out clean is byte-identical to today, as is a clean `verify` — pin both. A
|
|
111
|
-
test enumerates `CitationStatus` and asserts each has a line and a command that
|
|
112
|
-
`backdraft --help` lists. `verify` on an artifact with one edited snippet says
|
|
113
|
-
the file was changed rather than the sources. `demo/walkthrough.md`'s bind and
|
|
114
|
-
verify blocks show the real output. DESIGN row.
|
|
115
|
-
|
|
116
|
-
**Size.** Two days.
|
|
117
|
-
|
|
118
|
-
### 3. A chunk the table of contents lists is a chunk no read can ask for
|
|
119
|
-
|
|
120
|
-
**Intent.** Since 2026-09-07 `backdraft read <slug>` on a one-page source lists
|
|
121
|
-
that page's chunks — `p1.c9 From Wikipedia, the free encyclopedia...` — and the
|
|
122
|
-
walkthrough tells the reader the list is how to find where an article starts.
|
|
123
|
-
It then offers one way in: `backdraft read franklin-county p1`, which begins at
|
|
124
|
-
the site's navigation menu and, under the 2026-09-08 budget, spends 12,000
|
|
125
|
-
characters to reach `c25`. The locator the list just printed is not something
|
|
126
|
-
`read` accepts. `backdraft read franklin-county p1.c9` exits 1 with `no page or
|
|
127
|
-
sheet named 'p1.c9'; sheets: Franklin County, Ohio - Wikipedia`, which is wrong
|
|
128
|
-
twice: the selector is refused, and a web page's title is called a sheet. So an
|
|
129
|
-
agent that did the right thing and read the map is left to compute a character
|
|
130
|
-
`--offset` it has no way to derive, or to search for words it has not seen yet.
|
|
131
|
-
|
|
132
|
-
**Shape.** The gate's selector grammar, not the token grammar: `p1.c9` is a
|
|
133
|
-
locator `spec/tokens.md` already defines, and this only lets `select_pages`
|
|
134
|
-
accept `pN.cM` and `pN.cM-cK` as *read* selectors (SPEC § Gate lists the forms).
|
|
135
|
-
A chunk selection is a `Selection` that also carries an ordinal range, and
|
|
136
|
-
`render_page_read` shows exactly those chunks through `_Window`, so the budget,
|
|
137
|
-
the whole-chunk rule and the continuation line — with its `--limit` and typed
|
|
138
|
-
`--session` — keep working, and the continuation names a command in the same
|
|
139
|
-
selector form. Minting is the gate's rule unchanged: what is printed is minted
|
|
140
|
-
and nothing else. Only `page`-kind pages take a chunk selector; on a sheet it is
|
|
141
|
-
refused with a message saying a sheet is read by name or page and cited by cell.
|
|
142
|
-
Cell *ranges* are Parked's "Excel region maps and range reads" and stay there
|
|
143
|
-
with their objection. A backwards range, or one naming no chunk, is a
|
|
144
|
-
`GateError` naming the page's chunk span in `_what_exists`'s shape. Fix
|
|
145
|
-
`_what_exists` while there: it prints `sheets:` whenever every page has a name,
|
|
146
|
-
which is true of every titled web page, so key it on `kind`. The one-page
|
|
147
|
-
table of contents' `[Read one: …]` hint stays `p1`, which keeps every existing
|
|
148
|
-
TOC byte-identical; the walkthrough's paragraph about finding `p1.c9` gains the
|
|
149
|
-
command that reads from there.
|
|
150
|
-
|
|
151
|
-
**Acceptance.** In `demo/`, `backdraft read franklin-county p1.c9` prints `c9`
|
|
152
|
-
alone under its own token and the ledger gains that one anchor and no other;
|
|
153
|
-
`p1.c9-c14` prints six chunks; `p1.c14-c9` and `p1.c99` exit 1 naming the page's
|
|
154
|
-
chunk span. A chunk range longer than the budget closes with a continuation that,
|
|
155
|
-
followed, walks the range exactly once — drive it with
|
|
156
|
-
`tests/continuation_util.py`. `backdraft read underwriting-model p1.c1` exits 1
|
|
157
|
-
saying how a sheet is read and cited. A titled web page's unknown-selector error
|
|
158
|
-
no longer says `sheets:`. Every existing selector form prints byte for byte what
|
|
159
|
-
it does today, pinned by the existing goldens. SPEC § Gate, `site/docs.html`,
|
|
160
|
-
`site/llms.txt`, `skills/backdraft/SKILL.md` and `demo/walkthrough.md` name the
|
|
161
|
-
form.
|
|
162
|
-
|
|
163
|
-
**Size.** Two days.
|
|
164
|
-
|
|
165
|
-
### 4. A search hit's excerpt can leave out the words that matched
|
|
166
|
-
|
|
167
|
-
**Intent.** `search` prints each hit's first 160 characters, and
|
|
168
|
-
`gate.reader.excerpt`'s NOTE says so on purpose: the cut is from the start rather
|
|
169
|
-
than centred on the match, because FTS5 decides what matched and the gate
|
|
170
|
-
re-derives nothing the registry owns. The walkthrough's own step 11 shows the
|
|
171
|
-
cost. `backdraft search "replacement reserve"` returns
|
|
172
|
-
`bd:t12-summary:p3.c4:6f0f` over an excerpt about capital expenditures that never
|
|
173
|
-
contains the phrase — the sentence that does starts past character 200 — and
|
|
174
|
-
the walkthrough can say "that is the snippet that actually says it" only because
|
|
175
|
-
it ran `show` next. An agent choosing among twenty hits does not run `show` on
|
|
176
|
-
each; it picks by excerpts that, for any chunk longer than a couple of lines, are
|
|
177
|
-
the chunk's opening rather than its evidence. Web pages, whose chunks run to a
|
|
178
|
-
thousand characters, made that the common case.
|
|
179
|
-
|
|
180
|
-
**Shape.** Answer the NOTE rather than override it: ask the registry, which does
|
|
181
|
-
own what matched. The `search` table in `registry/schema.sql` is a
|
|
182
|
-
content-storing FTS5 table, so `Registry.search` can return FTS5's own window
|
|
183
|
-
for each hit — `snippet()`, or match offsets out of `highlight()` markers — and
|
|
184
|
-
`SearchHit` gains that field. The gate then renders the window it was handed,
|
|
185
|
-
which is the rule the NOTE protects, and `Registry.search` stays the only place
|
|
186
|
-
an FTS5 function is called. Display only: token, anchor and receipt are
|
|
187
|
-
untouched, and the excerpt stays one line of at most `EXCERPT_CHARS` with an
|
|
188
|
-
elision mark at whichever end was cut. Keep byte-identity where it is free: a
|
|
189
|
-
hit whose match already falls inside the first `EXCERPT_CHARS` prints exactly
|
|
190
|
-
today's excerpt, so only the hits that were hiding their evidence change, and
|
|
191
|
-
most documented `search` blocks do not move. A phrase-fallback retry windows on
|
|
192
|
-
the phrase that actually ran. The DESIGN row says what replaced the NOTE and why
|
|
193
|
-
the registry, not the gate, computes the window.
|
|
194
|
-
|
|
195
|
-
**Acceptance.** In `demo/`, `backdraft search "replacement reserve"` shows the
|
|
196
|
-
`p3.c4` excerpt containing "replacement reserve", opening with an elision mark.
|
|
197
|
-
`backdraft search "24850000"` and every other `search` block in
|
|
198
|
-
`demo/walkthrough.md` and `README.md` whose match lies in the first 160
|
|
199
|
-
characters is byte-identical, pinned. A query that matches only late in a long
|
|
200
|
-
chunk, and a query run through the phrase fallback, each window on what FTS5
|
|
201
|
-
matched, asserted by test. No `registry-v1` export field changes. SPEC § Gate's
|
|
202
|
-
`search` line follows. DESIGN row.
|
|
203
|
-
|
|
204
|
-
**Size.** Two days.
|
|
205
|
-
|
|
206
|
-
### 5. A small table is marked a shell, and the skill says not to cite it
|
|
207
|
-
|
|
208
|
-
**Intent.** The thin-source mark (2026-08-20, carried onto every list
|
|
209
|
-
2026-09-09) is a character count under `THIN_SOURCE_CHARS`, and for prose that
|
|
210
|
-
is a fair proxy: a login wall and a scanned PDF both extract almost nothing. For
|
|
211
|
-
a table it is not. A three-row `rates.csv` — a going-in and an exit cap rate —
|
|
212
|
-
ingests at 188 characters, and ingest says `note: little text extracted` and
|
|
213
|
-
tells the agent to "tell the user the source came back thin rather than citing
|
|
214
|
-
the shell of it"; `ls` ends its row `little text: 188 chars`; and
|
|
215
|
-
`skills/backdraft/SKILL.md` says a row carrying that mark "is a source to read
|
|
216
|
-
and report on, not one to cite". The heuristic built to stop an agent citing an
|
|
217
|
-
empty page now stops it citing a correct, complete rate table, and has it tell
|
|
218
|
-
the user something false about the source. Sheets are where small and complete
|
|
219
|
-
is normal: an assumptions tab, a rate card, a cap table.
|
|
220
|
-
|
|
221
|
-
**Shape.** `gate.thin_mark` owns the rule and is the only place it changes;
|
|
222
|
-
`ingest`'s grouped note and all three list surfaces follow, which is what the
|
|
223
|
-
2026-09-09 single owner was for (and what Friday's cleanup made true of `ingest`,
|
|
224
|
-
which had kept a private copy of the threshold). Decide by page kind, not by
|
|
225
|
-
extension: an extraction whose pages are sheets is thin when it holds no
|
|
226
|
-
non-empty cell values at all — `Registry.pages` already returns sheet pages with
|
|
227
|
-
their `cells` — because a sheet's shell is a workbook of charts, images or pivot
|
|
228
|
-
caches, not a short table, and the rendered text's pipes and `[B10]` prefixes
|
|
229
|
-
make a character count meaningless there anyway. Prose keeps the character
|
|
230
|
-
threshold exactly. `cli._THIN_CAUSE` gains the sheet cause. The DESIGN row
|
|
231
|
-
amends 2026-08-20 rather than contradicting it: still display only, still exit
|
|
232
|
-
0, still no token, anchor or status derived from it. A registry's output changes
|
|
233
|
-
only on the table rows this exists to unmark and on sheet sources that hold no
|
|
234
|
-
values at all.
|
|
235
|
-
|
|
236
|
-
**Acceptance.** `backdraft ingest rates.csv` for a three-row table prints no
|
|
237
|
-
thin note, and `ls`, `backdraft read` and its table of contents carry no mark. A
|
|
238
|
-
workbook whose only sheet holds no cell values is still marked and gets the
|
|
239
|
-
sheet cause. A 27-character HTML login wall, a zero-page PDF and every existing
|
|
240
|
-
thin-source test are unchanged, and the demo's three sources print byte for byte
|
|
241
|
-
what they do today. `skills/backdraft/SKILL.md`,
|
|
242
|
-
`skills/backdraft-backfill/SKILL.md`, `site/llms.txt` and `README.md` say what the
|
|
243
|
-
mark means for a table. DESIGN row.
|
|
244
|
-
|
|
245
|
-
**Size.** One day.
|
|
246
|
-
|
|
247
|
-
### 6. `ingest --dry-run` names the source and not the run
|
|
35
|
+
### 1. `ingest --dry-run` names the source and not the run
|
|
248
36
|
|
|
249
37
|
**Intent.** The dry run (2026-09-10) answers what a source would be called, and
|
|
250
38
|
`tests/test_dry_run.py` pins that the prediction equals the outcome — for the
|
|
@@ -292,7 +80,7 @@ row.
|
|
|
292
80
|
|
|
293
81
|
**Size.** Two days.
|
|
294
82
|
|
|
295
|
-
###
|
|
83
|
+
### 2. `session show` counts what was read and cannot say what was not
|
|
296
84
|
|
|
297
85
|
**Intent.** `session show` (2026-08-31) answers "have I read enough to write
|
|
298
86
|
this yet?" with a count per document. Since the page-read budget (2026-09-08) a
|
|
@@ -336,7 +124,7 @@ ask it after a context loss rather than re-reading from the top. DESIGN row.
|
|
|
336
124
|
|
|
337
125
|
**Size.** Two days.
|
|
338
126
|
|
|
339
|
-
###
|
|
127
|
+
### 3. An artifact word-diffs a drifted citation against whatever now stands at its old address
|
|
340
128
|
|
|
341
129
|
**Intent.** The artifact's drift block (`render/html/text.py`'s `_drift_block`)
|
|
342
130
|
shows `as cited` and `now`, word-diffed with `<del>` and `<ins>`, where `now` is
|
|
@@ -388,7 +176,7 @@ DESIGN row.
|
|
|
388
176
|
|
|
389
177
|
**Size.** Three days.
|
|
390
178
|
|
|
391
|
-
###
|
|
179
|
+
### 4. `locate` leaves the fix to hand edits of tokens, which the skill forbids
|
|
392
180
|
|
|
393
181
|
**Intent.** `locate` prints `moved: <old> — now at <new>` and closes by telling
|
|
394
182
|
the writer to put each moved token in place of the old one, while the writing
|
|
@@ -433,7 +221,7 @@ pins for the plain run. DESIGN row.
|
|
|
433
221
|
|
|
434
222
|
**Size.** Two days.
|
|
435
223
|
|
|
436
|
-
###
|
|
224
|
+
### 5. `locate` calls intact words `gone` when an insertion merged or split their chunk
|
|
437
225
|
|
|
438
226
|
**Intent.** `locate` matches the cited snippet's hash exactly, and the 2026-09-17
|
|
439
227
|
row named the price: "a chunk that an insertion under 200 characters merged into"
|
|
@@ -481,7 +269,7 @@ every existing `tests/test_locate.py` expectation is unchanged. SPEC Addendum B'
|
|
|
481
269
|
|
|
482
270
|
**Size.** Two days.
|
|
483
271
|
|
|
484
|
-
###
|
|
272
|
+
### 6. A draft outside its project is told there is no registry, about the wrong directory
|
|
485
273
|
|
|
486
274
|
**Intent.** `bind` and `locate` find the registry from the document's directory
|
|
487
275
|
(SPEC § CLI), and when that walk finds nothing `cli_context.open_registry`
|
|
@@ -520,7 +308,7 @@ must live under its project or name it with `BACKDRAFT_HOME`. DESIGN row.
|
|
|
520
308
|
|
|
521
309
|
**Size.** One day.
|
|
522
310
|
|
|
523
|
-
###
|
|
311
|
+
### 7. After a re-ingest, nothing says which drafts cite the source that moved
|
|
524
312
|
|
|
525
313
|
**Intent.** `ingest`'s new-generation note says citations "may now report
|
|
526
314
|
`drifted`", that `bind` on a document citing the source says which, and names
|
|
@@ -561,6 +349,172 @@ the note names the drafts. DESIGN row.
|
|
|
561
349
|
|
|
562
350
|
**Size.** Two days.
|
|
563
351
|
|
|
352
|
+
### 8. `render` ships a draft edited after `bind` as if the record had judged it
|
|
353
|
+
|
|
354
|
+
**Intent.** `render` reads two files, the draft and the record `bind` wrote for
|
|
355
|
+
it, and never checks that they still describe the same text. Checked on
|
|
356
|
+
2026-10-09 in a scratch project: bind a memo with one resolved citation, then add
|
|
357
|
+
a sentence citing `bd:notes:p1.c1:ffff` — a token no gate ever emitted — and
|
|
358
|
+
`backdraft render memo.md` writes the artifact at exit 0 with that claim in it
|
|
359
|
+
and not a word said. An agent's normal loop is draft, bind, fix a sentence,
|
|
360
|
+
render, so the commonest way to ship an unchecked citation is not to skip `bind`
|
|
361
|
+
but to edit after it, and the artifact — the file whose whole claim is that every
|
|
362
|
+
citation in it was checked — is the thing that carries it out.
|
|
363
|
+
|
|
364
|
+
**Shape.** `render/cli.py`, before anything is written: parse the draft with
|
|
365
|
+
`kernel.claims.parse_claims` and compare it against the record's `claims` on what
|
|
366
|
+
a claim is — its text, its tokens in order, and its `start`/`end`, since render
|
|
367
|
+
places each claim by offset and a shifted offset misplaces every highlight after
|
|
368
|
+
it. Any difference is a `UsageError` (exit 1, the code `render` already uses for a
|
|
369
|
+
record it cannot use) naming how many claims differ, the first one by offset, the
|
|
370
|
+
record's `bound_at`, and the one fix: `backdraft bind <doc>` then render again.
|
|
371
|
+
No artifact-format change — the comparison uses what the record already carries;
|
|
372
|
+
a content hash of the draft in the record is the alternative and is a format
|
|
373
|
+
change, so name it in the DESIGN row and say why it lost. Prose edits outside any
|
|
374
|
+
claim that move no offset render as today. `verify` is not touched: it checks an
|
|
375
|
+
artifact against itself, and the artifact embeds the draft it was rendered from.
|
|
376
|
+
|
|
377
|
+
**Acceptance.** The scratch repro above exits 1 with the message, and no
|
|
378
|
+
`memo.backdraft.html` is written or overwritten (assert the old file's bytes are
|
|
379
|
+
unchanged). Re-binding then rendering succeeds. An edit that changes no claim and
|
|
380
|
+
no offset (a typo fixed after the last claim) renders byte-identical to before.
|
|
381
|
+
`demo/`'s render is byte-identical. `render --help`, README's render section,
|
|
382
|
+
`site/docs.html`, `site/llms.txt` and `skills/backdraft/SKILL.md` say that render
|
|
383
|
+
refuses a draft edited since bind. DESIGN row.
|
|
384
|
+
|
|
385
|
+
**Size.** One to two days.
|
|
386
|
+
|
|
387
|
+
### 9. An older backdraft reads a newer registry as if what it does not know were not there
|
|
388
|
+
|
|
389
|
+
**Intent.** The registry travels with the project folder — the documented answer
|
|
390
|
+
to a sandbox that cannot reach the vision model is to ingest locally and hand the
|
|
391
|
+
folder over — so registries routinely meet a different backdraft than the one
|
|
392
|
+
that wrote them. The schema has grown by adding tables (`document_meta`,
|
|
393
|
+
`withdrawals`, `page_meta`) under idempotent `CREATE ... IF NOT EXISTS` DDL, and
|
|
394
|
+
nothing records which backdraft last wrote a registry. So a registry with a
|
|
395
|
+
`withdrawals` row, opened by a release from before `forget`, lists the withdrawn
|
|
396
|
+
source in `ls` and binds its citations `resolved`; the older binary cannot know
|
|
397
|
+
it is missing anything, and nothing tells it. The same will happen to every
|
|
398
|
+
table added from here on.
|
|
399
|
+
|
|
400
|
+
**Shape.** `registry/store.py`'s `Registry.open` and `schema.sql`. Give the
|
|
401
|
+
schema an integer version, stamped with `PRAGMA user_version` (SQLite's own slot,
|
|
402
|
+
no table needed), bumped by any change that adds meaning an older reader would
|
|
403
|
+
silently drop. `open` reads it after the DDL: a registry stamped lower is stamped
|
|
404
|
+
up (the DDL already made it current); one stamped higher than this build knows
|
|
405
|
+
raises `RegistryError` naming both numbers and the fix (upgrade backdraft), at
|
|
406
|
+
exit 1 through the guard, before anything is written. Releases from before the
|
|
407
|
+
stamp cannot be fixed by it, which the DESIGN row must say plainly: the guard
|
|
408
|
+
protects every pair of versions from this one on. `doctor`'s registry line shows
|
|
409
|
+
the schema version; `export` carries it. SPEC Addendum A and `spec/registry.md`
|
|
410
|
+
name the stamp, since another implementation opening the same file must honour
|
|
411
|
+
it.
|
|
412
|
+
|
|
413
|
+
**Acceptance.** A registry created by this build carries the current
|
|
414
|
+
`user_version`. An existing registry with `user_version` 0 opens and is stamped,
|
|
415
|
+
with every existing test unchanged. A registry whose `user_version` is set one
|
|
416
|
+
higher refuses every verb with the message and exits 1, and `doctor` reports it
|
|
417
|
+
as the registry's gap at exit 0; assert the file's bytes are unchanged after both.
|
|
418
|
+
DESIGN row.
|
|
419
|
+
|
|
420
|
+
**Size.** One to two days.
|
|
421
|
+
|
|
422
|
+
### 10. `ingest` reports per source in columns an agent has to scrape
|
|
423
|
+
|
|
424
|
+
**Intent.** `bind` and `verify` gained `--json` (2026-09-14) because their output
|
|
425
|
+
is read by agents and every wording fix silently broke a parser. `ingest` is the
|
|
426
|
+
other command an agent reads closely and acts on, and its line is two-space
|
|
427
|
+
separated fields whose name column is a URL or a filename that may hold spaces,
|
|
428
|
+
followed by marks that now come in five kinds — `unchanged`, `new generation`,
|
|
429
|
+
`restored`, `--slug not applied`, and the `!` failure lines after a count — with
|
|
430
|
+
the notes that explain them in prose below. An agent ingesting a folder has to
|
|
431
|
+
pull the slug of each source out of that to cite it, and tell a thin source, a
|
|
432
|
+
failed one and a renamed-refused one apart by matching sentences this repo
|
|
433
|
+
rewords most weeks.
|
|
434
|
+
|
|
435
|
+
**Shape.** `--json` on `ingest` in `cli.py`, printing one object instead of the
|
|
436
|
+
report, with the exit codes exactly where they are. Per source: `source` as
|
|
437
|
+
typed, `slug`, `name` (`source_name`), `media_type`, `pages`, `chars`, `outcome`
|
|
438
|
+
(`Ingested.outcome`'s values), `restored`, `slug_applied` (null when no `--slug`
|
|
439
|
+
was given), `thin` (bool, by `gate.THIN_SOURCE_CHARS`), `snapshots` (captured,
|
|
440
|
+
not captured with the reason, not applicable); failures as `{source, reason}` with
|
|
441
|
+
the reason string the `!` line prints. Run-level: the notes as an array of the
|
|
442
|
+
same strings the report prints, so one owner per wording still holds. Built from
|
|
443
|
+
the same values the report is built from — collect first, then present twice,
|
|
444
|
+
the 2026-09-14 shape — not a second walk. `--dry-run --json` is the same object
|
|
445
|
+
with `outcome: "would-ingest"` or the dry run's own marks, or is refused; decide
|
|
446
|
+
and say which in the row.
|
|
447
|
+
|
|
448
|
+
**Acceptance.** A test ingests a fresh file, an unchanged one, a changed one, a
|
|
449
|
+
withdrawn one and a missing path in one `--json` run and asserts each object's
|
|
450
|
+
fields and the exit code (1, for the missing path). The report without `--json`
|
|
451
|
+
is byte-identical, pinned. `site/llms.txt`, `skills/backdraft/SKILL.md`,
|
|
452
|
+
`site/docs.html` and SPEC § CLI name the flag and its object. DESIGN row.
|
|
453
|
+
|
|
454
|
+
**Size.** Two days.
|
|
455
|
+
|
|
456
|
+
### 11. `forget` asks for confirmation without saying which drafts it will break
|
|
457
|
+
|
|
458
|
+
**Intent.** `forget` is the one command that takes something away, so it asks
|
|
459
|
+
first, and what it says is general: a token already written into a draft still
|
|
460
|
+
shows its receipt, and `bind` will report it `unresolved` naming the withdrawal.
|
|
461
|
+
Whether any draft in this project cites the source — the one fact that decides
|
|
462
|
+
whether forgetting it is tidying or breaking a memo the user is about to send —
|
|
463
|
+
is not said, though every rooted bind wrote its record under
|
|
464
|
+
`.backdraft/records/` listing every token it cited. An agent removing what looks
|
|
465
|
+
like a duplicate source cannot tell from the prompt that the memo it bound
|
|
466
|
+
yesterday cites only the duplicate.
|
|
467
|
+
|
|
468
|
+
**Shape.** Depends on the records reader in `registry/` that "After a re-ingest,
|
|
469
|
+
nothing says which drafts cite the source that moved" adds (the walk over
|
|
470
|
+
`record_path`'s layout that maps each record back to a draft that still exists
|
|
471
|
+
and skips a malformed record); if that item has not landed, land that reader here and
|
|
472
|
+
let that item reuse it. `forget`'s description before the prompt names each draft whose
|
|
473
|
+
record cites the slug and how many citations each would turn `unresolved`, a
|
|
474
|
+
count past a handful, and nothing new when none does. `--yes` prints the same
|
|
475
|
+
lines before withdrawing, since an agent passing `--yes` is the caller most in
|
|
476
|
+
need of them in the output it reads afterwards. Display only: the prompt's
|
|
477
|
+
question, the exit codes and the withdrawal itself do not move. The records
|
|
478
|
+
reflect the last bind, not the draft's current text, and the wording must say
|
|
479
|
+
"as last bound".
|
|
480
|
+
|
|
481
|
+
**Acceptance.** In a scratch project with two bound memos, one citing `notes`
|
|
482
|
+
and one not, `backdraft forget notes --yes` names the first memo and its citation
|
|
483
|
+
count and not the second. A registry with no records prints exactly today's
|
|
484
|
+
output, pinned. A record file holding invalid JSON does not stop the forget. The
|
|
485
|
+
`forget` row in `site/docs.html` and the forget paragraph in README say it names
|
|
486
|
+
the drafts.
|
|
487
|
+
|
|
488
|
+
**Size.** One day, two if it lands the records reader.
|
|
489
|
+
|
|
490
|
+
### 12. `doctor` is read by agents and prints only columns
|
|
491
|
+
|
|
492
|
+
**Intent.** The writing skill's first instruction is now "Run `backdraft doctor`
|
|
493
|
+
before you promise anything" and plan from its `missing` lines. An agent does
|
|
494
|
+
that by scraping aligned columns whose third field is a sentence, whose
|
|
495
|
+
continuation lines are told apart by leading spaces, and whose summary is a
|
|
496
|
+
bracketed line — the shape `bind --json` and `verify --json` (2026-09-14) were
|
|
497
|
+
added to stop agents depending on. The capability set will grow; each new line
|
|
498
|
+
moves the column widths.
|
|
499
|
+
|
|
500
|
+
**Shape.** `--json` on `doctor` in `cli.py`, printing one object from the same
|
|
501
|
+
`_Capability` list the report is built from: per capability `name`, `ready`
|
|
502
|
+
(bool), `gap` (the verb's own string, or null), `costs` (null when ready),
|
|
503
|
+
`ready_detail`, and `key` as `{name, set}` where the capability spends one —
|
|
504
|
+
never a value. The registry's entry carries the root and document count as
|
|
505
|
+
fields. Exit 0 always, as today. Document the object in SPEC § CLI beside
|
|
506
|
+
`doctor`'s paragraph; it is not part of the artifact format and does not go in
|
|
507
|
+
`spec/`.
|
|
508
|
+
|
|
509
|
+
**Acceptance.** A test runs `doctor --json` with everything missing and with
|
|
510
|
+
everything ready (the existing fixtures), parses it, and asserts every field; a
|
|
511
|
+
test with a fake key set asserts its value appears nowhere in the output. The
|
|
512
|
+
report without `--json` is byte-identical, pinned. `skills/backdraft/SKILL.md`
|
|
513
|
+
tells an agent to read `doctor --json`; `site/llms.txt` and `site/docs.html`
|
|
514
|
+
name the flag.
|
|
515
|
+
|
|
516
|
+
**Size.** One day.
|
|
517
|
+
|
|
564
518
|
## Parked
|
|
565
519
|
|
|
566
520
|
Deliberately not queued, each with the reason, so picking one up starts from
|