The MSRV check gate (rust:1.78-slim build) was already fully
implemented in the initial CI workflow. This verification note
documents the existing implementation and confirms all acceptance
criteria are met.
Acceptance criteria:
- Gate runs in pdftract-ci on every PR: PASS
- Failure blocks PR merge: PASS
- Successful run reports artifact: PASS
- Failure mode produces actionable error: PASS
No changes to the workflow were required.
Related: pdftract-2rf (quality gates coordinator)
Document the implementation of the cargo-audit quality gate with
severity gating and audit.toml allow-list.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Documents the implementation of the clippy quality gate with INV-8
enforcement via clippy::unwrap_used and clippy::expect_used lints.
Bead: pdftract-3cp3a
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add Licensing section to CONTRIBUTING.md explaining:
- Dual MIT OR Apache-2.0 licensing model
- Apache NOTICE file policy (optional for upstream, redistributors MAY add)
- Attribution guidelines for downstream redistributors
Also add verification note confirming all acceptance criteria PASS:
- LICENSE-MIT and LICENSE-APACHE files present at repo root
- All workspace crates declare "MIT OR Apache-2.0" license
- cargo deny check licenses passes (implicit deny-by-default via allow list)
- Binary and wheel distributions configured to include both license files
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
All acceptance criteria verified:
- deny.toml exists with correct configuration
- All cargo-deny checks pass (licenses, advisories, sources)
- CI integration complete (cargo-deny step in pdftract-ci.yaml)
- All ADR exceptions documented (0001, 0002, 0003)
No changes to deny.toml required - existing configuration is correct.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Add dual MIT OR Apache-2.0 licensing at repo root with proper copyright
notices. Configure all workspace and non-workspace crates to declare the
license. Wire license files into Python wheels and Docker images.
Files added:
- LICENSE-MIT: MIT License with "Copyright (c) 2026 Jed Cabanero"
- LICENSE-APACHE: Apache License 2.0 (verbatim from apache.org)
Files modified:
- Cargo.toml: Updated authors to "Jed Cabanero <me@jedcabanero.com>"
- crates/pdftract-py/pyproject.toml: Added license-files to maturin config
- crates/pdftract-cer-diff/Cargo.toml: Added license.workspace = true
- xtask/Cargo.toml: Added license = "MIT OR Apache-2.0"
- fuzz/Cargo.toml: Added license = "MIT OR Apache-2.0"
- Cargo-dist.toml: Created to include license files in binary archives
- notes/pdftract-aawrz.md: Verification note
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add thread sanitizer verification results to notes/pdftract-1eaxm.md
- Improve conformance.c to gracefully handle error JSON responses
- Update test_hash.c to test version and ABI version functions
These changes improve the test coverage and documentation for the
libpdftract C FFI implementation.
Related: pdftract-1eaxm
- Add Homebrew formula template (homebrew-formula.rb.erb)
- Add vcpkg port template with submission instructions
- Add C conformance test (conformance.c) with thread safety verification
- Add simple link test (simple_test.c) to verify library linkage
- Add hash test (test_hash.c) for hash API verification
- Add parse debug test (test_parse.rs) for development
- Add test fixtures (test-minimal.pdf, valid-minimal.pdf)
- Add PROVENANCE.md entry for valid-minimal.pdf
All tests pass: version, abi_version, free(NULL), hash, extract methods.
Co-Authored-By: Claude Code <noreply@anthropic.com>
## Summary of Work Completed
Implemented the libpdftract C FFI library as the fourth workspace member.
All 9 contract methods exposed as extern "C" functions with proper memory
management and thread-safety.
## Acceptance Criteria
- ✅ Fourth workspace member exists with cdylib + staticlib targets
- ✅ Library builds successfully (libpdftract.so + libpdftract.a)
- ✅ Header file exists and is regenerated by cbindgen
- ✅ C program links and calls API successfully (conformance test)
- ✅ Thread-safe (verified with -fsanitize=thread)
- ✅ All 9 contract methods exposed
- ✅ pdftract_free() correctly frees strings (ThreadSanitizer verified)
- ✅ vcpkg port template exists
- ⚠️ Valgrind not available on this system (environment limitation)
- 🔜 Homebrew formula PR automation (deferred to pdftract-libpdftract-build bead)
## Files Created
- crates/pdftract-libpdftract/ (full FFI crate)
- tests/conformance.c (C conformance test)
- distribution/homebrew/pdftract.rb.template
- distribution/vcpkg/*.template
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement the libpdftract native FFI library as a cdylib + staticlib
with cbindgen-generated headers and full extern "C" API.
Components:
- crates/pdftract-libpdftract/ with cdylib + staticlib targets
- All 9 contract methods + utility functions as extern "C"
- cbindgen config and generated pdftract.h header
- pkg-config template (pdftract.pc.in)
- Homebrew formula template (distribution/homebrew/)
- vcpkg port template (distribution/vcpkg/)
- C conformance test (tests/conformance.c)
API features:
- Owned JSON strings returned via CString::into_raw()
- Caller frees with pdftract_free() (not libc free())
- Thread-local error storage (pdftract_last_error)
- Thread-safe and reentrant (no global mutable state)
- ABI version function for compatibility checking
Verification:
- cargo build produces libpdftract.so and libpdftract.a
- Conformance test compiles and runs successfully
- Thread safety verified with 4 concurrent threads
References:
- Plan line 3477: SDK Architecture / The Ten SDKs
- Bead: pdftract-1eaxm
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Updated test_api_null.c to run 10,000 alloc/free cycles (was 100)
- Updated verification note to mark memory roundtrip as PASS
- Improved stream_next implementation to use reference-based approach
instead of Box::from_raw/leak dance for cleaner memory handling
All acceptance criteria for pdftract-5ya9x now PASS:
- 12 exported symbols verified via nm -D
- C client tests (test_api.c, test_api_null.c)
- C++ client test (test_extract.cpp)
- Null pointer safety
- Panic safety (catch_unwind on all entry points)
- Memory roundtrip (10,000 iterations)
- Thread safety (8 pthreads)
Co-Authored-By: Claude Code <noreply@anthropic.com>
Add cbindgen infrastructure to auto-generate C/C++ header from Rust extern
"C" surface at build time.
- Add cbindgen.toml config (C language, include guard, pragma_once, cpp_compat)
- Add build.rs to generate include/pdftract.h during cargo build
- Generated header compiles cleanly with gcc (C) and g++ (C++)
The header is the contract between libpdftract and C/C++ consumers.
Future extern "C" functions will automatically appear in the header.
Refs: pdftract-5rl5o
- Add exit code policy to doctor command help text
- Update --exit-on-fail flag help to clarify default behavior
- Add code comment explaining why --exit-on-fail is a no-op
Exit codes per plan section 6.10:
- Exit 0: all checks OK or WARN (no FAIL)
- Exit 1: at least one check is FAIL
- Exit 2: CLI parse error (clap default)
Closes: pdftract-4sky1
Co-Authored-By: Claude Code <noreply@anthropic.com>
The detail field truncation in human.rs only applied to TTY output,
causing lines to exceed 80 columns when piping to cat or using --no-color.
Fix: Apply truncation uniformly across all output modes:
- TTY mode: Use actual terminal width from terminal_size crate
- Non-TTY/--no-color: Assume 80 columns and truncate accordingly
- Detail field max width: term_width - 38 columns
Max line width now exactly 80 characters for all output modes.
Acceptance criteria verified:
- TTY colored table with summary ✓
- Non-TTY plain text, no ANSI ✓
- --json single JSON object ✓
- --json summary counts ✓
- --features list, exit 0 ✓
- --no-color plain text in TTY ✓
- 80-column terminal width ✓
- N/A excluded from human, in JSON ✓
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Verified all three output formats (colored table, JSON, --features)
work correctly. No code changes required - implementation was
already complete in output/ module.
Acceptance criteria:
- PASS: Default TTY colored table with summary
- PASS: Non-TTY plain text (no ANSI codes when piped)
- PASS: --json output parses correctly with jq
- PASS: --features lists compiled features, exit 0
- PASS: --no-color forces plain text
- PASS: 80-column width compliance
- PASS: N/A rows excluded from human, included in JSON
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Documents the LRU eviction policy implementation with all acceptance
criteria passing (7/7 PASS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Verification note confirming all 18 acceptance criteria PASS for the
cache filesystem layout implementation in commit 624fc49.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Add --receipts CLI flag accepting "off" (default), "lite", or "svg" values.
Thread ExtractionOptions.receipts through all entry points (CLI, PyO3, MCP)
to the extraction pipeline where receipts are generated per span/block.
Changes:
- CLI: Add --receipts flag with value_parser and feature check
- PyO3: Add receipts kwarg with validation
- MCP tools: Add receipts parameter to ExtractArgs/ExtractTextArgs/ExtractMarkdownArgs
- Update extract tests to use ensure_test_pdf() helper
Acceptance criteria:
- CLI validates receipts mode (off/lite/svg)
- SVG mode errors when receipts feature not enabled
- PyO3 extract(path, receipts="lite") works
- MCP tools/call with receipts arg works
- Receipt generation <= 10% overhead for lite, <= 25% for svg
Refs: pdftract-39g4j
Implement the --receipts CLI flag accepting "off" | "lite" | "svg" with default "off".
Thread the ExtractionOptions.receipts field through the extraction pipeline so that
receipts are generated for spans and blocks based on the selected mode.
Changes:
- CLI: Added --receipts flag with clap value_parser for runtime validation
- CLI: Added feature check for SVG mode (requires 'receipts' feature)
- MCP tools: Added receipts field to ExtractArgs, ExtractTextArgs, ExtractMarkdownArgs
- MCP tools: Added build_extraction_options() to parse receipts mode
- Core: Added extract.rs module with extract_pdf(), extract_page(), generate_receipt()
- Core: Added ExtractionOptions with ReceiptsMode enum (Off/Lite/SvgClip)
- Core: Added receipts feature flag to Cargo.toml
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add value_parser = ["off", "lite", "svg"] to --receipts CLI flag for clap validation
- Add receipts field to ExtractTextArgs and ExtractMarkdownArgs in MCP tools args
- Add ExtractionOptions and ReceiptsMode to pdftract-core (options.rs module)
- Expose options module in pdftract-core/lib.rs
The CLI now validates receipts mode at parse time with helpful error messages.
MCP tools accept receipts argument matching the schema defined in sibling 6.7.5.
ExtractionOptions struct provides the threading mechanism for the extraction pipeline.
Acceptance criteria:
- PASS: CLI validates --receipts values (off/lite/svg only)
- PASS: CLI shows proper help text with possible values
- PASS: ExtractionOptions serializes for HTTP/MCP transport
- PASS: MCP tools args have receipts field
- WARN: Full extraction implementation pending (deferred to extraction beads)
Closes pdftract-39g4j
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement the Receipt struct and lite-mode JSON serialization for
visual citation receipts. This provides cryptographic proof of
provenance for extracted text.
Changes:
- Add Receipt struct with 6 fields (pdf_fingerprint, page_index,
bbox, content_hash, extraction_version, svg_clip)
- Implement Receipt::lite() constructor with NFC normalization
- Integrate Receipt into SpanJson and BlockJson schemas
- Add unicode-normalization and serde_json dependencies
Acceptance criteria:
- Receipt::lite() produces valid receipts with svg_clip=None
- Lite mode JSON omits svg_clip key via skip_serializing_if
- Content hash uses NFC normalization for cross-platform stability
- Receipt wired into SpanJson and BlockJson types
Note: 100 receipts aggregate size is ~27 KB (not 15 KB as planned).
The 15 KB target is not achievable with required field sizes.
Refs: pdftract-5zm86, Phase 6.8 Visual Citation Receipts (lines 2351-2417)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bead pdftract-zltqd implements bearer-token authentication for the
MCP HTTP+SSE transport. The implementation was already complete.
This verification note confirms all acceptance criteria are met.
Verification summary:
- Non-loopback binds without token abort with exit code 78
- Env var and token-file auth sources work correctly
- Insecure CLI token requires PDFTRACT_INSECURE_CLI_TOKEN=1
- /health endpoint is auth-exempt (returns 200 without credentials)
- POST requests require valid Authorization: Bearer header
- Constant-time token comparison using subtle crate
- IPv4 and IPv6 loopback addresses are exempt from token requirement
All unit tests pass (90 MCP tests). Manual testing confirms
the plan critical test: "--bind 0.0.0.0:8080 without token
aborts startup; with token, valid requests succeed and
missing tokens get 401"
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The --root DIR flag was already fully implemented in the codebase.
All 25 tests pass (12 unit + 13 integration tests).
Acceptance criteria verified:
- Path traversal rejected with -32602
- Absolute paths rejected when --root is set
- HTTPS URLs bypass the check
- Symlink escapes detected via canonicalize
- Startup validation for root directory
Co-Authored-By: Claude Code <noreply@anthropic.com>
Per ADR-006: stdio and HTTP transports are mutually exclusive because they
have opposite stdout discipline (stdio: JSON-RPC sink; HTTP: log channel).
Changes:
- Add clap ArgGroup with multiple(false) to enforce --stdio XOR --bind
- Default to stdio mode when neither flag is specified
- Change --bind from required String to Option<String>
- Add ADR-006 reference to help text and doc comments
- Add unit tests for CLI argument validation
Acceptance criteria:
- pdftract mcp → launches in stdio mode (default)
- pdftract mcp --stdio → launches in stdio mode
- pdftract mcp --bind ADDR → launches in HTTP+SSE mode
- pdftract mcp --stdio --bind ADDR → exits 2 with clap conflict error
- pdftract mcp --help shows mutual exclusivity note
- Unit test verifies ArgGroup conflict on dual-transport invocation
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the HTTP+SSE transport for the MCP server per bead pdftract-g0ro2.
All acceptance criteria PASS.
Routes:
- POST /: JSON-RPC requests (single or batch)
- GET /sse: Server-Sent Events for notifications
- GET /health: Health check (auth-exempt)
Key features:
- Reuses axum/tokio/tower-http from Phase 6.4 (no new deps)
- Bearer token auth (from sibling bead 6.7.7)
- Request body limit (256 MB default, configurable via --max-upload-mb)
- SSE keepalive every 30 seconds
- Broadcast channel for fan-out notifications
- Backpressure handling (drops lagged clients with WARN log)
- 100-client SSE limit (MAX_SSE_CLIENTS)
- Custom 413 Payload Too Large JSON response
- Batch request support per JSON-RPC 2.0 spec
All 10 integration tests pass:
- test_post_tools_list: POST / returns tool catalog
- test_get_sse_stream: GET /sse opens SSE stream with keepalive
- test_50_concurrent_clients: 50 concurrent clients succeed
- test_health_during_load: GET /health returns 200 under load
- test_post_batch_request: Batch requests return batch responses
- test_post_payload_too_large: POST / over limit returns 413 with JSON body
- test_auth_required_for_non_loopback: Bearer auth returns 401 with WWW-Authenticate
- test_post_single_request_returns_single_response: Single request returns single response
- test_unknown_method: Unknown method returns method_not_found error
- test_get_health: GET /health returns 200 with version info
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the stdio transport for the MCP server, enabling communication
with local agents (Claude Desktop, Claude Code, Continue, Cursor) over
standard input/output with Content-Length framing.
Core features:
- LSP-style Content-Length framing with \r\n terminators
- JSON-RPC 2.0 message parsing and serialization
- INV-9 compliance: stdout contains only JSON-RPC frames
- Panic hook redirects panics to stderr
- SIGTERM handler for graceful shutdown
- Parse errors return -32700 with id: null, then continue
Acceptance criteria:
- ✅ Piping tools/list with framing produces expected response < 50ms
- ✅ EOF on stdin → clean exit within 100ms
- ✅ Malformed JSON → -32700 error, subsequent requests work
- ✅ No println!/log output to stdout (INV-9 enforced)
- ✅ Panics go to stderr, no partial JSON on stdout
- ✅ SIGTERM → exit 0, SIGINT → immediate non-zero exit
Tests added:
- crates/pdftract-cli/tests/mcp-stdio.rs (8 integration tests, all pass)
- All 49 existing unit tests continue to pass
Refs: pdftract-67tm8, plan Phase 6.7.2
Add verification note documenting JSON-RPC 2.0 framing implementation
with all acceptance criteria PASS.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add compute-sha256sums step to pdftract-ci publish-if-tag that produces
an aggregate SHA256SUMS file covering all distributed artifacts: binary
archives, Python wheels, sdist, and CycloneDX SBOM.
Key changes:
- Glob-based artifact collection (tar.gz, zip, whl, cdx.json)
- Deterministic sorting with LC_ALL=C sort -k 2 for reproducibility
- Local verification via sha256sum --check before publishing
- Dynamic artifact upload array instead of hardcoded EXPECTED_ARTIFACTS
- SBOM added as optional input artifact
The SHA256SUMS file format matches GNU coreutils sha256sum output,
enabling one-command verification with cosign verify-blob.
References:
- Plan line 3369: SHA256SUMS aggregate
- Plan line 3419: sign-blob of SHA256SUMS
- Plan line 3460: one cosign verify-blob umbrella
Co-Authored-By: Claude Code <noreply@anthropic.com>
- Wire generate-provenance and verify-provenance steps into workflow DAG
- Update publish-if-tag to upload multiple.intoto.jsonl to GitHub Release
- Fix provenance reproducibility by using SOURCE_DATE_EPOCH from git commit
- Docker images already have cosign attest --type slsaprovenance
Acceptance criteria:
- PASS: generate-provenance step wired into DAG
- PASS: provenance uploaded to GitHub Release
- PASS: Docker image cosign attest already implemented
- WARN: Full slsa-verifier verification requires OIDC issuer registration
- PASS: Provenance is reproducible using git commit timestamp
- PASS: Automated smoke test validates JSON structure
Refs: pdftract-3gk5, plan line 3415 (Signing and Provenance)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add verification note confirming that per-page Resource dictionary
inheritance is complete and all acceptance criteria are met.
The implementation in resources.rs and pages.rs provides:
- Per-namespace merging (Font, XObject, ExtGState, ColorSpace, etc.)
- Per-key last-write-wins semantics
- Arc sharing for memory efficiency when pages lack /Resources
- Support for inline ColorSpace arrays
All 10 resource-related tests pass, including:
- 3-level inheritance test
- Per-key override test
- Arc sharing test
- ColorSpace inline array test
- Empty root /Resources test
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Enhanced the `detect_linearization` function to avoid false matches when
extracting keys from the linearization dictionary. Previous implementation
could incorrectly match "/L" within "/Linearized" or "/H" within other keys.
Changes:
- Added loop-based search in extract_number helper to skip substring matches
- Added similar substring-aware logic for /H (hint stream) parsing
- Added new diagnostic codes for /Prev chain error handling
- Added comprehensive verification note
Acceptance criteria PASS:
- Non-linearized files return None
- Valid linearized dict detected correctly
- File size mismatch (incremental update) invalidates linearization
- No /H entry returns None for hint_stream_offset
- Random bytes never panic (proptest)
- Forward scan disabled for linearized files
- INV-8 maintained (no panics on arbitrary input)
Co-Authored-By: Claude Code <noreply@anthropic.com>
The 512 MiB DEFAULT_MAX_DECOMPRESS_BYTES change was implemented in
commit e94f2ab (fix(bf-49wmw)). This note documents the verification.
Co-Authored-By: Claude Code <noreply@anthropic.com>