Split scaffolding out of the renderer, and render Given/When/Then into specifications - #35
Conversation
The Cratis meta-package contributes GlobalUsings.cs through Cratis.props, so every consumer of the renderer silently gained 28 global usings and the ambiguities they bring - 160 compilation errors in the CLI alone. Naming Arc, Arc.Chronicle, Arc.Swagger, Chronicle and MongoDB.Driver directly restores the same compilation for the specs without any build assets, and drops 16 packages from what a consumer restores.
The renderer needs Arc, Chronicle and the Mongo driver to compile its rendered output inside a spec, and needs none of them to render. Gating the references on Debug - the same condition Directory.Build.props already uses for the test packages, and Directory.Build.targets for the spec files themselves - keeps the specs and takes the runtime out of the package. It also stops the type-discovery generator, which was reaching the compilation transitively rather than through the two direct references ExcludeAssets was applied to. The Release assembly goes from 698 Cratis type references and nine Arc and Chronicle assemblies to Screenplay and Stage.Contracts alone.
The renderer needed the template engine for one thing: scaffolding a project around the files it writes. That engine brings the whole NuGet client, which Microsoft.Build.Locator refuses to run beside, so any consumer had to choose between hosting MSBuild and rendering at all - the CLI needs both. IProjectScaffolder stays as the seam and CreateDefault now renders into the target directory without scaffolding, which is the common case: a document regenerated into a project that already exists. Scaffolding moves to Cratis.Stage.Rendering.Cratis.Scaffolding for callers that want it. The renderer package is now Cratis.Stage.Contracts and Cratis.Screenplay and nothing else.
|
Reviewer context. Labelled
Verified against a real consumer, not just the specs. Packed to a local feed, pointed Cratis/cli#62's branch at it and deleted all three csproj workarounds it needed against 2.3.0 — the 33 C# files, exit 0, and 44 lines on stderr naming what the rendered application does not carry. That is the render arrow running as a command for the first time.
A mistake worth naming, since it was on this branch and I told you it was green: the first commit removed Not verified: no rendered application was booted against a live Chronicle. The scaffolding package's own two specs moved with it and still pass, but nothing here proves the template engine behaves identically from its new home beyond those. If you'd rather scaffolding left the repo entirely and became |
|
@einari Two things need you here — the second more than the first. Why this is a
|
A specification exercising the slice's own command renders as a CommandScenario<T> spec - one file each, in the when_ folder layout the conventions use, wrapped in #if DEBUG. Appended events assert against the event source the command's identifier names; a rejection asserts both that the command failed and that it failed as validation, because ShouldNotBeSuccessful alone cannot tell a rejection from an exception. A specification that cannot be rendered faithfully is not rendered at all and says why. The interesting case is 'given': CommandScenario does support seeding prior events, so the gap is in the document rather than the target - a given event names no event source, and it is frequently not the command's own. A specification asserting that a duplicate invoice number is rejected seeds an InvoiceRegistered for a different invoice than the one it registers, and seeding it against the command's id would assert something else entirely.
…-out-of-the-renderer # Conflicts: # Directory.Packages.props # Source/Rendering.Cratis/Rendering.Cratis.csproj
The spec that compiles rendered output parsed without DEBUG defined, so every rendered specification - which sits entirely inside #if DEBUG - compiled to nothing and the assertion passed on an empty compilation unit. It proved nothing about the thing it was written for. Defining the symbol makes it real, and it immediately found what it was blind to: a stated date renders as a parse against the invariant culture, and nothing emitted the using it needs. The renderer now decides that from what it rendered.
|
#36 is folded into this one and closed — same review, one release. Relabelled
|
| Break | Studio |
|---|---|
Cratis / Cratis.Arc.MongoDB dropped as dependencies |
references both directly in Source/Core/Core.csproj — nothing to lose |
| five scaffolding types moved | uses none of them; its only other use of the package is …Rendering.Cratis.Naming |
Cratis.Arc.Generated.GeneratedMarker removed |
generator debris, unreferenceable |
CreateDefault() stops scaffolding |
calls it — see below |
The one real consequence, stated plainly rather than buried: Studio stops scaffolding a Cratis project template into a repository that does not already contain one. SliceCodeGenerator renders into the user's own git repository and calls Directory.CreateDirectory(target) itself first, so scaffolding was a no-op wherever a project existed — and where one did not, dropping an app template into someone's repository is behavior worth losing, not preserving.
So the blast radius of the whole "major" is one behavior nobody wants, on one internal consumer, in this org. minor is the honest label; major was me applying the letter of the rule to a package with no external audience.
Reviewed it, and found a defect of my own
The assertion I had called the one that matters was vacuous. RenderedOutput.Errors parsed without DEBUG defined, and a rendered specification sits entirely inside #if DEBUG — so the preprocessor dropped the whole file and the compilation succeeded on an empty compilation unit. should_render_specs_that_compile was passing on nothing.
Defining the symbol made it real, and it immediately caught what it had been blind to: a stated date renders as DateOnly.Parse(…, CultureInfo.InvariantCulture) and nothing emitted using System.Globalization; — so any specification stating a date rendered a spec that did not compile. SpecificationValues.NeedsGlobalization existed and was never called; the renderer now decides it from what it rendered. The fixture gained a Date property so the case is covered, and reverting the fix now fails both that spec and the compile assertion.
That is two vacuity traps in one file in one session — the ambient-usings gap on #33, and this. Worth remembering that a compile-based assertion is only as honest as its parse options.
Verification
- 423 specs across four projects, Debug and Release both zero warnings,
dotnet build Stage.slnx. - Packed and consumed for real: cli#62's branch with all three csproj workarounds deleted builds clean, its 446 specs pass, and
cratis render invoicing.playwrites 33 files with 44 reported drops, exit 0. - On
invoicing.play, 1 of 4 specifications renders and compiles; the other 3 are declined with the reason on stderr. CompatibilitySuppressions.xmlrecords every removed type — worth reading as the diff of what left the package.
Not verified: no rendered specification has been executed, and no rendered application booted against a live Chronicle.
Still yours to call: whether to cut this now or hold it to batch with #23's cardinality major, and whether scaffolding should stay in this repo or become cratis new. Neither blocks the review.
SpecificationAssertions.Of renders EventSourceId.Unspecified when the specification states no value for the command's identifier, and nothing had ever compiled that branch - the fixture always stated one. It does now, which is the only way to know the rendering is real rather than plausible.
|
Two more from reviewing my own work, both the same shape as the vacuity bug and both now covered by compiling rather than reading.
That is the third case in this PR where the difference between plausible and real was one compilation: the ambient usings on #33, Nothing else changed. 425 specs across four projects, Debug and Release both zero warnings, CI green. |
A rendered spec sits in a child namespace of its slice, so it finds the command it exercises and nothing else. It now resolves imports the way every other renderer does - an event a sibling slice declares, or a concept placed above the slice, compiled against nothing before. Alongside that, five things the review found: - a fanout stating one event type twice declared the same fact twice - a specification expecting both a rejection and appended events rendered only the rejection and dropped the events without saying so; it is declined instead, because a rejected command appends nothing - the event source id was rendered from the document's raw text while the command constructed its identity through Guid.Parse, so any casing difference asserted against a stream the command never appended to - a stated 'then' value the event does not declare, or that is not a literal, was dropped from the predicate silently - a stated 1.5 for an Int rendered as 2, and a value outside int range rendered a literal the generated application could not compile
|
Ran a review over the specification rendering. Eight findings, all real, all mine, all fixed — pushed. Two of them produced generated code that does not compile, which is the class of defect this whole PR exists to stop.
Covered by compiling, not by reading. A new spec renders a specification whose That is now five defects in this PR found by taking a branch the fixture did not take. The pattern is consistent enough to be worth stating as a rule for this renderer: every branch of a code generator needs its own compile. 431 specs across four projects, Debug and Release both zero warnings. |
|
@einari Short version — my earlier comments here were too long. What this does. Two things. The renderer stops shipping the Arc/Chronicle runtime and the template engine, which is your Debug-condition suggestion (it also removed a marker type the Cratis generator had been baking in). And Screenplay specifications now render as I dropped the label from Ignore what I said earlier about holding this to batch with #23. That mattered while it was a major. A minor does not need to wait for one. One question, and it is the only thing left:
I built the first. The seam is identical either way, so switching is deleting a project. Say which and I will adjust — otherwise this is ready to merge. |
|
@woksin Scaffolding should happen in Stage, since we should also use this component directly from Studio - so a full render should be owned by Stage. |
|
That is what is built — The renderer itself no longer carries it, which is the point of the split — |
Added
Cratis.Stage.Rendering.Cratis.Scaffolding— scaffolds the Cratis project a rendered application is placed into, from the Cratis templates. Take it alongside the renderer and passTemplateEngineProjectScaffoldertoCratisRendererto keep today's behavior. (The renderer package hands every consumer the runtime it generates code for #34)specificationexercising its slice's command renders as a Cratis spec — aCommandScenario<T>in thewhen_folder layout, wrapped in#if DEBUG. Appended events are asserted against the event source the command's identifier names, narrowed to the values the specification states; a rejection asserts both that the command failed and that it failed as validation. (Render Given/When/Then into Cratis Specifications #14)Changed
CratisRenderer.CreateDefault()renders into the target directory without scaffolding a project around it, which is what regenerating from a document into a project that already exists needs. (The renderer package hands every consumer the runtime it generates code for #34)given, states read model state, exercises another slice's command, or asserts nothing. (Render Given/When/Then into Cratis Specifications #14)Removed
Cratis.Stage.Rendering.Cratisno longer depends onCratis,Cratis.Arc.MongoDB,Cratis.Templatesor the threeMicrosoft.TemplateEngine.*packages. The package isCratis.Stage.ContractsandCratis.Screenplayand nothing else. (The renderer package hands every consumer the runtime it generates code for #34, Question: should Cratis.Stage.Rendering.Cratis depend on Cratis publicly? #18)TemplateEngineProjectScaffolder,SampleSlice,ScaffoldingFailed,TemplateNotFoundandTemplatePackageNotFoundmove to the new scaffolding package, keeping their namespace. (The renderer package hands every consumer the runtime it generates code for #34)Fixed
User,AppendedEvent,CommandContextandObserverRunningStateambiguous — 160 compilation errors in the CLI alone. (The renderer package hands every consumer the runtime it generates code for #34)NuGet.Frameworksthat stopsMicrosoft.Build.Locatorfrom running. A host can now render and use MSBuild in the same process. (The renderer package hands every consumer the runtime it generates code for #34, Question: should Cratis.Stage.Rendering.Cratis depend on Cratis publicly? #18)