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.
| Producer | What it knows |
|---|---|
| A running Wolverine application | Its 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 store | One View slice per registered projection, named after the document type, carrying the events it really applies. |
| A Bobcat spec assembly | The 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:
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-modelLive, as you edit: Bobcat's watch command redraws bobcat ui's canvas on each rebuild:
bobcat watch-event-model src/MyAppFrom 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:
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
SourceDisagreementwhere 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:
wolverine scaffold --json | stoat warnings pushbobcat import-event-model board.json --json | stoat warnings pushstoat 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
- Runs and evidence — where the evidence comes from
slice_coveragein the MCP tool reference