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

yaml
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."
FieldRequiredMeaning
idyeskebab-case, and it reads as the problem, not the fix: queue-missing-dlq, never add-dlq
severityyeserror, warning or info
selectyesWhat the rule is evaluated against, once per match
whenyesThe predicate. The rule fires where it is true
messageyesOne 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.

FormSubject
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: trueThe design itself, once

The predicate vocabulary

Predicates compose with all, any and not. Everything else is a leaf.

Composition

yaml
when:
  all:
    - prop: "replication"
      equals: "none"
    - not:
        requirement: "availability.multiRegion"
        equals: true
FormTrue when
all: [...]Every listed predicate is true
any: [...]At least one is true
not: <predicate>The predicate is false

Node properties

yaml
when:
  prop: "evictionPolicy"
  exists: false
PredicateTrue when
prop: p, equals: vThe effective value of p is v
prop: p, in: [v1, v2]The effective value is one of the list
prop: p, exists: boolWhether p resolves to any value at all
prop: p, gt: n / lt: nInteger 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

yaml
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

yaml
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

yaml
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

yaml
when:
  requirement: "availability.multiRegion"
  equals: true
PredicateTrue when
requirement: path, equals: vThe requirement at path equals v
requirement: path, exists: boolWhether the requirement is set
requirement: path, gt / lt: nInteger 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

yaml
when:
  hasEdge:
    direction: "in"
    mode: "async"

hasEdge is true when at least one edge in that direction matches the filter.

KeyValues
directionin, out, any
modesync, async
deliveryat-most-once, at-least-once, exactly-once
peerKindThe kind of the node at the other end
peerCategoryThe category of the node at the other end
countA comparison — { gt: 3 } — over the number of matching edges
yaml
when:
  hasEdge:
    direction: "out"
    mode: "sync"
    peerCategory: "external"
    count: { gt: 2 }

Nodes in the design

yaml
when:
  hasNode:
    kind: "custom"

True when the design contains at least one node of that kind or category. Available on every subject.

Groups

yaml
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".

yaml
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

yaml
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

yaml
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

SeverityForEffect
errorData loss, availability, correctnessFails a CI gate
warningPerformance and costComments without failing
infoStyleListed 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.

RuleSeveritySelectsWhy
ai-api-key-unmanagedwarningevery node of kind ai-serviceThis AI vendor is reached with an API key, and nothing in the design holds or rotates that key.
ai-no-gatewayinfoevery designWith 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-propagationwarningevery node of kind sql-databaseApplication servers write this database and the design keeps a search index, but nothing captures the database's changes.
cache-as-primary-store-without-persistenceerrorevery node of kind cacheThis cache is the only copy of its data and has persistence turned off.
cache-missing-eviction-policywarningevery node of kind cacheA cache with no declared eviction policy inherits whatever the vendor chose, and most vendors default to evicting nothing.
credentials-without-rotationwarningevery designThe 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-callerrorevery synchronous edgeIn a cell architecture each cell has to be able to fail on its own.
custom-nodewarningevery node of kind customA 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-presentwarningevery designThe design places nodes the catalogue does not name, so the rules are blind to that part of the architecture.
datastore-missing-backuperrorevery node of kind sql-databaseA system of record with backups off, or with no retention period stated, cannot be restored.
datastore-single-regionerrorevery node of kind sql-databaseThe design promises multi-region availability, but this datastore sits in one region and does not replicate.
derived-store-without-cdc-or-rebuilderrorevery node of kind search-indexA search index is derived data and needs an asynchronous feed from a stream, change data capture or an indexing worker.
edge-auth-key-where-identityinfoevery edgeThe 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-unsetinfoevery edgeHow 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-effectswarningevery edge declaring exactly-once deliveryExactly-once delivery stops at the consumer.
external-call-inline-without-durable-pathwarningevery node of kind app-serverA 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-pathwarningevery node of kind app-serverCalling a worker synchronously puts the background job on the request path.
materialised-cache-without-rebuild-patherrorevery node of kind cacheA cache holding materialised views has to be rebuildable from a durable store or a worker.
money-mutation-without-idempotency-keyerrorevery edgeA call that moves money is retried after a lost response.
money-store-without-double-entry-ledgererrorevery designThe design promises that money movements sum to zero, but it has no ledger to enforce double entry.
multi-leader-store-without-conflict-policywarningevery node of kind nosql-databaseMulti-leader replication accepts concurrent writes in several regions.
object-store-single-zone-with-durability-targeterrorevery node of kind object-storeEleven nines of durability assumes redundancy spread over at least three failure domains.
pubsub-replay-requirederrorevery node of kind pubsub-topicThe design declares that consumers must be able to replay history.
push-fanout-without-celebrity-pathwarningevery node of kind workerPure 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-dlqerrorevery node of kind queueA queue that receives asynchronous traffic and has no dead-letter queue either drops poison messages or retries them forever.
read-heavy-path-without-cachewarningevery node of kind sql-database or nosql-databaseThe design states a read-heavy workload and application servers read this datastore directly, yet the design has no cache anywhere.
read-replica-as-sole-scalingwarningevery node of kind sql-databaseAsynchronous read replicas scale reads only.
reconnect-stampede-without-guarderrorevery node of kind realtime-gatewayWhen 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-dlqerrorevery node of kind queueA retry queue is where poison messages end up.
retry-without-adaptive-sheddingerrorevery edgeRetries 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-backoffwarningevery node of kind queueRetrying a transient failure at full speed turns one slow dependency into a retry storm.
shared-dependency-without-redundancyerrorevery node of kind dns or coordination-serviceThe 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-isolationerrorevery node of kind realtime-gatewayWith heavy-tail traffic a few channels carry most of the load.
shared-worker-pool-across-queueswarningevery node of kind workerOne worker pool consuming several queues lets a slow or poisoned queue starve the others.
single-writer-primary-at-peak-qpserrorevery node of kind sql-databaseA single-writer, unsharded relational primary has a throughput ceiling.
stateful-engine-pinned-to-single-zoneerrorevery node of kind matching-engine, coordination-service or event-storeA 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-managerwarningevery node of kind app-serverA 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-redundancyerrorevery node in category computeA stateful compute node pinned to exactly one availability zone disappears with that zone, and its state goes with it.
stateful-websocket-scale-in-without-drainwarningevery node of kind realtime-gatewayScaling a gateway down kills every live WebSocket on the removed instance at once.
sync-chain-too-deepwarningevery node of kind api-gatewayEvery 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-idempotencyerrorevery node of kind webhook-endpointA webhook is a public endpoint that its caller retries.
windowed-aggregation-without-watermarkwarningevery node of kind stream-processorEvent-time windows need a watermark to know when a window is complete.
On this page