Rules and formulas
Every rule in a service is a formula on a fixed property (SCRUM-155). There is one language (Formula, in
modules/requests/Domain/Formula.cs) and one judge (Rules), asked by the form, the review, the engine and the
editor's preview alike. A new need is a formula, or a new Formula function; never a new member on FormField,
StepDef, DeadlineRule or FieldCheck (ArchitectureTests.LogicIsAFormulaNotAMember freezes them).
Where formulas live
| Property | What it decides | Judged by |
|---|---|---|
FormField.ShownIf («يظهر عندما») |
whether a field shows, and so whether it is asked | Rules.Shows |
FormField.RequiredIf («مطلوب عندما») |
whether a shown field is required | Rules.Requires |
FormField.ValidIf + ValidMessageAr |
whether an answer is valid, and the sentence when it is not | Rules.Invalid |
StepDef.OnRouteIf |
whether a step is on the request's route | Rules.OnRoute |
DeadlineRule.If, DeadlineRule.AmountFrom |
which deadline rule applies, and its amount | Rules.Holds, Rules.Number |
| A field group's rule and named result | a group's check, and a computed value the route can read | FieldChecks |
The language
A small, closed language read like a spreadsheet:
- Values: numbers (
12,2.5), text in quotes ("الشارقة"),true,false, and fields by their keys (go_date). - Operators:
+ - * /on numbers;= != < <= > >=between two values of one kind (dates with dates, times with times, numbers, text; yes/no with=and!=only);and,or,not; parentheses. - Functions:
answered(a, b…),asked(a, b…),sum,min,max,days(from, to),hours(from, to),round(x)andround(x, places),abs,if(condition, then, else),empty(a),filled(a),today(). - Blanks: an unanswered or hidden field is no value, and what reads it is no value too, except
answered,asked,empty,filled, anandwith a false side and anorwith a true one. A rule that comes to no value is not judged.
Text comparison goes through ArabicText.Normalize, so "ابوظبي" in a rule matches the list's "أبوظبي". A list
answer that holds several values (a requester's positions) is equal when one of them is (Formula.Many).
Parsing is bounded (token count and nesting depth), typed, and pure; Formula.Parse remembers a parsed text, since a
field's rule is asked on every render.
What a rule can read
- The form's fields, by key, limited by the moment: a step's rule reads the submission's fields only; a field's
rule reads fields asked no later than itself (
CatalogValidation.WhyNotRead). - Context values (
RuleContext, SCRUM-156), written into the form byContextValuesListenerbefore it is judged:requester.department,requester.branch,requester.positions,requester.manages, and a chosen list value's detail columns (car.seats). They are kept as submitted, so a route never moves when a card or a list changes later. - A field group's named result, as a field of its type.
The builder and the formula
The editor's rows (Condition: a field, a comparison, a value) are a view of a formula (Formula.FromRows,
Formula.Rows), never a second model. Groups joined by «أو» are the formula's ors. A rule the rows cannot say is
typed as a formula («تعديلها كصيغة», RuleBox) and kept as typed.
Checked before it runs
CatalogValidation types every formula at publishing and at startup: an unknown key, two kinds compared, a value
the field's list does not have, a field read before it is asked. Each refusal names the card it is about. The stored
conditions were folded into formulas once (ConditionFold, marker catalog.formulas_folded), and
GeneralityTests builds held-out scenarios as data alone (docs/checklists/general-logic-scenarios.md).