Each prompt adds to the page rather than replacing it, unless it asks to change one that is already there: drop a block, set its width, or move it. The address carries the latest one, so a page here is still a link you can send, and reloading rebuilds from that prompt alone.

untitled.html 0 blocks · 6 col

Nothing on the page yet. Describe one above, pick an example below, or ask the desk to build one for you.

It knows five blocks: an intro paragraph, a card, a callout, a stat tile and a form. Say "side by side" and it puts two of them in a row.

Nothing in the closed set matched that. Try asking for an intro, a card, a callout, a stat or a form.

The composition, as JSON

The artifact rather than a description of one, and what Open reads back in. A form block's data is what matchSpec returned, one level in. The page reads its own state back out of here on load, which is how a static host can tell a fresh visit from one a server already composed. The exported file is this plus one line naming GRAIN, because a file you take away should say where it came from and a pane showing you your own state does not need signing.


        
Taking the page away

Three files off the one composition, and they answer three different questions. The JSON is the composition, and it is the only one that comes back: Open reads it and the page is the page again, which makes it the file to keep. The page is a whole document you can open or send, and it links this site's stylesheets rather than copying them, so it needs the network to look like what you built. The tags are the markup on its own, for pasting into a page that already loads GRAIN.

The tag source is the interesting one, because it is not a translation. Every class in it belongs to GRAIN, and a molecule in GRAIN is a documented class convention a page author writes by hand, so what a composed page produced and what a developer would have typed are the same markup. The builder did the choosing, not the inventing. The one thing that does not travel with it is the six-column grid the widths key off, which is this page's own CSS rather than GRAIN's, so it ships with the page export and is named in a comment on the tag export.

Both HTML forms come out without the builder's fingerprints on them: the block addresses, the block ids, and the binding directives the browser uses to fill a cloned block. An exported page ships no dispatcher, so an address on it would advertise an operation nothing can perform, and that is the one mistake this whole design keeps saying out loud that it will not make.

All three carry the GRAIN byline, and two of them carry it twice. The footer is the half you can see and the half someone will eventually delete; the comment at the top of the file and the attribute on the root element are the half that survives that. They are named here rather than hidden, because a mark you have to find is a different thing from a mark you are told about.

Open replaces the canvas rather than adding to it, which is the opposite of what a prompt does and the reason the button says Open rather than Import. A hand-edited file gets the same treatment a description gets: anything this build cannot render is listed by name under Can't build, and the rest of the page still comes up.

What it builds, what it edits, and where it stops

Describe a page and press Build it. A fixed, code-owned set of GRAIN blocks decides what renders, expanded by the same renderer every other page on this site goes through. Once there are blocks on the canvas, the same box aims at them: say drop the second card, and a half-billion-parameter model running in this browser reads what is on the page and answers with one verb pointed at one id the rail is printing. Whether it answers well is a separate question, it has been measured, and this drawer says so below.

The model chooses and code holds the fence. It is shown the blocks that exist and the verbs each one takes, and everything it answers is checked against that same list before anything moves: a verb that is not in the vocabulary, a block that is not here, a width outside the three. What no check can catch is a choice that is legal and wrong, so the page names the block it is about to touch before it touches it. If the model cannot run on your machine, this box says so rather than falling back to something that is not the model and calling it one.

for (const block of composition.blocks)
  render(block.component, block.data, block.props)
Describe a pageadds blocks. Code picks the components, never the model, and this is the only way to add one.
Drop the second cardremoves one block, by kind, by position, or by its id.
Make the callout halfsets a width. Full, half and third, and there is no fourth word.
Move the form upshifts one block one place, clamped at the ends rather than wrapped.

Those four lines are the whole vocabulary, and only the first one has ever been shown to work end to end. The editing half was measured on 2026-08-15 rather than assumed, over thirty-three answers from the real model, and not one of them landed a correct edit. The first eighteen never aimed at a block at all. Then the list the model reads was narrowed to only the things a verb can touch, and seven of the next fifteen named a block, five of those named the right block and the right verb, and every one of the five was refused for writing b2 where the page addresses that block as block:b2. Those numbers are a dated measurement of that build rather than a standing fact about small models.

That last refusal is gone as of 2026-08-19. The fence now reads a short address up to the long one when exactly one block on the page answers to it, and still refuses an ambiguous or an absent one. Then it was run, the same day, against the same five scenarios that had scored zero before it, and here is the number it earned. Drop b4 now lands, twice in three tries. That is the first correct edit this model has ever completed on this page, and it is the scenario that asks only for the vocabulary. The four that also ask it to work out which block you meant, the second card, the callout, move one up, still never landed once.

The same run turned up something the deduction would never have found. Asked for a change no verb can make, the model reached for a verb anyway and removed an unrelated block, once in four tries. Under the strict fence that answer was refused for its address and nothing moved. Loosening the fence is what let it through. Both numbers are one afternoon against a model that answers differently every time, not a rate.

The obvious guard was to stop resolving a short address for a verb that removes something. That was built, and then measured, and it took the one edit that works with it, because the edit that works is a drop written short. So the answer here is Undo instead. A wrong drop is one press to put back, with the block's own content intact, and the same button covers a width or a move. That is the trade this page is willing to make: an edit that lands, next to a mistake that costs a keystroke rather than the page.

This drawer used to promise that the honest demo is the one where you watch it pick the wrong block. Under the strict fence it never got that far, and a promise you cannot keep is worse than a limit you state. Under the loose one it got there once, in the worst way, by dropping a block nobody asked about. So the older claim, that every wrong answer was caught and the canvas came back byte-identical every single time, was true of that build and is not true of this one. What still holds is the shape of the guarantee: a refusal names itself in the visitor's own words, and every wrong answer that IS refused changes nothing. A builder that quietly edits the wrong block is worse than one that admits it did not understand, because the first kind of mistake is the kind you find later.

So describing a page is the part that works, and it works on a machine that cannot run a model at all. Editing one is the model's job, and it can now do the easiest version of it and none of the rest. Both halves are on this page on purpose, and the line under the prompt box is where you watch the second one land or fail in front of you.

Nothing on this page was written by the model. Which components appear is the block set's decision, and the words inside each one are sample content the same table carries, drafted by code. A wording pass is an open seam rather than a shipped feature.

The Can't build list holds two different answers. A shell, a side rail or a top bar is the frame a page sits in rather than content it can hold, so it is refused on principle. Everything else is a gap with a date on it: the component exists, it is documented, and the block set has not grown a template for it. A picture is the honest example, because a generated page has no real image to point at and an invented one is a broken picture with a confident name.

About the form on this page

The form on the canvas is one block, and its insides are the same closed set of fields this page used to be entirely about. Ask for a box to tick and you get one, and the desk can tick it: until 2026-08-14 the vocabulary had no verb that could, because the only verb for a form control writes the control's value, and a tick box's value is what the form submits rather than whether it is ticked, so the write landed, reported success and moved nothing. A tick box carries a different kind of address for exactly that reason.

Fields are grouped by kind, text fields first, then a message box, then choices, then anything to tick. Interleaving them in the order you asked is not supported, on purpose, a v1 limit. The address each field carries sits on the control itself, so the desk writes into a generated field with nothing registered by hand. That was not true when this page was first built: the atoms addressed the label around the control, a label has nothing to write into, and the write was dropped without a word. Building this found it and the atoms were fixed, which is the whole argument for building a demo rather than describing one.

Nothing here submits anywhere. This is a static site, there is no submit control on a generated form, and there is no verb in the vocabulary that could press one.

Examples

Real links, real query strings. Every state on this page is shareable and reproducible: the same prompt always builds the same page.

An intro, two cards side by side, and a calloutthree blocks, and the layout word does the placing.
An opening paragraph and a statthe smallest page the set can make.
A card, a gallery of screenshots, and a side railone block, and two refusals of different kinds.
Part of tjakoen.github.io, built with itself.

🤖 Built with Claude. I don't prompt and pray, I prompt and prove. Every commit here is co-authored with an AI, on purpose. How I actually work with AI, receipts and all →