Specification · v1
Evaluation
1. decide
A valid contract is compiled once and then decides actions:
decide(action, facts) → { verdict: "PERMIT" | "DENY", clause_id }
action must be listed in the contract's TRIGGER. Asking about any other action is a usage error (ulang-lite throws a RangeError), not a verdict. Only the clauses for that action take part, in source order:
- The first
FORBIDwhose condition holds decides:DENY <Contract>.<id>. - Otherwise, the first
ENSUREwhose condition fails decides:DENY <Contract>.<id>. - Otherwise:
PERMIT <Contract>.<action>.permit.
Prohibitions are checked before guarantees, whatever their order in the source. Within each group, source order decides which clause a DENY names when more than one applies. A contract that lists its prohibitions from most to least specific therefore produces the most useful DENY.
// Expense approvals: who may approve, and up to how much.
CONTRACT ExpenseApproval {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Expense {
amount: float
approver_is_submitter: bool
approver_limit: float
receipt_attached: bool
category: string
}
CONSTANTS {
receipt_threshold: float == 75.0 [SOURCE:FIN-POLICY-4.2]
hard_cap: float == 10000.0 [SOURCE:FIN-POLICY-4.1]
}
}
TRIGGER {
approve_expense()
}
GUARANTEE {
ENSURE approve_expense() WHEN Expense.receipt_attached == true OR Expense.amount < receipt_threshold [SOURCE:receipt_required]
}
PROHIBITION {
FORBID approve_expense WHEN Expense.approver_is_submitter == true [SOURCE:no_self_approval]
FORBID approve_expense WHEN Expense.amount > hard_cap [SOURCE:over_hard_cap]
FORBID approve_expense WHEN Expense.amount > Expense.approver_limit [SOURCE:over_approver_limit]
FORBID approve_expense WHEN Expense.category == "gift_card" [SOURCE:no_gift_cards]
}
OBSERVABILITY {
LOG "expense approval decided" ON decision
}
}
valid ExpenseApproval 5 clauses · sha256 5a6326d24470e02c54a4f175994a1b0c2c4a6efa08baec2c025b0af1fc78e7fd
Facts of the first request; each other request changes only the fields shown.
{
"Expense": {
"amount": 120,
"approver_is_submitter": false,
"approver_limit": 500,
"receipt_attached": true,
"category": "travel"
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| within limits, receipt attached | approve_expense | as above | PERMIT | ExpenseApproval.approve_expense.permit |
| approver submitted it | approve_expense | approver_is_submitter: true | DENY | ExpenseApproval.no_self_approval |
| above the approver’s limit | approve_expense | amount: 800 | DENY | ExpenseApproval.over_approver_limit |
| no receipt above the threshold | approve_expense | receipt_attached: false | DENY | ExpenseApproval.receipt_required |
| no receipt, under the threshold | approve_expense | amount: 40 receipt_attached: false | PERMIT | ExpenseApproval.approve_expense.permit |
| self-approval and no receipt: prohibitions are checked first | approve_expense | approver_is_submitter: true receipt_attached: false | DENY | ExpenseApproval.no_self_approval |
2. Facts
Facts are the values of the contract's one entity: { "<Entity>": { "<field>": value, … } }. Before deciding, facts are checked (checkFacts):
- the object has exactly one key, the entity's name;
- every declared field is present, and there are no other keys;
intis an integer with|n| ≤ 2^53-1;floatis a finite number;boolistrue/false;stringis a string.
decide assumes checked facts. Applications may impose narrower domains; the µLang Git forge checks its facts against its own schema first.
3. Comparisons
| Comparison | Semantics |
|---|---|
Entity.field == "text" / != | exact string equality, by code points. No case folding, no Unicode normalisation. |
Entity.field == true / != | boolean equality |
number < <= > >= number | exact IEEE-754 comparison |
number == / != number | scaled tolerance, below |
Operands evaluate as follows: an integer literal is its exact value, a float literal is its IEEE-754 double, a path is the fact's value, and a constant is its declared value.
3.1 Numeric equality
a == b ⇔ |a − b| < 2^-52 · max(1, |a|, |b|)
a != b ⇔ not (a == b)
This tolerates last-bit noise in binary floating point. 0.1 + 0.2 arriving as a fact equals the literal 0.3. Distinct integers below 2^52 stay distinct. Ordering operators are exact, so it is possible for a == b and a < b to hold together when a and b differ in the last bit. Write thresholds with <=/>= when the boundary itself matters.
int and float operands mix freely. Both are numbers.
4. Logic
AND and OR short-circuit, left to right. Evaluation has no side effects, so short-circuiting never changes a result, only its cost.
5. What does not affect a decision
- The enforcement mode.
enforced,advisoryandauditdecide alike. The mode states what a caller should do with the verdict; the evaluator does not look at it. OBSERVABILITY.LOG "…" ON eventdeclares what a caller should record. It is part of the canonical form and the digest, and plays no part in deciding.- Order of
TRIGGERentries. It changes the canonical text, and so the digest, but not any decision.
6. Determinism
The same contract, action and facts give the same decision, everywhere. There is no clock, randomness, I/O, locale or hidden state in the evaluation path. The canonical digest identifies the contract that produced a decision, so a decision recorded with the digest can be reproduced later by anyone who holds the same source.
7. Many contracts, one action
An application that holds several contracts routes each action to the contract whose TRIGGER lists it. The command line refuses an action listed by more than one loaded contract, or by none. The µLang Git forge pins every contract to its schema: each TRIGGER must list exactly that contract's actions, and each entity must match the schema's fields and types.