pageledger 0.1.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.
Files changed (49) hide show
  1. pageledger-0.1.0/CHANGELOG.md +93 -0
  2. pageledger-0.1.0/CITATION.cff +19 -0
  3. pageledger-0.1.0/LICENSE +21 -0
  4. pageledger-0.1.0/MANIFEST.in +19 -0
  5. pageledger-0.1.0/PKG-INFO +500 -0
  6. pageledger-0.1.0/README.md +463 -0
  7. pageledger-0.1.0/docs/adapter-protocol.md +229 -0
  8. pageledger-0.1.0/docs/audit-spec.md +56 -0
  9. pageledger-0.1.0/docs/comparison.md +101 -0
  10. pageledger-0.1.0/docs/design.md +180 -0
  11. pageledger-0.1.0/docs/examples/page-taxonomy.yml +27 -0
  12. pageledger-0.1.0/docs/examples/pageledger.yml +86 -0
  13. pageledger-0.1.0/docs/examples/run-policy.yml +65 -0
  14. pageledger-0.1.0/docs/examples/table-schema.yml +47 -0
  15. pageledger-0.1.0/docs/ocr-options.md +77 -0
  16. pageledger-0.1.0/docs/pdf-ocr-first-run.md +169 -0
  17. pageledger-0.1.0/docs/provenance-spec.md +144 -0
  18. pageledger-0.1.0/docs/reports/2026-06-24-pdf-ocr-readiness-final.md +148 -0
  19. pageledger-0.1.0/docs/reports/2026-06-24-pdf-ocr-stress.md +137 -0
  20. pageledger-0.1.0/docs/rerun-manifest-spec.md +120 -0
  21. pageledger-0.1.0/docs/route-map-spec.md +99 -0
  22. pageledger-0.1.0/docs/run-manifest-spec.md +218 -0
  23. pageledger-0.1.0/examples/cloud_vlm_adapter_skeleton.py +50 -0
  24. pageledger-0.1.0/examples/ocrmypdf_preprocess.sh +7 -0
  25. pageledger-0.1.0/examples/tesseract_pdftoppm_adapter.py +100 -0
  26. pageledger-0.1.0/pageledger/__init__.py +19 -0
  27. pageledger-0.1.0/pageledger/__main__.py +5 -0
  28. pageledger-0.1.0/pageledger/adapters.py +338 -0
  29. pageledger-0.1.0/pageledger/artifacts.py +234 -0
  30. pageledger-0.1.0/pageledger/cli.py +305 -0
  31. pageledger-0.1.0/pageledger/compare.py +169 -0
  32. pageledger-0.1.0/pageledger/config.py +299 -0
  33. pageledger-0.1.0/pageledger/doctor.py +112 -0
  34. pageledger-0.1.0/pageledger/runner.py +1226 -0
  35. pageledger-0.1.0/pageledger.egg-info/PKG-INFO +500 -0
  36. pageledger-0.1.0/pageledger.egg-info/SOURCES.txt +47 -0
  37. pageledger-0.1.0/pageledger.egg-info/dependency_links.txt +1 -0
  38. pageledger-0.1.0/pageledger.egg-info/entry_points.txt +2 -0
  39. pageledger-0.1.0/pageledger.egg-info/requires.txt +10 -0
  40. pageledger-0.1.0/pageledger.egg-info/top_level.txt +1 -0
  41. pageledger-0.1.0/pyproject.toml +68 -0
  42. pageledger-0.1.0/schemas/audit.schema.json +47 -0
  43. pageledger-0.1.0/schemas/cost.schema.json +67 -0
  44. pageledger-0.1.0/schemas/manifest.schema.json +151 -0
  45. pageledger-0.1.0/schemas/provenance-line.schema.json +91 -0
  46. pageledger-0.1.0/schemas/quality-line.schema.json +44 -0
  47. pageledger-0.1.0/schemas/run-log-line.schema.json +46 -0
  48. pageledger-0.1.0/setup.cfg +4 -0
  49. pageledger-0.1.0/skills/pageledger/SKILL.md +100 -0
@@ -0,0 +1,93 @@
1
+ # Changelog
2
+
3
+ All notable changes to PageLedger will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to the artifact compatibility policy documented in
7
+ `docs/run-manifest-spec.md` → Compatibility Policy.
8
+
9
+ ## 0.1.0 — 2026-07-06
10
+
11
+ ### Added
12
+
13
+ - `pageledger run` — extraction command supporting dry-run artifact
14
+ generation and built-in `text` and `pdf_text` adapter execution.
15
+ - `pageledger rerun` — re-extracts exactly the pages listed in a previous
16
+ run's `rerun-manifest.yml`, preserving page ids, recording parent lineage,
17
+ enforcing `run.max_rerun_depth`, and warning when a source checksum no
18
+ longer matches the parent manifest.
19
+ - `pageledger compare-runs` — page-by-page diff of two run directories:
20
+ character/word deltas, warnings resolved/introduced, adapters, cost.
21
+ - `pageledger init-config` — generates a minimal valid config to stdout or file.
22
+ - `pageledger inspect-run` — summarizes a completed or failed run directory.
23
+ - `pageledger doctor` — reports optional dependencies, external tool versions,
24
+ and redacted cloud environment status.
25
+ - Filesystem-native run artifacts: `manifest.json`, `route-map.yml`,
26
+ `config-snapshot.yml`, `audit.json`, `audit.md`, `provenance.jsonl`,
27
+ `quality.jsonl`, `cost.json`, `run.log`, `rerun-manifest.yml`, and per-page
28
+ output under `raw/`.
29
+ - Page-denominated budget enforcement — caps on pages, tokens, and dollars
30
+ with configurable warning thresholds, enforced preflight and mid-run.
31
+ - Cost provenance: `cost.json` reports `cost_basis` (`adapter_reported`,
32
+ `configured_rate`, `mixed`, `none`) and runner-measured
33
+ `extraction_seconds`; provenance lines carry per-page `extraction_seconds`.
34
+ - Retry with configurable `max_retries` and optional exponential backoff
35
+ (`retry.backoff: exponential`, 0.5 s base doubling to an 8 s cap).
36
+ - Quality signal diagnostics with six-item warning taxonomy (`empty_text`,
37
+ `short_text`, `replacement_characters`, `control_characters`,
38
+ `suspicious_symbol_density`, `suspicious_embedded_text_delta`).
39
+ - Quality-warning pages automatically routed to audit `review_queue` with
40
+ `reason: "quality_warning"`.
41
+ - `quality_warning_pages` rollup count in `manifest.summary`.
42
+ - Custom adapters via `module.path:object` import strings.
43
+ - Adapter conformance checker: `pageledger.adapters.adapter_conformance_check()`.
44
+ - Adapter metadata validation at load time (name, version, deterministic,
45
+ input_types, output_types, capabilities, supports, extract).
46
+ - `usage.pages == 1` enforcement — each `extract()` call handles exactly one page.
47
+ - Executable rerun manifests: `rerun_status`
48
+ (`executable`/`empty_queue`/`no_further_generations`), `rerun_depth`
49
+ generation tracking, and the `max_rerun_depth == 0` guard producing empty
50
+ items.
51
+ - Config validation with key-path error messages and suspicious-config warnings
52
+ (empty taxonomy, unknown top-level keys, impossible budget thresholds).
53
+ - JSON Schema files for all JSON/JSONL artifacts under `schemas/`.
54
+ - Schema validation tests covering dry-run, execute, budget failure, adapter
55
+ failure, and empty-review-queue scenarios.
56
+ - Failure recovery: partial-run guarantees documented with scenario table,
57
+ write-order, and common error/user-action mappings.
58
+ - Comprehensive test suite: 197 tests covering all CLI commands, adapters,
59
+ quality signals, rerun execution, cross-run comparison, cost provenance,
60
+ schemas, failure paths, and edge-case inputs.
61
+ - CI workflow with test matrix (with/without PDF extra, Python 3.10–3.13)
62
+ and wheel/sdist smoke tests.
63
+ - Release checklist under `.planning/release-checklist.md`.
64
+
65
+ ### Changed
66
+
67
+ - "Core Modules" → "Design Architecture" with explicit implementation-status
68
+ labels in README.
69
+ - Added "Current Runtime Capabilities" (three tiers) and "Known Limits" sections.
70
+ - Claim audit: removed optimistic score language, constrained the public promise
71
+ to implemented behavior only.
72
+ - `adapter-protocol.md` rewritten with frozen contract, `usage.pages == 1` rule,
73
+ and subprocess/timeout guidance.
74
+
75
+ ### Removed
76
+
77
+ - Python 3.14 classifier (not yet released).
78
+
79
+ ### Performance
80
+
81
+ - Tested scale: 5,000 text pages in 2.4s (~2,100 pages/sec) locally.
82
+ Artifact counts verified: raw files == provenance lines == quality lines
83
+ == pages extracted. Stress tests reproducible without credentials or
84
+ private data.
85
+
86
+ ### Fixed
87
+
88
+ - `--json` now emits parseable error JSON (`{"status": "error", "error": "..."}`)
89
+ on failure instead of unstructured stderr-only errors.
90
+ - Unreadable input files now produce clean `RuntimeError` instead of raw
91
+ `PermissionError` tracebacks.
92
+ - Rerun manifest `max_rerun_depth == 0` now correctly produces empty `items: []`
93
+ with `rerun_status: "no_further_generations"`.
@@ -0,0 +1,19 @@
1
+ cff-version: 1.2.0
2
+ title: "PageLedger: Auditable OCR/VLM Extraction Workflows"
3
+ message: "If you use PageLedger in research, please cite the software and separately cite the source dataset or extraction run you created with it."
4
+ type: software
5
+ authors:
6
+ - family-names: Busscher
7
+ given-names: Peter
8
+ abstract: "PageLedger is an alpha toolkit for routing, auditing, budgeting, and preserving filesystem-native provenance around OCR/VLM document extraction workflows."
9
+ keywords:
10
+ - document extraction
11
+ - OCR
12
+ - vision-language models
13
+ - digital humanities
14
+ - provenance
15
+ - corpus infrastructure
16
+ license: MIT
17
+ repository-code: "https://github.com/peterbussch/pageledger"
18
+ version: "0.1.0"
19
+ date-released: "2026-07-06"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peter Busscher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ include README.md
2
+ include LICENSE
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ recursive-include docs *.md *.yml
6
+ recursive-include examples *.py *.sh
7
+ recursive-include schemas *.json
8
+ recursive-include skills *.md
9
+
10
+ # Local-only material that may exist in a development working tree.
11
+ # Never let it into the sdist even when present on disk.
12
+ exclude docs/autoresearch.md
13
+ exclude _session_research.md
14
+ recursive-exclude prompts *
15
+ recursive-exclude assets *
16
+ prune .planning
17
+ prune .stress
18
+ prune .hermes
19
+ prune runs
@@ -0,0 +1,500 @@
1
+ Metadata-Version: 2.4
2
+ Name: pageledger
3
+ Version: 0.1.0
4
+ Summary: Page-denominated document extraction run ledger
5
+ Author: Peter Busscher
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/peterbussch/pageledger
8
+ Project-URL: Documentation, https://github.com/peterbussch/pageledger#readme
9
+ Project-URL: PDF/OCR First Run, https://github.com/peterbussch/pageledger/blob/main/docs/pdf-ocr-first-run.md
10
+ Project-URL: OCR Options, https://github.com/peterbussch/pageledger/blob/main/docs/ocr-options.md
11
+ Project-URL: Issues, https://github.com/peterbussch/pageledger/issues
12
+ Project-URL: Source, https://github.com/peterbussch/pageledger
13
+ Keywords: document-ai,ocr,vlm,digital-humanities,provenance
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
24
+ Classifier: Topic :: Text Processing
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: PyYAML>=6.0.2
29
+ Provides-Extra: dev
30
+ Requires-Dist: build>=1.2; extra == "dev"
31
+ Requires-Dist: jsonschema>=4.0; extra == "dev"
32
+ Requires-Dist: pytest>=8.0; extra == "dev"
33
+ Requires-Dist: twine>=6.0; extra == "dev"
34
+ Provides-Extra: pdf
35
+ Requires-Dist: pypdf>=6; extra == "pdf"
36
+ Dynamic: license-file
37
+
38
+ <p align="center">
39
+ <picture>
40
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/peterbussch/pageledger/main/assets/pageledger-lockup-horizontal-reversed.png">
41
+ <img src="https://raw.githubusercontent.com/peterbussch/pageledger/main/assets/pageledger-lockup-horizontal.png" alt="PageLedger — the tallied page" width="440">
42
+ </picture>
43
+ </p>
44
+
45
+ <p align="center"><em>Record, route, and review document extraction — one page at a time.</em></p>
46
+
47
+ **PageLedger is for researchers who extract text from archives at scale** —
48
+ digital humanities labs, libraries, historians, and social scientists running
49
+ OCR/VLM over thousands of scanned pages who must be able to answer, months
50
+ later: which pages went through which engine and prompt, what it cost, which
51
+ pages are suspect, and what happened when they were re-extracted.
52
+
53
+ It is a page-denominated **run ledger** — not an OCR model, not a
54
+ PDF-to-Markdown converter. You bring the extraction engine (Tesseract,
55
+ Docling, Marker, a cloud VLM); PageLedger routes pages to it, enforces
56
+ page/token/dollar budgets, and writes the evidence as plain files:
57
+ provenance, quality diagnostics, cost with its basis, audit queues, and
58
+ executable rerun plans. No service, no database — a run directory you can
59
+ grep, cite, and reproduce.
60
+
61
+ PageLedger grew out of
62
+ [soviet-corpus](https://github.com/peterbussch/soviet-corpus), a Soviet census
63
+ digitization and corpus-linguistics project where those questions had to be
64
+ answerable for source citation and methodological review. The run-ledger
65
+ layer was extracted from that working pipeline and rebuilt as a standalone,
66
+ domain-neutral tool.
67
+
68
+ `0.1.0` is an early alpha focused on the run-ledger core: built-in `text` and
69
+ `pdf_text` (born-digital) adapters, custom adapters via import strings,
70
+ dry runs, budgets, quality signals, page-scoped reruns
71
+ (`pageledger rerun`), and cross-run diffs (`pageledger compare-runs`). It
72
+ does not yet OCR images itself, call network VLMs out of the box,
73
+ automatically classify pages, align records to schemas, or compute audit
74
+ grades — see [Current Runtime Capabilities](#current-runtime-capabilities).
75
+
76
+ ## Install
77
+
78
+ ```bash
79
+ pip install pageledger
80
+ ```
81
+
82
+ Optional born-digital PDF text extraction:
83
+
84
+ ```bash
85
+ pip install "pageledger[pdf]"
86
+ ```
87
+
88
+ ## Quickstart
89
+
90
+ Create a text fixture and a starter config:
91
+
92
+ ```bash
93
+ printf 'first page\fsecond page\n' > sample.txt
94
+ pageledger init-config --out pageledger.yml
95
+ ```
96
+
97
+ Inspect the run without extraction:
98
+
99
+ ```bash
100
+ pageledger run sample.txt --config pageledger.yml --out runs/dry --dry-run
101
+ ```
102
+
103
+ Run the built-in text adapter, then summarize the run directory:
104
+
105
+ ```bash
106
+ pageledger run sample.txt --config pageledger.yml --out runs/text --json
107
+ pageledger inspect-run runs/text
108
+ ```
109
+
110
+ For a born-digital PDF, install the optional PDF extra and use `pdf_text`:
111
+
112
+ ```bash
113
+ pip install "pageledger[pdf]"
114
+ cat > pageledger-pdf.yml <<'YAML'
115
+ schema_version: "0.1"
116
+ taxonomy:
117
+ page_types:
118
+ prose:
119
+ default_action: transcribe_text
120
+ run:
121
+ adapter: pdf_text
122
+ YAML
123
+ pageledger run document.pdf --config pageledger-pdf.yml --out runs/pdf-dry --dry-run
124
+ pageledger run document.pdf --config pageledger-pdf.yml --out runs/pdf-text --json
125
+ ```
126
+
127
+ `--out` must point to a new empty directory. For dry runs over PDFs, PageLedger
128
+ uses `pypdf` page counts when the PDF extra is installed; execution with
129
+ `pdf_text` extracts an existing text layer and does not OCR scanned images.
130
+
131
+ The run directory contains `manifest.json`, `route-map.yml`, `audit.json`,
132
+ `audit.md`, `provenance.jsonl`, `quality.jsonl`, `cost.json`, `run.log`,
133
+ `rerun-manifest.yml`, and per-page raw output under `raw/`.
134
+
135
+ For a real PDF/OCR walkthrough, see
136
+ [`docs/pdf-ocr-first-run.md`](docs/pdf-ocr-first-run.md). For local, free,
137
+ open-source, cloud, and hybrid extraction choices, see
138
+ [`docs/ocr-options.md`](docs/ocr-options.md). Custom adapter examples live
139
+ under [`examples/`](examples/), including Tesseract via `pdftoppm`, OCRmyPDF
140
+ preprocessing, and a redacted cloud/VLM skeleton.
141
+
142
+ ## How a Run Works
143
+
144
+ ```mermaid
145
+ flowchart TD
146
+ A["inputs (text / PDF)"] --> B["paginate<br/>(form-feed or PDF pages)"]
147
+ B --> C["route pages<br/>route-map.yml"]
148
+ C --> D{"budget preflight<br/>max_pages"}
149
+ D -- over cap --> X["refuse: nothing written"]
150
+ D -- ok --> E["extract page via adapter<br/>(retry + backoff)"]
151
+ E --> F["quality signals<br/>quality.jsonl"]
152
+ E --> G["provenance.jsonl<br/>+ cost.json (cost_basis)"]
153
+ E --> H{"budget mid-run<br/>pages / tokens / USD"}
154
+ H -- over cap --> Y["halt: manifest status=failed,<br/>partial artifacts consistent"]
155
+ H -- ok --> E
156
+ F -- warnings --> I["review queue<br/>audit.json / audit.md"]
157
+ I --> J["rerun-manifest.yml"]
158
+ J -- "pageledger rerun<br/>(stronger adapter)" --> E
159
+ G --> K["pageledger compare-runs<br/>(parent vs rerun)"]
160
+ ```
161
+
162
+ Every box on the right is a plain file in the run directory; nothing needs a
163
+ service or database.
164
+
165
+ ## Current Runtime Capabilities
166
+
167
+ **Built-in and tested:**
168
+
169
+ - `pageledger run` for text fixtures (form-feed pagination) and born-digital PDF
170
+ text layers (via `pageledger[pdf]`).
171
+ - Dry-run mode that generates full run artifacts without calling extractors.
172
+ - Per-page provenance (`provenance.jsonl`), quality diagnostics
173
+ (`quality.jsonl`), cost rollups (`cost.json`), structured run logs
174
+ (`run.log`), and audit/review queues (`audit.json`, `audit.md`).
175
+ - Page-denominated budget enforcement (pages, tokens, dollars) with preflight
176
+ and per-page caps.
177
+ - Retry with configurable `max_retries` and optional exponential backoff
178
+ (`retry.backoff: exponential`).
179
+ - `pageledger rerun` — re-extracts exactly the pages listed in a previous
180
+ run's rerun manifest (typically with a stronger adapter), preserving page
181
+ ids, recording parent lineage, enforcing `max_rerun_depth`, and warning if
182
+ a source file changed since the parent run.
183
+ - `pageledger compare-runs` — page-by-page diff of two runs: character/word
184
+ deltas, warnings resolved or introduced, adapters, and cost.
185
+ - Cost provenance: `cost.json` records `cost_basis` (`adapter_reported`,
186
+ `configured_rate`, `mixed`, or `none`) so derived accounting rates are
187
+ never mistaken for provider-billed spend, plus runner-measured
188
+ `extraction_seconds` per page and in total.
189
+ - `pageledger doctor` diagnostics for optional PDF/OCR/cloud tooling.
190
+ - Configurable custom adapters via `module.path:object` import strings.
191
+
192
+ **Adapter-supported but user-supplied:**
193
+
194
+ - OCR via Tesseract (external `pdftoppm` + `tesseract` commands), OCRmyPDF
195
+ preprocessing, or any external engine wrapped as a custom adapter.
196
+ - Cloud OCR/VLM adapters (user provides API keys, adapter code, and pricing).
197
+ - Local document-conversion engines (Docling, Marker, Surya) through custom
198
+ adapters.
199
+ - PDF page counting for custom adapters that expose `page_count(source)`.
200
+
201
+ **Documented future work (not yet implemented):**
202
+
203
+ - Automatic page classification (the alpha routes every page to `review` in
204
+ dry-run mode; no classifier ships).
205
+ - Schema alignment execution (the schema config section is parsed but the
206
+ aligner does not yet produce normalized records).
207
+ - Audit grading (review queues are populated but no grades are computed).
208
+ - Multi-adapter routing chains, `rerun_if`/`quarantine_if` policy enforcement,
209
+ and staged CLI commands (`classify`, `extract`, `align`, `audit`).
210
+
211
+ ## Known Limits
212
+
213
+ - **`pdf_text` reads existing text layers.** It does not perform OCR. Scanned
214
+ PDFs, image-heavy PDFs, or PDFs with noisy/absent text layers need an external
215
+ OCR step (OCRmyPDF, Tesseract, Docling, Marker, Surya, or cloud OCR) before
216
+ PageLedger can record useful text.
217
+ - **Quality signals are diagnostic, not calibrated.** `quality.jsonl` records
218
+ per-page evidence: character/word counts, replacement characters, control
219
+ characters, suspicious symbol density, and embedded-text deltas. A six-item
220
+ warning taxonomy (empty_text, short_text, replacement_characters,
221
+ control_characters, suspicious_symbol_density, suspicious_embedded_text_delta)
222
+ flags pages for review. These signals are NOT OCR accuracy scores — they are
223
+ evidence a human should weigh.
224
+ - **Quality-warning pages appear in the audit review queue.** For execution
225
+ runs, every page with one or more quality warnings is added to
226
+ `audit.json` → `review_queue` with reason `"quality_warning"`. Dry-run
227
+ review entries use route-based reasons (`"no_classifier_available"`).
228
+ - **No automatic page routing.** Every page routes to the configured
229
+ `default_action` (or `review` in dry-run mode). Projects that need
230
+ page-type-aware routing must classify pages outside PageLedger or wait for
231
+ a future classifier.
232
+ - **Schema alignment does not execute.** The schema config section is validated
233
+ and preserved in the config snapshot, but the runner does not yet align
234
+ extractor output to declared columns, types, or checks.
235
+ - **Reruns re-extract listed pages; they do not merge results.**
236
+ `rerun-manifest.yml` lists pages from `audit.json` → `review_queue`;
237
+ `pageledger rerun` re-extracts exactly those pages into a new run directory
238
+ with parent lineage. `rerun_status` is `"executable"`, `"empty_queue"`, or
239
+ `"no_further_generations"` (depth cap reached). Combining parent and rerun
240
+ outputs into one corpus remains the project's decision —
241
+ `pageledger compare-runs` shows the per-page evidence for making it.
242
+
243
+ ## Tested Scale
244
+
245
+ The 0.1.0 alpha has been tested locally on:
246
+
247
+ - **5,000 synthetic text pages** (2.4s, ~2,100 pages/sec, artifact counts
248
+ verified — raw files = provenance lines = quality lines = pages extracted).
249
+ - **1,000 synthetic text pages** (0.25s, ~4,000 pages/sec).
250
+ - **72-page born-digital PDF** (via `pageledger[pdf]`).
251
+ - **5 small checked-in text fixtures** covering clean, multipage, OCR-noisy,
252
+ blank, and short text.
253
+
254
+ This is evidence of the envelope, not a universal performance guarantee.
255
+ Stress tests are marked `@pytest.mark.stress` and skipped in default CI.
256
+ Run them with:
257
+
258
+ ```bash
259
+ python -m pytest tests/pageledger/ -m stress
260
+ ```
261
+
262
+ ## The Canonical Unit: Pages
263
+
264
+ PageLedger's defining decision is that **the page is the canonical unit of
265
+ work.** It is the only unit every extraction backend shares:
266
+
267
+ - Cloud OCR (Textract, Azure Document Intelligence, Google Document AI, Mistral
268
+ OCR) bills and reports **per page**.
269
+ - VLM/LLM extractors expose **tokens** — but only on model-backed paths; tokens
270
+ are meaningless for classical OCR.
271
+ - Self-hosted engines (e.g. Docling) have **no dollar cost at all**, only compute
272
+ time.
273
+
274
+ Because pages are the common denominator, denominating routing, budgeting, and
275
+ audit in pages is what lets you **compare, budget, and reproduce a run across
276
+ heterogeneous providers** — a Textract run against a Mistral run against a local
277
+ model — with one number. Tokens, compute-seconds, and dollars are all carried as
278
+ *optional, provider-conditional* signals on top.
279
+
280
+ Every adapter reports a usage record where **`pages` is required** and
281
+ everything else is optional:
282
+
283
+ ```python
284
+ usage = {
285
+ "pages": 1, # REQUIRED — the portable unit
286
+ "tokens": None, # VLM/LLM paths only
287
+ "compute_seconds": None, # self-hosted engines
288
+ "cost_usd": None, # optional adapter-reported passthrough
289
+ }
290
+ ```
291
+
292
+ Dollar cost is **derived by PageLedger, never required of the adapter**, in
293
+ priority order: (1) adapter-reported `cost_usd`, (2) configured unit rates
294
+ (`cost_per_page` / `cost_per_1k_tokens`), (3) otherwise `null` — the run still
295
+ reports raw page counts. Budgets cap on **pages, tokens, or dollars**, whichever
296
+ the config sets, because the page count is the only value always present.
297
+
298
+ ## Why Not Just Use OCR?
299
+
300
+ OCR and document-AI tools are improving quickly. Mistral OCR, Google Document
301
+ AI, Azure Document Intelligence, AWS Bedrock Data Automation, Docling, Marker,
302
+ Surya, olmOCR, OCR-D, and Unstructured all cover parts of extraction,
303
+ conversion, layout analysis, validation, or workflow orchestration.
304
+
305
+ That makes PageLedger's scope deliberately narrow: it is useful when you need a
306
+ local, provider-neutral ledger around those tools, not when you only need one
307
+ tool to convert a document once.
308
+
309
+ | Tool | Strong At | PageLedger Difference |
310
+ |---|---|---|
311
+ | Mistral OCR | Hosted OCR/document understanding with Markdown and table reconstruction. | PageLedger records why pages went to Mistral, what it cost, and which pages need review or rerun. |
312
+ | Google Document AI / Gemini layout parser | Managed OCR, layout parsing, table structure, and RAG-oriented chunking. | PageLedger keeps provider-neutral run artifacts and project-local audit queues outside a cloud workflow. |
313
+ | Azure Document Intelligence | Managed text, key-value, table, and field extraction. | PageLedger can track Azure runs beside local or other-provider runs with the same page-denominated manifest. |
314
+ | AWS Bedrock Data Automation | Managed classification, extraction, validation, HITL review, and business-rule workflows. | PageLedger stays lighter and filesystem-native for research projects that do not want a cloud stack. |
315
+ | Docling | Converting PDFs and documents into structured output. | PageLedger records which pages went to Docling, the usage and cost evidence, and which pages need review or rerun. |
316
+ | Marker | High-quality PDF-to-Markdown conversion. | PageLedger handles routing, run manifests, cost controls, and review queues around conversion. |
317
+ | Surya | OCR, layout analysis, table recognition. | PageLedger can call Surya as an extractor, then audit the result across a full archive run. |
318
+ | olmOCR | LLM-based PDF extraction with document-oriented output. | PageLedger adds page routing, quality diagnostics, cost evidence, and rerun manifests. |
319
+ | OCR-D | Mature OCR workflow model with METS/PAGE/ALTO conventions. | PageLedger aims to be lighter, VLM-aware, and friendlier to local research workflows that need JSON/YAML artifacts before full library infrastructure. |
320
+ | Unstructured | Document partitioning and preprocessing for downstream use. | PageLedger focuses on extraction run control: what ran, what passed, what failed, what cost money, and what should be rerun. |
321
+
322
+ Non-goal: PageLedger is not an OCR engine, PDF converter, or layout detector.
323
+ If a project already has clean structured text from a single tool and does not
324
+ need routing, schema checks, review queues, cost tracking, or reruns, it likely
325
+ does not need PageLedger.
326
+
327
+ For practical provider-agnostic choices, see
328
+ [`docs/ocr-options.md`](docs/ocr-options.md). The recommended workflow: run cheap
329
+ local extraction first, inspect `quality.jsonl` warnings to identify weak pages,
330
+ re-extract just those pages with a stronger adapter via
331
+ `pageledger rerun runs/first/ --config stronger.yml --out runs/second/`, then
332
+ `pageledger compare-runs runs/first/ runs/second/` to see what improved.
333
+
334
+ ## What PageLedger Cannot Do
335
+
336
+ PageLedger does not make OCR or VLM output correct by itself. It cannot
337
+ calibrate confidence scores across unrelated extractors, guarantee text
338
+ accuracy, infer a project's source citation, or make a right-to-left,
339
+ mixed-script, tabular, or handwritten collection work without explicit adapter
340
+ and schema configuration.
341
+
342
+ Its job is narrower: preserve enough evidence that a researcher can see what
343
+ ran, what failed, what is uncertain, and what should be reviewed or rerun.
344
+
345
+ ## Why This Exists
346
+
347
+ Modern OCR and document-AI tools can produce impressive text, tables, and
348
+ Markdown, but real archive work fails in smaller and more frustrating ways:
349
+
350
+ - Some pages are title pages, indexes, maps, appendices, blanks, or prose,
351
+ not data pages.
352
+ - Expensive VLM calls should be routed deliberately instead of sprayed across
353
+ every page.
354
+ - Model-emitted tables often have variant headers, shifted columns, missing
355
+ rows, merged cells, or inconsistent numeric formats.
356
+ - Confidence scores are weak unless they are checked against schema,
357
+ arithmetic, page totals, and model agreement.
358
+ - Long extraction runs need cost tracking, retry logic, checkpoints, and
359
+ provenance that survives reruns.
360
+
361
+ PageLedger is for projects where "the model returned JSON" is only the
362
+ beginning of the work. The current alpha is for proving local run evidence and
363
+ adapter contracts before trusting larger extraction workflows.
364
+
365
+ ## Audience
366
+
367
+ - Digital humanities labs processing archival scans.
368
+ - Libraries and archives building reproducible extraction pipelines.
369
+ - Historians and social scientists extracting structured data from source
370
+ books, statistical tables, reports, and registers.
371
+ - Civic, legal, nonprofit, and local-government document projects.
372
+ - Document-AI engineers who need a lightweight local control plane around
373
+ Docling, Marker, Surya, olmOCR, Tesseract, or API VLMs.
374
+
375
+ ## Architecture and Roadmap
376
+
377
+ The alpha implements the **run controller** (budgets, retry, provenance,
378
+ quality signals, audit queues, rerun execution, cross-run comparison) and
379
+ the **adapter protocol** (thin wrappers that accept a routed page and return
380
+ serializable content, warnings, confidence hints, and usage metadata — see
381
+ [`docs/adapter-protocol.md`](docs/adapter-protocol.md)).
382
+
383
+ Automatic page routing, schema alignment, audit grading, rerun execution, and
384
+ staged CLI commands are design targets, documented with examples in
385
+ [`docs/design.md`](docs/design.md).
386
+
387
+ ## CLI
388
+
389
+ Six commands ship in the alpha; `run` and `rerun` are the ones that extract:
390
+
391
+ ```bash
392
+ pageledger run scans/ --config pageledger.yml --out runs/run-001/
393
+ pageledger run scans/ --config pageledger.yml --out runs/run-001/ --dry-run
394
+ pageledger run scans/ --config pageledger.yml --out runs/run-001/ --json --log-level INFO
395
+ pageledger rerun runs/run-001/ --config stronger.yml --out runs/run-002/
396
+ # re-extract only the flagged pages
397
+ pageledger compare-runs runs/run-001/ runs/run-002/
398
+ # page-by-page diff of two runs
399
+ pageledger init-config --out pageledger.yml # write a minimal valid config
400
+ pageledger inspect-run runs/run-001/ # summarize a run directory
401
+ pageledger doctor --json # check optional dependencies
402
+ ```
403
+
404
+ `--dry-run` generates the route map and planning artifacts without calling
405
+ extractors, so you can inspect routing before spending money or trusting
406
+ output. `--json` makes command status scriptable (parseable stdout, errors on
407
+ stderr), and `--log-level` controls the detail written to the run log.
408
+
409
+ Current non-dry-run support is deliberately tiny. Set `run.adapter: text` to
410
+ write each page of a UTF-8 text input to `raw/doc_0001_page_0001.txt` (text
411
+ sources are split into pages on the form-feed character), or install
412
+ `pageledger[pdf]` and set `run.adapter: pdf_text` for born-digital PDF text
413
+ extraction through `pypdf`. Both paths emit one `provenance.jsonl` line per
414
+ page. This proves the preflight paginate → route → extract → provenance path
415
+ before OCR/VLM integrations. For scanned PDFs or richer layout extraction, run
416
+ an OCR/conversion tool first or point `run.adapter` at a project adapter such as
417
+ `my_project.adapters:TesseractCliAdapter`; PageLedger will keep the same
418
+ manifest, cost, log, and provenance envelope around that external engine.
419
+
420
+ The doctor output reports optional packages, common OCR commands, and whether
421
+ cloud OCR/VLM keys are present without printing their values.
422
+
423
+ Starter config examples live in `docs/examples/`:
424
+
425
+ - `pageledger.yml` (recommended first-use combined config)
426
+ - `page-taxonomy.yml`
427
+ - `table-schema.yml`
428
+ - `run-policy.yml`
429
+
430
+ The split files are useful for documentation and larger projects. The
431
+ recommended v0.1 user experience should start with one `pageledger.yml` that
432
+ contains `taxonomy`, `schema`, and `run` sections.
433
+
434
+ ## Run Artifacts
435
+
436
+ A run should be inspectable without a database or service:
437
+
438
+ ```text
439
+ runs/run-001/
440
+ ├── manifest.json
441
+ ├── config-snapshot.yml
442
+ ├── route-map.yml
443
+ ├── raw/
444
+ │ └── doc_0001_page_0002.txt
445
+ ├── normalized/
446
+ ├── audit.json
447
+ ├── audit.md
448
+ ├── provenance.jsonl
449
+ ├── quality.jsonl
450
+ ├── cost.json
451
+ ├── run.log
452
+ └── rerun-manifest.yml
453
+ ```
454
+
455
+ The important artifact is not only the extracted data, but the evidence around
456
+ it: which pages were skipped, which model or engine was used, what usage and
457
+ cost evidence was observed, which pages failed or need review, and what should
458
+ be rerun or reviewed.
459
+
460
+ `manifest.json` is the canonical run artifact. `audit.md` is a human rendering
461
+ of `audit.json`, not a second independent source of truth. Quarantined pages
462
+ live in `audit.json` under `quarantine_queue`. `config-snapshot.yml` preserves
463
+ the exact user config that produced the run. `run.log` is JSONL — one line per
464
+ extractor call with timestamp, `page_id`, adapter, status, and any error — so
465
+ partial or failed runs stay greppable. Every artifact validates against the
466
+ JSON Schemas in [`schemas/`](schemas/). The artifact directory keeps the name
467
+ `normalized/` for stability, but the current alpha does not write normalized
468
+ records yet; schema alignment remains a documented design target.
469
+
470
+ ## Design Principles
471
+
472
+ - Record uncertainty; do not silently fix it.
473
+ - Treat heuristic confidence as evidence, not probability. Uncalibrated
474
+ extractors should not imply certainty.
475
+ - Every run produces inspectable artifacts on disk.
476
+ - Adapters are thin: PageLedger does not own extraction, it owns the process
477
+ around extraction.
478
+ - The manifest, route map, and provenance files should be useful without a
479
+ running service or database.
480
+ - Preserve separate citations for software and source data.
481
+
482
+ ## Related Documents
483
+
484
+ - [`schemas/`](schemas/) — JSON Schema files, the machine-readable authority
485
+ for artifact contracts
486
+ - [`docs/design.md`](docs/design.md) — design targets: router, schema aligner,
487
+ rerun policies, staged CLI, open research questions
488
+ - [`docs/comparison.md`](docs/comparison.md) — positioning against the 2026
489
+ extraction ecosystem
490
+ - [`docs/run-manifest-spec.md`](docs/run-manifest-spec.md),
491
+ [`docs/route-map-spec.md`](docs/route-map-spec.md),
492
+ [`docs/audit-spec.md`](docs/audit-spec.md),
493
+ [`docs/provenance-spec.md`](docs/provenance-spec.md),
494
+ [`docs/rerun-manifest-spec.md`](docs/rerun-manifest-spec.md) — per-artifact
495
+ field specs
496
+ - [`docs/adapter-protocol.md`](docs/adapter-protocol.md) — how to wrap an
497
+ OCR/VLM tool as an adapter
498
+ - [`AGENTS.md`](AGENTS.md) — orientation for AI coding agents working in this
499
+ repository
500
+ - [`CITATION.cff`](CITATION.cff) — how to cite PageLedger in research output