Skip to content

Drop validation rules stated against a runtime value - #31

Merged
woksin merged 1 commit into
mainfrom
fix/drop-rules-stated-against-a-runtime-value
Aug 12, 2026
Merged

Drop validation rules stated against a runtime value#31
woksin merged 1 commit into
mainfrom
fix/drop-rules-stated-against-a-runtime-value

Conversation

@woksin

@woksin woksin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Fixed

  • A command validation rule stated against a value the application resolves while it runs — dueDate > today, or a threshold naming another property — is no longer carried with a substituted operand. Screenplay allows it, a Stage rule holds a fixed value, and the conversion filled the gap with 0 (or, for matches, an empty pattern), so dueDate > today became "greater than zero". The rule is dropped instead, leaving a caller free to report what was not carried rather than reading a rule the document never stated.

A Stage rule holds a fixed operand. Screenplay lets one be stated against
a value the application only resolves while it runs — 'dueDate > today',
or a threshold naming another property — and the conversion substituted 0
for the missing number and an empty string for the missing pattern rather
than declining to carry the rule.

Measured on the language's own invoicing.play, that turned 'dueDate >
today' into "greater than zero" and 'olderThan < today' into "less than
zero": rules asserting something the document never said, carried with the
same confidence as the ones it did.

Drop them instead, so what cannot be carried faithfully is not carried at
all and a caller can report it. The converter had no specs; it has them now.
@woksin woksin added the patch label Aug 12, 2026
@woksin

woksin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer context — how this was found and what it does not do.

Found by measurement, not review. Running the language's own invoicing.play through this chain and Studio's parallel one (the two implementations Cratis/StudioIssues#101 is about) showed this chain carrying 15 command rules to Studio's 11. The four extra were not extra coverage:

Source Was converted to
dueDate > today GreaterThan(Threshold = 0)
olderThan < today LessThan(Threshold = 0)

today is not a literal, so DoubleOperand fell through to its : 0 default. The other two were lines.quantity all > 0 / lines.unitPrice all >= 0, which are correct — see below.

Deliberately unchanged: all > / all >=. These still convert to the scalar comparison. The property path names the element (lines.quantity), so the scalar rule carries the same intent applied per element; the comment now says that rather than calling it an approximation. Studio's converter drops them outright, so the two implementations still disagree here — worth settling alongside #23, not in this PR.

Verification

  • 9 new specs. ValidationRuleConverter had none before — this was public surface with zero coverage.
  • Non-vacuity checked: reverting the source fix while keeping the specs fails 6 of the 9, and the 3 that still pass are the literal-operand baseline, which is the intended split.
  • Full suite green: 376 specs (Contracts 134, Stage 19, Rendering.Cratis 223), Debug and Release both clean with zero warnings.

No issue was filed for this beforehand, so the body carries no reference.

@woksin woksin left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed against what Stage is for — the contract between a Screenplay document and both the engine that runs it and the code it renders. Verdict: safe to release. Two scope findings that change how the change should be read, and one gap worth naming rather than fixing here.

The blast radius is narrower than the PR implies — codegen is untouched

Worth stating explicitly, because the obvious reviewer worry is "does this change what my generated app validates?" It does not. There are two independent paths out of a validation rule:

  • ContractEventModelLoaderScreenplayEventModelVisitorSliceConverterCommandConverterValidationRuleConverter. This is the only caller, and it feeds Cratis.Stage.Contracts.EventModel.
  • RenderingCommandValidatorRenderer / ConceptRendererValidationRuleRenderer, which reads ValidationRuleSyntax directly and never touches this converter.

So this changes what the contract holds — what the engine runs and what Studio's canvas shows — and changes generated C# not at all.

The renderer already established the right pattern, and it is ahead of the contract

CommandValidatorRenderer.RenderRule resolves an operand naming another command property into the member-access lambda FluentValidation needs, so amount > discount renders as real validation. When it genuinely cannot resolve one it emits:

// TODO: validation rule '{kind}' on '{property}' compares against a value the command does not carry

and adds a diagnostic. So the house answer to "operand I cannot express" is already report, do not fabricate — this change brings the converter's honesty in line with that, and the substituted 0 it removes was the only place in either path that invented one.

It also reframes the fix: the renderer can express amount > discount and RuleDefinition cannot, because its vocabulary holds a fixed value only. Dropping is correct for the contract as it stands — the durable fix is an operand that can name a property, which belongs with #23 rather than here.

The gap: the drop is unreported at the contract layer

Having removed the fabrication, the rule now disappears from the contract with nothing said about it. UnrenderedConstructs reports whole families — command, projection, readmodel, reducer, reactor, query — not individual rules, so it does not cover this.

Not a blocker, and not a regression: this converter already dropped five rule kinds silently (Equal, NotEqual, AllGreaterThan's quantifier semantics aside, Rule), so the change makes one more case consistent with established behavior rather than opening a new hole. It is also reported on the path where a human is actually looking — Studio surfaces it as UncarriedRuleReason.ValueIsNotFixed (Cratis/Studio#1143). The unreported case is the Stage engine path.

Release assessment

patch is right — no public signature changes, and the behavior change is strictly from "wrong value" to "no value". 376 specs green across Contracts/Stage/Rendering.Cratis, Debug and Release, zero warnings. Nothing downstream reads CommandPropertyRules expecting the fabricated thresholds; Studio consumes them for display and already reports the ones that do not survive.

@woksin
woksin merged commit 97bf712 into main Aug 12, 2026
1 check passed
@woksin
woksin deleted the fix/drop-rules-stated-against-a-runtime-value branch August 12, 2026 22:20
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