Skip to content

Model comparison API

Use ModelComparison.Compare from Cratis.Screenplay.Comparison in the Cratis.Screenplay package to inspect structural changes without running MCP. The result describes authored structure, not executable equivalence or a specification execution verdict.

This example compares two in-memory sources and finds an added event property:

using Cratis.Screenplay.Comparison;
const string source = "module Projects\n feature Registration\n slice StateChange Register\n event Registered\n name String\n";
var before = ComparedModel.FromSources("Projects",
new Dictionary<string, string> { ["application.play"] = source });
var after = ComparedModel.FromSources("Projects",
new Dictionary<string, string> { ["application.play"] = source + " extra String\n" });
var difference = ModelComparison.Compare(before, after);
var addition = difference.Events.Single(change => change.Change == EventContractChangeKind.PropertyAdded);
Console.WriteLine($"{difference.Matching}: {addition.Property}, breaking={addition.ContractBreaking}");

The output is Address: extra, breaking=True. Address matching intentionally leaves identity coverage incomplete; that does not hide the known property change.

FactoryInputPersisted identities
ComparedModel.WithIdentities(workspace)An immutable ScreenplayWorkspace with an authoritative catalogYes
ComparedModel.WithoutIdentities(workspace)A workspace whose catalog must not establish continuityNo
ComparedModel.FromSources(applicationName, sources)Portable .play paths mapped to complete source stringsNo
ComparedModel.FromWorkspaceExport(json)A complete canonical UTF-8 workspace export as ReadOnlySpan<byte>Yes

Workspace retains the source and compilation diagnostics. HasPersistedIdentities records your explicit choice, not a guess based on catalog origin. FromSources creates documents in memory; it does not read paths from disk. Invalid-but-editable source remains available for comparison. Invalid document paths, workspace admission or exports raise the existing workspace contract exceptions. Bound export input size before passing it to FromWorkspaceExport; see Workspace transport.

  • Identity: both inputs declare persisted identities and share the same application identity. Renames and owner/document moves preserve semantic continuity. Two identity-bearing inputs from different applications throw IncompatibleModelIdentities rather than falling back.
  • Address: every other combination, including different applications when either side has no persisted identities. Keys contain the normalized kind and all typed semantic address parts except the application. Owner kind and event generation remain part of the key. Display addresses remain dotted. Renames and owner moves appear as removal plus addition; document movement is not compared. ComparedDeclaration.SemanticId is null and Identities is empty.

When both models are executable, Address matching uses catalog addresses and compares property-level members. If either model is not executable, both use exact authoring declaration keys and report DeclarationLevelOnly.

To compare an authored model with a model generated from code, use WithIdentities for the authored workspace and WithoutIdentities or FromSources for the generated side. An automatically allocated catalog does not make generated identities authoritative.

ModelDifference exposes Matching, Complete, HasSemanticChange, BeforeExecutable, AfterExecutable and declaration group counts for each side. HasSemanticChange is true when a known structural change exists, false only when the comparison is complete with no semantic change, and null when coverage is incomplete without a known semantic change. A document-only move is not a semantic change.

The typed lists are:

PropertyRecordMeaning
DeclarationsDeclarationChangeAdded, removed, renamed or moved declarations; moves distinguish owner from document
MembersMemberChangeChanged authored members or opaque content; hashes are null when a member is absent on that side
EventsEventContractChangeProperty addition/removal/type change and generation addition/removal
SpecificationsSpecificationChangeAdded/removed specifications and effective expected-outcome or opaque changes
DependantsDirectDependantDirect indexed references from either snapshot, with role and resolution confidence
IdentitiesIdentityChangeAssigned, retired or migrated semantic/event-contract identities in Identity mode

Each change identifies its declaration, with nullable before/after addresses. Event property changes keep ContractBreaking visible even when GenerationCovered is true. Event type strings are serialized syntax types, not runtime type names. Specification example expansion is included in the compared outcomes. Member hashes use SHA-256 of normalized authored members; descriptions and documentation do not create semantic changes.

Inspect every ComparisonSection in Sections, not only the change lists. Gaps contain typed ComparisonGapKind values and stable explanatory statements. The kinds cover incomplete source, failed example resolution, noncomparable assigned/indexed declarations, address-only unassigned declarations/events/specifications, uncertain dependant references, omitted identity comparison and declaration-level fallback.

NotCompared carries stable limit statements. Comparison does not analyze behavior inside opaque code, open external implementation files, prove transitive/runtime impact or execute specifications. Direct dependants retain resolved, unresolved, ambiguous, incomplete and wrong-kind resolution states. Properties use owner references; containers aggregate external references to contained declarations while excluding references inside the container.

The MCP semantic-diff views use the same structural comparison implementation. Their existing revision checks, JSON layout and paging remain transport concerns; the library result is typed and unpaged.