Stop the proxy generator and command pipeline erasing distinctions the client needs - #2444
Stop the proxy generator and command pipeline erasing distinctions the client needs#2444woksin wants to merge 17 commits into
Conversation
A calendar date and a time of day are not instants, and the generated
proxy declared both as a JavaScript Date. That invents an instant the
server never sent: new Date("2026-05-12") is UTC midnight, which every
browser-local getter west of UTC reads back as the 11th, and
new Date("14:30:45") is not a date at all and yields Invalid Date.
Both types already cross the wire as their ISO-8601 string, so carrying
that string through is the faithful mapping - nothing is invented,
nothing is lost, and the timezone decision moves to the call site that
needs one instead of being made silently by the deserializer.
DateTime and DateTimeOffset are left on Date; they do denote instants.
Two specs cover it: one over the emitted TypeScript, since the defect
lives in a generated artifact nobody diffs, and one that deserializes
through the generated proxy in V8 and pins the day west of UTC.
The proxy generator's type map was recorded nowhere, so the only way to learn that a DateOnly arrives as a string - or that a Guid arrives as a Guid from @cratis/fundamentals - was to read the generator source or the generated output. Spells out the temporal cases in particular, since they are the ones where the wrong mapping is silently plausible rather than obviously broken.
The extractor DynamicInvoked FluentValidation's _errorMessageFactory during generation and wrote the returned string into the generated client validator as a literal. A factory is deferred precisely because its value is not known yet. Calling it on the build machine answers for a different process, at a different time, under ambient state that machine cannot stand in for - the culture, the clock, a tenant, a feature flag. A delegate is opaque, so there is no way to tell a factory returning a constant from one that is not, which leaves not calling it as the only guess-free move. The frozen literal was not merely stale, it was authoritative: a failing client rule short-circuits the request, so the build machine's answer is what the user sees and the server never gets asked. The rule still mirrors; only the message stays behind, and the client rule falls back to its own default. Fixed in GetCustomErrorMessage so all four generation paths - model-bound and controller-based, commands and queries - are covered at once. A fix at any call site would have covered one path and looked complete. The eager _errorMessage branch is untouched: a literal genuinely is context-free. Two existing specs pinned the old behavior and are inverted here.
The page claimed custom messages are carried over, without qualification. That is true of a literal and not of a factory, and the difference is the one a reader authoring localized messages needs to know about.
A ValidationResult carried nothing but prose to say what kind of rejection it was, so a client could only tell a retryable concurrency violation from a business-rule rejection by matching an English sentence - which breaks the moment the wording changes, and cannot distinguish the framework's sentence from an authored rule worded the same way. Adds ValidationResultReason and a Reason member defaulting to Rule, so anything the framework composes on the application's behalf has to say so. Applied to the concurrency path on all three sites - append, append many, and the aggregate root - and to the constraint path, which was equally mislabeled as an authored rule. Deliberately not State: that slot already carries FluentValidation's WithState value, so it belongs to whoever wrote the rule. Deliberately not an enum: rejections are composed in Arc, in Chronicle and in application code, and a closed set makes every new kind a breaking change for whoever switches over it. Additive on the wire and at the source level - Reason is init-only with a default, and the TypeScript parameter is optional, so a result from a server that predates this reads as Rule. The aggregate-root path had no concurrency coverage at all, which is how it came to carry the same flattening as the event-log path unnoticed.
When a validator throws, the framework substitutes CouldNotValidateMessage for that validator's entire result set. The substitute is shaped exactly like a genuine rejection - Error severity, free text, no members, no state - so a consumer could only recognize it by matching the English sentence, which is a literal out of Arc's source with no compatibility promise and no way to tell it from an authored rule worded the same. The result now carries ValidationResultReason.ValidatorFailed, so a consumer can render its own copy for the case instead. The fail-closed behavior is unchanged and deliberately so: returning a validation failure rather than propagating, logging the detail server-side, and saying nothing in the response about what went wrong are all correct. The message text is unchanged too - it is a developer diagnostic, and making it translatable would invite displaying it. ValidatorInvoker had no direct spec coverage; this adds it, including the contrast case proving an authored rejection still reads as a rule.
Use NullLogger rather than substituting the logger - logging is not specified - and split the fixture types into one file each.
CommandFormFields validated on every value change, and passed
autoServerValidate straight through - so with server validation on, each
keystroke was a POST to {route}/validate with no timer anywhere between
them. autoServerValidateThrottle governed a different effect, one gated
on all fields already being valid, so it damped a second request rather
than the per-character one.
Measured on a ten-character burst: 11 requests at the default throttle
(ten per-character plus one trailing), and 10 with the throttle raised
past the burst - the prop moved nothing. Now 1 and 0.
The per-change validation stays immediate but goes client-side only. It
is still the immediate driver of isValid, at no latency and no network
cost, and the throttled effect becomes the single server path. That
effect now also feeds silentValidationResult, so a rule only the server
can express still has the final say on isValid - just once the typing
stops rather than once per character.
This makes the code match what the documentation already promised:
"With throttle: 1 server call (after user stops typing for 500ms)".
Also tightens the existing throttle spec, whose toBeLessThanOrEqual(5)
bound is what let a round trip per keystroke hide underneath it.
XElement.Value returns the concatenated text content of an element and
its descendants, so every documentation element whose payload lives in
an attribute was silently deleted on its way into the generated proxy -
a self-closing see cref, seealso, paramref, typeparamref or see langword
has no text child at all. The prose then fused around the hole, leaving
only the two spaces that had surrounded it:
/// A <see cref="Widget"/> and a <c>gadget</c>.
-> A and a gadget.
Worse the more idiomatically the source is documented, since those are
the elements the .NET conventions ask for, and invisible either way
because the artifact is generated and nobody diffs it.
Replaces the three separate .Value reads with one element-aware walk:
a cref renders as {@link Name}, a langword and a paramref as inline
code, <c> keeps its backticks, an explicit label wins over any of them,
and anything else keeps its prose. Whitespace collapses last, so nothing
rendered away leaves a seam.
Routing all three entry points through it also fixes a property summary
spilling its source newlines and indentation straight into the JSDoc,
which only the type path had ever normalized. Visible in the TestApps
proxies regenerated here.
The pages stated that a metadata attribute declared without concurrency: true tags the appended events "without affecting concurrency control". It does affect it. The value enters the command context regardless of the flag and reaches the append regardless of the flag, and the fallback strategy then resolves its expected tail from that metadata - so the flag governs whether the command declares a scope, while the tag governs what the check is narrowed by. Corrects events.md:141 and concurrency.md:74, completes the clause concurrency.md:101 stopped one short of, and sharpens the EventForEventSourceId note at events.md:217 to say what is inherited - not one shared scope, but the declaration, applied once per target. Also corrects the rationale on the ConcurrencyScopeBuilder spec, which stated the same false inference in source. Its assertions were always right; only the reason given for them was wrong, which is what makes this a misconception rather than a stale page. Adds the spec nothing anywhere had: that a routing-only command reaches the append with no scope AND with its tag intact. Asserting the null scope alone is exactly what let the misconception survive in a repository whose specs are otherwise thorough. No behavior change. The implementation is coherent and deliberate; making a routing-only tag concurrency-inert would silently widen every existing consumer's guard.
The ownership model's remarks explained which provider wins and stopped there. What follows from winning is that the provider also decides which serialization boundary the injected read model crosses - and the three shipped providers cross entirely different ones. So a MongoDB convention pack, class-map customization, element rename or custom serializer reaches a command-side read model only where the MongoDB resolver claimed it. Chronicle and Entity Framework Core both declare, and declaring beats fallback regardless of registration order, so in an application whose read models are owned by either, it reaches none of them - while the same customization stays plainly at work on the query side, which is what makes the absence hard to notice. States it in all three places a reader might look: the ownership remarks where the precedence rule already is, and each resolver's own remarks where someone investigating a non-applying convention would go. Entity Framework Core's materialization path was recorded as unknown in the proposal this came from. It is DbContext.FindAsync through EF Core's own entity model, established here so the paragraph could name all three rather than two.
Arc coerces a query argument to the declared parameter type before validation runs, and resolves a ConceptAs<T>'s validator by the value's runtime type. So a parameter declared as the concept is validated and a raw string or Guid is not - and converting inside the body produces the concept the query wanted while skipping the rules meant to guard it, with nothing in the build, the lint step or the spec suite to notice. The shape is usually inherited rather than chosen: a query that began as a string keyed lookup keeps its parameter through every later refactor while the conversion migrates into the body as a cast, so each refactor makes it less conspicuous rather than more. Three decisions the proposal left open: - Public and internal, not public only. Query discovery registers internal methods, so an internal query is just as routable and just as unvalidated. - Warning, not error. The shape is legal and retyping is not behavior-neutral - an omitted argument deserializes to null rather than NotSet, and a strict validator becomes reachable - so a rule that breaks a warnings-as-errors build would be suppressed, not adopted. Both caveats are in the descriptor's description. - ConceptAs only, no EventSourceId arm. EventSourceId is a ConceptAs<string> and is matched anyway, which keeps a Chronicle namespace out of a package that does not know Chronicle exists. The rule also sees through the .Value on a nullable parameter, since there is no conversion from Guid? to a concept and the author has to write (RequestId)id.Value - matching only the bare reference would leave the rule silent on the shape a nullable parameter forces. Registered as an operation-block analyzer, which has no precedent in either analyzer project here - the one existing operation-based analyzer uses RegisterOperationAction for invocations. Worth a reviewer's attention for that reason.
The nine assertions in CommandResultShouldExtensions had no extension point of any kind, so a repository wanting a house rule about what a good assertion looks like could only install it by namespace shadowing - invisible at the call site, silently disarmed by moving the file, and payable once per root namespace. ICommandResultAssertionPolicy is discovered the way ICommandScenarioExtender already is, through the type discovery system, so a repository installs its policy in one visible place and it runs everywhere without a call site being edited. Three decisions the proposal left open: - Discovered, not a settable static. Per-scenario is unreachable - the assertions extend CommandResult, not the scenario - and a mutable static would be shared state across a parallel xUnit run. Discovery is process-wide but immutable, and it is the pattern this package already uses. - Strengthen only. A policy is consulted after the built-in check has passed and never when it failed, so a passing assertion can become a failure but a failing one can never become a pass. The package's own guarantees are not negotiable by a consumer. - All nine, not just ShouldHaveValidationErrors. A seam covering one assertion and not its siblings is a strange public surface, and the assertion name comes from CallerMemberName so adding one needs no second edit and no parallel vocabulary - those names are already matched as strings by Screenplay. Behavior is byte-identical with no policy installed, which the compatibility pin asserts. Cratis.Arc.Testing had no spec project and its assertions had no direct coverage at all; this adds both.
The reason discriminator introduced for the concurrency and thrown- validator paths only covered the producers those two reports named. This carries it across the rest, so a client branching on it is not silently reading Rule for a rejection no rule produced. Two categories the earlier pass did not need: - DependencyUnavailable, for a read model the pipeline could not resolve from the command's key. Raised before any rule runs, so nothing about the command's own rules was decided - which is exactly what makes it the most dangerous one to report as an ordinary rejection: neuter every rule a slice has and its specs still pass, because this rejects in their place. - MalformedRequest, for a body that could not be read or a value that could not be bound. No rule was reached either. An authored rule stays Rule and is untouched - a DataAnnotation attribute and an aggregate's own Failed() are the author's rules, not the framework's. Adds ShouldHaveValidationErrorBecauseOf, the named counterpart to ShouldHaveValidationErrors, and lists it in Screenplay's NamedRejections where matching is by name string. Asserting only that a command was rejected cannot distinguish a rule from a race, a constraint, a throw or an unresolvable dependency; this is what lets a spec pin which. FluentValidation's ErrorCode is deliberately still not carried. Doing so is one line, but it would make that library's defaults - NotEmptyValidator, NotNullValidator - a wire-visible value consumers start depending on, which is a contract Arc would then owe. WithState remains the seam for a rule that wants to name itself.
A validator whose constructor asks for a read model a spec forgot to seed is never constructed, so not one of its rules runs - and the command is rejected anyway, by the pipeline. ShouldHaveValidationErrors passed on that, so the spec was green and would have stayed green with the rule it was named after deleted. Mutation-proven in a consumer: neutering one rule left all 29 of a slice's specs passing, with that single pipeline rejection the only error present. Now that a dependency failure says so through its reason, the testing tier can tell the two apart without matching prose - which is what the proposal recorded as the reason a testing-package-only fix had been withdrawn. Only fires when the dependency failure is the whole story. A result carrying a real rule rejection alongside it has a rule that ran, so the assertion has something to be about and stands. A spec that genuinely means to assert the case says so with ShouldHaveValidationErrorBecauseOf, which the failure message names. This is a deliberate behavior change to the testing package: a spec that was passing only on a dependency failure will now fail. That is the point - it was asserting nothing.
A CommandForm child was a field only if component.displayName equaled 'CommandFormField', so any transform that set displayName unbound every field with no error and no warning - the form renders, the input accepts typing, and nothing reaches the command. react-docgen-typescript does exactly that by default, and Storybook selects it through a documented option. Adds an isCommandFormField/isCommandFormColumn static marker, checked first, with the displayName comparison kept as the fallback. The fallback is not on a deprecation path and is the point: it is what lets a version of this package interoperate with a version of a consuming package that knows only the string, in both directions. Removing it would silently unbind every field across that boundary - the exact failure the marker exists to prevent, caused by the fix. Covers the column half too. Fixing only fields would leave columns breakable by the same transform. Shipped as a static property rather than a Symbol. A Symbol resists a name-based transform more thoroughly, but it is a public-surface choice for Cratis to make, and it needs Symbol.for to survive a duplicate install - both reasons to put the question rather than answer it here. Owed, and not mine to do: the three read sites in @cratis/components. Until they move, a field surviving a displayName rewrite is recognized by CommandForm and not by CommandDialog. Nothing regresses in the meantime, because the fallback keeps both directions working. Recount for the record: Arc has 3 field read sites, 1 column read, 4 field stamps and 1 column stamp. The corrected "six field read sites" in the proposal is the total across both repositories.
The marker is a cross-package contract that nothing enforces. @cratis/components reads the field marker written here and writes the column marker read here, and neither imports the other's helper — a consuming package declares this one as a version range, so a named import would be a hard module-link error against any version in that range predating the marker. The contract is therefore carried entirely by two property names being spelled identically in two repositories. That is not hypothetical. The two packages were briefly implemented with different shapes, one a static property and the other a Symbol, and every spec in both repositories passed: both kept the displayName fallback, so nothing threw, and the marker simply stopped crossing the boundary. A field whose displayName a build transform had rewritten bound in a bare CommandForm and silently unbound inside a CommandDialog — the exact failure the marker was added to prevent, surviving the fix and invisible to every gate. Adds the spec that catches it, in both directions: a component marked the way a consuming package marks one, with its displayName then overwritten, is recognized here; and a component marked here exposes the marker under the property name that package reads, with the legacy displayName still set for one that predates the marker. Renaming either marker now reds this spec. Also corrects "recognising" to American English per .ai/rules/general.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRCRgRvMz9N8P8NGwR34MM
|
Reviewer context — not release notes. How each change was arrived atEvery item was reproduced in this repository before being fixed, and mutation-proven: red before, green after, red again with the fix deleted. Three are worth a reviewer's attention because the evidence is unusual.
That fix makes the code match documentation it had diverged from. The routing-only concurrency claim is now pinned end to end. Design decisions a maintainer may want to revisit
Verification
Coverage gaps this closed on the way
|
| foreach (var operation in block.Descendants()) | ||
| { | ||
| if (operation is not IConversionOperation conversion) continue; | ||
| if (!ReferencesParameter(conversion.Operand, parameter)) continue; | ||
| if (conversion.Type is { } target && IsOrDerivesFromConceptAs(target)) | ||
| { | ||
| return target; | ||
| } | ||
| } |
| catch | ||
| { | ||
| _typeScriptIsValid = false; | ||
| } |
There was a problem hiding this comment.
Pull request overview
This PR tightens the Arc proxy generator and command pipeline contracts so clients can reliably distinguish rejection causes, preserve cross-reference documentation, and avoid incorrect client-side behavior (notably around temporal types and command-form field recognition).
Changes:
- Introduces machine-readable validation “reason” on server and client (
ValidationResultReason) and adds assertion seams (ICommandResultAssertionPolicy,ShouldHaveValidationErrorBecauseOf) to prevent false-green specs. - Adjusts proxy generation behavior (XML doc rendering, deferred FluentValidation messages, DateOnly/TimeOnly TS mapping) and adds coverage/specs for the previously missing edge cases.
- Hardens React command-form field identification against
displayNamerewrites and fixes redundant/over-eager server validation requests by ensuring throttling governs the actual typing path.
Reviewed changes
Copilot reviewed 105 out of 105 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| TestApps/Shared/Arc/Queries/QuerySubscriptionAggregate.ts | Regenerates docs so inline code formatting is preserved in generated proxies. |
| TestApps/Shared/Arc/Queries/QuerySubscriber.ts | Regenerates docs with inline-code formatting in generated proxies. |
| TestApps/AspNetCore/Arc/Queries/QuerySubscriptionAggregate.ts | Regenerates docs so inline code formatting is preserved in generated proxies. |
| TestApps/AspNetCore/Arc/Queries/QuerySubscriber.ts | Regenerates docs with inline-code formatting in generated proxies. |
| Source/JavaScript/Arc/validation/ValidationResultReason.ts | Adds TS validation-reason vocabulary for machine-readable discrimination. |
| Source/JavaScript/Arc/validation/ValidationResult.ts | Carries validation reason client-side, defaulting missing reason to rule. |
| Source/JavaScript/Arc/validation/index.ts | Exports the new ValidationResultReason entrypoint. |
| Source/JavaScript/Arc/queries/QueryResult.ts | Carries optional reason from server query validation results. |
| Source/JavaScript/Arc/commands/for_CommandResult/when_constructing_from_a_server_result_with_reasons.ts | Adds JS spec coverage for reason round-tripping and defaulting. |
| Source/JavaScript/Arc/commands/CommandResult.ts | Carries optional reason from server command validation results. |
| Source/JavaScript/Arc.React/commands/CommandForm/index.ts | Re-exports marker helpers for command-form field/column recognition. |
| Source/JavaScript/Arc.React/commands/CommandForm/for_commandFormMarkers/when_exchanging_marked_components_with_a_consuming_package.ts | Adds cross-package compatibility spec for marker-based field/column recognition. |
| Source/JavaScript/Arc.React/commands/CommandForm/for_commandFormMarkers/when_a_transform_has_rewritten_displayName.ts | Adds spec coverage for surviving displayName rewrites (e.g., docgen tooling). |
| Source/JavaScript/Arc.React/commands/CommandForm/for_CommandForm/when_typing_into_a_field_with_autoServerValidate.ts | Adds spec to verify throttling governs per-keystroke server validation behavior. |
| Source/JavaScript/Arc.React/commands/CommandForm/for_CommandForm/when_autoServerValidate_with_throttle.ts | Tightens existing test to assert exact request counts (prevents false greens). |
| Source/JavaScript/Arc.React/commands/CommandForm/fields/RadioGroupField.tsx | Switches field recognition to marker helper (vs displayName only). |
| Source/JavaScript/Arc.React/commands/CommandForm/fields/RadioButtonField.tsx | Switches field recognition to marker helper (vs displayName only). |
| Source/JavaScript/Arc.React/commands/CommandForm/commandFormMarkers.ts | Introduces marker-based recognition for fields/columns with compatibility fallback. |
| Source/JavaScript/Arc.React/commands/CommandForm/CommandFormFields.tsx | Ensures per-keystroke validation remains client-side; server round-trip is throttled elsewhere. |
| Source/JavaScript/Arc.React/commands/CommandForm/CommandFormField.tsx | Marks the marker component itself using the shared marker helper. |
| Source/JavaScript/Arc.React/commands/CommandForm/CommandForm.tsx | Uses marker helpers for recognition and ensures throttled server verdict updates silent validity. |
| Source/JavaScript/Arc.React/commands/CommandForm/CommandForm.stories.tsx | Updates Storybook examples to carry reason on validation results. |
| Source/JavaScript/Arc.React/commands/CommandForm/asCommandFormField.tsx | Marks wrapped fields using marker helper (improves robustness). |
| Source/DotNET/Tools/ProxyGenerator/XmlDocumentation.cs | Fixes XML doc rendering to preserve cref/paramref/langword semantics in generated JSDoc. |
| Source/DotNET/Tools/ProxyGenerator/ValidationRulesExtractor.cs | Stops resolving deferred FluentValidation message factories at generation time. |
| Source/DotNET/Tools/ProxyGenerator/TypeExtensions.cs | Maps DateOnly/TimeOnly to TS string to avoid timezone/invalid-date issues. |
| Source/DotNET/Tools/ProxyGenerator.Specs/Scenarios/for_ProxyGeneration/when_generating_type_with_temporal_properties.cs | Adds proxy-generation spec coverage for temporal type mapping. |
| Source/DotNET/Tools/ProxyGenerator.Specs/Scenarios/for_ProxyGeneration/when_generating_query_with_a_deferred_message.cs | Adds query-side spec coverage for deferred validation messages. |
| Source/DotNET/Tools/ProxyGenerator.Specs/Scenarios/for_ProxyGeneration/when_generating_command_with_every_rule_shape.cs | Updates rule-shape spec expectations for deferred messages. |
| Source/DotNET/Tools/ProxyGenerator.Specs/Scenarios/for_ProxyGeneration/when_generating_command_with_a_deferred_message.cs | Adds command-side spec coverage for deferred validation messages. |
| Source/DotNET/Tools/ProxyGenerator.Specs/Scenarios/for_ProxyGeneration/when_deserializing_temporal_properties_through_the_generated_proxy.cs | Adds runtime-behavior spec for DateOnly/TimeOnly deserialization correctness. |
| Source/DotNET/Tools/ProxyGenerator.Specs/Scenarios/for_ProxyGeneration/TemporalTypes.cs | Adds temporal test model used by proxy generator specs. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_XmlDocumentation/when_getting_documentation/SampleTypeWithCrossReferences.cs | Adds sample type to validate cref/langword/paramref rendering. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_XmlDocumentation/when_getting_documentation/for_a_type_with_cross_references.cs | Adds spec asserting type summary cross-reference rendering. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_XmlDocumentation/when_getting_documentation/for_a_property_with_a_langword.cs | Adds spec asserting langword rendering survives generation. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_XmlDocumentation/when_getting_documentation/for_a_parameter_with_a_cross_reference.cs | Adds spec asserting parameter docs render cross-references. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_ValidationRulesExtractor/when_extracting_rules_for_a_record_command_with_a_concept_property.cs | Updates extractor spec to stop projecting deferred messages. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_ValidationRulesExtractor/when_extracting_a_rule_whose_message_is_deferred.cs | Adds direct extractor spec for deferred-message behavior. |
| Source/DotNET/Tools/ProxyGenerator.Specs/for_ValidationRulesExtractor/DeferredMessageValidators.cs | Adds validator fixtures to test eager vs deferred message projection. |
| Source/DotNET/Testing/Commands/ICommandResultAssertionPolicy.cs | Adds seam for repo-specific assertion policies on top of built-ins. |
| Source/DotNET/Testing/Commands/CommandResultShouldExtensions.cs | Applies assertion policies and adds ShouldHaveValidationErrorBecauseOf. |
| Source/DotNET/Testing/Commands/CommandResultAssertionPolicies.cs | Discovers and applies installed ICommandResultAssertionPolicy implementations. |
| Source/DotNET/Testing.Specs/Testing.Specs.csproj | Adds new test project for Testing assertion behavior coverage. |
| Source/DotNET/Testing.Specs/Commands/for_CommandResultShouldExtensions/when_asserting_validation_errors/and_the_built_in_check_fails.cs | Pins policy ordering (policy can’t override failing built-in assertion). |
| Source/DotNET/Testing.Specs/Commands/for_CommandResultShouldExtensions/when_asserting_validation_errors/and_only_a_dependency_rejected.cs | Prevents false-green assertion when only dependency failure rejected the command. |
| Source/DotNET/Testing.Specs/Commands/for_CommandResultShouldExtensions/when_asserting_validation_errors/and_a_policy_rejects.cs | Verifies policy can strengthen (fail) an otherwise passing assertion. |
| Source/DotNET/Testing.Specs/Commands/for_CommandResultShouldExtensions/when_asserting_validation_errors/and_a_policy_is_installed.cs | Verifies policy is consulted on passing assertions. |
| Source/DotNET/Testing.Specs/Commands/for_CommandResultShouldExtensions/when_asserting_every_shape/and_no_policy_rejects.cs | Pins compatibility and coverage across all assertion shapes. |
| Source/DotNET/Testing.Specs/Commands/for_CommandResultShouldExtensions/when_asserting_a_reason/and_the_result_carries_it.cs | Adds spec coverage for ShouldHaveValidationErrorBecauseOf. |
| Source/DotNET/Testing.Specs/Commands/for_CommandResultShouldExtensions/given/a_recording_policy.cs | Adds recording policy fixture for assertion-policy specs. |
| Source/DotNET/Testing.Specs/Commands/for_CommandResultShouldExtensions/AssertionPolicyCollection.cs | Serializes policy specs due to process-wide discovery behavior. |
| Source/DotNET/Screenplay/Analysis/Specifications/SpecificationAssertions.cs | Adds new assertion name to screenplay recognized assertion list. |
| Source/DotNET/MongoDB/MongoDBReadModelForCommandResolver.cs | Documents ownership/serialization-boundary implications for resolver fallback behavior. |
| Source/DotNET/EntityFrameworkCore/EntityFrameworkReadModelForCommandResolver.cs | Documents EF ownership and serialization boundary implications for command-side resolution. |
| Source/DotNET/Chronicle/ReadModels/ChronicleReadModelForCommandResolver.cs | Documents Chronicle ownership and serialization boundary implications for command-side resolution. |
| Source/DotNET/Chronicle/Commands/ConstraintViolationExtensions.cs | Marks constraint violations with ValidationResultReason.ConstraintViolation. |
| Source/DotNET/Chronicle/Commands/ConcurrencyViolationExtensions.cs | Adds conversion helper producing reason + state for concurrency violations. |
| Source/DotNET/Chronicle/Commands/AppendResultExtensions.cs | Uses concurrency violation conversion helper for consistent reason/state. |
| Source/DotNET/Chronicle/Commands/AggregateRootCommitResultExtensions.cs | Uses concurrency violation conversion helper for consistent reason/state. |
| Source/DotNET/Chronicle.Specs/Commands/for_SingleEventCommandResponseValueHandler/when_handling_with_metadata/without_concurrency_on_event_source_type.cs | Adds spec coverage proving routing-only metadata still reaches append and narrows strategy. |
| Source/DotNET/Chronicle.Specs/Commands/for_ConcurrencyScopeBuilder/when_building/and_the_command_carries_metadata_without_concurrency.cs | Clarifies doc/spec semantics: no declared scope doesn’t mean routing tags are inert. |
| Source/DotNET/Chronicle.Specs/Commands/for_AppendResultExtensions/when_converting_append_result/with_constraint_violations.cs | Adds spec coverage for constraint-violation reason propagation. |
| Source/DotNET/Chronicle.Specs/Commands/for_AppendResultExtensions/when_converting_append_result/with_concurrency_violation.cs | Adds spec coverage for concurrency-violation reason propagation. |
| Source/DotNET/Chronicle.Specs/Commands/for_AppendResultExtensions/when_converting_append_many_result/with_concurrency_violations.cs | Adds spec coverage for concurrency-violation reason propagation (many). |
| Source/DotNET/Chronicle.Specs/Commands/for_AggregateRootCommitResultExtensions/when_converting/a_result_with_constraint_violations.cs | Adds spec coverage for constraint-violation reason propagation (aggregate path). |
| Source/DotNET/Chronicle.Specs/Commands/for_AggregateRootCommitResultExtensions/when_converting/a_result_with_concurrency_violations.cs | Adds spec coverage for concurrency-violation reason propagation (aggregate path). |
| Source/DotNET/Arc/Validation/ModelErrorExtensions.cs | Marks model-binding failures as MalformedRequest rather than a rule rejection. |
| Source/DotNET/Arc.Core/Validation/ValidatorInvoker.cs | Marks substituted “validator threw” result as ValidatorFailed. |
| Source/DotNET/Arc.Core/Validation/ValidationResultReason.cs | Adds server-side concept type for machine-readable validation reason. |
| Source/DotNET/Arc.Core/Validation/ValidationResult.cs | Adds Reason to validation results and extends factories to accept it. |
| Source/DotNET/Arc.Core/Queries/UnableToResolveReadModelFromCommandContext.cs | Marks read-model resolution failure as DependencyUnavailable. |
| Source/DotNET/Arc.Core/Queries/ReadModelForCommandServiceCollectionExtensions.cs | Documents how resolver ownership affects serialization boundary and customization. |
| Source/DotNET/Arc.Core/Queries/ReadModelDoesNotExistForCommand.cs | Marks missing read model as DependencyUnavailable. |
| Source/DotNET/Arc.Core/Commands/CommandResult.cs | Marks invalid-body as MalformedRequest. |
| Source/DotNET/Arc.Core.Specs/Validation/for_ValidatorInvoker/when_a_validator_throws.cs | Adds spec coverage for ValidatorFailed reason when validator throws. |
| Source/DotNET/Arc.Core.Specs/Validation/for_ValidatorInvoker/when_a_validator_rejects.cs | Adds spec coverage for default Rule reason on authored rejections. |
| Source/DotNET/Arc.Core.Specs/Validation/for_ValidatorInvoker/ThrowingValidator.cs | Adds validator fixture that rejects then throws. |
| Source/DotNET/Arc.Core.Specs/Validation/for_ValidatorInvoker/Subject.cs | Adds subject fixture for validator-invoker specs. |
| Source/DotNET/Arc.Core.Specs/Validation/for_ValidatorInvoker/RejectingValidator.cs | Adds authored-rejection fixture to validate default reason and state handling. |
| Source/DotNET/Arc.Core.Specs/Validation/for_ValidatorInvoker/given/a_validator_invoker.cs | Adds shared test context for ValidatorInvoker specs. |
| Source/DotNET/Arc.Core.Specs/Validation/for_ValidationResult/when_creating_a_result_without_saying_why.cs | Pins default reason semantics (Rule) for all factory paths. |
| Source/DotNET/Arc.Core.Specs/Queries/for_ReadModelDoesNotExistForCommand/when_the_pipeline_cannot_load_the_read_model.cs | Adds spec coverage for DependencyUnavailable reason on read-model load failure. |
| Source/DotNET/Arc.Core.Specs/Queries/Filters/for_FluentValidationFilter/when_validating/and_the_validator_throws.cs | Pins query-side propagation of ValidatorFailed reason. |
| Source/DotNET/Arc.Core.Specs/for_JsonSerializerOptionsConfiguration/when_doing_a_roundtrip_serialization/with_a_validation_result_carrying_a_reason.cs | Pins JSON shape for reason (string) and round-trip behavior. |
| Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_a_validator_dereferencing_a_null_concept_member.cs | Pins command-side propagation of ValidatorFailed reason. |
| Source/DotNET/Arc.Core.CodeAnalysis/QueryParameterConceptTypeAnalyzer.cs | Adds ARC0015 analyzer for raw query parameters converted to concepts in-body (skips validators). |
| Source/DotNET/Arc.Core.CodeAnalysis/DiagnosticDescriptors.cs | Registers ARC0015 diagnostic descriptor and messaging. |
| Source/DotNET/Arc.Core.CodeAnalysis/AnalyzerReleases.Unshipped.md | Tracks ARC0015 in analyzer release notes. |
| Source/DotNET/Arc.Core.CodeAnalysis.Specs/for_QueryParameterConceptTypeAnalyzer/for_ARC0015/when_two_raw_parameters_are_both_converted.cs | Adds analyzer spec coverage: one diagnostic per parameter. |
| Source/DotNET/Arc.Core.CodeAnalysis.Specs/for_QueryParameterConceptTypeAnalyzer/for_ARC0015/when_the_type_is_not_a_read_model.cs | Adds analyzer spec coverage: non-read-model methods aren’t flagged. |
| Source/DotNET/Arc.Core.CodeAnalysis.Specs/for_QueryParameterConceptTypeAnalyzer/for_ARC0015/when_the_query_method_is_internal.cs | Adds analyzer spec coverage: internal queries are still routed/validated. |
| Source/DotNET/Arc.Core.CodeAnalysis.Specs/for_QueryParameterConceptTypeAnalyzer/for_ARC0015/when_the_parameter_is_already_declared_as_the_concept.cs | Adds analyzer spec coverage: correct signatures are not flagged. |
| Source/DotNET/Arc.Core.CodeAnalysis.Specs/for_QueryParameterConceptTypeAnalyzer/for_ARC0015/when_a_string_parameter_is_cast_to_a_concept.cs | Adds analyzer spec coverage: cast conversion is flagged. |
| Source/DotNET/Arc.Core.CodeAnalysis.Specs/for_QueryParameterConceptTypeAnalyzer/for_ARC0015/when_a_raw_parameter_is_never_converted.cs | Adds analyzer spec coverage: raw parameters without conversion are not flagged. |
| Source/DotNET/Arc.Core.CodeAnalysis.Specs/for_QueryParameterConceptTypeAnalyzer/for_ARC0015/when_a_raw_parameter_is_converted_to_a_non_concept.cs | Adds analyzer spec coverage: conversions not to concepts are not flagged. |
| Source/DotNET/Arc.Core.CodeAnalysis.Specs/for_QueryParameterConceptTypeAnalyzer/for_ARC0015/when_a_nullable_guid_parameter_is_converted_to_a_concept.cs | Adds analyzer spec coverage: nullable raw identifiers converted to concepts are flagged. |
| Documentation/frontend/react/command-form/custom-fields.md | Documents marker-based field recognition and how to avoid displayName pitfalls. |
| Documentation/frontend/core/commands/command-result.md | Documents validation reason and how clients should interpret it. |
| Documentation/backend/proxy-generation/validation.md | Updates docs to explain deferred-message behavior in proxy generation. |
| Documentation/backend/proxy-generation/type-mapping.md | Adds mapping reference, including DateOnly/TimeOnly → string rationale. |
| Documentation/backend/proxy-generation/toc.yml | Adds “Type Mapping” to proxy-generation docs navigation. |
| Documentation/backend/proxy-generation/index.md | Links to new type mapping documentation page. |
| Documentation/backend/chronicle/commands/events.md | Corrects/clarifies how metadata affects concurrency behavior in practice. |
| Documentation/backend/chronicle/commands/concurrency.md | Corrects/clarifies routing-only tag impact and concurrency scope semantics. |
| Arc.slnx | Includes new Testing.Specs project in the solution. |
| | `validatorFailed` | A validator threw; nothing the author wrote survives | Show your own generic copy, and check the server log | | ||
|
|
||
| ```typescript | ||
| import { ValidationResultReason } from '@cratis/arc'; |
| export function markAsCommandFormField<T>(component: T): T & CommandFormMarked { | ||
| const marked = component as T & CommandFormMarked; | ||
| marked.isCommandFormField = true; | ||
| marked.displayName = CommandFormFieldDisplayName; | ||
| return marked; | ||
| } | ||
|
|
||
| /** | ||
| * Marks a component as a command form column. | ||
| * @param component The component to mark. | ||
| * @returns The same component, typed as marked. | ||
| */ | ||
| export function markAsCommandFormColumn<T>(component: T): T & CommandFormMarked { | ||
| const marked = component as T & CommandFormMarked; | ||
| marked.isCommandFormColumn = true; | ||
| marked.displayName = CommandFormColumnDisplayName; | ||
| return marked; | ||
| } |
|
Superseded by #2446, which carries this unchanged so the whole set lands as one release rather than four. Closing so it cannot be merged twice — the branch is untouched. |
Summary
Twelve defects and missing seams found by a consumer, each reproduced here before being fixed. Three are breaking.
Added
ValidationResultReasononValidationResult, so a rejection can be told apart from another without matching message textShouldHaveValidationErrorBecauseOf, for asserting why a command was rejectedICommandResultAssertionPolicy, a seam for applying a repository's own policy on top of the built-inCommandResultassertionsARC0015, warning when a query parameter is declared raw and converted to a concept in the method body, skipping the concept's validatormarkAsCommandFormFieldandmarkAsCommandFormColumn, so a command form field survives a build transform that rewritesdisplayNameChanged
DateOnlyandTimeOnlynow generate asstringrather thanDate, which rendered a calendar date a day early west of UTC and a time of day asInvalid DateautoServerValidateThrottlenow governs the per-keystroke validation request instead of a second, redundant oneShouldHaveValidationErrorsno longer passes when the only thing that rejected the command was a dependency the pipeline could not resolveFixed