Cairn

Compile-time verification for your code, and for what people experience.

Every action in a Cairn app is a claim with a proof. If a proof fails, the app doesn't build.

Get started Try it Read the docs v0.5.2

What a claim is

A claim is one statement about the app under one id, with a half for people and a half for the checker. Here is the whole of Reset in Counter, one of the example apps:

move ^2 reset (s : State) "Reset" in ^3 where12  does "Press Reset."3  sees "The count is zero, and Reset disappears."  enabled := resetEnabled s45  on press    apply := { s with count := 0 }6    post s' := s'.count = 0 ∧ resetEnabled s' = false7    sound := by intro s _; exact ⟨rfl, rfl⟩8
  1. The id Permanent: never renumbered or reused, and what reviews, docs and agents cite. Like a data-testid that can't drift.
  2. Where it lives ^3 is a claim too, a place: Counter's window.
  3. For people does and sees, in plain English for reviewers and the claim's page. Lean doesn't check them.
  4. When it's offered The button is proved on screen exactly when this holds, in every state. Playwright's toBeVisible() checks one page load.
  5. For the checker Everything below. Lean checks it when the app builds.
  6. What it does A pure copy of s with the count at 0.
  7. The promise Afterwards the count is zero and Reset is gone. An expect() stated for every state, not asserted on one run.
  8. The proof Like assert_eq!, except the compiler settles it for every s. No proof, no build.

Because the update is built only from moves, two things hold for every app, proved once in the framework: an action that no claim owns changes nothing and asks nothing of the host (update_unclaimed), and every effect the app does ask for comes from a move (update_effect). Claims shows one running, and what they buy you.

Ids are the contract

An id is a data-testid that can't drift, and semver for guarantees. ^2 means Reset for as long as the counter exists: never renumbered or reused, and a claim that goes away is retired with a reason. claims.lock records every id with a hash of its statement; the build fails when an id vanishes or changes kind, and warns when a statement's meaning changes. A repeated control carries a key (^5#12 is ^5 for item 12), and each package numbers in its own scope (cairn^N for Cairn's own).

Six kinds

KindWhat it saysExamples
moveSomething a person can do, what follows, and the proof.^1 Increment, ^2 Reset
flowA goal and a path of moves that reaches it. Lean checks every step is really answered.^4 Count to three, ^5 Start over
invariantA property every move keeps, so it holds in every state the app can reach.yap^311 A shortcut never sends (Yap)
assumeSomething the proofs rely on but can't prove. Recorded, and visible in every theorem that uses it.cairn^7 The host reports clicks where they happen
placeA region of the view where moves appear.^3 Counter window, in the figure above
retireA tombstone: the claim is gone, the id stays issued.^6 No press left in progress: the runtime owns presses now

How sure we are

Every claim carries its evidence, and its claim page shows which kind. Strongest first:

Each one rules out something different and still trusts something: how much to trust a claim says what, with a bug the tests miss and the proof catches.

Every package on the registry gets claim pages: every claim it makes, grouped by place, with its proofs, generated from what it published and nothing written by hand. See the example apps' for Counter, Basics and TodoMVC, and Cairn's for its trusted base.

The idea

A cairn is a stack of stones that marks a trail: placed by hand, built to last, and trusted by whoever comes next. Here the trails are a product's flows, and the stones are claims about them: “pressing ^12 clears the completed items”, “^12's button is visible and on top whenever there's something to clear”. Each claim is backed by a Lean proof, a model check or a conformance run against the real app.

To make those claims mean something, the code they talk about has to be the code that runs. So the app's state, view, layout, hit testing, focus and text editing are all pure Lean, and the theorems are about exactly those functions.

Main thread Swift, AppKit trustedkept thin

key pressfrom AppKit
queueevents, as bytes
accessibility, text inputanswered from frame 42

Lean thread owns the state provedclaims are theorems

updateviewlayouthit mapencode
pure, total functions
latest frameframe 42, as bytes

Render thread Swift, Metal trustedprobes, goldens

One frame, from a key press to the screen. The render thread repaints the latest frame on every display refresh, and accessibility answers from it: neither waits for Lean.