The Lean half of a contract between a corpus of mathematical papers and a Lean formalization of results from it.
It contains three things.
@[cites] binds a Lean declaration to a statement in a paper.
import MathFormalContract
@[cites "stmt:9f4c1a20b7d3:bridgeland2007.lem-8.2" (relation := one_way)
(frontier := ["gltilde-universal-cover"])
(note := "Acts on PreStabilityCondition, not StabilityCondition.")]
theorem myResult : ... := ...MathFormalContract.Emit sweeps the environment and reports what was
proved, from what, and under which axioms, as emission/1.0.
contract/ is the machine-readable half: the JSON schemas for
those artifacts and the mfc tooling that polices them, including the lint that
turns the trust-language policy into a build failure. It lives here rather than
in the corpus server's repo by
ADR-0009.
It adds no Lake dependency and cannot weaken the zero-dependency invariant
below.
Every fact it writes comes from Environment.constants and
Lean.collectAxioms in the environment that actually elaborated.
That is the difference between catching this and not:
set_option maxHeartbeats 400000 in theorem sneaky : False := by sorry
open Classical in theorem sneaky : False := by sorryA source-parsing extractor is defeated by declaration syntax — the corpus
server's Lean auditor matches neither the declaration site nor the declaration
name in either form, and returns a clean verdict for a sorry-backed proof. An
environment sweep cannot fail that way, because it never sees syntax. Measured
against a compiled fixture carrying all three evasion forms: three constants
emitted with sorryAx in their axiom closure, exit code 1.
Two further properties are load-bearing, and both are enforced by tests rather than by care:
- Module scope, never name prefix. A declaration at the root namespace or
under a foreign namespace, sitting inside a topic module, still lands in the
.oleanand is importable downstream. A prefix scope misses it, and a mis-set prefix sweeps nothing and passes. - Byte-reproducible. Neither constant iteration order nor
collectAxiomsoutput order is specified —collectAxiomswas measured returning[propext, Quot.sound, Classical.choice]for one declaration and[Quot.sound, propext, Classical.choice]for the next in the same run. Every array is sorted before serialization, so two runs differ only inemitted_at.
A topic repo generates one small file and nothing else:
import MathFormalContract
def main (args : List String) : IO UInt32 :=
MathFormalContract.emitMain
(rootLib := `BridgelandStabLean)
(leanOptions := [("autoImplicit", .bool false),
("relaxedAutoImplicit", .bool false)])
argsleanOptions is declared, not observed, and the artifact should be read
that way. Elaboration options are compile flags and are not recorded in the
.olean, so an emitter that imports a module cannot recover the options that
built it. Reading the ambient options instead makes the record report the
emitter process's defaults — measured, this package emitted
autoImplicit: true while its own lakefile.toml sets it false. Copier
renders that file and lakefile.toml from the same answers, and mfc lint
fails a mismatch.
The emission carries no digests. Lean core at v4.32.1 ships no SHA-256, and
Lake's Hash is a 64-bit non-cryptographic value that is not portable across
toolchains, so all canonicalization lives downstream in one language.
lakefile.toml has no [[require]] block and must never get one. Not Mathlib,
not Batteries, not anything.
This package is the single named exception to the consuming repo's one-pin rule, and the exception is argued from the leaf property: a package with no transitive dependencies cannot drag anything into a topic repo's environment and cannot disagree with that repo's anchor about a Mathlib revision. Add a dependency and the exception lapses — meaning the dependency comes out of every topic repo that took this one.
That is why Cites.lean hand-rolls its key validation instead of reaching for
a regex. If you want a helper, write the helper.
stmt:<12 lowercase hex>:<label>
The hex is a registry id minted once per topic repo; the label is chosen by whoever writes the registry entry.
The paper coordinate is deliberately not in the key. No arXiv id, no version, no printed number — those are typed fields of the registry entry. Two reasons:
- A
textbook:source has no arXiv version, and a DOI contains characters that break positional parsing. Typed fields express both; a colon-delimited key does not. - Nothing a corpus computes appears in the key. Corpus chunk ids are content hashes that rotate on any re-parse, with no forwarding — so a citation built from one silently stops resolving. A key nothing rotates cannot.
@[cites] refuses a chunk-id-shaped key explicitly, because it is exactly what
a corpus hands you and therefore exactly what someone will paste in.
There is no safe default. A missing relation that defaulted to anything would be a trust axis inferred from silence.
| value | meaning |
|---|---|
exact |
states the cited statement, no added hypotheses |
equivalent |
interderivable with it in this environment |
specialization |
a special case of it |
one_way |
it implies this, not conversely |
no_claim |
related; no implication claimed |
Every value is a claim by the author, never a verified fact. Machine
artifacts always spell it relation_claimed; only a dated, named human review
may promote a claim to confirmed.
note is optional and free text, and the emitter writes null rather than
"" when it is absent — a consumer must be able to tell "no note" from "a note
that says nothing". It is not decoration: relation := no_claim says two
statements are related without saying how, which is unreadable without one.
mfc lint rule E-06 fails a no_claim binding whose note is null. That rule
lives on the mfc side rather than here, deliberately, so the rejection
fixture it names stays authorable.
It does not check the mathematics, read the paper, or know whether the registry key exists — the registry lives in the topic repo and is validated there. What it checks is that the key is well formed, so a typo fails at compile time instead of surfacing much later as a citation that resolves to nothing.
lake buildBuilds the library, its tests, and both emitter binaries — all in
defaultTargets on purpose: a file no target builds is a file that rots.
Run the emitter against this package:
lake exe mfc_emit_self --out attest/lean-emission.jsonmfc_emit_selftest is the same emitter pointed at the test library, which is
the only module tree here carrying @[cites]. It exists because extension
state nearly did not survive the round trip: importModules takes
loadExts := false by default, and with it unset every extension arrives
empty rather than absent. Measured before the fix — all 150 constants
reported is_instance: false, all seven @[cites] bindings emitted
cites: [], exit code 0, artifact valid. An emission asserting that a repo
cites nothing, produced by the tool whose purpose is to make vacuous passes
impossible.
Inspect what is tagged in scope:
#cites_dumpNo native_decide, anywhere. It introduces Lean.ofReduceBool into the proof
term — an axiom outside the propext / Classical.choice / Quot.sound
allowlist this contract exists to enforce, and a package that polices axiom
hygiene must not launder one into its own build. Checks run at elaboration time
in run_cmd and throw on failure instead.
The decisions behind this package are recorded in the first consuming repo,
under .claude/decisions/ — principally ADR-0008 (why this is a shared
dependency rather than vendored, and why it does not live inside the corpus
server's repo) and ADR-0002 (why identity is minted in the topic repo).