RuneFort — a specification in the ComputeDriven worldlive_deployedThat rung covers the four layout primitives at /app/ — rune-floor, rune-room, rune-claim and rune-link, measured compiling a declaration into real CSS grid tracks in a browser on this domain. It does not cover the .rune DSL, which has no parser, or cloud sync, which points at a data layer this portfolio has abandoned.
The question this site exists to answer

Can a layout be a file you can diff?

Where a thing sits on the screen is usually the least reviewable fact about a system. It lives in a canvas file, in a hosted dashboard, or in whatever a force-directed graph decided this morning. RuneFort declares it instead — rooms at integer cells, the files each room owns, which room is next, and what colours it — as JSON that sits in your repository and shows up in a pull request.

Walk a fort  See what was measured

6.6 KB
Gzipped, to render a floor
41.1 KB
Gzipped, at the documented entry
14
Custom elements registered
0
Build steps
0
Renderers other than ours
The whole vocabulary

Four primitives. This section is laid out with them.

The four cards below are placed at the cells they name, on a six-column floor, with plain CSS grid and no components. It is the cheapest available test of whether the vocabulary can describe an ordinary piece of a page — and if it could not, that would be worth knowing before anything else on this site.

0,0 · 3,1

Room

a tile

An id, an integer position, an integer size in grid cells, and arbitrary metadata. [0,0] is top-left; size [3,2] is three columns by two rows. The renderer picks the pixel size — the protocol only specifies cell counts.

3,0 · 3,1

Claim

the files it owns

A glob and an anchor. The room owns that region of the filesystem, and activating the tile surfaces those files — inline, or handed off to your own editor through a vscode:, cursor: or zed: URL.

0,1 · 2,1

Neighbor

which tile is next

An adjacency between two rooms. It drives h/j/k/l navigation and focus order, and may be drawn as a line. Adjacency is declared, not inferred from pixels — which is why the keyboard path survives a re-layout.

2,1 · 4,1

Binding

what colours the tile

A named runtime signal, a threshold, and the class it applies. Live state colours a tile without the layout knowing what the signal means. Unknown vocabulary degrades rather than fails.

Everything else in the protocol is a vocabulary layered on top of these four. A renderer that does not recognise a vocabulary must still draw the layout using the primitives — unknown vocabulary degrades, it does not fail.

What a declaration looks like

Two rooms, one file, no build step.

This is the whole quick-start, and both paths in it are resolved against the tree before this page is emitted. The components are ES modules served as written — there is no bundler in this repository and no built output directory, which is worth stating plainly, because the README pointed at one that does not exist and the first line anyone copied returned 404.

<script type="module" src="/packages/core/src/index.js"></script>
<link rel="stylesheet" href="/packages/core/theme.css">

<rune-floor columns="6" rows="2" editor="vscode">
  <rune-room id="memory" position="0,0" size="3,2" label="memory.ex">
    <rune-claim pattern="lib/my_app/memory/**/*.ex" anchor="lib/my_app/memory.ex#L24"></rune-claim>
  </rune-room>
  <rune-room id="deploy" position="3,0" size="3,2" label="deploy.ex"></rune-room>
  <rune-link from="memory" to="deploy" kind="adjacent" bidirectional></rune-link>
</rune-floor>
What it actually weighs

Measured by walking the import graph, not estimated.

This site printed one weight, seven times, and never once said the weight of what. The build now resolves each entry point's ES module imports transitively, concatenates every file it reaches and gzips the result at level 9, and refuses to publish the page if a figure moves without the record moving with it.

What you loadEntry pointReachedRawGzip level 9
The four layout primitives
What a page needs to render rooms on a floor. rune-floor pulls in the threshold parser; rune-room pulls in the editor-handoff URL builder.
rune-floor.js + rune-room.js + rune-claim.js + rune-link.js6 modules24,443 bytes6,794 gzipped
The documented entry point
What import "@runefort/core" costs. It registers every element and re-exports the templates, the generation runtime and the cloud adapter.
index.js19 modules163,749 bytes42,065 gzipped
The cloud-sync adapter, inside the entry point
Inside the row above. Loaded whether or not it is configured.
supabase.js1 module12,487 bytes3,704 gzipped
theme.css
The stylesheet the components expect. Not JavaScript, and not counted in either figure above.
theme.css1 module22,314 bytes4,478 gzipped

Read the two rows against each other: the smallest module graph that renders a floor of rooms is 6,794 bytes gzipped, and the entry point the documentation tells you to import is 42,0656.2× larger, because it registers all 14 elements and re-exports the templates, the generation runtime and the cloud adapter. Both are true, and the single figure this site used to print was neither. Splitting the entry point so the four primitives can be imported alone is the obvious fix and it has not been done, so the larger figure is the honest one to quote.

Evidence

A declaration was loaded from this domain, and the grid it asked for was measured.

The protocol's claim at this rung is narrow and checkable: what you declare in cells is what the browser lays out in tracks. So it was checked in a browser, on the deployed domain, rather than asserted.

Where
https://runefort.com/app/, in a browser, on 2026-08-16. Not a local server and not this repository.
The floor
Computed display was grid, and grid-template-columns resolved to six tracks of 157.828px.
A wide room
A room declaring size="6,1" measured 1017px — six tracks plus the five gaps between them.
Two half rooms
Rooms declaring position="0,1" and position="3,1", both size="3,1", measured 502px each, at x=224 and x=740. Side by side, three tracks each, one gap between.
Upgraded
All 14 custom elements were defined and every <rune-room> in the document had been upgraded by the time the measurement ran.
What it is not
A conformance test. There is no corpus of declarations with expected geometry anywhere in this tree, so this is one reading of one page, repeatable by hand and by nothing else.
What is not established

There is one renderer, and portability is the point.

Everything above establishes that this renderer honours a declaration. The protocol exists so that a second one would honour the same declaration identically, and there is no second one, so that property is unmeasured rather than proven.

A renderer we did not write

spec

The protocol's reason to exist: the same runefort.json should render in Elixir/Phoenix, in TypeScript/React, or in static HTML, and the protocol is the only thing those three share. One implementation cannot demonstrate portability; it can only fail to contradict it.

Needs: A second renderer, and a fixture that says what geometry counts as agreement. Built: no.

The .rune DSL

spec

A short authoring syntax that compiles to runefort.json, specified in docs/spec/runefort.dsl.md. There is no parser for it anywhere in this repository. The JSON is the only thing that exists.

Needs: A parser, and a conformance suite that proves it round-trips to the same JSON. Built: no.

A conformance suite

spec

Nothing in this tree defines what makes a renderer conformant. There is no test corpus of declarations with expected geometry, which is what the paragraph above would have to be checked against.

Needs: Declarations plus expected cell geometry, in a form a second implementation can run. Built: no.

Cloud sync

spec

The entry point exports sign-in, push, pull, fork and public-fort loading against Supabase — twelve and a half kilobytes of it, loaded whether or not you configure it. This portfolio ruled the shared-Supabase route abandoned on 2026-07-30 and its replacement is not built, so this path has no live backend to reach.

Needs: A ruling on where a fort is stored, which is not this domain's to make. Built: code yes, backend no.
Status

Where this stands, and what would move it.

Status
live_deployed — A declaration was loaded from this domain and the grid it asks for was measured in the browser, cell by cell. The components are published and the page you are reading is laid out with the same vocabulary they implement.
Last verified
2026-08-16
Source
https://runefort.com/app/ opened in a browser on 2026-08-16. rune-floor resolved to display:grid with six 157.828px tracks; a room declaring size 6,1 measured 1017px wide and rooms declaring position 0,1 and 3,1 measured 502px wide at x=224 and x=740 — side by side, three tracks each, one gap between. Fourteen custom elements upgraded. The weights below are recomputed from the component files by this site's build.
Limit
This establishes that ONE renderer — the one in this repository — compiles a declaration into the grid that declaration asks for. Renderer portability is the protocol's entire claim and there is exactly one renderer, so nothing here tests it. It does not establish that any layout outside this repository has been authored, that the .rune DSL works (it has no parser), or that cloud sync functions; that path is wired to Supabase, which this portfolio ruled out on 2026-07-30.
Next rung
external — A second renderer, written by someone who is not us, that compiles the same runefort.json into the same cell geometry. Elixir/Phoenix or React are the two the spec names. Until one exists, 'renderer-portable' is a design intention rather than a measured property.
Build steps
0. The components are ES modules served as written. There is no bundler, no transpiler and no dist/ directory in this repository — which is also why the README's quickstart, which pointed at a built output file, returned 404 for as long as it existed. The path it named is quoted once, in the retraction.
Renderers other than ours
0. Checked by looking. The protocol's claim is that its declaration is renderer-portable; there is one renderer and it is the one in packages/core/src.

Retraction — the weight on this page was never measured

Until this revision this site printed ~5KB in its title, in its description, twice in its social cards, in its own eyebrow, and twice more in the comparison table — seven times, and not once beside the name of the thing being weighed. The README called it a target. The landing page stated it as a fact.

Measured, by walking the ES module import graph and gzipping at level 9: the four layout primitives with their own dependencies come to 6,794 bytes gzipped, and the entry point the documentation tells you to import comes to 42,065. Neither is five kilobytes. The nearest true statement is that the four primitive files alone gzip to about five kilobytes — and they cannot be loaded alone, because the documented entry point registers all 14 elements and pulls in the templates, the generation runtime and the cloud adapter with them.

A second thing went with it. The README's quick-start pointed at /packages/core/dist/index.js, and there is no dist/ in this repository and no build step that would produce one, so the first line anyone copied returned 404. Both are now structural rather than careful: the figures are recomputed from the component files on every build and the build refuses when one moves without its record, and every documented path is resolved against the tree before the page is emitted.

What you can actually do here

Two rungs, two lists. Nothing asks you to use what is not built.

A correction is worth more than a compliment

Tell us a figure on this page is wrong.

Every number here is a measurement with a date on it, and measurements go stale. The form below is a real form with a real action: it posts to formspree.io when you press the button, and it works with JavaScript off. The script that upgrades it to an inline reply prints “sent” only when the endpoint actually answers 2xx — a form that says thank-you on submit and drops the message is the exact failure this page argues against. If you would rather leave a public trail, open an issue instead.