This document covers two things: the rules CI actually enforces, and the design conventions this library has settled on so far. If you're only fixing a typo or a single proof, the "Non-negotiables" section is enough. If you're adding a new file or subject area, read "Design conventions" too.
See README.md for how to build the project and the three ways to browse it
(Lean source, blueprint, doc-gen4).
lake buildmust be green.blueprint.yml(push tomain) explicitly runs withlint: trueandmk_all-check: true.lean_action_ci.yml(every push/PR) doesn't set either input, andmk_all-checkdefaults tofalseinlean-actionwhen unset — somk_all-checkis only actually checked on pushes tomain, not on every PR. Run the full thing locally before opening a PR either way:lake exe cache get # first time only lake build- No
sorry, noadmit. A proof that doesn't go through isn't done. - Every public declaration needs a docstring — the
docBlamelinter enforces this and will fail CI otherwise. (It only checks that a docstring exists, not what's in it — see theReference:convention below, which is not currently linted.) - New custom axioms must be justified and registered, not scattered inline.
- Only add an axiom for a standard, well-established mathematical result that isn't (yet) in Mathlib — e.g. Picard–Lindelöf existence, or a real-analysis smoothing lemma. Don't axiomatize the thing you're actually trying to prove.
- Write a doc comment on the axiom stating what standard result it captures and why it's reasonable to take as given (a citation is good; "obviously true" is not).
- Put it in
LeanForControl/axioms.leanorLeanForControl/Comparison/Axioms.lean(or an equally-named central file for a new subject area) — not inline in a proof file, so the full list of assumptions stays auditable in one place. - Say so explicitly in the PR description: which axiom, why it's needed, why it's standard.
- If you touch a file with
@[blueprint ...]annotations, keepleanblueprint checkdeclspassing — it checks that blueprint labels still point at real Lean declarations.
These aren't CI-enforced, but deviating from them without a reason makes review harder and the codebase less consistent.
- Definitions live apart from theorems. Structures and predicates go in a dedicated
Defs*.lean(e.g.DefsAutonomous.lean,DefsNonAutonomous.lean); theorems proved from them go in files grouped by result, not by definition (e.g.Autonomous.lean,LaSalle.lean,NonAutonomous.lean). - Generic infrastructure stays generic. The comparison-function library (class K,
K∞, KL, L) lives in
LeanForControl/Comparison/, decoupled from any specific stability theory, so both the autonomous and non-autonomous stability work can reuse it. If you're building something reusable across subject areas, it belongs in its own directory, not buried inside the file that first needed it. - One lemma, one fact. Each lemma should state and prove exactly one clean,
reusable mathematical claim, rather than one monolithic proof term that inlines every
sub-argument — the Lean equivalent of "a function should do one thing and do it well."
Autonomous.leanis the model:hasDerivAt_V_comp_traj(just the chain rule),trajectory_continuous,V_nonincreasing_on, andV_limit_zero_of_compactare each one fact, and the top-level theorems (lyapunov_stable,lyapunov_asymptotic_stable) are just those pieces composed together. The payoff: a small lemma is independently reusable (V_limit_zero_of_compactis used by both the local and global stability proofs) and a change only breaks its direct callers instead of one giant proof. Don't over-split, either — if a "sub-lemma" only ever gets used once and needs its own name, hypotheses, and doc comment to say less than thehaveblock it replaced would have, inline it instead. - Prefer pointwise hypotheses over trajectory-quantified ones when formalizing a
classical condition — e.g.
∀ x, fderiv ℝ V x (f x) ≤ 0is easier to consume than∀ φ t, HasDerivAt (V ∘ φ) ... t. Write one chain-rule bridge lemma once (hasDerivAt_V_comp_traj-style) to connect the two forms, rather than threading the trajectory-quantified version through every downstream proof. - Bundle data that multiple proof branches need into the defining
structure, even if it looks redundant at the definition site — e.g.hequil : f x_eq = 0is stored on the Lyapunov-function structs because several proofs need it and re-deriving it ad hoc each time is worse than storing it once. - Naming: predicates read as English (
IsLocalLyapunovFunction,LyapunovStable,UniformlyAsymptoticStableNA); hypotheses areh-prefixed with a descriptive suffix (hV_diff,hLie_nonpos,hΩ_compact), matching Mathlib convention. - Citing a textbook: attribute the source by title only — e.g.
Reference: Khalil, *Nonlinear Systems* (3rd ed.)— without edition-specific theorem/lemma/definition numbers. Numbers drift across editions and mean nothing to a reader who doesn't own that exact edition. Refer to results by their descriptive or eponymous name instead (Barbashin's theorem,class-K sandwich bounds,Osgood's construction), the way the rest of the file names its own lemmas. - State where every new theorem or lemma came from. Add a
Reference:line to its doc comment (or the module docstring, if it covers the whole file) naming the textbook, paper, or standard result it formalizes, using the citation style above. If the result is original to this repo, say so explicitly (Original.or a one-line note on why it's needed) instead of leaving the provenance unstated — and repeat the source in the PR description too, so it's visible in review without opening every file. This is a convention, not yet a CI check — thedocBlamelinter only verifies a docstring exists, not what it says, so nothing currently fails a build over a missingReference:line. Most of the existing source predates this convention and doesn't have one yet; treat it as required for new/changed theorems going forward, not as a claim that the whole codebase already complies. - Blueprint annotations (
@[blueprint "label" (statement := ...) (proof := ...)]) go on definitions and named/main theorems worth exposing in the readable blueprint — not on every internal helper lemma (roughly a third of declarations carry one; that's the right ratio to aim for, not 100%). Thestatement/prooftext is hand-written prose, not auto-extracted from the Lean signature — keep it tight and faithful, and treat the Lean source as ground truth if the two ever drift. - Keep a living
plan.mdin any actively-developed subject-area directory (seeStability/plan.mdfor the template: a status table of what's proved vs. planned, file-by-file notes, and a "lessons learned" section). Update it as part of the PR that changes that area's status — a roadmap that isn't updated with the code it describes is worse than no roadmap.
- Keep it focused: one subject area per PR.
lake buildgreen locally first (the Mathlib cache fromlake exe cache getmakes this fast after the first run).- State the source of any new theorem in the PR description (textbook/paper/original) — see "State where every new theorem or lemma came from" above.
- If you're introducing a new subject-area directory meant for ongoing work, add a
plan.mdalongside it.