git:20261014.97fa750 to git:20261014.8b817dc
1 added, 1 removed. Audit A to A.
---
name: cratis-chronicle-event-modeling
description: Design a Chronicle event model before writing code - stream boundaries and event-source identities, commands, past-tense events, read models, automations and translations, compliance subjects, and the specification outline. Use when behavior, event vocabulary, stream boundaries, or a multi-slice flow is not yet settled. Do not use to draw or update an existing model diagram, and do not use for a mechanical change to a flow that is already decided.
license: MIT
---
# Chronicle event modeling
> **Model-first check.** If an accepted model under the model root covers this scope, or the
- > repository is opted in (run `cratis-screenplay-modeling-lifecycle`'s `scripts/model-first-status.mjs`), the model is the source of truth: use
+ > repository is opted in (per `general.md` "Model first"; where `cratis-screenplay-modeling-lifecycle` is installed, run its `scripts/model-first-status.mjs`), the model is the source of truth: use
> `cratis-screenplay-event-modeling` (and the lifecycle in
> `cratis-screenplay-modeling-lifecycle`, which holds the master definition)
> instead of this skill. This skill is for
> hand-written C# models where the repository has not opted in (never force a
> model; only the entry-point session proposes one, at most once per session), and for
> code-level work against an already accepted contract (stream identity,
> compliance, specifications). If the repository is opted in and the Screenplay
> skills are not installed, report the missing capability and block the
> model-authoring work; do not write a competing brief here and never author
> model syntax from memory.
Use this **before writing code**, when behavior, event vocabulary, stream
boundaries, or a multi-step flow is not already settled. The output is an
implementation brief: which commands exist, which stream each event lands on,
which read models consume those events, which automations and translations react,
and which specifications prove the flow.
Skip it for a mechanical change where the event types and the flow already exist
and the request is wiring or a narrow fix.
> **Lineage.** The four behavior types and the given/when/then-per-behavior
> discipline follow **Event Modeling** (Adam Dymitruk; Martin Dilger,
> *Understanding Eventsourcing*). This skill applies that method to Chronicle.
## Verified product sources
Verified against **Cratis/Chronicle v19.50.1**:
`Source/Clients/DotNET.CodeAnalysis/DiagnosticIds.cs`,
`ProtectedValues/EncryptedAttribute.cs`, `EventSources/`, `Events/PublicAttribute.cs`,
`Sinks/PublishToAttribute.cs`, and `Events/Constraints/ClosesStreamAttribute.cs`
(paths under `Source/Clients/DotNET/` unless otherwise stated).
Tool versions: `cratis-screenplay-toolchain` `SKILL.md`.
| Package | Purpose |
| --- | --- |
| `Cratis.Chronicle` | event types, event-source identities, subjects, `[PII]`, `[Encrypted]` |
| `Cratis.Chronicle.CodeAnalysis` | `CHR0012`, `CHR0026`, `CHR0034`, `CHR0035`, `CHR0052` |
Reverify product sources before claiming support for another version.
## The four behavior types
Classify every behavior in the model as exactly one:
| Type | What it does | Built from |
| --- | --- | --- |
| **State change** | accepts a command, appends events | command, validation, events |
| **State view** | projects events into a queryable read model | read model plus a projection or a reducer |
| **Automation** | reacts to our own events and causes a side effect: an external call, a follow-up event (Screenplay `produces`) or a follow-up command (Screenplay `invokes`) | a reactor |
| **Translation** | takes data from outside our own facts (an external system or another service) and records it as our own facts | an adapter at the boundary, often a reactor |
The two differ in where the data comes from. An automation answers one of our own
events. A translation brings outside data in and keeps the external fact distinct
from the local one. A reactor that turns our own event into a follow-up event or
command is therefore an **automation**, however it is built. In Screenplay terms
these are the `Automation` and `Translate` slice types.
## Decide before implementing
- **Behavior name and type** for each step in the flow.
- **Commands** — their inputs and the authorization that gates them. Commands
are imperative intents. For each rejection condition, name the values it reads
as input, read state or context (time, caller, tenant). Context comes from the
command pipeline or an injected time source, never from the wall clock inside
the decision, so a specification can fix it.
- **Who issues each command**: a named role or an automation, never a generic
user. Each role's "cannot" becomes authorization plus a denial specification.
- **Story order**: the flow from its first trigger to every ending, with
branches and terminal facts named.
- **Refused transitions**: per state, which commands are refused and by which
rule (a state-by-command table).
- **Events** — past-tense, one-purpose facts. Names must be self-describing:
`<Entity><PastTenseVerb>`, never a bare `Created`. Name them for one real
flow with real values. On a generic platform, model a case it handles
(`HullDamageReported`), not its machinery (`FormSubmitted` with `content`).
- **The event-source id for every event.** Events never carry their own
event-source id as a payload property; it lives in the event context. Deciding
the id *is* deciding the stream boundary.
- **Event properties are non-nullable.** Analyzer `CHR0012` warns otherwise.
Model an optional fact as a separate event, not a nullable field.
- **Read models** — their consumers and their source events, and whether the
model is projection-backed, reducer-backed, or `[Passive]` for a
strongly-consistent command-side decision.
- **Automations and translations** — which events (or which outside data) they
react to, whether they emit follow-up events or run commands, and which side
effects must not repeat.
- **Specifications** — the happy path, the validation failures, the constraints,
the projections and reducers, and the reactor side effects.
Do not append an event for derived or aggregate state. Project it from the source
events instead; an event that records a total is a fact that can disagree with
its own inputs.
## Public facts and streams that end
Declare source vocabulary with `IEventSource`, `[EventSource]` and `[EventStream]`;
use **cratis-chronicle-client-dotnet** for append routing and
**cratis-chronicle-reactor** for `[FromEventSource<T>]` filters. A stream that
ends needs an explicit lifetime and scope: does closing cover one source, a
period across sources, or every stream of a source type? Prefer a past-tense
closing fact with `[ClosesStream]`; use **cratis-chronicle-event-constraints** for
manual scope completion, reopening and its crash/reindex limits. Closing refuses
new appends; it does not erase history or stop projection replay. Specify a
late append refusal and an unrelated scope remaining open.
`[Public]` grants publication eligibility; `[PublishTo("sequence")]` selects the
projection/reducer event-output destination (outbox by default), not eligibility.
Owning-store membership can also grant eligibility, so private registration
visibility alone does not forbid publication. The event log is not a publication target. Translate inbox facts
into local facts at the boundary rather than treating external contracts as
local commands. Named tags are plaintext, indexed metadata: never personal data.
State-dependent decisions need **cratis-chronicle-decision-reads** when their
read facts must remain valid at append; an ordinary passive read alone does not
close the read/append race. Arc command integration belongs to
**cratis-arc-command-validation**.
## Information completeness — trace every field to an event
This is the check that pays for itself, and it belongs at modeling time rather
than after the projection misbehaves.
- **Backward:** for each read model, walk every property back to the event that
carries it. A field with no source event is a **missing event or command** —
not a nullable column. Resolve it in the model.
- **Forward:** every event you define should feed at least one read model,
automation, or translation. An event nothing consumes is a smell: either a
consumer is missing or the event should not exist.
If a field can only be filled by reaching into another behavior's read model to
*learn a fact the events should have carried*, you have found a missing event or
a wrong stream boundary. Fix the model. A read-only cross-slice read through
Chronicle is allowed where the rule needs it (an injected read model read by
explicit key, or a `[Passive]` projection for a current read; a rule that must hold under
concurrency needs a decision read or constraint, see
[choosing how a rule holds](../cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md)); see
Cross-slice patterns in `rules/vertical-slices.md`. Make such a read deliberate
and name it in the brief, with the consistency it needs: a rule that must hold
under concurrency needs enrolled guarded reads, not plain injection or a stale
materialized snapshot. In Arc v22.75.0 use `IStreamReads`/`StreamDecision` for one
stream or `[ProtectedDecision]` + admitted `DecisionRead<T>` for projected state;
`Result<,>` alone supplies rejection, not a concurrency guard.
## Compliance modeling
Decide this *before* choosing event and read-model shapes, because erasure
operates on the subject and the subject follows the stream.
- Prefer **one subject per event stream** for person-level personal data. The
subject then defaults correctly to the event-source id and needs no attribute.
- Mark inherently personal values with `[PII]` — names, email addresses, phone
numbers, identity-provider subjects, personal notes. Leave business metadata
unmarked; every marked value becomes unreadable once the subject is erased.
- Record the rationale with `[ComplianceDetails("...")]`. A string argument
passed to `[PII]` is **not** read by Chronicle.
- Set `[Subject]` only when the person is not the event source. A stored
read-model document carries one subject — do not mix several people's personal
data into one document.
- Never place `[PII]` or `[Encrypted]` on an event-source id: Chronicle cannot encrypt it, and
analyzers `CHR0034` and `CHR0052` reject them. When the natural identifier is itself
sensitive, model a surrogate stream id and carry the sensitive value as a
`[PII]` property.
- Do not place `[Key]` or `[Subject]` on an `EventSourceId<T>` value — it is
already both, and analyzer `CHR0026` says so.
- Bearer tokens, magic links, and signed URLs are not durable facts. Model a
keyed hash or an opaque reference, never the secret. Chronicle has no attribute
that withholds a value from the log. Where an operational secret must genuinely be
retained (an API key, a signing secret), mark it `[Encrypted]` (encrypted at rest,
recoverable, not a PII erasure subject; never on a `[PII]` value or an event-source id)
and keep it out of the causation chain with Arc's `[NotAudited]`; see `cratis-arc-command`
and `cratis-chronicle-compliance`.
If a subject boundary cannot be made person-level without changing product
behavior, **stop and surface that trade-off** before implementing.
## Output shape
Write the brief in this order:
1. stream boundaries and subjects
2. commands and events
3. read models and their consumers
4. automations and translations
5. compliance notes
6. specifications
## The lifecycle: draft, ready, working, done
The behavior is the unit of work, and it moves through four states. The model —
events, commands, read models, screens, specifications — lives inside it, and
anyone may author or update it.
- **Draft** — being modeled; events, commands, read models, and boundaries are
still in flux.
- **Ready** — the handoff gate. The model is *information-complete*: the checks
above pass, and commands, authorization, compliance, and the specification
outline are all decided. A ready behavior can be implemented with **no further
modeling decisions**.
- **Working** — an implementer has picked it up and is running the
implementation workflow end to end.
- **Done** — every quality gate is green.
**Marking a behavior ready is the signal to implement it.** Do not batch ready
items behind one another. Branching, pull requests, merging, and publication
remain separately authorized; a ready model authorizes none of them.
## Verify the brief before handing it over
- Every behavior has exactly one type.
- Every event is past-tense, self-describing, single-purpose, and non-nullable.
- Every event has a decided event-source id, and none carries that id as a
property.
- Every read-model field traces back to an event.
- Every event has at least one consumer.
- Every cross-slice read is deliberate, read-only, goes through Chronicle, and
is named in the brief with the consistency it needs; none stands in for a
missing event.
- Personal data has a decided subject, and the subject is person-level or the
trade-off is surfaced.
- No data-loading command (`LoadOrders`), UI event (`ButtonClicked`), technical
event (`ApiCalled`) or recomputed event.
- Every state has its refusals listed.
- The specification outline names the failures, not only the happy path.
## Route near misses
- A model covers the scope or the repository opted in (see the check at the top): start from `cratis-screenplay-event-modeling`; do not write the brief here.
- Modeling the same system as a **Screenplay `.play` document** rather than as a
brief for hand-written C#: `cratis-screenplay-event-modeling`. The method is the
same; the artifact is a file that compiles, and the four behavior types above
map onto its `StateChange`, `StateView`, `Automation` and `Translate` slices.
- Drawing a decided model as a diagram: `cratis-event-model-diagram`.
- A flow that crosses services, each owning its own event store: `cratis-multi-service-architecture`.