Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Decision Guides

This page answers: when faced with multiple ways to express something, how do I pick?

Each guide is a short decision tree. Follow the tree to reach a recommendation; cross-link explains why.

derive vs compute vs query

What's the output?

├── A new fact (or facts) that the kernel should treat as derived,
│   visible to other rules and queries
│   → use `derive`
│
├── A scalar / record / collection computed from inputs,
│   not stored as a fact in the knowledge graph
│   → use `compute`
│
└── A retrieval that answers "what facts match this shape, right now?"
    → use `query`

A derive rule’s head facts participate in saturation and become inputs to other rules. A compute returns a value at the call site without entering the fact base. A query projects existing facts (asserted or derived) into a result set without producing new facts.

See RFD-0007, RFD-0006.

Which compute form?

Is the body a single expression?

├── Yes
│   → Form 1: `compute f(args) = expr`
│
└── No — needs multiple statements
    │
    ├── Per-tenant customization required without a Rust rebuild?
    │   → Form 2: `compute f(args) { input ... body ... out ... }`
    │
    ├── Native library access or unbounded performance flexibility needed?
    │   → Form 3: `compute f(args) -> R { ... impl rust("path::to::fn") }`
    │
    └── Otherwise (multi-step but tier-bounded math)
        → Form 2 (default): inline body in the constraint sublanguage

Form 2 is the recommended default for non-trivial computes. Form 3 is the escape hatch when Form 2 isn’t expressive enough; it carries a Rust-rebuild cost.

See RFD-0006, ch02-05.

query vs mutation

Does this operation change kernel state?

├── No — pure retrieval, no side effects
│   → `query`
│
└── Yes — asserts new facts, retracts existing facts, or emits events
    → `mutation`

A query is read-only: it never changes the fact base. A mutation runs require / do / retract / emit clauses against the current state and returns a result. Mutations carry transition-trace provenance; queries carry derivation provenance.

See RFD-0007.

frame vs fixture vs world (world is deferred — see backlog)

Where does this test scaffolding apply?

├── To this single test only — diagnostics scoped, no reuse
│   → `fixture { ... }` inline in the test
│
└── To multiple tests — composable mini-vocabulary
    → `pub frame F { ... }` declaration; tests reference via `using F`

Fixtures are inline mini-modules elaborated under capture mode; their diagnostics don’t leak to the parent. Frames are reusable; multiple tests can using F and they compose with conflict detection (OE0214-OE0218).

See ch03-03, RFD-0008.

When to escalate tier

Is the rule expressible in the structural / closure tier?

├── Yes
│   → Default tier (no `#dec` needed)
│
└── No
    │
    ├── Recursion needed?
    │   → `#dec(tier:recursive)`
    │
    ├── Negation, disjunction, complex quantification?
    │   → `#dec(tier:expressive)`
    │
    ├── Full FOL needed (existential quantification, complex Skolemization)?
    │   → `unsafe logic { ... }` — carries `tier:fol`
    │
    ├── Modal (necessity / possibility) reasoning?
    │   → `#dec(tier:modal)` — `box()` / `diamond()` operators
    │
    └── Multi-level types (a metatype's instances are themselves types)?
        → `#dec(tier:mlt)` — escape hatch for foundational-ontology bootstrapping

The tier ladder is graduated by evaluation cost. Default is fast and ergonomic; escalation costs decidability and runtime predictability. Recognized shapes (transitive, symmetric, etc.) inhabit the recognized tier regardless of syntactic surface — see RFD-0004.

See tier-selection.md, ch05-03.

where clause vs decorator

What kind of constraint?

├── Structural — references only the concept's own fields and ancestors
│   → `where { ... }` clause in the concept body
│
└── Algebraic — relation property like transitivity, symmetry, asymmetry
    → `@[decorator]` on the relation declaration

where clauses lower to structural validation rules. Decorators lower to recognized shapes that the reasoner dispatches to specialized fast-paths.

See RFD-0018.

Functor module vs pattern vs nothing (one-off concepts)

How much structure do you need to reuse?

├── A single declaration shape, parameterized
│   → Just write the concept directly, possibly with one or two parameters
│
├── A small recurring shape (3–10 declarations) that pairs related concepts
│   → `pattern` declaration (RFD-0019)
│
└── A whole module's worth of declarations parameterized by other modules
    → Functor module (RFD-0009)

See RFD-0019, RFD-0009.

assert vs pub declaration

Are you stating a fact about an individual, or declaring a type/concept/rule?

├── Fact about an individual (ABox)
│   → `assert Person(alice)` or similar
│
└── Type, concept, relation, property, rule, etc. (TBox)
    → `pub <metatype> Foo ...` etc.

ABox assertions become axiom events in the kernel’s event log (see RFD-0021). TBox declarations become schema events.

Set vs List vs Map vs Optional[Set] — which collection?

What's the shape?

├── 0 or 1 value
│   → `Optional[T]` (sugared `T?`)
│
├── Many values, no order, no duplicates, membership-driven
│   → `Set[T]`
│
├── Many values, ordered, indexable, duplicates meaningful
│   → `List[T]`
│
├── Key-value lookup, K orderable
│   → `Map[K, V]`
│
└── 0 or many values where presence-of-the-set matters
    (an absent set is distinct from an empty set)
    → `Optional[Set[T]]`

The last case is rare but real: an absent set means “this field has not been determined” while an empty set means “this field is determined to have no elements.” If the model doesn’t distinguish the two readings, use bare Set[T] and let an empty set carry both meanings.

For relation-shaped fields (a building’s tenants; an organisation’s employees), [T; <bound>] is the conventional surface; collection-typed Set[T] works when you want method-call access on the field’s value.

See ch02-08, RFD-0039.

Comprehension vs method chain

Is the projection a small expression (field access, arithmetic, constructor)?

├── Yes
│   → comprehension: `[u.number for u in b.units where pred(u)]`
│
└── No — each step has a name worth preserving
    → method chain: `b.units.filter(pred).map(unit_to_number).filter(...)`

Comprehensions read closer to the intent when the projection is small. Method chains preserve named intermediates. They desugar to the same form.

See ch02-08, idioms.md.

See also