Encapp.Workflow
A workflow is the same List (String × Payload) that reflRun already interprets, wrapped with
a name and its post-conditions. That is what makes one definition serve as both a proof and a
corpus entry.
WStep
inductive WStep where
| fire : String → Payload → WStep -- click / nav → step(tag, payload)
| input : String → Atom → WStep -- typing → set a scalar
| back : WStep -- browser history backdef WStep.apply (hs : List RHandler) (d : Doc) : WStep → Doc
| .fire tag p => reflStep hs tag p d
| .input k v => d.setScalar k v
| .back => dback is identity in the proof — browser history is a runtime concern, not part of the Doc
model — and is genuinely executed by the replayers. Assert its effect with a ui check.
WCheck
inductive WCheck where
| scalarEq : String → Atom → WCheck
| tableLen : String → Nat → WCheck
| rowField : String → Nat → String → Atom → WCheck
deriving DecidableEq, Inhabiteddef WCheck.eval (d : Doc) : WCheck → BoolTotal and decidable, which is what lets the whole workflow reduce under native_decide. An
out-of-range rowField index evaluates to false rather than failing.
UICheck
inductive UICheck where
| seesText : String → UICheck
| namedControl : String → UICheckReplay-only DOM assertions. Not part of the proof — the Doc model does not describe the
rendered page. The replayers evaluate them against the live DOM.
Workflow
structure Workflow where
name : String
users : List String := ["alice"]
steps : List WStep
checks : List WCheck := []
ui : List UICheck := []
deriving InhabitedThe proof tier
def Workflow.run (app : ReflApp β) (w : Workflow) : Doc :=
w.steps.foldl (WStep.apply app.handlers) app.init
def Workflow.holds (app : ReflApp β) (w : Workflow) : Bool :=
w.checks.all (WCheck.eval (w.run app))holds is a Bool, so:
theorem w_holds : w.holds app = true := by native_decideThe app logic is proven at build time. If a handler or the interpreter drifts, it fails to compile.
Aggregate over a corpus:
theorem corpus_all_hold : allWorkflows.all (·.holds app) = true := by native_decide
theorem corpus_single_user :
allWorkflows.all (fun w => decide (w.users.length ≤ 1)) = true := by native_decidecorpus_single_user guards the emit: Workflow.toJson renders steps as a single user, so a
two-user workflow would be silently misrepresented in the corpus. The theorem makes the
boundary explicit.
The execution tier
def Workflow.toJson (w : Workflow) : StringRenders the runner-shaped JSON the replayers execute:
{ "name": "...", "users": ["alice"],
"steps": [ {"as":"alice","do":"fire","tag":"...","payload":...},
{"as":"alice","do":"input","field":"...","value":...},
{"as":"alice","do":"back"} ],
"checks": [ {"kind":"scalar","key":"...","eq":...},
{"kind":"tableLen","table":"...","eq":N},
{"kind":"rowField","table":"...","i":N,"field":"...","eq":...} ],
"ui": [ {"kind":"seesText","text":"..."},
{"kind":"namedControl","name":"..."} ] }Supporting serializers: payloadJson, WStep.json, WCheck.json, UICheck.json.
Boundary
reflRun is single-Doc, so the proof tier is single-app-instance logic. Multi-user and
cross-enclave workflows belong to the node layer; the emitted corpus carries users so a
multi-client runner can project it, but the theorem is about one instance.
See Workflows and proofs for the guide, and Testing for the replayers and coverage gates.