backdraft 0.2.1__tar.gz → 0.3.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.3.0/.github/workflows/ci.yml +14 -0
- backdraft-0.3.0/.github/workflows/release.yml +23 -0
- backdraft-0.3.0/.github/workflows/site.yml +26 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/.gitignore +1 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/DESIGN.md +17 -17
- {backdraft-0.2.1 → backdraft-0.3.0}/PKG-INFO +11 -6
- {backdraft-0.2.1 → backdraft-0.3.0}/README.md +6 -5
- {backdraft-0.2.1 → backdraft-0.3.0}/ROADMAP.md +20 -21
- {backdraft-0.2.1 → backdraft-0.3.0}/SPEC.md +4 -4
- {backdraft-0.2.1 → backdraft-0.3.0}/demo/generate_sources.py +31 -2
- {backdraft-0.2.1 → backdraft-0.3.0}/demo/memo.backdraft.html +396 -17
- {backdraft-0.2.1 → backdraft-0.3.0}/demo/sources/t12-summary.pdf +2 -2
- backdraft-0.3.0/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/pyproject.toml +7 -1
- {backdraft-0.2.1 → backdraft-0.3.0}/site/demo.html +396 -17
- {backdraft-0.2.1 → backdraft-0.3.0}/site/docs.html +24 -11
- {backdraft-0.2.1 → backdraft-0.3.0}/site/index.html +37 -12
- {backdraft-0.2.1 → backdraft-0.3.0}/site/llms.txt +5 -2
- {backdraft-0.2.1 → backdraft-0.3.0}/spec/artifact.md +12 -4
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/bind/evidence.py +44 -7
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/extract/base.py +20 -4
- backdraft-0.3.0/src/backdraft/extract/csv.py +97 -0
- backdraft-0.3.0/src/backdraft/extract/image.py +88 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/extract/vlm.py +3 -4
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/extract/vlm_settings.py +11 -11
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/extract/xlsx.py +154 -10
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/gate/searcher.py +3 -3
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/kernel/artifact.py +7 -2
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/kernel/model.py +8 -2
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/registry/ledger.py +1 -1
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/registry/schema.sql +8 -1
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/registry/store.py +37 -5
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/render/html.py +134 -16
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/render/backfill.sidecar.json +1 -1
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/render/demo.sidecar.json +1 -1
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_bind_evidence.py +35 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_cli.py +17 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_extract.py +163 -4
- {backdraft-0.2.1 → backdraft-0.3.0}/uv.lock +1 -1
- backdraft-0.2.1/demo/sources/underwriting-model.xlsx +0 -0
- backdraft-0.2.1/research/20260727-excel-region-detection.md +0 -53
- backdraft-0.2.1/research/20260727-research-findings.md +0 -182
- {backdraft-0.2.1 → backdraft-0.3.0}/.python-version +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/LICENSE +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/demo/memo.md +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/demo/walkthrough.md +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/skills/backdraft/SKILL.md +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/skills/backdraft-artifact/SKILL.md +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/skills/backdraft-backfill/SKILL.md +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/spec/chunking.md +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/spec/tokens.md +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/bind/binder.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/bind/cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/bind/verify/entail.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/cli_context.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/credentials.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/extract/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/extract/pdf_text.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/gate/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/gate/cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/gate/reader.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/kernel/chunking.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/registry/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/render/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/render/_text.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/render/cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/render/footnotes.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/render/markdown.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/src/backdraft/render/sidecar.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/conftest.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/conftest_registry.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/conftest_render.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/fakes.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/gate/fake_registry.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/gate/test_gate_cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/gate/test_gate_ledger.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/gate/test_gate_reader.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/gate/test_gate_searcher.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/golden_util.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_bind_cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_chunking.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_claims.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_credentials.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_footnotes.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_gate_integration.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_hashing.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_html.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_invariants.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_model.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_placement.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_records_location.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_registry.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_reingest.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_render_cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_render_markdown.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_tokens.py +0 -0
- {backdraft-0.2.1 → backdraft-0.3.0}/tests/test_value_trace.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- uses: astral-sh/setup-uv@v5
|
|
11
|
+
- run: uv sync --all-extras
|
|
12
|
+
- run: uv run pytest
|
|
13
|
+
publish:
|
|
14
|
+
needs: test
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
environment: pypi
|
|
17
|
+
permissions:
|
|
18
|
+
id-token: write # trusted publishing: OIDC, no stored token
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: astral-sh/setup-uv@v5
|
|
22
|
+
- run: uv build
|
|
23
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: site
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
paths: ["site/**"]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
pages: write
|
|
10
|
+
id-token: write
|
|
11
|
+
concurrency:
|
|
12
|
+
group: pages
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
jobs:
|
|
15
|
+
deploy:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
environment:
|
|
18
|
+
name: github-pages
|
|
19
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/upload-pages-artifact@v3
|
|
23
|
+
with:
|
|
24
|
+
path: site/
|
|
25
|
+
- id: deployment
|
|
26
|
+
uses: actions/deploy-pages@v4
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Backdraft — Design
|
|
2
2
|
|
|
3
|
-
Status: v0 design, 2026-07-27. Decisions below are made unless marked open.
|
|
3
|
+
Status: v0 design, 2026-07-27. Decisions below are made unless marked open.
|
|
4
4
|
|
|
5
5
|
## The point
|
|
6
6
|
|
|
7
|
-
Drop-in provenance for factual claims: one click from any claim to the evidence behind it.
|
|
7
|
+
Drop-in provenance for factual claims: one click from any claim to the evidence behind it. The working problem statement is **verification cost** — an analyst who can't show where a number came from either re-reads the document or doesn't use the output, and both outcomes destroy the value of extraction.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Designed from first principles against the failure modes that kill citation systems in practice: random anchor ids, competing citation grammars, duplicated resolvers, silent failure, anchors that die on reprocessing.
|
|
10
10
|
|
|
11
11
|
## Principles
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ Not a port. The app proved the product (citations worked great from the frontend
|
|
|
14
14
|
2. **Content-addressed everything.** Anchors derive their identity from document content + location, not from a run. Reprocessing the same bytes yields the same anchors; citations survive.
|
|
15
15
|
3. **The receipt travels with the claim.** An anchor is not a pointer — it is a pointer plus captured evidence: verbatim snippet, content hash, location. Defensible without reopening the source.
|
|
16
16
|
4. **Failures are data.** Unresolvable tokens, drifted sources, unmatched claims are first-class records in reports and artifacts — never warn-and-drop.
|
|
17
|
-
5. **Coordinates live in-band.**
|
|
17
|
+
5. **Coordinates live in-band.** Sub-page attribution works when markers like `[B10]` sit in the text the model reads. Human-readable location in the token; side-channel geometry rots (a bbox plumbed through six layers ends up read by nothing).
|
|
18
18
|
6. **The format is the product.** Substrates (skill, CLI, SDK, viewer) are thin shells over one spec + one library. Artifacts are self-describing and outlive any substrate (subtext's `$format` + embedded legend pattern).
|
|
19
19
|
|
|
20
20
|
## Architecture: three layers
|
|
@@ -27,13 +27,13 @@ Not a port. The app proved the product (citations worked great from the frontend
|
|
|
27
27
|
### 2. The library / CLI (the system)
|
|
28
28
|
Four verbs:
|
|
29
29
|
- **`ingest`** — deterministically anchor source documents (PDF/xlsx/md/…) into the registry: extract, locate, snapshot snippets, hash.
|
|
30
|
-
- **`read` / `search`** — the gate. Emit token-marked context for a model. Search results are themselves mintable evidence (
|
|
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
32
|
- **`render`** — produce the clickable artifact.
|
|
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.
|
|
36
|
-
2. **SDK middleware** — wrap a model call: minted context in, bound citations out. The
|
|
36
|
+
2. **SDK middleware** — wrap a model call: minted context in, bound citations out. The drop-in for application developers.
|
|
37
37
|
3. **Viewer** — the artifact carries its own single-file viewer; a component version for embedding comes later.
|
|
38
38
|
|
|
39
39
|
## The registry
|
|
@@ -43,7 +43,7 @@ Four verbs:
|
|
|
43
43
|
Tables (shape, not final DDL):
|
|
44
44
|
- `documents` — path, display name, **slug**, content sha256, media type, ingest metadata.
|
|
45
45
|
- `anchors` — token, document, locator, verbatim snippet, snippet sha256, created-by (ingest run).
|
|
46
|
-
- `ledger` — session id, token, when shown. Records every token minted into context. Bind distinguishes "cited what you saw" from "cited a valid token you were never shown" — a caught hallucination class
|
|
46
|
+
- `ledger` — session id, token, when shown. Records every token minted into context. Bind distinguishes "cited what you saw" from "cited a valid token you were never shown" — a caught hallucination class most systems cannot express.
|
|
47
47
|
- `claims` (written at bind) — claim span text, source doc, citation tokens, verification records.
|
|
48
48
|
|
|
49
49
|
FTS5 backs `search`. No server, no tenancy — single-analyst/single-project scope; multi-user is a substrate concern, not a registry concern.
|
|
@@ -54,7 +54,7 @@ FTS5 backs `search`. No server, no tenancy — single-analyst/single-project sco
|
|
|
54
54
|
|
|
55
55
|
- `slug` — short kebab doc identifier assigned at ingest, unique per registry (`t12-audit`, `rent-roll-2025`).
|
|
56
56
|
- `locator` — media-native:
|
|
57
|
-
- PDF/text: `p8` (page), `p8.c3` (chunk ordinal within page; chunking is deterministic with an explicit within-page ordinal
|
|
57
|
+
- PDF/text: `p8` (page), `p8.c3` (chunk ordinal within page; chunking is deterministic with an explicit within-page ordinal)
|
|
58
58
|
- Excel: `Rent!B10`, `Rent!B10:C12` (sheet + cell/range, spreadsheet-native)
|
|
59
59
|
- Prose files: heading-path or line-range forms (open detail)
|
|
60
60
|
- `hash` — first 4+ hex of sha256 of the normalized snippet. Catches transcription typos, detects drift after re-ingest, and makes the token content-addressed. Registry extends length on collision.
|
|
@@ -65,11 +65,11 @@ Reserved extension — **declared derivations**: a claim whose value appears in
|
|
|
65
65
|
|
|
66
66
|
## The gate (context construction)
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
The read tool's shape:
|
|
69
69
|
- Unified dispatch: list docs → per-doc TOC of page/sheet summaries → page/range read → batch read. Short slugs everywhere.
|
|
70
70
|
- Progressive disclosure with continuation, plus search hints.
|
|
71
|
-
- **Excel**: sheet-per-page markdown table with `Row | A | B | …` headers and `[B10] value` cell prefixes — the representation that
|
|
72
|
-
-
|
|
71
|
+
- **Excel**: sheet-per-page markdown table with `Row | A | B | …` headers and `[B10] value` cell prefixes — the representation that makes cell-level attribution work. Hardening: dimension caps, inflated-sheet placeholders, trimming.
|
|
72
|
+
- Search mints citable snippet tokens; Excel long-context gets structural navigation (detected regions/ranges) rather than char-offset slicing (open design area).
|
|
73
73
|
|
|
74
74
|
The skill's core instruction is a substitution: for source documents, use `backdraft read`/`search`, not raw Read/Grep.
|
|
75
75
|
|
|
@@ -97,11 +97,11 @@ Independent checks per citation, recorded as graded evidence, never gating gener
|
|
|
97
97
|
| 3 | `entail` | Model judge: does snippet support claim — tiny closed question, batchable, async | qualitative |
|
|
98
98
|
| 4 | `recompute` | Deterministic re-execution of declared derivations | derived values |
|
|
99
99
|
|
|
100
|
-
Rationale for default-off: out of the box this is pure provenance (one click to the receipt); verification is opt-in per claim class. Rung-pass rates aggregated per run/model double as an eval substrate
|
|
100
|
+
Rationale for default-off: out of the box this is pure provenance (one click to the receipt); verification is opt-in per claim class. Rung-pass rates aggregated per run/model double as an eval substrate.
|
|
101
101
|
|
|
102
102
|
## Modes
|
|
103
103
|
|
|
104
|
-
- **Front-walk** — citations minted during generation; bind resolves against the session ledger. The high-quality integrated path
|
|
104
|
+
- **Front-walk** — citations minted during generation; bind resolves against the session ledger. The high-quality integrated path.
|
|
105
105
|
- **Backfill** — an existing document + ingested sources; for each claim: search → propose anchors → bind via the fuzzier strategies (overlap, value-trace, structured lookup). Unmatched claims land in an explicit unresolved list, not silent non-attribution. Zero integration required — the demo wedge.
|
|
106
106
|
|
|
107
107
|
Same anchors, same bind, same artifact out.
|
|
@@ -127,9 +127,9 @@ Self-contained HTML (plus the machine-readable sidecar): **quotes + receipts + e
|
|
|
127
127
|
| 2026-07-27 | CLI unified on the gate's patterns: shared `cli_context` (constants, discovery, sessions, one error guard mapping `BackdraftError` → exit 1), `Annotated` params, relative imports. Test fakes renamed so the real and fake registries cannot be confused. |
|
|
128
128
|
| 2026-07-27 | File naming is part of the artifact format, owned by `kernel/artifact.py`: `<stem>.bound.md`, `<stem>.backdraft.json`, `<stem>.backdraft.html`, `<stem>.footnotes.md`. |
|
|
129
129
|
| 2026-07-27 | `bind --check entail` without the extra records `skip` verdicts carrying the reason — verifiers never gate, including at the CLI layer. |
|
|
130
|
-
| 2026-07-27 | **Supersedes "VLM when key present":** credentials are explicit. Ambient provider keys (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`) are never read; a credential reaches backdraft only via `--config`, a `BACKDRAFT_*` variable, or `.backdraft/env` (`init` writes a template). `vlm_ready` — the `auto` gate — is therefore equivalent to consent. Decided after a real unapproved-spend incident during the
|
|
130
|
+
| 2026-07-27 | **Supersedes "VLM when key present":** credentials are explicit. Ambient provider keys (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`) are never read; a credential reaches backdraft only via `--config`, a `BACKDRAFT_*` variable, or `.backdraft/env` (`init` writes a template). `vlm_ready` — the `auto` gate — is therefore equivalent to consent. Decided after a real unapproved-spend incident during the first field trial: presence of a generic key in the environment is not consent to spend it or send documents to its provider. |
|
|
131
131
|
| 2026-07-28 | **Reader doctrine (supersedes "no script"):** the artifact is designed for two readers — the writer verifying the AI's work, and a normal person the document is sent to — in three disclosure layers: the document (editorial typography, quiet marks), the footnote (click a claim → the source's own words, named and paged — no tokens, no hashes, no jargon), and the record (one deliberate step to the full machine-readable layer). **Success is silent; only failure speaks**: a fully-resolved artifact looks like a well-made document, while unresolved citations announce themselves before the reader invests belief. The load-bearing constraint is *no network, single file* — enforced by a CSP `meta` tag (`default-src 'none'`) — not *no script*: inline JS is allowed as progressive enhancement over a CSS-only baseline that survives script-stripping. Decided after field use: the v1 artifact rendered success and failure at equal volume and read as machinery, not a document. |
|
|
132
|
-
| 2026-07-28 | **Evidence embeds in the artifact (supersedes "no page images in v0"):** the footnote layer carries the evidence itself, not just the quote — the cited page as an image, the cited cell in a mini-grid window (neighbors, row labels, column headers). Bounded by *what is cited*, never the corpus, so a memo citing ten pages costs ~2–3 MB; `--lean` skips images. Evidence is assembled at **bind** (the step that has the registry) and travels in the sidecar; render stays registry-blind and the artifact stays reproducible from the two files a reader was handed. For the VLM path, the page image the model was shown is stored at **ingest** — for a non-deterministic extractor the input pixels are the only reproducible snapshot, so the image completes the receipt chain (claim → quote → page). Reference shapes
|
|
132
|
+
| 2026-07-28 | **Evidence embeds in the artifact (supersedes "no page images in v0"):** the footnote layer carries the evidence itself, not just the quote — the cited page as an image, the cited cell in a mini-grid window (neighbors, row labels, column headers). Bounded by *what is cited*, never the corpus, so a memo citing ten pages costs ~2–3 MB; `--lean` skips images. Evidence is assembled at **bind** (the step that has the registry) and travels in the sidecar; render stays registry-blind and the artifact stays reproducible from the two files a reader was handed. For the VLM path, the page image the model was shown is stored at **ingest** — for a non-deterministic extractor the input pixels are the only reproducible snapshot, so the image completes the receipt chain (claim → quote → page). Reference shapes: WebP quality 85 for pages; sheet windows as compact JSON with typed cell values and column-letter headers. |
|
|
133
133
|
| 2026-07-28 | **Exhibits: the evidence-first inversion.** The artifact carries a second reading mode: every source document with the data points cited from it, each with its evidence context and backlinks to the claims that lean on it — "this memo rests on N data points across M documents." Ranking is deterministic in v1 (an anchor cited by three claims outranks one cited once); model-judged importance is a later switch. This is the skeptical reader's entry point and the writer's spot-check checklist. |
|
|
134
134
|
|
|
135
135
|
## Open
|
|
@@ -139,8 +139,8 @@ substrates); this list is questions still awaiting a decision:
|
|
|
139
139
|
|
|
140
140
|
- Re-bind/orphan pass on re-ingest of changed docs (chunk ordinal drift)
|
|
141
141
|
- Prose-file locator forms; slug assignment/collision rules (the trailing-hyphen
|
|
142
|
-
truncation from the
|
|
142
|
+
truncation from the first field trial lives here)
|
|
143
143
|
- Distribution: published to PyPI as `backdraft` 0.2.0 (2026-07-28); domain
|
|
144
144
|
backdraft.dev acquired; still open: skill registration, deploying site/
|
|
145
|
-
- First external user (
|
|
145
|
+
- First external user (design-partner validation layer vs eval substrate vs
|
|
146
146
|
open release feedback)
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backdraft
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Drop-in provenance for factual claims: one click from any claim to the evidence behind it.
|
|
5
|
+
Project-URL: Homepage, https://backdraft.dev
|
|
6
|
+
Project-URL: Documentation, https://backdraft.dev/docs.html
|
|
7
|
+
Project-URL: Repository, https://github.com/spencerbraun/backdraft
|
|
8
|
+
Project-URL: Roadmap, https://github.com/spencerbraun/backdraft/blob/main/ROADMAP.md
|
|
5
9
|
License: MIT
|
|
6
10
|
License-File: LICENSE
|
|
7
11
|
Requires-Python: >=3.13
|
|
@@ -45,7 +49,8 @@ cd demo
|
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
**Ingest** the sources. Every anchor is minted here, chunks for PDF pages,
|
|
48
|
-
cells for spreadsheet sheets.
|
|
52
|
+
cells for spreadsheet sheets. Formats: PDF, XLSX/XLSM, CSV/TSV, images
|
|
53
|
+
(png, jpeg, tiff, through the vision model), plain text and Markdown.
|
|
49
54
|
|
|
50
55
|
```console
|
|
51
56
|
$ backdraft init
|
|
@@ -210,12 +215,12 @@ uv run pytest
|
|
|
210
215
|
|
|
211
216
|
## Skills
|
|
212
217
|
|
|
213
|
-
Three agent skills ship
|
|
214
|
-
agent's skills directory, for Claude Code, `~/.claude/skills/` (personal) or
|
|
215
|
-
`.claude/skills/` (per project):
|
|
218
|
+
Three agent skills ship inside the package, and the CLI installs them:
|
|
216
219
|
|
|
217
220
|
```bash
|
|
218
|
-
|
|
221
|
+
backdraft skill install # the writing skill, into ~/.claude/skills/
|
|
222
|
+
backdraft skill install --all # plus backfill and artifact-reading
|
|
223
|
+
backdraft skill install --project # into this repo's .claude/skills/
|
|
219
224
|
```
|
|
220
225
|
|
|
221
226
|
| Skill | For |
|
|
@@ -28,7 +28,8 @@ cd demo
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
**Ingest** the sources. Every anchor is minted here, chunks for PDF pages,
|
|
31
|
-
cells for spreadsheet sheets.
|
|
31
|
+
cells for spreadsheet sheets. Formats: PDF, XLSX/XLSM, CSV/TSV, images
|
|
32
|
+
(png, jpeg, tiff, through the vision model), plain text and Markdown.
|
|
32
33
|
|
|
33
34
|
```console
|
|
34
35
|
$ backdraft init
|
|
@@ -193,12 +194,12 @@ uv run pytest
|
|
|
193
194
|
|
|
194
195
|
## Skills
|
|
195
196
|
|
|
196
|
-
Three agent skills ship
|
|
197
|
-
agent's skills directory, for Claude Code, `~/.claude/skills/` (personal) or
|
|
198
|
-
`.claude/skills/` (per project):
|
|
197
|
+
Three agent skills ship inside the package, and the CLI installs them:
|
|
199
198
|
|
|
200
199
|
```bash
|
|
201
|
-
|
|
200
|
+
backdraft skill install # the writing skill, into ~/.claude/skills/
|
|
201
|
+
backdraft skill install --all # plus backfill and artifact-reading
|
|
202
|
+
backdraft skill install --project # into this repo's .claude/skills/
|
|
202
203
|
```
|
|
203
204
|
|
|
204
205
|
| Skill | For |
|
|
@@ -14,7 +14,7 @@ losing it. Items graduate by getting built and deleted.
|
|
|
14
14
|
2026-07-28 pending a real audience for it.*
|
|
15
15
|
|
|
16
16
|
- **Excel region maps and range reads** — navigation for large sheets. The
|
|
17
|
-
research is done (
|
|
17
|
+
research is done (notes kept privately): a
|
|
18
18
|
deterministic detection cascade (declared tables → named ranges → print
|
|
19
19
|
areas / freeze panes → merged-aware connected components) producing a
|
|
20
20
|
table of contents per sheet. Two hard constraints from that research:
|
|
@@ -23,24 +23,22 @@ losing it. Items graduate by getting built and deleted.
|
|
|
23
23
|
|
|
24
24
|
## Formats
|
|
25
25
|
|
|
26
|
-
Today: PDF (VLM primary, text-layer floor), XLSX,
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
Today: PDF (VLM primary, text-layer floor), XLSX/XLSM, CSV/TSV, images
|
|
27
|
+
(png, jpeg, tiff — VLM, the file is its own snapshot), plain text and
|
|
28
|
+
Markdown. Each new format is one Extractor implementing the existing
|
|
29
|
+
protocol — pages out, receipts quotable, nothing downstream changes.
|
|
29
30
|
|
|
30
|
-
- **Images (png, jpeg, tiff)** — a photographed or scanned page is a
|
|
31
|
-
one-page document; the VLM extractor already is this pipeline minus the
|
|
32
|
-
pdf2image step, and the source file is its own stored snapshot. Cheapest
|
|
33
|
-
high-value addition.
|
|
34
|
-
- **CSV** — a workbook with one sheet; reuse the sheet representation (A1
|
|
35
|
-
refs, cell anchors) wholesale. Deterministic, keyless, near-trivial.
|
|
36
31
|
- **DOCX** — leases, LOIs, agreements. Structured XML, so the deterministic
|
|
37
32
|
path is the good path (paragraphs and tables straight out of the file, no
|
|
38
|
-
OCR); tables render to markdown like sheets do.
|
|
33
|
+
OCR); tables render to markdown like sheets do. Needs a decision row
|
|
34
|
+
first: DOCX has no pages, so locators want synthesizing from heading
|
|
35
|
+
sections.
|
|
39
36
|
- **PPTX** — offering memoranda that arrive as decks. Slide text is
|
|
40
|
-
extractable deterministically as the floor; the faithful path
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
extractable deterministically as the floor; the faithful path (slide
|
|
38
|
+
images) needs a renderer on PATH, LibreOffice or similar, behind a
|
|
39
|
+
capability check like poppler's.
|
|
40
|
+
- **XLS** — legacy workbooks; python-calamine as the tolerant fallback
|
|
41
|
+
behind an extra.
|
|
44
42
|
- **Someday**: HTML pages, email (.eml) — both arrive in diligence folders,
|
|
45
43
|
both have messy identity questions (what is "the source" of a web page?)
|
|
46
44
|
that deserve a decision row before code.
|
|
@@ -52,10 +50,11 @@ out, receipts quotable, nothing downstream changes.
|
|
|
52
50
|
malformed-on-purpose). The field-trial case: "about 4% of embedded
|
|
53
51
|
loss-to-lease," derived from two cited cells, currently an honest
|
|
54
52
|
value-trace fail with nowhere to point.
|
|
55
|
-
- **
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
- **Merged-cell and frozen-pane rendering** — ingest already captures
|
|
54
|
+
`merged` and `frozen` in sheet meta; the artifact's sheet view renders
|
|
55
|
+
neither yet (merges want colspan/rowspan grid building, frozen panes want
|
|
56
|
+
a second sticky axis). Styling, number formats, and column widths already
|
|
57
|
+
render.
|
|
59
58
|
- **Living documents** — re-ingest, re-bind, and a drift-first report: "these
|
|
60
59
|
claims cite figures that changed; cited-then vs. now." The primitives
|
|
61
60
|
(generations, `drifted`, the word-diff) all exist; the missing piece is a
|
|
@@ -64,8 +63,8 @@ out, receipts quotable, nothing downstream changes.
|
|
|
64
63
|
and `--to json` contracts are already shaped for it), and a client-side
|
|
65
64
|
drag-drop viewer page (no upload — the file never leaves the reader's
|
|
66
65
|
machine).
|
|
67
|
-
- **Distribution** —
|
|
68
|
-
|
|
66
|
+
- **Distribution** — published on PyPI; still queued: a skills-registry
|
|
67
|
+
listing when one becomes the standard channel.
|
|
69
68
|
|
|
70
69
|
## Someday
|
|
71
70
|
|
|
@@ -44,9 +44,9 @@ backdraft/
|
|
|
44
44
|
extract/
|
|
45
45
|
base.py # Extractor protocol + registry of extractors
|
|
46
46
|
pdf_text.py # pdfplumber text-layer; paragraph breaks rebuilt from line geometry
|
|
47
|
-
xlsx.py #
|
|
47
|
+
xlsx.py # openpyxl → markdown table with [B10] refs
|
|
48
48
|
text.py # md/txt passthrough
|
|
49
|
-
vlm.py # [vlm] extra:
|
|
49
|
+
vlm.py # [vlm] extra: pdf→images→VLM per page
|
|
50
50
|
vlm_settings.py # provider/model/base-url resolution; stdlib-only, imports without the extra
|
|
51
51
|
gate/
|
|
52
52
|
reader.py # ls / toc / read page-range → token-marked context; records ledger
|
|
@@ -113,7 +113,7 @@ Deterministic, pure: `chunk(page_text) -> [Chunk(ordinal, text, start, end)]`.
|
|
|
113
113
|
1. Split on blank lines: regex `\n\s*\n`.
|
|
114
114
|
2. Merge forward: any segment < 200 chars merges into the following segment (the last merges backward).
|
|
115
115
|
3. Split long: any segment > 2400 chars splits at the sentence boundary nearest each 1200-char multiple (sentence = terminal `.!?` + space + uppercase/digit start; no abbreviation table in v0 — splitting slightly wrong is fine, it only shifts chunk boundaries deterministically). A final piece under 200 chars merges backward, since merging ran before splitting and could not see it; a chunk is therefore under 200 chars only when it is the only chunk on its page.
|
|
116
|
-
4. Ordinals `c1..cN` in order. No per-page rebalancing, no chunk-count caps (
|
|
116
|
+
4. Ordinals `c1..cN` in order. No per-page rebalancing, no chunk-count caps (rebalancing causes tail-loss bugs; deliberately avoided).
|
|
117
117
|
|
|
118
118
|
Anchor identity for a chunk: (extraction, page number, ordinal). `start`/`end` are char offsets into the page text — stored, enabling future span/region features.
|
|
119
119
|
|
|
@@ -218,7 +218,7 @@ class Extractor(Protocol):
|
|
|
218
218
|
def extract(self, path: Path, config: dict) -> Iterator[ExtractedPage]: ...
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
Registered in a plain dict; `ingest --extractor auto` picks the first `can_handle`, built-ins ordered `xlsx, pdf-text, text`. `vlm` (extra)
|
|
221
|
+
Registered in a plain dict; `ingest --extractor auto` picks the first `can_handle`, built-ins ordered `xlsx, pdf-text, text`. `vlm` (extra) is pdf→images→VLM per page, OpenAI-compatible client, model configurable. **For PDFs, `auto` prefers `vlm` when it is installed and an API key is configured** (glossy layouts and info boxes extract badly from the text layer, and the snapshot is the receipt); otherwise it falls back to `pdf-text` and prints a one-line nudge naming the VLM option. An explicit `--extractor` always wins. xlsx's representation: `Row | A | B...` header, `[B10] value` prefixes, dimension caps, inflated-sheet placeholder pages; cell values are never rounded, since the snapshot is the receipt a claim is traced against. `pdf-text` reconstructs paragraph breaks from line geometry before handing the page over, so the chunker's blank-line rule has something to fire on.
|
|
222
222
|
|
|
223
223
|
## Gate (gate/)
|
|
224
224
|
|
|
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
19
|
from openpyxl import Workbook
|
|
20
|
-
from openpyxl.styles import Alignment, Font
|
|
20
|
+
from openpyxl.styles import Alignment, Font, PatternFill
|
|
21
21
|
from reportlab.lib.enums import TA_JUSTIFY
|
|
22
22
|
from reportlab.lib.pagesizes import LETTER
|
|
23
23
|
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
|
@@ -306,13 +306,42 @@ def build_xlsx(path: Path) -> None:
|
|
|
306
306
|
for row in ASSUMPTIONS:
|
|
307
307
|
assumptions.append(list(row))
|
|
308
308
|
|
|
309
|
+
# Styling, the way a sponsor's model actually looks: a dark header band,
|
|
310
|
+
# inputs in modeling blue, currency and percent number formats, a frozen
|
|
311
|
+
# header row. Values are untouched — the snapshot text and every token
|
|
312
|
+
# are identical with or without this block.
|
|
313
|
+
header_fill = PatternFill("solid", fgColor="FF1F4E79")
|
|
309
314
|
for sheet in (rent, assumptions):
|
|
310
315
|
for cell in sheet[1]:
|
|
311
|
-
cell.font = Font(bold=True)
|
|
316
|
+
cell.font = Font(bold=True, color="FFFFFFFF")
|
|
317
|
+
cell.fill = header_fill
|
|
312
318
|
cell.alignment = Alignment(horizontal="left")
|
|
313
319
|
widths = {"A": 30, "B": 14, "C": 14, "D": 16, "E": 16, "F": 28}
|
|
314
320
|
for column, width in widths.items():
|
|
315
321
|
sheet.column_dimensions[column].width = width
|
|
322
|
+
sheet.freeze_panes = "A2"
|
|
323
|
+
|
|
324
|
+
for row in range(2, 6): # unit-mix money and occupancy columns
|
|
325
|
+
for column, fmt in (("C", "#,##0"), ("D", '"$"#,##0'), ("E", '"$"#,##0')):
|
|
326
|
+
rent[f"{column}{row}"].number_format = fmt
|
|
327
|
+
rent[f"F{row}"].number_format = "0.0%"
|
|
328
|
+
for cell in rent[7]:
|
|
329
|
+
cell.font = Font(bold=True)
|
|
330
|
+
rent["F7"].number_format = "0.0%"
|
|
331
|
+
rent["B9"].number_format = '"$"#,##0'
|
|
332
|
+
rent["B10"].number_format = "0.0%"
|
|
333
|
+
rent["B11"].number_format = '"$"#,##0'
|
|
334
|
+
|
|
335
|
+
input_font = Font(color="FF0000CC") # modeling convention: inputs in blue
|
|
336
|
+
percent_rows = (2, 3, 5, 6, 7, 8, 12, 16)
|
|
337
|
+
money_rows = (9, 10, 11, 14)
|
|
338
|
+
for row in range(2, 17):
|
|
339
|
+
cell = assumptions[f"B{row}"]
|
|
340
|
+
cell.font = input_font
|
|
341
|
+
if row in percent_rows:
|
|
342
|
+
cell.number_format = "0.00%"
|
|
343
|
+
elif row in money_rows:
|
|
344
|
+
cell.number_format = '"$"#,##0'
|
|
316
345
|
|
|
317
346
|
book.save(path)
|
|
318
347
|
|