Skip to content

KumikiA web framework of AI, by AI, for AI

Definitions interlock like Japanese joinery (kumiki) so AI can write, edit, and reassemble an app in parallel. Experimental.

KumikiKumiki

Same UI, different philosophy

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

kumiki
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

tsx
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.
  • The side effect is declared, not hidden. caps = [http.get] is checked by the compiler; an effect with an undeclared capability is a compile error, not a surprise.
  • Every definition is flat and independent. An AI can replace 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.