Write The Spec
A rngo spec describes a system's behavior, and the rngo run CLI command checks
if that descriptions holds for a running instance of that system.
In particular, the spec describes behavior in terms of:
- a simulation, which applies effects to the system via channels, all while gathering data
- an audit, which evaluates invariants against the gathered data
So, our goal is to write a spec for your system that results in a realistic simulation followed by a comprehensive audit.
Layout
You should commit a system's spec into its repository at .rngo/spec.yml.
The entire spec could live in that file, but you can also break it out into files at the following locations:
.rngo/channels/*.yml.rngo/effects/*.yml.rngo/invariants/*.yml.rngo/schemas/*.yml
A spec can get large, so this approach makes it easier to manage. The CLI expects this layout, and rngo init will
(partially) set it up for you.
The base spec file at .rngo/spec.yml contains a few parameters that you may want to adjust:
keyis a meaningful identifier for the specseedensures the determinism of the simualation's RNGstart/endcontrol the duration of the simulation
Channels
You should define a channel for any system interface that inputs effects or outputs signals (or both).
For example, here's a channel for a Postgres DB that might live at .rngo/channels/db.yml:
format:
type: sql
target:
type: stream
command: psql -q $DATABASE_URLEvery spec will include at least one public channel, which may correspond to:
- REST API
- GraphQL API
- CLI
Additionally you may want to include private channels in order to:
- apply historical effects (assuming public channels do not support backdating)
- set up or query state in SaaS sub-systems
- capture observability signals
These could correspond to:
- database - PostgreSQL, MySQL, SQLite, MongoDB, DynamoDB, DuckDB, etc.
- SaaS - Stripe, Better Auth, Resend, Shopify, Slack, etc.
- monitoring - Sentry, Datadog, Splunk, log files, etc.
- infrastructure - Cloudflare, AWS, GCP, Azure, etc.
Effects
You should write an effect for any of the following reasons:
- to model a client interaction
- to simulate historical usage
- to query data to be referenced by other effects
For example, this effect creates a user and might live at .rngo/effects/user.create.yml
trigger: hz(1, hour)
channel: db
metadata:
table: USERS
schema:
type: object
properties:
name:
type: string
pattern: .{10,50}
age:
type: number
minimum: 18
scale: 0Effects can be inferred from API definitions, e.g. add an effect for every endpoint:
- OpenAPI operations
- GraphQL queries / mutations
- RPC service methods
Or from database schemas - add an effect to create a record for every table:
- PostgreSQL:
pg_dump --schema-only mydb - MySQL:
mysqldump --no-data mydb - SQLite:
sqlite3 mydb .schema - Drizzle ORM:
cat db/schema.ts - Rails:
cat db/migrate/*.rb
For schemaless DBs, infer writes from the source code and write effects.
Invariants
You should write an invariant for every behavorial pattern you expect to observe.
For example, this invariant states that any attempt to create a user with an age less than 18
should result in an API error. It might live at .rngo/invariants/minor-users-rejected.yml:
type: sql
query: >
SELECT count(*)
FROM signals s
JOIN effects e ON s.effect_id = e.id
WHERE e.key = 'user.post'
AND e.value ->> '$.age' < 18
AND s.data NOT LIKE '%422 Unprocessable Content%';
expect: result == 0Ideally, you'll be able to derive invariants from product and architectural documentation, although in many cases the invariants themselves will be the source of truth.
Schemas
You should avoid duplicating a schema across multiple effects. Instead, add a custom schema type and reference it by name.
For example, if you add the following at .rngo/schemas/uuid.yml:
schema:
type: string
pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$You can reference it at, e.g., .rngo/effects/user.create.yml:
trigger: hz(1, hour)
channel: db
schema:
type: object
properties:
id:
type: uuid
name:
type: string