{"slug": "millwright-the-three-layers-of-a-malleable-ui", "title": "Millwright: the three layers of a malleable UI", "summary": "Millfolio introduces Millwright, a three-layer system for rendering AI-generated analytics without allowing models to touch markup or the DOM. The system uses typed data contracts, versioned board specs, and additive navigation to ensure security and reversibility. This approach enables safe public demos by keeping model output as inspectable, validatable documents.", "body_md": "# Millwright: the three layers of a malleable UI\n\nThe [local AI budget](https://millfolio.com/blog/local-ai-infra-tags/) post was about how to get value out of your local GPU. This one is about seeing more from your data — how millfolio renders\nmodel-generated analytics without ever letting a model touch markup, styles, or\nthe DOM. The feature is called **Millwright**, and it’s built as three nested\nlayers, each one a plain data contract the client validates before rendering.\n\n## Layer 1: the widget — typed results, not markup\n\nA widget’s content is the output of a small **program** — the same sandboxed\nprograms that answer one-off questions in Ask. A program never returns HTML or\nmarkdown. It returns a *result spec*: a versioned JSON envelope of typed blocks —\n\n```\n{ \"v\": 1,\n  \"text\": \"Your top 5 merchants over the last 3 months.\",\n  \"data\": [{ \"kind\": \"table\",\n             \"headers\": [\"Merchant\", \"Spent\"],\n             \"rows\": [[{ \"type\": \"text\",  \"value\": \"WHOLE FOODS\" },\n                       { \"type\": \"money\", \"raw\": 612.4, \"text\": \"$612.40\" }]] }] }\n```\n\nEvery value is **typed** — `money`\n\ncrosses the boundary as `{raw, text}`\n\nso the\nchart axis uses the number and the label uses the exact formatted string; the\nclient never parses `\"$612.40\"`\n\nback out of a display string. Besides tables\nthere are KPIs, time/category series, share-of-whole pies, and an offline\nproportional-symbol map. The renderer picks the visualization from the data’s\nshape; the program only says what the data *is*.\n\nThis is the trusted-chrome invariant: **programs produce data, the chrome**\n**manages interactions.** When a table column is tagged as a merchant\nor tag, the chrome — not the program — turns cells into deep links into your\nVault records. A generated program can’t inject a link any more than it can\ninject a script tag, because there is nowhere in the contract to put one.\n\n## Layer 2: the Board — a semantic, versioned spec\n\nThe Board itself is another plain document: an ordered list of widgets, each\nwith an id, a title, a size hint, and a pointer to the program that computes\nit. Editing is where it gets interesting. Every change — resize a tile, edit a\nprogram, remove a widget — produces a **new content-addressed version** of the\nspec (a 16-hex FNV-1a of its bytes), appended to a version log. The “current”\nboard is just a pointer into that log, so *undo is a pointer move*, and an\nedit that breaks something can’t destroy the earlier version of the board.\n\nBefore any candidate spec is accepted — whether it came from the inline ✎ editor or from the model — it passes a validator: widget ids must be path-safe and unique, referenced programs must exist, remote URLs are rejected outright, and structural limits are enforced. The model proposes; the validator disposes.\n\n## Layer 3: pages — additive navigation\n\nA group of widgets can be promoted into a **page** — it gets its own top-level\nnav button next to Ask and Vault, and dissolving the page returns its widgets\nto the Board. Pages are the same spec document (a `pages[]`\n\nsection), the same\nversioning, the same validator — with one extra rule: navigation changes are\n**additive-only**. Generated edits can add a page; they can never rename or\nremove the built-in tabs. The parts of the UI you rely on to *inspect* what\nthe model did are not themselves editable by the model.\n\n## Why layers instead of letting the model write UI\n\nThe obvious alternative would be to have the model emit HTML/JSX and sandbox it. The downside of that approach is that you can’t diff it, you can’t validate it structurally, you can’t revert it by moving a pointer, and every render is a security decision. Three data layers give the opposite trade: every model contribution is a document you can inspect, version, validate, and refuse — and the pixels are always drawn by code that shipped with the app.\n\nThe same layering is what makes the [public demo](https://demo.millfolio.app)\nsafe to expose: the demo board is the identical machinery over a synthetic\nvault, with edits kept in your browser’s localStorage — same hashing, same\nvalidator, same chrome.\n\nTry it: [demo.millfolio.app](https://demo.millfolio.app) — edit a widget with\n✎, break it, and revert.", "url": "https://wpnews.pro/news/millwright-the-three-layers-of-a-malleable-ui", "canonical_source": "https://millfolio.app/blog/millwright-ui-layers/", "published_at": "2026-07-14 06:24:00+00:00", "updated_at": "2026-07-14 13:29:37.629758+00:00", "lang": "en", "topics": ["ai-tools", "ai-safety", "ai-infrastructure"], "entities": ["Millfolio", "Millwright"], "alternates": {"html": "https://wpnews.pro/news/millwright-the-three-layers-of-a-malleable-ui", "markdown": "https://wpnews.pro/news/millwright-the-three-layers-of-a-malleable-ui.md", "text": "https://wpnews.pro/news/millwright-the-three-layers-of-a-malleable-ui.txt", "jsonld": "https://wpnews.pro/news/millwright-the-three-layers-of-a-malleable-ui.jsonld"}}