6. Rules
A rule says what must, should or probably is true, and what follows from it. Rules are pure: they read values and return verdicts or proposed values. They never act. Anything that moves values or touches the world is a contract (chapter 7).
6.1 The shape of a rule #
rule: dvdIsMpeg2
for: OpticalDisc
when: format is DVD
then: videoCodec is MPEG-2
strength: suggest
because: DVD-Video is MPEG-2 by standard
version: 2026-10-13
| Part | Holds | Default |
|---|---|---|
for |
where the rule applies: a class (its wearers), a kind, a level (unit::), or everywhere |
everywhere |
when |
a condition (6.3); the rule applies only where it holds | always |
then |
the consequence (6.4) | required |
strength |
must, should or suggest (6.2) |
required |
because |
the reason shown to people when the rule refuses, flags or fills | none |
version |
the dated version | required when published |
Rules are named like keys and versioned like facets (chapter 4.1). A rule expanded from a shortcut (chapter
4.4) or a class line (chapter 5.3.1) is named by where it came from: title: required,
DvdOnly: format{DVD}.
6.2 Strengths #
| Strength | A write that breaks it | A value it needs that is empty | A value it can produce |
|---|---|---|---|
| must | refused, with the reason | the record is incomplete | derived, and a person may not adopt it |
| should | saved and flagged | flagged | derived; adopting it is flagged |
| suggest | nothing; a person's value wins | filled, with the rule as its source | derived; a person may adopt it freely |
"A write that breaks it" means a write to a value the rule's then tests. A rule whose verdict changes
because another value changed never deletes anything: see 6.7.
6.3 Conditions #
condition = term *( " and " term / " or " term ) ; "and" binds tighter than "or"
term = [ "not " ] ( test / "(" condition ")" )
test = path " is " value ; equals
/ path " is _" / path " is -" ; is unknown · does not apply
/ path " in " values ; one of: "in DVD, Blu-ray"
/ path " in " number ".." number ; inside a range, ends included
/ path " has" [ " " value ] ; has a value · the list holds this item
/ path ( " before " / " after " ) value ; dates, moments, times of day, or now
/ path ( " over " / " under " ) value ; numbers, quantities, money, ranked choices
/ path " is not " value ; the same as "not path is value"
/ path " wears " class ; the record there wears the class (chapter 7.6)
/ path " may " verb " " target ; the caller there holds that item (chapter 14.6)
/ path " is unique" ; global (6.6); in "then" only
/ path " shown to " audience ; visibility (chapter 12.4); in "then" only
/ path " is " state ; a value's state (6.3.2)
path = address / aggregate / envelope
envelope = "id" / "level" / "of" / "within" / "status" ; the record's envelope (chapter 1.1); "status" is its state
aggregate = combine " of " address [ " where " condition ] ; "worst of parts[*].condition"
/ ( "count" / "least" / "most" ) " of " address [ " where " condition ] ; how many · smallest · largest
- Addresses are chapter 1.10's, plus the wildcard
[*]for every row or part, the level prefix (unit::condition) to read a lower level from above, and a step through a link facet (works[*].genres) to read shared parts (1.6). An aggregate over a compound's rows or a record's parts MAY carry a row filter,whereand a condition read against each row:count of Manifest[*] where manifestItemType is disc; a bareManifest[*]counts rows. A path MAY start withcaller.(the caller, chapter 7.6) orcontext.(a page's record, chapter 12.2) to read another record than the one the condition is on;callerandcontextalone, beforewearsormay, name that record itself. - Envelope fields read as paths:
statusisactivefor a record that has none, sonot status is archivedholds for it;wearsis tested withwears, never read as a value. They are always readable with the core prefix (crubal:status,crubal:id); a barestatusoridreads the envelope unless the package defines a facet of that name, which then wins. A package SHOULD NOT name a facet after an envelope field. - Values are written as in the text form (chapter 2), read by the facet at the path.
nowis the moment the rule is evaluated, supplied by the caller so results stay reproducible:when: auctionEnds before now. Moments compare by instant; a local date or time of day compared with a moment is undetermined; a plain date compared with a moment compares by the moment's UTC date.- Aggregates use the combine rules (chapter 4.4),
count, andleastandmost(the smallest and largest number, quantity, money or date; for ranked choices, usebestandworst).overandunderon a ranked choice compare positions in the list: earlier is higher.
6.3.1 Empties in conditions #
A test on an absent or unknown value is undetermined. A test on does not apply (-) is
false, except is -, which is true.
and |
true | false | undetermined |
|---|---|---|---|
| true | true | false | undetermined |
| false | false | false | false |
| undetermined | undetermined | false | undetermined |
or is the mirror (true wins), and not undetermined is undetermined. has, is _ and is - are never
undetermined: they test the empties themselves. is unique holds for an empty value: empties are never
compared with other records.
A rule whose when is false or undetermined doesn't apply. A must rule whose then is undetermined makes
the record incomplete; a should rule's flags it; a suggest rule's fills it when it can.
6.3.2 Value states #
A condition MAY test the state of a value, not only the value: is confirmed, is contradicted, is unobserved (chapter 9.7.2), is conflicted (it has alternatives, chapter 8.4), is new (a value the
vocabulary doesn't hold yet), is held (6.7), is adopted (chapter 8.2) and is suggested (it has
suggestions). State tests are never undetermined.
They are what lets contracts form a cascade: cheap sources first, expensive ones only when needed.
contract: aiTitle
kind: receive
when: title is conflicted or not title has
6.4 Consequences #
then is a condition (6.3) that should hold, read in one of two ways:
A check. Any test. It holds or it breaks. then: runtime in 1..600, then: not hdrFormats has.
A production. A test of the form key is value produces that value when the key is empty:
| Right side | Kind | Example |
|---|---|---|
| a plain value | a fill | then: videoCodec is MPEG-2 |
| a template | a derivation | then: listingTitle is "{title} [{format}]" |
| an aggregate | a rollup | then: condition is worst of parts[*].condition |
- A fill produces only at
suggest. Atmustandshould, a plain value is a check. - A derivation or rollup produces at every strength. The engine writes it, recomputes it whenever
what it reads changes, and stores it with
origin: derivedand the rule as itsvia. - A rollup over a lower level (
count of unit::cashierfuId) reads the active records below, as views do (1.12); merged ones never. A row filter that namesstatusreads the archived too:count of unit::cashierfuId where status hascounts a product's units with history included. - A production writes only at a level where its key is stored: the top level, or a level where the key varies (chapter 1.7). Below that, records read the produced value through, as they read any other.
- A person who edits a produced value adopts it (✎, chapter 8): the value stops following the rule until it is given back. The strength says whether adopting is allowed (6.2).
6.4.1 Templates #
A template is a quoted text with placeholders: {address} is replaced by the value there, printed as in
the text form without its detail. {{ and }} write a literal brace.
{a ?? b ?? c}takes the first of them that has a value.{a?}is replaced by nothing whenais empty, and a space or one of· , - :written right before it is dropped with it:"{title} · {subtitle?}"is"Vacation"when there is no subtitle.- If any other placeholder's value is empty, the template produces nothing. Templates are the only computation inside rules: arithmetic, lookups and AI belong to contracts.
6.5 What rules are for #
Every one of these is a rule of the shape above; no other mechanism exists.
| Use | Example |
|---|---|
| a check | then: runtime in 1..600 · should |
| a requirement | then: title has · must |
| forbidding | when: format is DVD then: not hdrFormats has · must |
| narrowing | for: OpticalDisc then: format in DVD, Blu-ray, 4K · must |
| a gate | when: not format in Blu-ray, 4K then: hdrFormats is - · must |
| a fill | when: format is DVD then: videoCodec is MPEG-2 · suggest |
| a derivation | then: listingTitle is "{title} [{format}]" · should |
| a rollup | then: condition is worst of parts[*].condition · must |
| a global rule | then: gtin is unique · must |
6.6 Global rules #
A global rule reads other records: is unique (no other record in the scope holds the same value) and
view membership (chapter 12). Its scope is the rule's for; is unique compares against every record there
at the same level, active or archived, but one archived as merged (chapter 1.12). Levels don't
compete: a value copied down onto an archived or detached record (chapter 1.12, 1.7) still names the same
thing, and two standing-alone records at one level that share an identify key are an arrival to review, not
a refusal. Global rules are their own class of rule
because they need indexes; a conforming store at the Rules level MUST enforce must-strength global rules
at write.
6.7 When a verdict changes #
A rule's verdict can change without anyone writing the value it tests: a gate closes because format
changed, a new rule version narrows a list. Then the engine never deletes and never refuses the unrelated
write. The affected value is held: kept, hidden from forms and pages, and flagged with the rule. It
comes back if the verdict changes again, and a person or a migration recipe (chapter 10) can resolve it.
Holding and releasing are logged writes (as: held, chapter 8.5), made by the engine with the rule as by.
They are never worked out again from the rules, so a later rule version can't change what was held in the
past.
6.8 Rule sets #
The rules that apply to a record are those whose for it matches, from every class it wears, every facet
shortcut, and the package. A package MUST be refused at publish when its rule set breaks any of these:
- Layered. Draw an edge from every address a rule reads (in
whenor on the right ofthen) to the address it produces. The graph MUST have no cycle: no rule depends on its own effect, and no two fills chase each other. Addresses, not bare keys: a rollup that readsparts[*].conditionand producesconditionon the whole is not a cycle. - No contradictions. Two
mustrules that can apply to the same record MUST NOT require disjoint values of the same key. Implementations MUST detect this at least foris,inand range tests whosewhenconditions are both satisfiable at once; they MAY detect more. - One spelling. A shortcut and a rule written in full that say the same thing MUST NOT both be present.
Order. Evaluation order never changes a result, except where a replace combine meets contributions in
order (chapter 4.4). An implementation MUST say which order it used in that case.
Traceable. Every refusal, flag, fill, derivation and hold names its rule, and every produced value's
source names it as via.
6.9 Results #
After a write, a record's rule results are:
| Result | Means |
|---|---|
| complete | for each class it wears: no must rule is undetermined or broken |
| completeness | a number from 0 to 1: the asked facets that have a value, divided by all asked facets. Unknown counts as empty, does-not-apply as answered. Parts are measured on themselves and never added to the whole's count, so a five-disc set isn't less complete than one disc for the same answers |
| incomplete | a must rule needs a value that is empty; lists the rules |
| flagged | a should rule broke or is undetermined; lists the rules and addresses |
| filled · derived | values the rules produced, with their sources |
| held | values hidden by a changed verdict (6.7) |
A write that breaks a must rule is not a result: it is refused, and nothing changes.
6.10 The JSON form #
{
"rule": "dvdIsMpeg2",
"for": "OpticalDisc",
"when": "format is DVD",
"then": "videoCodec is MPEG-2",
"strength": "suggest",
"because": "DVD-Video is MPEG-2 by standard",
"version": "2026-10-13"
}
Conditions are stored as their text (6.3), so there is one grammar for them, not two.
6.11 Events and current values #
A facet holds the current value; it never holds a history. How a value got there is the log (chapter 8.5), and anything that happens in the world (a sale, a reading, a game) is a record of a kind with a moment facet, data like any other. So a series worth asking about (prices over time, readings) is a kind of record with a moment, or a many-valued compound whose rows carry one; rules read the current value and aggregates over those rows, and no facet needs a history of its own.
Which rules a store compiles into its database and which it checks in the application is an implementation choice (chapter 13.6); the results MUST be the same.