Document Extraction Pipeline (Phase 1 fleet)
Shipped: 2026-08-05 (entirely-api 04e0fec + 8397586, migration 033,
deployed; first live batch submitted same session). The fleet stage of the
ML/workflow-intelligence plan, following the passed calibration
(memory project_phase1_extraction_calibration).
Shape
Hourly INTERVAL_TASKS job document_extraction →
services/document_extraction_service.run_extraction_cycle in the worker:
- poll_batches — retrieve every Anthropic Message Batch referenced by
submittedrows; when ended, ingest results intodocument_extractions.extracted(jsonb) orerror. - When nothing is in flight: discover_documents (glob
/data/projects/*/CERTS|PERMITS/*.pdf, paginated known-path diff — NB an IN-filter on hundreds of paths 400s at the PostgREST URL limit) → submit_batch (400 docs; PyMuPDF text, <300 chars →skipped_no_textfor the future OCR queue; claude-haiku-4-5 + structured outputs at 50% batch pricing).
Cycle never raises — a bad hour logs with traceback and self-heals next hour. ~27k text-layer docs drain in a few days at ~$0.0013/doc batch-priced.
Fleet schema (calibration fixes applied)
visit_dates is an ARRAY (multi-visit certs made a scalar ambiguous);
doc_type enum definitions spelled out (cert packages titled “Field Inspection
Certification Report” were misread as field_report); no enum on nullable
unions (API 400).
Table: document_extractions (migration 033)
file_path (unique), project_ss_id (from path), doc_kind, status (pending | submitted | extracted | error | skipped_no_text), batch_id, model, extracted jsonb, error, timestamps.
Known benign race
Running a manual cycle while the scheduler’s own firing is mid-flight can
double-submit the same 400 docs (both select pending before either marks
submitted); happened once at launch — duplicate batch canceled, ~$0.25
waste, rows kept the second batch_id. Steady state is safe (ARQ dedups the
scheduler job). Don’t run manual cycles while the hourly job is live unless
checking status='submitted' is empty first.
Next
Phase B: extend DISCOVERY_ROOTS to the archives (path→project via
project_files linkage) once project folders drain; OCR queue for
skipped_no_text; downstream consumers = permit-timeline intelligence,
cert QA, Supabase gap backfill.
Related: date-parser-consolidation, status-event-log.