A second independent review read `230d1cb` -- the commit that closed the `v0.10.0` review's two MAJOR findings -- and found one of them open. The bound read `/Width` and `/Height`, which an untrusted document writes, while `get_data()` pays for the stream beside them; `/Length` is the COMPRESSED length and the two numbers are independent. Re-measured here on `ed8d9d7` before anything changed, in its own interpreter: a 408 516-byte PDF declaring 1x1 and carrying 400 MB of deflated zeros was CARRIED, no rejection, 891 904 000 B peak RSS. After: 0 carried, `asset_too_large`, 57 065 472 B. At 1,2 GB of zeros, 2 436 MB -> 64 569 344 B -- the cost no longer scales with the bomb. End to end through the CLI with the shipped defaults: 838 000 640 B and an asset written -> exit 0, 79 650 816 B, `0 carried of 1 found`, no `assets/`. Three numbers are bounded now, not one: what a container DECLARES, what a carried FILE measures (`read_image`, so a 49 MP PNG of 47 705 bytes is not passed on to a consumer), and what a PDF stream DECOMPRESSES to (`assets.inflated_size`, a chunk at a time, output discarded, before `get_data()`). The limit is stated rather than implied: the stream measurement runs where `FlateDecode` is the first filter and the document is not encrypted; every other chain is a check on the decoded length AFTER the decode, a counted refusal and not a bounded one. A non-positive declared dimension is `asset_size_invalid`, its own code, raised before the stream is read. `-1 x 40000000000` is a NEGATIVE pixel count, under which every `>` bound read as satisfied, so the check returned silently and the refusal arrived from `encode_png` as `asset_samples_invalid`. Its own code because a publisher shipping a picture bigger than this package carries and a dictionary written to be read wrong are different facts about a document. Two smaller findings in the line that says what is missing, both introduced by the first fix: the address was written twice, once bare, and a linkifying renderer autolinks a bare URL -- written once now, in one code span; and `label` became a dead parameter, so the figure's caption was dropped, a regression against 0.10.0. It is written again in the `-- <label>` form a carried pointer uses. Version bumped to 0.10.1 across all ten places. Nine were unbound and stale: four README install lines naming the previous release, two prose lines, the "current tag" entry, `uv.lock`, and a CHANGELOG whose 0.10.1 content sat under `[Unreleased]`. Two new packaging tests bind them to `__version__`, and the README's guard tag to `[tool.uv.sources]`. Every test was red first. The fate of every image is identical with and without the new bound on three K2 PDFs carrying 800 images (464/464, 311/311 with the same 12 rejections, 25/25), and the second inflate is below the noise floor there. 0 shipped artifacts move: no bundle under `examples/`, `skills/` or `tests/fixtures/` carries an image pointer at all, measured against a known-positive control. `asset_too_large` was undocumented in the error registry; both codes are there now. `tools/okf_accounting_gate.py` gains the new code in its closed list -- one string, no behaviour change, stated because that file belongs to another order. Suite 2141 passed / 1 skipped, ruff + format + mypy --strict clean. Report: docs/2026-09-18-bildestien-holder-0-10-1.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
177 lines
8.5 KiB
Python
177 lines
8.5 KiB
Python
"""Packaging contract: a PEP 561 typed package with exactly one dependency.
|
|
|
|
Consumers run mypy --strict against the inline annotations; without the
|
|
py.typed marker mypy degrades every imported symbol to Any.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import llm_ingestion_okf
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def test_package_ships_py_typed_marker() -> None:
|
|
package_dir = Path(llm_ingestion_okf.__file__).parent
|
|
assert (package_dir / "py.typed").is_file()
|
|
|
|
|
|
def test_the_only_runtime_dependency_is_the_security_boundary() -> None:
|
|
"""The stdlib-only rule, enforced rather than asserted in prose.
|
|
|
|
One dependency is permitted — the guard — because security is the one
|
|
thing this library must not implement. Everything else stays stdlib, so
|
|
a consumer vendoring this package takes on no transitive surface. The
|
|
version RANGE is the pin: it resolves against a package index, and is
|
|
satisfied by the git+https tag install until that index exists.
|
|
"""
|
|
tomllib = pytest.importorskip("tomllib") # stdlib from 3.11; the pin holds on 3.10 too
|
|
pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
|
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_ocr_group_is_pinned_and_is_not_a_runtime_dependency() -> None:
|
|
"""An inference runtime is the last thing that may arrive by accident.
|
|
|
|
Two claims, and the second is the one worth a test: the group's contents
|
|
are pinned like the extra's, AND none of them appears in
|
|
`project.dependencies`. The single-dependency test above would already
|
|
catch that, but it reads the list and this reads the names -- so a future
|
|
entry named differently still fails here.
|
|
"""
|
|
tomllib = pytest.importorskip("tomllib")
|
|
pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
|
assert pyproject["project"]["optional-dependencies"]["ocr"] == [
|
|
"rapidocr>=3.9,<4",
|
|
"onnxruntime>=1.20,<2",
|
|
"pypdfium2>=4,<6",
|
|
]
|
|
runtime = " ".join(pyproject["project"]["dependencies"])
|
|
for package in ("rapidocr", "onnxruntime", "pypdfium2"):
|
|
assert package not in runtime
|
|
|
|
|
|
def test_the_declared_version_agrees_with_the_packaged_one() -> None:
|
|
"""The two places a version is written must not drift apart.
|
|
|
|
The install channel is a direct git reference, so a consumer pins a TAG
|
|
while pip records `project.version`. Nothing in the run path reads
|
|
`__version__` — which is exactly why a stale one survives a green suite,
|
|
and why a consumer installing at a pre-release tag can end up with a
|
|
package that reports the previous release. This is the only machine check
|
|
on that; the tag name itself remains a human step.
|
|
"""
|
|
tomllib = pytest.importorskip("tomllib")
|
|
pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
|
assert llm_ingestion_okf.__version__ == pyproject["project"]["version"]
|
|
|
|
|
|
def test_operational_tooling_stays_out_of_the_wheel() -> None:
|
|
"""`tools/` is ours, not the consumer's.
|
|
|
|
The upstream watch drives git and the coord mailbox — machinery that is
|
|
meaningful on this machine and meaningless in a consumer's site-packages.
|
|
It lives outside `src/` so it cannot ship, and this test is what makes
|
|
that a promise instead of an accident of the current build config.
|
|
"""
|
|
tomllib = pytest.importorskip("tomllib")
|
|
pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
|
packages = pyproject["tool"]["hatch"]["build"]["targets"]["wheel"]["packages"]
|
|
assert packages == ["src/llm_ingestion_okf"]
|
|
assert (PROJECT_ROOT / "tools" / "okf_watch.py").is_file(), "the test must have a subject"
|
|
|
|
|
|
def test_every_place_that_publishes_a_version_names_the_packaged_one() -> None:
|
|
"""A tag is a promise about bytes, and nine places here repeat it.
|
|
|
|
`test_the_declared_version_agrees_with_the_packaged_one` binds two of
|
|
them. An independent review of 0.10.1 found the other seven unbound and
|
|
all of them stale: four README install lines telling a consumer to install
|
|
`@v0.10.0`, two prose lines about what that tag declares, the "current tag"
|
|
entry, and a CHANGELOG whose 0.10.1 content sat under `[Unreleased]`. A
|
|
`v0.10.1` tag cut from that tree would have shipped a package reporting
|
|
0.10.0 and a README installing the release before it -- and the suite was
|
|
green, because nothing looked.
|
|
|
|
The guard tag is bound the same way and for the same reason: the README
|
|
tells a plain-pip user to install a specific guard tag first, and that
|
|
instruction is wrong the moment `[tool.uv.sources]` moves without it.
|
|
"""
|
|
tomllib = pytest.importorskip("tomllib")
|
|
pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
|
version = llm_ingestion_okf.__version__
|
|
guard = pyproject["tool"]["uv"]["sources"]["llm-ingestion-guard"]["tag"]
|
|
readme = (PROJECT_ROOT / "README.md").read_text(encoding="utf-8")
|
|
|
|
install = re.findall(r"llm-ingestion-okf\.git@(v[0-9][^\"\s]*)", readme)
|
|
assert install, "the test must have a subject"
|
|
assert set(install) == {f"v{version}"}, f"install lines name {sorted(set(install))}"
|
|
|
|
guard_lines = re.findall(r"llm-ingestion-pipeline-security\.git@(v[0-9][^\"\s]*)", readme)
|
|
assert guard_lines, "the test must have a subject"
|
|
assert set(guard_lines) == {guard}, f"guard install lines name {sorted(set(guard_lines))}"
|
|
|
|
current = re.search(r"^- `(v[^`]+)` — the current tag", readme, re.MULTILINE)
|
|
assert current is not None, "the test must have a subject"
|
|
assert current.group(1) == f"v{version}"
|
|
|
|
# The prose between "## Install in detail" and the history list explains
|
|
# what THIS tag declares and which guard tag it is paired to. A stale
|
|
# number there is an instruction that fails, not a historical note.
|
|
detail = readme.split("## Install in detail", 1)[1].split("### Earlier tags, as history", 1)[0]
|
|
named = set(re.findall(r"`(v\d+\.\d+\.\d+[^`]*)`", detail))
|
|
assert named, "the test must have a subject"
|
|
assert named <= {f"v{version}", guard}, f"stale tags in the install prose: {sorted(named)}"
|
|
|
|
# The tenth place, which uv rewrites on its own and which is therefore the
|
|
# easiest of all to commit stale.
|
|
lock = (PROJECT_ROOT / "uv.lock").read_text(encoding="utf-8")
|
|
locked = re.search(r'name = "llm-ingestion-okf"\nversion = "([^"]+)"', lock)
|
|
assert locked is not None, "the test must have a subject"
|
|
assert locked.group(1) == version
|
|
|
|
|
|
def test_the_changelog_heads_with_the_packaged_version() -> None:
|
|
"""The release notes for the version being shipped are not `[Unreleased]`.
|
|
|
|
`[Unreleased]` is the right place for work in flight and the wrong place
|
|
for the content of a tag someone is about to cut: a reader arriving at
|
|
`v0.10.1` would find its own entry under a heading saying it had not been
|
|
released. Found by an independent review of 0.10.1.
|
|
"""
|
|
changelog = (PROJECT_ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
|
heading = re.search(r"^## \[([^\]]+)\](?: — (\d{4}-\d{2}-\d{2}))?$", changelog, re.MULTILINE)
|
|
assert heading is not None, "the test must have a subject"
|
|
assert heading.group(1) == llm_ingestion_okf.__version__, (
|
|
f"the changelog heads with [{heading.group(1)}], the package is "
|
|
f"{llm_ingestion_okf.__version__}"
|
|
)
|
|
assert heading.group(2), "a released section carries its date"
|