Invariants, enforced not documented
Each of these is a property of every document that exists. They are not review guidelines. Most are enforced by the schema itself, and all nine are permanent CI in the block-ir suite.
| # | Invariant | Enforced by |
|---|---|---|
| 1 | Every block has a document-unique id. Ids are stable across a move and all-new after cloneSubtree. | A superRefine on documentSchema; inserts that would collide throw. |
| 2 | Exactly one root, type: 'body' with id: 'body'. Every other block has exactly one parent. | A literal in the body variant; body is in no type's allowedChildren, so it cannot nest. |
| 3 | Containers have children; leaves omit the key. Every parent → child pair satisfies allowedChildren. | Each container’s children is typed to a union of exactly its allowed types; .strict() rejects a leaf carrying children. |
| 4 | Frames nest at most 3 deep, and a row frame has at most 4 children. | The frame’s registry layout (maxNesting, maxRowChildren), checked by structuralIssues() in the schema and the tree ops, and unrolled in the AI schema. |
| 5 | Order is the array index. No position field exists anywhere. | .strict(): a block with position fails to parse. |
| 6 | No style-like key ever validates inside content. | Per-type content schemas are strict, and the suite asserts every style key is rejected for every type. |
| 7 | repeat validates only on section and frame; when never on body. | The bindings schema is assembled from each definition’s bindable capabilities. |
| 8 | No restricted style key validates anywhere in documentSchemaForAI. | The AI schema is generated from the registry with restricted keys omitted. |
| 9 | Key-parity across both registries, and the AI schema matches the canonical schema. | assertRegistryParity() from @repo/block-ir/testing, imported by the compiler’s own suite; every canonical document, written without ids, parses in the AI schema. |
A change that cannot satisfy an invariant is a change to a locked decision. The correct response is to surface the conflict, not to relax the assertion.
What that looks like in practice
The value of enforcing this in the schema rather than in review is that bad states are not representable. Each of these throws or fails to parse:
documentSchema.parse({ version: 2, root: { …body, id: 'not-body' } })
// ✗ invariant 2
documentSchema.parse(docWithASectionInsideAFrame)
// ✗ invariant 3: a frame accepts frames and leaves only
documentSchema.parse(docWithFourNestedFrames)
// ✗ invariant 4
documentSchema.parse(docWhereALeafHas({ position: 0 }))
// ✗ invariant 5
contentSchemas.heading.parse({ runs: [{ text: 'Hi' }], level: 1, align: 'center' })
// ✗ invariant 6: 'align' is a style
documentSchema.parse(docWithRepeatOnAButton)
// ✗ invariant 7
insertBlockInTree(root, frameId, blockAlreadyInTheTree)
// ✗ invariant 1: "clone with cloneSubtree() first"The structural rule the tests also cover
Alongside the invariants, a test scans the compiler source and fails the build on any switch (x.type) or comparison of a block type to a string literal outside the registry; the adapter scan does the same for the MCP server and CLI. It is crude, and it works: the rule that keeps this codebase extensible is the easiest one to break by accident at 2am.