7 layers
type / slot / effect / reducer / tile / fn / app. State, side effects, and UI are split by role, with no implicit rules.
Definitions interlock like Japanese joinery (kumiki) so AI can write, edit, and reassemble an app in parallel. Experimental.
The same app — a quote fetcher with loading and error states — in two worlds: feature example 19 on the left, an idiomatic React equivalent on the right.
Kumiki — 30 lines · 278 tokens
type Quote = {text: Text, author: Text}
type Load = Idle | Loading | Loaded(Quote) | Failed(Text)
slot state : Load = Idle
effect fetchQuote cap=http.get
in=Unit
out=Result(Quote, HttpError)
policy=latest
map-request={url: "/api/quote", decode: Decoder.Json(Quote)}
reducer load on=ui.click(LoadBtn) do= state := Loading
emit fetchQuote()
reducer loaded on=fetchQuote.ok($q, _) do= state := Loaded($q)
reducer failed on=fetchQuote.err($e, _) do= state := Failed("request failed")
tile LoadBtn = button(text="Load quote", onClick=load)
tile App = column(
LoadBtn,
match state with
| Idle -> text("Click to load.")
| Loading -> spinner()
| Loaded(q) -> card(text(q.text), text("— " + q.author)) {pad: "md"}
| Failed(msg) -> text(msg) {color: "danger"})
{pad: "lg", gap: "md"}
app EffectHttp
caps = [http.get]
routes = {"/" -> App, "/404" -> App}
init = []React — 40 lines · 328 tokens
import { useRef, useState } from "react";
type Quote = { text: string; author: string };
type Load =
| { tag: "idle" } | { tag: "loading" }
| { tag: "loaded"; quote: Quote }
| { tag: "failed"; message: string };
export function App() {
const [state, setState] = useState<Load>({ tag: "idle" });
const ctrl = useRef<AbortController | null>(null);
async function load() {
ctrl.current?.abort(); // "latest" policy, by hand
const c = (ctrl.current = new AbortController());
setState({ tag: "loading" });
try {
const res = await fetch("/api/quote", { signal: c.signal });
setState({ tag: "loaded", quote: await res.json() });
} catch {
if (!c.signal.aborted)
setState({ tag: "failed", message: "request failed" });
}
}
return (
<div>
<button onClick={load}>Load quote</button>
{state.tag === "idle" && <p>Click to load.</p>}
{state.tag === "loading" && <Spinner />}
{state.tag === "loaded" && (
<blockquote>
<p>{state.quote.text}</p>
<footer>— {state.quote.author}</footer>
</blockquote>
)}
{state.tag === "failed" && <p className="error">{state.message}</p>}
</div>
);
}And the Kumiki column is not pseudocode — this is it running, compiled by the in-browser compiler against a demo http.get provider. Click the button:
loading demo…
Want to change it? It's loaded in the Playground.
What to notice:
policy=latest is one annotation. Dropping stale responses in React is a hand-written AbortController ritual — exactly the kind of outside-the-text machinery that breaks when an AI edits it.caps = [http.get] is checked by the compiler; an effect with an undeclared capability is a compile error, not a surprise.reducer failed or tile App alone — nothing else needs to move.The gap grows with app size: on the benchmarked TodoMVC, Kumiki is ~1.4× fewer tokens and ~2× fewer lines than the React equivalent. See Benchmarks.