Rule¶
A rule pairs fact-pattern conditions (the when clause) with a
decision and optional fact assertions (the then clause). Rules compile
to CLIPS defrule constructs scoped to their enclosing module. For
conceptual context see Five Primitives;
for how salience and last-write-wins interact at evaluation time see
Runtime & Working Memory.
Top-level fields — RuleDefinition¶
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
name |
str |
— | yes | CLIPS identifier. Must match ^[A-Za-z_][A-Za-z0-9_\-]*$. Emitted as (defrule <module>::<name> …). |
description |
str |
"" |
no | Author-facing prose. Not emitted to CLIPS. |
salience |
int |
0 |
no | Priority hint. Emitted as (declare (salience N)) only when != 0. |
when |
list[FactPattern] |
— | yes | LHS fact patterns. Pydantic accepts an empty list; compile_rule raises CompilationError when empty. |
then |
ThenBlock |
— | yes | RHS decision and/or asserts. See ThenBlock below. |
name is validated by _name_must_be_clips_ident. compile_rule in
src/fathom/compiler.py re-checks emptiness and the when-list.
Fact-pattern fields — FactPattern¶
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
template |
str |
— | yes | The template name this pattern matches. Emitted as the head of the pattern CE: (<template> …). |
alias |
str \| None |
None |
no | Optional name used as the cross-fact prefix in other patterns' expressions ($alias.slot). Resolved via _resolve_cross_refs. |
conditions |
list[ConditionEntry] |
— | yes | Slot constraints and/or test CEs. An empty list emits a bare (<template>) pattern. |
Condition entry — ConditionEntry¶
A ConditionEntry supports four shapes. The enforcing validator is
_require_bind_or_expression in src/fathom/models.py.
Shape 1 — slot + expression¶
Emits a CLIPS slot constraint via _compile_condition. See
Supported operators.
Shape 2 — slot + bind (no expression)¶
bind must start with ? (enforced by _bind_must_start_with_question_mark).
Emits (<slot> ?sid) — captures the slot's value so peer conditions and
the RHS can refer to it.
Shape 3 — standalone test¶
Only test is set. test must be a parenthesized CLIPS expression
(enforced by _test_must_be_wrapped). Emits (test <expr>) on the rule
LHS after all pattern CEs — the escape hatch for custom functions
registered via Engine.register_function.
Shape 4 — combinations¶
bind + expression constrains and captures in the same slot;
test combined with a slot/expression appends a (test …) CE after
the enclosing pattern.
What the validator rejects¶
- Empty entry (no
expression,bind, ortest). slotset but neitherexpressionnorbindprovided.slotset alongside a standalonetestwith noexpression/bind— the slot would have no effect; drop it or add anexpression/bind.bindthat does not start with?.testthat is empty or not parenthesized.
Supported operators in expression¶
Syntax: operator(arg). arg may be a literal or a cross-fact reference
$alias.field (resolved via _resolve_cross_refs to ?alias-field).
Source: _compile_condition docstring in src/fathom/compiler.py.
| Group | Operator |
|---|---|
| Comparison | equals, not_equals, greater_than, less_than |
| Set | in, not_in |
| String | contains, matches |
| Classification | below, meets_or_exceeds, within_scope |
| Temporal | changed_within, count_exceeds, rate_exceeds, last_n, distinct_count, sequence_detected |
Classification operators require a classification function declared with
a hierarchy_ref elsewhere in the YAML bundle — the operator emits a
call to the generated below / meets-or-exceeds / within-scope
CLIPS deffunction. Temporal operators emit (test …) CEs that call
external functions registered at runtime.
Any other operator raises CompilationError from _compile_condition.
For worked examples of each operator see
Writing rules.
ThenBlock fields¶
| Field | Type | Default | Description |
|---|---|---|---|
action |
ActionType \| None |
None |
One of allow, deny, escalate, scope, route. Emitted as an unquoted symbol on the __fathom_decision fact. |
reason |
str |
"" |
Free text. {placeholder} refs compile via _compile_reason to (str-cat "…" ?placeholder "…"); otherwise a quoted literal. |
log |
LogLevel |
LogLevel.SUMMARY |
One of none, summary, full. Emitted as the log-level slot on the decision fact. |
notify |
list[str] |
[] |
Notification targets. Joined with ", " and emitted as a single quoted string in the notify slot. |
attestation |
bool |
False |
Emitted as TRUE/FALSE on the decision fact's attestation slot. Not a signing switch — see Audit & Attestation. |
metadata |
dict[str, str] |
{} |
JSON-serialized (sorted keys) and emitted as a quoted string when non-empty; otherwise an empty quoted string. |
scope |
str \| None |
None |
Accepted for authoring but not emitted by _compile_action (reserved). |
asserts |
list[AssertSpec] |
[] |
YAML key is the singular assert (mapped via populate_by_name). Each entry becomes one (assert (<template> …)) on the RHS. |
ThenBlock validator¶
_require_action_or_asserts enforces that at least one of action or a
non-empty assert list is provided. Rules may assert-only (no
__fathom_decision fact is emitted), decide-only, or do both.
AssertSpec fields¶
| Field | Type | Default | Description |
|---|---|---|---|
template |
str |
— | Must match ^[A-Za-z_][A-Za-z0-9_\-]*$. |
slots |
dict[str, str] |
{} |
Keys must be valid CLIPS identifiers. Values pass through _validate_slot_value: ?var refs must be well-formed, s-expressions must have balanced parens, and embedded NULs are rejected. |
Enums¶
ActionType¶
| YAML value | Meaning |
|---|---|
allow |
Permit the operation. |
deny |
Refuse the operation. |
escalate |
Forward for human/higher review. |
scope |
Narrow the action's scope. |
route |
Direct to a different handler/path. |
LogLevel¶
| YAML value | Emitted audit verbosity |
|---|---|
none |
No audit entry. |
summary |
Decision + rule id. |
full |
Decision + all facts. |
Salience convention¶
Fathom's fail-closed default is deny rules at lower salience than
allow rules, so deny fires last and wins under last-write-wins on
the decision fact. Mechanics in
Runtime & Working Memory.
CLIPS emission¶
compile_rule composes the rule in this fixed order: header,
(declare (salience N)) (only when non-zero), pattern CEs in when
order, test CEs collected from every pattern, the => arrow, the
__fathom_decision assert (only when action is set), then user
asserts in declared order. Indentation is four spaces.
YAML input¶
- name: deny_large_transfer
salience: -10
when:
- template: transfer
alias: $t
conditions:
- slot: amount
bind: ?amt
expression: greater_than(100)
- slot: currency
bind: ?ccy
- test: (blocked-country ?amt)
then:
action: deny
reason: "Transfer of {amt} {ccy} exceeds limit"
notify: [compliance, ops]
attestation: true
assert:
- template: audit-log
slots:
subject: "?amt"
CLIPS output¶
(defrule finance::deny_large_transfer
(declare (salience -10))
(transfer (amount ?amt&?s_amount&:(> ?s_amount 100)) (currency ?ccy))
(test (blocked-country ?amt))
=>
(assert (__fathom_decision
(action deny)
(reason (str-cat "Transfer of " ?amt " " ?ccy " exceeds limit"))
(rule "finance::deny_large_transfer")
(log-level summary)
(notify "compliance, ops")
(attestation TRUE)
(metadata "")))
(assert (audit-log (subject ?amt))))
Notes on the shape:
(declare (salience N))is omitted whensalience == 0.- Test CEs appear after all pattern CEs, in source order across patterns.
reasonwith{placeholder}placeholders becomes(str-cat …); literal reasons are emitted as plain quoted strings.notifyis always quoted — empty list emits"".metadatais empty-string when{}, otherwisejson.dumps(metadata, sort_keys=True).- The
__fathom_decisionassert precedes user asserts in document order (AC-1.3). - When
actionisNone, the decision assert is skipped entirely and only user asserts are emitted.
Validators — what is rejected¶
Model- or compile-time errors you will hit:
- Empty
nameor invalid CLIPS identifier —ValueError/CompilationError. - Empty
when—CompilationErrorfromcompile_rule. - Unsupported operator in an
expression—CompilationErrorfrom_compile_condition. AssertSpec.templateor slot key that is not a valid CLIPS identifier —ValueError.- Slot value with unbalanced parens, malformed
?var, or embedded\x00—ValueErrorfrom_validate_slot_value. ThenBlockwith neitheractionnor a non-emptyassertlist —ValueErrorfrom_require_action_or_asserts.ConditionEntryrejections listed under What the validator rejects.