# Development and documentation
Read the [hosted documentation](https://nicolasschuler.github.io/fledge/) without
installing Fledge or its documentation tools.
Work from a source checkout or extracted source distribution. Keep private papers
outside the repository or in `.local/`. Review the paths to be staged: generated
outputs and environments are ignored, while manuscript PDFs, images, and other
source assets remain trackable. Fledge is released under the MIT License (`LICENSE`).
## Install from source
Python 3.11 or later is required. Create a virtual environment and install the
checkout in editable mode with the development tools:
```sh
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e '.[dev,docs,build]'
fledge inspect examples/nested-paper
fledge check examples/nested-paper --main main.tex
fledge prepare examples/nested-paper --main main.tex --layout flat \
--output /tmp/prepared-paper
```
`uv tool install .` or another package manager also exposes the `fledge`
executable.
The `dev` extra provides pytest with pytest-xdist, Ruff and ty; `docs` provides Sphinx and MyST; `build`
provides the Python distribution builder. None installs TeX or PDF tools.
## Build the documentation
From the source root, install the documentation extra and build HTML:
```sh
python -m pip install -e '.[docs]'
python -m sphinx -n -W --keep-going -b html docs docs/_build/html
```
Open `docs/_build/html/index.html` in a browser. The build uses Sphinx's built-in
Alabaster theme and MyST Markdown, with warnings treated as errors and no warning
suppression. This build needs no TeX, online checks, or external theme. Its local
source/test/example links are copied into the HTML output as downloads. The
HTML build does not validate the current availability of external websites.
The [Sphinx Markdown documentation](https://www.sphinx-doc.org/en/master/usage/markdown.html)
describes enabling MyST, and the
[MyST cross-reference documentation](https://myst-parser.readthedocs.io/en/latest/syntax/cross-referencing.html)
describes document and download links.
## Publish the documentation
The [Deploy documentation workflow](https://github.com/NicolasSchuler/fledge/actions/workflows/docs.yml)
publishes this site to GitHub Pages from `main` when documentation or its included
source, tests, examples, scripts, or project metadata change. It installs the
`docs` extra and runs the strict Sphinx build above, then deploys the HTML in a
separate job only after the build succeeds. Only `main` can publish.
The repository's **Settings → Pages → Build and deployment → Source** must be
**GitHub Actions**. To publish manually, open the workflow link, choose **Run
workflow**, select `main`, and run it. The same page shows build and deployment
status; the deployed site is at .
## Run checks
```sh
python -m pytest tests -n auto
ruff check src tests docs/conf.py scripts
ruff format --check src tests docs/conf.py scripts
ty check src/latexprep
```
Ruff enforces, besides formatting and import order, the rule families `B`, `UP`, `C4`,
`PIE`, `PERF`, `PTH`, `SIM`, `RUF`, `PGH`, `T20`, `BLE`, `ARG`, `G` and `LOG`, the
keyword-only rule for boolean parameters (`FBT001`, `FBT002`) and ceilings on complexity
(10), branches (12), statements (50) and arguments (8); `[tool.ruff.lint]` in
`pyproject.toml` names the few deliberate exceptions and says why. Split a function
instead of raising a ceiling. A blind `except Exception` is allowed only at a boundary
that reports the cause, with `# noqa: BLE001 - `, and a type-checker suppression
names its code and says why (`# ty: ignore[code]`).
`tests/__init__.py` puts `src/` first on the import path, so no `PYTHONPATH` is
needed, and it fails loudly if a stale installed `latexprep` would shadow the
checkout. Remove it with `python -m pip uninstall fledge-latex fledge latex-preparation`
(the current distribution name and the two former ones), then run
`python -m pip install -e .`.
Live tests are opt-in with `FLEDGE_RUN_INTEGRATION=1`. Run them separately
where application isolation and the relevant tools can actually start:
```sh
FLEDGE_RUN_INTEGRATION=1 python -m pytest tests/test_integration.py \
tests/test_integration_checks.py tests/test_integration_packages.py \
tests/test_integration_backends.py tests/test_integration_realistic.py \
tests/test_runtime_inputs.py tests/test_runtime_monitoring.py -v
```
Every live test carries the `@live_test(...)` decorator from `tests/support.py`,
which skips it unless the variable is set and applies the registered `live` marker.
Select them with `-m live`, or leave them out of a run with `-m 'not live'`.
`tests.test_integration_realistic` builds, checks and prepares projects with
constructs found in ordinary papers and journal templates (template classes,
`[0,1)` intervals, several matching graphics, a spare `.bib`, Biber).
Tests distinguish component and workflow tests with controlled tool doubles from
real tool execution. Controlled doubles establish component behavior, not live
toolchain support, and skipped or blocked live checks must remain visible in
validation results. Every registered code links to tests that exercise detection
or measurement with source fixtures or controlled tool output; a catalogue
integrity test rejects missing associations. Each pytest-xdist worker is a
separate process that runs one test at a time, so Click's process-global stream
capture stays isolated. Online tests use
fake transports, so they verify request handling, not provider availability.
Optional `qpdf`/`mutool` adapters must remain inconclusive when their tools or
evidence are absent. The [limitations](limitations.md) page lists what has not
been validated live.
## Test structure and seams
Tests are `unittest.TestCase` classes, run by pytest. Shared fixtures live in three
support modules, never in a test module: a rule in
`tests/test_module_boundaries.py` rejects `from tests.test_x import ...`.
- `tests/support.py` holds project builders (`write_project`, `minimal_project`),
the `ProjectTestCase` and `AsyncProjectTestCase` bases (a temporary `self.root`
with `write` and `write_project`), the `@live_test` decorator, the controlled
document backends below, the golden-file helpers and a small JSON Schema
validator for the subset of keywords the report schema uses.
- `tests/fakes.py` holds `FakeCommandRunner`, a scripted implementation of the
`CommandRunner` protocol that the PDF, formatting and comparison adapters
depend on: outputs per tool, missing tools, a failure mode, events that hold a
command, and a log of every call. `FakeBuildRunner` and `LifecycleRunner` are
`ToolRunner` doubles for builds and process lifetimes; they use the `sandbox`,
`toolchain` and `monitor_factory` parameters of `ToolRunner` instead of
reaching into its private state.
- `tests/pdf_fakes.py` holds the PDF tool output fixtures and the runners built on
`FakeCommandRunner` (`DetailRunner`, `ArtworkRunner`, `FakePdfRunner`).
A job that would run TeX or the PDF tools takes its tool-driven operations from a
`DocumentBackends` value (`run_job(..., backends=...)`): the build, the PDF
inspection and the PDF comparison. The defaults run the real tools. Tests pass
`controlled_backends(build=..., inspect_pdf=..., compare_pdfs=...)` from
`tests/support.py` instead of patching module globals; each argument is an async
function, a value to return, or an exception to raise. Patch a module global only
to test genuinely global behaviour, such as the lookup of the host's tools or a
module-wide limit.
## Golden outputs
`tests/test_golden_outputs.py` compares three outputs with files in `tests/golden/`:
the `--help` text of every command (at width 100, without colour), the JSON report
of a `check` run with controlled backends, and its terminal summary. The JSON
report is normalised first: paths, the version and measured times and resource
figures become placeholders, so the golden changes only when the report does. The
same report is validated against `docs/_static/report.schema.json`.
After an intended change of an option, a message or the report, regenerate the
files and review the diff before committing:
```sh
FLEDGE_UPDATE_GOLDEN=1 python -m pytest tests/test_golden_outputs.py
git diff tests/golden
```
The help goldens follow the installed Click and Rich versions; regenerate them after
a dependency update that changes the layout.
## Continuous integration
The [Tests workflow](https://github.com/NicolasSchuler/fledge/actions/workflows/tests.yml)
runs on pushes to `main`, pull requests, manual dispatch and weekly. Every run:
- lints with Ruff (`check` and `format --check`) and checks types with `ty`;
- lints `install.sh` with ShellCheck;
- builds the documentation with `-n -W`, as the Deploy documentation workflow does,
so a broken link fails the pull request instead of the merge;
- builds the source archive and wheel, installs the wheel into a fresh
environment, runs `fledge --version` from another directory, and runs
`tests/test_docs.py` and `tests/test_packaging.py` from the extracted source
archive, so a file missing from either distribution fails the pull request;
- runs the unit tests with pytest on macOS and Ubuntu with Python 3.11, 3.12 and 3.13.
Manual and weekly runs add two live jobs that run the opt-in integration tests
(`FLEDGE_RUN_INTEGRATION=1`) without tool doubles. Each lists its test files by
hand, and `tests/test_packaging.py` fails when a test file with live tests
is missing from both lists:
- `live-macos` installs BasicTeX, Poppler, `tex-fmt` and Biber and also runs the
macOS sandbox tests.
- `live-linux` installs TeX Live, Biber, Poppler and Bubblewrap from apt plus a
pinned `tex-fmt` release, and runs the integration tests under Bubblewrap. It
leaves out `test_runtime_inputs.py` and `test_runtime_monitoring.py`, whose live tests
run only on macOS, and
adds `test_runtime_backends.py` for the live Bubblewrap isolation test.
**This job is unvalidated until a maintainer has run it from the Actions tab
and it passes**; until then the documentation must keep saying that Linux has
not run live. On Ubuntu 24.04 it sets
`kernel.apparmor_restrict_unprivileged_userns=0` on the disposable runner, a
CI setting that users must decide on for their own hosts (see
[installation](installation.md#linux)).
Every third-party action in every workflow is pinned to a commit SHA, and
Dependabot (`.github/dependabot.yml`) proposes a grouped update each week. The
[Release workflow](https://github.com/NicolasSchuler/fledge/actions/workflows/release.yml)
publishes to PyPI; see [release to PyPI](#release-to-pypi).
To run Fledge on a paper repository rather than on Fledge itself, see
[check your paper in CI](continuous-integration.md).
## Release to PyPI
Fledge is published on PyPI as `fledge-latex`. The PyPI project named `fledge` is
an unrelated project, which is why the distribution has a different name from the
command. The `fledge` and `latex-prep` commands and the `latexprep` module are
unchanged. The Release workflow uses PyPI trusted publishing (OpenID Connect), so
no PyPI token is stored in GitHub.
### One-time setup
1. On PyPI, sign in to the account that should own the project, which should have
two-factor authentication enabled. Open the account menu, choose
**Publishing**, and add a new **pending publisher** for GitHub Actions with
these values:
| Field | Value |
| --- | --- |
| PyPI project name | `fledge-latex` |
| Owner | `NicolasSchuler` |
| Repository name | `fledge` |
| Workflow name | `release.yml` |
| Environment name | `pypi` |
2. On GitHub, open **Settings → Environments** for the repository and create an
environment named `pypi`. Adding yourself as a required reviewer makes every
release wait for your approval before it uploads.
A pending publisher does not reserve the name, and the project does not exist
until the first release uploads. Someone else could register `fledge-latex`
first, so publish soon after registering. The first successful upload creates the
project and turns the pending publisher into an ordinary one. Because the
[installation guide](installation.md#install-with-pipx-or-uv) tells readers to run
`pipx install fledge-latex`, publish the first release soon after that page
reaches `main`.
### Each release
1. Set the new version in `__version__` in `src/latexprep/__init__.py`, the only
place it is defined: `pyproject.toml` reads it through
`[tool.setuptools.dynamic]`, and `fledge --version` reports it. Give the
version's heading in `CHANGELOG.md` its release date. The workflow
(`python scripts/release_checks.py version`) refuses to build if the version
is defined anywhere else or if the release tag is not `v` followed by that version.
2. Merge to `main` and wait for the Tests workflow to pass. Run the live jobs
from the Actions tab when the release depends on them.
3. Optional dry run: on the Release workflow page choose **Run workflow** on
`main`. It builds the source archive and wheel, installs the wheel into a fresh
environment, checks `fledge --version` and the packaged presets, and uploads
the files as the `python-distributions` artifact, but never publishes.
4. Create a GitHub release: choose or create the tag `vX.Y.Z` (for the current
version, `v0.3.0`) on `main` and publish the release. Publishing starts the
workflow, which verifies the tag, builds, smoke-tests and then uploads to PyPI
from the `pypi` environment. Approve the deployment if you added a reviewer.
5. Check the result in a clean environment: `pipx install fledge-latex==X.Y.Z`,
then `fledge --version`.
PyPI never accepts a replacement for a file it already has. If a release is
wrong, fix the problem and publish a new version; yank the bad one on PyPI if
needed.
## Agent skill
`skills/fledge/SKILL.md` is the Agent Skill described in
[use Fledge from an AI agent](agents.md). A test checks that every command and
option it names exists in the CLI, so update the skill together with CLI changes.
## Build local distributions
```sh
python -m build
```
This creates a wheel and source archive in `dist/`; it does not publish them.
The wheel installs the CLI. The source archive also carries the documentation,
examples, tests, agent skill, architecture, requirements, changelog and the scripts, so it can be
used to build these docs and run the documented source checks. It also includes
the opt-in macOS installer; installer tests use controlled commands, not native
Homebrew or MacTeX installation.
## Design and planning references
The requirements and the parallel execution design are historical design
records: they explain why Fledge is built as it is, were written on 6 to
12 October 2026, and are not updated with the code.
- {download}`Architecture <../architecture.md>`: component responsibilities,
stage boundaries, isolation, and parallel execution design, including future
interface work.
- {download}`Requirements <../requirements.md>`: the original product requirements
and acceptance criteria (historical design record); use
[what prepare does](workflow.md) and the [workflow reference](workflow-reference.md)
for current scope.
- [Check backlog](check-backlog.md): implemented subsets, partial behavior,
outstanding implementation, and missing validation.
- [Parallel execution design](parallelization-plan.md): rationale and acceptance
criteria of the scheduler work, with the starting seams it replaced (historical
design record).
- [Bounded benchmark](parallelization-benchmark.md): recorded local synthetic
measurements and their limits.
`fledge rule CODE` lists the behavioral tests associated with each check.
## Extend an implemented check
Follow the current option and dispatch paths rather than treating the design
documents as an implemented plugin API. A check usually needs its typed options,
family rule metadata, execution dispatch, and selection policy in
{download}`check_policy.py <../src/latexprep/check_policy.py>` to agree. The
per-rule facts are fields of the rule itself: `mandatory` for a guard that check
selection never removes, `reviewable` when a reviewed exception may target it, and
`not_checked_area` for an opt-in area that a run reports as not checked. Only the
mapping from an opt-in code to its option fields stays in `check_policy.py`.
Add an explicit code, descriptive title, scope and fix text to the applicable
data-only `*_rules.py` module, registered by
{download}`rules.py <../src/latexprep/rules.py>`. Codes are never reused. A name that a finding
carries but that describes the run or the tool environment rather than the manuscript
belongs in `OPERATIONAL_RULE_NAMES` instead; a test fails for an emitted name that is
neither catalogued nor operational. Link
the rule to a unit test that runs the actual checker on a meaningful fixture and
asserts both the code and the intended behavior. Include a valid counterpart, a
violating case, and an unavailable or ambiguous evidence case when applicable.
Reuse an existing behavior test when it already proves the contract; do not add
a test that only constructs a `Finding`. Keep evidence types and failure states
intact when changing execution order, and check scheduling and serial/parallel
equivalence separately from rule behavior.
After changing a rule, regenerate the catalogue with
`python scripts/generate_check_docs.py`. It derives each code's group (on by
default, opt-in or always enforced) and its enabling settings from
`check_policy.py`, and shows the rule's fix text. A test fails while
`docs/checks.md` is stale; the generated tables are never edited by hand.
The package is flat but layered, and [the module map](modules.md) assigns each of its
modules to one layer with a one-line responsibility. A module imports from its own layer and
the layers below it, and only the presentation layer imports `click` or `rich`;
`tests/test_module_boundaries.py` reads every import and fails on an upward import, on a
cycle, and when `tests/module_layers.py` and the tables of the module map disagree. Add a
new module to both.
The job workflow starts at `run_job` in
{download}`core.py <../src/latexprep/core.py>`, which validates the request,
dispatches the command and publishes the result.
{download}`document_run.py <../src/latexprep/document_run.py>` carries one
document through analysis, baseline, staged transformations and verification;
{download}`document_tasks.py <../src/latexprep/document_tasks.py>` builds its
scheduled tasks; {download}`documents.py <../src/latexprep/documents.py>` runs
configured `workflow.documents`; and
{download}`publication.py <../src/latexprep/publication.py>` writes verified
outputs. Around them,
{download}`task_reporting.py <../src/latexprep/task_reporting.py>` writes task
results, builds and selections into the report,
{download}`document_builds.py <../src/latexprep/document_builds.py>` runs the
staged rebuilds and repeat baselines,
{download}`package_verification.py <../src/latexprep/package_verification.py>`
archives, re-extracts and re-checks the package, and
{download}`main_selection.py <../src/latexprep/main_selection.py>` chooses the
main file. A new check of the document's sources is one entry in `_SOURCE_CHECKS`
and one in `TaskName`, both in `document_tasks.py`; it then runs on the original
sources and again on the final extraction.
Small modules under the job workflow each own one concern and import nothing
above them. {download}`errors.py <../src/latexprep/errors.py>` defines the
expected failures: `PreparationError`, `RequestError`, and the kinds callers act
on, `ToolUnavailableError` (the missing `tool`), `SettingsError` (the `settings`
at fault) and `SandboxRefusedError`. Branch on these types and attributes, never
on the wording of a message. Raise them only for problems a researcher can fix;
a violated internal contract raises `ValueError` or `RuntimeError`, which
`defects.is_expected_failure` reports as a defect. Per-item failures are
findings; only `PreparationError` aborts a job. A check that cannot examine one
file, URL or entry reports a finding (or a gap in its evidence) and the run
continues. An exception that a module uses to stop its own work, such as the
BibTeX parser's, the transport's `NetworkFailure` or a bibliography proposal's
`ProposalBlocked`, is not a `PreparationError`: it never leaves its feature's
modules and is turned into a finding at their boundary.
{download}`findings.py <../src/latexprep/findings.py>` defines `Finding`,
{download}`report.py <../src/latexprep/report.py>` the `Report` with its
`Outcome` and exit codes, and
{download}`setting_choices.py <../src/latexprep/setting_choices.py>` the closed
values that settings accept. For concurrency,
{download}`cancellation.py <../src/latexprep/cancellation.py>` holds the
cooperative-cancellation helpers (modules that parse files import it, never the
scheduler),
{download}`resource_budget.py <../src/latexprep/resource_budget.py>` the shared
CPU, memory and tool-class reservations,
{download}`scheduler.py <../src/latexprep/scheduler.py>` the dependency
scheduling of tasks, and
{download}`process_control.py <../src/latexprep/process_control.py>` the start
and kill helpers for child processes.
Diagnostic output goes through `logging`, never `print`. A module that reports its progress
declares `logger = logging.getLogger(__name__)` and logs with lazy `%` arguments
(`logger.debug("task %s: start", name)`; Ruff's `G` rules reject f-strings). Log at DEBUG,
and at INFO only what an investigator should notice, such as a limit that stopped a tool.
A message names tools, hosts, task names and counts, never a path, a URL, a query string
or a contact address, and never an exception's text when that may contain one. The
library configures nothing: `latexprep/__init__.py` gives the package logger a
`NullHandler`, and the command line attaches one stderr handler for the length of a
command when `--debug` or `FLEDGE_DEBUG=1` asks for it
({download}`diagnostic_logging.py <../src/latexprep/diagnostic_logging.py>`). That handler
prints the time since start, the module name and the message, replaces the home and
temporary directories as reports do, and leaves stdout to the report. See the
[diagnostic log](troubleshooting.md#diagnostic-log) for the user's side.
Option dataclasses validate in `__post_init__` with the kit in
{download}`option_config.py <../src/latexprep/option_config.py>`, which also
converts TOML tables into those classes. `validate_scalars` checks every boolean,
integer, number and `Literal` field against its annotation and the limits in the
class's `FIELD_BOUNDS`; `require_unique_strings`, `require_text`, `require_pairs`
and their siblings check lists and text. A wrong type and an out-of-range value
therefore give the same message, which names the key as a settings file spells it
and states the accepted range. Write each limit once, as a named constant with its
unit, and build the message from it rather than repeating the number.
For normal preparation, online checks run once on the fresh archive extraction;
source inspection and dry runs use the initial snapshot. Preserve the request
budget per document when changing this dispatch. Controlled tests should make
the inspected content and phase explicit.
The architecture document includes target design: per-task timeout/artifact
descriptors and a structured progress-event schema are not implemented as
described there. Current execution uses progress strings and a shared whole-job
deadline alongside per-tool limits. Bibliography files and PDF measurements are
still parsed by each check that reads them. Checkers that read TeX source share two
public modules:
{download}`tex_lexer.py <../src/latexprep/tex_lexer.py>` provides the bounded
lexical subset (commands and their argument groups, comment and verbatim
masking, conditional spans), and
{download}`source_graph.py <../src/latexprep/source_graph.py>` builds the literal
dependency graph of one selected main file with `SourceGraph.build`. The graph
is split by concern: `source_loading.py` reads and lexes a file within the byte and
command budgets, `source_command_checks.py` judges its commands,
`source_resolution.py` turns a file name into a project file, and
`source_file_commands.py` holds the command tables they share. Reports about a graph
are in `source_analysis.py` (roots, dependencies, lint) and `flat_layout.py` (flat-layout
plans).
{download}`expanded_document.py <../src/latexprep/expanded_document.py>` splices
that graph into one document whose offsets map back to file and line
(`expand_document`), and
{download}`literal_text.py <../src/latexprep/literal_text.py>` holds the shared
command tables and plain-text reading used by manuscript, author-record and
submission-text checks. The vocabularies those readers have in common (TeX file
suffixes, math markers, macro-definition and citation commands) are in
{download}`tex_commands.py <../src/latexprep/tex_commands.py>`, and the wording of
unfinished-work and private-note markers is in
{download}`comment_markers.py <../src/latexprep/comment_markers.py>`. Add a command
or phrase there, not as a private list in a check.
The checks of one job do not each read the project again:
{download}`analysed_source.py <../src/latexprep/analysed_source.py>` holds the graph
and expanded document of one project root and main file (`AnalysedSource`), builds
each on first use, and `JobContext.sources` hands the same one to every task that
reads that root, however many threads ask. A check accepts it as an optional
`analysed` argument and builds its own when it runs stand-alone; an analysis of
another root or main file is refused. It is read-only, so the transformations that
plan each edit on a staged copy that earlier edits changed (`source_transform`,
`bibliography_transform`, `flat_layout`) build their own graph.
{download}`text_safety.py <../src/latexprep/text_safety.py>` escapes untrusted text
for the terminal, HTML reports, CI annotations and one-line messages. Checks that
scan every supplied file use the bounded inventory and per-scan byte budget in
{download}`file_scan.py <../src/latexprep/file_scan.py>`, comment extraction in
{download}`tex_comments.py <../src/latexprep/tex_comments.py>`, and bounded redaction
in {download}`redaction.py <../src/latexprep/redaction.py>` and
{download}`finding_text.py <../src/latexprep/finding_text.py>`. Every read of a
project, build or bibliography file goes through `read_regular_file` or
`read_file_prefix` in {download}`file_safety.py <../src/latexprep/file_safety.py>`,
which opens the file without following a final link, checks the opened
descriptor and never reads past its limit; do not reopen a file with
`Path.read_bytes` after checking its size. Path rules live in
{download}`path_safety.py <../src/latexprep/path_safety.py>`:
`is_relative_posix_path` validates a project-relative name with explicit options
(normalized spelling, whether `.` is allowed, how unusual characters are treated,
a per-segment pattern), and `is_contained` and `is_link_free_regular_file` decide
containment. The byte units and the few limits that several modules share are in
{download}`limits.py <../src/latexprep/limits.py>`; a limit that one module uses
stays beside its code. Bibliography checks,
transformations and online lookups share the bounded BibTeX reader in
{download}`bibtex_syntax.py <../src/latexprep/bibtex_syntax.py>`, the one loader
of declared .bib files (`read_declared_bibliographies`, with typed problems and
byte budgets) in
{download}`bibliography_files.py <../src/latexprep/bibliography_files.py>`, the
index of entries and their reach in
{download}`bibliography_index.py <../src/latexprep/bibliography_index.py>` and the
literal citation graph of the selected main file in
{download}`citation_graph.py <../src/latexprep/citation_graph.py>`
(`build_citation_graph`). The online checks are layered, each module importing
only those before it: `online_options` (settings and outcomes), `public_url`
(which addresses may be requested), `pinned_dns` (resolution in a disposable
child), `online_http_wire` (the HTTP/1.1 bytes, testable on a `StreamReader`),
`online_http_session` (the client, with `resolver` and `transport` seams that
tests replace), `crossref` (typed replies), `online_inputs` and `online_checks`
(the findings). No test needs a network.
PDF checkers share a few modules. {download}`pdf_tools.py <../src/latexprep/pdf_tools.py>`
holds the tool session that freezes the inputs and runs each Poppler, qpdf or MuPDF
command in its own directory, the one argument builder for each tool command, the
preflight (document, properties and page count) that marks every configured rule
unavailable when it fails, and `ToolMeasurement`, the one way a command's output becomes
findings or, when it cannot be read, inconclusive ones. The parsers for Poppler and
MuPDF output are in {download}`pdf_tool_output.py <../src/latexprep/pdf_tool_output.py>`,
the qpdf object graph is {download}`pdf_qpdf_graph.py <../src/latexprep/pdf_qpdf_graph.py>`,
page, color and render-size arithmetic is
{download}`pdf_geometry.py <../src/latexprep/pdf_geometry.py>`, and the builders for
measurement and unavailable-evidence findings are
{download}`measurement_findings.py <../src/latexprep/measurement_findings.py>`. The
option schema of the configured measurements is
{download}`pdf_check_options.py <../src/latexprep/pdf_check_options.py>`, and the
`pdf.*` rule names are the `PdfRule` enum in
{download}`pdf_rule_names.py <../src/latexprep/pdf_rule_names.py>`. A session remembers
each completed read-only measurement by its arguments, so two checks that need the same
output run the tool once. The check groups of one PDF in a pipeline run
({download}`pdf_check_pipeline.py <../src/latexprep/pdf_check_pipeline.py>`) go further:
they share one `SharedPdfSession`, which freezes the PDF on first use and hands each group
a view with its own working directory and its own tool record over one shared memo, so a
read-only measurement runs once per PDF for the whole run. The memo keeps the output of
each distinct read-only command (at most the runner's output limit each) until the run
ends and then releases it; renders are never kept. Each group entry point takes
`session=` and works unchanged without it. Each PDF adapter module states the external tools of its
configured checks (`required_pdf_check_tools`, `required_artwork_tools`,
`required_structure_tools`, `required_inspection_tools`, `COMPARISON_TOOLS`), and
`fledge doctor` unions them.
Binary image handling sits in two leaf modules that import no tool, runtime or
scheduler code:
{download}`png_support.py <../src/latexprep/png_support.py>` (the PNG encoder,
validator, strict chunk walker and header reader) and
{download}`raster_pixels.py <../src/latexprep/raster_pixels.py>` (the bounded PNM
reader and the ink, extrema and changed-pixel analysis, which use only
C-speed byte operations). Sharing further parsed
evidence between checkers is a future maintenance tradeoff; extending a check does
not require a new plugin framework.