backdraft 0.8.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {backdraft-0.8.0 → backdraft-0.9.0}/.claude-plugin/plugin.json +1 -1
- {backdraft-0.8.0 → backdraft-0.9.0}/DESIGN.md +5 -1
- {backdraft-0.8.0 → backdraft-0.9.0}/PKG-INFO +37 -4
- {backdraft-0.8.0 → backdraft-0.9.0}/README.md +34 -3
- backdraft-0.9.0/ROADMAP.md +393 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/SPEC.md +26 -10
- {backdraft-0.8.0 → backdraft-0.9.0}/demo/memo.backdraft.html +11 -5
- {backdraft-0.8.0 → backdraft-0.9.0}/demo/walkthrough.md +101 -8
- {backdraft-0.8.0 → backdraft-0.9.0}/pyproject.toml +4 -1
- {backdraft-0.8.0 → backdraft-0.9.0}/site/demo.html +11 -5
- {backdraft-0.8.0 → backdraft-0.9.0}/site/docs.html +24 -10
- {backdraft-0.8.0 → backdraft-0.9.0}/site/llms.txt +35 -4
- {backdraft-0.8.0 → backdraft-0.9.0}/skills/backdraft/SKILL.md +31 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/skills/backdraft-artifact/SKILL.md +89 -52
- {backdraft-0.8.0 → backdraft-0.9.0}/skills/backdraft-backfill/SKILL.md +4 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/spec/artifact.md +24 -6
- backdraft-0.9.0/spec/registry.md +332 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/bind/binder.py +18 -24
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/bind/cli.py +66 -19
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/cli.py +7 -1
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/cli_context.py +42 -3
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/gate/cli.py +4 -1
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/gate/searcher.py +27 -4
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/kernel/artifact.py +27 -7
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/registry/__init__.py +2 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/registry/store.py +111 -15
- backdraft-0.9.0/src/backdraft/render/cli.py +468 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/html/assets.py +5 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/html/components.py +18 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/html/page.py +17 -7
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/markdown.py +41 -9
- backdraft-0.9.0/src/backdraft/render/math.py +145 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/sidecar.py +55 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/gate/fake_registry.py +8 -3
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/gate/test_gate_searcher.py +81 -1
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/render/backfill.sidecar.json +5 -4
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/render/demo.sidecar.json +5 -4
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_bind_cli.py +129 -1
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_gate_integration.py +20 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_registry.py +34 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_render_cli.py +60 -1
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_render_markdown.py +33 -0
- backdraft-0.9.0/tests/test_render_math.py +217 -0
- backdraft-0.9.0/tests/test_spec_registry.py +324 -0
- backdraft-0.9.0/tests/test_verify_cli.py +564 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/uv.lock +15 -2
- backdraft-0.8.0/ROADMAP.md +0 -340
- backdraft-0.8.0/src/backdraft/render/cli.py +0 -184
- {backdraft-0.8.0 → backdraft-0.9.0}/.claude-plugin/marketplace.json +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/.github/workflows/ci.yml +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/.github/workflows/release.yml +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/.github/workflows/site.yml +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/.gitignore +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/.python-version +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/LICENSE +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/demo/generate_sources.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/demo/memo.md +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/demo/sources/t12-summary.pdf +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/site/index.html +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/site/privacy.html +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/spec/chunking.md +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/spec/tokens.md +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/__init__.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/__main__.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/bind/evidence.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/bind/verify/entail.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/credentials.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/__init__.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/base.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/csv.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/docx.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/html.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/image.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/pdf_text.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/pptx.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/sheet.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/snapshots.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/vlm.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/vlm_settings.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/xls.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/extract/xlsx.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/fetch.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/gate/__init__.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/gate/reader.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/kernel/chunking.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/kernel/model.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/registry/ledger.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/registry/schema.sql +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/__init__.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/_text.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/footnotes.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/html/__init__.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/html/fmt.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/html/fmt_vectors.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/html/text.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/theme.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/themes/default.toml +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/themes/press.toml +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/src/backdraft/render/themes/slate.toml +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/conftest.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/conftest_registry.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/conftest_render.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/css_util.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/fakes.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/gate/test_gate_cli.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/gate/test_gate_ledger.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/gate/test_gate_reader.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/gate/test_reader_show.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/golden_util.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_bind_evidence.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_card_sizing.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_chunking.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_claims.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_cli.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_credentials.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_docs.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_extract.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_extract_html.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_fetch.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_fmt_parity.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_footnotes.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_hashing.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_html.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_ingest_url.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_invariants.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_model.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_placement.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_records_location.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_reingest.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_snapshot_settings.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_snapshots.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_theme.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_tokens.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_value_trace.py +0 -0
- {backdraft-0.8.0 → backdraft-0.9.0}/tests/test_version.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backdraft",
|
|
3
3
|
"displayName": "Backdraft",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"description": "Drop-in provenance for factual claims: one click from any claim to the evidence behind it. Ships the backdraft writing, backfill, and artifact-reading skills; the CLI installs separately (uv tool install backdraft).",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Spencer Braun",
|
|
@@ -29,7 +29,7 @@ Four verbs:
|
|
|
29
29
|
- **`ingest`** — deterministically anchor source documents (PDF/xlsx/md/…) into the registry: extract, locate, snapshot snippets, hash.
|
|
30
30
|
- **`read` / `search`** — the gate. Emit token-marked context for a model. Search results are themselves mintable evidence (search that returns nothing citable forces a page read purely to obtain an anchor).
|
|
31
31
|
- **`bind`** — postprocess a written document: resolve tokens, run enabled verification switches, backfill surface forms (doc names, pages, quotes), emit sidecar claims file + binding report.
|
|
32
|
-
- **`render`** — produce the clickable artifact.
|
|
32
|
+
- **`render`** — produce the clickable artifact. Its reader half is `verify`: an artifact carries the checks it wants run in its own legend, so the tool that writes them ships the command that runs them.
|
|
33
33
|
|
|
34
34
|
### 3. Substrates (sequenced)
|
|
35
35
|
1. **Skill + CLI, together** (first release). The CLI is the system; the skill is ~a page instructing an agent to use it. Ships both the agent-native wedge and the zero-integration backfill demo.
|
|
@@ -147,6 +147,10 @@ Self-contained HTML (plus the machine-readable sidecar): **quotes + receipts + e
|
|
|
147
147
|
| 2026-08-18 | **One rule names a source, and every surface that names one uses it.** A fetched page's `filename` is the temporary file `fetch.filename_for` staged the bytes in — `index.html` for a permanent link, a name on nobody's disk — and the 2026-08-06 row settled what to do about it ("showing both would give a reader two names for one thing and let the fictional one look authoritative") for the artifact alone. The four CLI surfaces that name a document kept the fiction: `ingest`'s line, the gate's document list, its table-of-contents headline, and `ls` — which showed *both*, filename in column two and URL in a fifth field, the exact shape that row rejected. `gate.source_name` is now the single owner, exported the way `gate.unit` is and imported downward by `cli.py`, so a writing agent learns a source's provenance from the surface it already lives on instead of being sent to `ls` by the skill. Display only: the slug, the token and everything a citation resolves through are untouched, which is the 2026-08-05 "provenance, never identity" rule. Two consequences named rather than discovered. `ls` loses a field — a fetched row is four columns, not five — so anything parsing column five stops seeing one; that is the point, since column two was a lie and one true name beats two names of which one is fictional. And in the gate's aligned list a URL *overflows* the name column rather than widening it: the column is sized on filenames alone, so one 80-character address pushes its own row's later columns right instead of pushing every row's past column 100. The cost is a ragged right on fetched rows, accepted because the file rows a mostly-file registry is made of stay byte-identical to what they printed before — pinned by a test, the way the artifact's byte-identity is. `bind`'s unmatched-claim report still names a source by filename: the fix cannot be shared into it without a sideways `bind` → `gate` import, which SPEC § Dependency rule forbids, and duplicating the rule there is the reinvention this row exists to end. |
|
|
148
148
|
| 2026-08-19 | **A slug is assigned by a rule, and the rule is written down.** The Open list carried "slug assignment/collision rules" from the first field trial, and URL sources sharpened it into a live failure: `fetch.filename_for` took the stem from a URL's last path segment, so `https://a.example/reports/2025/index.html` and `https://b.example/docs/index.html` both staged as `index.html` and became `index` and `index-2` — two handles that say which is which to nobody, carried by every token in every document written against them, permanently, since a slug is stable once assigned. The rule now has two clauses and they are stated together for files and URLs. A **file** is named by its filename's stem, kebabed and cut to 32 characters, which is unchanged and was never the problem. A **URL** is named by its last path segment, *except* when that segment names the site's plumbing rather than its page — a directory index or generic handler (`_GENERIC_STEMS`: `index`, `view`, `page`, `show`, and a dozen more), an all-digit id, or no segment at all — in which case the host goes in front of it, minus `www.` and minus the port, so the two pages above become `a-example-index` and `b-example-index`. The common case is deliberately untouched: `https://example.com/reports/q4-2025` is still `q4-2025`, because a rule that made every good name uglier to fix the bad ones would be the wrong trade. `_dedupe`'s `-2` stays, demoted from the normal outcome to the backstop it was meant to be — the host is a *better* stem, not a unique one, and two `index.html` pages on one site still collide. Truncation is trimmed on both cuts now (`slug_for` and `_dedupe`), which is the other half of the Open line: a 32-character cut lands on a hyphen often enough that `q4-report-` was a slug this repo could produce, legal in the grammar and reading like a bug. The compatibility statement, said out loud because nothing else will say it: two registries built on either side of this change disagree about a fetched page's slug, and an artifact's tokens are not portable between them. Existing registries are unaffected, and that is not luck — `_find_document` matches a re-fetch on the URL, never the slug, so re-ingesting a page keeps the slug it already has. `--slug` remains the way to overrule all of this and is still the advice for a fetched page worth citing, because `a-example-index` names a site rather than nothing, which is better and is not good. |
|
|
149
149
|
| 2026-08-20 | **`ingest` says which of its three outcomes happened, and how much text came out.** One line served three different events — a document created, a *new generation* of one whose bytes moved, and a no-op that wrote nothing — so an agent re-ingesting after a fix could not tell whether anything had changed, at exactly the moment it most needed to: a new generation is when citations into the previous snapshot begin resolving to a superseded one and `bind` starts calling them `drifted`. The registry already distinguished all three internally (`_find_document`, `_is_noop`, `_upsert_document`) and threw the answer away, so `ingest` now returns an `Ingested` — a `Document` subclass carrying `outcome`, the shape `SearchResults` established for `search`: Addendum A's pinned `-> Document` still holds, every `.slug` caller and every fake is untouched, and the one consumer that cares reads the extra field. Re-deriving the outcome in the CLI was the alternative and would have meant a second implementation of `_is_noop` living away from the first. The wart is named rather than papered over: a dataclass compares by class, so an `Ingested` never equals the plain `Document` the read side returns for the same row, and the two tests that compared them now compare what persists. The second half is a number: the extracted character count, beside the page count on every line. A scanned PDF with no text layer and a page behind a login both ingest cleanly, exit 0 and print a page count like any success, which is how a shell of a source gets cited — and the writing skill's instruction to notice "thin or empty" gave the agent nothing to notice it with. Under `THIN_SOURCE_CHARS` (200) a note names the likely cause by media type and what to do about it. That threshold is a heuristic, which this repo refuses elsewhere on purpose — the `html` extractor is a parse and not a readability guess, because a heuristic that changes its mind moves anchors. This one is admissible for the reason those are not: it is display only, at exit 0, and no token, anchor, status or byte of the artifact derives from it, so being wrong costs one line of advice and never a citation. `chars` counts sheets too, where `read`'s window unit is rows: this is the extraction's volume, not a window into it. Grouping follows the 2026-08-13 rule — thin sources group by cause, since one missing text layer explains every scan at once and each document's own count is already on its own line, while the failure report still lists by source. |
|
|
150
|
+
| 2026-08-24 | **`backdraft verify` — the recipient's check, and the drift bug it found in the spec.** The artifact's whole promise is that someone holding only the file can check it, and until now the instructions for doing so were prose: `skills/backdraft-artifact/SKILL.md` walked an agent through recomputing hashes by hand, which is a deterministic procedure re-implemented per reading — the way a recipient's audit ends up weaker than the producer's. It is now a command, in `render/cli.py` because that module already holds the format's reader half (`render.sidecar`), taking either the `.backdraft.html` or the `.backdraft.json`. Two tiers, and the output names which ran: the record against itself, which needs nothing but the file, and — only where a `.backdraft/` is discoverable — every token re-resolved against the sources. Discovery is from **cwd**, not from the artifact's own directory as `bind` does it from the document's: an artifact is a file people forward, so the folder it landed in is evidence about the recipient's machine and not about which registry produced it. Exit 2 joins `bind`'s, which was documented as "`bind`'s alone"; the shared meaning is "the run completed and the document is not clean", which is exactly what a Stop hook gates on, so a hook written for one now catches the other — the gate's `show` stays at 1, because a lookup answering "that token names nothing" is not a document failing. Read-only and minting nothing, which is the line between it and `show`: showing is minting, and an audit that made its own subject citable would manufacture the evidence it was sent to inspect. A record faithfully carrying an `unresolved` citation still passes tier one — a kept failure is the record working, so the same bytes exit 0 in an inbox and 2 in the project, answering different questions. Two consolidations came with it rather than a third copy of each: `registry.citation_for` is the token-to-status walk `bind` and `verify` both need and the dependency rule forbids either importing from the other, so it sits beside `current_at` for the same reason, with `not_shown` deliberately left out as a fact about a session the caller holds; and `cli_context.claim_words` is the line-item shape, since two commands reporting on the same claims must not print two layouts. The real find was in the spec. `spec/artifact.md` § Checking an artifact and the legend's `verify_this_record` both said the token's hash must prefix `anchor.snippet_sha256` — false for every `drifted` citation by construction, since the token names `drifted_from` (what the author cited) while `anchor` carries what stands there now, and the two differing *is* what drift means. Implemented as written, `verify` would have called every drifted artifact forged: the one status the format exists to carry honestly. Both statements are corrected, which moves the legend and therefore every artifact's bytes — goldens and the demo regenerated — and is worth the churn, because a known-wrong normative instruction shipped inside every artifact is a worse cost than a diff. |
|
|
151
|
+
| 2026-08-24 | **Math is rendered, not forbidden, and MathML is why that is affordable.** An agent writing a memo about coverage writes `$\mathrm{DSCR} = \frac{NOI}{D}$`, and the renderer handed the reader raw TeX — or worse, corrupted TeX, since `$x_1$ and $x_2$` paired its subscripts into emphasis and `\(a^2\)` lost its delimiters to the backslash-escape pass. The alternative on the table was to forbid math in the writing skill, and it was rejected twice over: this product's documents are exactly the finance and science memos that contain formulas, and a skill instruction cannot be enforced against an agent that did not read it carefully, so the corruption would have arrived anyway. Shipping *unrendered* TeX was rejected too — nobody wants to read `\frac{NOI}{D}`, and a document a reader cannot read is not a lesser artifact but a broken one. The target is MathML via `latex2mathml`, behind a `[math]` extra: it is static markup the browser lays out itself, so rules 1 and 2 of § The HTML artifact hold *by construction* rather than by care — no script, no font file, no request — where inlining KaTeX would have cost roughly a megabyte of fonts as `data:` URIs on every artifact, math or not. The tradeoff, stated because it is the one place the format's fixed-at-render promise is soft: MathML leans on whatever math font the reader's system has, so two readers of one artifact may see different glyph quality. That is a smaller and more honest cost than the alternative, and it is bounded — the markup, the spacing, and every character are identical; only the typeface is not. Dollars were the real hazard and the guard is Pandoc's: a memo saying `$250 per unit` and `$1.2M` twice in a paragraph would pair them into nonsense under a naive rule, so an opening `$` must be followed by a non-space and a closing `$` must be preceded by one and not followed by a digit — a currency amount is preceded by a space and therefore can never close a span. Code is skipped rather than scanned, so a shell snippet's `$PATH` and `$HOME` are safe, and the demo regenerates with zero math elements, which is the check that matters. Math is opt-in per call and **off for evidence**: a receipt's snippet must read as it was extracted, not as this renderer would prefer it. Conversion failure is data, at two different reliabilities — `latex2mathml` raises for some malformed input, which surfaces as a wavy mark at the formula and a note giving the reason, and silently degrades for the rest (`\notacommand{x}` becomes an identifier), which cannot be caught and is recorded here as a known limit rather than papered over. |
|
|
152
|
+
| 2026-08-25 | **The registry export is a specified format, or it is not a format.** `backdraft export` has written `"$format": "backdraft/registry-v1"` since v0 — a version string promising a specification that did not exist — while being the only complete, portable form a registry has, and therefore what a second implementation, a migration or an audit actually reads. The gap stopped being theoretical when URL sources added a conditional `meta` key to every exported document: a change to a format with nothing to change against. So spec/registry.md joins tokens, chunking and artifact as normative prose, and the pin is the spec itself rather than a golden file beside it — tests/test_spec_registry.py parses the spec's key tables, exports a registry built to exercise every branch, and fails in both directions, so an undocumented key and a documented-but-absent one are equally a test failure. Two boundaries were drawn rather than blurred. `bindings[].report` stays spec/artifact.md's, quoted opaquely here, because one format with two specifications is one too many and they would drift. And the export is content, not a backup: page images, sheet styling and the FTS index are deliberately outside it, so an export round-trips every citation and every receipt and does not round-trip a database — stated in the spec as a limit rather than left for a reader to discover by restoring one. |
|
|
153
|
+
| 2026-08-26 | **`search` reports the size of the answer, not the size of the page.** `--limit` defaults to 20 and the count line said `20 results` whether twenty matched or two hundred did, which is the silent cap this repo rejects everywhere else — `read` closes with `[Showing 0-464 of 3115 chars. Continue with: …]` for exactly this reason. Search is the surface an agent leans on hardest, since it is how a claim finds its evidence, and relevance order is not the same as the sentence the writer needed: a reader told it has twenty results picks the best of twenty and never learns the twenty-first existed. So `SearchResults` gains `total` beside `phrase_fallback` — the same shape for the same reason, an out-of-band fact riding on Addendum A's pinned `list[SearchHit]` so every consumer and every fake is untouched — and a capped run renders `2 of 56 results` plus a closing line naming the exact widening command, shell-quoted, because a hint carrying `$` or a space that has to be repaired before it runs is not a hint. The count is a `COUNT(*)` over the *same* joins the fetch runs, shared through `_match_sql` rather than written twice: a count that joined differently would report a total the caller can never reach. It is asked for only when the page came back full, since a page the limit did not fill is already the whole answer — so the common uncapped run still runs one query, and the output it prints is byte-identical to what it printed before, pinned as a test the way the artifact's bytes are. Two things were deliberately not built. No `--offset`: paging a relevance-ordered list is a different design question, and `--limit` already exists for the hint to name. And the total is not carried into anything durable — no token, anchor, receipt or artifact byte moves, because how many other sentences also matched is a fact about a query, not about a citation. |
|
|
150
154
|
|
|
151
155
|
## Open
|
|
152
156
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: backdraft
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Drop-in provenance for factual claims: one click from any claim to the evidence behind it.
|
|
5
5
|
Project-URL: Homepage, https://backdraft.dev
|
|
6
6
|
Project-URL: Documentation, https://backdraft.dev/docs.html
|
|
@@ -18,6 +18,8 @@ Requires-Dist: python-pptx>=1.0
|
|
|
18
18
|
Requires-Dist: typer>=0.12
|
|
19
19
|
Provides-Extra: entail
|
|
20
20
|
Requires-Dist: anthropic>=0.40; extra == 'entail'
|
|
21
|
+
Provides-Extra: math
|
|
22
|
+
Requires-Dist: latex2mathml>=3.77; extra == 'math'
|
|
21
23
|
Provides-Extra: vlm
|
|
22
24
|
Provides-Extra: xls
|
|
23
25
|
Requires-Dist: python-calamine>=0.3; extra == 'xls'
|
|
@@ -128,6 +130,11 @@ $ backdraft search "24850000"
|
|
|
128
130
|
[Read the page: backdraft read underwriting-model p2]
|
|
129
131
|
```
|
|
130
132
|
|
|
133
|
+
Search shows twenty results by default. A run that hit the limit says so —
|
|
134
|
+
`20 of 56 results` — and closes with the command that shows the rest, because
|
|
135
|
+
"20 results" over an answer of 56 is how a reader concludes it has seen
|
|
136
|
+
everything when it has seen a third.
|
|
137
|
+
|
|
131
138
|
A cell you can already see mints directly:
|
|
132
139
|
|
|
133
140
|
```console
|
|
@@ -170,6 +177,7 @@ bound 17 claim(s), 18 citation(s) [frontwalk]
|
|
|
170
177
|
resolved: 17
|
|
171
178
|
unresolved: 1
|
|
172
179
|
overlap: pass 13, skip 4
|
|
180
|
+
skip 4 — wording overlap does not apply to a single cell
|
|
173
181
|
value-trace: pass 17
|
|
174
182
|
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @3629
|
|
175
183
|
wrote .backdraft/records/memo.backdraft.json
|
|
@@ -193,6 +201,29 @@ memo.backdraft.html
|
|
|
193
201
|
That file is the whole deliverable: send it over email or Slack and the
|
|
194
202
|
recipient gets the full experience by double-clicking it.
|
|
195
203
|
|
|
204
|
+
**The recipient can check it.** `backdraft verify` takes the artifact or its
|
|
205
|
+
sidecar and does the checking that the file is built to make possible: every
|
|
206
|
+
snippet rehashed against the sha256 recorded beside it, every token checked
|
|
207
|
+
against the anchor it names, `summary` recounted from the claims.
|
|
208
|
+
|
|
209
|
+
```console
|
|
210
|
+
$ backdraft verify memo.backdraft.html
|
|
211
|
+
checked memo.backdraft.html [backdraft/artifact-v1]
|
|
212
|
+
receipts: 17 of 17 hold
|
|
213
|
+
record: 17 claim(s), 18 citation(s); the summary recount agrees
|
|
214
|
+
recorded: resolved 17, unresolved 1
|
|
215
|
+
sources: no .backdraft/ found from here — not re-checked
|
|
216
|
+
[Re-check against the sources: run this inside the project it was bound in.]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
That first tier needs nothing but the file, which is the point — it is the check
|
|
220
|
+
someone who was emailed the artifact can still make. Run it inside the project
|
|
221
|
+
the document was bound in and it also re-resolves every citation against the
|
|
222
|
+
registry, so a source that has changed since shows up as `drifted` rather than
|
|
223
|
+
going unnoticed. Exit 0 when everything it checked passed, 2 when something did
|
|
224
|
+
not, so a hook can gate on it. It opens no session and mints nothing: an audit
|
|
225
|
+
must not make its subject citable.
|
|
226
|
+
|
|
196
227
|
**Theming.** The artifact ships with a default look and two alternates,
|
|
197
228
|
`press` and `slate`:
|
|
198
229
|
|
|
@@ -320,12 +351,13 @@ the keyless text layer and says so — still storing each page's image, which it
|
|
|
320
351
|
renders locally. PDF page rendering needs poppler, installed separately; without
|
|
321
352
|
it ingest succeeds without the images and names `snapshot-pages` as the backfill.
|
|
322
353
|
|
|
323
|
-
|
|
354
|
+
Three extras:
|
|
324
355
|
|
|
325
356
|
| Extra | Adds | For |
|
|
326
357
|
|---|---|---|
|
|
327
358
|
| `[entail]` | `anthropic` | `bind --check entail`, the model-judge verifier |
|
|
328
359
|
| `[xls]` | `python-calamine` | legacy `.xls` workbooks, values only, rendered the same shape as xlsx |
|
|
360
|
+
| `[math]` | `latex2mathml` | LaTeX in a document becomes MathML in the artifact; without it, formulas render verbatim and `render` says so |
|
|
329
361
|
|
|
330
362
|
`backdraft[vlm]` still installs: it is an empty compat alias from when the
|
|
331
363
|
vision deps were an extra.
|
|
@@ -346,7 +378,7 @@ Three agent skills ship inside the package and in this repo:
|
|
|
346
378
|
|---|---|
|
|
347
379
|
| [`backdraft`](skills/backdraft/SKILL.md) | writing a new document from sources, citing as it goes |
|
|
348
380
|
| [`backdraft-backfill`](skills/backdraft-backfill/SKILL.md) | attributing a document that already exists |
|
|
349
|
-
| [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | reading someone else's artifact cold |
|
|
381
|
+
| [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | checking and reading someone else's artifact cold |
|
|
350
382
|
|
|
351
383
|
The CLI is the system; a skill is one page telling an agent to use it. Nothing in
|
|
352
384
|
the skills is required to use backdraft by hand. How the skills reach your agent
|
|
@@ -404,9 +436,10 @@ spans are citable. In a sandboxed session run every command as
|
|
|
404
436
|
| [spec/tokens.md](spec/tokens.md) | the citation token grammar, normatively |
|
|
405
437
|
| [spec/chunking.md](spec/chunking.md) | the deterministic chunker |
|
|
406
438
|
| [spec/artifact.md](spec/artifact.md) | the artifact format, sidecar payload, legend, HTML rules |
|
|
439
|
+
| [spec/registry.md](spec/registry.md) | the registry export format `backdraft export` writes |
|
|
407
440
|
| [demo/walkthrough.md](demo/walkthrough.md) | the whole thing end to end, with real output |
|
|
408
441
|
|
|
409
|
-
The
|
|
442
|
+
The four files under `spec/` are the portable specification: another
|
|
410
443
|
implementation reads them and nothing else.
|
|
411
444
|
|
|
412
445
|
## Status
|
|
@@ -103,6 +103,11 @@ $ backdraft search "24850000"
|
|
|
103
103
|
[Read the page: backdraft read underwriting-model p2]
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
Search shows twenty results by default. A run that hit the limit says so —
|
|
107
|
+
`20 of 56 results` — and closes with the command that shows the rest, because
|
|
108
|
+
"20 results" over an answer of 56 is how a reader concludes it has seen
|
|
109
|
+
everything when it has seen a third.
|
|
110
|
+
|
|
106
111
|
A cell you can already see mints directly:
|
|
107
112
|
|
|
108
113
|
```console
|
|
@@ -145,6 +150,7 @@ bound 17 claim(s), 18 citation(s) [frontwalk]
|
|
|
145
150
|
resolved: 17
|
|
146
151
|
unresolved: 1
|
|
147
152
|
overlap: pass 13, skip 4
|
|
153
|
+
skip 4 — wording overlap does not apply to a single cell
|
|
148
154
|
value-trace: pass 17
|
|
149
155
|
! unresolved: bd:t12-summary:p4.c1:1a2b — replacement reserve of $250 per unit per year @3629
|
|
150
156
|
wrote .backdraft/records/memo.backdraft.json
|
|
@@ -168,6 +174,29 @@ memo.backdraft.html
|
|
|
168
174
|
That file is the whole deliverable: send it over email or Slack and the
|
|
169
175
|
recipient gets the full experience by double-clicking it.
|
|
170
176
|
|
|
177
|
+
**The recipient can check it.** `backdraft verify` takes the artifact or its
|
|
178
|
+
sidecar and does the checking that the file is built to make possible: every
|
|
179
|
+
snippet rehashed against the sha256 recorded beside it, every token checked
|
|
180
|
+
against the anchor it names, `summary` recounted from the claims.
|
|
181
|
+
|
|
182
|
+
```console
|
|
183
|
+
$ backdraft verify memo.backdraft.html
|
|
184
|
+
checked memo.backdraft.html [backdraft/artifact-v1]
|
|
185
|
+
receipts: 17 of 17 hold
|
|
186
|
+
record: 17 claim(s), 18 citation(s); the summary recount agrees
|
|
187
|
+
recorded: resolved 17, unresolved 1
|
|
188
|
+
sources: no .backdraft/ found from here — not re-checked
|
|
189
|
+
[Re-check against the sources: run this inside the project it was bound in.]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
That first tier needs nothing but the file, which is the point — it is the check
|
|
193
|
+
someone who was emailed the artifact can still make. Run it inside the project
|
|
194
|
+
the document was bound in and it also re-resolves every citation against the
|
|
195
|
+
registry, so a source that has changed since shows up as `drifted` rather than
|
|
196
|
+
going unnoticed. Exit 0 when everything it checked passed, 2 when something did
|
|
197
|
+
not, so a hook can gate on it. It opens no session and mints nothing: an audit
|
|
198
|
+
must not make its subject citable.
|
|
199
|
+
|
|
171
200
|
**Theming.** The artifact ships with a default look and two alternates,
|
|
172
201
|
`press` and `slate`:
|
|
173
202
|
|
|
@@ -295,12 +324,13 @@ the keyless text layer and says so — still storing each page's image, which it
|
|
|
295
324
|
renders locally. PDF page rendering needs poppler, installed separately; without
|
|
296
325
|
it ingest succeeds without the images and names `snapshot-pages` as the backfill.
|
|
297
326
|
|
|
298
|
-
|
|
327
|
+
Three extras:
|
|
299
328
|
|
|
300
329
|
| Extra | Adds | For |
|
|
301
330
|
|---|---|---|
|
|
302
331
|
| `[entail]` | `anthropic` | `bind --check entail`, the model-judge verifier |
|
|
303
332
|
| `[xls]` | `python-calamine` | legacy `.xls` workbooks, values only, rendered the same shape as xlsx |
|
|
333
|
+
| `[math]` | `latex2mathml` | LaTeX in a document becomes MathML in the artifact; without it, formulas render verbatim and `render` says so |
|
|
304
334
|
|
|
305
335
|
`backdraft[vlm]` still installs: it is an empty compat alias from when the
|
|
306
336
|
vision deps were an extra.
|
|
@@ -321,7 +351,7 @@ Three agent skills ship inside the package and in this repo:
|
|
|
321
351
|
|---|---|
|
|
322
352
|
| [`backdraft`](skills/backdraft/SKILL.md) | writing a new document from sources, citing as it goes |
|
|
323
353
|
| [`backdraft-backfill`](skills/backdraft-backfill/SKILL.md) | attributing a document that already exists |
|
|
324
|
-
| [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | reading someone else's artifact cold |
|
|
354
|
+
| [`backdraft-artifact`](skills/backdraft-artifact/SKILL.md) | checking and reading someone else's artifact cold |
|
|
325
355
|
|
|
326
356
|
The CLI is the system; a skill is one page telling an agent to use it. Nothing in
|
|
327
357
|
the skills is required to use backdraft by hand. How the skills reach your agent
|
|
@@ -379,9 +409,10 @@ spans are citable. In a sandboxed session run every command as
|
|
|
379
409
|
| [spec/tokens.md](spec/tokens.md) | the citation token grammar, normatively |
|
|
380
410
|
| [spec/chunking.md](spec/chunking.md) | the deterministic chunker |
|
|
381
411
|
| [spec/artifact.md](spec/artifact.md) | the artifact format, sidecar payload, legend, HTML rules |
|
|
412
|
+
| [spec/registry.md](spec/registry.md) | the registry export format `backdraft export` writes |
|
|
382
413
|
| [demo/walkthrough.md](demo/walkthrough.md) | the whole thing end to end, with real output |
|
|
383
414
|
|
|
384
|
-
The
|
|
415
|
+
The four files under `spec/` are the portable specification: another
|
|
385
416
|
implementation reads them and nothing else.
|
|
386
417
|
|
|
387
418
|
## Status
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
Sequenced intent. DESIGN.md records what was *decided*; this file records what
|
|
4
|
+
is *queued*, so tabling something is an act of writing it down here rather than
|
|
5
|
+
losing it. Items graduate by getting built and deleted.
|
|
6
|
+
|
|
7
|
+
## How the queue runs
|
|
8
|
+
|
|
9
|
+
The Now section is an ordered queue written for an implementing agent with no
|
|
10
|
+
session context: each item carries its intent, its shape, and its acceptance
|
|
11
|
+
test. The working agreement is one item at a time, top first, shipped complete
|
|
12
|
+
— code, tests, docs, and a DESIGN.md decision row where the item calls for one
|
|
13
|
+
— then deleted from this file in the same commit. Releases cut Friday, so an
|
|
14
|
+
item that lands mid-week waits on PyPI until the release; nothing ships
|
|
15
|
+
half-done to make a release. An item that turns out bigger than its sizing
|
|
16
|
+
gets split here, not stretched silently.
|
|
17
|
+
|
|
18
|
+
The cadence is daily and the runner is an agent: one item each morning Monday
|
|
19
|
+
through Thursday, landing as a direct commit to `main` — this repo does not
|
|
20
|
+
use pull requests, so a week of commits is the only review surface there is.
|
|
21
|
+
|
|
22
|
+
Friday builds nothing new. It reads that week's diff instead — duplication,
|
|
23
|
+
drift from the repo's own idioms, branches that landed untested, help text and
|
|
24
|
+
error messages that confuse, commands the docs describe wrongly — and lands
|
|
25
|
+
the fixes. Then the release, and only if something landed since the last tag:
|
|
26
|
+
the version moves in both `pyproject.toml` and `.claude-plugin/plugin.json`,
|
|
27
|
+
and publishing the GitHub release is what ships to PyPI. Then this section is
|
|
28
|
+
refilled with five items for the week ahead, drawn against the core — the four
|
|
29
|
+
verbs, the registry, the grammar, the artifact — and against where an agent
|
|
30
|
+
using backdraft actually stumbles. What the maintenance pass just found is the
|
|
31
|
+
best evidence available for what those five should be.
|
|
32
|
+
|
|
33
|
+
## Now
|
|
34
|
+
|
|
35
|
+
### 1. `session show` says what the ledger holds
|
|
36
|
+
|
|
37
|
+
**Intent.** The ledger is the mechanism the design rests on — the set of citable
|
|
38
|
+
tokens is exactly the set the gate emitted — and nothing reads it back.
|
|
39
|
+
`backdraft session show` prints `session default (from default)` and stops, so
|
|
40
|
+
an agent cannot answer "have I read enough to write this yet?" except by binding
|
|
41
|
+
and reading the `not_shown` count, which means finding out after the draft
|
|
42
|
+
exists. Worse, the default session is documented as stable across invocations
|
|
43
|
+
*so reads accumulate*, which means an agent that never exported
|
|
44
|
+
`BACKDRAFT_SESSION` has its `not_shown` judged against everything ever read in
|
|
45
|
+
that registry by any previous run on any document — a real weakening of the
|
|
46
|
+
system's strongest check, currently invisible at every surface. `skills/
|
|
47
|
+
backdraft/SKILL.md` tells the agent to start a named session and calls it
|
|
48
|
+
optional; nothing shows it what optional costs.
|
|
49
|
+
|
|
50
|
+
**Shape.** `gate/cli.py`'s `session show` and `gate/reader.py` (the ledger is
|
|
51
|
+
the gate's, and `session start` already lives there). Report what the session
|
|
52
|
+
holds: how many anchors, across how many documents, and per document a slug and
|
|
53
|
+
a count — read off the `shown` rows the registry already keeps, which may need
|
|
54
|
+
one accessor on `Registry` beside `was_shown` (SPEC Addendum A, so state it
|
|
55
|
+
there). When the session is the default one, say plainly that it accumulates
|
|
56
|
+
across runs and that a named session is what makes `not_shown` mean "this
|
|
57
|
+
draft's author saw it" — the same voice the `--config` and poppler notes use, a
|
|
58
|
+
note at exit 0 and never a failure. An empty session says so and names
|
|
59
|
+
`backdraft read` rather than printing a bare zero.
|
|
60
|
+
|
|
61
|
+
**Acceptance.** After reading two pages of one document and searching another,
|
|
62
|
+
`backdraft session show` names both documents with their counts and a total; on
|
|
63
|
+
a fresh registry it says the session is empty and what to run. Running under
|
|
64
|
+
`BACKDRAFT_SESSION=s-x` shows `s-x` and no accumulation note; running with
|
|
65
|
+
nothing set shows the default and the note. `README.md`, `site/docs.html`,
|
|
66
|
+
`site/llms.txt` and `skills/backdraft/SKILL.md` point at it as the way to check
|
|
67
|
+
coverage before binding, and the skill's "optional" for `session start` becomes
|
|
68
|
+
a stated tradeoff.
|
|
69
|
+
|
|
70
|
+
**Size.** Two days.
|
|
71
|
+
|
|
72
|
+
### 2. An unreadable source says what to do, not what errno it was
|
|
73
|
+
|
|
74
|
+
**Intent.** `ingest` now finishes its list and names each failure (2026-08-13),
|
|
75
|
+
which turned the reason string into something a calling agent actually reads —
|
|
76
|
+
and the reason is Python. A directory says `cannot read adir: [Errno 21] Is a
|
|
77
|
+
directory: 'adir'`; a locked file says `[Errno 13] Permission denied`; a missing
|
|
78
|
+
one says `[Errno 2] No such file or directory`. Each names the source three
|
|
79
|
+
times, leaks an errno an agent must not have to know, and — the real cost —
|
|
80
|
+
says nothing about the fix, though the fix differs completely between the three
|
|
81
|
+
and is obvious in each case. A directory means "name the files inside it, or
|
|
82
|
+
glob"; the repo's own rule is that an error says what went wrong *and* what to
|
|
83
|
+
do next, and this is the boundary where an unattended agent meets it most.
|
|
84
|
+
|
|
85
|
+
**Shape.** Wherever the read failure is raised on the ingest path — `cli.py`'s
|
|
86
|
+
`_staged`, `registry/store.py`'s ingest read, and `fetch.py` for the URL half —
|
|
87
|
+
turn the `OSError` into a `BackdraftError` whose message is written for the
|
|
88
|
+
caller: what the source is, why it could not be read, what to do. Map the causes
|
|
89
|
+
worth distinguishing (missing, a directory, unreadable, empty, and the fetch
|
|
90
|
+
side's own) and let anything unmapped keep the underlying text rather than
|
|
91
|
+
guessing — an unknown cause reported plainly beats a wrong suggestion. Say the
|
|
92
|
+
source once: the `!` line already leads with it, so the reason should not repeat
|
|
93
|
+
it. This is *not* the queued "Ingest says what it did and what it got" item, which is about what a successful ingest says
|
|
94
|
+
(no-op, new generation, character count); this is the failure half, and the two
|
|
95
|
+
should not be merged.
|
|
96
|
+
|
|
97
|
+
**Acceptance.** `backdraft ingest <a-directory>` names the directory once and
|
|
98
|
+
says to pass the files inside it or a glob; a missing file, an unreadable file
|
|
99
|
+
and an empty file each get their own reason and their own next step; none of the
|
|
100
|
+
four prints `[Errno`. A URL that 404s keeps saying so. The count line and the
|
|
101
|
+
closing re-run line are unchanged, and the clean-run stdout byte-identity test
|
|
102
|
+
from 2026-08-13 still passes. `site/llms.txt` says the failure reasons are
|
|
103
|
+
actionable so an agent surfaces them to the user rather than paraphrasing.
|
|
104
|
+
|
|
105
|
+
**Size.** One day.
|
|
106
|
+
|
|
107
|
+
### 3. A registry can forget a source
|
|
108
|
+
|
|
109
|
+
**Intent.** Ingest is one-way. An agent working unattended over a folder — which
|
|
110
|
+
is now the normal case, since `ingest` finishes its list — will ingest something
|
|
111
|
+
it should not have: a scratch copy, a duplicate under two names, a file the user
|
|
112
|
+
did not mean to include. Nothing removes it. It stays in `backdraft read`'s list
|
|
113
|
+
forever, its anchors stay in `search` competing with the real source, and an
|
|
114
|
+
agent picking evidence can cite the wrong copy without anything looking wrong.
|
|
115
|
+
The only recovery is deleting `.backdraft/` and starting over, which throws away
|
|
116
|
+
every other document and the whole ledger.
|
|
117
|
+
|
|
118
|
+
**Shape.** Withdraw, do not delete — a new `backdraft forget <slug>` in `cli.py`
|
|
119
|
+
beside `ingest`, and the storage half in `registry/store.py`. A withdrawn
|
|
120
|
+
document disappears from `documents()`, from the gate's list and table of
|
|
121
|
+
contents, and from `search`; its anchors keep resolving, because a token already
|
|
122
|
+
written into somebody's draft or artifact must not silently become `unresolved`
|
|
123
|
+
— that is the failure mode this whole system exists to prevent, and the
|
|
124
|
+
2026-08-05 "provenance, never identity" row governs. So `Registry.resolve` still
|
|
125
|
+
finds it and `show` and `bind` report it, with the closed status set gaining
|
|
126
|
+
nothing: reuse `unresolved` and let the *reason* say withdrawn, since adding a
|
|
127
|
+
status is an artifact-format change and this does not need one. Storage is a
|
|
128
|
+
flag on `documents`, not a DELETE, for the same reason. Require confirmation or
|
|
129
|
+
an explicit flag, since this is the one command that takes something away.
|
|
130
|
+
`spec/registry.md` has landed and pins the export against what it emits, so a
|
|
131
|
+
withdrawal flag reaching `documents[]` is a spec change: name the key there,
|
|
132
|
+
say what a reader must conclude from it, or keep it out of the export.
|
|
133
|
+
|
|
134
|
+
**Acceptance.** Ingest two documents, `forget` one: `read`, `ls` and `search`
|
|
135
|
+
show only the other; a token minted from the forgotten one still resolves
|
|
136
|
+
through `backdraft show`, saying it was withdrawn and when; `bind` on a document
|
|
137
|
+
citing it reports it and does not crash. `forget` on an unknown slug exits 1
|
|
138
|
+
naming the known ones. Re-ingesting the same file after forgetting it brings it
|
|
139
|
+
back as a new generation of the same document rather than a second slug — pin
|
|
140
|
+
that, it is the case that decides whether the flag was the right storage.
|
|
141
|
+
`README.md`, `site/docs.html`, `site/llms.txt` and `SPEC.md`'s command list gain
|
|
142
|
+
it, and it earns a DESIGN.md row: withdraw-not-delete, with the tradeoff that a
|
|
143
|
+
registry never actually shrinks.
|
|
144
|
+
|
|
145
|
+
**Size.** Three days.
|
|
146
|
+
|
|
147
|
+
### 4. Bind's markdown names a fetched source by a file nobody has
|
|
148
|
+
|
|
149
|
+
**Intent.** The 2026-08-18 row made `gate.source_name` the one owner of what a
|
|
150
|
+
source is called and named the one surface it could not reach — bind, because
|
|
151
|
+
`bind` → `gate` is a sideways import SPEC forbids. The exception is larger than
|
|
152
|
+
that row said. `binder.py`'s `_doc_name` returns `document.filename`, and it
|
|
153
|
+
feeds both the generated `## References` section of `bind --bound` and the
|
|
154
|
+
proposals in the unmatched-claims section, so the markdown projection — the
|
|
155
|
+
form that travels into a pull request or an email, where the HTML artifact
|
|
156
|
+
cannot follow — calls the demo's Wikipedia article `index.html`. Bind the demo
|
|
157
|
+
with `--mode backfill --bound` and `memo.bound.md` says
|
|
158
|
+
`**[2]** index.html — p1.c11 — resolved` for a source that `render --to
|
|
159
|
+
footnotes` names `**franklin-county** · <https://en.wikipedia.org/…>` and the
|
|
160
|
+
artifact names `Franklin County`. Two of backdraft's own output formats
|
|
161
|
+
disagree about what a source is, and the one that disagrees is the one a reader
|
|
162
|
+
gets without a browser.
|
|
163
|
+
|
|
164
|
+
**Shape.** Answer the dependency objection rather than working around it.
|
|
165
|
+
`source_name` is a pure function of a `Document`, and `Document` lives in
|
|
166
|
+
`kernel/model.py`, which `gate` and `bind` both import *downward* from — so the
|
|
167
|
+
kernel is the owner the rule was always looking for, and it stays stdlib-pure
|
|
168
|
+
there. Decide and write down whether `gate.source_name` becomes a redirect or
|
|
169
|
+
whether callers move to the kernel path, remembering that kernel API in this
|
|
170
|
+
repo is module paths rather than flat re-exports, and that `cli.py` imports
|
|
171
|
+
`gate.source_name` today and SPEC § Gate names it. Leave the render side alone:
|
|
172
|
+
`render/html/text.source_title` and `render/footnotes._origin` read a `url` off
|
|
173
|
+
the *artifact's* `docs` dict, a different shape from a `Document`, and they are
|
|
174
|
+
already correct — say so in the row rather than letting the next reader think
|
|
175
|
+
they were missed.
|
|
176
|
+
|
|
177
|
+
**Acceptance.** Add an uncited claim to a copy of `demo/memo.md`, run `bind
|
|
178
|
+
memo.md --mode backfill --bound`, and `memo.bound.md` contains no `index.html`
|
|
179
|
+
and names the URL in both its References entries and its unmatched proposals.
|
|
180
|
+
A References entry for a *file* source is byte-identical to today's — pin it,
|
|
181
|
+
the way the file rows of the gate's list are pinned. `render --to footnotes` is
|
|
182
|
+
unchanged. SPEC's dependency rule and § Gate are updated, and the 2026-08-18
|
|
183
|
+
row gains its correction: the exception it accepted is now closed and the row
|
|
184
|
+
should say how.
|
|
185
|
+
|
|
186
|
+
**Size.** Two days.
|
|
187
|
+
|
|
188
|
+
### 5. A web page has a name, and `read` shows it the navigation menu instead
|
|
189
|
+
|
|
190
|
+
**Intent.** `backdraft read franklin-county` — the demo's own web source —
|
|
191
|
+
prints `p1 Franklin County, Ohio - Wikipedia Jump to content Main menu Main
|
|
192
|
+
menu move to sidebar hide Navigation - Main page - Contents - Current events -
|
|
193
|
+
Random art...`. The table-of-contents line is the one surface that tells an
|
|
194
|
+
agent what is on a page before it spends context reading it, and for every web
|
|
195
|
+
source it is site chrome. The page's real name is sitting in the extracted text
|
|
196
|
+
and in the markup's `<title>`, unused: a fetched page ingested without `--slug`
|
|
197
|
+
also titles itself `En Wikipedia Org Index` in the artifact's source list,
|
|
198
|
+
because `render/html/text.source_title` falls back to the slug and title-cases
|
|
199
|
+
it. Two surfaces need one fact nothing captures. Separately, an HTML source is
|
|
200
|
+
a single page, so its table of contents is that one line and does no
|
|
201
|
+
navigating at all — for a 34,000-character article the citable unit is the
|
|
202
|
+
chunk and nothing lists the chunks.
|
|
203
|
+
|
|
204
|
+
**Shape.** Capture the fact once, at extraction: `extract/html.py` already
|
|
205
|
+
parses the markup, so the document's `<title>` becomes page or document meta
|
|
206
|
+
there, and everything downstream reads it instead of guessing. Then
|
|
207
|
+
`gate/reader.py`'s TOC preview (`TOC_PREVIEW_CHARS`, `_preview`) prefers it,
|
|
208
|
+
and `render/html/text.source_title` prefers it over the slug fallback for a
|
|
209
|
+
document that has one. For the single-page case, `render_toc` lists the page's
|
|
210
|
+
chunks with their opening words, so a one-page source's table of contents is
|
|
211
|
+
one. **This is display only and must stay that way**: no chunk boundary moves,
|
|
212
|
+
no anchor moves, no token changes, and the slug is not derived from the title —
|
|
213
|
+
that is the admissibility argument the 2026-08-20 thin-source note made, and
|
|
214
|
+
this row must make it again, because the neighbouring rule ("no boilerplate
|
|
215
|
+
stripping: a heuristic that changes its mind moves anchors") is about
|
|
216
|
+
extraction and is not being touched. A page with no `<title>`, and every
|
|
217
|
+
non-HTML source, keeps exactly what it prints today.
|
|
218
|
+
|
|
219
|
+
**Acceptance.** In `demo/`, `backdraft read franklin-county` names the county
|
|
220
|
+
rather than "Jump to content", and lists the page's chunks. `backdraft read
|
|
221
|
+
t12-summary` and `backdraft read underwriting-model` are byte-identical to
|
|
222
|
+
today's — pin both. `backdraft bind memo.md --session s-bridgeview --check
|
|
223
|
+
value-trace,overlap` still reports 17 resolved and the same one unresolved,
|
|
224
|
+
with the same tokens: mint them before and after and diff. An HTML fixture with
|
|
225
|
+
no `<title>` falls back to what it prints now. `demo/walkthrough.md`'s read
|
|
226
|
+
blocks and `README.md`'s show the real output. DESIGN row.
|
|
227
|
+
|
|
228
|
+
**Size.** Three days.
|
|
229
|
+
|
|
230
|
+
### 6. A page read has no budget and no closing line
|
|
231
|
+
|
|
232
|
+
**Intent.** `backdraft read franklin-county p1` prints 36,442 characters and
|
|
233
|
+
stops, with nothing to say how much that was or whether it was all of it — it
|
|
234
|
+
ends mid-navigation on `49 languages Add topic`, which reads exactly like a
|
|
235
|
+
truncation and is not one. `--limit` defaults to "all of them", so the
|
|
236
|
+
continuation hint this repo is proud of — `[Showing 0-464 of 3115 chars.
|
|
237
|
+
Continue with: …]` — only ever appears to a caller who already knew to pass
|
|
238
|
+
`--limit`. The gate is the one surface an agent is *required* to use, which
|
|
239
|
+
makes the default the case that matters: one `read` can spend a large fraction
|
|
240
|
+
of a context window with no warning, and the agent cannot tell a complete page
|
|
241
|
+
from a cut one. `search` caps at 20 by default; `read` caps at nothing. This is
|
|
242
|
+
the same silent-cap family as the queued `search` item, from the other end — an
|
|
243
|
+
uncapped read that looks capped, rather than a capped search that looks
|
|
244
|
+
complete.
|
|
245
|
+
|
|
246
|
+
**Shape.** `gate/reader.py`'s page rendering and `gate/cli.py`'s `--limit`. The
|
|
247
|
+
decision to make and write down is a default budget versus staying unbounded
|
|
248
|
+
and always closing with the size and the continuation line; prefer whichever
|
|
249
|
+
keeps a small page byte-identical, since most pages are small and their output
|
|
250
|
+
is a contract. Whatever lands, a read that shows part of a page must close by
|
|
251
|
+
naming the exact command that continues it, as the existing hint does. One
|
|
252
|
+
constraint to state and test rather than discover: the window slices by
|
|
253
|
+
characters and a chunk token stands above its chunk, so a cut must not leave a
|
|
254
|
+
token over a chunk the reader only half received — either the window respects
|
|
255
|
+
chunk boundaries or the closing line says the last chunk is partial. Do not add
|
|
256
|
+
an offset story to `search` here; that is the queued item's ground.
|
|
257
|
+
|
|
258
|
+
**Acceptance.** `backdraft read t12-summary p1` in `demo/` is byte-identical to
|
|
259
|
+
what it prints today — pin it. `backdraft read franklin-county p1` closes by
|
|
260
|
+
saying how much of the page it showed and how to see the rest, and running the
|
|
261
|
+
command it names continues where it stopped. No read output ever shows a token
|
|
262
|
+
above a chunk it did not finish. `demo/walkthrough.md`, `README.md`,
|
|
263
|
+
`site/llms.txt` and `skills/backdraft/SKILL.md` show the real output and tell
|
|
264
|
+
the agent to continue rather than assume it saw the page. DESIGN row.
|
|
265
|
+
|
|
266
|
+
**Size.** Two days.
|
|
267
|
+
|
|
268
|
+
### 7. The thin-source signal exists only in the ingest that printed it
|
|
269
|
+
|
|
270
|
+
**Intent.** 2026-08-20 gave `ingest` a character count and a `note: little text
|
|
271
|
+
extracted` naming the likely cause — the signal that a source is a shell and
|
|
272
|
+
must not be cited. Both are printed once and then gone: nothing stores the
|
|
273
|
+
count, and `ls`, the gate's document list and a document's table of contents
|
|
274
|
+
say nothing about it. The agent that writes is usually not the process that
|
|
275
|
+
ingested — the skill says to ingest everything up front and then read, and a
|
|
276
|
+
registry travels with the project folder — so the one signal that matters is
|
|
277
|
+
the one an agent is least likely to be present for. Off `backdraft read`'s
|
|
278
|
+
list, a 34,000-character article and a 60-character login wall are the same
|
|
279
|
+
row.
|
|
280
|
+
|
|
281
|
+
**Shape.** The count is derivable from the current extraction
|
|
282
|
+
(`sum(len(page.text))`), so this is a reporting change rather than a storage
|
|
283
|
+
one unless measurement says otherwise. One owner, shared between `cli.py`'s
|
|
284
|
+
ingest line and `gate/reader.py`'s list and headline, the way `unit` and
|
|
285
|
+
`source_name` already are — the point of this item is that three surfaces give
|
|
286
|
+
one answer, so a second implementation of the count would defeat it. Display
|
|
287
|
+
only, as the ingest note is: no token, anchor or status derives from it. Hold
|
|
288
|
+
the byte-identity rule the source-naming work established — a registry of
|
|
289
|
+
ordinary sources must not gain a column for this, so mark only the sources that
|
|
290
|
+
are thin rather than annotating every row.
|
|
291
|
+
|
|
292
|
+
**Acceptance.** Ingest a login-wall HTML file beside a real source: `backdraft
|
|
293
|
+
read` and `backdraft ls` mark the thin one and say nothing new about the other,
|
|
294
|
+
and `backdraft read <thin-slug>` says it in the headline too. A registry with
|
|
295
|
+
no thin source prints what it prints today, byte for byte — pinned, including
|
|
296
|
+
`demo/`'s three sources. `THIN_SOURCE_CHARS` is read from one place.
|
|
297
|
+
`skills/backdraft/SKILL.md` and `site/llms.txt` tell the agent that the
|
|
298
|
+
document list is where it learns this, not only the ingest it may not have run.
|
|
299
|
+
|
|
300
|
+
**Size.** Two days.
|
|
301
|
+
|
|
302
|
+
### 8. What a URL will be called, before the answer is permanent
|
|
303
|
+
|
|
304
|
+
**Intent.** Three docs now tell an agent to pass `--slug` when it ingests a URL,
|
|
305
|
+
because a slug is permanent once tokens carry it and the default may name a
|
|
306
|
+
site rather than a page. None of them lets the agent find out what the default
|
|
307
|
+
would be. `fetch.filename_for` settles the answer from the address alone,
|
|
308
|
+
before any bytes exist — that is the 2026-08-19 row's own words — so the
|
|
309
|
+
question is cheap and answerable, and today the only way to ask it is to ingest
|
|
310
|
+
and live with the result. An agent that guesses wrong has written a name into
|
|
311
|
+
every token of the document, and the fix is re-ingesting under a new slug and
|
|
312
|
+
rewriting the draft.
|
|
313
|
+
|
|
314
|
+
**Shape.** Small and read-only. The natural home is beside the thing it
|
|
315
|
+
predicts: a flag on `ingest` that resolves each source to the slug and media
|
|
316
|
+
type it would take and prints them without fetching, writing or minting
|
|
317
|
+
anything, or a sibling command if a flag on a writing verb reads badly — pick
|
|
318
|
+
one and say why in the DESIGN row, since "a verb that does not do its verb" is
|
|
319
|
+
the objection to answer. For a URL the answer comes from `fetch.filename_for`
|
|
320
|
+
plus `registry.slug_for` and needs no network; the media type is the honest
|
|
321
|
+
gap, because only the served content type settles it, and the output must say
|
|
322
|
+
so rather than guessing. For a file, both are already knowable. Existing
|
|
323
|
+
collisions matter: a slug already taken must be reported as taken, since that is
|
|
324
|
+
what turns a predicted `index` into a real `index-2`.
|
|
325
|
+
|
|
326
|
+
**Acceptance.** Against `demo/`, asking about
|
|
327
|
+
`https://en.wikipedia.org/w/index.php?title=Franklin_County,_Ohio&oldid=1367935775`
|
|
328
|
+
prints `en-wikipedia-org-index` and does not create a document, open a socket,
|
|
329
|
+
or touch the ledger — check the registry's document count and `ls` before and
|
|
330
|
+
after. Asking about a path prints the slug the file would take. Asking about a
|
|
331
|
+
source whose slug is already in the registry says so. `README.md`,
|
|
332
|
+
`site/docs.html`, `site/llms.txt` and `skills/backdraft/SKILL.md` name it where
|
|
333
|
+
they currently say "pass `--slug`", so the advice comes with the way to check
|
|
334
|
+
it.
|
|
335
|
+
|
|
336
|
+
**Size.** One day.
|
|
337
|
+
|
|
338
|
+
## Parked
|
|
339
|
+
|
|
340
|
+
Deliberately not queued, each with the reason, so picking one up starts from
|
|
341
|
+
the objection rather than rediscovering it.
|
|
342
|
+
|
|
343
|
+
- **Exhibits view** — the evidence-first inversion. Risk: it grows the
|
|
344
|
+
artifact and pollutes a design whose strength is restraint. Needs a
|
|
345
|
+
deliberate design pass and a real audience before any code; not a
|
|
346
|
+
weekend-feature shape.
|
|
347
|
+
- **Excel region maps and range reads** — the research notes exist, but no
|
|
348
|
+
confidence the deterministic detection cascade lands well on the first
|
|
349
|
+
try. Revisit when large-sheet navigation hurts in a real corpus, with
|
|
350
|
+
test workbooks in hand.
|
|
351
|
+
- **`bd:calc` derivations** — reserved in the grammar and parsed as
|
|
352
|
+
malformed-on-purpose, which is enough for now. Full support complicates
|
|
353
|
+
binding and verification for one field-trial case.
|
|
354
|
+
- **Merged-cell and frozen-pane rendering** — ingest captures `merged` and
|
|
355
|
+
`frozen` in sheet meta and keeps them (cheap, and capture is the part you
|
|
356
|
+
cannot backfill). Frozen-pane rendering is cut; merged-cell rendering
|
|
357
|
+
waits for a real sheet that renders wrong without it.
|
|
358
|
+
|
|
359
|
+
## Later
|
|
360
|
+
|
|
361
|
+
- **Living documents** — re-ingest, re-bind, and a drift-first report: "these
|
|
362
|
+
claims cite figures that changed; cited-then vs. now." The primitives
|
|
363
|
+
(generations, `drifted`, the word-diff) all exist; the missing piece is a
|
|
364
|
+
diff-shaped report and a demo.
|
|
365
|
+
- **Substrates beyond the CLI** — SDK middleware for pipelines (the exit-code
|
|
366
|
+
and `--to json` contracts are already shaped for it), and a client-side
|
|
367
|
+
drag-drop viewer page (no upload — the file never leaves the reader's
|
|
368
|
+
machine).
|
|
369
|
+
- **Email (.eml)** — arrives in diligence folders alongside everything else;
|
|
370
|
+
identity questions (headers vs. body, attachments as child sources) deserve
|
|
371
|
+
a decision row before code. The web-page half of this item shipped
|
|
372
|
+
2026-08-05; `document_meta` is where an email's provenance would land.
|
|
373
|
+
- **Anthropic-API provider for the VLM extractor** — Cowork's sandbox
|
|
374
|
+
egress allowlist includes api.anthropic.com but not the OpenAI-compatible
|
|
375
|
+
providers, so an Anthropic-native option would let sandboxed sessions
|
|
376
|
+
ingest at full fidelity. Needs a client abstraction the vlm module
|
|
377
|
+
currently doesn't have; the documented workaround (ingest locally, the
|
|
378
|
+
registry travels with the folder) covers it meanwhile.
|
|
379
|
+
- **Distribution** — published on PyPI; the repo ships
|
|
380
|
+
`.claude-plugin/plugin.json` and `marketplace.json`, so it installs as a
|
|
381
|
+
Claude Code plugin and self-hosts its marketplace, and the AGENTS.md
|
|
382
|
+
snippet is published in the README and site docs. The community-directory
|
|
383
|
+
submission went in 2026-07-29 and is awaiting review — approval is what
|
|
384
|
+
surfaces the skills in Cowork's directory.
|
|
385
|
+
|
|
386
|
+
## Someday
|
|
387
|
+
|
|
388
|
+
- **Hosted team features** — upload, access control, shared registries.
|
|
389
|
+
Deliberately parked: the product's trust story is the self-contained file,
|
|
390
|
+
and a hosted viewer inverts it. Revisit only as a distinct teams product.
|
|
391
|
+
- **Entail at scale** — the model-judge verifier is wired (`[entail]` extra)
|
|
392
|
+
but has never been field-calibrated; needs a corpus and a rubric before it
|
|
393
|
+
is recommended anywhere.
|