What the Gate decides
The Gate is the thing that looks at a proposed change and says one of three words: yes, ask a person, or no.
It is a pure function. Same change, same rules, same answer, every time — and it runs before anything is applied, so a refusal costs nothing and leaves nothing behind.
It answers in three ways, and never more
| Answer | What happens |
|---|---|
accepted | the change is applied |
requires-confirmation | the change waits for a person to say yes |
rejected | the change does not happen, and cannot be confirmed into happening |
The middle one is the one worth having. Software that can only allow or forbid has to decide, at design time, which changes are safe — and it will be wrong, because whether a change is safe depends on what it touches. Handing the uncertain ones to a person is not a fallback; it is the point.
The two questions it asks
Before deciding, the runtime measures two things about the change. Neither of them involves an opinion about whether the change is good.
How much is at stake? Not "how big is the delta" — how much would it matter if this were wrong. Removing twelve nodes is a bigger deal than removing one. Restructuring the top of the page is a bigger deal than editing something deep inside it. Touching a thing the deployment has declared consequential is a bigger deal than touching anything it has not.
Can it be undone? The runtime computes the exact delta that would put the page back, at the same moment it assesses the change. A change is reversible when that inverse would actually restore things — and some are not: a change that reaches outside the tree cannot be undone by editing the tree, however neatly the operations invert.
Try it below. Every chip on this example produces a different answer, and the box prints the factors the assessment found, in the runtime's own words.
A card, and a control on it
Every one of these goes through the same pipeline a model’s answer would: interpreted, analysed, weighed, judged, applied, appended. The log is real and it is in this tab — reload the page and the example is back as it was.
show the treehide the tree
{
"kind": "element",
"id": "n_cardcontrol10",
"type": "loom.page",
"props": {
"fills": true,
"width": "readable",
"loom:theme": {
"palette": "minimal",
"fontPack": "minimal-sans",
"stylePreset": "precise"
}
},
"children": [
{
"kind": "element",
"id": "n_cardcontrol2",
"type": "loom.heading",
"props": {
"level": 1
},
"children": [
{
"kind": "text",
"id": "n_cardcontrol1",
"value": "A card, and a control on it"
}
]
},
{
"kind": "element",
"id": "n_cardcontrol9",
"type": "loom.card",
"props": {
"tone": "surface",
"padding": "loose"
},
"children": [
{
"kind": "element",
"id": "n_cardcontrol4",
"type": "loom.heading",
"props": {
"level": 2
},
"children": [
{
"kind": "text",
"id": "n_cardcontrol3",
"value": "Everything in the archive"
}
]
},
{
"kind": "element",
"id": "n_cardcontrol6",
"type": "loom.prose",
"props": {
"tone": "muted"
},
"children": [
{
"kind": "text",
"id": "n_cardcontrol5",
"value": "Two hundred issues, searchable, with the whole back catalogue."
}
]
},
{
"kind": "element",
"id": "n_cardcontrol8",
"type": "loom.action",
"props": {
"href": "https://example.com/archive",
"variant": "primary",
"scale": "small"
},
"children": [
{
"kind": "text",
"id": "n_cardcontrol7",
"value": "Read the archive"
}
]
}
]
}
]
}The deployment decides what is consequential
Here is the part people expect to be built in and is not. The Gate ships with sensible structural thresholds — how much removal counts as large, how close to the root counts as restructuring — because those are questions about shape, and shape means the same thing everywhere.
It ships with no opinion about your primitives, because it cannot have one.
Only you know that commerce.checkout matters more than layout.stack.
So a deployment writes a policy:
const policy = gatePolicySchema.parse({
policyId: "acme-storefront",
protectedPrimitiveTypes: ["commerce.checkout", "legal.consent-notice"],
protectedPropKeys: ["price"],
})
This site has one too, and it is deliberately small so that its effects are legible:
const docsGatePolicy = gatePolicySchema.parse({
policyId: "loom-docs",
protectedPrimitiveTypes: ["loom.heading"],
interactiveTypes: interactiveTypesFor(docsRegistry),
})
That first line is why "demote the page's heading" is held for you and "add a sentence" is not. A heading is not intrinsically precious — this deployment said it was, and the two chips differ by nothing else. Change the policy and the verdicts change with it.
The one list you should not write by hand
interactiveTypes is the second line above, and it is derived rather than
typed. It answers "which primitives render a thing the reader aims at?" — a
link, a button — and the answer already lives on each primitive, because each
primitive declared it:
definePrimitive({ type: "loom.action", interactive: "always", … })
definePrimitive({ type: "loom.card", interactive: { whenProps: ["href"] }, … })
interactiveTypesFor(registry) reads them all. A hand-written copy would be
wrong the first day someone gave another primitive an href, and nothing would
say so.
What it buys is the most interesting refusal on this site. Press make the
whole card a link on the example above. The operation is a single configure
naming a single node — it looks like the re-theme, which is waved straight
through. But a card with an href is the thing the reader aims at, and the
button inside it becomes a link inside a link: markup a browser resolves by
throwing one of them away. The page still renders. Something on it silently
stops working.
Read the refusal itself and you will notice it does not say "a link inside a
link". It says the change puts a target where the reader cannot reach it —
and the difference matters, because a link inside a link is only one way to
produce that. A card can also cover itself: loom.article stretches its title's
anchor over the whole surface, so a button placed on top of it is perfectly
valid markup that receives no clicks at all. Same damage, no nesting to find.
The runtime names the damage rather than the mechanism, which is why one
sentence covers both.
Nothing about the operation shows that. The runtime finds it by looking at the tree the change would produce, and only counts the breakage this change introduces — not breakage the page already had.
Confirmation is not a way past the Gate
When a change is held and a person says yes, the runtime judges it again — the policy is resolved again and the change is assessed again, so the verdict is the one that holds now rather than the one recorded when it was held. A policy that narrowed while the change was waiting narrowed for the queue too, and a change that would now be refused is refused.
A person answering a hold is answering the question the Gate asked. They are not overruling it.
What the second look is not about is the page. A change judged against a revision the page has since moved past never reaches the Gate at all: it can no longer be applied to anything, so answering it ends its custody rather than re-opening the question. Answering a held change is the whole of that story — what is waiting, how to tell those two apart before somebody spends their attention on the wrong one, and what each answer does.
What a verdict tells you afterwards
Every decision records the rule that produced it, the factors that led there,
and the name of the policy that decided — so a change judged six months ago can
still say what it was judged by. That is why a policy has a policyId, and why
changing what a policy contains means giving it a new name: every decision
already written under the old one claims to have been judged by what that name
meant then.