Recover more of an application, describe it across projects, and report what cannot be recovered - #2413
Recover more of an application, describe it across projects, and report what cannot be recovered#2413woksin wants to merge 35 commits into
Conversation
…angling A reactor observing what a sibling bounded context publishes refers to an event a package declares. The document had nothing to say about it beyond SP0021, and then referred to a name it never introduced - which the official Screenplay compiler reports as an unknown event. Screenplay already has the construct for this: the compiler reads the last segment of an 'import' as the name of an event that is known. Every undeclared name is now looked for in the assemblies the compilation references, and the one that is found is imported rather than reported. SP0021 is left for what is really unresolvable - a name nothing at all declares an event under, where importing it would state that a package declares something it does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A compilation loaded from a project carries what every source generator emitted to the intermediate folder. Those files declare real members of the slice, so the folders they sit in were counted among the folders the slice is written across - which reported a slice sitting in one folder as spread over two, claimed the folder was shared with the next slice to be handed the same generator output, and widened the screen scan to a build folder. Generated source now answers neither where a slice is written nor where the project is. Where a build wrote every last file the whole set is kept, since answering with nothing writes every path against the machine that generated it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kebab case is idiomatic for the runtime name of a Chronicle constraint and a Screenplay identifier cannot carry a hyphen, so a transformation is forced. Deleting the separators was the least readable one available - every one of Ada's thirty one constraints came out as a run-together lower case word, and the word boundaries the source stated were thrown away with them. Each character an identifier cannot hold now ends a word instead, and the words are joined in Pascal case: 'unique-timesheet-start' reads as 'UniqueTimesheetStart'. This is every identifier position rather than only a constraint. A name carrying no separator is left exactly as it is, so nothing the application already writes as one word changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolving a type strips the collection it sits in and then strips the optionality of the element, while marking one as personal data stripped only the collection. A collection of an optional value therefore registered the mark under 'Nullable' - a name no concept is declared with - so the document said the value was not sensitive while the runtime encrypted it. Both now strip the same wrappers through one path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The statement pattern anchors on a line starting with 'import', which a commented out one still does, so a component carrying the leftovers of a change bound its screen to queries it never calls. Comments are now removed before statements are read, and the line breaks a block comment spanned are kept - joining the line after one to the line before would hide the real import that follows it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Only an interface parameter was read as a collaborator rather than as input, which is not the whole of what a host hands a query. A cancellation token, the page asked for and the order asked for are all filled in from the request and none of them is an interface, so each was stated as caller input - a parameter no caller sends, typed by a name the document never declares. They are named explicitly, because there is no property of a type that tells infrastructure from a value a caller really sends. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sequence jumped from SP0028 to SP0030 with nothing saying why. A code is what a consumer suppresses and groups on, so a number handed to something else later would silently change what an existing suppression means. The gap is now stated as deliberate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An accented letter can be written as one character or as a letter followed by a combining mark, and Unicode calls the two canonically equal. A combining mark is not a letter, so stripping first kept the accent of one spelling and quietly removed it from the other - two documents for one application, decided by how an editor happened to save the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
C# says a value may be absent in two ways - a nullable annotation on a reference type and a Nullable wrapper on a value type - and Screenplay says it in one, a trailing question mark. Unwrapping distinguished the two all along and nothing asserted that either survived as far as the printed text, where getting it wrong describes a shape the application does not have or names a type the document never declares. The specification is end to end and covers a command property, an event property, an enumeration, a collection of optional values and the parameter of a query. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The generator resolved them against the assembly it was reading and the syntax builder resolved them again against the domain of the model, so one generation ran two resolutions with two different fallbacks and let whichever came last decide. They agree today, which is the only reason nothing showed. The emitter is now the single place the emission half resolves, and the builder is handed options that are already resolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every Screenplay specification builds its compilation from source strings, which is what makes them hermetic and what puts a class of defect out of their reach by construction: a compilation built that way has no intermediate folder, no source generator output on disk and no referenced package declaring an event, so a generator that mis-attributes one of those produces a wrong document that every specification still passes. Two shipped that way. A console project reads a real project through MSBuild, generates from it and reads the document back with the compiler the language ships. It has to come back with nothing to say, warnings included - a warning there is the document referring to something it never introduces, which is this generator being wrong rather than the application. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Any compilation error at all made SP0024 an error saying "nothing recovered from it describes the application reliably". That claim is false whenever recovery succeeded, and it usually did: a host handing over a compilation assembled without the compile items a build generates leaves every reference to a generated type unresolved, and the errors then sit in code declaring no artifact while every command, event and reactor is read exactly as written. One real application produced 607 such errors alongside a complete and accurate document, which the host then discarded on the error alone. The severity now follows how many artifacts were recovered from a declaration no compilation error sits inside. None - nothing recovered at all, or every declaration something came out of being one the compiler could not make sense of - stays an error, because no part of that document can be trusted. Any at all is a warning stating how many errors there were, how many artifacts came through anyway and what usually causes it, so the result is successful and the document is kept. A count is used rather than a proportion because any threshold would make the same recovery pass for a large application and fail for a small one. Zero is the only number that means recovery was prevented rather than dented. Suppressing "declares nothing" is unchanged and, in the warning branch, can never apply - a warning is only reached when something was recovered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SP0024 suppressed the document check on the code alone, which was right while it was always an error: a model recovered from symbols the compiler never accepted describes an application that does not exist, so a document made from it being poor is the broken build rather than a second defect. Now that it can be a warning, that reasoning inverts. A warning says what was recovered stands, and a document built from a model that stands is exactly what the check exists for. Suppressing it there would hand back a document the language rejects with nothing wrong reported and a successful result, which is the one outcome the check was added to make impossible. The suppression therefore follows the severity rather than the code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The generator takes a Compilation and nothing else, so assembling one the way a real build would is the caller's job - and nothing said so. The part hosts get wrong is source generators: no workspace loading mode runs them, and an Arc application leans on generation heavily enough that a compilation loaded without them is missing every type they emit. Documents the contract, how to run the generators before handing the compilation over, and what the generator does when a host does not - which is now two different things depending on how much survived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A message reaches a rule through a lambda wherever an application declares its messages once and names them from every validator rather than repeating the text. The lambda computes nothing there - it names a constant the compiler already substituted - but only the direct form was read, so those applications ended up with a document carrying no message on any rule at all. The lambda that really does build its text while the request runs stays reported, and the report now names the expression it could not write down rather than only saying that there was one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A rule holding an end date to the start date it was sent with names that start date with a lambda, which is the same shape a chain names the property it is declared on. Only a constant was read, so every one of those rules looked like a comparison given nothing to compare against and was dropped - taking the layer a domain is really written in with it. A rule operand is a host expression, so the path is written as one. A rule held to a condition is stated as though it always holds, because a rule carries no condition of its own yet (Cratis/Screenplay#32). The report now carries the condition, so a reader can tell a rule that hardly ever applies from one that nearly always does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A record is referred to by name and never declared, so nothing inside one was ever named on its own and every concept reached only through a child record or a read model stayed out of the document. Where such a concept is marked as personal data, the document then understated what the application holds about people - which is the opposite of what declaring concepts is for. The walk is cycle safe and ordered by name, so a record referring to itself ends and the same source always yields the same document. The shape of the record itself still has nowhere to go (Cratis/Screenplay#29), so a property carrying one now says so rather than leaving the reader to notice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An import naming something a slice does not declare says nothing, which holds for a component, a command and a package and was taken to hold for everything. It does not hold for a query another slice declares - a screen aggregating several read models is what an Event Modeling screen routinely is, and those bindings were dropped without a word. Writing them down is what the language cannot do: a binding names a query by the bare name its own slice declares it under, and an application declares All once per read model (Cratis/Screenplay#28). Where the import was written does say which slice was meant, so the screen, the query and that slice are reported instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Where components are written against a view model, the component imports the view model and the view model imports the query - so reading the component alone found the name of a file and nothing about what the screen reads, and those screens ended up bound to nothing at all. The view model is written down rather than guessed at: the component names the module, it sits in the slice's own folder, and what it imports is read exactly the way the component's own imports are. One hop and no further, and only within that folder, because a chain across folders would be inferring an architecture rather than reading one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SP0013, SP0015, SP0016 and SP0020 each report something the application really declares that the document cannot yet hold. Which of them is a permanent gap and which is waiting on a specific change to the language was carried nowhere, so a reader had no way to tell a report that will always stand from one that disappears the moment an issue is resolved. Each reference is written into the explanation rather than appended to it, so it reads as the reason the gap exists, matching how SP0035 and SP0036 already cite theirs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TypeRegistry had grown past the size a file is meant to stay under by answering two questions at once: what name to write for a type, and what the application's concepts are. The second is a registry with rules of its own - what counts as a concept, which declaration wins when two share a name, how a personal-data mark and a validation rule arriving later still find the concept they belong to - and none of that has anything to do with unwrapping a collection or reporting what a name loses. ConceptRegistry now owns the concepts, and TypeRegistry keeps type resolution and the two things that fall out of writing a name. The enumeration and ConceptAs branches that were repeated between naming a type and walking what it carries collapse into one TryRegister, which is the same decision stated once. Behavior is unchanged: the document generated for a real application is byte-identical before and after, and all 973 specs pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An application written as several projects has one source root per project and a single directory to write all of their paths relative to, worked out from the roots together. Saying a project's root as a directory rather than only as the paths made from it is what lets that be asked at all. The comparing of directories moves out with it. The same three questions - normalize, share a prefix, is this a file system root - are asked of one project's directories and of the roots of all of them, so they belong beside neither. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A command handing its work to an aggregate root reads the behavior's body, and that body is the one an analysis reads that need not belong to the project the command does. A workspace loads a project reference as a compilation, so the sibling project's source is right there - and asking the wrong compilation for a semantic model is not a wrong answer but an ArgumentException. An aggregate root in a domain project called from the project above it therefore crashed generation rather than describing the layered application it is. A tree belonging to no project handed over is one that cannot be read at all, and that is now said rather than leaving the command stated as producing nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A layered application is not described by any one of its projects. The generator took a single compilation, so pointing it at one project of a solution described that project and referred to the events of the rest as things it never introduces - the difference between describing the project you pointed at and describing the application, which is the stated goal of the tool. Generate now also takes a list. Each compilation contributes what its own assembly declares, and what the projects hold together becomes one document: a namespace two of them declare into is one slice, a concept three of them refer to is declared once, a policy the host registers is found by the artifact naming it, an aggregate root in the project below is one a command hands its work to, and an event a sibling project declares is one the application has rather than one it imports. An event a package declares is still imported, because a package really is outside the application. Nothing of this reaches the document through the order the list arrives in. Nothing decides what order a host enumerates the projects of a solution in, so they are put into assembly name order before anything is read. Paths stay relative and now say which project a file belongs to: they are written relative to the directory the projects are all under. Projects that share no directory keep their own roots, because a path relative to the root of a file system is the machine's own layout wearing a relative path's clothes, and that is reported (SP0038). What two projects declare under one name in one slice cannot both be described, so the first is kept and the second is reported (SP0037). The single-compilation overload is unchanged and now runs through the same path as a list of one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two projects built from source strings, one referencing the other the way a workspace loads a project reference, is the whole shape of a layered application - and every one of these fails against a generator that reads one compilation, most of them by describing half of what the projects hold. Each is a claim that can fail on its own: the slice two projects write, the name only one of them can be described under, the concept declared once however many refer to it, the policy the host registers for an artifact elsewhere, the aggregate root a project above hands its work to, the event a sibling declares against the event a package does, the directory the paths are relative to when the projects share one and when they do not, and the same projects handed over the other way round printing the same bytes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The end to end check exists to get behind the seam every specification is built from strings on, and a compilation built from strings cannot have a sibling project loaded as a compilation reference - which is exactly where an application of several projects lives. Handing the harness a solution rather than a project is what puts a real layered application through the same gate: every project of it that is not a specification project, generated into one document, read back by the Screenplay compiler. Handing it a single project does exactly what it did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Screenplay has a specification construct - given, when, then - and nothing in the model to fill it from, so the generator has never written one. This introduces the shape a scenario is recovered into and the names the source of one is recognized by, ahead of the reading and the writing that follow. A given, a when and a then are the same shape - a name and a list of values - so one model covers all three and the step it sits in says which it is. A rejection carries the reason the source names for it, and an empty one where the source names none: the scenario is named after the words the source uses for it, so what it was about is already said there and inventing a sentence to repeat it would describe an application nobody wrote. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Cratis application carries its integration specifications beside the slice they are about, in a shape rigid enough to read: what had happened is stated against a scenario or appended to the event log, one command is issued, and each assertion says one thing about what followed. That is the specification construct exactly, so the scenarios were there all along and the document simply never carried them. Both shapes Arc documents are read - the pipeline driven in process and a running host driven over HTTP - matched on the names of the testing types alone, so neither testing package has to be referenced. A specification about a collaborator is passed over rather than reported, because what is read is decided by what a specification touches rather than by where it sits. Values are recovered the way a produces mapping is, one source shorter: a scenario refers to nothing outside itself, so a constant is the only source of a value there is. A value that is code is left out and the rest of the scenario stands. A step that is code, or one that only happens under a condition, takes the scenario with it - an example missing the state it started from is a different example from the one the source states, and stating it would be the one failure mode a reader cannot catch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A document stating what a slice does and carrying none of the scenarios proving it loses half the story, so every scenario recovered is written into the slice it belongs to, ordered by name like everything else in a slice. Nothing a step states is left out over its name. Every other block decides what a line is from its first word, so a property named after a directive collides with it; the values of a step are written one level deeper than the step itself and the step takes every line beneath it as a value, whatever its first word says. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both shapes Arc documents are specified against - the pipeline driven in process and a running host driven over HTTP - along with a rejection named and unnamed, a value that is code, and each of the three ways a scenario is left out whole. The testing surface is declared as source rather than referenced, which asks the recognition the only question worth asking of it: whether the names alone are enough. The end to end specification carries a scenario from source through the printed text and back through the Screenplay compiler, because a scenario is the one construct where the value of a property and the name of a step sit one indentation apart - and a document nobody reads back is a document that compiles by luck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two lines of work built from the same base are stacked here: reading an application written as several projects, and recovering the scenarios a slice is specified by. Both branches had independently claimed SP0037 and SP0038 for different meanings. A code is never reused and never renumbered, because consumers suppress and group on it, so one number cannot carry two meanings. The multiproject codes are already published on an open pull request whose reviewer notes cite them, so those keep the numbers - SP0037 stays RepeatedDeclarationAcrossProjects and SP0038 stays ProjectsWithoutASharedRoot - and the specification codes move on: UnreadableSpecification to SP0039 and UnreadableSpecificationValue to SP0040. ApplicationModelAnalyzer had to place specification recovery in the flow that now reads several compilations. A scenario reads the symbols of the project it is written in, so it is recovered per project exactly as an artifact is. Which slice it belongs to is a different question: it names no slice, it is placed by the nearest namespace above it that declares one, and nothing says that slice is declared by the project the scenario sits in - a bounded context handling its commands beside the contracts project publishing its events is specified from the project holding the handlers. So each project is asked for its scenarios only once the slices of every project have been joined, and placement is decided against the slices of the whole application. The scenarios landing in one slice are then joined by the rule the union already applies to everything else within a slice: one of a name, the first kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An application that speaks more than one language declares every message in a resource and names it from the validator. What the validator names is a property a build writes from that resource, whose body is a lookup resolved against the culture of the caller - so the compiler holds no value for it, and asking for one brought it back indistinguishable from a message assembled while the request runs. Such an application stated no message on any rule at all, which is the whole of what it says about what it will not accept. The key is stated rather than the text, because the text is one of several the application holds and settling on one would describe a language rather than an application. It is qualified by the resource declaring it, since a key is unique to that resource and to nothing wider - a real application names the same rule the same way in two areas of itself, and bare those two are one key that can carry only one text. What identifies the property is the shape of its getter rather than where it was declared or what it was called, because the file name and the naming are conventions of one generator while the lookup is what makes it a resource at all. A message put together from a resource rather than referring to one is still reported, and so is a key the grammar has no way of writing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The analyzer specification states what is recovered and what is not: a key qualified by the resource declaring it, two resources naming one key kept apart, a message put together from a resource left off its rule, a key the grammar cannot write left off its rule, and a message a constant holds still recovered by the route that already existed. The generator specification reads the document back. A key is the one thing on a message line written unquoted, so where a message that cannot be written merely reads wrong, a reference that cannot be written is a line the compiler rejects - and a rejected line takes the whole document with it rather than the one rule it sits on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewer context — deliberately kept out of the description. This is agent-authored code that no human has read yet. Green CI is not sign-off. Do not merge without explicit review. Top of the stack: this → #2411 → #2410 → #2408 → The grammar was checked before anything was built on it
Three decisionsQualified, The key comes from the No new diagnostic codes. SP0041–SP0043 were allocated and went unused: every remaining drop is still "a validator declares a rule that could not be expressed declaratively", which is what The issue's estimate was wrong, and the correction is the interesting part#2409 predicted The premise held: 509 of 510
For the ~285 whose rule lives in code, the rule is dropped and the message has nothing to attach to. Those still report 133 recovered against ~123 countable by regex — which under-counts multi-line chains — means every message reachable by this change was recovered. Closing the rest requires Verification
Caveat: that application is under active concurrent edit — a trial run before the measurement pair showed a different baseline entirely (9,164 lines vs 9,209). The reported pair ran back-to-back in one shell invocation, and every non- |
|
Consolidated reviewer context for the whole change. This PR supersedes #2408, #2410 and #2411; all 34 commits are here. This is agent-authored code that no human has read yet. Green CI is not sign-off. Do not merge without explicit review. Where to look firstThe The end-to-end CI gate ( Specification volume. The document roughly doubles: 5,348 → 9,263 lines on the reference application, of which ~3,900 are the 507 recovered specifications. That is ~7 lines per scenario rather than the explosion feared, because most values are runtime identities and get dropped. No option to disable was added, on the reasoning that if a larger application does struggle, the right lever is a per-slice filter driven by a real complaint rather than a global switch added on speculation. Whether 507 scenarios belong in a document meant to be read is a judgment left to you. Four of the issues rested on wrong evidenceAll checked against source; corrections posted to the issues themselves.
And one estimate of mine was wrong: #2409 predicted Defects found that no issue described
Decisions taken against the issues, deliberately
Diagnostics on the reference application
Verification
Caveat on every measurement above: that application is under active concurrent edit — absolute line counts drift by tens of lines between runs. Every before/after pair was taken back-to-back in one shell invocation, which is what isolates a delta to the change rather than to the moving source. |
|
One addition to the reviewer notes above, on the weakest content in the generated document. 347 of the 507 recovered specifications end in This is forced by the grammar, not a shortcut: Filed upstream as Cratis/Screenplay#46, proposing a bare Until then the options are to keep them as they are, or to skip scenarios whose only assertion is an unnamed failure — a one-line change that would drop those 347 from the document. Kept as they are, on the grounds that the scenario name carries the meaning and "it fails" is true; happy to switch if you would rather the document did not carry them. |
The page had fallen behind the generator, and one row of it had become false: model-bound children and nested objects are read back, so a reader was being told to reach for the fluent form to get something the attributes already give. What is left of that row is narrower and worth saying precisely - a removal is read from the type of the child rather than from the property holding the collection, and nothing carries a scope being emptied again at all. Checking the rest of that table against the source turned up two more things it was wrong about. The rationale for read model tags was transport, which is not why they are absent - a read model has no declaration to hang one on, and event tags are recovered and printed. And the table claimed every row is reported, while paging, sorting, route templates and read model tags pass without a word; each row that does report now names its code so the exception is visible rather than asserted. Three capabilities went undocumented. Scenarios recovered from a slice's own specifications are the largest thing the document gained and belong beside Screens, as the other construct read from outside the slice file. Several projects read as one application is a statement about what the generator is handed, so it sits with the rest of the host contract. Resource keys are one instance of a discipline the page never stated - that a value is only ever what the source states - and stating it once keeps the scenarios section from having to repeat it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| foreach (var assembly in compilations | ||
| .SelectMany(_ => _.SourceModule.ReferencedAssemblySymbols) | ||
| .Where(_ => !analyzed.Contains(_.Identity.Name)) | ||
| .DistinctBy(_ => _.Identity.GetDisplayName(), StringComparer.Ordinal) | ||
| .OrderBy(_ => _.Identity.GetDisplayName(), StringComparer.Ordinal)) | ||
| { | ||
| if (names.Any(assembly.TypeNames.Contains)) | ||
| { | ||
| Collect(assembly.GlobalNamespace, names, found); | ||
| } | ||
| } |
| foreach (var compilation in compilations) | ||
| { | ||
| if (compilation.ContainsSyntaxTree(tree)) | ||
| { | ||
| return compilation.GetSemanticModel(tree); | ||
| } | ||
| } |
| foreach (var assignment in creation.Initializer?.Expressions.OfType<AssignmentExpressionSyntax>() ?? []) | ||
| { | ||
| if (assignment.Left is IdentifierNameSyntax identifier) | ||
| { | ||
| yield return (identifier.Identifier.ValueText, assignment.Right); | ||
| } | ||
| } |
| foreach (var project in solution.Projects | ||
| .Where(_ => !IsSpecifications(_.Name)) | ||
| .OrderBy(_ => _.Name, StringComparer.Ordinal)) | ||
| { | ||
| if (await project.GetCompilationAsync() is { } compilation) | ||
| { | ||
| compilations.Add(compilation); | ||
| } | ||
| } |
| foreach (var property in type.DeclaredProperties()) | ||
| { | ||
| var carried = UnderlyingTypes.Of(property.Type); | ||
| var name = carried.ToDisplayString(); | ||
|
|
||
| found.TryAdd(name, carried); | ||
|
|
||
| if (IsRecord(carried) && walked.Add(name)) | ||
| { | ||
| Walk(carried, found, walked); | ||
| } | ||
| } |
|
Superseded by #2418, which now targets Nothing is lost: every commit here is an ancestor of that branch, including the documentation correction, and the branch is kept. Reviewer context for all of it is consolidated into a single comment there. Closing so there is one Arc PR to review rather than two that have to be merged in order. |
Added
specificationblocks, so a document states what the application is specified to do rather than only what it declares; a scenario with a step that cannot be read statically is reported rather than guessed at (Screenplay generation: recover 'specification' blocks from Chronicle integration specs #2404)Changed
Fixed
unique-timesheet-startis written asUniqueTimesheetStart(Screenplay generation: separator-carrying constraint names are sanitized into unreadable identifiers #2398)