This guide explains the contributor workflow for the generated project.
Pylint discovers the plugin through register() in
df12_python_lints/__init__.py, the entry point pylint calls when
load-plugins names the package. It instantiates and registers the ten
checkers, each defined in its own module: MatchDispatchChecker,
AssertMessageChecker, ConstantChainChecker, TrivialWrapperChecker,
ReexportAssignmentChecker, SuppressionCommentChecker,
SnapshotAssertionChecker, DataclassSlotsChecker, TypeAliasChecker, and
FutureAnnotationsChecker. Between them, they expose thirteen messages. The
last two are gated on pylint's py-version option: the type-alias check needs
a 3.12+ baseline (PEP 695) and the future-annotations check a 3.14+ baseline
(PEP 749 deferred evaluation), so the end-to-end shim tests pass
--py-version=3.14 explicitly — the shim runs under PyPy, whose interpreter
version would otherwise gate both checks off.
Reusable or substantial checker analysis lives in private helper modules:
_chains.pyholds the traversal used by the dispatch-oriented checkers to walk a headifstatement and itselifchain. Its pure selection kernels (repeated_subject,narrowing_prefix) carry PEP 316pre:/post:contracts so CrossHair can model-check them symbolically, separate from the astroid-bound checker classes that consume them._expressions.pyholds the attribute-chain and name-binding helpers — for example resolving the baseNameof a purename.attr.deeperchain — used by the wrapper and re-export checkers._dataclass_decorators.pyresolvesdataclasses.dataclassand related decorators from active lexical import bindings. It deliberately avoids qualified-name spelling alone, inference that imports the linted program, and ambiguous lookup chains. This strict resolver is intentionally distinct from the type-alias checker's import recognition: a dataclass decorator is valid only when one unambiguous active binding proves its identity, while the type-alias checker conservatively classifies a name when its lookup chain contains a supported import. Keep the shared primitives policy-neutral if these implementations are consolidated; do not weaken either checker's ambiguity contract merely to remove similar traversal code._dataclass_state.pydistinguishes runtime__slots__assignments and real dataclass fields from class-only names. It is the shared source-state boundary for slot-layout and direct-method mutation analysis; keep inheritance and replacement-class decisions out of this module._dataclass_inference.pyresolves Astroid base candidates to unambiguous class definitions and defines the ordered layout classification used by inherited-layout and replacement-class hazard analysis._dataclass_analysis.pyclassifies direct-method state evidence, replacement-class hazards, and inherited layouts forDataclassSlotsChecker. ALayoutAnalyzeris created once per module. It caches eligibility and per-class inherited layouts, and performs a reverse inheritance pass before class visits, so local dataclass bases later combined through multiple inheritance are suppressed before either base can report. The provisional caches are cleared after that reverse pass so final decisions include every discovered conflict.
The runtime dependency is bounded to pylint>=3.3,<5. Dataclass base inference
uses Astroid's private Instance._proxied bridge because no public API exposes
the underlying ClassDef; a new Pylint major version therefore requires the
focused inference and inherited-layout tests to be revalidated before widening
the range. ADR 001 records
the conservative analysis, caching, and compatibility decision.
The dataclass-slots decorator pass preserves source order. Decorators below
dataclass run first and suppress the check unless they are a proven
identity-preserving marker; decorators above it see the replacement class and
do not suppress. Direct-method analysis uses each method's first instance
parameter and ignores static methods. Open-state checks do not enter nested
executable scopes. Replacement-class checks scan nested executable scopes but
stop at nested ClassDef bodies: nested helper classes own their class cells,
so a helper's zero-argument super() is not an outer dataclass hazard.
Inference ambiguity is a reason to stay silent.
Inherited-layout analysis is transitive, including through a local dataclass
that already requests generated slots. object and proven empty-slot marker
bases are neutral; a single non-empty slotted lineage is safe; unslotted,
unknown, variable-length, or conflicting lineages suppress. Reverse analysis
suppresses local dataclass bases only when a direct multiple-inheritance shape
would combine more than one prospectively non-empty slot lineage. A local
dataclass that is itself eligible for R9111 is treated as prospectively
slotted, allowing a safe single-inheritance chain to report every missing
declaration in one run. The checker never imports or executes the linted
program and never mutates its AST.
SuppressionCommentChecker is token-based rather than AST-based: it inspects
comment tokens to find suppression pragmas and the explanations that may
accompany them, because a bare pragma carries no node in the abstract syntax
tree to attach a check to.
The ambrleaks subpackage is a separate, standalone scanner exposed as its own
console script, split into four modules: rules.py pairs each detection
pattern with an optional entropy floor and allowlists, scanner.py walks
.ambr files line by line and attributes findings to their # name: test
block, config.py holds the Config model and its parser, and cli.py
provides the command-line entry point. Each module keeps a pure core behind a
thin filesystem boundary, so the reusable logic reads no files and never
consults the working directory: scanner.py pairs the pure scan_text with the
scan_file boundary, config.py pairs the pure parse_config with the
read_config boundary, and cli.py pairs apply_baseline with
read_baseline. Both scanner entry points take an explicit base_dir; the CLI
resolves it from the working directory once, at its own boundary, and injects
it into scanning, so path canonicalization stays deterministic. Suppression
lives in external configuration and baseline files rather than inline markers
because syrupy rewrites .ambr files wholesale on --snapshot-update and
would destroy any annotation.
ambrleaks emits standard-library structured logs at its operational
boundaries — configuration load, scan (with a finding count), baseline
suppression (with a hit count), and scan failure — through the ambrleaks
logger. It follows the library convention of attaching a NullHandler, so the
logs are silent by default and never pollute the finding output; -v /
--verbose attaches a stderr handler at INFO, and an embedding process can
configure the ambrleaks logger to route the records anywhere. This keeps the
default run's user-facing contract unchanged: an exit status (0 clean, 1
findings remain, 2 a configuration, I/O, or decode error), one line per
finding on stdout (each tagged with its rule id), a finding-count summary and
any ambrleaks: error: message on stderr.
The tool deliberately stops there — no metrics backend, tracing, or alerting.
It is a synchronous, single-shot lint-style CLI (the class of ruff, pylint,
or grep) with no long-running process, network calls, or shared mutable state
to instrument; the boundary logs carry the bounded counts (findings, baseline
hits, failures) that a metrics layer would otherwise expose, and the invoking
shell or CI job captures the status and output. If ambrleaks ever grows a
long-running or service mode, revisit whether a metrics or tracing stack is
then warranted.
The scan streams end to end and holds no whole-result collection in memory.
discover yields .ambr paths straight from the recursive walk (no sort, so
they arrive in filesystem order), and _collect_findings scans one file at a
time and applies the allowlist inline, so a file's findings surface before the
next file is read. Every mode consumes that stream incrementally:
--write-baseline (write_baseline) appends one JSON fingerprint entry per
finding as it arrives — building the array in a same-directory temporary file
that is atomically moved into place — and --baseline streams the findings
through apply_baseline, a generator that consumes one baselined occurrence
per matching fingerprint and yields the survivors immediately. Scanned and
suppressed counts are tallied incrementally for logging, never by materializing
or re-scanning. Baseline entries therefore follow discovery order rather than a
sorted order; occurrence semantics (a fingerprint recorded n times suppresses
the first n matches) are unchanged.
The public entrypoint for formatting, linting, typechecking, tests, and
spelling is make all. Narrower Make targets may be invoked when investigating
a specific failure, and changes should be reconciled with the aggregate gate
before being considered complete.
make lint runs Ruff, interrogate --fail-under 100 $(PYTHON_TARGETS) for
100% docstring coverage across $(PYTHON_TARGETS), and Pylint.
Run make audit as the dependency vulnerability gate. It runs pip-audit for
Python dependencies, and Rust-enabled projects also run cargo audit from the
rust_extension crate directory.
The Scripting standards document provides guidance
for adding or updating helper scripts. New and updated scripts are expected to
use Cyclopts for command-line interfaces, cuprum for typed and
catalogue-bound external command execution, pathlib for filesystem paths, and
cmd-mox for tests that mock external executables.
Script changes should update the scripting guide when they introduce a new convention, command catalogue, testing pattern, or operational expectation that future contributors need to follow.
The generated repository includes GitHub Actions workflows and local composite
actions under .github/.
.github/workflows/ci.ymlruns on pushes tomainand on pull requests. It sets up Python 3.13, installsuv, validates the generatedMakefilewithmbake, runsmake build,make check-fmt,make lint(Ruff +interrogate --fail-under 100 $(PYTHON_TARGETS)+ Pylint),make typecheck,make spelling, andmake auditexcept for Dependabot pull requests viaif: github.actor != 'dependabot[bot]', then delegates coverage generation to the shared coverage action. When the Rust extension is enabled, it also sets up Rust, installs Rust lint and test tools, and passesrust_extension/Cargo.tomlto coverage..github/workflows/audit.ymlrunsmake auditagainst the default branch weekly as the compensating control for the Dependabot CI bypass..github/workflows/act-validation.ymlruns rendered workflow validation in a separate workflow. It installsact, checks Docker availability, and runsmake test WITH_ACT=1outside the coverage path..github/workflows/release.ymlpublishes wheels when av*.*.*tag is pushed. It builds a pure Python wheel, creates a GitHub release with generated release notes, downloads wheel artefacts, and uploads them to the tag release..github/workflows/build-wheels.ymlis a reusable workflow for extension builds. It accepts a Python version and builds wheels across Linux, Windows, and macOS architectures via.github/actions/build-wheels..github/workflows/get-codescene-sha.ymlis manually dispatched. It fetches the CodeScene coverage CLI installer, computes its SHA-256 digest, and writes the result to theCODESCENE_CLI_SHA256repository variable..github/actions/build-wheelswrapscibuildwheelwithuvxand uploads architecture-specific wheel artefacts..github/actions/pure-python-wheelbuilds a pure Python wheel withuv build --wheeland uploads the resulting artefact..github/dependabot.ymlenables dependency update pull requests for GitHub Actions and Python packages. Rust-enabled projects also receive Cargo updates.
The CS_ACCESS_TOKEN secret must be configured when CodeScene coverage upload
is required. The CODESCENE_CLI_SHA256 variable should be populated using the
refresh workflow, so CI can verify the downloaded CodeScene installer before
upload.
Run make spelling to enforce en-GB-oxendict spelling. The generator fetches
the estate-wide base from leynos/agent-helper-scripts only when its authority
is newer than the ignored local cache. A populated cache supports offline
generation. Add only project-specific terms and exclusions to
typos.local.toml; never edit generated typos.toml by hand.
The test suite is layered, so each verification tier runs at the right cadence:
- Example tests (
make test) run the pytest suites, including the Hypothesis property tests intests/test_properties.py. The properties cover the checkers' decision kernels across whole input families — chain lengths, guard-run lengths, literal nesting, and generated pragma comments — and any shrunk counterexample should be promoted to a named regression test beside the checker's example suite. - Symbolic model checks (
make crosshair) run CrossHair over the pure selection kernels indf12_python_lints/_chains.py, which carry PEP 316 contracts (pre:/post:docstring clauses). Thepre:clauses deliberately bound the symbolic domain — short chains drawn from a two-symbol alphabet — so the search can confirm everypost:clause over all paths rather than merely exhaust its budget.tests/test_crosshair.pyrequires those four confirmations (one inrepeated_subject, three innarrowing_prefix) and treats a "Not confirmed" verdict as a failure; the bounds scope the proof only, while the Hypothesis tier covers the unbounded runtime domain. The gate is opt-in; run it on changes to the kernels rather than on every push. - End-to-end shim tests (part of
make test,tests/test_e2e_shim.py) lint fixture modules through the pinnedleynos/pylint-pypy-shimrunner with the plugin loaded, proving every checker fires — and stays silent on clean code — under the same PyPy-backed pylint that the project's own lint gate uses. The shim ref is read from the Makefile so the two cannot drift apart.