# backdraft, provenance for factual claims (agent reference)

Backdraft gates how you read source documents, so every span you can cite is a
span you were shown, with a receipt. You write claims as markdown links whose
hrefs are citation tokens; `bind` resolves them and records evidence; `render`
emits one self-contained HTML artifact for humans.

Full skill (the writing contract, load it if you can):
https://github.com/spencerbraun/backdraft/blob/main/skills/backdraft/SKILL.md

## Install

# Sandboxed session (Cowork, Codex cloud, CI): do NOT install. Run every
# command through uvx instead: `uvx backdraft init`, `uvx backdraft read` …
# No PATH edits, never run `backdraft skill install`, never modify agent
# config from inside a session.
# Owned machine:
uv tool install backdraft   # pip install backdraft where uv is absent
# Human-run setup step (not for sessions): `backdraft skill install` copies
# the writing skill into ~/.claude/skills/ (Claude Code); add
# `--agent codex` to target ~/.agents/skills for the Codex family.
# Vision-model extraction for PDFs and images ships by default (needs poppler
# for PDFs + BACKDRAFT_VLM_API_KEY). Ambient provider keys (OPENAI_API_KEY
# etc.) are NEVER read; only BACKDRAFT_*.
# Every PDF ingest stores each page's image (poppler renders them locally, no
# model calls) so artifacts show the cited page. No poppler: ingest still
# succeeds, notes it, and `backdraft snapshot-pages <slug>` backfills later.
# Formats: pdf, xlsx/xlsm, xls (via [xls]), csv/tsv, docx, pptx,
# png/jpeg/tiff, html/htm, txt/md. pptx is slide text only; export visual-heavy
# decks to PDF and ingest via the vision extractor.
# ingest attempts every source named. One that cannot be read does not stop the
# rest: the command exits 1 printing "N of M sources ingested" and one `!` line
# per failure with its reason; every source not named there is in the registry.
# Re-run the same list after a fix — a source already ingested and unchanged
# re-ingests as a no-op.
# `--config k=v` (repeatable) is checked against the extractor that was chosen,
# so an unknown key exits 1 naming the ones that apply — it is never ignored.
# Keys: PDFs (pdf-text, vlm) take dpi, snapshot_quality, snapshot_max_height;
# the vision paths (vlm, image) also take api_key, base_url, model, timeout,
# retries; vlm alone takes concurrency; image takes no dpi. Every other
# format reads no config keys at all.
# An ingest source can be an http(s) URL, not just a path: the page is fetched
# once and snapshotted like a file. Identity is the sha256 of the fetched
# bytes, so re-ingesting a changed page is a new generation and old citations
# report drifted; the URL + fetched_at ride along as provenance and reach the
# artifact — a receipt on a fetched page links to it with the fetch date. Every
# surface names a fetched source by its URL, never by the filename the fetch
# invented to stage the bytes in: `backdraft ingest`, `ls` and `read` all print
# the URL in the filename's place, so the list is where you learn a source came
# off the web. Prefer an address that serves one fixed revision (Wikipedia's
# ?oldid= permanent link, a DOI, an archived snapshot): a citation into a page
# that gets edited reports drifted, correctly and uselessly. Pass --slug with
# it — a URL ending /index.php, /view or a bare id falls back to a slug built
# from the host, which names the site and still not the page, and a slug is
# permanent once tokens carry it. JavaScript-rendered pages and anything
# behind a login are out of reach — you get what a plain GET returns. No
# boilerplate stripping: nav and footers are part of the page.
# Each ingest line ends with the extracted character count and what happened:
# nothing extra for a new document, `unchanged` for a no-op, `new generation`
# when the bytes moved — the last one means citations into the previous
# snapshot may now be drifted, so re-bind and read the report rather than
# assuming. A source that came back thin (a scan with no text layer, a login
# wall, a deck whose slides are all images) gets a `note: little text
# extracted` line naming the cause at exit 0; that note is the signal a source
# is a shell — surface it, do not cite around it.
# Sandboxes usually cannot reach model providers: if VLM ingest fails on
# network, continue with the text layer and say so. The registry travels
# with the project folder (.backdraft/), so a registry ingested with the
# vision model elsewhere works in any session; bind and render need no key.

## The contract

Read source documents ONLY through `backdraft read` / `backdraft search` /
`backdraft cell` / `backdraft show`. Never Read/cat/grep a source file directly:
text obtained outside the gate has no receipt and cannot be cited. Never
construct or edit a token by hand; copy it from gate output.

## Workflow

backdraft init                              # once per project
backdraft ingest report.pdf model.xlsx      # every source up front
backdraft ingest <url> --slug <name>        # a URL is a source too
backdraft session start --id s-<name>       # enables not_shown detection
export BACKDRAFT_SESSION=s-<name>

backdraft read                              # list documents
backdraft read <slug>                       # table of contents
backdraft read <slug> p3                    # page/range/sheet; chunks arrive
                                            # with tokens: [bd:slug:p3.c2:7f11]
backdraft search "24850000"                 # hits are citable directly
backdraft cell <slug> "sheet!D24"           # mint a specific cell's token
backdraft show bd:slug:p3.c2:7f11 ...       # the inverse: what a token says.
                                            # Status + locator + verbatim
                                            # snippet, in argument order; also
                                            # mints, so a shown token is citable.
                                            # Exit 1 if any named nothing.

Write claims as links; multiple tokens are ;-separated in one href:
  [net operating income of $1,429,600](bd:t12:p1.c3:f10b)
An italic line directly under the # title becomes the artifact subtitle.

backdraft bind memo.md --check value-trace,overlap
backdraft render memo.md --to html          # -> memo.backdraft.html
# Look: --theme <default|press|slate|file.toml>, else .backdraft/theme.toml
# then ~/.config/backdraft/theme.toml. Display only; never a citation concern.
# `backdraft theme list` / `theme show <name>` if the user asks about looks.

## Exit codes (bind)

0 every citation resolved · 1 usage/environment error · 2 something did not
resolve, act on it. Each line item reads
  ! <status>: <token> — <the claim's own words> @<character offset>
so the report names the sentence to fix; don't grep for the token. Statuses:
unresolved (token names nothing: search, fix, re-bind, or state "not supported
by the ingested sources"), not_shown (real anchor you were never shown: read
it, re-bind), drifted (source changed: re-read, confirm), malformed (fix the
href). `backdraft show <token>` is the first move on any of them: it says which
half of an unresolved token is wrong, prints both snippets for a drifted one,
and mints a not_shown one so the next bind resolves. NEVER fix exit 2 by
deleting the token; a kept failure is the honest outcome. Show the user the bind
report verbatim.

## Files

memo.md (authored, yours) · memo.backdraft.html (the deliverable, document +
receipts + evidence, one file, no network) · the record lives at
.backdraft/records/<doc>.backdraft.json. `backdraft clean` tidies strays.
Verification (--check) is evidence, never a gate: a partial is not a problem
to fix.

## Format

Token grammar: bd:<slug>:<locator>:<hash>, locators p8, p8.c3, sheet!B10.
Artifact format string: backdraft/artifact-v1; the JSON island inside the HTML
is self-describing ($legend). Specs:
https://github.com/spencerbraun/backdraft/tree/main/spec
