feat(extract): Door B extraction registry (Phase 2 step 1)

First, guard-independent step of Phase 2: a stdlib-only registry mapping a
dropped file's extension to its text extractor, with the fail-fast gates that
keep binary parsing out of core. `extract_text(filename, data)` dispatches
(case-insensitively) to:

- `md`/`txt` — utf-8-sig passthrough (BOM never leaks, baseline parity with
  Door A's read_csv);
- `csv` — the Phase 1 `render_table` (renderer reused, not duplicated);
- `json` — verbatim inside `render_fenced_block`;
- `html`/`htm` — text via `html.parser`, `script`/`style` stripped, tags as
  word boundaries (spec B3: adequate for v1, richer is out of scope).

`pdf`/`docx`/`xlsx` are `[extract]`-gated; until that extra ships a parser they
fail fast with a typed error naming the extra — never a silent skip, never a
bundled parser in core. New `ExtractionError(IngestError)` carries four stable
codes (`extractor_unknown`, `extractor_extra_missing`, `extractor_decode_error`,
`extractor_empty_csv`); a non-UTF-8 file is a typed corrupt-input failure, never
a leaked UnicodeDecodeError. `extract_text` returns text content only — LF
framing and concept frontmatter are the materializer's job (step 2).

No runtime dependency and no guard call yet (the guard pin and 0.4.0 land with
the persist gate in steps 4–5). TDD: test_extract.py + the four codes in the
test_error_codes.py registry precede the implementation; mypy --strict, ruff,
and the `sanitize|quarantine|lexicon` boundary grep-gate all clean; the Phase 1
golden suite still passes byte-for-byte.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBbjgS5A55RVavoyjJC4FX
This commit is contained in:
Kjell Tore Guttormsen 2026-07-24 20:18:23 +02:00
commit db93de4aef
5 changed files with 301 additions and 0 deletions

View file

@ -72,6 +72,23 @@ class SourceError(IngestError):
"""
class ExtractionError(IngestError):
"""A dropped file could not be converted to text (Door B, Phase 2).
File-type -> text extraction is text-only plumbing; a corrupt file, an
unknown type, or a binary type without its optional parser is a typed
per-file failure never a silent skip, never a leaked stdlib error, and
never a bundled parser in core.
Codes:
- `extractor_unknown` no extractor is registered for the file extension
- `extractor_extra_missing` a `[extract]`-gated binary type (pdf/docx/
xlsx) was given but the optional extra is not installed
- `extractor_decode_error` a text-type file's bytes are not valid UTF-8
- `extractor_empty_csv` a CSV has no header row
"""
class MaterializationError(IngestError):
"""Materialization refused or failed (ingest-spec §5).