Release note · August 6, 2026

A batch report should know less than its manifest

By SybilGambleyyu · PDFFence 1.25.0

A handoff rarely contains one PDF. It contains a release directory, a contract set, an evidence bundle, or a deliverable tree. That creates a tension for a review tool: it needs a way to connect the before and after documents, including a rename, but the connection data itself can be sensitive. A CI artifact that includes customer directory names, document titles, or filenames is often not a safe artifact.

PDFFence 1.25.0 adds pdffence batch, a manifest-driven batch workflow built around that boundary. The manifest knows the private paths. The report does not.

Declare the review set; do not guess it

Folder-comparison tools often infer a match from a relative filename. That is convenient until a document is renamed, moved, intentionally removed, or added. PDFFence instead takes two local roots and a small strict JSON manifest. Every item has a caller-chosen public ID plus independently selected before and after paths:

{
  "version": 1,
  "documents": [
    {
      "id": "review-001",
      "before": "contracts/draft.pdf",
      "after": "contracts/final.pdf"
    },
    {
      "id": "review-002",
      "before": "retired/notice.pdf",
      "after": null
    }
  ]
}

The first entry is an explicit rename. The second is a removed document. An added document uses before: null. PDFFence does not scan a directory, decide that same-looking files correspond, or silently omit an unpaired file. A malformed, missing, unsafe, or symbolic-link source stops the whole batch rather than emitting a partial success.

Only the opaque review label is public

The roots and every manifest path remain local inputs. JSON, Markdown, SARIF, findings, and expected errors contain no root, manifest path, filename, page text, URI, attachment name, or hash. Results are ordered by id and use a compact status:

The ID is deliberately public—SARIF places it in properties.document_id so a finding stays actionable without a file location. That means it should be an opaque label such as review-001, not a customer name or filename. PDFFence constrains it to a small ASCII syntax and documents that opt-in boundary rather than pretending a caller-provided label is secret.

Make a changed document set fail CI

Batch output always records an added or removed item. With a policy, the new high-severity require_same_document_set rule (PFP016) turns each such state into a CI finding. It is enabled by the conservative starter policy:

pdffence init pdffence.yml
pdffence batch baseline/ candidate/ \
  --manifest review.json --policy pdffence.yml --format sarif

Existing pair-level rules remain independent. For example, no_active_content_changes still runs for every compared entry, while PFP016 handles the separate question of whether the declared set itself changed. A normal two-file pdffence check has both inputs by definition, so PFP016 does not alter its behavior.

Evidence across the full public corpus

The release passed 269 tests, Ruff, and bytecode compilation. Its batch tests include an intentionally sensitive marker in both private paths and PDF content, then assert that JSON, Markdown, SARIF, and errors do not reproduce it. They also cover duplicate JSON keys, traversal attempts, renames, additions, removals, output exit status, and symbolic-link roots and sources.

For an independent integration exercise, an opaque-ID manifest paired every one of the 161 PDFs in PDFCAB 1.24.1. The batch report recorded 161 changed documents and no policy findings when run without a policy. For every entry, its public before snapshot, after snapshot, change list, and finding list exactly matched the corresponding standalone pdffence diff --format json result. Fixture roots and baseline/candidate paths were absent from the artifact.

The wheel and source archive built byte-for-byte reproducibly with a fixed timestamp, passed Twine checks, and passed fresh Python 3.12 and 3.13 installs. The public GitHub wheel was then downloaded, checksum-verified, installed through its published URL, and used for the same 161-document batch run.

What batch review does not claim

This is a structural review workflow, not a rendered visual comparison, a directory inventory, malware analysis, PDF conformance check, or signature validation service. A manifest is intentionally a declaration of the set to review; it cannot detect a file that was never declared. PDFFence never renders a page, executes an action, follows a URI, extracts text, decrypts a PDF, or decides whether a signature is valid, trusted, authorized, or safe.

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.25.0/pdffence-1.25.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.