Table of Contents

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) and round(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, an and with a false side and an or with 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 by ContextValuesListener before 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).