Status: Superseded by ADR-270 — its D1-D2 are D4, D3-D7 are D19-D23, D8 is D15 there
Date: October 2, 2026
Answers: the owner’s decision not to hold the solver’s search to
determinism - it slows a solver that is meant to be fast, and a proof one
machine, one cache or one long run has found is legitimate on every other -
together with the owner’s condition that what one machine proved must not be
rejected by CI or by a colleague’s machine
Supersedes: ADR-265 D4 (an answer depends on the query and
the budget, never on time or threads); ADR-267 D7 (switching
only at work counts), D8 (a result cache keyed with the budget, lemmas shared
only as a committed input), D11 (the parallel rules) and D12’s criteria that
rest on them - see D9
Related: ADR-264 D4 (a check is dropped where a certificate
shows it holds), ADR-265 D5 (a certificate makes a solver usable
without trusting it), ADR-266 D5-D6 (the ledger publishes
contracts; a contract changed in the breaking direction is said), Part III
13.1 and 13.5 (nikaia.lock, nikaia.contracts, the determinism guarantee
and --locked), ADR-021 (a build is keyed on content). Sizes:
docs/solver-workload.md
ADR-265 D4 made every answer of the solver a function of the query and a budget counted in work, so that two machines build the same program. It is paid for in speed: no portfolio that stops at the first engine to finish, no time limits, clause sharing only at epochs where every thread waits for the slowest, nothing learned in one run used in the next.
The answer matters beyond the run that asked. It decides whether a check is
emitted (ADR-264 D4), whether a precondition or postcondition is published
in nikaia.contracts (ADR-266 D5), and whether NK1207 and NK1208 are
said. If one machine proves what another does not, the two build different
programs from one commit, CI rejects a ledger that a developer’s machine
wrote, and a warning appears on one desk and not on the next.
Can the search be free and the build still the same everywhere?
| Tool | Its answer | Source |
|---|---|---|
| Cargo | Cargo.lock records what resolution found; --locked fails where a build would change it. Resolution may depend on the registry’s state; the build does not, once the lock is committed |
Cargo documentation, from memory |
| Dafny, F*, Verus | Proofs are re-searched by z3 on every build; a proof that passes on one machine and times out on another is a known source of “brittle” or “flaky” verification, fought with resource limits (rlimit) instead of time |
from memory, not checked |
| Coq, Lean | The proof term is in the source and is checked, never searched, at build; a tactic that searches runs when the proof is written | from memory, not checked |
| Isabelle (Sledgehammer) | External provers search freely; what they find is replayed by the kernel and written into the theory as a reconstructable step | from memory, not checked |
What the last two do is what this record takes: search when writing, check when building, and keep what was found in the source. What does not transfer: the proof here is not written by the developer; the compiler finds it, so the file holding it is generated - as a lock file is.
Every machine finds the same proofs within the same work budget. Costs the speed the owner will not pay, and still does not let one machine’s long run help another.
Fastest, and the build depends on the machine: CI and a colleague may reject what worked, and a warning comes and goes. Rejected by the owner’s condition.
A machine reuses its own finds; CI and a colleague do not have them. The same failure as B one step later.
The search may do anything; the build uses only what the file holds, every entry of which is checked when read. A new find changes the file, and the file is committed with the code that needed it.
| D1 the special case pays | D2 the compiler works | D3 no cost for the common case | D4 consistent | If it is wrong | |
|---|---|---|---|---|---|
| A | No: every query pays for determinism | Yes | No: a parallel solver waits at epochs | Yes | Slow, never wrong |
| B | Yes | No: the developer chases machine-dependent builds | Yes | No | Builds differ by machine |
| C | Yes | No, for the same reason | Yes | No | Builds differ by cache |
| D | Yes: only a new query searches | Yes: the file is written by the build | Yes: CI checks, it does not search | Yes: one file decides | A forgotten commit fails --locked, with the fix in the message |
D is chosen.
A solver may limit itself by time, run engines against each other and take the first answer, share clauses whenever it likes, use every thread the machine has, and read and write caches and lemma stores of its own. Nothing about the search has to be reproducible. Work counters stay what they are good for: measuring, and the features a strategy reads (ADR-267 D7).
nikaia.proofs and the toolchain.Every query the prover asks while building a package has an entry in that
package’s nikaia.proofs, committed beside nikaia.contracts. The emitted
code, the published contracts and the diagnostics NK1207 and NK1208 are
computed from the entries, never from a search that was not recorded. Part
III 13.5’s determinism guarantee holds with nikaia.proofs among its inputs:
the same sources, the same nikaia.proofs and the same toolchain give the
same program, the same nikaia.contracts and the same diagnostics on every
machine.
nikaia.lock already uses. It does not include the solver’s
version: a proof stays a proof when the solver changes.proved with its certificate (ADR-267 D4’s proof log, about the normal
form);refuted with its model;unknown with the version of the solver that gave up - no evidence,
because an unknown only keeps a check and says nothing.nikaia.proofs.d/, named by its key, and the line points to it.A proved entry’s certificate is replayed by the checker (ADR-265 D5) against
the query in hand; a refuted entry’s model is evaluated. An entry that does
not check - a corrupt file, a hand edit, a hash collision, a frontend that now
asks a different query under the same key - counts as missing. Nothing in
the file can make the compiler drop a check that does not hold or say a
warning that is not true: the file needs no more trust than the solver does.
Checking costs 1.9 µs per certificate on average today.
For each query: an entry that checks is used. A missing one is searched for,
freely (D1), and its answer written. An unknown entry is searched again
only when the solver is newer than the version it records, or when the
developer asks for it (nikaia prove, with a larger limit). Entries no query
asked are removed. The file is written before the ledger is derived, so the
ledger follows from the file as it now stands. A changed nikaia.proofs is
part of the change the developer commits, as a changed nikaia.contracts is.
--locked searches nothing and fails on any difference.nikaia build --locked reads the file, checks every entry it uses, and
compares the set of keys the build asks with the set the file holds. A query
with no entry, or an entry that no longer checks, fails the build and names
the query’s source position and the fix - run nikaia build and commit
nikaia.proofs; an entry nothing asks fails it too, so the file cannot rot.
An unknown entry is used as it is: the check stays. CI with --locked
therefore never rejects what a developer’s machine built and committed, and
never accepts what it did not: the difference is the uncommitted file, and
the message says so. A CI build with --locked costs a check per entry, not
a search.
nikaia.proofs is resolved like a lock file’s:
take both sides’ entries, or either, and run nikaia build. Taking both is
always safe, because every entry is checked (D4).nikaia.proofs for the queries its own
build asks. A query asked while building the consumer - a precondition at a
call into the dependency - is the consumer’s, and lives in the consumer’s
file.A local result cache, a lemma store, measurements per query and anything a
strategy learns live under target/nikaia/, are not committed and are never
read under --locked. They are the search’s, and D1 lets the search do what
it likes.
nikaia.proofs (D2-D3). The lemma store may grow
during builds; it is local.--locked build is byte-identical on every machine
and at every thread count, and every entry checks. Step 2 builds
nikaia.proofs, D5 and D6 in place of the result cache.nikaia.proofs as it can change
nikaia.contracts, and is narrated the same way.nikaia.proofs is shipped or rebuilt by
the consumer when its sources are present; it is shipped like its ledger
until a case says otherwise.