# 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
# First command in an unfamiliar environment: `backdraft doctor` (or `uvx
# backdraft doctor`). It reports the registry, poppler, the vision model,
# [xls], [entail] and [math] as `ready` or `missing`, each gap with a `costs`
# line (what the verbs do without it) and its fix, in the words the verb
# itself prints. Keys show as `set`/`absent`, never their values. Always exit
# 0. Plan from it: do not promise a user scanned-PDF extraction when it says
# the vision model is missing.
# 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.
# LaTeX written in a document renders as math in the artifact when the [math]
# extra is installed ($...$, $$...$$, \(...\), \[...\]); without it a formula
# renders verbatim, never corrupted, and `render` says so at exit 0 and names
# the install. Currency like $250 is never read as math.
# 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.
# Each reason says what went wrong AND what to do next — a directory wants the
# files inside it or a glob, a missing path wants its spelling checked, an
# unreadable file wants its permissions fixed or a copy, a URL that 404s wants
# the page saved from a browser. Surface the `!` lines to the user as written
# rather than paraphrasing them; the next step is the part a paraphrase drops.
# A source with no bytes in it is a failure too, not a document with nothing to
# cite: an empty file, and a URL whose server sent an empty body (the shape a
# JavaScript-rendered page takes — nothing here runs JavaScript).
# `--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`, `read` and the
# References section `bind --bound` writes 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. `backdraft ingest <source> --dry-run` prints
# the slug and media type each source would take and stops there — nothing
# fetched, nothing written, no anchor minted — so ask it before you commit to a
# name rather than after: it also says when a source is already ingested (and
# under which slug) and when the name it wants is taken. --slug only names a new
# document: on one already ingested the line ends `--slug not applied` and a
# note names the slug it keeps — cite it by that one. Nothing renames a
# document, `forget` then a re-ingest included: it comes back under the old
# slug. A slug comes from the address alone, so the dry run settles it; a media
# type comes from the content type the server sends, so for a URL it is the
# address's implication until the fetch. 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. Then `backdraft locate <doc.md>`: a paragraph inserted above a
# cited one moves its text without changing it, and locate finds the exact
# cited text in the new snapshot — `moved` names the token that now holds it,
# `ambiguous` names several places (or a cell, whose value alone proves no
# move), `gone` proposes nothing. It rewrites nothing and mints nothing: swap
# the moved tokens in, run the `backdraft show` its closing line names, re-bind.
# 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. You do not have to have run
# the ingest to know: `backdraft read`'s document list, that source's table of
# contents and `backdraft ls` all mark it `little text: N chars`. Check the
# list before you cite, not just the ingest you may not have been present for.
# 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> --dry-run            # what would this be called?
backdraft ingest <url> --slug <name>        # a URL is a source too
backdraft forget <slug> --yes               # a source ingest should not have
                                            # taken (a scratch copy, the same
                                            # report twice): out of read,
                                            # search and ls, with its anchors
                                            # untouched, so tokens already
                                            # written still show their
                                            # receipts. Ingesting the file
                                            # again brings it back. Ask the
                                            # user before running it.
backdraft ls --withdrawn                    # what was withdrawn, when, and the
                                            # ingest that brings each back
backdraft session start --id s-<name>       # do this. Without it every run in
export BACKDRAFT_SESSION=s-<name>           # the project shares one ledger that
                                            # is never reset, so not_shown drops
                                            # from "this writer never saw it" to
                                            # "nothing here ever did".

backdraft read                              # list documents. A row ending
                                            # "little text: N chars" is a
                                            # source that extracted almost
                                            # nothing — a login wall, a scan
                                            # with no text layer. Read it
                                            # before citing it and tell the
                                            # user it came back a shell.
backdraft read <slug>                       # table of contents. A web page is
                                            # a single page, so it lists that
                                            # page's chunks and what each one
                                            # opens with. Read this before a
                                            # page read: one page can be tens
                                            # of thousands of characters.
backdraft read <slug> p3                    # page/range/sheet; chunks arrive
                                            # with tokens: [bd:slug:p3.c2:7f11]
                                            # One read shows 12000 chars (200
                                            # sheet rows) unless --limit says
                                            # otherwise. A page longer than that
                                            # closes with "[Showing 0-11531 of
                                            # 34031 chars. Continue with: ...]":
                                            # you have part of the page, so run
                                            # the command that line names rather
                                            # than writing as if you had it all.
                                            # A read with no such line is the
                                            # whole page. The cut never lands
                                            # inside a chunk, so a token always
                                            # names text you were shown whole.
                                            # A read that reaches the end of its
                                            # page closes with "next page
                                            # begins: [token]" and an excerpt:
                                            # the paragraph may cross the break.
backdraft search "24850000"                 # hits are citable directly.
                                            # A count line reading "2 of 56
                                            # results" means --limit cut the
                                            # rest: the best evidence may be
                                            # below the cut, so run the widening
                                            # command the last line names rather
                                            # than settling for what you were
                                            # shown. A bare "N results" is all
                                            # of them. A hit may carry a second
                                            # token indented under it — "same
                                            # paragraph, after:", "previous page
                                            # ends:" — the chunk its text runs
                                            # on into. If your sentence
                                            # continues there, cite both tokens.
                                            # Both are minted.
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.
backdraft session show                      # coverage, before you write: which
                                            # session is in effect and how many
                                            # distinct anchors it holds per
                                            # document. What is counted binds
                                            # resolved; everything else in the
                                            # registry binds not_shown, so a
                                            # source ingested and never read is
                                            # the row that is missing. Mints
                                            # nothing. Says at exit 0 when you
                                            # are in the shared default session.

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
                                            # first line names the session
                                            # not_shown was judged against;
                                            # the shared default closes the
                                            # report with session show's note.
backdraft locate memo.md                    # after a re-ingest, where each
                                            # drifted citation's exact text
                                            # stands now: moved (new token),
                                            # ambiguous (several places, or a
                                            # cell), gone. Read-only, mints
                                            # nothing; exit 0 whatever it finds.
backdraft render memo.md --to html          # -> memo.backdraft.html
backdraft verify memo.backdraft.html        # check a record you were handed:
                                            # snippets rehashed, tokens checked
                                            # against their anchors, summary
                                            # recounted. Plus, only when a
                                            # .backdraft/ is found from cwd or
                                            # named by --against <project>,
                                            # every token re-resolved against
                                            # the sources. Read-only: mints
                                            # nothing, unlike `show`. Exit 2 if
                                            # something did not verify.
# 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, verify)

0 every citation resolved · 1 usage/environment error · 2 something did not
resolve, act on it. Each line item reads
  ! <status>: <token> [— <reason>] — <the claim's own words> @<character offset>
so the report names the sentence to fix; don't grep for the token. The reason is
there when the status alone does not say enough — a malformed token's parse
error, or a source somebody withdrew. Statuses:
unresolved (token names nothing: search, fix, re-bind, or state "not supported
by the ingested sources" — but if the reason says the source was *withdrawn*,
it was taken out of the registry on purpose: do not re-ingest it to make the
error go away, ask the user, and until then treat the claim as uncited;
`backdraft ls --withdrawn` lists every source withdrawn and when),
not_shown (real anchor you were never shown: read
it, re-bind), drifted (source changed: `backdraft locate <doc.md>` first,
since a moved paragraph is not a changed one; otherwise re-read, confirm),
malformed (fix the href). `backdraft show <token>` is the first move on every
other status: it says which half of an unresolved token is wrong and mints a
not_shown one so the next bind resolves. On a drifted one it prints what you
cited and what stands at that locator now — after an edit above, a different
passage, which is why locate comes first there. NEVER fix exit 2 by
deleting the token; a kept failure is the honest outcome. Show the user the bind
report verbatim.

`verify` shares the codes and the line shape. Its 0 means everything it checked
passed — not that every claim is supported, and not that the sources were
looked at unless the `sources:` line says they were. A `! receipt:` line means
the file was edited after it was written; report that first. A tier-two line
ending `— the record says resolved` means the source moved since the document
was bound: re-read and confirm, do not delete. To check a file you were sent
against a project you have, pass `--against <project root or its .backdraft>`
rather than copying the file into it — only when you know the file came from
there, since verify never infers the link and the wrong registry reports honest
citations as unresolved or drifted. The sources: line names the registry that
answered; a path with no registry exits 1.

Parse keys, not lines: the lines are worded for the user and get reworded. To
act on a result as data, add --json — same exit codes, one JSON object on
stdout, nothing on stdout at exit 1. `bind --json` prints the record, the same
bytes bind writes under .backdraft/records/: every citation's token, status and
error under claims[].citations[], counts in summary.by_status. It carries the
embedded evidence (page images make it hundreds of KB), so pipe it into a
parser. `verify --json` prints a small object (format backdraft/verify-v1):
record.ran and sources.ran say which tiers ran, and each findings[] entry has a
kind — receipt (the file was edited; check names which), recount (summary
disagrees with claims) or source (status is today's, recorded is the record's;
differing means the source moved). findings is empty exactly when exit is 0.
Relay the plain report to the user, never the JSON.

## 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 verify` reads either the
artifact or the record. `backdraft clean` tidies strays.
Verification (--check) is evidence, never a gate: a partial is not a problem
to fix, and a skip prints the reason it declined on the line under its method
(most often: a claim citing a single cell, where wording overlap means nothing)
— also 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
