Skip to content

Build stranske-pdf-extract: single-source PDF-extraction library (contract + Protocols + fallback + reliability + Docling) in packages/ #2711

Description

@stranske

Why

PDF text-EXTRACTION is independently reimplemented in four fleet repos with four divergent
result contracts and three OCR strategies (verified by reading source, 2026-06-28, and confirmed by a
fresh clone-grep across all branches — exactly four, none missed):

  • Counter_Risk src/counter_risk/parsers/daily_holdings_pdf.py:81 (_extract_text ladder pdfplumber→pypdf→OCR).
  • Pension-Data src/pension_data/parser/pdf_pipeline.py:455 (parse_pdf_to_funded_input) + src/pension_data/extract/orchestration/fallback.py:53 (run_fallback_chain) + db/models/provenance.py.
  • Inv-Man-Intake src/inv_man_intake/extraction/providers/base.py:204 (ExtractionProvider Protocol; :218 MultiModalExtractionProvider) — current pdf_primary.py extractor is fixture-grade, not real.
  • Manager-Database utils/extract.py:20 (_extract_pdf, pdfplumber→str).

Missing/duplicated behavior: items 1–4 of the shape (text-extraction ladder, OCR fallback, orchestration,
result/provenance contract) are rebuilt per repo; a reliability layer (arithmetic/business-rule validation +
cross-check + calibrated confidence) is absent in all four. This is latent fragility + duplicated effort,
not a current break. Grounding: Code/Audits/2026-06-28-fleet-pdf-extraction-survey.md and …-methodology.md.
A validated scaffold (this design) already exists at packages/stranske_pdf_extract/ with 27 passing
deterministic tests; this issue tracks landing it on main and finishing the optional-dep paths.

Scope

Land the single-source library stranske-pdf-extract as a pip-installable subdirectory package in
Workflows at packages/stranske_pdf_extract/ (distribution decision: package, NOT sync-manifest copy-sync —
see packages/stranske_pdf_extract/docs/DESIGN.md §2). The package owns: the generalized result + page-level
provenance contract (contract.py, generalizing Pension-Data + Inv-Man-Intake), the provider Protocols + OCR
seam + registry (provider.py), the fallback-ladder primitive (orchestration.py, lifted from Pension-Data),
the greenfield reliability layer (reliability.py), one REAL extractor behind the Protocol
(providers/docling_provider.py, Docling/MIT/local, optional [docling] extra), a pure-python baseline
(providers/text_baseline.py), and the golden-set eval harness (eval/harness.py). Tag pdf-extract-v0.1.0.

Non-Goals

  • Do NOT add the package to .github/sync-manifest.yml — distribution is pip, not copy-sync. The only
    sync/doc touch is a note that the package exists and is pip-installed (so a future audit does not "fix" its absence).
  • Do NOT run Docling or OCR in any stlite/Pyodide browser path; keep them optional extras never imported there.
  • Do NOT migrate any consumer in this issue — migrations are separate, dependency-ordered tracking issues.
  • Do NOT learn any consumer's domain schema; domain field-parsing stays in each consumer.
  • Scaffold-only completion does NOT count: landing the tree with the named conformance gate collecting 0
    tests, or the deliberate-break below not demonstrated, is a failure of this issue.

Tasks

  • Land packages/stranske_pdf_extract/ on main (the validated scaffold): pyproject.toml (extras
    baseline,docling,ocr,textract,schema,eval), src/stranske_pdf_extract/{contract,provider,orchestration,reliability}.py,
    providers/{docling_provider,text_baseline}.py, eval/harness.py, docs/DESIGN.md, README.md, and tests/.
  • Wire the package's tests/ into Workflows CI (a non-default job, e.g. extend .github/workflows/selftest-ci.yml
    or add a packages-pdf-extract job) running PYTHONPATH=packages/stranske_pdf_extract/src python -m pytest packages/stranske_pdf_extract/tests.
  • Tag the release pdf-extract-v0.1.0 and document the install URL
    git+https://github.com/stranske/Workflows@pdf-extract-v0.1.0#subdirectory=packages/stranske_pdf_extract in README.md.
  • Complete the Docling real path (providers/docling_provider.py:_extract_real) behind the [docling] extra and
    add an opt-in test that runs only when docling_available() is true (skips cleanly otherwise).
  • Perform the deliberate-break verification (see Acceptance Criteria), capture the FAIL output, then revert.

Acceptance Criteria

  • Named test gate: packages/stranske_pdf_extract/tests/test_docling_provider.py::test_docling_provider_conforms_to_protocol
    passes in CI (asserts isinstance(DoclingProvider(), MultiModalExtractionProvider)), AND the full package suite
    collects a non-zero count and passes (confirmed-green locally: 27 passed).
  • Deliberate-break gate: temporarily comment out the name = "docling" attribute in
    src/stranske_pdf_extract/providers/docling_provider.py (class DoclingProvider). With this change,
    test_docling_provider.py::test_docling_provider_conforms_to_protocol must FAIL with
    assert isinstance(...) == False. Revert and confirm it passes. (Demonstrated locally during scaffolding.)
  • pip install "git+…#subdirectory=packages/stranske_pdf_extract" imports stranske_pdf_extract with the
    core (no extras) and import of providers does not require docling (only extract_modalities does).

Implementation Notes

  • Confirmed-green local reproduction: cd packages/stranske_pdf_extract && PYTHONPATH=src python -m pytest -q27 passed.
  • Core has zero runtime deps by design; native deps (Docling/Tesseract) are extras so confidentiality-sensitive
    consumers install only what they need.
  • Contract merge rule and migration order are in packages/stranske_pdf_extract/docs/DESIGN.md.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions