Skip to content

Name the constraint that rejected a command - #2532

Merged
woksin merged 2 commits into
mainfrom
feat/imp-2-carry-constraint-name-into-validation-result
Aug 12, 2026
Merged

Name the constraint that rejected a command#2532
woksin merged 2 commits into
mainfrom
feat/imp-2-carry-constraint-name-into-validation-result

Conversation

@woksin

@woksin woksin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Added

Fixed

A constraint violation reaching a client said only that some constraint
rejected the command, never which one. The name existed - Chronicle carries it
on the violation - and was dropped on the way into the validation result, so a
frontend wanting to say something specific about a particular constraint had no
choice but to match on the English message the kernel composed. Rewording that
message silently broke every consumer that did.

Carry the name through as ReasonDetail: Reason says what kind of thing rejected
the command, ReasonDetail says which one. It is a plain string rather than
Chronicle's ConstraintName because Arc.Core does not depend on Chronicle, and a
body property rather than a fifth positional parameter so the record's
constructor and deconstruction are unchanged.

This also closes a gap between the two halves of the framework. Chronicle's
testing package already offers ShouldHaveConstraintViolationFor against a raw
append, and Arc's own analyzer lists it among the named rejections, but the
assertion died at the command boundary because the name did not survive the
conversion - so the documented way to specify a constraint could not be written
against a command result. It can now.

Also fixes a client-side result composed after a failed validation losing its
reason, which has been reporting every such result as an authored rule since
the reason was introduced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: 49eab3a7-0743-4895-8303-bf5632b6a992
@woksin woksin added the minor label Aug 12, 2026
@woksin

woksin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer context, kept out of the body.

Shape and semver. ReasonDetail is an init-only body property on ValidationResult, mirroring Reason from v21.0.0. The record stays a four-parameter positional record, so its constructor, Deconstruct and with are unchanged — a fifth positional parameter would have been binary breaking. The three factories gained a trailing optional parameter, so existing call sites bind unchanged. It is a plain string? rather than Chronicle's ConstraintName because Arc.Core does not reference Chronicle and must not start.

Why the assertion matters as much as the field. Chronicle's testing package already ships ShouldHaveConstraintViolationFor against an IAppendResult, and this repository's own analyzer lists that name among its named rejections, while the .ai corpus tells authors to assert the constraint name and never the message. None of that was reachable from a command result, because the name was dropped in the conversion — the documented way to specify a constraint could not be written. The field and the helper are one gap seen from two sides.

Verification. Debug and Release, --no-incremental, for Arc.Core, Chronicle, Testing and their spec projects: zero warnings, zero errors. Arc.Core.Specs 1925, Chronicle.Specs 641, Testing.Specs 30, Arc.Specs 274 — no failures. TypeScript tsc -b clean, vitest 778, eslint clean.

Mutation evidence, each applied and restored with the restore verified by hash:

Mutation Result
drop reasonDetail from the conversion 4 assertions, 637 passing — specific
carry a wrong constraint name the same 4 — specific
helper's name comparison always true 3 assertions, specific
helper's reason check dropped, identity alone 3 assertions, specific
assertion policy call removed from the helper 1 assertion, specific

Two notes for whoever reviews the surrounding area. The reason a client-composed validation failure carries has been dropped since v21.0.0 introduced it; that is the Fixed bullet, and it is now pinned. And a first draft of the TypeScript member used string | undefined with a default, which is structurally required in TypeScript and broke a story file in Arc.React — caught only by building a package downstream of the one changed. It is ?: now.

Not covered. The TypeScript specs are asserted green but were not mutation-tested. ReasonDetail's wire encoding is argued from the serializer defaults rather than observed over HTTP. ConcurrencyViolation and DependencyUnavailable carry no detail yet — deliberately out of scope.

A failed query composed on the client dropped both the reason and the name of
the violated constraint, while the doc comment on it promises it mirrors the
command side so a caller reads the two the same way. The command side was
fixed and the query side was not, which made the promise less true than before
rather than more.

Neither the query result's server mapping nor the failure it composes had any
spec, so nothing noticed: removing both fields from the mapping left the whole
suite green.

Also corrects an assertion that could not fail. It filtered for a constraint
name that appears nowhere in its fixture and asserted the count was zero, which
holds whether or not the name is carried at all - it now filters for the name
the fixture does carry and asserts it is found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: 49eab3a7-0743-4895-8303-bf5632b6a992
@woksin

woksin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Independent review found three things worth acting on. All are addressed in ec43230e.

The Fixed bullet was only half true, and the review is what caught it. QueryResult.validationFailed dropped reason and reasonDetail exactly as CommandResult.validationFailed did — the same bug, in the file whose doc comment three lines above promises it mirrors the command side so a caller reads the two the same way. Fixing one and not the other made that promise less true than it was before. Both are fixed now, so the bullet holds for commands and queries alike.

Nothing on the query side was specced at all. The reviewer's mutation removing both fields from QueryResult's server mapping passed the entire suite. Two specs added — one for the mapping, one for the composed failure — and both mutations now die: dropping the mapping kills 2 assertions, reverting the validationFailed fix kills 2.

One of my new assertions could not fail. It filtered for a constraint name that appears nowhere in its fixture and asserted the count was zero, which holds whether or not the name is carried. It now filters for the name the fixture does carry and asserts it is found; it dies to the mapping mutation along with its siblings.

On semver, the reviewer did the experiment I had only argued. Compiling a consumer against 506ee40f and running it against 78226061 throws MissingMethodException — the three factories each gained a trailing optional parameter, and C# bakes defaults into the call site, so this is source-compatible but binary-breaking. Keeping minor deliberately rather than by omission: it is neither a removal nor a rename, and the only downstream callers across the Cratis repos are applications, which recompile. Worth recording that Reason's v21.0.0 precedent rode a major, so it does not settle the question, and that a with { ReasonDetail = … } expression would have delivered the same feature with no ABI change at the cost of an asymmetry with reason.

Record equality was checked and is inert — nothing keys, dedupes or groups by a whole ValidationResult, and the specs that compare one compare it to itself.

Gate after the fixes: 305 spec files / 782 tests in the Arc package, tsc -b clean, eslint 0 errors (5 pre-existing Unused eslint-disable warnings in for_ObservableQueryMultiplexer and for_QueryResult, unchanged with these edits stashed).

Two things the reviewer raised that I did not act on: the helper takes a string where Chronicle's takes a ConstraintNameCratis.Arc.Testing cannot reference Chronicle, and the implicit conversion covers the call site — and there is no spec for several validation results where the match is not the first. Any(...) makes that correct rather than lucky, but the input space is genuinely uncovered.

@woksin
woksin merged commit b1200cb into main Aug 12, 2026
57 checks passed
@woksin
woksin deleted the feat/imp-2-carry-constraint-name-into-validation-result branch August 12, 2026 21:54
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.

1 participant