Rules reference
How a rule file is shaped, every predicate in the vocabulary, and the full list of rules that ship.
A rule is one file, one check, and a sentence a reviewer would have written. The vocabulary below is fixed on purpose: a rule that cannot be written with it is a request to extend the list, with the rule as the evidence — not a request for an expression language.
The shape of a rule file
schemaVersion: "1" type: "rule" id: "queue-missing-dlq" severity: "error" select: node: kind: "queue" when: all: - prop: "deadLetterQueue" equals: false - hasEdge: direction: "in" mode: "async" message: "Queue '{{node.id}}' receives async traffic but has no dead-letter queue."
| Field | Required | Meaning |
|---|---|---|
id | yes | kebab-case, and it reads as the problem, not the fix: queue-missing-dlq, never add-dlq |
severity | yes | error, warning or info |
select | yes | What the rule is evaluated against, once per match |
when | yes | The predicate. The rule fires where it is true |
message | yes | One sentence. May interpolate {{node.id}}, {{edge.from}}, {{edge.to}}, {{design.id}} |
The display name, the rationale and the description live in locales/en.yaml, keyed by the rule
id, like everything else people read.
select
select chooses the subject, and the subject decides which predicates make sense.
| Form | Subject |
|---|---|
node: { kind: "queue" } | Every node whose component's kind is queue. kind may be a list |
node: { category: "data" } | Every node whose kind's category matches. May be a list |
edge: { mode: "sync" } | Every edge, optionally filtered by mode and delivery |
design: true | The design itself, once |
The predicate vocabulary
Predicates compose with all, any and not. Everything else is a leaf.
Composition
when: all: - prop: "replication" equals: "none" - not: requirement: "availability.multiRegion" equals: true
| Form | True when |
|---|---|
all: [...] | Every listed predicate is true |
any: [...] | At least one is true |
not: <predicate> | The predicate is false |
Node properties
when: prop: "evictionPolicy" exists: false
| Predicate | True when |
|---|---|
prop: p, equals: v | The effective value of p is v |
prop: p, in: [v1, v2] | The effective value is one of the list |
prop: p, exists: bool | Whether p resolves to any value at all |
prop: p, gt: n / lt: n | Integer comparison. An absent value is never greater or less |
The value tested is the effective value: the node's props, then the component's defaults,
then the property definition's default. A property that resolves to nothing is omitted, so
exists: false is true both for a property nobody set and for a name that is not a property of
this node's kind at all.
Exactly one comparison applies per predicate, and they are tested in this order: exists, in,
gt, lt, equals.
Edge fields
select: edge: mode: "sync" when: field: "timeoutMs" exists: false
field reads one of mode, delivery, protocol, timeoutMs, retryPolicy, idempotent,
circuitBreaker, fallback, batched, auth, and takes the same comparisons as prop.
Kind and provider
select: edge: {} when: all: - field: "auth" in: ["api-key", "basic"] - to: provider: ["azure", "aws", "gcp"]
kind is true when the node's component is of that kind, provider when it comes from that
provider; either may be a list. They read the node a rule is about, so on an edge they go under
from or to. An unknown component matches neither.
Node NFRs
when: nfr: { rps: { gt: 10000 } }
Compares a node's nfr value. rps and latencyP95Ms are allowed. The comparison object takes
exactly one of eq, gt or lt.
Design requirements
when: requirement: "availability.multiRegion" equals: true
| Predicate | True when |
|---|---|
requirement: path, equals: v | The requirement at path equals v |
requirement: path, exists: bool | Whether the requirement is set |
requirement: path, gt / lt: n | Integer comparison on the requirement |
Available on every subject, because the requirement belongs to the design, not to the node. An absent requirement never equals anything.
Edges around a node
when: hasEdge: direction: "in" mode: "async"
hasEdge is true when at least one edge in that direction matches the filter.
| Key | Values |
|---|---|
direction | in, out, any |
mode | sync, async |
delivery | at-most-once, at-least-once, exactly-once |
peerKind | The kind of the node at the other end |
peerCategory | The category of the node at the other end |
count | A comparison — { gt: 3 } — over the number of matching edges |
when: hasEdge: direction: "out" mode: "sync" peerCategory: "external" count: { gt: 2 }
Nodes in the design
when: hasNode: kind: "custom"
True when the design contains at least one node of that kind or category. Available on every
subject.
Groups
when: inGroup: kind: "region" count: { lt: 2 }
inGroup counts the groups of that kind in the node's ancestry — its group, that group's parent,
and so on. Without count, it means "at least one".
select: edge: {} when: crossesGroup: kind: "cell"
crossesGroup is an edge predicate: true when the two ends sit in different groups of that kind,
or when one of them is in none.
Call depth
when: syncChainDepth: { gt: 3 }
The longest path of consecutive sync edges starting at this node. Cycles are cut at the first
revisit, so the answer is always finite.
Reaching across an edge
select: edge: mode: "sync" when: all: - to: prop: "usedAsPrimaryStore" equals: true - field: "timeoutMs" exists: false
from: and to: evaluate a node predicate against the edge's source or target. They are how an
edge rule asks a question about the thing at the other end.
How "tripped by" works
Every rule ships with a reference design that trips it and one that does not.
designs/reference/<id>/design.yaml is the clean variant and must trip no rule.
design.broken.yaml beside it is a deliberately incorrect copy whose header comment lists the
rule ids it is expected to trip. The repository gate checks both directions: the clean design
must stay clean, the broken one must trip exactly what it claims, and a rule that no design trips
fails the gate.
That is why every rule's page lists the designs that trip it — the list is read out of those headers, not maintained by hand.
Severity
| Severity | For | Effect |
|---|---|---|
error | Data loss, availability, correctness | Fails a CI gate |
warning | Performance and cost | Comments without failing |
info | Style | Listed and nothing more |
No rule finding ever blocks a save, whatever its severity. Only structural findings do that.
Every rule that ships
Generated from the catalogue this site is serving. Each id links to its own page, with the condition in plain English, the kinds and components it covers, and the designs that trip it.
| Rule | Severity | Selects | Why |
|---|---|---|---|
| ai-api-key-unmanaged | warning | every node of kind ai-service | This AI vendor is reached with an API key, and nothing in the design holds or rotates that key. |
| ai-no-gateway | info | every design | With three or more AI vendors and no gateway, every caller holds every key, writes its own retries and fallbacks, and spend is spread across vendor dashboards. |
| business-updates-without-index-propagation | warning | every node of kind sql-database | Application servers write this database and the design keeps a search index, but nothing captures the database's changes. |
| cache-as-primary-store-without-persistence | error | every node of kind cache | This cache is the only copy of its data and has persistence turned off. |
| cache-missing-eviction-policy | warning | every node of kind cache | A cache with no declared eviction policy inherits whatever the vendor chose, and most vendors default to evicting nothing. |
| credentials-without-rotation | warning | every design | The design stores data but says nothing about rotating the credentials that reach it, and it has no key-management node that would rotate them. |
| cross-cell-call | error | every synchronous edge | In a cell architecture each cell has to be able to fail on its own. |
| custom-node | warning | every node of kind custom | A custom node is a hole in the model: the catalogue has no word for it, so no rule can check anything about it. |
| custom-nodes-present | warning | every design | The design places nodes the catalogue does not name, so the rules are blind to that part of the architecture. |
| datastore-missing-backup | error | every node of kind sql-database | A system of record with backups off, or with no retention period stated, cannot be restored. |
| datastore-single-region | error | every node of kind sql-database | The design promises multi-region availability, but this datastore sits in one region and does not replicate. |
| derived-store-without-cdc-or-rebuild | error | every node of kind search-index | A search index is derived data and needs an asynchronous feed from a stream, change data capture or an indexing worker. |
| edge-auth-key-where-identity | info | every edge | The cloud behind this hop can authenticate the caller by an identity it never holds: a managed identity on Azure, an IAM role on AWS, a service account on Google Cloud. |
| edge-auth-unset | info | every edge | How a caller proves who it is decides what a leak costs: a password or a key in configuration can be copied and used from anywhere, a managed identity or an IAM role cannot. |
| exactly-once-with-external-side-effects | warning | every edge declaring exactly-once delivery | Exactly-once delivery stops at the consumer. |
| external-call-inline-without-durable-path | warning | every node of kind app-server | A service that calls a third party synchronously, with no queue or workflow engine attached, has nowhere to park the request when the third party is slow or down. |
| fanout-inline-in-request-path | warning | every node of kind app-server | Calling a worker synchronously puts the background job on the request path. |
| materialised-cache-without-rebuild-path | error | every node of kind cache | A cache holding materialised views has to be rebuildable from a durable store or a worker. |
| money-mutation-without-idempotency-key | error | every edge | A call that moves money is retried after a lost response. |
| money-store-without-double-entry-ledger | error | every design | The design promises that money movements sum to zero, but it has no ledger to enforce double entry. |
| multi-leader-store-without-conflict-policy | warning | every node of kind nosql-database | Multi-leader replication accepts concurrent writes in several regions. |
| object-store-single-zone-with-durability-target | error | every node of kind object-store | Eleven nines of durability assumes redundancy spread over at least three failure domains. |
| pubsub-replay-required | error | every node of kind pubsub-topic | The design declares that consumers must be able to replay history. |
| push-fanout-without-celebrity-path | warning | every node of kind worker | Pure push fan-out over a social graph is brutal at the tail: one post from an account with millions of followers becomes millions of cache writes. |
| queue-missing-dlq | error | every node of kind queue | A queue that receives asynchronous traffic and has no dead-letter queue either drops poison messages or retries them forever. |
| read-heavy-path-without-cache | warning | every node of kind sql-database or nosql-database | The design states a read-heavy workload and application servers read this datastore directly, yet the design has no cache anywhere. |
| read-replica-as-sole-scaling | warning | every node of kind sql-database | Asynchronous read replicas scale reads only. |
| reconnect-stampede-without-guard | error | every node of kind realtime-gateway | When a gateway holding millions of sockets restarts, every client reconnects at once and the gateway asks the coordination service about each of them. |
| retry-queue-without-dlq | error | every node of kind queue | A retry queue is where poison messages end up. |
| retry-without-adaptive-shedding | error | every edge | Retries into a service that can neither shed load nor have its circuit opened produce a metastable failure: the retries keep the dependency saturated long after the original fault is gone. |
| retry-without-backoff | warning | every node of kind queue | Retrying a transient failure at full speed turns one slow dependency into a retry storm. |
| shared-dependency-without-redundancy | error | every node of kind dns or coordination-service | The design requires redundant discovery, but this DNS or coordination service, which everything depends on, runs on a single provider or a single replica. |
| shared-host-per-channel-without-isolation | error | every node of kind realtime-gateway | With heavy-tail traffic a few channels carry most of the load. |
| shared-worker-pool-across-queues | warning | every node of kind worker | One worker pool consuming several queues lets a slow or poisoned queue starve the others. |
| single-writer-primary-at-peak-qps | error | every node of kind sql-database | A single-writer, unsharded relational primary has a throughput ceiling. |
| stateful-engine-pinned-to-single-zone | error | every node of kind matching-engine, coordination-service or event-store | A matching engine, coordination service or event store living in exactly one availability zone, with no failover and no replication, is lost with that zone. |
| stateful-hash-ring-without-ring-manager | warning | every node of kind app-server | A stateful service on a consistent hash ring needs something that owns the ring: detect a dead host, pick a replacement, tell everyone. |
| stateful-node-without-zone-redundancy | error | every node in category compute | A stateful compute node pinned to exactly one availability zone disappears with that zone, and its state goes with it. |
| stateful-websocket-scale-in-without-drain | warning | every node of kind realtime-gateway | Scaling a gateway down kills every live WebSocket on the removed instance at once. |
| sync-chain-too-deep | warning | every node of kind api-gateway | Every consecutive synchronous hop from this entry point spends part of the stated p95 latency budget, and the chain's failures multiply. |
| webhook-without-signature-or-idempotency | error | every node of kind webhook-endpoint | A webhook is a public endpoint that its caller retries. |
| windowed-aggregation-without-watermark | warning | every node of kind stream-processor | Event-time windows need a watermark to know when a window is complete. |