From bf93e5ece4b4b69e4e2565c04a639430c3c85d60 Mon Sep 17 00:00:00 2001 From: delchev Date: Wed, 9 Sep 2026 15:35:45 +0300 Subject: [PATCH] site: the null case of a guard's key-tuple and of a posting's rule match Two lookups by value whose absent case the pages did not state. An aggregate already says a source row with any grouping key unset belongs to no tuple and is ignored; the guard section now says its half - such a record is not guarded at all. And a posting's rule match is a literal that has to say something: a blank one matches no rule row and sends every source document to the unposted worklist with nothing failing anywhere. Co-Authored-By: Claude Opus 5 --- docs/spec/entities.md | 2 ++ docs/spec/glue.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/spec/entities.md b/docs/spec/entities.md index 8e8b231..6ea737a 100644 --- a/docs/spec/entities.md +++ b/docs/spec/entities.md @@ -325,6 +325,8 @@ One outcome would have fitted exactly one real rule. Negative stock wants the wr The total is recomputed from the guarded entity's own rows for the incoming record's key-tuple - excluding the record being updated - rather than read from the materialised aggregate, so the decision cannot race the aggregate's maintenance. The guarded entity must be the aggregate's own source. +A record with any of the aggregate's grouping keys unset **is not guarded at all**. It belongs to no key-tuple - the aggregate itself ignores such a row, and materialises no target row for it - so it contributes to no total and can breach no minimum: the write proceeds, nothing is refused, a `marker` reads as holding and a `setStatus` is not written. This is the guard's half of a rule the aggregate already states, and it keeps the two a pair of computations of the same total rather than of two different ones. A grouping key that must always be there is declared `required`, which is the better statement where it is true. + `outcome: task` stamps a flag; it does not create or route to a task. A workflow [decision](/spec/processes#decision-steps) reads the marker and routes the record - the two constructs compose, and the guard is the part that computes. ## Role-scoped field visibility — `visibleTo` diff --git a/docs/spec/glue.md b/docs/spec/glue.md index 44a9b7e..495ecdb 100644 --- a/docs/spec/glue.md +++ b/docs/spec/glue.md @@ -695,6 +695,8 @@ postings: - { Account: rule(receivableAccount), credit: "Amount" } ``` +`rule.match` is a single `column: literal` selector, and the literal has to say something: it is matched against the rule row's own column as authored, so a blank one looks up the empty value, matches no rule row, and sends *every* source document to the unposted worklist while the model, the generation and the deployment all stay healthy. An absent or blank match value is an authoring error, reported when the intent is read. A rule *row* whose match column is empty is a different thing - an ordinary row of the rule table, matched by no non-blank literal. + ### Conditional rule column When the account column must be chosen by a **source value** — a payment posts to the bank account for a transfer, the cash account for cash — a single item row selects the rule column by a classifier instead of duplicating the row per case (the same `by` / `cases` / `default` shape a conditional value-copy uses). Quote it, since it carries colons and braces: