Skip to content

Observation comparison API

Compare a model with observed registrations and appends

Section titled “Compare a model with observed registrations and appends”

Use ObservationComparison.Compare from Cratis.Screenplay.Observations in Cratis.Screenplay to find disagreements between a compiled model and a bounded observation snapshot. A host supplies registrations, counts, producer identities and subscriptions; Screenplay performs no IO and has no Chronicle dependency.

This comparison answers whether observed event relationships agree with the model. It does not execute specifications, inspect payloads or prove behavioral correctness. Use model comparison for two authored models and specifications for executable examples.

Pass a successful SemanticCompilation from a file or composed folder model, an ObservationSnapshot, and optionally an ObservationIdentityMap. ObservationSnapshot.Create accepts these required arguments:

ArgumentEvidence
eventStore, namespaceHost identifiers for the observed scope
coverageOne ObservedSequenceCoverage: sequence, first/last examined number, captured tail and gaps
registeredEventTypesWhole registry of ObservedEventType(TypeId, Generation) pairs, not a ranged sample
appendsObservedAppend(Type, Count) for examined events
edgesObservedEdge(Producer, Type, Events), attributed to the innermost named producer
unattributedAppendsUnattributedAppend(Type, Count) without an attributable producer
consumersRegistered ObservedConsumer subscriptions, including other sequences
capturedAtHost capture time; no implicit clock read

Counts mean events, never command or reaction invocations. Producers have kind Command or Reaction and a host identifier. Consumers have an identifier, kind (Reaction, Projection, Reducer, Other), owner (Application, Platform, Unknown), sequence, subscribed event type/generation pairs and optional NextSequenceNumber / LastHandledSequenceNumber progress.

Creation sorts evidence ordinally and copies it into immutable collections. Duplicate keys, negative counts, invalid or overlapping ranges, attribution exceeding append counts and overflowing totals throw InvalidObservationSnapshot. Any append count not accounted for by producer edges remains unattributed even when the host omits it from unattributedAppends. The snapshot has no payload, event-source-identity or causation-property fields. There is no snapshot JSON API.

Sequence numbers are inclusive. StartsAtFirstEvent, an examined end equal to CapturedTail, and no Unexamined gaps establish full scan coverage. Null first, last and tail describe an empty sequence, which counts as fully examined. OccurredFrom and OccurredTo are informational; the host converts any time filter to sequence coverage and must disclose skipped ranges.

ObservationReport.Complete requires full scan coverage and no Unknown findings. HasDrift is true for any known drift, false only for a complete comparison without drift, and null otherwise. Do not turn null into false. A known drift remains true even when other evidence is incomplete.

Absence findings become Unknown PartialCoverage for a partial scan; the event registry is the exception because the host supplies it whole. Presence findings remain drift. “No matching appends within coverage” does not mean “never ran.”

Every ObservationFinding has a kind, outcome and stable human Statement. Where available it carries a reason, construct identity, typed address, owning slice, event type and generation, producer, consumer, sequence and count.

KindOutcome
EventNotRegisteredDrift: the current declared generation is missing from the whole registry
EventNotAppendedInformational for full coverage; Unknown for partial coverage
UndeclaredEventAppendedDrift, including in a shared namespace
UndeclaredEventRegisteredInformational when no examined append exists
AppendedWithoutRegistrationInformational for historical appends
UndeclaredGenerationDrift for a generation absent from current and prior model revisions
UndeclaredEdgeObservedDrift for a mapped producer outside its declared productions; Unknown for an opaque reaction
DeclaredEdgeNotObservedDrift only under the conditions below; otherwise Unknown
NoMatchingAppendsWithinCoverageInformational: no attributed event establishes that the producer ran
ConsumerNotRegistered / ConsumerOnOtherSequenceDrift for a missing application reaction subscription; Unknown for partial coverage
ConsumerMismatchDrift for a mapped subscription mismatch; Unknown for deferred or incomplete consumer evidence; platform consumers are Excluded
ForeignEventExcluded: the contract has an origin outside this application
NotEstablishedUnknown: a coverage, attribution, mapping or opaque-behavior limitation without a comparable edge

A missing production is drift only with full coverage, an unambiguous producer mapping, another event attributed to that producer, and no unattributed appends of the expected type. The production must be unconditional; a command must have no described handler, and a reaction needs an event trigger without a predicate or opaque requirement. A mapped reaction whose consumer has not reached the tail reports Unknown ConsumerBehind, including when progress is absent.

Other typed reasons are ConditionalProduction, DescribedHandler, OpaqueReaction, NonEventTrigger, AmbiguousIdentity, UnmappedProducer, UnmappedConstruct, UnattributedEvents and ConsumerKindNotCompared. PlatformConsumer explains an exclusion. Projection and reducer consumer checks are deferred; a matching identifier is not enough to establish their semantics.

ObservationIdentityMap.FromModel(compilation.Model) maps an event by StoredName ?? Name, never by portable ContractId. Its current revision is the observed generation; declared prior revisions are recognized too. A pinned event rename therefore keeps its stored identity.

Commands map by name only when that name is unique across the model. Duplicate names report AmbiguousIdentity. Reactions have no default producer or consumer mapping: qualified host names and reactor identifiers belong to the render target. Supply those explicitly with immutable overrides:

var map = ObservationIdentityMap.FromModel(compilation.Model)
.With(reaction.Id,
producer: new(ObservedProducerKind.Reaction, "follow-host"),
consumerIdentifier: "follow-host");
var report = ObservationComparison.Compare(compilation, snapshot, map);

This excerpt assumes a compiled model, its reaction declaration and a snapshot. With also accepts eventTypeId or a command producer override. It returns a new map; omitted arguments leave existing mappings unchanged. Do not guess host identities from display addresses. Unmapped observed producers report Unknown. Type-level reaction subscription checks still run without an identity mapping.

This in-memory example describes a registered event in an empty sequence. It needs only the Screenplay package and ordinary .NET collection imports:

using Cratis.Screenplay.Comparison;
using Cratis.Screenplay.Observations;
const string source = "module Projects\n feature Registration\n slice StateChange Register\n event Registered\n name String\n";
var workspace = ComparedModel.FromSources("Projects",
new Dictionary<string, string> { ["application.play"] = source }).Workspace;
var snapshot = ObservationSnapshot.Create(
"Projects", "tenant", new("log", true, null, null, null, []),
[new("Registered", 1)], [], [], [], [], DateTimeOffset.UnixEpoch);
var report = ObservationComparison.Compare(workspace.Compilation.Value!, snapshot);

Check workspace.Compilation.Success before comparing external source. Here report.HasDrift is false and report.Complete is true; the only finding is Informational EventNotAppended. Removing the registration instead reports EventNotRegistered drift. Add partial coverage, missing attribution and a lagging consumer to adapter tests rather than testing only a successful scan.

Slices presents owned event generation counts and registration, attributed producer edges, and consumer subscriptions with progress. Deferred consumers remain Unknown. Append totals live in Coverage and are computed once from the snapshot, not by summing slices; a second slice producing the same event shows only its own attributed count, not another copy of the appends.

ObservationReportJson.Write(report) writes deterministic JSON with camelCase properties, string enums, fixed field order and null fields omitted. A null HasDrift is omitted, never serialized as false. Findings carry exactly the same statements as the typed report. Model-position order comes first, then finding kind and ordinal observed identities; undeclared evidence comes last.

NotEstablished states the remaining limits: no payload agreement, behavioral correctness, specification execution, external-effect verification, comparison of other sequences or projection/reducer consumer semantics. JSON is a result format, not a transport for event contents or a deployment approval.