Catalogue / rules

Rules

Declarative predicates over a design. A rule reads a node's effective properties, its edges, its groups and the design's requirements. A finding never blocks a save; a structural problem always does. Open a rule for its rationale, the condition in plain English, and the reference designs that trip it.

AI API key not managedwarningnode.kind = ai-service
ai-api-key-unmanaged
AI service '{{node.id}}' is called with an API key, the key is not held in a key-management component, and the design has none.
This AI vendor is reached with an API key, and nothing in the design holds or rotates that key. It ends up in environment files or code, every caller shares it, and a single leak runs up the vendor bill until someone notices. Put the key in a key-management component, or say it is held there with keyStore.
Database updates not propagated to the search indexwarningnode.kind = sql-database
business-updates-without-index-propagation
Database '{{node.id}}' is written by application servers and the design has a search index, but no change-data-capture propagates its updates. Index writes rely on dual writes.
Application servers write this database and the design keeps a search index, but nothing captures the database's changes. The index then depends on the application writing to both places, and the two drift apart silently.
Cache without eviction policywarningnode.kind = cache
cache-missing-eviction-policy
Cache '{{node.id}}' has no eviction policy. Declare lru, lfu or ttl, or justify none explicitly.
A cache with no declared eviction policy inherits whatever the vendor chose, and most vendors default to evicting nothing. The cache fills up and then starts refusing writes.
Credentials without rotationwarningdesign
credentials-without-rotation
Design '{{design.id}}' holds data but declares no credential rotation period and has no key-management node.
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. A leaked credential stays valid forever.
Custom nodewarningnode.kind = custom
custom-node
Node '{{node.id}}' is custom: no rule can judge it. Replace it with a catalogue component or contribute one (docs/governance.md).
A custom node is a hole in the model: the catalogue has no word for it, so no rule can check anything about it. It is meant to be temporary, and naming it in the catalogue closes the hole.
Design has custom nodeswarningdesign
custom-nodes-present
This design has custom nodes; rules are blind to them. Aim for zero.
The design places nodes the catalogue does not name, so the rules are blind to that part of the architecture. The count belongs on the design, not only on the individual boxes.
Exactly-once delivery in front of an external side effectwarningedge
exactly-once-with-external-side-effects
Edge '{{edge.from}}' -> '{{edge.to}}' claims exactly-once delivery, but '{{edge.to}}' calls a third party synchronously. Exactly-once cannot cover that side effect; make the external call idempotent instead.
Exactly-once delivery stops at the consumer. When that consumer then calls a third party synchronously, the side effect sits outside the transaction and can still happen twice, so the exactly-once claim hides the need for an idempotent external call.
Inline external call without durable fallbackwarningnode.kind = app-server
external-call-inline-without-durable-path
Service '{{node.id}}' calls a third party inline with no queue or workflow engine to fall back to when that call fails.
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. The failure becomes the user's problem instead of a retried job.
Fan-out inline in the request pathwarningnode.kind = app-server
fanout-inline-in-request-path
Service '{{node.id}}' calls a worker synchronously. Hand the job to a queue or stream so the request path does not wait for fan-out.
Calling a worker synchronously puts the background job on the request path. The caller waits for the slowest part of the fan-out.
Multi-leader store without a conflict policywarningnode.kind = nosql-database
multi-leader-store-without-conflict-policy
Store '{{node.id}}' is multi-leader across regions with no conflict resolution policy. Declare last-writer-wins, vector-clock or app-merge.
Multi-leader replication accepts concurrent writes in several regions. With no conflict resolution policy declared, divergent replicas are reconciled by accident rather than by design.
Push fan-out without a celebrity thresholdwarningnode.kind = worker
push-fanout-without-celebrity-path
Worker '{{node.id}}' pushes fan-out over a social graph with no fanoutThreshold. Declare a follower threshold above which timelines are merged at read time.
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. A follower threshold above which readers merge at read time bounds that write amplification.
Read-heavy datastore without a cachewarningnode.kind = sql-database,nosql-database
read-heavy-path-without-cache
Datastore '{{node.id}}' serves a read-heavy workload directly from application servers and the design has no cache.
The design states a read-heavy workload and application servers read this datastore directly, yet the design has no cache anywhere. Every read pays a database round trip.
Read replicas as the only scalingwarningnode.kind = sql-database
read-replica-as-sole-scaling
Database '{{node.id}}' relies on async read replicas as its only scaling above 5000 rps. Replicas do not scale writes.
Asynchronous read replicas scale reads only. When the store is neither sharded nor distributed the writes still land on one primary, so the replicas add nothing at the declared peak.
Retry queue without backoffwarningnode.kind = queue
retry-without-backoff
Retry queue '{{node.id}}' declares no backoff. Retries without backoff amplify the outage they are retrying through.
Retrying a transient failure at full speed turns one slow dependency into a retry storm. A retry queue backs off, preferably exponentially and with jitter.
Shared worker pool across queueswarningnode.kind = worker
shared-worker-pool-across-queues
Worker '{{node.id}}' consumes more than one queue while the design requires per-queue isolation. Give each queue its own worker pool.
One worker pool consuming several queues lets a slow or poisoned queue starve the others. That is precisely what the design's per-queue isolation requirement forbids.
Stateful hash ring without ring managerwarningnode.kind = app-server
stateful-hash-ring-without-ring-manager
Service '{{node.id}}' is on a consistent hash ring but is not connected to a coordination service that manages the ring.
A stateful service on a consistent hash ring needs something that owns the ring: detect a dead host, pick a replacement, tell everyone. Without a coordination service the ring is edited by hand during the outage.
Stateful WebSocket scale-in without drainwarningnode.kind = realtime-gateway
stateful-websocket-scale-in-without-drain
Realtime gateway '{{node.id}}' autoscales but does not drain connections on scale-in. Every scale-down becomes a reconnect storm.
Scaling a gateway down kills every live WebSocket on the removed instance at once. Without a drain step those clients all reconnect together.
Synchronous call chain too deepwarningnode.kind = api-gateway
sync-chain-too-deep
Entry point '{{node.id}}' starts a synchronous call chain deeper than 3 hops while the design states a p95 latency target.
Every consecutive synchronous hop from this entry point spends part of the stated p95 latency budget, and the chain's failures multiply. More than three hops is a budget nobody is tracking.
Event-time windows without watermarkwarningnode.kind = stream-processor
windowed-aggregation-without-watermark
Stream processor '{{node.id}}' windows by event time but declares no watermark. Late data handling is undefined.
Event-time windows need a watermark to know when a window is complete. Without one the processor either waits forever or closes windows early and silently drops late events.