ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Docsort Pipeline Integration Plan
Context
The REFACTOR.md describes "docsort" -- a fully offline, privacy-first, German+English document sorting daemon. The existing OcrTextSnap (SnapText) codebase is a well-architected Rust app with OCR, SQLite persistence, state machine, event bus, and configuration hot-reload. This plan integrates docsort into the existing codebase, reusing patterns and dependencies wherever possible.
Goal: Add a src/docsort/ module and src/bin/docsort.rs binary that takes scanner-dropped documents (PDF, TIFF, images) from an inbox folder through OCR processing, LLM metadata extraction, and deterministic file naming -- all crash-safe and offline.
Phased approach: Phase 1 (this implementation) covers the full pipeline from inbox watcher through OCR, LLM extraction, filename generation, and file output. Phase 2 (future) adds embeddings/FAISS vector similarity.
Architecture Decision: Separate Binary
The existing main.rs is a synchronous macOS GUI event loop (tray icon, hotkeys, 60fps polling). Docsort is an async tokio daemon. These are fundamentally different runtimes, so docsort gets its own [[bin]] entry in Cargo.toml. Both binaries share the library crate (src/lib.rs) and all existing modules.
Files to Modify (minimal changes) ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β File β Change β ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β Cargo.toml β Add [[bin]] for docsort, add 6 new deps (clap, sha2, hex, reqwest, uuid, walkdir) β ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β src/lib.rs β Add pub mod docsort; and 3 new SnapTextError variants β ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β src/domain/mod.rs β Add pub mod document; and re-exports β ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β src/persistence/mod.rs β Add pub mod docsort_migrations; and pub mod docsort_repository; β ββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Files to Create
- Domain Layer
- src/domain/document.rs -- DocumentStage enum, ErrorCode enum, Document struct, DocumentMetadata (LLM-extracted fields)
- Pattern: follows src/domain/snapshot.rs (value objects, no dependencies)
- Docsort Module (src/docsort/)
- mod.rs -- Module root with re-exports
- config.rs -- DocsortConfig struct (base_dir, ocr_languages, ollama_url, llm_model, concurrency settings)
- Pattern: follows src/config/config.rs (serde + TOML + defaults + load/save)
- state.rs -- Pure transition(DocumentStage, PipelineEvent) -> Result function
- Pattern: follows src/app/state_machine.rs exactly (exhaustive match, tests)
- watcher.rs -- InboxWatcher using notify crate with stability checks (size+mtime unchanged x3)
- Reuses: notify dependency, pattern from src/config/watcher.rs
- ocr.rs -- OcrmypdfEngine calling ocrmypdf -l deu+eng --sidecar via tokio::process::Command
- llm.rs -- OllamaClient for metadata extraction via HTTP /api/generate (JSON mode)
- Uses: reqwest (new dep), serde_json (existing dep)
- normalize.rs -- Detects file types, converts TIFF/images to PDF
- Reuses: image crate (existing dep)
- filename.rs -- Deterministic filename builder from metadata, sanitization, collision handling (__v2, __v3)
- quality.rs -- QualityReport struct + assess_quality() + clean_text() (whitespace collapse, page separators)
- recovery.rs -- Queries interrupted documents on startup, re-enqueues or fails with RECOVERY_MISSING_ARTIFACT
- pipeline.rs -- Main orchestrator: watcher -> normalize -> OCR -> clean -> LLM extract -> filename -> move
- Bounded concurrency via tokio::sync::Semaphore
- SHA256 dedup before processing
- Atomic file moves (rename within same filesystem)
- Privacy: never logs OCR text or LLM content
- cli.rs -- Clap subcommands: daemon, status, retry, reocr, reextract, gc
- Persistence Layer
- src/persistence/docsort_migrations.rs -- Migration 002: WAL mode, documents table (all columns from REFACTOR.md), embeddings table,
settings table, indices
- Pattern: follows src/persistence/migrations.rs exactly
- src/persistence/docsort_repository.rs -- DocumentRepository trait + SqliteDocumentRepository impl
- Pattern: follows src/persistence/sqlite_repository.rs (r2d2 pooling, params!, error mapping)
- Methods: insert, update_stage, get_by_id, get_by_sha256, list_by_stage, count_by_stage, update_ocr_fields, update_llm_fields, mark_failed
- Binary Entry Point
- src/bin/docsort.rs -- #[tokio::main], clap CLI, structured JSON logging, startup checks (ocrmypdf, ollama, folders), runs pipeline or CLI command
Existing Code Reused βββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ β Component β Reuse β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β rusqlite + r2d2 + r2d2_sqlite β Same connection pooling pattern β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β persistence/migrations.rs pattern β Versioned schema migrations β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β notify crate β Inbox folder watching β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β config/config.rs pattern β TOML config with serde defaults β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β app/state_machine.rs pattern β Pure-function state transitions β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β SnapTextError enum β Extended with new variants β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β tracing β Structured logging β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β tokio β Async runtime β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β serde + serde_json β LLM JSON parsing β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β chrono β Timestamps β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β dirs β Config/data paths β βββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ€ β image β File type detection, TIFF handling β βββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββ New Dependencies βββββββββββ¬ββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ β Dep β Version β Purpose β βββββββββββΌββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ€ β clap β 4 (derive) β CLI subcommands β βββββββββββΌββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ€ β sha2 β 0.10 β SHA256 for duplicate detection β βββββββββββΌββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ€ β hex β 0.4 β Hex encoding of hashes β βββββββββββΌββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ€ β reqwest β 0.12 (json, rustls) β Ollama HTTP client β βββββββββββΌββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ€ β uuid β 1 (v4) β Document ID generation for file moves β βββββββββββΌββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ€ β walkdir β 2 β Directory traversal for GC β βββββββββββ΄ββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββ Pipeline Flow (Phase 1)
inbox/ file detected -> stability check (size+mtime unchanged x3, 2s intervals) -> atomic rename to processing/{uuid}.{ext} -> SHA256 -> check duplicate -> if dup, move to output/_duplicates/ -> INSERT documents (stage=queued) -> normalize to PDF (if TIFF/image, convert; else copy) -> stage=staged -> ocrmypdf -l deu+eng --sidecar -> ocr.pdf + ocr.txt -> stage=ocr_done -> clean_text() -> quality gate (alphabetic ratio, length) -> Ollama /api/generate (JSON mode) -> extract metadata -> stage=extract_done -> build deterministic filename -> sanitize + collision resolve -> atomic move ocr.pdf to output/{final_filename} -> stage=processed -> on failure: bundle artifacts to failed/{id}/, stage=failed
Folder Structure (created on startup)
docsort/ inbox/ # scanner drops files here processing/ # atomic move target (same fs) tmp/ # per-doc artifacts: {doc_id}/... output/ # final OCR PDFs with deterministic names _duplicates/ # by sha256 failed/ # failed/{doc_id}/ with diagnostic bundle db/ docsort.sqlite # WAL mode logs/ # structured JSON tracing
Implementation Order
- Cargo.toml -- add [[bin]] + deps
- src/domain/document.rs + src/domain/mod.rs -- domain types
- src/lib.rs -- add pub mod docsort;, error variants
- src/docsort/mod.rs -- module skeleton
- src/docsort/config.rs -- configuration
- src/docsort/state.rs -- state machine with tests
- src/persistence/docsort_migrations.rs -- DB schema
- src/persistence/docsort_repository.rs + src/persistence/mod.rs -- repository
- src/docsort/quality.rs -- text quality assessment
- src/docsort/normalize.rs -- file type normalization
- src/docsort/ocr.rs -- ocrmypdf adapter
- src/docsort/llm.rs -- Ollama LLM client
- src/docsort/filename.rs -- deterministic naming
- src/docsort/watcher.rs -- inbox watcher
- src/docsort/recovery.rs -- crash recovery
- src/docsort/pipeline.rs -- pipeline orchestrator
- src/docsort/cli.rs -- CLI subcommands
- src/bin/docsort.rs -- entry point
Verification
- cargo build -- both binaries compile
- cargo test -- existing tests still pass + new state machine and migration tests pass
- Create test inbox with a sample PDF: docsort daemon should detect, OCR, and move to output/
- docsort status should show pipeline counts
- Kill and restart daemon mid-processing -- recovery should resume