Skip to main content
All writing
Governance · · 7 min read

Forbidden, suggested, resolved

Why design-system guardrails want three tables, not one.

James Schuyler
James Schuyler Founder, preset AI

When we started modeling design-system rules, the obvious schema was one table called rules with a type column. A discriminator for whether the rule was a prohibition, a recommendation, or a routing shortcut. Clean. Compact. Wrong.

We've since pulled that apart into three first-class artifact types: forbidden_primitives, context_rules, and intent_routes. Each has its own schema, its own authoring surface, its own index strategy, its own runtime. This is the argument for why.

A taxonomy is a claim about what's different. If you can't say how two things differ, you don't need two tables.

The three types

Forbidden primitives are hard prohibitions. "Never use a raw <button> inside a Card." They carry a pattern, an optional scope (a selector chain like Card > * or EmptyState), a severity, and a suggested replacement. They fire on presence. The validator walks an AST, finds the pattern, checks the scope, and emits a violation with line and column.

Context rules are soft recommendations. "When you're building an empty state, prefer EmptyState over Box plus Heading plus Text." They carry a trigger context, a suggested preset, a rationale, and a confidence weight. They fire on request. An agent declares it's in a named context, and the system returns ranked suggestions.

Intent routes are semantic resolutions. "I need a delete-confirmation thing" → ConfirmDialog. They carry a natural-language intent phrase, a target preset, an embedding, and a canonical flag. They fire on free-text query. The router embeds the phrase and finds the nearest match by cosine similarity.

Why not one table

You can force all three into a single rules table with a discriminator column. It compiles. It even feels tidy at first. Then you start writing consumers, and every consumer is the same shape: branch on the type column, and re-implement the type-specific logic locally.

The validator needs selector compilation and AST walking, only for forbidden rows. The suggestion endpoint needs an index on context name, only for context_rule rows. The router needs HNSW over embeddings, only for intent_route rows. None of those index strategies is compatible with the others. They're not even the same query shape.

The authoring ergonomics don't fit either. A prohibition wants a typed selector chain and a severity picker. A context rule wants a context selector and a preset picker. An intent route wants free text and a model. You can try to build a polymorphic form that renders different fields for different types, or you can build three forms. We've done both. Three forms is shorter code and a better UI.

The axes that actually differ

The three types differ along every axis that matters:

  • Firing condition. Presence vs request vs free-text query. These are not variations on a theme; they are three different triggering models.
  • Matching. Structural (AST and selector) vs exact string lookup vs vector similarity. Three different kinds of algorithm.
  • Index type. B-tree on pattern and scope vs B-tree on context name vs HNSW on embedding. You will not share an index across these.
  • Failure mode. A forbidden rule's failure is a false positive that blocks a PR. A context rule's failure is a missed suggestion. An intent route's failure is a wrong route. The cost and remediation are different, and the rule's severity model has to reflect that.
  • Authoring cadence. Forbidden rules are drift-driven, authored when someone catches a violation in review. Context rules are curated, authored intentionally by the design-system owner. Intent routes are mixed: canonical set authored once, synonyms mined from usage traces.

If any of those axes lined up, you could share a table. None of them do.

How they compose

Separating them isn't about running them independently. It's about running them together with clear roles. A PR check runs forbidden_primitives first; any error-severity violation blocks, and the suggested replacement, drawn from the forbidden rule itself, feeds the autofix.

A new-code authoring flow runs context_rules with the user's declared context, returning ranked suggestions. A free-text intent from the user runs through intent_routes, returning the resolved preset, which is then cross-checked against context_rules and forbidden_primitives in the current scope. If the resolved preset is prohibited in the active context, the router drops it and logs a coherence warning.

That cross-check is why this has to be three tables. You can only ask "does the preset I'd suggest collide with a prohibition in this scope?" if the two types are queryable independently, with indexes optimized for their own access patterns. Collapse them into one table and the cross-check becomes a full scan with a discriminator filter.

What we got from it

The validator is the simplest it's ever been: one query, one index, one algorithm. The suggestion surface is fast because context lookup is an exact match. The router is isolated behind a vector index and can evolve its model without touching the other two. Authoring UIs are small and specific. New rule types can be added as new first-class tables rather than new cases in a polymorphic validator.

The cost is that authors learn three concepts instead of one. That turns out to be correct. The concepts really are different, and naming them separately in the UI helps people reason about what kind of rule they're writing.

The taxonomy is the point. Three tables means three things we're willing to say are structurally different about how design systems get enforced.


Note on prior art. This post is also a defensive publication. The specific decomposition of executable-design-system enforcement into three first-class artifact types (forbidden_primitives, context_rules, and intent_routes), with the distinct schemas, index strategies, firing conditions, and composition rules described above, is disclosed here as of April 19, 2026. This disclosure is licensed CC0 / public domain.

James Schuyler
James Schuyler · Founder, preset AI

Building preset AI to make design systems executable. Writing about governance, AI in design tooling, and the space between prototype and production.