Hotspots
A hotspot is the model admitting it does not know something yet. On a whiteboard it is the pink sticky note with a question mark on it; on a JasperFx Event Model it is a HotspotDescriptor, rendered in the wireframe lane in the canonical hotspot magenta (#E91E63).
Hotspots come from two places, and the difference between them matters more than it looks.
A pending specification is a hotspot
This is the primary mechanism, and the one you should reach for first.
When a slice has a specification bound to it that cannot pass yet — no bound steps, or steps that fail — the binding source stamps a hotspot on that slice automatically. Nobody writes it, and nobody has to remember to delete it: the day the spec passes, the hotspot is gone.
In IncidentService, the sample has no ResolveIncident slice yet. Rather than write a note about it:
public class ResolutionSlices : EventModelDefinition
{
public override string Name => "Helpdesk";
public override void Configure(EventModelBuilder builder)
{
builder.Slice("ResolveIncident")
// The question is sharp enough to name a scenario, so name it. The spec is
// pending, and a pending spec is a hotspot — one that retires itself the day
// the spec passes, which a prose note never does.
.TriggeredBy("Agent clicks Resolve")
.LinksToSpecification("Resolve Incident/An agent resolves a pending incident");
}
}The scenario named there does not exist yet either. That is the point — the model shows an unbuilt slice with an open question on it, and the question closes itself when someone writes the code that makes the scenario pass.
Prose hotspots
Sometimes there is nothing to write a specification against yet. The question is real, but it is not yet sharp enough to name a scenario — you do not know the rule, so you cannot write the test.
Hotspot("…") puts that question on the canvas:
public class IncidentServiceHotspots : EventModelDefinition
{
public override string Name => "Helpdesk";
public override void Configure(EventModelBuilder builder)
{
// Not about any one slice — nobody has decided this yet
builder.Hotspot("Do we own the SLA clock, or does the CRM?");
builder.Slice("CloseIncident")
// Both of these rules are sitting commented out in CloseIncidentEndpoint.
// Written down here, they show up on the canvas instead of in a code comment
// nobody outside the team will ever read.
.Hotspot("Can an incident be closed before the customer acknowledges the resolution?")
.Hotspot("What happens to an outstanding response to the customer when we close?");
builder.Slice("ArchiveIncident")
.Hotspot("Three days is a guess. Ask legal what retention actually requires.");
}
}Those first two are not invented. Open CloseIncidentEndpoint in the Wolverine sample and you will find both rules sitting in a commented-out block:
/* More logic for later
if (current.Status is not IncidentStatus.ResolutionAcknowledgedByCustomer)
throw new InvalidOperationException("Only incident with acknowledged resolution can be closed");
if (current.HasOutstandingResponseToCustomer)
throw new InvalidOperationException("Cannot close incident that has outstanding responses to customer");
*/That is a hotspot in its natural habitat: a real, undecided rule, parked in a comment where only somebody already reading that file will ever see it. Declared in the overlay, the same two questions appear on the canvas in front of whoever is looking at the model — including the people who would actually know the answer.
Slice-level or model-level
Hotspot exists on both builders and they mean different things:
| Call | Attaches to | Use when |
|---|---|---|
builder.Slice("X").Hotspot("…") | That slice | The question is about one thing the system does |
builder.Hotspot("…") | The whole model | The question spans the flow, or is not about any one slice |
A slice hotspot renders as an element in that slice's wireframe lane. A model hotspot lands on EventModelDescriptor.Hotspots and is rendered by the viewer wherever it shows model-level notes.
A slice that is nothing but a hotspot still renders one element — which is exactly what you want for a slice you have thought about but not built.
Prefer the pending spec
TIP
Prose is the escape valve, not the default.
The two forms have very different lifecycles:
- A pending-specification hotspot is evidence. It appears because a real spec is failing or unbound, and it disappears the moment that stops being true. It cannot lie to you.
- A prose hotspot is a note. Nothing retires it but you. Six months on, a stale prose hotspot describing a question the team settled long ago is worse than no hotspot at all, because it teaches people to ignore the magenta ones.
So the moment a question becomes sharp enough to name a scenario, promote it: write the pending spec, LinksToSpecification it, and delete the prose line. "Can an incident be closed before the customer acknowledges the resolution?" becomes "Close Incident/Rejects an incident with no acknowledged resolution", and from then on the model tracks it for you.
Reading them back
Both kinds arrive on the assembled descriptor, tagged with their origin:
// Ask every registered source — Wolverine's chains, the Bobcat generator, your
// overlays — for its view, then fold them into one descriptor per model name
var models = await EventModelDiscovery.AssembleAsync(services);
var helpdesk = models.Single(x => x.Name == "Helpdesk");
foreach (var slice in helpdesk.Slices)
{
Console.WriteLine($"{slice.Domain}/{slice.Name}: {slice.Pattern}");
foreach (var hotspot in slice.Hotspots)
{
Console.WriteLine($" ⚠ {hotspot.Origin}: {hotspot.Text}");
}
}
// Questions that belong to the model rather than to one slice
foreach (var hotspot in helpdesk.Hotspots)
{
Console.WriteLine($"⚠ {hotspot.Text}");
}HotspotOrigin tells them apart, and SpecificationIdentity is populated only for the pending-spec form. Hotspots from different sources are unioned and deduplicated on origin plus text — so prose and a pending spec that happen to share a string stay two distinct hotspots, because they mean two different things.
