backdraft 0.2.1__tar.gz → 0.4.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.4.0/.github/workflows/ci.yml +14 -0
- backdraft-0.4.0/.github/workflows/release.yml +23 -0
- backdraft-0.4.0/.github/workflows/site.yml +26 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/.gitignore +1 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/DESIGN.md +24 -21
- {backdraft-0.2.1 → backdraft-0.4.0}/PKG-INFO +42 -21
- {backdraft-0.2.1 → backdraft-0.4.0}/README.md +31 -18
- backdraft-0.4.0/ROADMAP.md +59 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/SPEC.md +15 -7
- {backdraft-0.2.1 → backdraft-0.4.0}/demo/generate_sources.py +31 -2
- {backdraft-0.2.1 → backdraft-0.4.0}/demo/memo.backdraft.html +681 -327
- {backdraft-0.2.1 → backdraft-0.4.0}/demo/sources/t12-summary.pdf +2 -2
- backdraft-0.4.0/demo/sources/underwriting-model.xlsx +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/demo/walkthrough.md +3 -3
- backdraft-0.4.0/pyproject.toml +61 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/site/demo.html +681 -327
- {backdraft-0.2.1 → backdraft-0.4.0}/site/docs.html +32 -15
- {backdraft-0.2.1 → backdraft-0.4.0}/site/index.html +39 -14
- {backdraft-0.2.1 → backdraft-0.4.0}/site/llms.txt +8 -3
- {backdraft-0.2.1 → backdraft-0.4.0}/skills/backdraft/SKILL.md +1 -2
- {backdraft-0.2.1 → backdraft-0.4.0}/spec/artifact.md +12 -4
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/evidence.py +45 -8
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/cli.py +39 -21
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/credentials.py +2 -1
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/__init__.py +1 -1
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/base.py +26 -7
- backdraft-0.4.0/src/backdraft/extract/csv.py +97 -0
- backdraft-0.4.0/src/backdraft/extract/docx.py +159 -0
- backdraft-0.4.0/src/backdraft/extract/image.py +89 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/pdf_text.py +3 -3
- backdraft-0.4.0/src/backdraft/extract/pptx.py +121 -0
- backdraft-0.4.0/src/backdraft/extract/sheet.py +147 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/vlm.py +13 -11
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/vlm_settings.py +43 -13
- backdraft-0.4.0/src/backdraft/extract/xls.py +79 -0
- backdraft-0.4.0/src/backdraft/extract/xlsx.py +283 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/gate/searcher.py +3 -3
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/artifact.py +7 -2
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/model.py +15 -2
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/registry/ledger.py +1 -1
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/registry/schema.sql +8 -1
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/registry/store.py +40 -5
- backdraft-0.4.0/src/backdraft/render/html/__init__.py +90 -0
- backdraft-0.4.0/src/backdraft/render/html/assets.py +633 -0
- backdraft-0.4.0/src/backdraft/render/html/components.py +301 -0
- backdraft-0.4.0/src/backdraft/render/html/fmt.py +80 -0
- backdraft-0.4.0/src/backdraft/render/html/fmt_vectors.py +65 -0
- backdraft-0.4.0/src/backdraft/render/html/page.py +206 -0
- backdraft-0.4.0/src/backdraft/render/html/text.py +244 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/render/backfill.sidecar.json +1 -1
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/render/demo.sidecar.json +1 -1
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_bind_evidence.py +42 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_cli.py +41 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_extract.py +476 -6
- backdraft-0.4.0/tests/test_fmt_parity.py +60 -0
- backdraft-0.4.0/tests/test_snapshot_settings.py +113 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/uv.lock +173 -7
- backdraft-0.2.1/ROADMAP.md +0 -79
- backdraft-0.2.1/demo/sources/underwriting-model.xlsx +0 -0
- backdraft-0.2.1/pyproject.toml +0 -44
- 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/src/backdraft/extract/xlsx.py +0 -244
- backdraft-0.2.1/src/backdraft/render/html.py +0 -1211
- {backdraft-0.2.1 → backdraft-0.4.0}/.python-version +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/LICENSE +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/assets/backdraft-mark.svg +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/demo/memo.md +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/skills/backdraft-artifact/SKILL.md +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/skills/backdraft-backfill/SKILL.md +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/spec/chunking.md +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/spec/tokens.md +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/binder.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/base.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/entail.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/overlap.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/recompute.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/bind/verify/value_trace.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/cli_context.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/extract/text.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/gate/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/gate/cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/gate/reader.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/chunking.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/claims.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/errors.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/hashing.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/kernel/tokens.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/registry/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/__init__.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/_text.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/footnotes.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/markdown.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/placement.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/src/backdraft/render/sidecar.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/conftest.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/conftest_registry.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/conftest_render.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/fakes.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/conftest.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/fake_registry.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/test_gate_cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/test_gate_ledger.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/test_gate_reader.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/test_gate_searcher.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/gate/test_reader_cells.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/bind/backfill.bound.md +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/bind/frontwalk.bound.md +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/long_tail.json +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/long_tail.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/many_small.json +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/many_small.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/mixed.json +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/mixed.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/one_giant.json +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/one_giant.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/short_page.json +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/short_page.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/unsplittable.json +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/chunking/unsplittable.txt +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/render/backfill.footnotes.md +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden/render/demo.footnotes.md +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/golden_util.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_bind_binder.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_bind_cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_bind_verify.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_chunking.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_claims.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_credentials.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_footnotes.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_gate_integration.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_hashing.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_html.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_invariants.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_model.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_placement.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_records_location.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_registry.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_reingest.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_render_cli.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_render_markdown.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_sidecar.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.0}/tests/test_tokens.py +0 -0
- {backdraft-0.2.1 → backdraft-0.4.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,20 +127,23 @@ 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
|
|
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. |
|
|
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
|
+
| 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. *(2026-07-29: parked in ROADMAP — file-size and design-restraint concerns; needs a deliberate design pass and a real audience first.)* |
|
|
134
|
+
| 2026-07-29 | **Office formats extract natively, never through conversion.** DOCX and PPTX are read straight from their XML (python-docx / python-pptx, core deps). Converting to PDF was rejected on principle: conversion is rendering, so it needs a layout engine (LibreOffice, MS Office, or a cloud API — a 700MB dependency, a platform lock, or documents leaving the machine), and converter-dependent pagination would break content-addressed locator stability. DOCX pages are heading sections: the split level is the smallest outline level present among {Heading 1, Heading 2}; preamble is section 1; no headings → one section — locators stay `pN.cM`, no grammar change. PPTX pages are slides (slide N is slide N in every renderer). The text floor deliberately misses slide visuals; the ingest note says so and names the path that captures them — the user exporting the deck to PDF with the real renderer they already have, ingested through the vision extractor — so a calling agent can relay it. |
|
|
135
|
+
| 2026-07-29 | **The vision extractor ships by default (supersedes "[vlm] as extra").** `openai` + `pdf2image` moved into core dependencies; `[vlm]` remains as an empty compat alias. Rationale: the VLM path is the recommended path for real PDFs, and an extra made the recommended path opt-in. The consent boundary does not move — installing deps was never consent; the backdraft-scoped `BACKDRAFT_VLM_API_KEY` is, and ambient provider keys stay unread. There is no slim install: pip extras can only add, never subtract; a separate core distribution is possible later if anyone asks. poppler stays a runtime capability check. |
|
|
136
|
+
| 2026-07-29 | **Renderer decomposed; Python is the formatting authority.** `render/html.py` (1,300 lines) became the `render/html/` package (fmt, text, components, assets, page), byte-identical at the seam. The deliberate `fmt_cell` (Python) / `fmtCell` (JS) duplication is now pinned by one shared vector table driving both tests, with the JS run under node when available. The JS was found diverging on exact rounding ties (`toFixed` rounds half-up; Python rounds half-to-even on the double's value) and was rewritten to round exactly — Python defines correct, JS matches it. The artifact emits minified CSS/JS and unindented markup; the `$legend` and sidecar JSON stay readable in view-source (spec constraint). Snapshot budget knobs `BACKDRAFT_SNAPSHOT_QUALITY`/`BACKDRAFT_SNAPSHOT_MAX_HEIGHT` tune page-image weight at ingest — display only, tokens never derive from pixels. |
|
|
134
137
|
|
|
135
138
|
## Open
|
|
136
139
|
|
|
137
|
-
Queued work lives in ROADMAP.md
|
|
138
|
-
|
|
140
|
+
Queued and parked work lives in ROADMAP.md; this list is questions still
|
|
141
|
+
awaiting a decision:
|
|
139
142
|
|
|
140
143
|
- Re-bind/orphan pass on re-ingest of changed docs (chunk ordinal drift)
|
|
141
|
-
-
|
|
142
|
-
|
|
144
|
+
- Slug assignment/collision rules (the trailing-hyphen truncation from the
|
|
145
|
+
first field trial lives here)
|
|
143
146
|
- Distribution: published to PyPI as `backdraft` 0.2.0 (2026-07-28); domain
|
|
144
147
|
backdraft.dev acquired; still open: skill registration, deploying site/
|
|
145
|
-
- First external user (
|
|
148
|
+
- First external user (design-partner validation layer vs eval substrate vs
|
|
146
149
|
open release feedback)
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backdraft
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
12
|
+
Requires-Dist: openai>=1.0
|
|
8
13
|
Requires-Dist: openpyxl>=3.1
|
|
14
|
+
Requires-Dist: pdf2image>=1.17
|
|
9
15
|
Requires-Dist: pdfplumber>=0.11
|
|
16
|
+
Requires-Dist: python-docx>=1.1
|
|
17
|
+
Requires-Dist: python-pptx>=1.0
|
|
10
18
|
Requires-Dist: typer>=0.12
|
|
11
19
|
Provides-Extra: entail
|
|
12
20
|
Requires-Dist: anthropic>=0.40; extra == 'entail'
|
|
13
21
|
Provides-Extra: vlm
|
|
14
|
-
|
|
15
|
-
Requires-Dist:
|
|
22
|
+
Provides-Extra: xls
|
|
23
|
+
Requires-Dist: python-calamine>=0.3; extra == 'xls'
|
|
16
24
|
Description-Content-Type: text/markdown
|
|
17
25
|
|
|
18
26
|
<img src="assets/backdraft-mark.svg" width="56" alt="">
|
|
@@ -40,12 +48,14 @@ Docs live at [backdraft.dev](https://backdraft.dev), one page for people, and
|
|
|
40
48
|
## 60 seconds
|
|
41
49
|
|
|
42
50
|
```bash
|
|
43
|
-
uv tool install
|
|
51
|
+
uv tool install backdraft
|
|
44
52
|
cd demo
|
|
45
53
|
```
|
|
46
54
|
|
|
47
55
|
**Ingest** the sources. Every anchor is minted here, chunks for PDF pages,
|
|
48
|
-
cells for spreadsheet sheets.
|
|
56
|
+
cells for spreadsheet sheets. Formats: PDF, XLSX/XLSM, XLS, CSV/TSV, DOCX,
|
|
57
|
+
PPTX, images (png, jpeg, tiff, through the vision model), plain text and
|
|
58
|
+
Markdown.
|
|
49
59
|
|
|
50
60
|
```console
|
|
51
61
|
$ backdraft init
|
|
@@ -55,7 +65,7 @@ documents: 0
|
|
|
55
65
|
$ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
|
|
56
66
|
t12-summary t12-summary.pdf pdf 3 pages
|
|
57
67
|
underwriting-model underwriting-model.xlsx xlsx 2 pages
|
|
58
|
-
note: extracted with pdf-text (the embedded text layer). Glossy or scanned PDFs extract better through a vision model:
|
|
68
|
+
note: extracted with pdf-text (the embedded text layer). Glossy or scanned PDFs extract better through a vision model: set BACKDRAFT_VLM_API_KEY in .backdraft/env.
|
|
59
69
|
```
|
|
60
70
|
|
|
61
71
|
**Read.** The page arrives with a citable name over each chunk. This is the whole
|
|
@@ -184,21 +194,33 @@ confidential corpora**.
|
|
|
184
194
|
## Install
|
|
185
195
|
|
|
186
196
|
```bash
|
|
187
|
-
uv tool install
|
|
188
|
-
uv add backdraft
|
|
189
|
-
pip install backdraft
|
|
197
|
+
uv tool install backdraft # CLI, recommended form
|
|
198
|
+
uv add backdraft # as a dependency
|
|
199
|
+
pip install backdraft # or plain pip
|
|
190
200
|
```
|
|
191
201
|
|
|
192
|
-
Python 3.13+.
|
|
202
|
+
Python 3.13+. The vision extractor ships by default, and it is the
|
|
203
|
+
recommended path for real PDFs (glossy layouts, info boxes, scans): a vision
|
|
204
|
+
model reads each page and its clean representation becomes the receipt.
|
|
205
|
+
Installing it is not consent to use it. `--extractor auto` prefers it only on
|
|
206
|
+
explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
|
|
207
|
+
`.backdraft/env`, `backdraft init` writes a template). Ambient
|
|
208
|
+
`OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic
|
|
209
|
+
key in the environment is not consent to send documents to its provider. The
|
|
210
|
+
default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
|
|
211
|
+
base_url + model, set explicitly. Without a scoped key, `auto` falls back to
|
|
212
|
+
the keyless text layer and says so. PDF page rendering needs poppler,
|
|
213
|
+
installed separately.
|
|
214
|
+
|
|
215
|
+
Two extras:
|
|
193
216
|
|
|
194
217
|
| Extra | Adds | For |
|
|
195
218
|
|---|---|---|
|
|
196
|
-
| `[vlm]` | `openai`, `pdf2image` | the VLM extractor, the recommended path for real PDFs (glossy layouts, info boxes, scans): a vision model reads each page and its clean representation becomes the receipt. `--extractor auto` prefers it only on explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or `.backdraft/env`, `backdraft init` writes a template). Ambient `OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic key in the environment is not consent to send documents to its provider. The default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is base_url + model, set explicitly. Without a scoped key, `auto` falls back to the keyless text layer and says so. Needs poppler installed separately |
|
|
197
219
|
| `[entail]` | `anthropic` | `bind --check entail`, the model-judge verifier |
|
|
220
|
+
| `[xls]` | `python-calamine` | legacy `.xls` workbooks, values only, rendered the same shape as xlsx |
|
|
198
221
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
```
|
|
222
|
+
`backdraft[vlm]` still installs: it is an empty compat alias from when the
|
|
223
|
+
vision deps were an extra.
|
|
202
224
|
|
|
203
225
|
From a checkout:
|
|
204
226
|
|
|
@@ -210,12 +232,12 @@ uv run pytest
|
|
|
210
232
|
|
|
211
233
|
## Skills
|
|
212
234
|
|
|
213
|
-
Three agent skills ship
|
|
214
|
-
agent's skills directory, for Claude Code, `~/.claude/skills/` (personal) or
|
|
215
|
-
`.claude/skills/` (per project):
|
|
235
|
+
Three agent skills ship inside the package, and the CLI installs them:
|
|
216
236
|
|
|
217
237
|
```bash
|
|
218
|
-
|
|
238
|
+
backdraft skill install # the writing skill, into ~/.claude/skills/
|
|
239
|
+
backdraft skill install --all # plus backfill and artifact-reading
|
|
240
|
+
backdraft skill install --project # into this repo's .claude/skills/
|
|
219
241
|
```
|
|
220
242
|
|
|
221
243
|
| Skill | For |
|
|
@@ -244,8 +266,7 @@ implementation reads them and nothing else.
|
|
|
244
266
|
## Status
|
|
245
267
|
|
|
246
268
|
v1. The registry format, the token grammar and the artifact format are pinned
|
|
247
|
-
by prose specs and golden-file tests. What is
|
|
248
|
-
|
|
249
|
-
[ROADMAP.md](ROADMAP.md).
|
|
269
|
+
by prose specs and golden-file tests. What is queued, and what is deliberately
|
|
270
|
+
parked with the objection written down, lives in [ROADMAP.md](ROADMAP.md).
|
|
250
271
|
|
|
251
272
|
MIT licensed.
|
|
@@ -23,12 +23,14 @@ Docs live at [backdraft.dev](https://backdraft.dev), one page for people, and
|
|
|
23
23
|
## 60 seconds
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
uv tool install
|
|
26
|
+
uv tool install backdraft
|
|
27
27
|
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, XLS, CSV/TSV, DOCX,
|
|
32
|
+
PPTX, images (png, jpeg, tiff, through the vision model), plain text and
|
|
33
|
+
Markdown.
|
|
32
34
|
|
|
33
35
|
```console
|
|
34
36
|
$ backdraft init
|
|
@@ -38,7 +40,7 @@ documents: 0
|
|
|
38
40
|
$ backdraft ingest sources/t12-summary.pdf sources/underwriting-model.xlsx
|
|
39
41
|
t12-summary t12-summary.pdf pdf 3 pages
|
|
40
42
|
underwriting-model underwriting-model.xlsx xlsx 2 pages
|
|
41
|
-
note: extracted with pdf-text (the embedded text layer). Glossy or scanned PDFs extract better through a vision model:
|
|
43
|
+
note: extracted with pdf-text (the embedded text layer). Glossy or scanned PDFs extract better through a vision model: set BACKDRAFT_VLM_API_KEY in .backdraft/env.
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
**Read.** The page arrives with a citable name over each chunk. This is the whole
|
|
@@ -167,21 +169,33 @@ confidential corpora**.
|
|
|
167
169
|
## Install
|
|
168
170
|
|
|
169
171
|
```bash
|
|
170
|
-
uv tool install
|
|
171
|
-
uv add backdraft
|
|
172
|
-
pip install backdraft
|
|
172
|
+
uv tool install backdraft # CLI, recommended form
|
|
173
|
+
uv add backdraft # as a dependency
|
|
174
|
+
pip install backdraft # or plain pip
|
|
173
175
|
```
|
|
174
176
|
|
|
175
|
-
Python 3.13+.
|
|
177
|
+
Python 3.13+. The vision extractor ships by default, and it is the
|
|
178
|
+
recommended path for real PDFs (glossy layouts, info boxes, scans): a vision
|
|
179
|
+
model reads each page and its clean representation becomes the receipt.
|
|
180
|
+
Installing it is not consent to use it. `--extractor auto` prefers it only on
|
|
181
|
+
explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or
|
|
182
|
+
`.backdraft/env`, `backdraft init` writes a template). Ambient
|
|
183
|
+
`OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic
|
|
184
|
+
key in the environment is not consent to send documents to its provider. The
|
|
185
|
+
default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is
|
|
186
|
+
base_url + model, set explicitly. Without a scoped key, `auto` falls back to
|
|
187
|
+
the keyless text layer and says so. PDF page rendering needs poppler,
|
|
188
|
+
installed separately.
|
|
189
|
+
|
|
190
|
+
Two extras:
|
|
176
191
|
|
|
177
192
|
| Extra | Adds | For |
|
|
178
193
|
|---|---|---|
|
|
179
|
-
| `[vlm]` | `openai`, `pdf2image` | the VLM extractor, the recommended path for real PDFs (glossy layouts, info boxes, scans): a vision model reads each page and its clean representation becomes the receipt. `--extractor auto` prefers it only on explicit, backdraft-scoped consent: `BACKDRAFT_VLM_API_KEY` (env or `.backdraft/env`, `backdraft init` writes a template). Ambient `OPENAI_API_KEY`/`OPENROUTER_API_KEY` are deliberately never read; a generic key in the environment is not consent to send documents to its provider. The default provider is OpenRouter running Gemini 3.1 Flash Lite; direct OpenAI is base_url + model, set explicitly. Without a scoped key, `auto` falls back to the keyless text layer and says so. Needs poppler installed separately |
|
|
180
194
|
| `[entail]` | `anthropic` | `bind --check entail`, the model-judge verifier |
|
|
195
|
+
| `[xls]` | `python-calamine` | legacy `.xls` workbooks, values only, rendered the same shape as xlsx |
|
|
181
196
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
```
|
|
197
|
+
`backdraft[vlm]` still installs: it is an empty compat alias from when the
|
|
198
|
+
vision deps were an extra.
|
|
185
199
|
|
|
186
200
|
From a checkout:
|
|
187
201
|
|
|
@@ -193,12 +207,12 @@ uv run pytest
|
|
|
193
207
|
|
|
194
208
|
## Skills
|
|
195
209
|
|
|
196
|
-
Three agent skills ship
|
|
197
|
-
agent's skills directory, for Claude Code, `~/.claude/skills/` (personal) or
|
|
198
|
-
`.claude/skills/` (per project):
|
|
210
|
+
Three agent skills ship inside the package, and the CLI installs them:
|
|
199
211
|
|
|
200
212
|
```bash
|
|
201
|
-
|
|
213
|
+
backdraft skill install # the writing skill, into ~/.claude/skills/
|
|
214
|
+
backdraft skill install --all # plus backfill and artifact-reading
|
|
215
|
+
backdraft skill install --project # into this repo's .claude/skills/
|
|
202
216
|
```
|
|
203
217
|
|
|
204
218
|
| Skill | For |
|
|
@@ -227,8 +241,7 @@ implementation reads them and nothing else.
|
|
|
227
241
|
## Status
|
|
228
242
|
|
|
229
243
|
v1. The registry format, the token grammar and the artifact format are pinned
|
|
230
|
-
by prose specs and golden-file tests. What is
|
|
231
|
-
|
|
232
|
-
[ROADMAP.md](ROADMAP.md).
|
|
244
|
+
by prose specs and golden-file tests. What is queued, and what is deliberately
|
|
245
|
+
parked with the objection written down, lives in [ROADMAP.md](ROADMAP.md).
|
|
233
246
|
|
|
234
247
|
MIT licensed.
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
## Now
|
|
8
|
+
|
|
9
|
+
Empty, deliberately. The 0.4.0 wave graduated the whole section: DOCX, PPTX,
|
|
10
|
+
and XLS extractors, the shared sheet-extraction module, the renderer
|
|
11
|
+
decomposition with pinned Python/JS format parity, minified artifact
|
|
12
|
+
emission, and the snapshot budget knobs. The next item gets promoted out of
|
|
13
|
+
Parked or Later on purpose, not by momentum.
|
|
14
|
+
|
|
15
|
+
## Parked
|
|
16
|
+
|
|
17
|
+
Deliberately not queued, each with the reason, so picking one up starts from
|
|
18
|
+
the objection rather than rediscovering it.
|
|
19
|
+
|
|
20
|
+
- **Exhibits view** — the evidence-first inversion. Risk: it grows the
|
|
21
|
+
artifact and pollutes a design whose strength is restraint. Needs a
|
|
22
|
+
deliberate design pass and a real audience before any code; not a
|
|
23
|
+
weekend-feature shape.
|
|
24
|
+
- **Excel region maps and range reads** — the research notes exist, but no
|
|
25
|
+
confidence the deterministic detection cascade lands well on the first
|
|
26
|
+
try. Revisit when large-sheet navigation hurts in a real corpus, with
|
|
27
|
+
test workbooks in hand.
|
|
28
|
+
- **`bd:calc` derivations** — reserved in the grammar and parsed as
|
|
29
|
+
malformed-on-purpose, which is enough for now. Full support complicates
|
|
30
|
+
binding and verification for one field-trial case.
|
|
31
|
+
- **Merged-cell and frozen-pane rendering** — ingest captures `merged` and
|
|
32
|
+
`frozen` in sheet meta and keeps them (cheap, and capture is the part you
|
|
33
|
+
cannot backfill). Frozen-pane rendering is cut; merged-cell rendering
|
|
34
|
+
waits for a real sheet that renders wrong without it.
|
|
35
|
+
|
|
36
|
+
## Later
|
|
37
|
+
|
|
38
|
+
- **Living documents** — re-ingest, re-bind, and a drift-first report: "these
|
|
39
|
+
claims cite figures that changed; cited-then vs. now." The primitives
|
|
40
|
+
(generations, `drifted`, the word-diff) all exist; the missing piece is a
|
|
41
|
+
diff-shaped report and a demo.
|
|
42
|
+
- **Substrates beyond the CLI** — SDK middleware for pipelines (the exit-code
|
|
43
|
+
and `--to json` contracts are already shaped for it), and a client-side
|
|
44
|
+
drag-drop viewer page (no upload — the file never leaves the reader's
|
|
45
|
+
machine).
|
|
46
|
+
- **HTML pages and email (.eml)** — both arrive in diligence folders, both
|
|
47
|
+
have messy identity questions (what is "the source" of a web page?) that
|
|
48
|
+
deserve a decision row before code.
|
|
49
|
+
- **Distribution** — published on PyPI; still queued: a skills-registry
|
|
50
|
+
listing when one becomes the standard channel.
|
|
51
|
+
|
|
52
|
+
## Someday
|
|
53
|
+
|
|
54
|
+
- **Hosted team features** — upload, access control, shared registries.
|
|
55
|
+
Deliberately parked: the product's trust story is the self-contained file,
|
|
56
|
+
and a hosted viewer inverts it. Revisit only as a distinct teams product.
|
|
57
|
+
- **Entail at scale** — the model-judge verifier is wired (`[entail]` extra)
|
|
58
|
+
but has never been field-calibrated; needs a corpus and a rubric before it
|
|
59
|
+
is recommended anywhere.
|
|
@@ -44,10 +44,16 @@ 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
|
-
|
|
47
|
+
sheet.py # the shared tabular representation: bounds caps, [B10] row rendering
|
|
48
|
+
xlsx.py # openpyxl → sheet representation + styling meta
|
|
49
|
+
xls.py # [xls] extra: calamine → sheet representation, values only
|
|
50
|
+
csv.py # sniffed-dialect csv/tsv → sheet representation
|
|
51
|
+
docx.py # python-docx; heading sections become pages
|
|
52
|
+
pptx.py # python-pptx; one page per slide, text floor
|
|
48
53
|
text.py # md/txt passthrough
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
image.py # an image is a one-page document; VLM transcription
|
|
55
|
+
vlm.py # pdf→images→VLM per page (deps ship by default; key gates use)
|
|
56
|
+
vlm_settings.py # provider/model/base-url resolution; stdlib-only
|
|
51
57
|
gate/
|
|
52
58
|
reader.py # ls / toc / read page-range → token-marked context; records ledger
|
|
53
59
|
searcher.py # FTS5 → minted snippet results; records ledger
|
|
@@ -59,7 +65,9 @@ backdraft/
|
|
|
59
65
|
base.py # Verifier protocol
|
|
60
66
|
value_trace.py overlap.py recompute.py entail.py # entail behind [entail]
|
|
61
67
|
render/
|
|
62
|
-
html
|
|
68
|
+
html/ # single-file artifact, embedded viewer + legend
|
|
69
|
+
# fmt.py text.py components.py assets.py page.py
|
|
70
|
+
# fmt_vectors.py: one vector table drives fmt_cell (py) and fmtCell (js)
|
|
63
71
|
markdown.py # the authored document's markdown → HTML, stdlib only
|
|
64
72
|
placement.py # locating each claim's span in the document render was handed
|
|
65
73
|
footnotes.py # markdown projection
|
|
@@ -113,7 +121,7 @@ Deterministic, pure: `chunk(page_text) -> [Chunk(ordinal, text, start, end)]`.
|
|
|
113
121
|
1. Split on blank lines: regex `\n\s*\n`.
|
|
114
122
|
2. Merge forward: any segment < 200 chars merges into the following segment (the last merges backward).
|
|
115
123
|
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 (
|
|
124
|
+
4. Ordinals `c1..cN` in order. No per-page rebalancing, no chunk-count caps (rebalancing causes tail-loss bugs; deliberately avoided).
|
|
117
125
|
|
|
118
126
|
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
127
|
|
|
@@ -126,7 +134,7 @@ CREATE TABLE documents (
|
|
|
126
134
|
sha256 TEXT NOT NULL UNIQUE,
|
|
127
135
|
path TEXT NOT NULL, -- as given at ingest; informational
|
|
128
136
|
filename TEXT NOT NULL,
|
|
129
|
-
media_type TEXT NOT NULL, -- 'pdf' | 'xlsx' | 'text'
|
|
137
|
+
media_type TEXT NOT NULL, -- 'pdf' | 'xlsx' | 'xls' | 'csv' | 'docx' | 'pptx' | 'image' | 'text'
|
|
130
138
|
created_at TEXT NOT NULL -- ISO-8601 UTC, everywhere
|
|
131
139
|
);
|
|
132
140
|
|
|
@@ -218,7 +226,7 @@ class Extractor(Protocol):
|
|
|
218
226
|
def extract(self, path: Path, config: dict) -> Iterator[ExtractedPage]: ...
|
|
219
227
|
```
|
|
220
228
|
|
|
221
|
-
Registered in a plain dict; `ingest --extractor auto` picks the first `can_handle`, built-ins ordered `xlsx, pdf-text, text
|
|
229
|
+
Registered in a plain dict; `ingest --extractor auto` picks the first `can_handle`, built-ins ordered `xlsx, xls, csv, docx, pptx, pdf-text, image, text` (an extractor whose optional extra is missing is skipped, not fatal). `vlm` is pdf→images→VLM per page, OpenAI-compatible client, model configurable; its deps ship by default and the backdraft-scoped key gates use. **For PDFs, `auto` prefers `vlm` when it is ready (importable, key 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. The sheet representation (shared by xlsx, xls, csv): `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. `docx` synthesizes pages from heading sections (the document's smallest present outline level among Heading 1–2 splits; no headings → one page), so locators stay `pN.cM`. `pptx` is one page per slide, text floor only; ingest notes that slide visuals need the PDF-export path. `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
230
|
|
|
223
231
|
## Gate (gate/)
|
|
224
232
|
|
|
@@ -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
|
|