skip to the page

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

AnswerWhat happens
acceptedthe change is applied
requires-confirmationthe change waits for a person to say yes
rejectedthe 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 holding a controllive · rendered through the runtime

A card, and a control on it

Everything in the archive

Two hundred issues, searchable, with the whole back catalogue.

Read the archive
Propose a change:

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 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"
            }
          ]
        }
      ]
    }
  ]
}
A perfectly ordinary composition: a surface with a heading, a sentence and a button on it. Ask to make the whole card a link and watch what the Gate says.

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.