Import a real .play, export it again, and the result is not a legal Screenplay document.
Measured on Screenplay's own 913-line invoicing.play fixture (17 slices, 19 events, 7 commands):
regenerated document compiles = FALSE (2 errors)
line 252: Projection 'InvoiceLineReportReadModel' must contain at least one directive
line 283: Projection 'SystemActivityReadModel' must contain at least one directive
This is not a fidelity nicety. The renderer and the Stage container both consume that text, so code generation, "play this model", and "run specifications" all sit downstream of it.
Cause
An event a slice consumes but no State Change slice owns is imported as an external event and stored on the slice's ExternalEvents collection. The exporter never reads that collection — ExternalEvents has zero references anywhere under Source/Core/Screenplay/, while the import path populates it in two places and Slice documents it as a first-class member.
So when a State View slice's projection sources are all external, the exported projection is emitted with automap and a key but no from directive, and the compiler rejects it.
Traced concretely: InvoiceLineItemAdded is produced only inside a capture block. Captures are not carried on import, so the event has no owning slice, becomes external, and disappears. It appears 3 times in the source document and 0 times in the regenerated one.
A second, independent trigger for the same failure
The consumed-event walk handles from, join, children, nested, remove and clear, but not all / every. A projection written with all therefore harvests zero events and produces the same empty projection — silently, with no warning.
Suggested direction
Include external events when building the exported slice, and add all/every to the consumed-event walk. Both are small and localized.
The check that would have caught this, and should exist
There is no spec anywhere that round-trips a document and asserts the result is valid. The nearest one asserts produces against the syntax tree rather than the imported model, so it passes while the imported model carries none.
The loop runs headlessly in about 110 ms with no server, database or browser — parse, map to the canvas model, generate — so a spec that imports a .play, exports it, and asserts the regenerated text compiles costs one file in the existing harness and catches this entire class of defect. That single assertion is worth more than any of the individual carries below.
Related fidelity gaps found in the same run
All of these are things the Stage contract can already express — they are gaps on this side, not blocked on Cratis/Stage#23:
produces is dropped on import (commands are imported with an empty produced-event list) although the contract carries produced events with a full condition tree, and export writes them.
- Constraints are dropped on import although export writes them and the contract carries them on the event definition. (PR #1044 carried specifications and rules; constraints were not included.)
- Read-model schema is always empty on import, although the contract has the field.
- Projection bodies — key and
from semantics — are dropped; only event names are harvested.
- Validation rules with a non-literal threshold are silently dropped. A rule comparing against another property rather than a constant vanishes. This accounted for 2 of the 6 rules lost in the measured run.
NotEqual is dropped and, unlike the other unmapped kinds, is not documented as such.
Two defects in the warnings themselves
- The specification warning is false. The importer reports "N specifications declared here were not added" while the specifications are added — 4 declared, 4 carried, and still warned about. PR #1044 added the carry and left the warning behind.
- A spec asserts that false warning, so the incorrect message is now pinned in place.
Silent drops that should at minimum warn
Application-level imports and authentication; slice-level readmodel and reducer; command-level reads, handler and require; every query construct; non-literal rule thresholds; NotEqual.
Export has no warning channel at all
Considerable care has gone into import warnings. Everything lost on the way out — constraints, concurrency, projection bodies, queries, NotNull, EmailAddress, Phone, Url — vanishes with no diagnostic of any kind.
Since code generation renders from the canvas through this same converter, every export gap is also a code-generation gap: a dropped authorize means generated code with no authorization on it.
Import a real
.play, export it again, and the result is not a legal Screenplay document.Measured on Screenplay's own 913-line
invoicing.playfixture (17 slices, 19 events, 7 commands):This is not a fidelity nicety. The renderer and the Stage container both consume that text, so code generation, "play this model", and "run specifications" all sit downstream of it.
Cause
An event a slice consumes but no State Change slice owns is imported as an external event and stored on the slice's
ExternalEventscollection. The exporter never reads that collection —ExternalEventshas zero references anywhere underSource/Core/Screenplay/, while the import path populates it in two places andSlicedocuments it as a first-class member.So when a State View slice's projection sources are all external, the exported projection is emitted with
automapand a key but nofromdirective, and the compiler rejects it.Traced concretely:
InvoiceLineItemAddedis produced only inside acaptureblock. Captures are not carried on import, so the event has no owning slice, becomes external, and disappears. It appears 3 times in the source document and 0 times in the regenerated one.A second, independent trigger for the same failure
The consumed-event walk handles
from,join,children,nested,removeandclear, but notall/every. A projection written withalltherefore harvests zero events and produces the same empty projection — silently, with no warning.Suggested direction
Include external events when building the exported slice, and add
all/everyto the consumed-event walk. Both are small and localized.The check that would have caught this, and should exist
There is no spec anywhere that round-trips a document and asserts the result is valid. The nearest one asserts
producesagainst the syntax tree rather than the imported model, so it passes while the imported model carries none.The loop runs headlessly in about 110 ms with no server, database or browser — parse, map to the canvas model, generate — so a spec that imports a
.play, exports it, and asserts the regenerated text compiles costs one file in the existing harness and catches this entire class of defect. That single assertion is worth more than any of the individual carries below.Related fidelity gaps found in the same run
All of these are things the Stage contract can already express — they are gaps on this side, not blocked on Cratis/Stage#23:
producesis dropped on import (commands are imported with an empty produced-event list) although the contract carries produced events with a full condition tree, and export writes them.fromsemantics — are dropped; only event names are harvested.NotEqualis dropped and, unlike the other unmapped kinds, is not documented as such.Two defects in the warnings themselves
Silent drops that should at minimum warn
Application-level
importsandauthentication; slice-levelreadmodelandreducer; command-levelreads,handlerandrequire; every query construct; non-literal rule thresholds;NotEqual.Export has no warning channel at all
Considerable care has gone into import warnings. Everything lost on the way out — constraints, concurrency, projection bodies, queries,
NotNull,EmailAddress,Phone,Url— vanishes with no diagnostic of any kind.Since code generation renders from the canvas through this same converter, every export gap is also a code-generation gap: a dropped
authorizemeans generated code with no authorization on it.