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_extractionservices/document_extraction_service.run_extraction_cycle in the worker:

  1. poll_batches — retrieve every Anthropic Message Batch referenced by submitted rows; when ended, ingest results into document_extractions.extracted (jsonb) or error.
  2. 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_text for 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.