build(extract): vendor the converter binary as a pinned wheel

`pypandoc-binary==1.17` joins the `[extract]` extra, and the extra's contents
are now pinned by a test -- `test_packaging.py` asserted `project.dependencies`
only, so a second package could have arrived in the extra unnoticed, which is
precisely where an unexamined transitive tree shows up.

WHY VENDORED RATHER THAN FOUND ON PATH: the xlsx and pptx readers exist only
from pandoc 3.8.3. Debian 12 ships 2.17.1.1 and Ubuntu 24.04 ships 3.1.3, so a
PATH binary cannot deliver two of the five office formats on current stable
distributions. The pin is exact rather than a range for the same reason
pdfminer.six's is: extraction is deterministic within a converter version and
not across one.

The single-runtime-dependency rule is untouched -- it governs
`project.dependencies`, which still names the guard alone.

Measured after installing, on this host:

  bundled binary   pandoc 3.9   (inside the wheel, as intended)
  pypandoc picks   3.10.2       (the host's PATH binary)

That is the third independent measurement of the trap: pypandoc searches PATH
before its own bundled binary and takes the highest version it finds, so
"vendored" buys nothing until something resolves the path explicitly. The
resolver is the next step; until it lands, the vendoring is a pin without an
effect and should not be described as more than that.

Suite 886 -> 887 (the extra is installed, so the packaging pin runs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-02 14:03:23 +02:00
commit fbd2f3fde5
2 changed files with 46 additions and 4 deletions

View file

@ -34,6 +34,29 @@ def test_the_only_runtime_dependency_is_the_security_boundary() -> None:
assert pyproject["project"]["dependencies"] == ["llm-ingestion-guard>=1.2,<2.0"]
def test_the_extract_extra_pins_exactly_what_it_ships() -> None:
"""`project.dependencies` was pinned; the extra's contents were not.
The single-dependency test above reads `project.dependencies` only, so a
second package could be added to `[extract]` and no test would notice --
and an extra is exactly where an unexamined transitive tree arrives. The
extra is opt-in, but "opt-in" is a statement about who installs it, not
about whether its contents were chosen.
Both entries are pins with a stated reason, not conveniences:
`pdfplumber` for the pdf reader, `pypandoc-binary` because the converter
BINARY travels with the wheel. Vendoring the binary is what makes the
output reproducible -- see the resolver, which refuses any version but the
pinned one.
"""
tomllib = pytest.importorskip("tomllib")
pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
assert pyproject["project"]["optional-dependencies"]["extract"] == [
"pdfplumber>=0.11.10,<0.12",
"pypandoc-binary==1.17",
]
def test_the_declared_version_agrees_with_the_packaged_one() -> None:
"""The two places a version is written must not drift apart.