LeanTEA

Lean 4 + TEA (The Elm Architecture). A tiny full-stack framework โ€” and a handful of apps built on top of it (a functional spreadsheet with an MCP endpoint, a board-game SPA, a Chrome-CDP MCP server, and several other AI-driving MCP servers).

ci pages Discord

๐Ÿ“– Read the docs โ€” the LeanTEA book ยท HTTP benchmark trend

Questions, design discussion, and weekly progress threads live in the Discord channel above.

  • Pure Lean stack: HTTP server, WebSocket client, and SQLite live in Lean.
  • No Node.js, no Python at runtime (Python is used only by a few build helpers in tools/).
  • SQLite is vendored โ€” c/sqlite3.c is the amalgamation, linked into the binary, so deployment doesn't need -lsqlite3.
  • The browser sees plain HTML + inlined CSS, plus one small runtime JS file served from the same origin (/runtime.js). Beyond the standard fetch / History / DOM APIs any SPA needs, the Web Speech API is the only domain-specific browser API used.
  • Matches (slightly beats) tuned nginx on hello-world / 5-field JSON: the LeanTea.Net.ReactorServer backend is a c/leantea_reactor.c non-blocking event loop (kqueue on macOS/BSD, epoll on Linux) driving a Lean ByteArray โ†’ IO ByteArray callback per request. 72 k RPS at c=128 vs nginx 69 k on the same box. Full numbers in docs/BENCHMARKS.md.

Inspirations

The name LeanTEA is Lean 4 + TEA โ€” The Elm Architecture. The framework borrows ideas across the Elm + Haskell-flavoured ecosystem and ports them into Lean 4:

  • The Elm Architecture (TEA) โ€” Model / Msg / update / view on both the TUI and the in-browser runtimes. Lean's structures + dependent types let Msg be an inductive that the compiler exhaustiveness-checks for you.
  • Yesod โ€” the "full-stack typed web framework" framing: routing, sessions, OAuth, and templates as first-class Lean values rather than stringly-typed configuration.
  • Persistent โ€” the Entity / Repo typeclasses under LeanTea.Persist.* are Persistent-style: define a record, derive a backend-agnostic store, swap SQLite / MySQL / in-memory at the call site.
  • Servant โ€” the typed-RPC layer (LeanTea.Rpc) treats the API surface as a Lean type the server and client share. No hand-written JSON wrangling, no schema drift.

Not affiliated with or endorsed by the Elm, Yesod, Persistent, or Servant projects.

Sibling projects in the Lean 4 ecosystem

  • Verso โ€” a Lean-native authoring tool for documentation and books (Scribble / Sphinx lineage). Complementary to LeanTEA, not overlapping: Verso generates static documents; LeanTEA serves dynamic web apps. We're considering migrating docs/ to Verso once the framework stabilises so the code snippets in the book are actually type-checked.

Secure by Construction

LeanTEA's headline property is that whole classes of vulnerabilities can't be expressed in user code that compiles. Nine primitives ship today; one more is planned. The shipped set covers most of the IPA ใ€Œๅฎ‰ๅ…จใชใ‚ฆใ‚งใƒ–ใ‚ตใ‚คใƒˆใฎไฝœใ‚Šๆ–นใ€ 11 categories and OWASP Top 10 2021. See SECURITY.md for the design + threat model, docs/11 for the walk-through, and ROADMAP.md for sequencing.

Vulnerability classLeanTEA primitiveIPA / OWASPStatus
Authorization bypass / IDORLeanTea.Auth.Proof (Proof c + Capability lattice + dependent Proof (.owner id))IPA ยง3.7 / A01โœ… shipped โ€” walk ยท demo
SQL injectionLeanTea.Persist.SafeQuery (typed Where / Select / Update / Delete + .trusted decl_name% audit)IPA ยง3.1 / A03โœ… shipped โ€” walk ยท demo
XSS (URL scheme + event-handler names)LeanTea.Html.SafeAttr (private mk + URL allow-list + on* rejection)IPA ยง3.5 / A03โœ… shipped โ€” walk ยท demo
Path traversalLeanTea.Net.SafePath (workspace-relative + .. / NUL / sibling-prefix reject)IPA ยง3.4 / A01โœ… shipped โ€” walk ยท demo
OS command injectionLeanTea.Os.SafeCmd (args : List String + shell-name allow-list reject + grep-able SafeCmd.shell audit)IPA ยง3.3 / A03โœ… shipped โ€” walk ยท demo
HTTP header injectionResponse.setHeader (CR / LF / NUL reject)IPA ยง3.6 / A03โœ… shipped โ€” walk ยท demo
Clickjacking + MIME sniffingResponse.defaultSecurityHeaders (XFO / nosniff / Referrer-Policy / Permissions-Policy)IPA ยง3.10 / A05โœ… shipped โ€” walk ยท demo
Open redirectLeanTea.Net.SafeRedirect (allow-listed origin + relative-path-only mode + scheme reject + sibling-prefix reject)IPA ยง3.9 / A01โœ… shipped โ€” walk ยท demo
CSP typos / misconfigurationLeanTea.Net.Csp (typed CspSrc directives โ€” a mistyped source or directive doesn't compile)IPA ยง3.5 / A05โœ… shipped โ€” demo
Invalid state transitionsOrderState / Transition s s' style proofsโ€”๐Ÿšง planned

Snippet โ€” SafeQuery rejects string-shaped SQL at compile time

-- โœ… Compiles โ€” typed builders, positionally bound:
let rows โ† SafeQuery.run users
  { where_ := .and (UserCols.email.eq "alice@x.com")
                   (.not (UserCols.deleted.eq true)) }

-- โŒ Compile error โ€” `Where.eq` is `private` to SafeQuery.lean.
--   The framework gives no path from a raw String to a `Where` clause.
let bad := Where.eq "email" rawUserInput
-- error: Unknown constant `LeanTea.Persist.SafeQuery.Where.eq`

Snippet โ€” Auth.Proof enforces the auth check in the type signature

-- The admin handler demands an unforgeable `Proof .admin`:
def handleAdminDelete (proof : Proof .admin) (req : Request) : IO Response := โ€ฆ

-- Removing the `proof` parameter breaks the route registration:
def handleAdminDelete (req : Request) : IO Response := โ€ฆ
-- error: Type mismatch in route registration
--   expected: Proof .admin โ†’ Request โ†’ IO Response
--   got:      Request โ†’ IO Response

The proof's mk is private to the auth module โ€” only Proof.issue (which checks the session) can mint one. Forgetting the auth check is now a build failure, not a CVE.

To close the "added a route but forgot to guard it at all" gap, collect routes as a SecureRoute list: every entry must be either .needs c (proof enforced) or .anyone (explicitly public). There's no unstated third case, so an unguarded endpoint is a visible, greppable .anyone rather than an accidental omission.

For the full walk-through (capability lattice, dependent Proof (.owner id), the .trusted decl_name% audit-grep escape, the ~480-LOC trusted core across the shipped primitives), see docs/11-secure-by-construction.md.

Layout

LeanTea/
โ”œโ”€โ”€ Cmd.lean Sub.lean Runtime.lean    -- TUI Elm runtime
โ”œโ”€โ”€ Web.lean Html.lean Css.lean Js.lean -- WebApp (Model/Msg/update/view) + DSLs
โ”œโ”€โ”€ Template.lean                     -- {{var}} / {{#each}} / {{#if}} / {{#include}}
โ”œโ”€โ”€ Rpc.lean JsonRpc.lean             -- Servant-style typed RPC + JSON-RPC envelope
โ”œโ”€โ”€ Mcp.lean                          -- MCP Handler (stdio + HTTP transports)
โ”œโ”€โ”€ Markdown.lean Markdown/           -- CommonMark-ish parser
โ”œโ”€โ”€ Json/                             -- terse Json accessors (.getStrD etc.)
โ”œโ”€โ”€ Net/
โ”‚   โ”œโ”€โ”€ Http.lean Server.lean         -- HTTP/1.1 server + Request/Response/Handler
โ”‚   โ”œโ”€โ”€ HttpClient.lean               -- pure-Lean HTTP/1.1 client
โ”‚   โ”œโ”€โ”€ WebSocket.lean                -- pure-Lean RFC 6455 client (handshake, masking)
โ”‚   โ”œโ”€โ”€ Desktop.lean Memcached.lean   -- OS desktop FFI, memcached client
โ”œโ”€โ”€ Persist/
โ”‚   โ”œโ”€โ”€ Sqlite.lean Mysql.lean        -- backend FFI
โ”‚   โ”œโ”€โ”€ Store.lean Query.lean Backend.lean Migrate.lean -- Entity / Repo / migration
โ”‚   โ””โ”€โ”€ SafeQuery.lean                -- typed Where / Select / Update / Delete (no `String โ†’ SQL`)
โ”œโ”€โ”€ Auth.lean                         -- session store
โ”‚   โ”œโ”€โ”€ OAuth2.lean Saml.lean Passkey.lean Security.lean
โ”‚   โ””โ”€โ”€ Proof.lean                    -- Capability + Proof.issue (Authorization)
โ”œโ”€โ”€ Crypto/                           -- Base64 / SHA-1 / SHA-256 / HMAC / PBKDF2 / JWT
โ”œโ”€โ”€ Browser.lean Comfy.lean Diffuse.lean -- 3rd-party tool bridges
โ”œโ”€โ”€ Llm/Openai.lean                   -- streaming OpenAI-compatible client (LM Studio)
โ”œโ”€โ”€ Agent/                            -- run history, replayable scripts
โ”œโ”€โ”€ LSpec.lean                        -- tiny test runner (group / it / lspecIO)
โ””โ”€โ”€ assets/runtime.js styles.css      -- embedded client runtime

LeanJs/                               -- Lean-subset โ†’ JavaScript compiler
โ”œโ”€โ”€ Ast.lean Parser.lean JsParser.lean
โ”œโ”€โ”€ Check.lean                        -- arity + record-field guard
โ”œโ”€โ”€ Codegen.lean Eval.lean LeanEmit.lean Includes.lean

c/
โ”œโ”€โ”€ sqlite3.c sqlite3.h               -- SQLite amalgamation (vendored, ~9 MB)
โ”œโ”€โ”€ leantea_sqlite.c                  -- SQLite FFI wrapper
โ”œโ”€โ”€ leantea_mysql.c                   -- MySQL FFI (opt-in via LEANTEA_MYSQL=1)
โ”œโ”€โ”€ leantea_crypto.c                  -- OpenSSL bindings (opt-in via LEANTEA_CRYPTO=1)
โ””โ”€โ”€ leantea_desktop.c                 -- macOS Quartz bindings (opt-in via LEANTEA_DESKTOP=1)

examples/
โ”œโ”€โ”€ Counter/ Quiz/ CounterWeb/         -- TUI + browser TEA demos (~50 lines each)
โ”œโ”€โ”€ Sheet/                             -- functional spreadsheet + /mcp (typed Rpc + Persist + Mcp)
โ”œโ”€โ”€ Reversi/                           -- board game (.leanjs client, Lean server)
โ”œโ”€โ”€ Gpu/                               -- WebGPU demo
โ”œโ”€โ”€ ChromeCdpMcp/                      -- real-Chrome driver via CDP (10 tools)
โ”œโ”€โ”€ BrowserMcp/ BrowserAgent/          -- Playwright-driven browser + LLM agent
โ”œโ”€โ”€ ComfyuiMcp/                        -- ComfyUI HTTP/WebSocket driver
โ”œโ”€โ”€ DesktopMcp/                        -- OS-level mouse + screenshot (macOS Quartz)
โ”œโ”€โ”€ ImageMcp/                          -- HTML/CSS โ†’ PNG compositor
โ”œโ”€โ”€ UiScript/ UiReport/                -- AI-driven E2E test runner + HTML report
โ”œโ”€โ”€ Smoke/                             -- subsystem smoke tests (one per area)
โ”œโ”€โ”€ Tests/                             -- LeanJs spec runner
โ”œโ”€โ”€ Tools/                             -- gen_site + leanjs_{compile,interp,run} CLIs
โ””โ”€โ”€ Docs/                              -- runnable doc examples

tools/
โ”œโ”€โ”€ dev.py                             -- file watcher + auto reload for the dev loop
โ”œโ”€โ”€ browser-bridge/                    -- node + Playwright (used by BrowserMcp)
โ””โ”€โ”€ run-tests.sh run-docs.sh           -- CI entry points

Build and run

# Build everything (~3 min cold, seconds incrementally)
lake build

# In-browser counter (TEA in 50 lines)
./.lake/build/bin/counter_web --port 8001
open http://127.0.0.1:8001/

# Multi-user SVG editor + MCP endpoint at /mcp
./.lake/build/bin/sheet_serve --port 8002 --db ../.leantea-state/sheet.sqlite
open http://127.0.0.1:8002/

# Board game (Reversi) โ€” client logic is a .leanjs file compiled at startup
./.lake/build/bin/reversi_serve --port 8005

# Chrome-CDP MCP server (drives your already-open Chrome)
# 1. Launch Chrome with: --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-cdp
# 2. Then:
./.lake/build/bin/chrome_cdp_mcp_serve --stdio

Tests are organised into two consolidated LSpec runners plus a handful of subsystem smokes:

  • persist_spec โ€” Store roundtrips + Query DSL + Migration runner + Auth.Proof dispatch + SafeQuery typed SQL (32 assertions, one binary, one CI step).
  • security_spec โ€” SafeHtml + SafePath + SafeCmd + SafeHeader
    • SafeRedirect construction-time guarantees (60 assertions, one binary, one CI step).
  • template_smoke / crypto_smoke / http_*_smoke โ€” narrower per-module subsystem smokes that don't yet have an LSpec runner.

Read the runner source under examples/Tests/ as the shortest "this is what works" demonstration of any given area.

Architecture

[Browser]
  index.html  โ†โ”€โ”€โ”€โ”€ GET /, /styles.css, /runtime.js
  runtime.js  โ”€โ”€โ”
                โ”‚ fetch('/api/step?msg=...', X-Model: <encoded>)
                โ–ผ
[Lean: sheet_serve]
  Net.Server (Std.Internal.Async.TCP)
    โ†“
  SheetServe.handler
    โ”œโ”€ "/"                โ†’ render the toolbar + SVG host from Template
    โ”œโ”€ "/cells"          โ†’ SVG fragment built from Persist.Store.shapes
    โ”œโ”€ "/api/*"           โ†’ Rpc.dispatch (typed Endpoint records)
    โ”œโ”€ "/mcp"  (POST)     โ†’ LeanTea.Mcp.handleMcp (text / image content)
    โ””โ”€ everything else    โ†’ 404
[Lean: Persist.Store (SQLite via FFI)]
  shapes (id, kind, x, y, w, h, text, color, page_id)
  pages  (id, name)
  audit  (id, action, ts)

The client encodes the current Model in the X-Model header on every action; the server runs WebApp.step (pure) and ships the new model back the same way. SQLite is for things that need to outlive a restart (shape DB, sessions, audit). No middleware stack, no implicit context โ€” every clause in handler is one function from a Request to a Response.

Persistent-style typed DB API

structure CellRow where
  kind  : String  -- "rect" / "ellipse" / "text" / "sticky" / "pen"
  x y   : Int
  w h   : Int
  text  : String
  color : String

instance : Entity CellRow where
  table   := "shapes"
  ddl     := "CREATE TABLE IF NOT EXISTS shapes(...)"
  columns := ["kind", "x", "y", "w", "h", "text", "color"]
  toRow s   := #[s.kind, toString s.x, ..., s.color]
  fromRow r := ...

-- Usage โ€” SafeQuery makes SQL injection unrepresentable:
namespace CellCols
  open LeanTea.Persist.SafeQuery
  def kind : Col CellRow String := โŸจ"kind"โŸฉ
  def x    : Col CellRow Int    := โŸจ"x"โŸฉ
end CellCols

let shapes : Repo CellRow := Repo.new db
shapes.migrate
let _ โ† shapes.insert { kind := "rect", x := 0, y := 0, w := 80, h := 40,
                        text := "hello", color := "#38bdf8" }
let rects โ† SafeQuery.run shapes
  { where_ := .and (CellCols.kind.eq "rect")
                   (CellCols.x.gt 100) }

Where's value-leaf constructors are private to the SafeQuery module โ€” there's no path from a raw String into a Where, so an LLM-generated Where.eq "email" userInput is a compile error.

Sheet app + MCP server

examples/Sheet/ is a small functional spreadsheet:

lake build sheet_serve
./.lake/build/bin/sheet_serve --port 8002 --db ../.leantea-state/sheet.sqlite
open http://127.0.0.1:8002/
  • SVG rendering for rect / ellipse / text / sticky / freehand pen
  • Click to select, drag to move, drag corner handles to resize, double-click for in-place text editing (foreignObject editor), separate โœ๏ธ Pen tool, color picker and W / H inputs
  • State lives in SQLite (cells table)

MCP (Model Context Protocol) support

POST /mcp is a minimal JSON-RPC 2.0 endpoint so Claude or other clients can edit cells directly:

# handshake
curl -X POST http://127.0.0.1:8002/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'

# list tools
curl -X POST http://127.0.0.1:8002/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# add a shape
curl -X POST http://127.0.0.1:8002/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"set_cell",
                 "arguments":{"kind":"rect","x":100,"y":200,"text":"Hello"}}}'

Tools exposed:

  • add_shape(kind, x, y, w?, h?, text?, color?) โ†’ new id
  • move_cell(id, x, y), resize_shape(id, w, h)
  • set_text(id, text), set_color(id, color)
  • delete_shape(id), list_shapes(), clear_all()

When wiring this into Claude Desktop / Claude Code, point its MCP client at transport: "http", url: "http://localhost:8002/mcp".

Typed RPC (Servant-style, checked on both sides)

LeanTea.Rpc.Typed makes the request and response types part of the endpoint. One declaration is the single source of truth, and both the server and the browser client are checked against it โ€” a wrong field is a compile error, not a runtime surprise.

structure SetCellReq  where ref : String; formula : String
  deriving Lean.ToJson, Lean.FromJson
structure SetCellResp where ok : Bool;   value : String
  deriving Lean.ToJson, Lean.FromJson

def setCell : Endpoint SetCellReq SetCellResp :=
  { name := "apiSetCell", path := "/api/set",
    reqType := "SetCellReq", respType := "SetCellResp" }

Server. serve setCell (fun (r : SetCellReq) => โ€ฆ) โ€” the handler is SetCellReq โ†’ IO SetCellResp. Dispatch decodes the body to SetCellReq via the derived codec and encodes the result; a malformed or wrong-shape body becomes a 400 at runtime (the wire carries untrusted bytes, so that check is unavoidable), while the handler code itself only ever sees a valid, typed request.

Client. The same endpoint generates the browser JS (Endpoint.clientFn โ†’ fetch + JSON.stringify(req) + r.json()) and a type-check stub. A .leanjs client that builds the request or reads the response is checked against the very same SetCellReq / SetCellResp: accessing a field the type doesn't have fails to compile. LeanJs has no type system of its own, so rather than reinvent one, LeanJs.TypeCheck emits the client to Lean and lets Lean's own elaborator do the checking (asyncโ†’do, awaitโ†’โ†); the shared types are imported, not re-declared, so there is exactly one definition.

correct client            โ†’ type-checks โœ“
resp.value โ†’ resp.valueX  โ†’ REJECTED โœ—  (SetCellResp.valueX doesn't exist)

See examples/Tests/TypedRpcSpec.lean for the end-to-end proof (server dispatch + client accept/reject + generated JS). The legacy stringly-typed LeanTea.Rpc (Handler := List String โ†’ IO String) remains for back-compat.

CSS / JS DSLs

Both CSS and JS are also small ASTs with a render step.

open LeanTea.Css in
def sheetStyles : Sheet := [
  rule ".btn" [("background", "#0284c7"), ("color", "#fff")],
  rule ".btn:hover" [("background", "#0369a1")],
  keyframes "ripple" [
    ("0%,100%", [("box-shadow", "0 0 0 8px rgba(239,68,68,0.25)")]),
    ("50%",     [("box-shadow", "0 0 0 12px rgba(239,68,68,0.2)")])
  ]
]
open LeanTea.Js LeanTea.Js.E LeanTea.Js.S LeanTea.Js.Dom in
def helloFn : Stmt :=
  afn "hello" [] [
    constV "btn" (getById "btn-hello"),
    doE (addEventListener (i "btn") "click"
      (aarrow [] [doE (await_ (call (i "alert") [s "hi"]))]))
  ]

Block.render produces compact (one-line) JavaScript โ€” readable but not pretty. Used by LeanTea.Rpc.clientLib to emit the typed RPC client functions.

Google OAuth login

LeanTea.Auth is plugged into the example servers (sheet_serve, reversi_serve, โ€ฆ) and activates only when the environment variables below are set so local development is unaffected.

VariableRequiredExample
GOOGLE_CLIENT_IDyes123-abc.apps.googleusercontent.com
GOOGLE_CLIENT_SECRETyesGOCSPX-...
BASE_URLyeshttps://your-app.fly.dev (for redirect_uri)
COOKIE_SECUREno1 to flag cookies Secure on HTTPS
ALLOWED_EMAILSnocomma-separated email allowlist

When both *_ID and *_SECRET are present:

  • GET /auth/google/login โ†’ mints a CSRF state and 302-redirects to Google
  • GET /auth/google/callback?code=&state= โ†’ posts to Google's /token and /userinfo via curl(1), mints a session, sets an HttpOnly cookie
  • GET /auth/logout โ†’ drops the cookie and the DB row
  • /mcp and a few static asset paths are on a public allowlist
  • API paths under /api/* return 401 when unauthenticated; UI paths 302 to the login page
  • Sessions live in the sessions SQLite table, CSRF state in oauth_states

Google Cloud Console setup

  1. Create an OAuth 2.0 Client ID (Web application) at https://console.cloud.google.com/apis/credentials.

  2. Add ${BASE_URL}/auth/google/callback to "Authorized redirect URIs".

  3. Approve email, profile, openid on the consent screen.

  4. Push the secrets to Fly:

    flyctl secrets set \
      GOOGLE_CLIENT_ID=โ€ฆ \
      GOOGLE_CLIENT_SECRET=โ€ฆ \
      BASE_URL=https://your-app.fly.dev \
      COOKIE_SECURE=1 \
      ALLOWED_EMAILS=you@example.com
    

Implementation notes

  • HTTPS calls to Google go through curl(1) (the Lean stdlib has no TLS). The runtime image already bundles curl.
  • Session tokens are 32 random bytes from /dev/urandom, hex-encoded (Auth.randomToken). Important: use IO.FS.Handle.mk + read 32 โ€” IO.FS.readBinFile reads to EOF, and /dev/urandom never EOFs, so it spirals into an OOM. Caught the hard way.
  • Auth.gate cfg store publicPaths inner is a Handler โ†’ Handler wrapper. The inner handler is typed as Session โ†’ Handler so the logged-in user is available without leaking through globals.

Cloud deployment

Docker (works on any container host)

Dockerfile and fly.toml live at the repo root. The multi-stage build installs the Lean toolchain on Debian Bookworm, builds the binary, then copies just the binary into a slim runtime image (~170 MB).

docker build -t leantea-sheet .
docker run -d --name leantea \
    -p 8080:8080 \
    -v leantea_data:/data \
    leantea-sheet
open http://127.0.0.1:8080/

/data holds the SQLite file, so a container restart keeps history.

Fly.io (free tier)

The hobby tier is free for shared-cpu-1x ร— 3 machines (256 MB each) with up to 3 GB of persistent volume. auto_stop_machines is on, so machines park themselves when idle and there's a 10โ€“20 s cold start on the next request โ€” perfect for a personal app.

curl -L https://fly.io/install.sh | sh
flyctl auth login

flyctl launch --no-deploy          # picks app name + region
flyctl volumes create sheet_data --region nrt --size 1
flyctl deploy
flyctl open

Other options

  • Oracle Cloud Free Tier โ€” Always-free ARM Ampere instances (4 vCPU, 24 GB RAM). SSH in and run the binary, or deploy the same Docker image.
  • Render โ€” Free web service tier (sleeps after 15 min idle). Dockerfile works out of the box; persistent disk requires a paid plan.
  • Google Cloud Run โ€” Pay-per-use, 2 M requests/month free. The Lean binary runs fine; persistence has to move to Firestore or Cloud SQL since the FS is ephemeral.

Development & testing

LeanTEA's testing story has three layers, by intent:

LayerToolPurposeStatus
Type-level proofs (the negative space)The compiler itself + Proof of Authorization + SafeQuery"Could this auth bypass / SQL injection / XSS ever happen?" โ€” answered statically. What the compiler proves, you don't have to test.โœ… shipped
Unit & smokeLeanTea.LSpec โ€” a tiny LSpec-shaped runner with group / it + tree outputPure-function business logic, update : Msg โ†’ Model โ†’ Model, codec round-trips, render output. Used by every examples/Smoke/* binary.โœ… shipped
E2E (LLM-driven, exploratory)examples/UiScript + browser_mcp_serveDeclarative JSON scripts (click โ†’ screenshot โ†’ LLM classify) that survive DOM refactors because the LLM reasons about intent. Pairs with examples/BrowserAgent for record-once / replay-many.โœ… shipped
E2E (typed, deterministic)LeanTea.WebSpec (planned v0.2) โ€” do-notation over ChromeCdpMcpCI/CD regression tests with group "login flow" [ it "rejects bad password" do โ€ฆ ]. Same mental model as LSpec, but with navigate / fill / click / expectText primitives.๐Ÿšง planned

What this means in practice:

  • Security tests don't exist in a LeanTEA codebase โ€” the compiler already enforced the property. Your test suite is just the happy-path business logic, which is drastically smaller than the equivalent Rails / Django suite (most of which exists to assert "the framework didn't let this bad input through").
  • For UI regressions today, write a UiScript JSON and let the LLM drive it. For deterministic golden tests, wait for v0.2 or hand-roll the chrome_cdp_* calls in a smoke binary โ€” the surface area is already there.

Dev loop

tools/dev.py is a tiny stdlib-only file watcher: it lake builds on save, restarts the dev server with DEV_MODE=1, and the page polls GET /_dev/ping once a second so the browser auto-reloads after a successful build.

python3 tools/dev.py --app sheet --port 8801

Community

License

c/sqlite3.c / c/sqlite3.h are public domain (https://www.sqlite.org/copyright.html). Everything else is MIT.