Architecture
Layer map
┌──────────────────────────────────────────────────────────────┐
│ YOUR APP Encapp/Examples/Hello.lean │
│ typed face App Model Msg · View Msg │
│ reflected ReflApp β · RView · Eff · RHandler │
│ tied by theorem hello_reflected_matches │
└───────────────┬──────────────────────────────────────────────┘
│
┌────────────────────┴───────────────────┬─────────────────────────┐
│ AUTHORING │ VERIFICATION │
│ Core.lean App / Cmd / Event │ Workflow.lean │
│ View.lean the typed vocabulary │ WStep · WCheck │
│ Ui.lean the minimal vocabulary │ UICheck · Workflow │
│ ViewDsl.lean authoring sugar │ → native_decide proof │
│ Doc.lean Atom · Row · Doc │ → runner JSON corpus │
└────────────────────┬───────────────────┴─────────────────────────┘
│
┌────────────────────┴─────────────────────────────────────────────┐
│ SEMANTICS + PROOF │
│ Store.lean World · Refines · refines_sound · refines_log │
│ Adapter.lean reversedWorld — a real refinement witness │
│ Replay.lean applyEv_idem · replayLog_append │
│ Bridge.lean the interpreter as a Foundation.World / Layer │
└────────────────────┬─────────────────────────────────────────────┘
│
┌────────────────────┴─────────────────────────────────────────────┐
│ CODEGEN │
│ AppGen.lean reflStep (the Lean spec) │
│ appCoreJs — the shared state machine │
│ appDomJs — the web renderer │
│ appRnJs — the React Native renderer │
│ emit · emitNative · emitDesktop* │
│ EmuGen.lean the ENC emulator authored as RView data │
│ Codegen.lean the minimal Spec → SPA path (counter) │
└──────────────────────────────────────────────────────────────────┘
The two faces, and the theorem between them
An app is authored twice and proven identical.
-- typed: a real inductive Msg, type-safe buttons, ergonomic
def helloApp : App Model Msg := …
-- reflected: string tags, a finite handler table, serializable
def helloReflected : ReflApp Unit := …
-- the tie: the reflected interpreter agrees with the typed update
theorem hello_reflected_matches : … := by native_decideWithout that theorem the reflected form would be a hand-maintained copy, which is exactly the drift the framework exists to prevent.
Doc — the state model
Real apps need more than a counter's key → Int. Doc is named scalars plus named tables of
rows, where every leaf is a typed Atom:
inductive Atom where
| str : String → Atom | int : Int → Atom | bool : Bool → Atom | null : Atom
abbrev Row := List (String × Atom)
structure Doc where
scalars : List (String × Atom)
tables : List (String × List Row)Everything is DecidableEq, which is what makes native_decide able to settle a workflow's
post-conditions. See Encapp.Doc.
From one value to three artifacts
AppGen.emit serializes the ReflApp into JSON, embeds it in an HTML document together with
the runtime, and writes one file. The runtime is two concatenated strings:
def appRuntimeJs : String := appCoreJs ++ "\n" ++ appDomJsappCoreJs— the state machine:S(theDoc),step(tag, payload), theEffinterpreter, gates and routing. It is the operational mirror ofreflStep.appDomJs— the DOM renderer:RView→ DOM nodes, event wiring, history.appRnJs— the React Native renderer: the sameRViewvocabulary → RN elements.
emitNative emits appCoreJs ++ appRnJs. Because the core string is literally the same Lean
definition, a test can assert byte-identity between the web and native cores — and
test/native-core-identity.mjs does exactly that.
Desktop does not get a third renderer. emitDesktopMain / emitDesktopPreload /
emitDesktopPackageJson produce a generic Electron shell that loads the web artifact, so the
desktop renderer cannot drift from web by construction.
The runtime hook
The emitted page exposes a small handle:
window.__enc = { S, step, render }S is the live Doc, step(tag, payload) dispatches a handler, render() repaints. Every
replayer drives the app through this hook, which is why the same corpus runs on happy-dom,
Chromium, React-Native-web and Electron without per-tier test code.
The replayers mirror the runtime exactly: an input step sets a scalar with no re-render
(matching the emitted oninput, which assigns S.scalars[f]), and a fire step dispatches
and then renders (matching step). Adding a render after input would make the test diverge
from the real app.
Dependency direction
Encapp depends on Foundation (for the Layer.mono keystone used by Bridge.lean) and on
nothing else. It has no dependency on any protocol, SDK or network library. Apps depend on
Encapp; Encapp never depends on an app.
The one place an app's world could leak in is the host binding, and that is why
HostBinding is a typeclass with a single toJson method and no
knowledge of what a binding means.