Release note · August 6, 2026

A batch manifest must not double-count its sources

By SybilGambleyyu · PDFFence 1.27.0

A private batch manifest is a mapping: it says which before source belongs with which after source, while the public report uses only an opaque review ID. That mapping is often edited by hand to pair a rename. A copy-and-paste error can therefore assign one private source to two public IDs and make a handoff appear to contain more independently reviewed documents than it does.

PDFFence 1.27.0 adds a narrow guard for that failure mode: require_unique_document_sources (PFP017).

One source, one review item—unless a reviewer says otherwise

With the conservative policy created by pdffence init, PFP017 fails when an exact private relative manifest path occurs under more than one opaque ID on the same before or after side. Each affected ID gets a high-severity generic finding:

{
  "rule_id": "PFP017",
  "severity": "high",
  "details": {
    "side": "before",
    "declared_document_count": 2
  }
}

That is the entire public evidence. It does not reveal the repeated path, root, filename, file content, or hash. SARIF still has no locations or artifact URIs; it carries only the deliberately public review ID and the same generic detail.

The boundary is deliberately strict and small. PFP017 compares equal manifest paths on the same side. It does not inspect bytes to decide whether files are “really” the same, use filename similarity, guess a rename, or treat hard links and other filesystem aliases as identity. A genuine reviewed one-to-many workflow is allowed: disable this one policy switch explicitly, rather than letting a heuristic decide the mapping.

Duplicated work should not multiply parser work

Repeated source references had a second cost: the same PDF could be parsed once per manifest entry. PDFFence now reuses one validated snapshot for an exact same-side source during a batch invocation. The report remains one result per declared opaque ID, but a duplicated entry cannot turn a 4,096-document manifest limit into 4,096 parses of the same source.

Evidence beyond the unit test

PDFFence 1.27 passed 277 tests, Ruff, bytecode compilation, and dependency consistency checks. The test suite covers before- and after-side repeats, disabled-policy behavior, interaction with PFP016’s added/removed-document gate, JSON/Markdown/SARIF redaction, location-free SARIF, deterministic generic findings, and single-snapshot reuse.

For integration evidence, the 161-entry manifest generated from all PDFCAB 1.24.1 pairs ran with PFP017 enabled and produced 161 changed documents with zero findings. An adversarial manifest then reused one real corpus before path with two distinct after paths. It produced exactly two PFP017 findings, each with only side: before and declared_document_count: 2; the reused path remained absent from the rendered report.

Two fixed-timestamp builds produced byte-identical wheel and source archives, both passed Twine checks, and fresh Python 3.12/3.13 wheel installs plus a fresh Python 3.12 source-archive install repeated the corpus and adversarial cases. The public GitHub assets were re-downloaded, checksum-verified, installed from the published wheel URL, and exercised again. A clean dependency audit found no known vulnerabilities.

What it does not settle

PFP017 catches an exact mapping mistake; it does not prove that a manifest is complete, that differently named files correspond, or that a review decision is correct. PDFFence remains structural, local review tooling: it does not render PDFs, compare visual appearance, validate PDF conformance or signatures, decrypt sources, extract text, execute actions, follow URIs, or detect malware.

The tagged batch-manifest contract, policy reference, threat model, and validation record define the supported boundary.

python -m pip install https://github.com/SybilGambleyyu/pdffence/releases/download/v1.27.0/pdffence-1.27.0-py3-none-any.whl

PDFFence is MIT-licensed and available on GitHub. The release assets include SHA-256 checksums for the wheel and source archive.