# rusty-pdfgrep — v0.2.0 Feature Layout
**Status**: implementation draft for the v0.2.0 Cargo features convention
backfill (spec 00011, Phase 10 — rusty-pdfgrep).
**Authority**:
- `specs/adrs/0006-cargo-features-convention-for-portfolio-ports.md` (why)
- `project-instructions.md` §Cargo Feature Surface (what)
- This document — the per-port carving + WHY for each leaf, per HINT-003
+ HINT-009 of spec 00011.
**Reference port**: rusty-figlet v0.2.0 — see `../../rusty-figlet/docs/feature-layout.md`
(FROZEN reference port) for the format anchor. rusty-pdfgrep conforms to the
same shape with the minimum-convention surface dictated by its
tightly-coupled-capability scope. The companion sibling ports rusty-sponge
v0.2.0, rusty-vipe v0.2.0, rusty-pee v0.2.0, rusty-pwgen v0.2.0, rusty-detox
v0.2.0, and rusty-pv v0.2.0 (see `../../rusty-pv/docs/feature-layout.md`)
established the zero-leaf precedent for ports whose entire surface lives
behind a single CLI umbrella.
**Iteration model**: v0.2.0 is a **purely additive** SemVer-minor release.
Every v0.1.x feature name and composition is preserved verbatim; new
umbrellas (`full`, `pdfgrep-classic`, `pdfgrep-minimal`) are layered on top
without renaming or narrowing the existing `cli` / `default` features.
Library and binary API surfaces are unchanged.
## Tightly-coupled-capability port — spec 00011 §Scope Edge Cases
rusty-pdfgrep is explicitly called out in spec 00011 §Scope Edge Cases:
> A port has many tightly-coupled capabilities (e.g., rusty-pdfgrep's
> regex + recursive walking + encryption) — leaves can be coarse-grained
> when finer splits would be artificial. Pragmatism over purity.
The documented capability is "grep through PDF text content" — a Rust port
of Hans-Peter Deifel's `pdfgrep(1)` v2.2.0. The implementation requires:
1. **PDF text extraction** (`lopdf`) — foundational, always-on. Every
match the tool reports comes from this pipeline.
2. **Pattern matching engines** (`regex` + `fancy-regex`) — foundational,
always-on. The library API (`PdfGrep`, `PdfGrepBuilder`) selects
between RE2 and PCRE via the `perl_regexp` builder flag at runtime.
3. **CLI surface** (`clap` + `clap_complete` + `anyhow` + `termcolor` +
`anstyle` + `walkdir` + `globset`) — already gated by the v0.1.x
`cli` umbrella.
Carving the CLI sub-capabilities (recursive walking, GREP_COLORS, encrypted
PDF retry, etc.) into separate leaves would either (a) violate the
additive-v0.2.0 contract by narrowing the existing `cli` composition, or
(b) introduce artificial splits that no consumer benefits from since the
binary always-on flag set ships all of them together. Per the spec
guidance above and the established sibling-port precedent (rusty-sponge,
rusty-vipe, rusty-pee, rusty-pwgen, rusty-detox, rusty-pv), rusty-pdfgrep
adopts the zero-leaf path with the minimum convention surface.
## Source-tree walk
`src/` modules (v0.1.0, post-Phase-1 baseline):
| `error.rs` | yes | (thiserror — always-on) | `PdfGrepError` enum; library + binary need it. |
| `engine.rs` | yes | (regex + fancy-regex — always-on) | Pluggable regex engine (`Engine::Regex` / `Engine::Fancy`). |
| `pdf.rs` | yes | (lopdf — always-on) | PDF text extraction with per-page panic boundary. |
| `lib.rs` | yes | none | Public API (`PdfGrep`, `PdfGrepBuilder`, `Match`, `PdfGrepError`, `PageIterator`). |
| `main.rs` | no — `cli` | clap, clap_complete, anyhow, termcolor, anstyle, walkdir, globset | Binary entry; gated by `required-features = ["cli"]`. |
## Leaf-carving criteria (HINT-009)
A capability becomes a leaf when ALL of the following hold:
1. It is **self-containable** — gated cleanly via `#[cfg(feature = "<leaf>")]`
at the module or top-level item boundary (HINT-004).
2. Either (a) it has a **sole optional dependency** that no other leaf needs
(HINT-005), OR (b) it is a pure-cfg-gate of an internal module worth
exposing as a knob.
3. Disabling it does NOT break any always-on library/CLI surface.
A capability does NOT become a leaf when:
- It is foundational (the `lopdf` extractor, `regex` / `fancy-regex`
engines, `Match` / `PdfGrep` / `PdfGrepBuilder` API surface — each is
part of the always-on library contract).
- It is part of the tightly-coupled CLI bundle already gated by the v0.1.x
`cli` umbrella (recursive walking, GREP_COLORS color output, encrypted
PDF retry, glob include/exclude, Strict-mode parser, completions
subcommand).
- Carving it would require splitting an existing v0.1.x dep out of the
`cli` umbrella (violates additive-v0.2.0 contract per spec 00011
Phase 4-9 precedent).
## v0.2.0 Carved Leaves
**ZERO new leaves carved at v0.2.0**. Every capability inside rusty-pdfgrep
is either:
1. Foundational always-on library code (PDF extraction, regex engines,
`PdfGrepBuilder` / `PdfGrep` / `Match` / `PageIterator`, `PdfGrepError`
types) — cannot be stripped without breaking the public library surface.
2. Already gated by the v0.1.x `cli` umbrella (clap-derived argument
parsing, completions subcommand, recursive walker, color writer,
include/exclude glob filters, Strict-mode argv pre-scanner).
### Leaves intentionally NOT carved
The following candidate leaves were considered + rejected:
- **`pdf-encryption`**: Encrypted PDF retry lives in `pdf::PdfDocument::open`,
which is part of the always-on library surface. The `--password` CLI
flag dispatches through `PdfGrepBuilder::password()`, which would still
need to compile even if the encryption-retry path were cfg-gated. The
underlying `lopdf::Document::decrypt()` is always linked because
`lopdf` is a foundational dep. Carving this as a leaf would add cfg
scaffolding with zero observable benefit. Rejected per HINT-009
criterion 3.
- **`walk-recursive`**: The `-r`/`--recursive` walker uses `walkdir` +
`globset`, both of which are CLI-only deps already in the v0.1.x `cli`
umbrella. Carving this out would require splitting `walkdir` and
`globset` away from `cli` and renaming the existing `cli` feature —
violates the additive-v0.2.0 contract per spec 00011 §Brownfield Notes.
The capability survives untouched inside the existing `cli` composition.
- **`regex-pcre`**: The `-P`/`--perl-regexp` engine selector dispatches
to `fancy_regex::Regex` via `engine::compile`. Both `regex` and
`fancy-regex` are part of the always-on library API surface — the
`Engine::Fancy` enum variant is exposed in the public type signature
and removing `fancy-regex` would break the library contract. The
runtime cost of compiling but not using `fancy-regex` is negligible
(the unused engine is dead-code-eliminated by LLVM/LTO). Rejected per
HINT-009 criterion 3.
- **`color` (GREP_COLORS / `--color=auto|always|never`)**: The color
writer is currently always-on in `main.rs` via `termcolor` and
`anstyle`, both of which are CLI-only deps in the v0.1.x `cli`
umbrella. Carving this out would require splitting `termcolor` and
`anstyle` away from `cli` and renaming — violates the additive
contract. The capability survives untouched inside `cli`.
- **`strict-compat`**: rusty-pdfgrep's Strict mode dispatches inline in
`main.rs` via `resolve_strict_mode` and the hand-rolled getopt mirror
in `run_strict`. Both are gated by the `cli` umbrella in v0.1.x.
Carving out a separate `strict-compat` leaf would require splitting
the inline dispatcher away from `main.rs` and renaming — violates the
additive contract. The capability survives untouched inside `cli`.
(Note: rusty-figlet carves `strict-compat` because its Strict-mode
parser is dep-free hand-rolled getopt in a dedicated module;
rusty-pdfgrep's Strict dispatcher is inline in `main.rs` and shares
the `clap`-pulled argv with the Default-mode parser, so it cannot
stand alone without `cli`.)
- **`completions`**: Could be carved as `["dep:clap_complete"]`, but
`clap_complete` is bundled into the v0.1.x `cli` umbrella verbatim.
Carving it would either rename `cli` (breaking SemVer additivity) or
duplicate the surface.
- **`glob-include-exclude`**: The `--include` / `--exclude` glob filters
use `globset`, which is already in the v0.1.x `cli` umbrella. Same
rejection reasoning as `walk-recursive`.
- **`stdin-cap`**: The `--max-stdin-bytes` cap lives in `main.rs::read_stdin_capped`
and is a single function with no external dep. Not worth a leaf —
pure runtime knob.
## Preset bundles (FR-007 — 2 required for tightly-coupled-capability ports)
Per spec 00011 §Scope Edge Cases + FR-007, even tightly-coupled-capability
ports declare 2 preset bundles to give the keep-list workaround
documentation something concrete to point at.
### `pdfgrep-classic` (REQUIRED — bare port, 1:1 with upstream pdfgrep 2.2.0)
```toml
pdfgrep-classic = ["cli"]
```
- Includes `cli` so the binary exists.
- Tightly-coupled-capability port; the `cli` umbrella IS the bare-port
surface (recursive walking, GREP_COLORS, encrypted PDF retry,
include/exclude globs, Strict mode are all in `cli`).
- Use case: `cargo install rusty-pdfgrep --no-default-features --features pdfgrep-classic`
for a pdfgrep 2.2.0 drop-in replacement (Strict mode is invoked via
the `--strict` flag, `RUSTY_PDFGREP_STRICT` env var, or
`pdfgrep` / `pdfgrep-alias` argv[0] auto-detect — none of these
require additional features).
### `pdfgrep-minimal`
```toml
pdfgrep-minimal = ["cli"]
```
- Same composition as `pdfgrep-classic` (tightly-coupled-capability
port has no smaller subset to carve).
- Use case: explicit "minimal CLI install" alias for users who prefer
the `<port>-minimal` naming convention seen across other Rusty ports
(figlet-minimal, ts-minimal, sponge-minimal, vipe-minimal,
pee-minimal, pwgen-minimal, detox-minimal, pv-minimal).
Documented as an intentional semantic alias rather than a distinct
composition.
## Cross-port glossary candidates
No leaves carved → no cross-port glossary contributions from rusty-pdfgrep
in this iteration. If a future minor release adds an orthogonal capability
(e.g., a `pdf-extract-backend` leaf swapping in `pdf-extract` or
`pdfium-render` as an alternative extractor, an `output-json` leaf
emitting JSON match records for tooling integration, or a `cache` leaf
adding the deferred upstream `--cache` capability), the leaf would be a
candidate for promotion to `docs/feature-vocabulary.md` per FR-053.
## CI matrix shape (FR-010..FR-014)
Per plan §Per-Port v0.2.0 CI Matrix, scaled to a zero-leaf port:
- **Tier 1 — `test-default`**: full DDR-003 cross-compile matrix
(5 targets). Post-v0.2.0 `default = ["full"]` and `full = ["cli"]`,
so the kitchen-sink test resolves to the same set as v0.1.0
`default = ["cli"]` — no regression in coverage.
- **Tier 2 — `test-no-default`**: Linux x86_64 only. `cargo test
--no-default-features --lib` + dep-tree audit (SC-001 evidence).
- **Tier 3 — `test-<bundle>`**: one job per preset bundle. Linux only.
- `test-pdfgrep-classic`
- `test-pdfgrep-minimal`
- **Tier 4 — `check-leaf-<leaf>`**: SKIPPED. Zero leaves → no
per-leaf compile-check jobs. A placeholder comment in `ci.yml`
documents why this tier is empty.
- **Tier 5 — `lint-convention`**: single Linux job invoking the
vendored `tools/feature-lint/run.sh` script.
Per FR-014, bundle/lint jobs are constrained to Linux x86_64.
## Vendored feature-lint
Per spec 00011 §Phase 2 iteration 6 precedent (rusty-figlet vendored
the lint script because the umbrella `jsh562/rustylib` is private and
cross-repo `actions/checkout` cannot reach it), rusty-pdfgrep vendors
`tools/feature-lint/{lint.sh,run.sh,README.md}` from the umbrella into
the port repo. The vendored copy is byte-equal to the umbrella source
of truth as of the freeze commit (post the dev-tooling-allowlist +
benches/tests-search + additive-CHANGELOG-support fixes from rusty-ts
v0.2.0 / E011 Phase 3 iteration 2, the path-sanitization fixes from
rusty-sponge v0.2.0 / E011 Phase 4, and the additional sibling-port
iterations through rusty-pv v0.2.0 / E011 Phase 9).
## Why no new leaves — explicit rationale
Spec 00011 §Scope Edge Cases anticipates this case verbatim:
> A port has many tightly-coupled capabilities (e.g., rusty-pdfgrep's
> regex + recursive walking + encryption) — leaves can be coarse-grained
> when finer splits would be artificial. Pragmatism over purity.
rusty-pdfgrep deliberately chooses the zero-leaf path because:
1. The library API surface (`PdfGrep`, `PdfGrepBuilder`, `Match`,
`PdfGrepError`, `PageIterator`, `Engine`) requires `lopdf`, `regex`,
`fancy-regex`, and `thiserror` always-on. None of these are
strippable without breaking the public type signatures.
2. Every CLI sub-capability (recursive walking via `walkdir` + `globset`,
GREP_COLORS color output via `termcolor` + `anstyle`, encrypted
PDF retry, `--include` / `--exclude` glob filters, Strict-mode
dispatcher, completions subcommand) is already bundled into the
v0.1.x `cli` umbrella. Splitting any of them out would narrow `cli`
— violates the additive-v0.2.0 contract per spec 00011 §Brownfield
Notes.
3. The cost of carving a speculative leaf (cfg-gate scaffolding,
per-leaf CI matrix entry, README/CHANGELOG row, glossary candidacy)
outweighs the value when no orthogonal capability exists to gate.
4. The portfolio-wide convention shape (umbrella set, README "Cargo
Features" section, lint compliance) is preserved verbatim — a
downstream library consumer reading the README for rusty-pdfgrep
gets the same one-glance feature matrix UX as one reading
rusty-figlet, rusty-ts, rusty-sponge, rusty-vipe, rusty-pee,
rusty-pwgen, rusty-detox, or rusty-pv.
5. v0.2.0 is **purely additive**. Every v0.1.x feature is preserved
verbatim; no SemVer break. Future minor releases can add leaves
without breaking the v0.2.0 contract: a hypothetical
`pdf-extract-backend` v0.3.0 feature swapping the extractor would
slot in as `pdf-extract-backend = ["dep:pdf-extract"]` alongside the
existing umbrellas with zero migration cost.