The Dataroom Extractor Learns to Read Executed Paper
A portfolio archive put 45% of its documents in the unknown bucket and classified a cap table as product documentation. A shared text layer, a name-stripping classifier, and the legal extractor that was never built take that to 1%.
new(dataroom): shared document_text layer — one place that turns a file into text
fix(classifier): strip company and firm names before pattern matching
new(extractors): legal_extractor — deal terms from executed financing paper
fix(scanner): report skipped files instead of dropping them silently
fix(analyzer): create a caller-supplied output_dir before writing into it
new(tests): 25 regression tests over the failure modes found in real data
A real portfolio archive — five companies, 81 files, mostly executed
financing documents — was run through the dataroom analyzer as a
baseline. It classified 45% of the corpus as "unknown" and, worse,
was confidently wrong about most of the rest.
The headline defect: a company named "Vantage Products" put the
token `product` into all 43 of its filenames, and the classifier's
substring matching labelled its cap table, its charter, its voting
agreement, and its Keystone side letter alike as `product_documentation`
at 0.83 confidence. The company's own name was the strongest signal in
its own dataroom. Names are now stripped from the matching surface
before any pattern runs.
Three quieter defects sat underneath it. `pypdf` was in
requirements.txt but not in the venv, and every call site wrapped its
import in a bare `except` returning `""` — so a missing library and an
empty PDF were indistinguishable. `CONTENT_SIGNALS` was defined and
never referenced by anything. And `legal_docs` had been a declared
slot in `DataroomAnalysis` since the beginning, initialized to `[]`
and never filled, which is survivable for a company being diligenced
and fatal for a portfolio archive where the executed paper IS the
corpus.
`document_text.py` is now the single path from a file to text —
PyMuPDF then pdfplumber then pypdf for PDFs, python-docx including
table cells, openpyxl, python-pptx with speaker notes, and RFC-822
mail. It fails loudly, caches per process, flags probable scans by
characters-per-page, and falls back to Tesseract OCR when the text
layer comes back empty. It also recovers passwords stated in a
filename, which unlocked a 74-page board deck filed as
`... - pwd VPBODJUNE2025.pdf`.
`legal_extractor.py` reads deal terms regex-first and model-second,
and stores the sentence each figure came from so a reviewer can check
it. The one piece of domain knowledge worth naming: a Y Combinator
SAFE reading `"Discount Rate" is 85%` grants a **15%** discount. The
rate is the fraction of the price paid, not the reduction. Recording
85 there would overstate the discount by more than five times and flow
straight into any conversion math built on it.
Result on the same archive: 1% unknown, 73 of 78 files readable
(up from a pipeline that could not read DOCX at all), 31 legal
documents with extracted terms, and cross-document reconciliation that
surfaced a stale Schedule of Purchasers naming the wrong investor for
a position the executed documents and the cap table both assign
elsewhere.
**Also included:** the README's dependency documentation rebuilt.
Tesseract joins Pandoc, Poppler, and the WeasyPrint stack in a
System Dependencies table that names what each one enables and what
breaks without it. Below it, all 32 direct Python dependencies are
documented with their floor, their verified version, and what they
are for, plus the two optional packages and the four dev packages —
and a note on why `pyproject.toml` and `requirements.txt` both exist
and answer different questions.
Documenting them surfaced drift. `anthropic` is imported directly by
15 modules but was declared nowhere; it arrived transitively through
`langchain-anthropic`, so a langchain upgrade could have broken every
extractor at import time. `pillow` was the same story via `pdf2image`.
Both are now declared. `pypdf` went the other way: `document_scanner`
and `competitive_extractor` still imported it inside bare excepts, so
with it uninstalled the inventory reported zero total pages and the
competitive extractor reported "no text extracted" for readable PDFs.
Both now read through the shared layer, which leaves `pypdf` a genuine
optional third fallback — documented as such in both files.
The "scanned images won't work" constraint is corrected too: still
true of the deck path, no longer true of the dataroom path.
**What's next:** archive expansion (a nested .zip is currently
reported rather than walked), Apple iWork bundles, and wiring the
reconciled legal terms into the cap-table extractor so SAFEs and notes
convert against a real share count.