Skip to content

Stop the proxy generator and command pipeline erasing distinctions the client needs - #2444

Closed
woksin wants to merge 17 commits into
mainfrom
feat/proxy-generator-and-validation-hardening
Closed

Stop the proxy generator and command pipeline erasing distinctions the client needs#2444
woksin wants to merge 17 commits into
mainfrom
feat/proxy-generator-and-validation-hardening

Conversation

@woksin

@woksin woksin commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Twelve defects and missing seams found by a consumer, each reproduced here before being fixed. Three are breaking.

Added

  • ValidationResultReason on ValidationResult, so a rejection can be told apart from another without matching message text
  • ShouldHaveValidationErrorBecauseOf, for asserting why a command was rejected
  • ICommandResultAssertionPolicy, a seam for applying a repository's own policy on top of the built-in CommandResult assertions
  • ARC0015, warning when a query parameter is declared raw and converted to a concept in the method body, skipping the concept's validator
  • markAsCommandFormField and markAsCommandFormColumn, so a command form field survives a build transform that rewrites displayName
  • Documentation for how .NET types map to TypeScript in generated proxies

Changed

  • DateOnly and TimeOnly now generate as string rather than Date, which rendered a calendar date a day early west of UTC and a time of day as Invalid Date
  • A validator message declared as a factory is no longer resolved at generation time and baked into the generated client validator; the rule still mirrors, the message is left to the server
  • autoServerValidateThrottle now governs the per-keystroke validation request instead of a second, redundant one
  • ShouldHaveValidationErrors no longer passes when the only thing that rejected the command was a dependency the pipeline could not resolve

Fixed

  • Cross-references in XML documentation are rendered into the generated TypeScript instead of erased, along with the prose that fused around them
  • Documentation stating that routing-only stream metadata does not affect concurrency control, which it does
  • Documentation not saying that the provider resolving a command-side read model also decides which serialization boundary it crosses

woksin and others added 17 commits August 4, 2026 12:05
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
Copilot AI lite review requested due to automatic review settings August 4, 2026 10:13
@woksin woksin added the major label Aug 4, 2026
@woksin

woksin commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer context — not release notes.

How each change was arrived at

Every 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.

autoServerValidateThrottle was measured, not reasoned about. Ten characters typed into one field, counting POSTs to {route}/validate: 11 requests at the default throttle, 10 with the throttle raised past the burst — the prop moved nothing. Now 1 and 0. The eleventh request is a second, throttled call that only exists because the first one already ran. when_typing_into_a_field_with_autoServerValidate.

That fix makes the code match documentation it had diverged from. Documentation/frontend/react/command-form/auto-server-validation.md already promised "With throttle: 1 server call (after user stops typing for 500ms)". The page needed no change.

The routing-only concurrency claim is now pinned end to end. without_concurrency_on_event_source_type asserts the pair that matters — a routing-only command reaches Append with no scope and its tag intact — which is what makes the fallback strategy narrow by it. Asserting the null scope alone is what let the misconception survive; the existing ConcurrencyScopeBuilder spec's rationale said so in source and is corrected here.

Design decisions a maintainer may want to revisit

  • ValidationResultReason is an open ConceptAs<string>, not an enum. Rejections are composed in Arc, in Chronicle and in application code, so a closed set makes every new kind a breaking change for anyone switching over it. It is deliberately not carried in ValidationResult.State, which already carries FluentValidation's WithState and therefore belongs to whoever wrote the rule.
  • FluentValidation's ErrorCode is still not carried through. One line, but it would make that library's defaults (NotEmptyValidator, …) a wire-visible value Arc then owes.
  • ARC0015 is a Warning, and covers internal as well as public query methods. The shape is legal and retyping is not behaviour-neutral — an omitted argument deserializes to null rather than NotSet, and a strict validator becomes reachable — so an Error would be suppressed rather than adopted. Both caveats are in the descriptor's description. It registers an operation-block action, which has no precedent in either analyzer project here.
  • DateOnlystring is a floor, not a ceiling. A first-class DateOnly in @cratis/fundamentals would be better; that package ships no such type today, and an Arc-defined class cannot work because the converter registry is module-private. If Fundamentals ships one, this becomes a two-line change.
  • The command form marker is a static property, not a Symbol. A Symbol resists a name-based transform more thoroughly but is a public-surface choice, and needs Symbol.for to survive a duplicate install. The displayName fallback stays permanently on both sides — it is what lets independently versioned packages interoperate in both directions. The Components half is Identify CommandForm fields and columns by a tamper-resistant marker Components#112.

Verification

  • dotnet build Debug and Release: clean. Release multi-targets and treats warnings as errors.
  • 15 C# spec projects green; Cratis.Arc.Testing had no spec project and no assertion coverage before this, and now has both.
  • 5 TypeScript workspaces green across three consecutive runs; yarn lint and npx tsc -b clean on each.
  • Pre-existing and unrelated: a CS0436 warning in TestApps/Chronicle, reproduced on a clean tree with --no-incremental; and for_ObservableQueryDemultiplexer…and_query_context_has_paging, which flakes under parallel load and passed 50/50 on three isolated runs.

Coverage gaps this closed on the way

ValidatorInvoker, the Cratis.Arc.Testing assertions, and the aggregate-root concurrency path each had no direct coverage at all — which is how the aggregate-root path came to carry the same flattening as the event-log path unnoticed. An existing throttle spec asserted toBeLessThanOrEqual(5), which is what let a round trip per keystroke hide underneath it; it now asserts toBe(1).

Comment on lines +119 to +127
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;
}
}
Comment on lines +38 to +41
catch
{
_typeScriptIsValid = false;
}

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 displayName rewrites 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';
Comment on lines +41 to +58
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;
}
@woksin

woksin commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

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.

@woksin woksin closed this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants