Cratis.Stage.Contracts — already a dependency, and shipping this since well before the Screenplay 2.x wave — contains a complete Screenplay-to-Stage converter under Cratis.Stage.Contracts.Screenplay:
ScreenplayEventModelVisitor SliceConverter CommandConverter ReadModelConverter
ProjectionConverter ProducesConverter EventConverter SpecificationConverter
ValidationRuleConverter SchemaSynthesizer ScreenplayExpression DeterministicId
ScreenplayEventModelVisitor : IApplicationSyntaxVisitor<EventModel> — the exact signature of Studio's Source/Core/Screenplay/EventModelSyntaxVisitor.cs:14.
Studio never references it. It maintains roughly 530 lines doing the same job across nine files: EventModelSyntaxVisitor (26), ModuleSyntaxVisitor (27), FeatureSyntaxVisitor (26), SliceSyntaxVisitor (114), SpecificationSyntaxVisitor (71), SyntaxValidationExtensions (73), SyntaxSchemaExtensions (85), SyntaxEventReferenceExtensions (40), ScreenplayEventOwners (66).
Stage's version is ahead of Studio's
CommandConverter fills Produces and resolves the command's Identifier. EventConverter carries constraints. Studio passes Produces: [] at SliceSyntaxVisitor.cs:56 and raises a warning saying it dropped both — data the contract can already hold, dropped by code written before the contract grew the ability to hold it.
The cost is drift, and it has already happened once
Screenplay 2.x made SliceSyntax.Projections plural, so both implementations had to decide how to name a read model when a slice declares several. Stage's ReadModelConverter resolved it as projection?.ReadModel ?? query return type ?? projection?.Name ?? slice.Name. Studio's resolved it differently — meaning the same document could come back from a round trip named differently from how it went out.
Cratis/Studio#1108 aligned Studio to Stage by hand and left a comment at SliceSyntaxVisitor.ReadModelName saying "change both together or neither". A comment is the weakest available mechanism for keeping two implementations honest, and it only works for as long as someone reads it.
Suggested direction
Delete Studio's chain and call ScreenplayEventModelVisitor. This is not a pure refactor — there are two real behavioural differences to decide first:
- Identity. Stage uses
DeterministicId.From(path); Studio uses Guid.NewGuid(). Deterministic identity is arguably better, since re-importing the same document would yield the same graph rather than a new one — but it changes what an import produces.
- Cross-slice event ownership. Studio's
ScreenplayEventOwners assigns each State Change slice's events an identity up front, so a consuming slice references the producing slice's event rather than a fresh one. Stage has no equivalent and relies on path-derived determinism instead. Whether that preserves Studio's incoming arrows needs checking before anything is deleted.
Both belong in the same design conversation as Cratis/Stage#23. Worth settling before the contract widens rather than after — widening it twice, in two implementations, is how a round trip acquires its third disagreement.
Cratis.Stage.Contracts— already a dependency, and shipping this since well before the Screenplay 2.x wave — contains a complete Screenplay-to-Stage converter underCratis.Stage.Contracts.Screenplay:ScreenplayEventModelVisitor : IApplicationSyntaxVisitor<EventModel>— the exact signature of Studio'sSource/Core/Screenplay/EventModelSyntaxVisitor.cs:14.Studio never references it. It maintains roughly 530 lines doing the same job across nine files:
EventModelSyntaxVisitor(26),ModuleSyntaxVisitor(27),FeatureSyntaxVisitor(26),SliceSyntaxVisitor(114),SpecificationSyntaxVisitor(71),SyntaxValidationExtensions(73),SyntaxSchemaExtensions(85),SyntaxEventReferenceExtensions(40),ScreenplayEventOwners(66).Stage's version is ahead of Studio's
CommandConverterfillsProducesand resolves the command'sIdentifier.EventConvertercarries constraints. Studio passesProduces: []atSliceSyntaxVisitor.cs:56and raises a warning saying it dropped both — data the contract can already hold, dropped by code written before the contract grew the ability to hold it.The cost is drift, and it has already happened once
Screenplay 2.x made
SliceSyntax.Projectionsplural, so both implementations had to decide how to name a read model when a slice declares several. Stage'sReadModelConverterresolved it asprojection?.ReadModel ?? query return type ?? projection?.Name ?? slice.Name. Studio's resolved it differently — meaning the same document could come back from a round trip named differently from how it went out.Cratis/Studio#1108 aligned Studio to Stage by hand and left a comment at
SliceSyntaxVisitor.ReadModelNamesaying "change both together or neither". A comment is the weakest available mechanism for keeping two implementations honest, and it only works for as long as someone reads it.Suggested direction
Delete Studio's chain and call
ScreenplayEventModelVisitor. This is not a pure refactor — there are two real behavioural differences to decide first:DeterministicId.From(path); Studio usesGuid.NewGuid(). Deterministic identity is arguably better, since re-importing the same document would yield the same graph rather than a new one — but it changes what an import produces.ScreenplayEventOwnersassigns each State Change slice's events an identity up front, so a consuming slice references the producing slice's event rather than a fresh one. Stage has no equivalent and relies on path-derived determinism instead. Whether that preserves Studio's incoming arrows needs checking before anything is deleted.Both belong in the same design conversation as Cratis/Stage#23. Worth settling before the contract widens rather than after — widening it twice, in two implementations, is how a round trip acquires its third disagreement.