---
name: cratis-specification-by-example
description: Structure and name executable specifications the Cratis way — one behavior per specification, a for_/when_/and_ path that reads as an English sentence, layered given/ contexts, and an explicit decision about what is not worth specifying. Use when deciding how to organize, name, or scope specifications in any language. Do not use for language mechanics; route to the C# or TypeScript specification skill for those.
license: MIT
---

# Cratis specification by example

Cratis calls automated tests **specifications**. That is not a vocabulary
preference — it changes what you write. A test asks "does this code still do
what it did yesterday?". A specification states what the software promises, in
the language of the domain, in a form a machine can check. The folder tree is
the table of contents; the file names are the sentences; the assertions are the
promises.

This skill is the language-agnostic layer. It settles the questions that are the
same in C#, TypeScript, and anything else: what to specify, how to name it,
where to put it, and when to stop.

## Verified product sources

| Source | Version | What it grounds |
| --- | --- | --- |
| `Cratis.Specifications` analyzers | `CRSPEC0001`–`CRSPEC0007` | The naming and structure rules below are machine-enforced in C#, not taste |

The seven diagnostics are declared in `Cratis.Specifications.CodeAnalysis.DiagnosticIds`:

| Id | Rule |
| --- | --- |
| `CRSPEC0001` | A test method inside a specification must be named `should_*` |
| `CRSPEC0002` | A file declares at most one specification |
| `CRSPEC0003` | A test method must not sit on a reusable `given/` context |
| `CRSPEC0004` | A `should_*` method without a test attribute never runs |
| `CRSPEC0005` | A lifecycle method must not call its base implementation |
| `CRSPEC0006` | A specification declaring test methods must be public so the runner finds it |
| `CRSPEC0007` | The action under test must not sit on a reusable `given/` context |

In a language without those analyzers the same rules hold; the reviewer enforces
them instead of the compiler. Reverify against the owning product repository
before claiming behavior for another version.

The `Cratis.Synopsis.Tool` dotnet tool (verified) reads these
names statically, never running code, and writes one HTML or JSON behavior
document from C#, TypeScript, Gherkin and Screenplay specifications:
`dotnet tool install --global Cratis.Synopsis.Tool`, then `synopsis . --open`
(or `synopsis . --format both --output Artifacts/synopsis.html`). Good names are
the documentation it produces.

## Route near misses

- Writing the C# mechanics — the `Specification` base, `Establish`/`Because`,
  substitutes, assertions: use `cratis-specifications-csharp`.
- Writing the TypeScript mechanics — `describe`/`it`, Sinon, the Chai `should`
  interface: use `cratis-specifications-typescript`.
- Specifying an event-sourced application slice with the in-process scenario
  family: use `cratis-application-slice-specifications`.
- Deciding what a command, projection, reducer, or reactor *should do*: that is
  a modeling question. Settle the behavior first; a specification records a
  decision, it does not make one.
- The scope is model-first (the behavior lives in `specification` blocks of the
  model). Decide this with the decision rule in
  `cratis-screenplay-modeling-lifecycle`: model-first only when an accepted
  model under the model root covers this scope, or the repository is opted in
  (opted in per `general.md` "Model first"; where `cratis-screenplay-modeling-lifecycle` is installed, run its `scripts/model-first-status.mjs`); framework, infrastructure, client, adapter and not-opted-in
  brownfield work stays code-first. Opted in but no model for this scope: model
  it first (`cratis-screenplay-discovery`, then `cratis-screenplay-slice-design`)
  before writing specifications. If those Screenplay skills are not installed,
  say so and stop; never author `.play` from memory. Never edit Stage-managed
  output, and never change a model specification to match existing code. Write model specifications with
  `cratis-screenplay-specifications`, decide coverage with
  `cratis-screenplay-scenario-coverage`, and treat code specifications as
  realizations of them (`cratis-application-slice-specifications`;
  `cratis-application-slice-conformance` checks the two against each other).

## Step 1 — State the behavior as a sentence

Before creating a file, say the specification out loud as one English sentence:

> **for** the changeset, **when** adding changes, **and** there are differences,
> it **should** record them.

Every clause becomes one level of the path. If the sentence does not survive
being spoken, the specification is not focused enough yet — that is the signal
to split it, not to write a longer name.

## Step 2 — Build the path from the sentence

```
for_<SubjectUnderTest>/
├── given/
│   ├── all_dependencies            ← substitutes every collaborator
│   └── a_<subject>                 ← builds the subject, layered on the above
├── when_<behavior>/                ← a behavior with several outcomes
│   ├── and_<condition>
│   ├── with_<state>
│   └── without_<requirement>
└── when_<simple_behavior>          ← a single outcome is a single file
```

- **`for_<Subject>`** names the thing being specified.
- **`when_<behavior>`** names the action. This is the only place the word
  **`when`** may appear.
- **Outcome names** use one of five prepositions: `and_`, `with_`, `without_`,
  `having_`, `given_`.

**Two `when`s in one path is always wrong.** A file named
`with_a_registered_migration_when_appending_a_generation_1_event` is two
sentences pretending to be one. Fold the context into the `when_` folder and let
the outcomes be flat files:

```
# Wrong
when_appending_event_with_migrations/
└── with_a_registered_migration_when_appending_a_generation_1_event

# Also wrong — a folder level that holds a single file
when_appending_event_with_migrations/
└── and_event_is_generation_1/
    └── with_a_registered_migration

# Correct
when_appending_event_with_registered_migration/
├── and_event_is_generation_1
├── and_event_is_generation_2
└── and_event_has_default_value
```

Add a sub-folder under `when_` only when that condition has its own several
outcomes. One outcome is one flat file.

## Step 3 — One behavior, one specification

Each distinct outcome is its own file. When a specification fails you should
know from its name alone which promise broke, without reading the diff.

- A file that specifies a whole class is not a specification; it is a test suite
  wearing the name.
- Assertions inside one file all describe the *same* outcome from different
  angles. A second setup means a second file.
- Optimize for readability over removing duplication. Repeating three lines of
  setup so a specification is self-contained is a good trade; a shared helper
  the reader must go and open is not.

## Step 4 — Put the world in `given/`, the action in the specification

A context captures the world *before* the action. Layer them:
`all_dependencies` substitutes the collaborators, `a_<subject>` builds the
subject on top of it, and the concrete specification adds only what is unique to
its case.

- Name a context `a_` or `an_` so it reads as "given an observer, when
  handling".
- **The action under test never appears in a context** (`CRSPEC0007`) and
  **assertions never appear in a context** (`CRSPEC0003`). A context that acts is
  a specification that several files silently share, and the failure it produces
  names the wrong subject.
- Contexts build in layers: `all_dependencies` → `a_reactor_handler` →
  `when_handling`.

## Step 5 — Decide what not to specify

Specify decisions, transformations, branching rules, and coordination between
collaborators — the places defects live. Leave alone:

- Auto-properties and properties that return a constructor parameter.
- Simple delegation that forwards to a collaborator and adds nothing.
- Logging. It is fragile to specify and worth nothing when it passes.
- Trivial null checks the type system already enforces.

A name beginning `when_getting_` or `when_returning_` is the tell: that is a
getter, not a behavior. Delete it rather than maintaining it.

## Step 6 — Make the outcome observable

A specification is only as good as the signal it reads.

- **Assert on the outcome by default.** A presentation string is copy;
  it changes for reasons that have nothing to do with the behavior. Assert on
  the identity of what failed — an error code, a constraint name, a typed
  result — rather than the sentence shown to a user, unless exact wording is
  the specified behavior.
  - **In code (C#, TypeScript):** do not assert message strings by default;
    assert exact text only when that wording is the specified behavior, and
    name that behavior in the fact.
  - **In a `.play` model:** a rejection specification pins the message, or
    its localization key, with `then error "<message>"` or
    `then error "$strings.<key>"`, because the model has no reason codes. Bare
    `then error` leaves the reason to the specification's name. The code
    specification pins the kind by default; assert the model's exact message
    too only when its wording is
    the specified behavior, and name that behavior in the fact.

```screenplay
concept InvoiceId : Uuid
concept InvoiceNumber : String
module Invoicing
  feature Registration
    slice StateChange RegisterInvoice
      command RegisterInvoice
        invoiceId     InvoiceId identifier
        invoiceNumber InvoiceNumber
        validate
          invoiceNumber not empty message "Invoice number is required"
        produces event InvoiceRegistered
          invoiceNumber InvoiceNumber = invoiceNumber
      specification RejectingAnEmptyInvoiceNumber
        when RegisterInvoice
          invoiceId     = "9c858901-8a57-4791-81fe-4c455b099bc9"
          invoiceNumber = ""
        then error "Invoice number is required"
```
- **Never wait on the clock.** A sleep before an assertion passes because the
  machine happened to be fast, and writes today's latency into the suite. Await
  a completion signal, under a deadline. A timeout turns a hang into a named
  failure; a sleep turns a race into a coin flip.
- **Nothing ambient.** The real clock, a random value, the network, shared
  storage, or an ambient culture makes the outcome depend on something the
  specification never stated. Inject it.
- **A specification that cannot fail proves nothing.** If you cannot describe
  the change that would make it red, it is not specifying anything yet.

## Step 7 — Say what the specification did not cover

Finishing a behavior means naming its unspecified edges, not claiming the
behavior is proven. A suite that lists only what passed reads as if everything
was checked. Record the cases you deliberately left out and why — an untested
edge someone chose is a different thing from one nobody saw.

## Verify

- Every path reads as one English sentence, and `when` appears only in a
  `when_<behavior>` folder name.
- Every outcome file name starts with `and_`, `with_`, `without_`, `having_`, or
  `given_`.
- One specification per file; one outcome per specification.
- No action and no assertion sits on a `given/` context.
- Nothing trivial, delegated, or compiler-verified is specified.
- Code assertions do not pin presentation messages by default. Exact wording
  is asserted only when it is the specified behavior, named in the fact; a
  `.play` rejection pins its message or key deliberately.
- No sleep, bare delay, or poll loop stands in for a completion signal.
- The suite runs green, and the report names what was not covered.
