Recover more of an application and report what cannot be recovered - #2408
Recover more of an application and report what cannot be recovered#2408woksin wants to merge 21 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>
|
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. Stacked on #2407, which is stacked on #2406; it must merge last of the three. Three of the issues' premises turned out to be wrongThis is the most important part of this PR. The acceptance criteria could not be met as written, because the evidence they rest on does not hold against the current source. #2397 — the message recovery does not fire on the real application, and the 643 figure is not what it looks like. All 510 #2403 — 6 → 2, not 6 → 0. Only four of the six #2400 — only 7 of the 15 named concepts are recoverable, and the three PII ones are not among them. Personal data did improve where it was reachable: #2396 — the diagnostic works; the optional ViewModel hop is inert here. Diagnostics, before and after, same commit lineage
Verification
Caveat
|
|
Consolidated reviewer context for the whole change — deliberately kept out of the description. This PR now supersedes #2406 and #2407; all 19 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 I would look firstThe suppression interaction between #2393 and the document check. The The end-to-end CI gate ( Worth knowing: because the workflow triggers on Three of the issues' premises did not holdChecked against the source, not taken from the issues:
Diagnostics on the reference application, across the whole change
Verification
Caveats
|
|
Repository-wide verification, which should have run before this was pushed rather than after.
|
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>
|
Three follow-ups pushed ( Diagnostics now name the upstream language issue they wait on. Five candidates were considered and rejected, which is the more useful half:
Also verified as having no corresponding diagnostic at all: #31 (a CLI concern), #34 (no side-effect concept exists — a returning reactor is silently recorded as translating), #35, #36, #37 (
Comparison against the reference document. Fresh output 5,526 lines against the 5,348-line reference: 52 removed, 230 added, 61 hunks, and every line classified — nothing unplaced.
One name looked wrong and was chased down rather than waved past: Determinism and refactor safety in one check: the document generated before the split, after it, and on a second run afterwards all hash Caveat on that comparison: the reference application's working tree has 87 uncommitted modifications and was being edited concurrently during the run, so the drift classification is against a live tree rather than a commit. It does not weaken the byte-identity guard above, and Verification: Release 0 errors / 0 warnings; 973 specs, count unchanged as a refactor requires; end-to-end gate exit 0, reads back clean; worktree clean. |
| foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols | ||
| .OrderBy(_ => _.Identity.GetDisplayName(), StringComparer.Ordinal)) | ||
| { | ||
| if (names.Any(assembly.TypeNames.Contains)) | ||
| { | ||
| Collect(assembly.GlobalNamespace, names, found); | ||
| } | ||
| } |
| 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 #2413, which now targets Nothing is lost — every commit here is an ancestor of that branch, and the branch is kept. Reviewer context for all of it is consolidated into a single comment there. Closing so there is one PR to review and one set of release notes rather than four that have to be merged in order. |
Added
Changed
Fixed
unique-timesheet-startis written asUniqueTimesheetStart(Screenplay generation: separator-carrying constraint names are sanitized into unreadable identifiers #2398)