Skip to content

The Event Model ​

The system drawn as an Event Model — slices on a timeline, grouped into chapters, with the specifications bound to each one and the run evidence that says whether they pass.

Two pages draw it, with the same renderer. bobcat ui (http://localhost:5525/event-model, in the free bobcat tool) is where to watch a model while you work on it. Stoat's own page (http://localhost:5526/event-model) draws the model that reached Stoat's receiver on 5527, and adds what only Stoat has: the tools' warnings, the producers' halves, the slice a plan card's ⚠ badge opens, and a Run this slice button for a resident runner.

Where the model comes from ​

Nobody draws it by hand in Stoat. The model is assembled from producers that each see part of the system, and merged.

ProducerWhat it knows
A running Wolverine applicationIts handler chains and HTTP routes (commands, handlers, emitted events, messages, read models), plus any slices declared in code ahead of their implementation, such as the ones bobcat import-event-model writes.
The storeOne View slice per registered projection, named after the document type, carrying the events it really applies.
A Bobcat spec assemblyThe slices its specs are bound to ([BobcatSpec], [BobcatSlice]) and the specifications bound to each.

Each pushes what it can see, under a source name, and PUT /api/event-model/{source} replaces only that source. GET serves the merge.

Merging is by model name, which is why [assembly: EventModelName("…")] matters: the Wolverine-derived model is named for the service, so a spec assembly called MyApp.Tests never joins the chains it describes unless it declares the service's name. Slices merge by slice name within that, so a spec bound to a View slice joins the store's only if it names the document type exactly.

Getting a model in ​

From a running application, whose host exports its own descriptor — to bobcat ui on 5525, and to Stoat on 5527 when you want its page to carry the host's half too:

bash
dotnet run --project src/MyApp -- event-model --url http://localhost:5525/api/event-model
dotnet run --project src/MyApp -- event-model --url http://localhost:5527/api/event-model

Live, as you edit: Bobcat's watch command redraws bobcat ui's canvas on each rebuild:

bash
bobcat watch-event-model src/MyApp

From an eventmodelers.ai board export, with Bobcat's free tool. The import writes the model into your codebase as C# (stub types and a model definition per chapter, and with --specs a spec per board example), so it reaches Stoat the next time the application pushes its model:

bash
bobcat import-event-model board.yaml --model MyApp --namespace MyApp --out ./src/MyApp --specs

--model must be the application's Wolverine service name, or the imported model and the running application show as two models.

Review what it wrote before editing it. How a board splits into slices, aggregates and stream starts is a set of reported guesses, and a wrong guess should be a one-line fix in the generated model rather than a re-import. Pipe its --json report to Stoat to keep those guesses in view (below).

From a spec run: a Bobcat run publishes its spec assembly's half of the model to every address in BOBCAT_MONITOR_URL, under a source named for the assembly. Like the run itself, this never slows or fails the suite. A run Stoat launched lists Stoat's receiver, so its half reaches both.

The halves, and a stale one ​

Each producer pushes its own half of the model, and every half naming the same model is merged into one canvas. A half stays until its producer pushes it again, so an old build's half (a renamed spec assembly, a host export from last month) can keep merging into today's model.

N halves on the canvas lists them: the source, the model it names, when it was last pushed and how many slices it carries. A half not pushed for a week while another half of the same model was is flagged, and a banner names it. The threshold is Stoat:EventModelStaleAfterDays (default 7).

A half is also flagged when it was pushed from another repository or checkout than the newest half: an old clone, or a worktree nobody builds any more. That needs the producer to say where it pushed from, with the optional root, repository and branch query parameters on the push (PUT /api/event-model/{source}?root=/code/wallets&branch=main). Stoat works out the repository from the checkout's git remote when only root is sent. A half whose push names none of them is never judged this way.

A flagged half is still merged. A host export is often pushed far less often than the spec halves and is still the most authoritative half there is, so dropping a half is never automatic. Remove takes one out (DELETE /api/event-model/{source}), and its producer's next push brings it back.

What the canvas draws ​

Stoat, bobcat ui and CritterWatch draw the model with the same renderer, so one model looks the same in each.

  • Lanes for the Event Modeling grammar: triggers, commands, handlers, aggregates, events, projections, read models, external systems.
  • Edges within a slice, drawn from each element's role: Trigger→Command→Handler→ {Aggregate, Event, Message}, Event→Projection→ReadModel, ReadModel→Trigger for a pure view slice.
  • Chapter bands above the lanes, one per contiguous run of same-chapter slices. The canvas never reorders slices, so an interleaved chapter draws one band per run with the same name.
  • Hotspots — a pending specification, or a SourceDisagreement where two producers claim the same slice differently. A View slice whose scenarios between them arrange fewer events than the projection applies is a disagreement, and the right reading is that the view is under-specified.
  • Slice colouring from run evidence, by spec identity, with a drill-down drawer showing each bound spec's step results and flagging touched types the model does not declare.

What the tools could not decide ​

wolverine scaffold and bobcat import-event-model each report what they could not decide on their own: a stream start inferred from a chapter that was not imported, an aggregate a command decides against with no {Aggregate}Id member, Apply methods to add by hand, an application with no public Program. Both write those as JSON with --json, and Stoat lists them beside the canvas so you can work through them:

bash
wolverine scaffold --json | stoat warnings push
bash
bobcat import-event-model board.json --json | stoat warnings push

stoat warnings push prints each warning as text (the --json flag took the tool's own report off the terminal) and sends the report to Stoat. --source <name> keeps two applications' reports apart, and --url names another Stoat; curl -X PUT --data-binary @- http://localhost:5526/api/event-model-warnings does the same without the text.

On the canvas, ⚠ n warnings opens them grouped by slice, each with its code, the flag or line that settles it (a --starts-stream override, public partial class Program;), the file, and the evidence it was drawn from. A slice's drill-down shows its own, and a plan's slice card carries a ⚠ badge linking to it.

  • Fixing clears it. Each push replaces that tool's last report, so fix the warning and run the tool again: what it no longer reports is gone. Only what asks something of a person is listed; a file written, or what a re-import added, is not.
  • Dismiss one you checked and judged fine, with why. It stays listed as dismissed while the tool still reports it, and the dismissal goes when the warning does, so a warning that returns later is not hidden.

Stoat never runs either tool. The warnings are open, like the model itself.

Identity ​

A specification's identity is {Feature}/{Scenario}, the same string a Bobcat run reports for it. So the model's bound specs and the run evidence join with no mapping table to maintain. A slice node in a plan names its specs by the same identities.

See also ​

Licensed under the Business Source License 1.1.