refactor(reader): extract.rs moves into ledger-reader (U0d)

D-61 (Q-9): the writer sheds the typesetter and the crate map is layered
again. The PDF/XLSX reader (extract.rs, plan P6) is now the plain library
crate ledger-reader, moved whole with git mv and no behavior change.
ledger-store depends on it instead of ledger-import, so the store no
longer builds Typst or the fonts; ledger-import re-exports it as
ledger_import::extract so gui, mcp, host and the fuzz targets compile
unchanged, and still never depends on the store. New crate carries only
the dependencies extract.rs imports (hayro, hayro-interpret, kurbo, zip,
quick-xml, thiserror); fuzz/Cargo.lock gains the new package.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-29 12:45:14 -06:00
1 parent f98471516e
commit bb1eb84200
15 files changed
+344 -68

No files matched your search

+3 -11
View File
@@ -18,17 +18,8 @@ typst = "0.15.1"
serde_json = "1"
typst-layout = "0.15.1"
qrcode = { version = "0.14.1", default-features = false, features = ["svg"] }
# Extraction (extract.rs, plan P6): a PDF's text, a page's pixels, an
# XLSX's cells. hayro (the renderer) and hayro-interpret (the text
# interpreter) are pure Rust and already the viewer's PDF reader, so the
# server and the viewer read a receipt through one implementation rather
# than two; `zip` and `quick-xml` read a spreadsheet's XML. None of them
# touches money: a number is kept as the decimal string the XML holds.
hayro = { version = "0.8", features = ["embed-fonts"] }
hayro-interpret = "0.8"
kurbo = "0.13"
zip = { version = "9.0.1", default-features = false, features = ["deflate"] }
quick-xml = "0.38"
# The reader (extract.rs moved out: D-61); re-exported as `extract`.
ledger-reader = { path = "../ledger-reader" }
# BSD-3-Clause: the IIF's transliteration to ASCII (iif.rs, rules.md B-34).
# Pinned exactly: a new table would change what a CPA's file says.
deunicode = { version = "=1.6.0", default-features = false }
@@ -38,4 +29,5 @@ workspace = true
[dev-dependencies]
proptest = "1"
zip = { version = "9.0.1", default-features = false, features = ["deflate"] }
rqrr = { version = "0.11.0", default-features = false }
+2 -1
View File
@@ -16,7 +16,8 @@
pub mod asset_register;
pub mod bank_csv;
pub mod chart_csv;
pub mod extract;
/// The one reader, now the `ledger-reader` crate (D-61), kept at its old path.
pub use ledger_reader as extract;
pub mod fonts;
pub mod iif;
pub mod invoice_pdf;
+1 -1
View File
@@ -1,7 +1,7 @@
//! The one extractor (plan P6 step 1): a PDF's text and pages, and an
//! XLSX's cells as exact decimal strings.
//!
//! Expected behavior comes from `crates/ledger-import/src/extract.rs`'s
//! Expected behavior comes from `crates/ledger-reader/src/lib.rs`'s
//! INVARIANTS, which come from plan/served.md P6 step 1 and D-15, D-32 and
//! D-53.
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "ledger-reader"
version = "0.1.0"
edition = "2024"
license = "MIT OR Apache-2.0"
description = "The one reader: a PDF's text and pages, an XLSX's cells as exact decimal strings (plan P6, D-61)."
[dependencies]
thiserror = "2"
# Extraction (plan P6): a PDF's text, a page's pixels, an XLSX's cells.
# hayro (the renderer) and hayro-interpret (the text interpreter) are pure
# Rust and already the viewer's PDF reader, so the server and the viewer
# read a receipt through one implementation rather than two; `zip` and
# `quick-xml` read a spreadsheet's XML. None of them touches money: a
# number is kept as the decimal string the XML holds.
hayro = { version = "0.8", features = ["embed-fonts"] }
hayro-interpret = "0.8"
kurbo = "0.13"
zip = { version = "9.0.1", default-features = false, features = ["deflate"] }
quick-xml = "0.38"
[lints]
workspace = true
File renamed without changes.
+2 -2
View File
@@ -8,11 +8,11 @@ description = "Durable persistence for wanderbooks: a hash-chained JSONL journal
[dependencies]
ledger-core = { path = "../ledger-core" }
# The one extractor (plan P6 step 1, D-53): the writer journals a file's
# The one reader (plan P6 step 1, D-53, D-61: its own crate): the writer journals a file's
# text at attach time and on `wanderbooks books extract`, so the extractor
# must be callable here rather than only from the server. It is pure and
# deterministic (no I/O, no network, no time source).
ledger-import = { path = "../ledger-import" }
ledger-reader = { path = "../ledger-reader" }
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+6 -6
View File
@@ -45,7 +45,7 @@ use ledger_core::{
payroll::{PayrollAccounts, payroll_entry_plan, payroll_remit_plan},
reports,
};
use ledger_import::extract::{EXTRACTOR as E_EXTRACTOR, VERSION as E_VERSION};
use ledger_reader::{EXTRACTOR as E_EXTRACTOR, VERSION as E_VERSION};
use rusqlite::{Connection, OptionalExtension, params};
use rust_decimal::Decimal;
use serde_json::{Value, json};
@@ -368,7 +368,7 @@ pub struct Extraction {
pub attachment_id: u64,
/// The text's blob (its SHA-256), when there is text.
pub text_sha256: Option<String>,
/// What read it (`ledger_import::extract::EXTRACTOR`).
/// What read it (`ledger_reader::EXTRACTOR`).
pub extractor: String,
/// That extractor's version.
pub version: String,
@@ -4620,7 +4620,7 @@ impl Store {
if let Some(already) = self.extraction_for(attachment_id, E_EXTRACTOR, E_VERSION)? {
return Ok(already);
}
let got = ledger_import::extract::extract(bytes, mime_type, file_name);
let got = ledger_reader::extract(bytes, mime_type, file_name);
let text_sha = match &got.text {
Some(text) => Some(self.blobs.put(text.as_bytes())?.0),
None => None,
@@ -4644,9 +4644,9 @@ impl Store {
version: E_VERSION.into(),
pages: got.pages,
status: match got.status {
ledger_import::extract::ExtractStatus::Ok => ExtractionStatus::Ok,
ledger_import::extract::ExtractStatus::None => ExtractionStatus::None,
ledger_import::extract::ExtractStatus::Failed => ExtractionStatus::Failed,
ledger_reader::ExtractStatus::Ok => ExtractionStatus::Ok,
ledger_reader::ExtractStatus::None => ExtractionStatus::None,
ledger_reader::ExtractStatus::Failed => ExtractionStatus::Failed,
},
seq: line.seq,
})
+2 -2
View File
@@ -1471,8 +1471,8 @@ fn extraction_is_journaled_once_per_version() {
let x = s.extraction(id).unwrap().expect("an extraction record");
assert_eq!(x.status, ExtractionStatus::Ok);
assert_eq!(x.pages, 1);
assert_eq!(x.extractor, ledger_import::extract::EXTRACTOR);
assert_eq!(x.version, ledger_import::extract::VERSION);
assert_eq!(x.extractor, ledger_reader::EXTRACTOR);
assert_eq!(x.version, ledger_reader::VERSION);
assert_eq!(
s.extraction_text(id).unwrap().as_deref(),
Some(String::from_utf8_lossy(text).as_ref())