AppView = (Enclave, @component specs, CSSApp). Lean DSL → React. 15 Cmd effects, 44 components, 214 lines handwritten JS.
A frontend application is fully determined by:
AppView = (Enclave, @component specs, CSSApp)
The Enclave (from server.md) provides writes, reads, and protocol operations. The @component specs (from Lean) provide state, messages, update logic, and view structure. The CSSApp (from Theme.lean) provides theme and CSS rules.
Every React component, hook, handler, effect, and CSS rule follows from this structure. No creative decisions.
server.md Enclave → runtime.ts writes + reads
client.md Client → runtime.ts SDK calls via Cmd
enc.md AppSDK W → Cmd.lean commit, fetch, navigate, toast
Lean specs @component JSON → gen_react.py state machine + JSX
The DSL consists of types organized in 4 groups:
Effects: Cmd (none, fetch, commit, navigate, toast, subscribe, storeGet, storeSet,
fetchJson, commitSigned, debounce, clipboard, focus, scrollToBottom, batch)
State: StateField (name, type, init)
Messages: MessageDef (name, payload?)
Update: Case (guard?, state?, cmd)
View: Element (tag, cls, attrs, on, children, if/then/else, map, ref, icon, expr, text, call)
Shared: Icon, Helper, SharedElement (from Elements.lean)
Styling: Var, Rule, MediaQuery, Animation, CSSApp (from CSS.lean)
The only handwritten JavaScript:
axioms/frontend/runtime.ts (214 lines).
// Cmd constructors (pure data, no side effects)
Cmd.none | Cmd.fetch | Cmd.commit | Cmd.navigate | Cmd.toast
| Cmd.subscribe | Cmd.storeGet | Cmd.storeSet | Cmd.fetchJson
| Cmd.commitSigned | Cmd.debounce | Cmd.clipboard | Cmd.focus
| Cmd.scrollToBottom | Cmd.batch
// Runtime hook
useLeanComponent(component, ctx) → [state, dispatch]
// Effect executor
executeCmd(cmd, dispatch, ctx) → void // pattern-match Cmd → browser effects
The runtime is the trust boundary. Everything above it (Lean specs, generated React, generated tests) is mechanically verifiable. Everything below it (browser APIs, network, DOM) is the execution environment.
Defined in spec-lean/Enc/Frontend/Cmd.lean:
inductive Cmd (Msg : Type)
| none
| fetch (method : HttpMethod) (url : String) (body : Option String)
(onOk : String → Msg) (onErr : String → Msg)
| commit (commitType : String) (content : String) (tags : List (List String))
(onOk : Msg) (onErr : String → Msg)
| navigate (path : String)
| toast (message : String) (level : String)
| subscribe (eventType : String) (onMsg : String → Msg)
| storeGet (key : String) (onResult : Option String → Msg)
| storeSet (key : String) (value : String)
| fetchJson (method : HttpMethod) (url : String) (body : Option String)
(onOk : String → Msg) (onErr : String → Msg)
| commitSigned (commitType : String) (content : String) (tags : List String)
(onOk : Msg) (onErr : String → Msg)
| debounce (key : String) (delayMs : Nat) (msg : Msg)
| clipboard (text : String)
| focus (elementId : String)
| scrollToBottom (elementId : String)
| batch (cmds : List (Cmd Msg))
Update signature: State → Msg → State × Cmd Msg
This is the key insight: effects are data. The update function returns a Cmd description; the runtime executes it. Lean can reason about Cmds without touching the browser.
Update cases return (state, cmd) where cmd is:
"cmd": "none"
"cmd": { "toast": ["message", "level"] }
"cmd": { "navigate": "/path" }
"cmd": { "commit": ["CommitType", "content", "tags", "OkMsg", "ErrMsg"] }
"cmd": { "fetch": ["url", "onOkExpr", "onErrExpr"] }
"cmd": { "batch": [cmd1, cmd2, ...] }
"cmd": { "storeGet": ["key", "LoadedMsg"] }
Guards:
"guard": "!state.text.trim()",
"guardCmd": { "toast": ["Enter text", "error"] },
"guardOver": "state.text.length > MAX",
"guardOverCmd": { "toast": ["`Exceeds ${MAX} chars`", "error"] }
Defined in spec-lean/Enc/Frontend/Component.lean:
Component = name + state + messages + update + view + subscriptions
Each component is a JSON block inside a Lean doc-comment:
/-! @component
{
"name": "ComponentName",
"app": "app-slug",
"route": "/path/:param",
"state": [
{ "name": "fieldName", "type": "TypeAnnotation", "init": "jsExpression" }
],
"messages": [
{ "name": "MsgName", "payload": "Type" },
{ "name": "MsgWithoutPayload" }
],
"consts": { "MAX_LENGTH": 280 },
"update": {
"MsgName": {
"guard": "boolExpr",
"guardCmd": { "toast": ["message", "level"] },
"state": { "field": "newValueExpr" },
"cmd": { "commit": ["Type", "content", "tags", "OkMsg", "ErrMsg"] }
}
},
"initCmd": { "fetch": ["url", "onOk", "onErr"] },
"subscriptions": [{ "event": "EventType", "msg": "handler" }],
"view": { ... },
"authRequired": true,
"authFallback": { ... }
}
-/
The view tree uses a JSON element representation:
{ "tag": "div", "cls": "class-name", "children": [...] }
{ "tag": "button", "cls": "btn", "on": { "click": "MsgName" }, "children": [...] }
{ "tag": "input", "attrs": { "type": "text", "value": "state.field" }, "on": { "input": "SetField(e.target.value)" } }
{ "if": "condition", "then": { ... }, "else": { ... } }
{ "map": "state.items", "as": "item", "body": { ... } }
{ "tag": "span", "cls": "tab", "clsIf": ["state.active === 'x'", "active"] }
{ "text": "Static text" }
{ "expr": "state.count > 0 ? state.count : ''" }
{ "icon": "back" }
{ "ref": "spinner" }
{ "call": "identicon", "args": ["pubHex", 40] }
Defined in spec-lean/Enc/Frontend/CSS.lean (types) and
spec-lean/Enc/Frontend/Theme.lean (rules):
/-! @css
{
"name": "shared",
"theme": { "--bg": "#000", "--text": "#e7e9ea", ... },
"rules": { ".badge": "background: var(--accent); ...", ... }
}
-/
CSSApp = theme (variables) + base (reset) + rules (per-class)
+ media (responsive) + animations (keyframes)
Theme variables are shared across all 5 apps. Each app has its own stylesheet (axioms/frontend/apps/*/style.css) plus the generated supplement (ts/core/frontend/generated-supplement.css).
gen_css.py reads @css to generate generated-supplement.css
and verifies 100% class coverage.
@component (Lean) → gen_react.py → .tsx (React + useReducer)
@elements (Lean) → gen_react.py → inlined in .tsx
@css (Lean) → gen_css.py → .css supplement
@component (Lean) → gen_tests.py → .test.js files
parse_component_spec(lean_path) — extract @component JSONload_element_library() — load @elements from Elements.leanrender_element(el) — Element JSON → JSX stringgen_component(spec) — full component → .tsxrender_cmd(cmd) — Cmd JSON → Cmd.xxx() callmain() — glob all Components/*.lean → generate .tsx filesFor each component, generates: import statements,
helper functions (from Elements.lean), constants, init
state object, update function (switch on msg.type), React
component with useReducer +
queueMicrotask(executeCmd), auth guard with fallback view
(if authRequired), JSX view tree.
extract_react_classes() — scan generated .tsx for classNamesextract_css_classes() — scan existing + generated CSSgenerate_supplement() — Theme.lean @css → CSS fileverify() — check 100% coverageparse_spec(lean_path) — extract @component JSONgen_update_fn(spec) — generate pure update functiongen_tests(spec) — generate test suite per componentmain() — glob all Components/*.lean → generate .test.js| App | Components | Generated React Lines |
|---|---|---|
| Timeline | 9 | 1,180 |
| DM | 7 | 602 |
| Chat | 8 | 629 |
| Messenger | 13 | 974 |
| Registry | 7 | 519 |
| Total | 44 | 3,904 |
Timeline: Feed, Compose, PostDetail, Profile, Search, Settings, Setup, Likes, TimelineView
DM: Inbox, Thread, Friends, Compose, Profile, Settings, Setup
Chat: GroupThread, GroupList, GroupInfo, Members, Compose, Profile, Settings, Setup
Messenger: ConversationList, DMThread, ChatThread, TimelineFeed, Profile, Search, Friends, ComposeChat, ComposeDM, GroupInfo, Members, Settings, Setup
Registry: RegisterIdentity, Nodes, NodeDetail, RegisterNode, IdentityDetail, EnclaveExplorer, ManifestWizard
| Property | Verified By |
|---|---|
| Messages complete | gen_tests.py (every msg has handler) |
| Update total | gen_tests.py (unknown msg → unchanged) |
| Guards safe | gen_tests.py (guard rejects correctly) |
| Cmds typed | gen_tests.py (commit/navigate/toast) |
| CSS coverage | gen_css.py (100% class coverage) |
| State init | gen_tests.py (init matches fields) |
| Proofs | ComponentProofs.lean |
44 components × ~5 tests each = 236 auto-generated tests.
| Category | Tests |
|---|---|
| Backend (axiom contracts) | 906 |
| Frontend hand-written | 54 |
| Frontend auto-generated | 236 |
| Total | 1,196 |
CSS class coverage: 107/107 non-wrapper classes = 100%
| Type | Lean File | Purpose |
|---|---|---|
Cmd Msg | Cmd.lean | 15 effect constructors |
Component | Component.lean | Component structure |
Element | (in @component) | View tree nodes |
Icon | Elements.lean | 6 SVG icons |
SharedElement | Elements.lean | 7 reusable elements |
Helper | Elements.lean | 3 utility functions |
Var | CSS.lean | CSS custom property |
Rule | CSS.lean | CSS selector + declarations |
MediaQuery | CSS.lean | Responsive breakpoint |
Animation | CSS.lean | Keyframe animation |
CSSApp | CSS.lean | Complete stylesheet |
| File | Lines | Purpose |
|---|---|---|
Cmd.lean | 62 | Effect type (Cmd Msg) |
Component.lean | 66 | Component structure |
Elements.lean | 189 | Shared icons, elements, helpers |
CSS.lean | 55 | CSS structure types |
Theme.lean | 79 | Theme + supplement rules |
ComponentProofs.lean | 46 | Well-formedness proofs |
runtime.ts | 214 | Only handwritten JS |
gen_react.py | 351 | Lean → React codegen |
gen_css.py | 68 | CSS gen + coverage |
gen_tests.py | 178 | Auto-generate tests |
Lean Cmd.lean ↔ runtime.ts Cmd constructors
Lean @component state ↔ React useReducer init
Lean @component update ↔ React update function (pure)
Lean @component view ↔ React JSX (gen_react.py)
Lean @elements ↔ Inlined icons/helpers
Lean @css Theme.lean ↔ generated-supplement.css
Lean ComponentProofs ↔ gen_tests.py assertions