Drop validation rules stated against a runtime value - #31
Conversation
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.
|
Reviewer context — how this was found and what it does not do. Found by measurement, not review. Running the language's own
Deliberately unchanged: Verification
No issue was filed for this beforehand, so the body carries no reference. |
woksin
left a comment
There was a problem hiding this comment.
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:
- Contract —
EventModelLoader→ScreenplayEventModelVisitor→SliceConverter→CommandConverter→ValidationRuleConverter. This is the only caller, and it feedsCratis.Stage.Contracts.EventModel. - Rendering —
CommandValidatorRenderer/ConceptRenderer→ValidationRuleRenderer, which readsValidationRuleSyntaxdirectly 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.
Fixed
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 with0(or, formatches, an empty pattern), sodueDate > todaybecame "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.