## index.md

# Fathom

Fathom is a deterministic reasoning runtime for AI agents, built on CLIPS via
clipspy. Rules are authored in YAML, compiled to CLIPS constructs, and
evaluated in microseconds with auditable traces.

Current release: `fathom-rules` 0.11.0 (requires Python 3.12+). <!-- x-release-please-version -->

## Start here

- [Getting Started](getting-started.md) — install, first template, first rule,
  first evaluation.
- [Tutorial 1 — Hello-world policy](tutorials/hello-world.md) — the same
  material, guided step by step.

## Documentation by quadrant

The docs follow the [Diátaxis](https://diataxis.fr) framework. Pick the
quadrant that matches what you're doing.

### Learn — Tutorials

- [Hello-world policy](tutorials/hello-world.md)
- [Modules and salience](tutorials/modules-and-salience.md)
- [Working memory across evaluations](tutorials/working-memory.md)

### Solve a task — How-to Guides

- [Writing rules](how-to/writing-rules.md)
- [Integrating with FastAPI](how-to/fastapi.md)
- [Using the CLI](how-to/cli.md)
- [Registering a Python function](how-to/register-function.md)
- [Loading a rule pack](how-to/load-rule-pack.md)
- [Embedding via SDK](how-to/embed-sdk.md)
- [Hot-reloading rulesets](how-to/hot-reload.md)
- [Verifying a signed release](how-to/release-signing.md)
- [Running Policy Studio](how-to/policy-studio.md)

### Understand — Concepts

- [Five Primitives](concepts/five-primitives.md)
- [Runtime and Working Memory](concepts/runtime-and-working-memory.md)
- [YAML Compilation](concepts/yaml-compilation.md)
- [Audit and Attestation](concepts/audit-attestation.md)
- [CLIPS Features Not in v1](concepts/not-in-v1.md)

### Look up — Reference

- [Configuration](reference/configuration.md) — every `FATHOM_*` variable
- YAML: [Template](reference/yaml/template.md) ·
  [Rule](reference/yaml/rule.md) ·
  [Module](reference/yaml/module.md) ·
  [Function](reference/yaml/function.md) ·
  [Fact](reference/yaml/fact.md)
- APIs: [REST](reference/rest/index.md) ·
  [gRPC](reference/grpc/index.md) ·
  [MCP Tools](reference/mcp/index.md)
- [CLI](reference/cli/index.md)
- SDKs: [Python](reference/python-sdk/index.md) ·
  [Go](reference/go-sdk/index.md) ·
  [TypeScript](reference/typescript-sdk/index.md)
- [Rule Packs](reference/rule-packs/owasp-agentic.md)
- [Planned Integrations](reference/planned-integrations.md)

## Stability

Fathom is pre-1.0. `fathom.__all__` is the covered public surface, and
[VERSIONING.md](https://github.com/KrakenNet/fathom/blob/main/VERSIONING.md)
states what a `0.x` minor and patch each mean for it, the YAML keys, the
REST/gRPC/MCP contracts, and the CLI.

## What is not in v1

Fathom v1 is forward-chaining only and deliberately narrow. Backward chaining,
COOL (CLIPS object system), and message handlers are out of scope for this
release. See [CLIPS Features Not in v1](concepts/not-in-v1.md) for the full
list and the rationale.


## getting-started.md

# Fathom -- Getting Started

Fathom is a deterministic reasoning runtime built on CLIPS via clipspy. Define rules in YAML, evaluate in microseconds, get auditable results with zero hallucinations.

## Installation

Requires Python 3.12 or later.

```bash
uv add fathom-rules
```

The core dependencies are `clipspy`, `pyyaml`, and `pydantic` -- they install automatically with the package. No other external packages are required.

## Create Your First Template

Templates define the shape of facts. Create a file `templates/agent.yaml`:

```yaml
templates:
  - name: agent
    description: "An AI agent requesting access or action"
    slots:
      - name: id
        type: string
        required: true
      - name: clearance
        type: symbol
        required: true
        allowed_values: [unclassified, cui, confidential, secret, top-secret]
      - name: purpose
        type: symbol
        required: true
      - name: session_id
        type: string
        required: true

  - name: data_request
    description: "A request to access a data source"
    slots:
      - name: agent_id
        type: string
        required: true
      - name: target
        type: string
        required: true
      - name: classification
        type: symbol
        required: true
      - name: action
        type: symbol
        allowed_values: [read, write, delete]
        default: read
```

Each slot has a `type` (`string`, `symbol`, `float`) and optional constraints (`required`, `allowed_values`, `default`). Fathom validates facts against these schemas at assertion time.

## Register a Classification Hierarchy

To compare clearance against data classification we need an ordered set of levels. Declare it in `hierarchies/clearance.yaml`:

```yaml
name: clearance
levels:
  - unclassified
  - cui
  - confidential
  - secret
  - top-secret
```

Then tie the hierarchy to the rule operators with a classification function in `functions/clearance.yaml`:

```yaml
functions:
  - name: clearance
    type: classification
    params: [a, b]
    hierarchy_ref: clearance.yaml
```

Loading this function registers the `below(...)`, `meets_or_exceeds(...)`, and `within_scope(...)` operators so rules can compare levels.

## Write Your First Rule

Rules define pattern-matching logic that fires when facts match conditions. Create `rules/access-control.yaml`:

```yaml
ruleset: access-control
version: "1.0"
module: governance

rules:
  - name: deny-insufficient-clearance
    description: "Deny access when agent clearance is below data classification"
    salience: 10
    when:
      - template: data_request
        alias: $data
        conditions:
          - slot: classification
            expression: "meets_or_exceeds(unclassified)"
      - template: agent
        conditions:
          - slot: id
            expression: "equals($data.agent_id)"
          - slot: clearance
            expression: "below($data.classification)"
    then:
      action: deny
      reason: "Agent clearance is below the data classification (no read up)"
      log: full
```

`when` is a **list** of fact patterns. Each pattern names a `template`, an optional `alias` (used for cross-fact references like `$data.classification`), and a list of `conditions`. Each condition pairs a `slot` with an `expression` -- a literal operator like `equals(...)` / `in([...])`, or a classification operator like `below(...)` / `meets_or_exceeds(...)`. **Pattern ordering matters**: the pattern that binds an alias must appear before any pattern that references it, so `data_request` (alias `$data`) is listed before the `agent` pattern that uses `$data.classification`.

`salience` controls firing priority -- higher values fire first. The evaluator uses last-write-wins on `__fathom_decision` facts, so the rule firing LAST wins. Deny rules should have LOWER salience than allow rules so they fire last and override any prior allow, enforcing fail-closed behavior.

## Set Up Modules

Modules namespace rules into isolated groups with controlled execution order. Create `modules/modules.yaml`:

```yaml
modules:
  - name: governance
    description: "Action-level governance rules"

focus_order: [governance]
```

The `focus_order` list determines evaluation sequence -- when you add more modules, rules in earlier modules fire before rules in later ones. Each rule file declares which module it belongs to via the `module` field.

## Run Your First Evaluation

```python
from fathom import Engine

engine = Engine()
engine.load_templates("templates/")
engine.load_modules("modules/")
engine.load_functions("functions/")
engine.load_rules("rules/access-control.yaml")

engine.assert_fact("agent", {
    "id": "agent-alpha",
    "clearance": "secret",
    "purpose": "threat-analysis",
    "session_id": "sess-001"
})

engine.assert_fact("data_request", {
    "agent_id": "agent-alpha",
    "target": "hr_records",
    "classification": "top-secret",
    "action": "read"
})

result = engine.evaluate()

print(result.decision)     # "deny"
print(result.reason)       # "Agent clearance is below the data classification (no read up)"
print(result.rule_trace)   # ["governance::deny-insufficient-clearance"]
print(result.module_trace) # ["governance"]
print(result.duration_us)  # ~90 (microseconds; varies by machine)
```

`load_functions` must run before `load_rules`, because the rules reference the `below(...)` operator the classification function registers. Getting that order wrong fails inside CLIPS, on the rule rather than on the order — `engine.load_pack_dir(".")` loads all four in the right sequence for you, and [Loading a rule pack](how-to/load-rule-pack.md) covers it. The full project layout is:

```
templates/agent.yaml          agent + data_request schemas
hierarchies/clearance.yaml     ordered classification levels
functions/clearance.yaml       registers below(...) / meets_or_exceeds(...)
modules/modules.yaml           single 'governance' module
rules/access-control.yaml      the deny rule
```

The agent's `secret` clearance is below the request's `top-secret` classification, so the deny rule fires. If the agent's clearance instead met or exceeded the classification, no deny rule would match and the engine would fall through to its fail-closed default (`decision = "deny"`, `reason = "default decision (no rules fired)"`). To grant access you would add an explicit allow rule with higher salience -- see [example 03](https://github.com/KrakenNet/fathom/tree/main/examples/03-classification-blp) for the full read/write Bell-LaPadula pattern.

The result object contains five fields:

- `decision` -- the action taken (`allow`, `deny`, `escalate`, etc.)
- `reason` -- human-readable explanation with interpolated variables
- `rule_trace` -- ordered list of rules that fired, prefixed by module name
- `module_trace` -- ordered list of modules that were evaluated
- `duration_us` -- evaluation time in microseconds

Working memory persists across evaluations within a session. This enables cumulative reasoning ("agent accessed PII from 3 sources -- deny the 4th") and temporal patterns that stateless engines cannot express.

## Next Steps

- [Tutorial 1 -- Hello-world policy](tutorials/hello-world.md) -- the guided version of this same material
- [Five Primitives](concepts/five-primitives.md) -- templates, facts, rules, modules, and functions in depth
- [YAML Rule reference](reference/yaml/rule.md) -- operators, conditions, actions, and rule packs
- [Runtime and Working Memory](concepts/runtime-and-working-memory.md) -- session state, queries, cumulative reasoning
- [Planned Integrations](reference/planned-integrations.md) -- library, sidecar, MCP tool, and framework adapters

## Do Not

- Do not skip template definitions and assert untyped facts -- Fathom validates at assertion time.
- Do not set deny rules to higher salience than allow rules -- deny must have lower salience so it fires last and wins under last-write-wins (fail-closed).
- Do not assume working memory resets between `evaluate()` calls within the same session -- it persists.
- Do not use raw CLIPS constructs when YAML equivalents exist -- YAML is the primary authoring interface.


## tutorials/index.md

# Tutorials

1. [Hello-world policy](hello-world.md) — install Fathom, write your first rule, evaluate a fact.
2. [Modules & salience](modules-and-salience.md) — split rules across modules, control execution order.
3. [Working memory across evaluations](working-memory.md) — persist facts between `evaluate()` calls.

Each tutorial takes ~20 minutes and assumes no prior CLIPS or Fathom knowledge.


## tutorials/hello-world.md

# Hello-world policy

In this tutorial you'll install Fathom, define one template and one rule in YAML, load them into an engine, assert a fact, and read the evaluation result that comes back.

## 1. Install

```bash no-verify
pip install fathom-rules
```

## 2. Define a template

A template is the schema for a fact. Save this as `agent.yaml`:

```yaml
templates:
  - name: agent
    slots:
      - name: id
        type: string
        required: true
      - name: clearance
        type: symbol
        required: true
        allowed_values: [public, confidential, secret]
```

## 3. Define a module

Rules in Fathom must belong to a named module so that the engine knows how to prioritize them.
`Compiler.compile_module` (in `src/fathom/compiler.py`) emits the CLIPS `defmodule` construct; modules are
loaded into the engine with `Engine.load_modules`.

Save as `modules.yaml`:

```yaml
modules:
  - name: governance
    description: Access-control governance layer
focus_order:
  - governance
```

## 4. Define a rule

Save this as `rules.yaml`:

```yaml
ruleset: demo
version: "1.0"
module: governance
rules:
  - name: allow-public
    when:
      - template: agent
        conditions:
          - slot: clearance
            expression: "equals(public)"
    then:
      action: allow
```

## 5. Load and evaluate

```python no-verify
from fathom.engine import Engine

engine = Engine()
engine.load_templates("agent.yaml")
engine.load_modules("modules.yaml")
engine.load_rules("rules.yaml")

engine.assert_fact("agent", {"id": "a-1", "clearance": "public"})
result = engine.evaluate()

print(result.decision)      # -> "allow"
print(result.rule_trace)    # -> [governance::"allow-public"]
```

The `no-verify` tag skips snippet execution because the install step and file paths aren't part of the test harness. The engine calls themselves are verified in [working memory](working-memory.md), which builds on this example with a real in-memory path.

## What just happened?

- Fathom compiled your YAML to CLIPS constructs via `fathom.compiler.Compiler` and loaded them into an embedded CLIPS environment.
- Your fact matched the condition `clearance == public`, the rule fired, and the rule's `then.action: allow` became the decision on the evaluation result.
- The `EvaluationResult` captures which rules fired (`rule_trace`), the final `decision`, and the evaluation duration — see [Audit & Attestation](../concepts/audit-attestation.md).

## Next

- [Modules & salience](modules-and-salience.md) — add a deny rule with lower salience so it fires last and wins.


## tutorials/modules-and-salience.md

# Modules & salience

This tutorial builds on [Hello-world policy](hello-world.md). You will add a
deny rule alongside an allow rule, tune their salience so the deny always wins,
and confirm the fail-closed outcome.

## How salience and last-write-wins interact

Fathom compiles your YAML to CLIPS via `fathom.compiler.Compiler`. CLIPS fires
eligible rules in salience order — the **highest** salience rule fires **first**.
The `Evaluator` uses a **last-write-wins** strategy: the final
`__fathom_decision` fact asserted into working memory becomes the result that
`Engine.evaluate` returns as `EvaluationResult.decision`.

Put those two facts together and the fail-closed design becomes clear:

- Give the **allow** rule a **high** salience (e.g. 100) so it fires first.
- Give the **deny** rule a **low** salience (e.g. 10) so it fires after.
- Because the deny fact is written last, it overwrites the allow fact and wins.

`RuleDefinition.salience` (in `src/fathom/models.py`) is an `int` that defaults
to `0`. Any positive integer is valid.

## 1. Install

```bash no-verify
pip install fathom-rules
```

## 2. Define a template

Save as `agent.yaml`:

```yaml no-verify
templates:
  - name: agent
    slots:
      - name: id
        type: string
        required: true
      - name: clearance
        type: symbol
        required: true
        allowed_values: [public, confidential, secret]
```

## 3. Define a module

Rules in Fathom must belong to a named module. `Compiler.compile_module`
(in `src/fathom/compiler.py`) emits the CLIPS `defmodule` construct; modules are
loaded into the engine with `Engine.load_modules`.

Save as `modules.yaml`:

```yaml no-verify
modules:
  - name: governance
    description: Access-control governance layer
focus_order:
  - governance
```

The optional `focus_order` list tells the engine which modules to activate and
in what order: the first name runs first. It maps straight onto the CLIPS
`(focus ...)` command, which gives the focus to its first argument and queues
the rest behind it. Omit it and the engine focuses every declared module in
declaration order, which keeps a pack working but leaves the order to depend
on how the files happen to be written — write it out whenever the sequence
carries meaning.

Every listed module runs — an earlier one does not short-circuit the ones
after it. Because decisions are last-write-wins, the module listed last is
the one whose decision survives.

## 4. Define two rules with different salience

Both rules match an agent whose `clearance` slot equals `public`. The allow
rule fires first (`salience: 100`); the deny rule fires second (`salience: 10`)
and overwrites the allow decision via last-write-wins.

Save as `rules.yaml`:

```yaml no-verify
module: governance
rules:
  - name: allow-public
    salience: 100
    when:
      - template: agent
        conditions:
          - slot: clearance
            expression: "equals(public)"
    then:
      action: allow

  - name: deny-public
    salience: 10
    when:
      - template: agent
        conditions:
          - slot: clearance
            expression: "equals(public)"
    then:
      action: deny
      reason: "Public clearance is not sufficient"
```

## 5. Load, assert, and evaluate

The Python block below writes the three YAML definitions to a temporary
directory, loads them in the required order (templates → modules → rules),
asserts a fact, and verifies the deny outcome.

```python
import pathlib, tempfile
from fathom.engine import Engine

TEMPLATES_YAML = """
templates:
  - name: agent
    slots:
      - name: id
        type: string
        required: true
      - name: clearance
        type: symbol
        required: true
        allowed_values: [public, confidential, secret]
"""

MODULES_YAML = """
modules:
  - name: governance
    description: Access-control governance layer
focus_order:
  - governance
"""

RULES_YAML = """
module: governance
rules:
  - name: allow-public
    salience: 100
    when:
      - template: agent
        conditions:
          - slot: clearance
            expression: "equals(public)"
    then:
      action: allow

  - name: deny-public
    salience: 10
    when:
      - template: agent
        conditions:
          - slot: clearance
            expression: "equals(public)"
    then:
      action: deny
      reason: "Public clearance is not sufficient"
"""

with tempfile.TemporaryDirectory() as tmp:
    d = pathlib.Path(tmp)
    (d / "agent.yaml").write_text(TEMPLATES_YAML)
    (d / "modules.yaml").write_text(MODULES_YAML)
    (d / "rules.yaml").write_text(RULES_YAML)

    engine = Engine()
    engine.load_templates(str(d / "agent.yaml"))
    engine.load_modules(str(d / "modules.yaml"))
    engine.load_rules(str(d / "rules.yaml"))

    engine.assert_fact("agent", {"id": "a-1", "clearance": "public"})
    result = engine.evaluate()

    assert result.decision == "deny", f"expected deny, got {result.decision!r}"
    assert "governance::deny-public" in result.rule_trace, (
        f"rule_trace was {result.rule_trace}"
    )
```

Both rules match. `allow-public` fires first (salience 100) and writes an allow
fact. `deny-public` fires second (salience 10) and writes a deny fact. Because
the evaluator uses last-write-wins, the deny fact is the winner.

`result.decision` is `"deny"` and `result.rule_trace` includes
`"governance::deny-public"` (rules are recorded as `module::rule_name`).

## What just happened?

- **Modules** give rules a namespace. `Compiler.compile_module` emits
  `(defmodule governance (import MAIN ?ALL))` so governance rules can reference
  the internal decision template defined in `MAIN`.
- **Focus stack** — `Compiler.compile_focus_stack(["governance"])` produces
  `(focus governance)`. The evaluator pushes that onto the CLIPS agenda before
  running the forward-chain cycle.
- **Salience** controls firing order. Lower-salience rules fire later. The last
  decision fact written wins, so the deny rule at salience 10 overrides the
  allow rule at salience 100. This is the fail-closed design: allow rules can
  only succeed when no deny rule fires after them.
- `EvaluationResult.decision` and `EvaluationResult.rule_trace` (defined in
  `src/fathom/models.py`) capture the outcome.

## Next

- Explore [Working memory](working-memory.md) to see how facts persist across
  multiple evaluations within a session.


## tutorials/working-memory.md

# Working memory across evaluations

This tutorial builds on [Modules & salience](modules-and-salience.md). You will
assert facts in two separate steps, call `Engine.evaluate` after each step, and
confirm that the second evaluation sees facts from both steps. You will also
learn how to clear working memory when a session is done.

## Why this matters

Systems like OPA and Cedar are stateless: every evaluation starts from a blank
slate. Fathom is different — **facts asserted into a `Engine` instance persist
across `evaluate()` calls** for the lifetime of that instance. This is the
core design choice that makes Fathom useful for session-level reasoning: rules
can accumulate evidence across many events before reaching a conclusion.

A concrete example: an access-control policy that counts API calls made in the
last minute. Each `assert_fact` adds a new event to working memory. A rate rule
checks whether the total count exceeds a threshold. The count grows across
evaluations — no external state store required.

## How working memory works

When you call `engine.assert_fact(template, data)`, the fact is written into
the embedded CLIPS environment that `Engine` wraps. That environment is stateful:
facts remain until you explicitly retract them or reset the environment.
`engine.evaluate()` runs the forward-chain cycle against *all* facts currently
in working memory — not just the ones asserted since the last call.

## Demonstration

The block below proves persistence with a rule that requires **two facts** — one
asserted in the first step, one in the second. That combined rule can only fire
on the second evaluation because it needs both facts in working memory at once.

1. Loads a template and rules into a single `Engine`.
2. Asserts `agent a-1` (`role: "requester"`), evaluates — the combined rule
   cannot fire yet because `agent a-2` is missing; only the single-fact rule fires.
3. Asserts `agent a-2` (`role: "approver"`), evaluates again — *both* facts are
   now in working memory, the combined rule fires.

```python
import pathlib, tempfile
from fathom.engine import Engine

TEMPLATES_YAML = """
templates:
  - name: agent
    slots:
      - name: id
        type: string
        required: true
      - name: role
        type: symbol
        required: true
        allowed_values: [requester, approver]
"""

MODULES_YAML = """
modules:
  - name: access
    description: Access-control module
focus_order:
  - access
"""

RULES_YAML = """
module: access
rules:
  - name: allow-requester-alone
    salience: 10
    when:
      - template: agent
        conditions:
          - slot: role
            expression: "equals(requester)"
    then:
      action: allow
      reason: "requester present"

  - name: allow-dual-approval
    salience: 20
    when:
      - template: agent
        conditions:
          - slot: role
            expression: "equals(requester)"
      - template: agent
        conditions:
          - slot: role
            expression: "equals(approver)"
    then:
      action: allow
      reason: "dual approval confirmed"
"""

with tempfile.TemporaryDirectory() as tmp:
    d = pathlib.Path(tmp)
    (d / "agent.yaml").write_text(TEMPLATES_YAML)
    (d / "modules.yaml").write_text(MODULES_YAML)
    (d / "rules.yaml").write_text(RULES_YAML)

    engine = Engine()
    engine.load_templates(str(d / "agent.yaml"))
    engine.load_modules(str(d / "modules.yaml"))
    engine.load_rules(str(d / "rules.yaml"))

    # --- First evaluation: only the requester fact is in working memory ---
    engine.assert_fact("agent", {"id": "a-1", "role": "requester"})
    result1 = engine.evaluate()

    assert "access::allow-dual-approval" not in result1.rule_trace, (
        f"dual-approval should not fire yet, got {result1.rule_trace}"
    )

    # --- Second evaluation: approver fact is added; requester fact persists ---
    engine.assert_fact("agent", {"id": "a-2", "role": "approver"})
    result2 = engine.evaluate()

    # allow-dual-approval fires because BOTH facts are in working memory:
    # the requester fact asserted in step 1 is still present.
    assert "access::allow-dual-approval" in result2.rule_trace, (
        f"allow-dual-approval should fire on second evaluation, "
        f"got {result2.rule_trace}"
    )
    assert result2.reason == "dual approval confirmed", (
        f"unexpected reason: {result2.reason!r}"
    )
```

The key proof is `result2.rule_trace`: `allow-dual-approval` can only fire when
both a requester fact **and** an approver fact exist. The requester fact was
asserted in the first step, never retracted, and therefore still present when
the second evaluation runs. A stateless system would see only the approver fact
on the second call and the combined rule would never fire.

## Contrast with stateless systems

| | Fathom | OPA / Cedar |
|---|---|---|
| Facts between calls | Persist until retracted or reset | Discarded after each evaluation |
| Session-level accumulation | Built-in | Requires an external store |
| Rate / count policies | Native (working memory grows) | Must pass full history on every call |

## Clearing working memory

Two methods let you start fresh without constructing a new `Engine`:

### `engine.clear_facts()`

Retracts all user-asserted facts from every registered template. Internal CLIPS
facts (the decision template, `initial-fact`) are left intact. Rules and
templates remain loaded — only the data changes.

Use this when you want to start a new session but keep the same rule set.

```python no-verify
engine.clear_facts()
# Working memory is now empty; templates and rules are still loaded.
result = engine.evaluate()  # No facts → default decision ("deny")
```

### `engine.reset()`

Calls the underlying `clips.Environment.reset()`, which clears **all** facts
(including internal ones) and re-asserts `(initial-fact)`. The `__fathom_decision`
template is rebuilt automatically. Deftemplates, defmodules, and defrules survive
the reset — only facts are cleared.

Use this for a full session reset that mirrors starting a new CLIPS environment
while keeping compiled constructs.

```python no-verify
engine.reset()
```

### When to create a new `Engine`

If you need to change the rule set — load different templates, modules, or rules —
construct a new `Engine`. The current version does not support unloading individual
constructs. `clear_facts()` and `reset()` only affect data, not compiled constructs.

## What just happened?

- `Engine` wraps a single `clips.Environment` instance. That environment is
  stateful: `assert_fact` writes a CLIPS fact that persists until removed.
- `evaluate()` runs `env.run()` to quiescence each time. It does not reset the
  environment first, so all previously asserted facts participate in every run.
- `EvaluationResult.rule_trace` (defined in `src/fathom/models.py`) records
  every rule that fired during the run as `module::rule_name` strings.
- `clear_facts()` calls `FactManager.clear_all()`, which iterates the template
  registry and retracts each template's facts individually.
- `reset()` delegates to `clips.Environment.reset()`, which is the CLIPS standard
  reset — facts gone, constructs preserved.

## Next

- [Audit & Attestation](../concepts/audit-attestation.md) — every `evaluate()`
  call emits a structured JSON audit record; learn how to wire in a custom sink.


## how-to/index.md

# How-to Guides

- [Writing rules](writing-rules.md)
- [Integrating with FastAPI](fastapi.md)
- [Using the CLI](cli.md)
- [Registering a Python function](register-function.md)
- [Loading a rule pack](load-rule-pack.md)
- [Converting a Rego policy](convert-rego.md)
- [Serving an OPA client](opa-data-api.md)
- [Exporting rules as Rego](export-rego.md)
- [Embedding via SDK](embed-sdk.md)
- [Hot-reloading rulesets in a running server](hot-reload.md)
- [Verifying a signed release](release-signing.md)
- [Running Policy Studio](policy-studio.md)


## how-to/writing-rules.md

# Writing rules

Fathom rules are YAML files that compile to CLIPS `defrule` constructs. This guide covers
the full structure of a ruleset file, each field that the compiler accepts, and the
validation constraints enforced by the Pydantic models.

## Rule skeleton

Every rule file is a single YAML document that matches the `RulesetDefinition` model
(`src/fathom/models.py`).

```yaml
ruleset: access-control          # unique name — CLIPS identifier chars only
version: "1.0"                   # free string; no runtime effect
module: governance               # CLIPS module all rules belong to

rules:
  - name: deny-low-clearance
    description: "Deny when agent clearance is below data classification"
    salience: 10
    when:
      - template: agent
        conditions:
          - slot: clearance
            expression: equals(unclassified)
    then:
      action: deny
      reason: "Clearance insufficient"
```

The four top-level keys map directly to `RulesetDefinition` fields:

| Field | Type | Notes |
|-------|------|-------|
| `ruleset` | string | CLIPS identifier — `[A-Za-z_][A-Za-z0-9_-]*` |
| `version` | string | Defaults to `"1.0"` |
| `module` | string | CLIPS identifier; routes rules to a CLIPS module |
| `rules` | list | One or more `RuleDefinition` objects |

Each rule inside `rules` is a `RuleDefinition` and must supply `name`, `when`, and `then`.
`description` and `salience` are optional (salience defaults to `0`).

## `when` clauses: `FactPattern`

Each entry in `when` is a `FactPattern` (`src/fathom/models.py`). It matches a single
fact type in working memory.

```yaml
when:
  - template: data-request       # name of the deftemplate to match
    alias: $req                  # optional — names this pattern for $req.slot references
    conditions:
      - slot: action
        expression: equals(read)
```

| Field | Required | Notes |
|-------|----------|-------|
| `template` | yes | Must match a loaded `TemplateDefinition` name |
| `alias` | no | `$`-prefixed name for this pattern, referenced from another pattern's expression as `$alias.slot`. Must not be `$p<number>`, and no two patterns in a rule may share one |
| `conditions` | yes | List of `ConditionEntry` objects |

A rule with two `when` entries fires only when **both** facts exist simultaneously in
working memory — CLIPS evaluates the conjunction.

## `ConditionEntry` fields

`ConditionEntry` (`src/fathom/models.py`, line 203) represents one slot constraint
inside a `FactPattern`. At least one of `expression`, `bind`, or `test` must be present.

### `slot` + `expression`: value match

An `expression` is always one `operator(argument)` form — a bare value is
rejected by the model, not silently treated as `equals`.

```yaml
conditions:
  - slot: status
    expression: equals(active)
```

`slot` is required when `expression` is set. For the full operator list, the
argument forms (`$alias.slot` cross-references, `[a, b]` lists, literals), and
how a literal is emitted for each slot type, see the
[Rule reference](../reference/yaml/rule.md).

### `slot` + `bind`: capture a value

Use `bind` to capture a slot value into a CLIPS variable for use in other conditions or
the `then` block. The value must be `?` followed by a CLIPS identifier
(`^\?[A-Za-z_][A-Za-z0-9_\-]*$`) — the validator rejects anything else, because the
name is interpolated straight into the generated `defrule`.

```yaml
conditions:
  - slot: subject-id
    bind: "?sid"
```

`slot` is required when `bind` is set.

### `test`: standalone CLIPS expression

Use `test` for arbitrary CLIPS conditional elements — custom functions registered via
`Engine.register_function`, or any CLIPS built-in not in Fathom's operator allow-list.
The value **must be a parenthesized expression** (start with `(`, end with `)`).

```yaml
conditions:
  - test: "(my-fn ?sid)"
```

The compiler emits `(test (my-fn ?sid))` on the rule LHS after all slot patterns.

You can combine `bind` and `test` in the same `ConditionEntry` to both capture a slot
and run a test against it:

```yaml
conditions:
  - slot: subject-id
    bind: "?sid"
    test: "(> (string-length ?sid) 0)"
```

**Validator rules enforced by `ConditionEntry`:**

- `bind` must be `?` followed by a CLIPS identifier — e.g. `?sid`, not `sid` and
  not `?a b`.
- `expression` must be a single `operator(argument)` form — e.g. `equals(active)`,
  not `active`.
- `test` must be a parenthesized CLIPS expression — e.g. `(my-fn ?sid)`.
- `slot` must be present when `expression` or `bind` is set.
- Setting `slot` alongside `test` alone (no `expression` or `bind`) is **rejected** —
  the compiler has no slot position to emit; either add `expression`/`bind` or drop
  `slot`.
- At least one of `expression`, `bind`, or `test` must be set.
- Any key other than `slot`, `expression`, `bind`, and `test` is rejected. This
  holds for every model on this page: a misspelled field is an error, never a
  silently ignored line.

## `then` block: `ThenBlock`

The `then` block is a `ThenBlock` (`src/fathom/models.py`). It declares the decision and
any side effects when the rule fires.

```yaml
then:
  action: deny
  reason: "Subject ?sid is not authorized"
  log: full
  notify: [security-ops]
  attestation: true
  metadata:
    control: AC-3
  assert:
    - template: audit-record
      slots:
        subject: "?sid"
        outcome: denied
```

| Field | Type | Notes |
|-------|------|-------|
| `action` | `ActionType` or null | The decision outcome (see below) |
| `reason` | string | Human-readable explanation; defaults to `""` |
| `log` | `LogLevel` | `none`, `summary` (default), or `full` |
| `notify` | list of strings | Channel names to notify |
| `attestation` | bool | Whether to produce an attestation token |
| `metadata` | dict | Arbitrary string key-value pairs |
| `scope` | string or null | Scope qualifier for `scope` actions |
| `assert` | list of `AssertSpec` | Facts to assert when the rule fires |

Either `action` or a non-empty `assert` list is required — a `ThenBlock` with neither
is rejected by the model validator.

Note: in YAML files use the key `assert`; in Python you may use the attribute name
`asserts` (the model sets `populate_by_name=True`).

### `assert` examples

`AssertSpec` (`src/fathom/models.py`) lets rules write new facts into working memory.
Slot values starting with `?` are emitted as CLIPS variable references; values starting
with `(` are emitted as CLIPS s-expressions; all other values are emitted as quoted
string literals.

```yaml
assert:
  - template: decision
    slots:
      subject: "?sid"          # ?-prefixed → CLIPS variable reference
      outcome: "denied"        # plain string → CLIPS quoted literal
      score: "(compute-score ?sid)"  # (...) → CLIPS s-expression
```

Validators on `AssertSpec`:

- `template` must be a valid CLIPS identifier.
- Slot names must be valid CLIPS identifiers.
- `?`-prefixed values must be valid CLIPS variable references (e.g. `?sid`).
- `(`-prefixed values must have balanced parentheses.

## Salience overview

Salience is an integer on each `RuleDefinition` that controls firing order within a
module. Higher salience fires first. When salience is omitted it defaults to `0`.

Under Fathom's last-write-wins convention for the `__fathom_decision` fact, the rule
that fires **last** sets the final decision. This means **deny rules should be given
lower salience** so they fire after allow rules and their outcome wins. See
[Modules and salience](../tutorials/modules-and-salience.md) for a full worked example
and focus-stack ordering.

## Action values

The `action` field accepts any value from the `ActionType` enum
(`src/fathom/models.py`):

| Value | Meaning |
|-------|---------|
| `allow` | Permit the request |
| `deny` | Reject the request |
| `escalate` | Route to a human or higher-authority system |
| `scope` | Narrow the permission to a specific scope |
| `route` | Redirect to an alternate handler or service |


## how-to/fastapi.md

# Integrating with FastAPI

## What ships

Fathom includes a ready-made FastAPI application at
`src/fathom/integrations/rest.py`. The module-level object is named `app`
(a `FastAPI` instance). It exposes bearer-token-authenticated endpoints for
evaluation, compilation, and session inspection. For a full description of
every endpoint, request model, and response model, see
[REST API Reference](../reference/rest/index.md).

## Option A: Mount the bundled app

If you already have a FastAPI application and want to add Fathom under a
path prefix, import `app` from the integration module and mount it.

```python no-verify
from fastapi import FastAPI
from fathom.integrations.rest import app as fathom_app

app = FastAPI()
app.mount("/fathom", fathom_app)
```

After mounting, every Fathom endpoint is available under the `/fathom`
prefix — for example `/fathom/v1/evaluate` and `/fathom/v1/compile`. The
`FATHOM_API_TOKEN` and `FATHOM_RULESET_ROOT` environment variables must be
set before starting the server (see [Auth](#auth) below).

## Option B: Wrap Engine directly

For callers who need custom endpoints — different request shapes, streaming
responses, or tighter control over fact lifecycle — you can instantiate
`Engine` yourself and wire it into your own FastAPI routes.

```python no-verify
from fastapi import FastAPI
from fathom.engine import Engine
from fathom.models import FactInput

engine = Engine()
engine.load_templates("rules/templates")
engine.load_rules("rules/rulesets")

app = FastAPI()

@app.post("/evaluate")
def evaluate(fact: FactInput):
    engine.assert_fact(fact.template, fact.data)
    result = engine.evaluate()
    return {
        "decision": result.decision,
        "reason": result.reason,
        "rule_trace": result.rule_trace,
        "module_trace": result.module_trace,
        "duration_us": result.duration_us,
    }
```

`Engine.evaluate()` returns an `EvaluationResult` with the fields shown
above. The engine accumulates facts across calls in this example; call
`engine.clear_facts()` or `engine.reset()` between requests if you need
stateless evaluation.

For stateless, per-request evaluation, use the class method
`Engine.from_rules(path)` inside the route handler instead:

```python no-verify
from fastapi import FastAPI
from fathom.engine import Engine
from fathom.models import FactInput

app = FastAPI()

@app.post("/evaluate")
def evaluate(fact: FactInput):
    engine = Engine.from_rules("rules/")
    engine.assert_fact(fact.template, fact.data)
    result = engine.evaluate()
    return {"decision": result.decision, "rule_trace": result.rule_trace}
```

## Auth

The bundled `app` enforces bearer-token authentication on every endpoint
except `GET /health`. The implementation lives in
`src/fathom/integrations/auth.py` and works as follows:

1. Every protected endpoint declares `Depends(_require_auth)`.
2. `_require_auth` reads the `Authorization` HTTP header and calls
   `verify_token`.
3. `verify_token` expects the header value to be `Bearer <token>` and
   compares the presented token against `FATHOM_API_TOKEN` using a
   constant-time `hmac.compare_digest` to prevent timing attacks.
4. A missing, malformed, or incorrect token returns `401 Unauthorized`.

**Required environment variables when running the bundled app:**

| Variable | Purpose |
|---|---|
| `FATHOM_API_TOKEN` | Bearer token that clients must send in the `Authorization` header |
| `FATHOM_RULESET_ROOT` | Filesystem root that the server jails all ruleset paths under |

Set both before starting `uvicorn`:

```bash
export FATHOM_API_TOKEN="$(openssl rand -hex 32)"
export FATHOM_RULESET_ROOT="/var/lib/fathom/rulesets"
uvicorn fathom.integrations.rest:app --host 0.0.0.0 --port 8080
```

Clients pass the token in every request:

```bash
curl -H "Authorization: Bearer $FATHOM_API_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"ruleset": "access-control", "facts": [{"template": "request", "data": {"action": "read"}}]}' \
     http://localhost:8080/v1/evaluate
```

**Option B (Engine directly):** the bare `Engine` class has no built-in
auth. Add authentication via FastAPI middleware, an `HTTPBearer` dependency,
or a reverse proxy such as nginx or a cloud API gateway before exposing the
endpoint in production.

### OpenAPI / Swagger UI

API docs are disabled by default to avoid leaking route names to
unauthenticated callers. Set `FATHOM_EXPOSE_DOCS=1` to re-enable them for
local development:

```bash
FATHOM_EXPOSE_DOCS=1 uvicorn fathom.integrations.rest:app --reload
```

## Next

- [REST API Reference](../reference/rest/index.md) — full Swagger spec,
  request/response schemas, and error codes.
- [Python SDK Reference](../reference/python-sdk/index.md) — `Engine`
  constructor, `assert_fact`, `evaluate`, and the full public surface.


## how-to/cli.md

# Using the CLI

Install the CLI extra with `pip install fathom-rules[cli]`. The entry
point is the `fathom` command, built on Typer. To confirm installation,
print the runtime version with the global flag:

```shell
fathom --version
```

The short form `-V` works identically. The sections below cover every
sub-command, in the order they appear in `src/fathom/cli.py`, with a
worked example against one of the example rule packs shipped under
`examples/`.

## validate

Parse every YAML file under the given path and check that each document
is a well-formed Fathom template, module, rule, or function definition.

```shell
fathom validate examples/01-hello-allow-deny
```

Pass either a single file or a directory; the command walks directories
recursively and reports all parse and schema errors at once. It exits 0
on success, 1 on validation errors, and 2 when no YAML files are found.

## compile

Lower YAML definitions to the CLIPS constructs the engine actually
executes. Useful for debugging code generation or feeding constructs
into a raw CLIPS environment.

```shell
fathom compile examples/02-rbac-modules
```

The `--format` / `-f` option selects the output style: `raw` (the
default, a single flat string of valid CLIPS) or `pretty`, which inserts
newlines at the top-level paren boundaries so each construct sits on
its own line.

```shell
fathom compile examples/02-rbac-modules --format pretty
```

Literals are emitted according to the declared slot type, so `compile` needs
the templates in view: pointed at a directory it reads them from the pack, and
pointed at a single file under `rules/` it picks up the sibling `templates/`
directory. Without that context a `string` slot's literal is emitted bare —
CLIPS rejects that form with `[CSTRNCHK1]`, so the printed constructs would be
ones the engine never builds and that do not load.

## info

Load a rule pack and print a summary of everything the engine sees:
templates (with slot names and types), modules (with priority and the
configured focus order), rules (with salience), and registered
functions.

```shell
fathom info examples/03-classification-blp
```

Use `info` as a sanity check after editing a pack — if a template or
rule is missing from the listing, it did not compile into the engine.

## test

Run a YAML suite of test cases against a compiled rule pack. The
command takes two arguments: the rule pack directory and a test file
(or directory of test files).

```shell
fathom test examples/01-hello-allow-deny tests/cases.yaml
```

Each test file is a YAML list of cases. Every case recognises three
keys:

- `name` — a human-readable label printed in PASS/FAIL lines.
- `facts` — a list of fact specs, each with a `template` and a `data`
  mapping that the CLI asserts into a freshly reset engine.
- `expected_decision` — the decision string the evaluation must return
  for the case to pass.

```yaml
- name: admin can read
  facts:
    - template: subject
      data: { role: admin }
    - template: resource
      data: { kind: report }
  expected_decision: allow
```

The command exits non-zero if any case fails. It also exits non-zero,
rather than silently reporting success, when the suite could not be run at
all: when the test path contains no test cases (an empty directory, or files
that parse to nothing), and when a test file cannot be read or is not a list
of test cases. A suite that quietly matches zero cases used to exit 0, which
made a mis-typed path look like a passing run.

## bench

Measure evaluation latency for a rule pack. The benchmark resets the
engine between each iteration and reports p50, p95, p99, and mean
timings in microseconds.

```shell
fathom bench examples/04-temporal-anomaly
```

Two options tune the run:

- `--iterations` / `-n` — number of measured iterations (default
  `1000`).
- `--warmup` / `-w` — number of warmup iterations that run first and
  are excluded from the statistics (default `100`).

```shell
fathom bench examples/04-temporal-anomaly -n 5000 -w 500
```

## verify-chain

Offline-verify a hash-chained attestation log: every line's JWS
signature, the hash link to its predecessor, and the genesis record's
key fingerprint.

```shell
fathom verify-chain audit/chain.jsonl --pubkey audit/chain.jsonl.pub.pem
```

The `--pubkey` option is required and takes the Ed25519 public key PEM
that the log exports beside itself as `<log>.pub.pem`. Two optional
checks detect truncation, which a self-contained log cannot reveal:

- `--expected-head` — a line hash mirrored out-of-band; verification
  fails if it no longer appears in the log.
- `--anchor-token` — a checkpoint JWS token; the head it pins must
  appear in the log.

Pass `--json` to emit the verification result as JSON. The command
exits 0 when the chain is valid, 1 when verification fails, and 2 when
the log or key file cannot be read.

## repl

Start an interactive session for asserting facts and evaluating rules
by hand. Pass `--rules` / `-r` to preload a pack; without it, the REPL
starts with an empty engine.

```shell
fathom repl --rules examples/05-langchain-guardrails
```

Inside the REPL, these sub-commands are available:

- `assert <template> <json_data>` — assert a fact (the data argument
  is parsed as JSON).
- `evaluate` — run an evaluation and print decision, reason, and rule
  trace.
- `query <template>` — list facts whose template matches.
- `retract <template>` — retract all facts matching the template.
- `facts` — list every fact currently in working memory.
- `reset` — reset engine state.
- `help` — print the command list.
- `quit` / `exit` — leave the REPL.

Example session:

```text
fathom> assert subject {"role": "admin"}
Asserted subject fact.
fathom> evaluate
  decision: allow
  reason: admin override
fathom> quit
```

## convert rego

Translate the stateless subset of an OPA Rego policy into a Fathom pack.
Requires the `opa` binary, which does the parsing.

```shell
fathom convert rego authz.rego --out ./converted
```

Anything outside the convertible subset is reported on stderr with the
rule and the reason rather than approximated. The command exits non-zero
when nothing converted; `--strict` also fails when anything was skipped.
See [Converting a Rego policy](convert-rego.md) for the full mapping and
the list of refusals.

## convert to-rego

Export the stateless subset of a Fathom ruleset as Rego.

```shell
fathom convert to-rego ./my-ruleset -o policy.rego
```

Rules that join across facts, assert facts, or use a temporal or
classification operator have no Rego form and are reported on stderr
rather than written out. See [Exporting rules as Rego](export-rego.md).

## Full reference

For the complete flag matrix, exit codes, and error behaviour of each
command, see the generated reference pages:

- [CLI reference index](../reference/cli/index.md)
- [validate](../reference/cli/validate.md)
- [compile](../reference/cli/compile.md)
- [info](../reference/cli/info.md)
- [test](../reference/cli/test.md)
- [bench](../reference/cli/bench.md)
- [verify-chain](../reference/cli/verify-chain.md)
- [convert rego](../reference/cli/convert-rego.md)
- [convert to-rego](../reference/cli/convert-to-rego.md)
- [repl](../reference/cli/repl.md)


## how-to/register-function.md

# Registering a Python function

Sometimes a predicate or classifier is much easier to express in Python than in CLIPS —
regex matching, set overlap, temporal math, or an external lookup. Fathom lets you
register a plain Python callable once, then invoke it from the left-hand side of any
rule through the raw-CLIPS `test:` escape hatch on a `ConditionEntry`.

## Register the function

Call `Engine.register_function(name, callable)`. The callable becomes invocable from
CLIPS as `(name arg1 arg2 ...)`.

```python
from fathom.engine import Engine

engine = Engine()
engine.register_function("overlaps", lambda a, b: bool(set(a) & set(b)))
```

A few constraints worth knowing up front (see `src/fathom/engine.py`):

- `name` must be non-empty and match the regex `[A-Za-z][A-Za-z0-9_-]*`.
- `name` must not start with the reserved `fathom-` prefix — that namespace is
  reserved for builtins the engine registers itself.
- The callable takes positional arguments only.
- Re-registering an existing name overwrites the prior binding. This matches
  clipspy's semantics and is documented behaviour, not an error.

## Call it from a rule

`ConditionEntry.test` (defined in `src/fathom/models.py`) is the escape hatch for
calling user-registered functions from a rule's LHS. It emits a raw `(test <expr>)`
conditional element verbatim, so Fathom's operator allow-list does not apply — you
get the full CLIPS expression language, including any function you registered.

The value of `test` must be a parenthesized CLIPS expression. The Pydantic validator
rejects anything that does not both start with `(` and end with `)`.

```yaml
ruleset: demo
module: MAIN
rules:
  - name: route-shared-tag
    when:
      - template: request
        conditions:
          - slot: tags
            bind: ?req_tags
      - template: allowed
        conditions:
          - slot: tags
            bind: ?allowed_tags
          - test: "(overlaps ?req_tags ?allowed_tags)"
    then:
      action: allow
```

The `(fn-name ?arg1 ?arg2)` form is the convention: the function name comes first and
the arguments follow, all inside a single pair of parentheses. Bindings established
earlier in the `when:` block (here `?req_tags` and `?allowed_tags`) are in scope for
the test expression.

`test` may appear standalone in a `ConditionEntry` (no `slot`, `expression`, or
`bind`), in which case the pattern emits only the test CE. Combined with a slot
constraint, as above, both are emitted on the LHS.

## Name restrictions

`register_function` raises `ValueError` at registration time when the name violates
any of these rules:

- The name must be non-empty.
- The name must match `[A-Za-z][A-Za-z0-9_-]*` — an ASCII letter followed by letters,
  digits, underscores, or hyphens.
- The name must not start with the reserved `fathom-` prefix.

All three errors are raised synchronously by `register_function` before the callable
is handed to the underlying CLIPS environment, so misnamed registrations fail fast.

## When not to use this

Prefer the built-in YAML operators whenever the check can be expressed as a slot
comparison. A condition like `expression: equals(critical)` keeps the logic in YAML,
stays inside Fathom's operator allow-list, and is visible to static tooling.

Reach for `register_function` plus a `test:` clause only when you need Python that
CLIPS cannot express directly: regex matching, set membership or overlap, arithmetic
on timestamps, or calling out to an external service. Every custom function widens
your rule surface beyond what the allow-list can vet, so use it deliberately and
keep each registered callable small, pure, and deterministic.

## Related reading

- [Python SDK reference](../reference/python-sdk/index.md)
- [Writing rules](writing-rules.md)


## how-to/load-rule-pack.md

# Loading a rule pack

Fathom supports two ways to ship rules into an `Engine`: as a directory tree
you maintain alongside your application, or as an installable Python package
discovered at runtime through a setuptools entry point. Both routes funnel
through the same loaders, so the YAML you author looks identical either way —
only the delivery mechanism differs.

## Load from a directory

`Engine.from_rules(path, **kwargs)` is a classmethod that returns a configured
`Engine`. Any keyword arguments are forwarded to the `Engine(...)` constructor.

```python
from fathom.engine import Engine

engine = Engine.from_rules("examples/01-hello-allow-deny")
result = engine.evaluate()
```

The loader tries two discovery strategies, in order:

1. **Subdirectory convention** (preferred). If the pack directory contains any
   of `templates/`, `modules/`, `functions/`, or `rules/`, each present
   subdirectory is passed to the matching `engine.load_templates`,
   `load_modules`, `load_functions`, or `load_rules` method.
2. **Key-inspection fallback**. If none of those subdirectories exist, every
   `*.yaml` file directly under `path` is opened and routed by its top-level
   key: `templates` → templates loader, `modules` or `focus_order` → modules
   loader, `functions` → functions loader, `rules` or `ruleset` → rules
   loader.

Both strategies load in the same fixed order: **templates → modules →
functions → rules**. The order matters because templates define the slot
schemas that rules bind against, modules establish the namespaces that rules
live in, and functions may be referenced from a rule's left-hand side via the
raw-CLIPS `test:` clause — so everything a rule depends on must be compiled
before the rule itself.

## Directory layout

For anything larger than a toy example, use the subdirectory convention. The
reference shape is what `examples/01-hello-allow-deny` ships with:

```
my-pack/
├── templates/
│   └── *.yaml
├── modules/
│   └── *.yaml
├── functions/
│   └── *.yaml
└── rules/
    └── *.yaml
```

Every YAML file under a given subdirectory is loaded. The glob is `*.yaml`
only — files ending in `.yml` are **not** picked up by `from_rules`, so stick
to the long extension. Empty or missing subdirectories are fine; the loader
simply skips them.

The key-inspection fallback is handy for tiny single-file packs where
splitting into subdirectories would be overkill, but it's strictly less
expressive: it only scans the top level of `path` (no recursion) and each
file must declare exactly one top-level key that the loader recognises.

## Load a directory into an engine you already have

`from_rules` is a constructor: it hands you a new `Engine`. When the engine
already exists — a long-lived process, a pack uploaded at runtime, a second
pack added to the first — use `Engine.load_pack_dir(path)`:

```python
from fathom.engine import Engine

engine = Engine(audit_sink=my_sink)
engine.load_pack_dir("/srv/packs/kssi/v1")
engine.load_pack_dir("/srv/packs/tenant-overrides")
```

It accepts both layouts above, loads in the same fixed order, and is the same
implementation `from_rules` and `load_pack` use — so there is one place that
knows the order rather than one per caller.

Three things it does that hand-rolling the four `load_*` calls does not:

- **Loading the same directory twice is a no-op.** Identity is the resolved
  path, so a relative and an absolute spelling are one pack, not two.
- **A second pack may not redefine a template the first registered.** The
  check runs before anything is built, so a rejected pack leaves the engine
  untouched. This is the same check `load_pack` applies.
- **A directory holding nothing it recognises is an error**, not an engine
  with no rules in it. Pointing one level too high used to succeed silently.

What it does *not* do is resolve `PACK_DEPENDENCIES`: a directory has no
module to declare them on, so a pack that needs another one loaded first must
have that one loaded by its caller. `from_rules` keeps its older, laxer
behaviour on an empty directory — it returns an empty engine rather than
raising, because `FleetEngine` builds session engines that way.

## Load a distributed pack via entry point

Once a pack is packaged as a Python distribution, load it by name:

```python
from fathom.engine import Engine

engine = Engine()
engine.load_pack("owasp-agentic")
```

`Engine.load_pack` delegates to `RulePackLoader`, which walks the
`fathom.packs` entry-point group, imports the registered module, resolves its
on-disk location via `module.__path__`, and then runs the same
`templates/` → `modules/` → `functions/` → `rules/` subdirectory load as
`from_rules` does.

To expose your own pack, add an entry to your `pyproject.toml`:

```toml
[project.entry-points."fathom.packs"]
my-pack = "my_package.rules"
```

Here `my_package/rules/` is an importable package directory that contains the
familiar `templates/`, `modules/`, `functions/`, and `rules/` subdirectories
full of YAML. After `pip install`, any process with Fathom installed can call
`engine.load_pack("my-pack")`.

### Packs shipped with Fathom

Fathom currently ships seven first-party rule packs, registered under the same
entry-point group in its own `pyproject.toml`:

- `owasp-agentic`
- `ssvc`
- `nist-800-53`
- `hipaa`
- `cmmc`
- `schema-denoising`
- `conflict-detection`

The first five render decisions. The last two do not: their rules only assert
facts, and the host reads those facts or writes its own rules over them.

- [`schema-denoising`](../reference/rule-packs/schema-denoising.md) filters an
  upstream extraction stream down to the relation types that appear often
  enough to trust, leaving `aligned_fact` facts behind.
- [`conflict-detection`](../reference/rule-packs/conflict-detection.md) reports
  contradictions between claims as `conflict` facts. It does not resolve them:
  what to do about a contradiction is a policy question, so it belongs in your
  rules rather than in the pack.

### Pack dependencies

A pack module may declare `PACK_DEPENDENCIES`, a tuple of pack names to load
first:

```python
# my_package/rules/__init__.py
PACK_DEPENDENCIES = ("nist-800-53",)
```

`load_pack` resolves those before loading the pack itself. `cmmc` uses this:
it builds on `nist-800-53`, so `engine.load_pack("cmmc")` loads both.

### Loading the same pack twice is a no-op

`load_pack` tracks which packs an `Engine` already holds and skips a repeat
load rather than re-running the loaders (which CLIPS would reject with
`[CSTRCPSR4] Cannot redefine deftemplate … while it is in use`). This is what
makes shared dependencies safe: loading `nist-800-53` and then `cmmc` does
not load NIST twice.

The tracking is per-`Engine`, and `Engine.reload_rules()` clears it
(`engine.py` calls `packs.forget_packs`). A reload swaps in a fresh CLIPS
environment and discards the rule registry, so a previously-loaded pack's
rules are gone; keeping the record across that made the obvious repair — load
the pack again — a silent no-op that returned successfully while the rules
stayed absent. Clearing it means the retry is attempted, and fails loudly if
it cannot succeed. The same applies to `load_pack_dir`, which shares the
record.

### Packs that define the same template

Two packs may not define the same template name with different definitions in
one `Engine`. `load_pack` checks for this **before** loading anything and
raises `CompilationError`:

```
Rule pack 'nist-800-53' redefines template 'data_transfer' already
registered by rule pack 'hipaa'
```

The check runs first, so the second pack is not half-loaded: the engine is
left exactly as it was. Identical definitions are allowed and simply reused.

This affects real combinations today — `hipaa` and `nist-800-53`/`cmmc` both
define `data_transfer`, `audit_event` and `access_request` with different
slots, so they cannot share an `Engine`. Load them into separate `Engine`
instances and evaluate against each.

### Error handling

If you pass a name that isn't registered under `fathom.packs`,
`RulePackLoader.discover` raises `CompilationError` with
`construct="pack:<name>"`. The same error is raised if the registered module
has no resolvable path (neither `__path__` nor `__file__`), which in practice
only happens for exotic namespace-package setups.

## When to use which

- **`Engine.from_rules(path)`** — your application owns the rules, they live
  in a directory inside the repo (or a mounted volume), and you want the
  fastest edit-reload loop. Best for development, single-tenant deployments,
  and environment-specific overrides.
- **`engine.load_pack(name)`** — the rules are a redistributable asset
  consumed by multiple applications, need independent versioning, and can be
  published alongside your Python wheels. Enables `pip install
  compliance-pack` style workflows and clean upgrades.

Nothing stops you from mixing both in one `Engine`: call `load_pack` for a
shared baseline, then `load_rules` or `load_templates` on a local directory
for application-specific overlays.

## Related reading

- [Python SDK reference](../reference/python-sdk/index.md) — full `Engine`
  API, including every `load_*` method used here.
- [Writing rules](writing-rules.md) — YAML authoring conventions for the
  files inside a pack.
- [YAML schema reference](../reference/yaml/index.md) — the top-level keys
  (`templates`, `modules`, `functions`, `rules`) that the key-inspection
  fallback looks for.


## how-to/convert-rego.md

# Converting a Rego policy

`fathom convert rego` translates an [OPA](https://www.openpolicyagent.org/)
Rego policy into a Fathom ruleset. It is a migration aid, not a compatibility
layer: it converts the part of Rego that has an exact Fathom equivalent and
**reports everything else instead of approximating it**.

That refusal is the design. A policy converter that mistranslates is worse
than one that declines, because the output looks finished — nobody re-reads a
generated `allow` rule that silently dropped one of its three conditions.

## Prerequisites

Parsing is delegated to the real OPA binary rather than reimplemented, so
`opa` must be on your `PATH`:

```shell
opa version
```

If it is missing, the command fails with a message naming
[where to get it](https://www.openpolicyagent.org/docs/latest/#running-opa).
Only the parse step needs it; the conversion itself is pure Python, and
nothing at Fathom runtime depends on OPA.

## Convert a policy

Given `authz.rego`:

```rego
package authz.basic

import rego.v1

default allow := false

allow if {
    input.user.role == "admin"
    input.action != "delete"
}

deny if {
    input.user.suspended == true
}
```

Print the YAML:

```shell
fathom convert rego authz.rego
```

Or write a loadable pack directory:

```shell
fathom convert rego authz.rego --out ./converted
```

`--out` writes `templates/`, `modules/` and `rules/`, which is the layout
[`Engine.from_rules`](load-rule-pack.md) expects:

```python
from fathom.engine import Engine

engine = Engine.from_rules("./converted")
result = engine.evaluate_once(
    [("input", {"user_role": "admin", "action": "read", "user_suspended": "false"})]
)
assert result.decision == "allow"
```

## What the mapping looks like

Rego evaluates one `input` document; Fathom matches typed facts. So the
converter synthesises **one template** holding the `input` fields the policy
actually reads, with nested paths flattened into slot names —
`input.user.role` becomes the slot `user_role`. Rename the template with
`--template`.

Rego ORs the bodies that share a rule name; Fathom ORs separate rules. Each
body therefore becomes its own rule, named `<decision>-<index>`: the policy
above converts to `allow-1` and `deny-2`.

Slot types are inferred from the literals each field is compared against, and
widen to `string` when one field is compared against literals of different
types.

## The convertible subset

| Rego | Fathom condition |
|---|---|
| `input.x == "v"` | `equals(v)` |
| `input.x != "v"` | `not_equals(v)` |
| `input.n > 5`, `5 < input.n` | `greater_than(5)` |
| `input.n < 5`, `5 > input.n` | `less_than(5)` |
| `input.x in {"a", "b"}` | `in([a, b])` |
| `startswith(input.p, "/pub")` | `matches(^/pub)` |
| `endswith(input.p, ".key")` | `matches(\.key$)` |
| `contains(input.q, "DROP")` | `contains(DROP)` |
| `re_match("^t-[0-9]+$", input.id)` | `matches(^t-[0-9]+$)` |

`startswith` and `endswith` become anchored regexes with the literal escaped,
so a `.` in the needle stays a literal dot.

## What it refuses, and why

Each refusal is printed to stderr naming the rule and the construct.

| Construct | Why not |
|---|---|
| `not input.x` | Fathom fact patterns have no negation; a rule cannot match on the absence of a value. |
| `input.n >= 3`, `<= 3` | Fathom has no inclusive comparison. Rewriting `>= n` as `> n-1` is right for integers and wrong for everything else, so it is not done for you. |
| `input.a == input.b` | Both operands are references. A Fathom condition compares one slot against a literal. |
| `data.roles[_] == input.user` | `data` is a second document Fathom has no counterpart for. |
| `count(input.tags) > 2` | The operand is a function call or a computed reference, not a field. |
| `input.enabled` | A bare truthiness check. Compare the value explicitly. |
| Rules not named `allow` / `deny` | Only those two head names have an unambiguous Fathom decision. |
| Any other built-in | Unsupported; reported by name. |

### A rule with one unconvertible condition is dropped whole

This is the case that matters. Half of an `allow` rule matches **more
broadly** than the whole one, so keeping the convertible conditions would
quietly widen the policy. The whole rule is dropped and the reason says so.
Slots that only that rule read are dropped with it, so every slot in the
generated template is one a converted rule actually uses.

## Reading the output

The command exits non-zero when *nothing* converted. Use `--strict` to also
fail when anything at all was skipped — the right setting for a pipeline that
wants all-or-nothing, versus the default best-effort behaviour.

Two things are reported as notes rather than refusals:

- **`default allow := false`.** Fathom's default decision is set on the
  engine, not in the ruleset, so the declaration is not converted.
- **Booleans.** Fathom has no boolean slot type, so Rego's `true` / `false`
  become the symbols `true` / `false`. Assert them as the **strings**
  `"true"` / `"false"`; a Python `True` is rejected by slot validation rather
  than silently failing to match.

## What conversion cannot carry over

The subset is stateless by construction: one `input` fact, compared against
literals. Everything Fathom adds on top of that — facts that persist across
evaluations, cross-fact joins, temporal operators, classification hierarchies
— has no Rego source to convert *from*. Those are written by hand after the
conversion, against the template the converter generated. See
[Writing rules](writing-rules.md).


## how-to/opa-data-api.md

# Serving an OPA client

The Fathom REST server answers OPA's Data API at `POST /v1/data/<path>`. An
existing OPA caller — a sidecar client, an admission webhook, one of the OPA
SDKs — already speaks it, so a policy moved across with
[`fathom convert rego`](convert-rego.md) can be served without touching the
code that calls it.

## Address a decision

OPA addresses a document as `data.<package>.<rule>`. The same split applies
here: the leading segments name the ruleset directory under
`FATHOM_RULESET_ROOT`, and the last segment names the document.

```shell
curl -sS http://localhost:8000/v1/data/authz/basic/allow \
  -H "Authorization: Bearer $FATHOM_API_TOKEN" \
  -d '{"input": {"user": {"role": "admin"}, "action": "read"}}'
```

```json
{"result": true}
```

A trailing `allow` or `deny` answers with the bare boolean OPA would return.
The `GET` form works too, with `input` as a URL-encoded JSON query parameter —
convenient from a shell, though query strings are logged by intermediaries, so
anything sensitive belongs in the POST body.

## Address a package

Drop the trailing document name and the response carries the whole decision
instead of one boolean:

```shell
curl -sS http://localhost:8000/v1/data/authz/basic \
  -H "Authorization: Bearer $FATHOM_API_TOKEN" \
  -d '{"input": {"user": {"role": "admin"}, "action": "read"}}'
```

```json
{
  "result": {
    "allow": true,
    "deny": false,
    "decision": "allow",
    "reason": "authz.basic.allow (converted from Rego)",
    "rule_trace": ["authz_basic::allow-1"]
  }
}
```

`reason` and `rule_trace` have no place in OPA's boolean answer. This is where
to read them.

## How `input` becomes facts

OPA sends one arbitrary `input` document; Fathom matches typed facts. The
document is asserted as a single fact, flattened the same way the converter
flattens references — `input.user.role` becomes the slot `user_role` — so a
converted policy works with no configuration.

Three consequences worth knowing:

- **Booleans become the symbols `true` / `false`**, because Fathom has no
  boolean slot type. This is the same substitution `fathom convert rego`
  warns about, and the two halves are the same code.
- **Fields no slot declares are dropped**, not rejected. An OPA caller sends
  its whole input document; a field no rule reads must not fail the request.
- **Lists and nulls are dropped**, since Fathom has no slot type for them.

Assert into a different template with `?template=`; an unknown name is a 400
listing the templates the ruleset does declare.

## Where this differs from OPA

| | OPA | Fathom |
|---|---|---|
| Authentication | Open by default | Bearer token, as on every other route |
| Undefined document | `{}` with 200 | Always defined — the engine has a default decision (`deny`) |
| Error envelope | `{"code", "message"}` | Same, on these two routes only |
| `POST /v1/data` (no path) | Whole data document | 400 — name a ruleset |

The authentication difference is deliberate. Serving one unauthenticated
endpoint beside a server whose other routes all require a token is an
authentication hole, not a compatibility feature.

The undefined-document difference matters if your Rego relied on `allow`
being undefined rather than `false`. Fathom's engine always returns a
decision, which is the shape a policy with `default allow := false` produces —
and that default is what the converter tells you to write.

Errors on these two routes use OPA's `{"code", "message"}` envelope rather
than Fathom's `{"error", "detail"}`, so a client that parses OPA's errors
keeps working. The one exception is `401`, which comes from the shared auth
dependency and answers in Fathom's shape.

## What this does not give you

The Data API is a request/response surface: one input document, one decision.
Facts that persist across evaluations, cross-fact joins and the temporal
operators — the reasons to be on Fathom rather than OPA — are reached through
[`POST /v1/evaluate`](../reference/rest/index.md), which takes a list of typed
facts and can hold a session. Migrating the callers is a second step, after
the policy is running.


## how-to/export-rego.md

# Exporting rules as Rego

`fathom convert to-rego` writes the stateless subset of a Fathom ruleset out as
[Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) — for
handing a policy to a team that runs OPA, diffing it against an existing Rego
policy, or shipping one decision into an OPA deployment you do not control.

Rego is the smaller language here, so expect refusals. Most of what makes a
Fathom ruleset worth writing — facts that persist across evaluations, joins
across facts, the temporal and classification operators — has no Rego form at
all. Those rules are reported, not flattened into Rego that parses and means
something narrower.

## Export a ruleset

```shell
fathom convert to-rego ./my-ruleset
fathom convert to-rego ./my-ruleset -o policy.rego --package authz.basic
```

Without `--package`, the package name is the module the rules declare.

```rego
# Generated by `fathom convert to-rego`.
#
# The stateless subset only: rules that match one fact against literals.
# Anything the exporter refused was reported on stderr, not written here.

package authz.basic

import rego.v1

default allow := false
default deny := false

# authz.basic.allow (converted from Rego)
# fathom rule: allow-1
allow if {
    input.user_role == "admin"
    input.action != "delete"
}
```

The rule's `then.reason` becomes a comment. Rego's answer is a bare boolean
with nowhere to put a reason, and dropping it loses the one thing that
explains the rule.

## What exports

A rule exports when it matches **one** fact against literals and produces a
decision. The action becomes the Rego document name, so a Fathom `allow` rule
becomes `allow if { ... }` with a `default allow := false` beside it.

| Fathom condition | Rego |
|---|---|
| `equals(v)` / `not_equals(v)` | `== "v"` / `!= "v"` |
| `greater_than(n)` / `less_than(n)` | `> n` / `< n` |
| `in([a, b])` | `in {"a", "b"}` |
| `not_in([a, b])` | `not ... in {"a", "b"}` |
| `contains(v)` | `contains(input.x, "v")` |
| `matches(p)` | `regex.match("p", input.x)` |

Literals are rendered from the **declared slot type**, not from how the text
looks: `equals(5)` on a `string` slot is the string `"5"`, and on an `integer`
slot it is `5`. A `symbol` slot holding `true` or `false` becomes a Rego
boolean, which is the inverse of what
[the importer](convert-rego.md) does on the way in.

## How slots are addressed

Rego has one `input` document, and Fathom facts are typed. So the template
name stays in the path: a rule over template `request` reads
`input.request.role`. Two rules over different templates can never match the
same fact, and collapsing them onto one document root would say they can.

The exception is a ruleset whose only template is named `input` — the shape
`fathom convert rego` produces. There the slots sit at the document root
(`input.user_role`), so a policy imported from Rego exports back to the shape
it started in.

## What it refuses

Each refusal names the rule and the reason, on stderr. Refusals that share a
reason are grouped, so a ruleset built on cross-fact joins reports that once
with a count rather than once per rule.

| Rule | Why not |
|---|---|
| More than one fact pattern | The rule joins across facts; Rego has one input document to join against. |
| `then.assert` | The rule adds to working memory. Rego derives documents from one input and cannot. |
| No `then.action` | Pure inference — the part of Fathom Rego does not have. |
| `then.scope` | The decision carries a scope value; a Rego document is a boolean here. |
| A temporal operator | `changed_within`, `count_exceeds` and the rest ask about history, and Rego has no memory of the last input. |
| A classification operator | `below`, `meets_or_exceeds` and the rest resolve against a hierarchy, which is engine state. |
| `bind:` or `test:` | A bind is a join; a raw CLIPS conditional element has no translation. |
| A `$alias.field` reference | A cross-fact reference. |

A refused rule is left out entirely. `--strict` exits non-zero when anything
was refused; without it the command exits non-zero only when *nothing*
exported.

## What the export changes even when it succeeds

Two differences are reported as notes rather than refusals, because the rules
themselves convert cleanly:

- **Precedence.** Fathom picks one decision per evaluation. Rego evaluates
  `allow` and `deny` independently, so a policy defining both leaves the
  precedence to the caller — conventionally, deny wins.
- **Salience.** Rego has no rule ordering. A ruleset that relies on one rule
  firing before another does not mean the same thing once exported.

## Round-tripping

A Rego policy taken through [`fathom convert rego`](convert-rego.md) and back
out through `to-rego` lands on the same rules. The package name does not
survive on its own — Fathom module names cannot hold dots, so `authz.basic`
becomes the module `authz_basic` — so pass `--package authz.basic` to restore
it.


## how-to/embed-sdk.md

# Embedding via SDK

Fathom ships three client surfaces. Pick one based on how your application
is shaped:

- **Python in-process** (`fathom-rules`) — the `Engine` class runs in your
  process, holding working memory directly in memory. No server, lowest
  latency.
- **Go HTTP client** (`packages/fathom-go`) — talks to a running Fathom
  REST server at `POST /v1/evaluate` and friends. Session state lives on
  the server.
- **TypeScript HTTP client** (`@fathom-rules/sdk`) — same REST server,
  same four operations, `fetch`-based.

All three expose the same four working-memory operations —
`evaluate`, `assert_fact`, `query`, and `retract` — but Python embeds
the engine in your process while Go and TypeScript are thin wrappers
around the REST API.

## Python — in-process Engine

Install the package:

```bash
pip install fathom-rules
```

Load a rules directory with `Engine.from_rules`, assert facts, and
evaluate:

```python
from fathom.engine import Engine

engine = Engine.from_rules("examples/01-hello-allow-deny")
engine.assert_fact("access", {"role": "admin", "resource": "db"})

result = engine.evaluate()
print(result.decision)      # "allow" | "deny" | "escalate" | None
print(result.reason)
print(result.rule_trace)    # list[str] — rules that fired
print(result.module_trace)  # list[str] — modules traversed
print(result.duration_us)   # int — evaluation time in microseconds
```

`EvaluationResult` also carries `attestation_token` and `metadata`
(see [`EvaluationResult`](../reference/python-sdk/evaluationresult.md)).

### Working memory across calls

The embedded `Engine` is stateful — facts you assert remain in working
memory until you retract them or call `engine.reset()`. A second
`evaluate()` call on the same engine still sees the fact from the
first round:

```python
engine.assert_fact("access", {"role": "admin", "resource": "db"})
engine.evaluate()  # rules see the fact
engine.evaluate()  # second call — the fact is still there

engine.reset()     # clear all facts and reinitialise the environment
```

Use `engine.clear_facts()` to drop user facts without rebuilding
templates and rules.

### Query and retract

`query` returns facts matching a template and optional slot filter;
`retract` removes them and returns how many rows it pulled out:

```python
engine.query("access")
# [{"role": "admin", "resource": "db"}]

engine.query("access", fact_filter={"role": "admin"})
# [{"role": "admin", "resource": "db"}]

removed = engine.retract("access", fact_filter={"role": "admin"})
# removed == 1
```

Both operate on live working memory, so the results reflect rules
that asserted during the most recent `evaluate()` plus anything you
asserted by hand.

## Go — HTTP client

The Go client is published as a standalone module. Install it:

```bash
go get github.com/KrakenNet/fathom-go
```

Prerequisite: a running Fathom REST server — see the
[FastAPI how-to](fastapi.md) for how to start one and configure
`FATHOM_API_TOKEN` and `FATHOM_RULESET_ROOT`.

Minimal example — construct a client with a bearer token and call
`Evaluate`:

```go
package main

import (
    "context"
    "fmt"
    "log"

    fathom "github.com/KrakenNet/fathom-go"
)

func main() {
    client := fathom.NewClient(
        "http://localhost:8000",
        fathom.WithBearerToken("your-token"),
    )

    resp, err := client.Evaluate(context.Background(), &fathom.EvaluateRequest{
        Ruleset:   "",
        SessionID: "session-1",
        Facts: []fathom.FactInput{
            {Template: "access", Data: map[string]any{"role": "admin", "resource": "db"}},
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(resp.Decision, resp.Reason)
    fmt.Println(resp.RuleTrace)
    fmt.Println(resp.DurationUS)
}
```

`Ruleset` is a path under the server's `FATHOM_RULESET_ROOT`; empty
string evaluates against the root itself. `SessionID` is optional for
stateless evaluation — passing one creates a server-side session that
later `AssertFact` / `Query` / `Retract` calls can reuse.

### Working memory across calls

`AssertFact`, `Query`, and `Retract` all operate on a session
previously created by an `Evaluate` call with the same `session_id`.
Unknown session ids return `404`.

```go
ctx := context.Background()

// Create the session by evaluating once.
_, _ = client.Evaluate(ctx, &fathom.EvaluateRequest{
    Ruleset:   "",
    SessionID: "session-1",
    Facts:     []fathom.FactInput{},
})

_, err := client.AssertFact(ctx, &fathom.AssertFactRequest{
    SessionID: "session-1",
    Template:  "access",
    Data:      map[string]any{"role": "admin", "resource": "db"},
})
if err != nil {
    log.Fatal(err)
}

q, _ := client.Query(ctx, &fathom.QueryRequest{
    SessionID: "session-1",
    Template:  "access",
    Filter:    map[string]any{"role": "admin"},
})
fmt.Println(q.Facts) // []map[string]any{{"role": "admin", "resource": "db"}}

r, _ := client.Retract(ctx, &fathom.RetractRequest{
    SessionID: "session-1",
    Template:  "access",
    Filter:    map[string]any{"role": "admin"},
})
fmt.Println(r.RetractedCount) // 1
```

All methods return `(*Response, error)`; a non-2xx HTTP status is
surfaced as an `error` that includes the server's status code and
response body.

## TypeScript — HTTP client

Install the package:

```bash
npm install @fathom-rules/sdk
```

Construct a `FathomClient` and call `evaluate`:

```ts
import { FathomClient } from "@fathom-rules/sdk";

const client = new FathomClient({
  baseURL: "http://localhost:8000",
  bearerToken: "your-token",
});

const result = await client.evaluate({
  facts: [{ template: "access", data: { role: "admin", resource: "db" } }],
  ruleset: "",
  session_id: "session-1",
});

console.log(result.decision);    // "allow" | "deny" | "escalate" | null
console.log(result.reason);
console.log(result.rule_trace);  // string[]
console.log(result.duration_us); // number
```

The constructor accepts `baseURL` (required), `bearerToken` (optional
— injected as `Authorization: Bearer <token>`), and `headers` (optional
extras). Prefer `bearerToken` over hand-crafting an `Authorization`
header; the option takes precedence.

### Working memory across calls

`assertFact`, `query`, and `retract` target a server session created
by a prior `evaluate` call with the same `session_id`. Request
payloads use snake-case keys to match the REST schema:

```ts
await client.evaluate({
  ruleset: "",
  session_id: "session-1",
  facts: [],
});

await client.assertFact({
  session_id: "session-1",
  template: "access",
  data: { role: "admin", resource: "db" },
});

const q = await client.query({
  session_id: "session-1",
  template: "access",
  filter: { role: "admin" },
});
console.log(q.facts); // [{ role: "admin", resource: "db" }]

const r = await client.retract({
  session_id: "session-1",
  template: "access",
  filter: { role: "admin" },
});
console.log(r.retracted_count); // 1
```

### Error handling

Every failure raises a typed subclass of `FathomError`. Discriminate
with `instanceof`:

- `PolicyViolation` — HTTP `403`, the engine denied the request.
- `ValidationError` — HTTP `400` or `422`, the request body failed
  validation.
- `ConnectionError` — HTTP `>= 500` or a network/abort failure
  (status `0`).
- `FathomError` — base class, used for any other non-2xx status.

```ts
import {
  FathomClient,
  PolicyViolation,
  ValidationError,
  ConnectionError,
} from "@fathom-rules/sdk";

try {
  await client.evaluate({ ruleset: "", facts: [] });
} catch (e) {
  if (e instanceof PolicyViolation) { /* 403 */ }
  else if (e instanceof ValidationError) { /* 400 / 422 */ }
  else if (e instanceof ConnectionError) { /* 5xx or network */ }
  else throw e;
}
```

## Picking the right embedding

| Mode | When to choose it |
|---|---|
| **Python in-process** | Lowest latency, single-process Python app, no need to share session state with other processes or tenants. |
| **Go or TypeScript HTTP client** | Multi-language stack, multi-process deployment, centralised session state, horizontal scaling of the engine tier. |

REST trades a network hop for operational simplicity: one server
serves any language, sessions survive client restarts, and the engine
tier scales independently. The embedded Engine wins on latency and
removes the server from the deployment diagram entirely.

## Related reading

- [Integrating with FastAPI](fastapi.md) — run the REST server the
  Go and TypeScript clients talk to.
- [Python SDK reference](../reference/python-sdk/index.md) — full
  `Engine` surface including `EvaluationResult` fields.
- [Go SDK reference](../reference/go-sdk/fathom-go.md) — generated
  reference for every exported type and method.
- [TypeScript SDK reference](../reference/typescript-sdk/index.md)
  — typedoc-generated client, interface, and error reference.


## how-to/hot-reload.md

# Hot-reloading rulesets in a running server

Fathom can swap a running server's ruleset atomically, without a
restart, via `POST /v1/rules/reload`. Each reload:

- Compiles the new YAML in a fresh CLIPS environment and only swaps it
  in on success — a bad ruleset leaves the previous one serving
  traffic (NFR-8).
- Verifies an Ed25519 signature over the raw YAML bytes before
  accepting the payload — unsigned rulesets are **rejected by default**
  (fail-closed, AC-5.5).
- Emits a JWT-attested audit event (`ruleset_reloaded` on success,
  `ruleset_reload_rejected` on failure) carrying the
  `ruleset_hash_before`/`ruleset_hash_after` pair.

## Warning: a reload discards all working memory

The new ruleset is compiled into a **fresh** CLIPS environment, and that
environment starts empty. Every fact in working memory at the moment of the
swap is gone — session facts, fleet facts synced from the shared store, and any
long-lived facts the caller asserted at startup. Fact TTL timestamps are
cleared with them.

A failed reload changes nothing: the old environment keeps serving, so working
memory survives. Only a *successful* swap wipes it.

Plan for this. `Engine.subscribe_reload` is the seam: it registers a
zero-argument callback fired after each successful swap, outside the reload
lock, and returns an unsubscribe callable. A listener that raises is logged and
swallowed, so a wedged subscriber cannot break the reload.

```python
def reseed() -> None:
    engine.assert_fact("tenant", {"id": "acme", "tier": "gold"})

unsubscribe = engine.subscribe_reload(reseed)
```

Re-run `FleetEngine.sync_fleet_facts` from the same callback for fleet-scoped
state, and expect session callers to re-seed anything they asserted before the
reload.

This how-to covers how to sign a ruleset, wire up the deployment
config, use the dev escape during development, monitor the live
ruleset with `fathom status`, and rotate the runtime pubkey.

## Endpoint shape

```
POST /v1/rules/reload
Authorization: Bearer <operator-token>
Content-Type: application/json
```

Request body — supply **exactly one** of `ruleset_path` or
`ruleset_yaml`; `signature` is required in production (fail-closed
default):

```json
{
  "ruleset_path": "/etc/fathom/rulesets/prod-2026-04-22.yaml",
  "signature":    "<base64 Ed25519 over the raw YAML bytes>"
}
```

Inline form — useful when the operator lacks filesystem write access
on the Fathom host:

```json
{
  "ruleset_yaml": "rules:\n  - id: ...\n",
  "signature":    "<base64 Ed25519 over the ruleset_yaml bytes>"
}
```

Response (200):

```json
{
  "ruleset_hash_before": "sha256:aaaa…",
  "ruleset_hash_after":  "sha256:bbbb…",
  "attestation_token": "<JWT signed by AttestationService>"
}
```

Error codes:

- `400 invalid_request` — both or neither of `ruleset_path` /
  `ruleset_yaml` supplied; `ruleset_path` unreadable; `signature` is
  not valid base64; ruleset failed to compile.
- `400 unsigned_ruleset` — `require_signature=true` (the default) and
  either `signature` is missing or Ed25519 verification failed. Also
  emits `ruleset_reload_rejected` to the audit sink.
- `413 payload_too_large` — the request body exceeds the reload size
  cap (see below). Enforced before any YAML parsing.
- `500 server_misconfigured` — `require_signature=true` but the
  pubkey never loaded (should have failed at boot; treated as
  defence-in-depth).
- `503 not_ready` — engine or attestation service not yet configured
  on `app.state`.

## Admin token, body size cap, and rate limiting

`POST /v1/rules/reload` (REST) and the gRPC `Reload` RPC are
admin-scoped, defence-in-depth controls on top of signature
verification:

- **Scoped admin token.** Set `FATHOM_ADMIN_TOKEN` to require a
  dedicated token for reload. When it is set, the reload surface
  accepts **only** that token — the data-plane `FATHOM_API_TOKEN`
  (used by `/v1/evaluate`, `/v1/facts`, …) no longer grants reload,
  so a leaked data-plane token cannot replace the ruleset. When
  `FATHOM_ADMIN_TOKEN` is **unset**, reload falls back to
  `FATHOM_API_TOKEN` exactly as before (backward compatible). A
  rejected token returns `401 unauthorized`.
- **Body size cap.** Reload bodies are capped at 5 MB by default,
  overridable via `FATHOM_MAX_RELOAD_BYTES`. The cap is enforced on
  the **actual bytes received** (the body is streamed and aborted once
  the running total exceeds the limit), not the `Content-Length`
  header — a chunked request that under-reports its length is still
  rejected with `413 payload_too_large` before the YAML parser sees
  anything. The gRPC server inherits gRPC's default 4 MB
  `max_receive_message_length`, which already bounds `Reload` payloads
  below the REST cap.
- **Rate limiting is host-level by design.** Fathom does not implement
  per-client request throttling in-process. Run reload behind a
  reverse proxy (nginx `limit_req`, Envoy, an API gateway, …) and rate
  limit there. Keeping rate limiting at the host layer avoids
  duplicating shared infrastructure inside the engine and keeps the
  admin surface a thin, auditable control.

## Signing a ruleset

The runtime pubkey is a **separate concept** from the release-signing
key documented in [release-signing.md](release-signing.md). Per design
decision D2 you can either:

- **Reuse the release-signing keypair.** Simpler key inventory;
  operators who already run `scripts/sign_release.sh` can sign rulesets
  with the same material. Trade-off: a compromise of the release key
  also lets an attacker forge hot-reload payloads.
- **Generate a separate runtime keypair.** Blast-radius reduction: a
  compromised runtime key does not let the attacker forge release
  artifacts, and vice-versa. Recommended for production deployments.

### Generating a runtime keypair

The server expects a **raw Ed25519 PEM public key**, not minisign's
format. Generate with `openssl`:

```shell
openssl genpkey -algorithm ed25519 -out ruleset-signing.key
openssl pkey   -in ruleset-signing.key -pubout -out ruleset-signing.pub
```

Keep `ruleset-signing.key` in the same custody regime as your other
secrets (HSM, sealed secret, offline backup). The public half
(`ruleset-signing.pub`) is what the Fathom server loads at boot.

### Producing a detached signature

Sign the **raw YAML bytes** you intend to POST — not a hash, not a
normalised form. The server re-hashes the payload bytes internally and
uses them both for verification and for `ruleset_hash_after`. Any pre-signing
transformation (whitespace fix-ups, YAML re-emit) invalidates the
signature.

Python example using `cryptography`:

```python
from base64 import b64encode
from pathlib import Path
from cryptography.hazmat.primitives.serialization import load_pem_private_key

yaml_bytes = Path("prod-2026-04-22.yaml").read_bytes()
priv = load_pem_private_key(Path("ruleset-signing.key").read_bytes(), password=None)
signature_b64 = b64encode(priv.sign(yaml_bytes)).decode("ascii")
print(signature_b64)
```

The resulting base64 string goes into the `signature` field of the
reload request. OpenSSL's `pkeyutl -sign` path works too, but keeping
the YAML bytes byte-identical through the shell pipeline is error-
prone; scripted signing is recommended.

## Deployment configuration (fail-closed default)

Two independent controls must both line up for a production deployment:

1. **Config.** In `config.yaml` (or whichever layer feeds your server
   factory):

   ```yaml
   rules:
     hot_reload:
       require_signature: true   # default
   ```

2. **Environment.** Point at the public key PEM file:

   ```shell
   export FATHOM_RULESET_PUBKEY_PATH=/etc/fathom/ruleset-signing.pub
   ```

The server loads the pubkey **at boot** and pins it onto
`app.state.ruleset_pubkey`. If `require_signature=true` and the path
is missing, unset, or unreadable, `build_app()` raises a
`RuntimeError` with the message `ruleset pubkey unreadable or missing;
set FATHOM_RULESET_PUBKEY_PATH or enable dev escape` and the server
never starts. This is deliberate — failing at boot beats failing on
first reload attempt.

## Dev escape (development only)

There is a single supported way to run the server with signature
verification disabled, and it requires **both** of the following, by
design (belt-and-suspenders — a single stray flag is not enough to
lower the security floor):

1. Config: `rules.hot_reload.require_signature: false`.
2. Environment: `FATHOM_ALLOW_UNSIGNED_RULESETS=1`.

Neither alone is sufficient; any other combination keeps the server
fail-closed. When both are set, `build_app()` logs at WARN on startup:

```
ruleset signature verification disabled (require_signature=false +
FATHOM_ALLOW_UNSIGNED_RULESETS=1); hot-reload will accept unsigned
rulesets
```

The WARN line is emitted once per process; scrape for it in your log
aggregator to detect dev escape accidentally turned on in prod. Every
unsigned reload that succeeds under the dev escape still writes a
`ruleset_reloaded` audit record, so there is no silent window.

## Monitoring the live ruleset

`GET /v1/status` (unauthenticated; liveness-shaped) returns:

```json
{
  "ruleset_hash": "sha256:bbbb…",
  "version":      "0.3.1",
  "loaded_at":    "2026-04-22T22:15:07.482+00:00"
}
```

`loaded_at` is the timestamp of the most recent reload, or the
server's boot time if no reload has happened yet. The CLI wraps this
endpoint:

```shell
fathom status --server https://fathom.prod.example.com
```

Output:

```
ruleset_hash: sha256:bbbb…
version:      0.3.1
loaded_at:    2026-04-22T22:15:07.482+00:00
```

Pass `--token` (or set `FATHOM_TOKEN`) if the server fronts `/v1/status`
with auth in your environment. Exit code `0` means the call succeeded
and the ruleset hash was reported; non-zero means the server was
unreachable, returned an HTTP error, or replied with non-JSON.

The `ruleset_hash` you see here should match the `ruleset_hash_after` value
returned by the most recent `POST /v1/rules/reload`. If it does not,
either a later reload happened between your calls, or the audit log
and live state have diverged — investigate immediately.

## Audit events

Every reload — accepted or rejected — writes one record to the
configured audit sink. Successful reloads emit:

```json
{
  "event_type":         "ruleset_reloaded",
  "ruleset_hash_before": "sha256:aaaa…",
  "ruleset_hash_after":  "sha256:bbbb…",
  "actor":              "bearer-token",
  "timestamp":          "2026-04-22T22:15:07.482+00:00"
}
```

Rejected reloads emit `event_type: ruleset_reload_rejected` with a
`reason` field (`missing_signature`, `verification_failed`,
`unknown_template`, …) and the *unchanged* `ruleset_hash_before`.

The `attestation_token` returned from a successful reload is a JWT
signed by the server's `AttestationService.sign_event()` (C6); you can
verify it offline with the service's pubkey and replay it in your
audit trail as a non-repudiation proof.

## Rotating the runtime pubkey

Rotate on a schedule (annual, matching the release-signing cadence)
or immediately on suspected compromise:

1. Generate a new Ed25519 keypair with `openssl` (see above); store
   the private half in the same custody regime as the old one.
2. Copy the new public key to a fresh path (e.g. `.pub.v2`) on each
   Fathom host so you can roll back instantly if needed.
3. Update the deployment's `FATHOM_RULESET_PUBKEY_PATH` to point at
   the new `.pub.v2` file.
4. Restart the server. `build_app()` reloads the pubkey at boot; there
   is no hot path for pubkey rotation by design (the pubkey is the
   trust anchor for reloads, so rotating it via a reload would be
   circular).
5. Confirm the new pubkey is live by POSTing a reload signed with the
   new private key and watching `fathom status --server …` for a new
   `ruleset_hash` and advanced `loaded_at`.
6. Securely erase the previous private key once you have confirmed
   the next scheduled ruleset push signs cleanly under the new key.

The runtime pubkey and the release-signing pubkey rotate on
independent schedules — if you reuse the same key for both (D2
trade-off), plan one rotation event that covers both surfaces.

## Migration from earlier Fathom builds

This release is the **first** to ship `POST /v1/rules/reload`, so no
operator has ever had a "signatures optional" hot-reload in
production. However, operators who have been experimenting with
pre-release dev builds that accepted unsigned rulesets need to know:

- **The default is fail-closed.** After upgrading, a server started
  without `FATHOM_RULESET_PUBKEY_PATH` will refuse to boot with
  `ruleset pubkey unreadable or missing…`. This is intentional.
- **Before restart, pick one of:**
  1. **Production path (recommended).** Generate a runtime keypair
     (or decide to reuse the release key), publish the pubkey file,
     set `FATHOM_RULESET_PUBKEY_PATH` on every Fathom host, and keep
     `rules.hot_reload.require_signature: true`.
  2. **Dev/CI path.** Set **both** `rules.hot_reload.require_signature:
     false` in config **and** `FATHOM_ALLOW_UNSIGNED_RULESETS=1` in
     the environment. Either alone leaves fail-closed behaviour
     active — deliberately, to stop a partial config from silently
     downgrading security.
- **Audit sink consumers.** Start recognising the two new
  `event_type` values (`ruleset_reloaded`,
  `ruleset_reload_rejected`) alongside the existing eval-shaped
  records. They share the `actor` + `timestamp` envelope but carry
  `ruleset_hash_before` / `ruleset_hash_after` instead of eval fields.


## how-to/release-signing.md

# Verifying release signatures

Every Fathom release artifact from **0.5.0 onward** is signed with
[minisign](https://jedisct1.github.io/minisign/) (detached Ed25519) and
attached to its GitHub Release. This how-to shows you how to install minisign,
verify a downloaded wheel or sdist, and understand the custody, rotation, and
revocation policy for the signing key.

**Releases before 0.5.0 are unsigned.** Signing was introduced with the 0.4.0
tree, which reached PyPI as 0.5.0. The five earlier PyPI releases — 0.1.0,
0.2.0, 0.3.0, 0.3.1 and 0.3.2 — have no `.minisig`, and the GitHub releases for
0.3.0, 0.3.1, 0.3.3 and 0.4.0 carry no assets at all. There is nothing to
verify for those versions, and no signature will be published retroactively:
back-signing an old artifact today would assert a custody chain that did not
exist when it was built. Use 0.5.0 or later if you need a verifiable artifact.

## What is signed, and how

- **Artifacts**: every `fathom_rules-*.whl` and `fathom_rules-*.tar.gz`
  from 0.5.0 onward. `pypi-publish.yml` fails the release if any artifact
  in `dist/` reaches the publish step without a sibling `.minisig`.
- **Algorithm**: Ed25519 via minisign's standard `.minisig` format —
  detached signature, one file per artifact, binary-safe.
- **Where signatures appear**:
  - Alongside each wheel / sdist as `<artifact>.minisig` on the GitHub
    Release page.
  - In the `dist/` directory produced by `scripts/sign_release.sh`
    during the `pypi-publish` workflow.
- **Pubkey**: published at
  [`docs/reference/release-signing-pubkey.minisig`](../reference/release-signing-pubkey.minisig)
  and embedded in the Fathom wheel at
  `fathom/_data/release_pubkey.minisign` so that `fathom
  verify-artifact` works offline.

A `.minisig` file is a small text blob containing an Ed25519 signature
plus a trusted comment. It is not a bundle — the artifact and its
`.minisig` travel together but remain separate files.

## Installing minisign

Pick the instruction that matches your platform. All of them give you a
`minisign` binary on your `PATH`.

- **macOS** (Homebrew):

  ```shell
  brew install minisign
  ```

- **Linux** (Debian, Ubuntu):

  ```shell
  sudo apt install minisign
  ```

  On Fedora / RHEL: `sudo dnf install minisign`. On Arch: `sudo pacman
  -S minisign`.

- **Windows**: download a pre-built binary from the upstream release
  page at
  [`https://github.com/jedisct1/minisign/releases`](https://github.com/jedisct1/minisign/releases)
  and place it somewhere on `PATH`. Alternatively, use WSL and follow
  the Linux instructions above.

Confirm the install:

```shell
minisign -v
```

## Verifying a downloaded artifact

You have two equivalent paths. Use the first if you already have Fathom
installed; use the second if you only have `minisign` and the public
key file.

### Option A — `fathom verify-artifact` (embedded pubkey)

The CLI carries the release pubkey inside the wheel, so once you have
any recent Fathom version installed you can verify later downloads
offline:

```shell
fathom verify-artifact fathom_rules-1.2.0-py3-none-any.whl
```

If `<artifact>.minisig` lives next to the artifact, the CLI picks it up
automatically. Override either side with `--sig <path>` or `--pubkey
<path>` when auditing a specific file. Exit code `0` means the
signature verified; non-zero means the verification failed or the
inputs were malformed — re-download the artifact before trusting it.

### Option B — raw `minisign`

No Fathom install needed. Download the artifact, its `.minisig`, and
the pubkey file from
[`docs/reference/release-signing-pubkey.minisig`](../reference/release-signing-pubkey.minisig),
then:

```shell
minisign -Vm fathom_rules-1.2.0-py3-none-any.whl \
         -p release-signing-pubkey.minisig
```

`minisign` looks for `<artifact>.minisig` by default. Pass `-x <path>`
to point at a signature that lives elsewhere. A `Signature and comment
signature verified` line means the artifact is authentic.

The pubkey's first line is also pasted into every GitHub Release body,
so you can cross-check the key you fetched from the repository against
the one on the release page.

## M-of-N custody policy

The release private key is never held in a single place. Two named
custodians each hold one half of the operational state, and **both
must agree out-of-band before a release tag is pushed**.

- `MINISIGN_KEY_PRIMARY` — GitHub Actions secret, held by custodian A.
  Loaded into `scripts/sign_release.sh` during the `pypi-publish`
  workflow.
- `MINISIGN_KEY_SECONDARY` — offline backup of the same keypair, held
  by custodian B. Used only for rotation, recovery, and the custody
  handshake described below.
- Both custodians confirm the target tag, commit SHA, and intended
  version via a signed email or Signal message before custodian A
  authorises the tag push that triggers signing. No single custodian
  can ship a release on their own, because the handshake is the
  authorisation step — not merely a courtesy.

The policy exists so that a compromise of a single machine, inbox, or
GitHub token cannot produce a valid Fathom release.

## Rotation cadence

- **Scheduled**: rotate the keypair annually. Track the next rotation
  date in the repository's release runbook.
- **On compromise**: rotate immediately if either custodian suspects
  their copy of the key has been exposed, if a custodian's workstation
  is lost or stolen, or if `MINISIGN_KEY_PRIMARY` is ever printed to
  logs.

### Rotation procedure

1. Both custodians meet out-of-band and generate a new keypair with
   `minisign -G`.
2. Update `docs/reference/release-signing-pubkey.minisig` **and**
   `src/fathom/_data/release_pubkey.minisign` with the new public key,
   in a single commit.
3. Distribute the new private key: update the `MINISIGN_KEY_PRIMARY`
   GitHub Actions secret; custodian B stores the new offline backup.
4. Publish a dedicated release note flagging the rotation, including
   the new key ID and the date from which signatures use it.
5. Retire the previous private-key material (securely erase offline
   copies, delete the old GitHub secret).

## Revocation

minisign has no CRL or OCSP equivalent — trust in a key is defined by
the key file you hold. To revoke a compromised key:

1. Follow the rotation procedure above, treating it as an emergency.
2. Issue a release note that explicitly invalidates every artifact
   signed by the previous key and lists which published versions are
   affected.
3. Recommend that users re-fetch and re-verify any still-in-use
   artifacts against the new pubkey before continuing to rely on
   them.

Users who pin the pubkey (for example by vendoring
`release-signing-pubkey.minisig`) should update their pinned copy from
the new release. The embedded pubkey in `fathom/_data/` updates
automatically once users install a release signed under the new key.


## how-to/policy-studio.md

# Running Policy Studio

Policy Studio is a local browser UI over a real Fathom engine: load a ruleset,
push facts at it, and read the decision, the rules that fired, and the audit
line that came out. It is a **separate package** — `fathom-studio`, its own
version line — because the engine wheel ships no UI code.

## Install and run

`fathom-studio` is not published to PyPI yet. Run it from a checkout, where
it is a workspace member and `uv sync` already installs it:

```bash
git clone https://github.com/KrakenNet/fathom.git
cd fathom
uv sync

export FATHOM_API_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
uv run fathom-studio
```

The process prints the URL to open and binds `127.0.0.1:8020`. `--host` and
`--port` override the bind; `FATHOM_STUDIO_PORT` sets the port from the
environment.

Open it as `http://127.0.0.1:8020/?token=$FATHOM_API_TOKEN` **once**. The
token is validated and stored in the `fathom_token` cookie, which the SPA's
`fetch` calls and the plain HTML forms then carry for you.

## The token gate

The Studio mounts the production REST app in the same process, so it holds
itself to the same door: every route that loads a ruleset, drives the engine,
or mints an attestation token requires the same `FATHOM_API_TOKEN` the REST
app requires. There is no second secret. Present it as either
`Authorization: Bearer <token>` (for `curl` and tests) or the cookie above.

Only three things are ungated, and none of them carry engine data: `/health`,
the SPA shell at `/`, and its static assets under `/creem`. With
`FATHOM_API_TOKEN` unset, every gated route answers 401 — an unconfigured
Studio exposes nothing.

## What the views do

| View | What it drives |
|---|---|
| Reasoning Bench | Loads a bundled scenario, evaluates it, and shows decision, reason, and the firing rules |
| Live Wire | A rolling decision stream and deny-rate strip. The traffic is synthetic — it samples the bundled scenarios — but every packet is a real evaluation |
| Rules | The compiled rules of the selected ruleset, with their conditions |
| Templates | The fact schemas the ruleset declares |
| Audit | The signed, hash-linked record of what the Studio has evaluated so far |

Nine demo scenarios ship with the package, covering five example rulesets:
hello allow/deny, RBAC with modules, Bell-LaPadula classification, temporal
anomaly detection, and LangChain guardrails. Each is a copy of the matching
`examples/0N-*` directory, held byte-identical to it by a test. Point
`FATHOM_RULESET_ROOT` at your own directory to drive the Studio with your
rulesets instead.

Decisions are never faked. `POST /studio/api/evaluate` builds a fresh
`Engine` for the chosen ruleset, asserts the facts, evaluates, and renders
what came back — the same stateless shape `/v1/evaluate` uses, so working
memory never leaks from one bench run into the next.

## What it is not

- **Not a sandbox.** The REST app is *mounted*, not proxied: it runs in the
  Studio's process, under `/api`, sharing that module's in-memory session
  store. Anything driven through `/api` — including the server-rendered
  panels, which call it with a per-browser `X-Session-Id` — lands in the same
  working memory a REST client of that process sees.
- **Not a stable API.** The `/studio/api/*` routes are unversioned and
  excluded from [the compatibility policy](https://github.com/KrakenNet/fathom/blob/main/VERSIONING.md);
  they change with the UI.
- **Not your audit log.** The Audit view's chain lives in process memory,
  holds the most recent 200 records, and is signed with a keypair generated
  at startup — it dies with the process. For a durable, verifiable log, use
  `ChainedAttestationLog` from the library
  ([Audit & Attestation](../concepts/audit-attestation.md)).
- **Not hardened for exposure.** It binds loopback by default. Keep it there.


## concepts/index.md

# Concepts

- [Five Primitives](five-primitives.md) — templates, facts, rules, modules, functions.
- [Runtime & Working Memory](runtime-and-working-memory.md) — evaluation loop, focus stack, fail-closed salience.
- [YAML Compilation](yaml-compilation.md) — how YAML documents become CLIPS constructs.
- [Audit & Attestation](audit-attestation.md) — audit sinks, Ed25519 JWT attestation.
- [CLIPS Features Not In v1](not-in-v1.md) — COOL, backward chaining, generics, and other deferred features.


## concepts/five-primitives.md

# Five Primitives

Fathom's YAML author surface is deliberately small: five primitives that compile
one-to-one to CLIPS constructs running inside clipspy. Four of them —
**Templates**, **Rules**, **Modules**, **Functions** — are written by hand. The
fifth, **Facts**, is a runtime artifact: facts are instances of a template,
asserted by your application or by rules as they fire. Understanding where each
primitive lives (author-time vs. runtime) and what it becomes in CLIPS is the
shortest path to a working mental model of the engine.

The rest of this page walks the five in the order the engine loads them
(`templates → modules → functions → rules` — see
`src/fathom/engine.py`, `Engine.from_rules`, lines 496–570) and closes with how
they fit together at evaluate time.

## Templates

A **template** declares the schema of a kind of fact: its name, its slots, and
per-slot typing and constraints. Templates are the only way to introduce a new
fact shape; everything downstream — rule patterns, asserts, working memory —
references a template by name.

The `TemplateDefinition` model (`src/fathom/models.py`) carries:

- `name` — a CLIPS identifier.
- `description` — free prose for generated reference docs.
- `slots` — a list of `SlotDefinition`, each with a `name`, a `type`, and
  optional `required`, `allowed_values`, and `default`.
- `ttl` — optional integer time-to-live used by the fact manager.
- `scope` — `"session"` (default) or `"fleet"`.

Slot types come from the `SlotType` enum and map directly to CLIPS primitives:
`string`, `symbol`, `float`, `integer`.

A minimal template:

```yaml
templates:
  - name: session
    description: An authenticated session.
    slots:
      - { name: session_id, type: string, required: true }
      - { name: user_role,  type: symbol, allowed_values: [admin, user, guest] }
      - { name: created_at, type: integer }
```

The compiler (`compile_template`, `src/fathom/compiler.py` around line 90)
emits a single CLIPS construct scoped to the `MAIN` module:

```clips
(deftemplate MAIN::session
    (slot session_id (type STRING))
    (slot user_role  (type SYMBOL) (allowed-symbols admin user guest))
    (slot created_at (type INTEGER)))
```

Templates always live in `MAIN` so that any user-defined module can reference
them after importing `MAIN` (see Modules, below).

## Facts

A **fact** is a runtime instance of a template — the data rules actually match
against. Facts are **not** authored in YAML alongside the other primitives;
they enter working memory at runtime in one of three ways:

- The host program calls `Engine.assert_fact(template, data)` to inject facts
  from request payloads, database rows, or upstream events.
- A rule's `then:` block includes an `assert:` clause, which the compiler
  represents as an `AssertSpec` (`src/fathom/models.py`). When the rule fires,
  CLIPS materializes the spec into a new fact.
- A bundled rule pack may ship sample facts to load via `load_rules`-adjacent
  helpers, but these are still asserted at runtime, not compiled.

Two types describe facts at different lifecycle stages:

- **`AssertSpec`** — the *compile-time* description of a fact a rule will
  assert. Its `slots` are `dict[str, str]` because the values are CLIPS source
  text: either a literal, or a `?var` bound on the LHS and spliced into the
  RHS by the compiler.
- **`AssertedFact`** — a *runtime* snapshot captured for audit, with
  `slots: dict[str, Any]` because the values have been materialized by CLIPS
  back into Python types (int, str, symbol, float).

Facts in working memory persist across `Engine.evaluate` calls within the same
`Engine` instance. This is the core difference between Fathom and stateless
policy engines: forward chaining can accumulate derived facts over many
evaluations until you explicitly retract them or the session ends. The Runtime
concept page goes into the focus stack and working-memory lifecycle in more
detail.

## Rules

A **rule** is a pattern-action pair: a list of fact patterns to match, and a
`then` block describing what to do when the LHS becomes satisfied. The
`RuleDefinition` model (`src/fathom/models.py`) carries `name`, `description`,
`salience`, `when`, and `then`; rules are grouped into a `RulesetDefinition`
that declares which `module` they belong to.

Each entry in `when` is a `FactPattern` — a template name plus a list of
`ConditionEntry` items. A condition entry has four shapes, and which slots you
set determines which one the compiler emits:

1. **`slot` + `expression`** — constrain a slot with an operator (`== "admin"`,
   `> 3`, etc.).
2. **`slot` + `bind`** — capture the slot's value into a `?var` for use
   elsewhere in the rule.
3. **`slot` + `bind` + `expression`** — bind and constrain in one step.
4. **`test` alone** — a raw parenthesized CLIPS expression emitted verbatim as
   a `(test …)` conditional element; the escape hatch for calling
   Python-registered functions or CLIPS built-ins outside the allow-list.

The `then` block is a `ThenBlock` with an `action` (one of `allow`, `deny`,
`escalate`, `scope`, `route`), a `reason`, a `log` level, optional `notify`,
`attestation`, `metadata`, and `scope`, and an `asserts` list (spelled
`assert:` in YAML) of `AssertSpec` entries for facts the rule should add to
working memory when it fires.

The compiler (`compile_rule`, `src/fathom/compiler.py` around line 137) emits
a rule qualified by the owning module:

```clips
(defrule governance::deny-low-clearance
    (declare (salience -10))
    (session (user_role ?role))
    (test (eq ?role guest))
    =>
    (assert (__fathom_decision (action deny) (reason "guest role"))))
```

### Salience

`salience` is an integer; higher values fire first within a module. Fathom's
decision model is **last-write-wins**: whichever decision fact is written last
becomes the result. To make denial fail-closed by default, `deny` rules
conventionally get **lower** salience than `allow` rules so they fire *after*
allow and overwrite the allow decision. The `runtime-and-working-memory.md`
concept page and [`../how-to/writing-rules.md`](../how-to/writing-rules.md)
cover the convention and its trade-offs in depth.

## Modules

A **module** is a namespace for rules. The `ModuleDefinition` model
(`src/fathom/models.py`) is intentionally tiny — `name`, `description`, and a
`priority` integer — because a module's job at author time is just to exist
and let rules declare their home.

`compile_module` (`src/fathom/compiler.py` line 352) emits:

```clips
(defmodule governance (import MAIN ?ALL))
```

Every non-MAIN module imports all exports from `MAIN`, which is why templates
declared in `MAIN` are visible everywhere. Modules never contain inline rules
in Fathom's YAML surface; a rule's membership is set by the `module:` field on
its enclosing `RulesetDefinition`, and `compile_rule` prefixes the emitted
defrule name with `<module>::`.

At runtime, modules are the unit of ordered evaluation: the CLIPS **focus
stack** controls which module's agenda is active, and `load_modules` honours
an optional `focus_order` in the module file. Ordering, the focus stack, and
how `(focus …)` interacts with `allow`/`deny` salience belong in the Runtime
concept page.

## Functions

A **function** is a named, pure-ish computation callable from rule LHS `test`
CEs or from slot expressions. The `FunctionDefinition` model
(`src/fathom/models.py`) has `name`, `description`, `params`, an optional
`hierarchy_ref`, and a `type` field with three values:

- **`classification`** — generated from a `HierarchyDefinition` (e.g. a
  clearance ladder like `[public, confidential, secret, top-secret]`). Used
  for ordered-level comparisons; the `hierarchy_ref` points at the hierarchy
  the function is derived from.
- **`temporal`** — a time-window or decay helper generated from the YAML spec.
- **`raw`** — an escape hatch: the `body` is emitted verbatim as CLIPS source,
  so you can write any `(deffunction …)` you want.

`compile_function` (`src/fathom/compiler.py` line 359) emits one or more
`(deffunction <name> …)` constructs.

A second path exists for host-language helpers: `Engine.register_function(name,
callable)` makes a Python callable visible to CLIPS under the given name, so
a rule's `test` CE can invoke it like any other function. See
[`../how-to/register-function.md`](../how-to/register-function.md) for the
contract and the supported parameter/return shapes.

## How they fit together

The primitives layer cleanly because their dependencies run in one direction.
At load time, `Engine.from_rules` (and the `load_*` methods on `Engine`) walk
the bundle in a fixed order so every construct has what it needs:

```
templates        (deftemplate MAIN::…)
   │
   ▼
modules          (defmodule … (import MAIN ?ALL))
   │
   ▼
functions        (deffunction … )
   │
   ▼
rules            (defrule <module>::… … => … )
```

- Templates come first so module-scoped rules can pattern-match on them.
- Modules come next because rules are emitted qualified by their module.
- Functions come before rules because `test` CEs and slot expressions may
  reference them.
- Rules come last; loading a rule whose module is not yet registered is a
  `CompilationError`.

At evaluate time the picture inverts: the host asserts facts, CLIPS matches
them against rule LHS patterns, the rule with the highest salience on the
active module's agenda fires, its `then` block emits a decision and any
`AssertSpec` facts, and those new facts feed further matches. Working memory
accumulates across `evaluate` calls on the same `Engine` until you retract or
discard the session.

## Related reading

- [Writing rules](../how-to/writing-rules.md) — field-by-field reference for
  `RulesetDefinition` and the salience/last-write-wins convention.
- [Registering a function](../how-to/register-function.md) — how host-language
  callables become available to rule LHS expressions.
- [YAML reference](../reference/yaml/index.md) — generated schema index for
  every primitive's YAML surface.


## concepts/runtime-and-working-memory.md

# Runtime & Working Memory

The [Five Primitives](./five-primitives.md) page introduces the author-level
vocabulary — templates, facts, rules, modules, functions — and how each one
compiles to a CLIPS construct. This page is about the other half of the picture:
what actually happens when you call `evaluate()`. It walks the runtime's
moving parts from the top down — the session, how facts enter working memory,
the evaluation loop, module focus, and the salience/last-write-wins contract
that makes deny rules fail closed.

The mental model to hold: a Fathom engine is a stateful CLIPS environment with
a thin orchestration layer that drives the inference loop and reads a single
distinguished fact template back out.

## The engine as a session

Each `Engine` instance owns exactly one CLIPS environment, stored on
`self._env` in `src/fathom/engine.py`:

```python
self._env: clips.Environment = clips.Environment()
```

That environment is the session. Templates are built into it once at load
time, rules are built once at load time, and working memory — the full
collection of asserted facts — lives on it for as long as the engine lives.

This is the main thing that distinguishes Fathom from stateless policy
engines like OPA or Cedar. Those evaluate a single input document against a
policy and return a decision; nothing carries over. A Fathom engine can hold
facts asserted by one request, receive new facts from a second request, and
let rules that match on the combination fire on the third — all inside one
process, all without re-parsing rule sources. Rate-limit rules, trust-score
aggregation, and any policy whose correct answer depends on prior events are
expressible precisely because working memory persists across `evaluate()`
calls within a session.

Sessions do not persist to disk by default. If the process exits, working
memory goes with it. The REST layer adds a `SessionStore` with TTL and a
session cap to manage many concurrent engines — see the
[FastAPI how-to](../how-to/fastapi.md) for those transport details. This
page is about the runtime in isolation.

An engine can also be cleared without being destroyed. `Engine.reset()`
calls `env.reset()` (which clears facts and re-asserts `(initial-fact)`) and
then rebuilds the internal `__fathom_decision` template. `Engine.clear_facts()`
is narrower: it retracts only the user-asserted facts and leaves CLIPS
internals alone.

## Entry paths for facts

A fact is an instance of a template. There are three ways one lands in
working memory:

1. **REST / gRPC transports.** `POST /v1/facts` and the equivalent gRPC RPC
   call `Engine.assert_fact()` under the hood. Both transports accept one or
   many facts per request.
2. **Direct Python call.** `Engine.assert_fact(template, data)` from embedded
   use. `assert_facts([...])` is the atomic multi-fact form: every fact is
   validated against its template before any is asserted, so a bad slot value
   in the fifth entry aborts the whole batch.
3. **Rule RHS.** A rule's `then.asserts` list — modeled as `AssertSpec` in
   `src/fathom/models.py` — compiles to additional `(assert ...)` forms on
   the rule's right-hand side. When the rule fires, those facts enter working
   memory alongside the `__fathom_decision` fact that carries the rule's
   action.

In all three paths, Python values cross into CLIPS through clipspy: strings
become `STRING`, ints and floats become their CLIPS numeric counterparts,
and slot-level type constraints declared on the template are enforced at
assertion time, not at rule-fire time. Facts that successfully assert are
read back out as `AssertedFact` records in query results and audit
snapshots.

## The evaluation loop

`Engine.evaluate()` is short; the work is delegated to
`src/fathom/evaluator.py`. The sequence inside `Evaluator.evaluate()` is:

1. **Set the focus.** `_setup_focus_stack()` emits a single `(focus ...)`
   eval listing the modules in `focus_order`. `(focus A B C)` gives A the
   focus first and queues B and C behind it, so the configured order is
   the execution order.
2. **Expire TTL facts.** If a `FactManager` is wired in, expired facts are
   retracted before any rules get a chance to match them.
3. **Run to quiescence.** `self._env.run()` fires rules until no activations
   remain on any focused module.
4. **Read the winning decision.** `_read_decision()` iterates the
   `__fathom_decision` facts that rules emitted and picks a winner (next
   section).
5. **Capture traces.** `_capture_trace()` walks the same decision facts in
   order and records every rule that fired plus the modules those rules
   came from.
6. **Clean up.** All `__fathom_decision` facts are retracted so the next
   `evaluate()` call starts with a clean decision slate. User facts are
   not touched.

The return value is an `EvaluationResult` — decision, reason, rule trace,
module trace, duration in microseconds, and parsed metadata. If an
`AttestationService` is configured on the engine, the engine layer signs
the result before returning it.

One thing worth noting: `env.run()` is the single point where CLIPS does
inference. Fathom never calls it more than once per `evaluate()`. A rule
that needs to react to facts another rule asserted sees them because the
rete network is notified as part of that same run, not because the
runtime loops.

## Module focus

CLIPS's focus stack is the mechanism Fathom uses to make module ordering
deterministic. The compiler emits every non-MAIN module as:

```clips
(defmodule <n> (import MAIN ?ALL))
```

(see `Compiler.compile_module` in `src/fathom/compiler.py`). `MAIN` itself
is created with `(defmodule MAIN (export ?ALL))` the first time modules are
loaded, so every module can see the shared `__fathom_decision` template.

At evaluation, the runtime hands the focus to each registered module in
turn. CLIPS only considers activations from the module currently in focus;
when no rules in it can fire, the next module in the list gets its turn.
The first module in `focus_order` is the first to run, and
`_setup_focus_stack()` emits the list unchanged to say so.

`focus_order` comes from the `focus_order:` key in a module YAML file, or
from `Engine.set_focus(...)`. If a pack declares modules and sets neither,
`Engine.load_modules` focuses every declared module in declaration order —
without that fallback the focus list is empty, no module's agenda is ever
drained, and the caller gets the default decision back from a pack whose
rules all matched. Declaration order is a floor, not a design: state
`focus_order:` whenever the sequence matters. Each load appends to the
focus order rather than replacing it, so loading a second pack leaves the
first one enforced; only `Engine.set_focus(...)` replaces it outright.

The practical upshot is that modules are a coarse-grained ordering tool.
If `policy` comes before `logging` in the focus order, every
activatable `policy` rule fires before any `logging` rule gets a chance,
regardless of salience across module boundaries. Within a single module,
salience takes over.

Ordering is not short-circuiting: every module in the list runs. Since the
evaluator is last-write-wins, the module listed **last** writes the
decision the caller receives, so a module whose job is to overrule the
others belongs at the end of `focus_order`.

## Fail-closed salience and last-write-wins

This is the section [Five Primitives](./five-primitives.md) deferred.

When a rule with an `action` fires, its compiled right-hand side asserts a
`__fathom_decision` fact carrying the action (`allow`, `deny`, `escalate`,
`scope`, or `route`), a reason string, the rule name, and JSON-serialized
metadata. The compiler emits this block verbatim; see
`Compiler._compile_action` in `src/fathom/compiler.py`.

After `env.run()` returns, the evaluator reads decisions back in the order
they were asserted:

```python
facts = list(self._iter_decision_facts())
...
winner = facts[-1]
```

That is `src/fathom/evaluator.py`, around line 127. The last decision fact
asserted wins. Everything else about the salience contract follows from
that single line.

CLIPS fires higher-salience rules first. If an `allow` rule has salience
100 and a `deny` rule has salience 50, the `allow` rule fires first, then
the `deny` rule. Both assert `__fathom_decision` facts, both get collected,
and `facts[-1]` — the `deny` fact, the one asserted second — wins. The
final decision is deny.

That is Fathom's fail-closed default. To get it, author deny rules with
**lower** salience than allow rules. If you inverted the numbers, deny
would fire first and allow would overwrite it, which is the opposite of
what a safety-oriented policy usually wants.

A few consequences follow:

- **No rule fires ⇒ default decision.** The engine is constructed with
  `default_decision="deny"`, so an empty `facts` list after `env.run()`
  returns `("deny", "default decision (no rules fired)", {})`. The only
  way to get `allow` out of a Fathom engine is to have a rule explicitly
  assert one.
- **One rule fires ⇒ that decision wins trivially.** No ordering concerns.
- **Both fire ⇒ salience determines order, last-asserted wins.** The
  failure mode to avoid is giving deny rules *higher* salience than the
  allow rules they are supposed to override, because then deny asserts
  first and allow clobbers it.
- **Reason strings track the winner.** The `reason` field on
  `EvaluationResult` comes from the winning decision fact, not a
  concatenation of every rule that fired. If you need the full firing
  history, use `rule_trace`.

The salience field is declared on each rule's YAML and compiles to
`(declare (salience N))` inside the `defrule` — see
`Compiler.compile_rule` around line 165. Salience 0 is the default and is
omitted from the compiled output. For authoring guidance, see the
[writing rules how-to](../how-to/writing-rules.md).

## Conflict resolution within a salience bucket

What if two rules share the same salience and both activate? CLIPS uses a
conflict resolution strategy to break the tie. The default is `depth`,
which roughly prefers activations whose supporting facts were asserted
more recently. Fathom does not override this.

The strategy is deterministic given a fixed CLIPS state, but the order
it produces is an implementation detail of CLIPS, not a contract Fathom
exposes. Do not rely on it for semantic ordering. If two rules should fire
in a specific order, give them distinct salience values and make the
intent explicit in the YAML.

The pragmatic rule: use salience coarsely. A handful of well-known bands
(for example, a `deny` band below an `allow` band below a logging band) is
easier to audit than dozens of one-off values.

## How it all fits together

Reading back through the primitives with the runtime in hand:

1. **Templates** define the shape of facts and build CLIPS `deftemplate`
   constructs on the environment at load time.
2. **Facts** populate working memory — asserted through the REST/gRPC
   transports, the SDK, or by rules firing.
3. **Modules** namespace rules and are pushed onto the focus stack in the
   order the author declared; the top module runs to local quiescence
   before the next one is considered.
4. **Functions** extend the conditions and actions rules can express —
   custom CLIPS deffunctions or Python callables exposed through clipspy.
5. **Rules** fire during `env.run()` in order of salience within a module,
   asserting `__fathom_decision` facts on their RHS.
6. **The evaluator** reads the last `__fathom_decision` fact as the winner
   and returns an `EvaluationResult`.

The stateful working memory in step 2 and the last-write-wins contract in
step 6 are the two properties that most shape how a Fathom policy behaves
in practice. The rest of the runtime is plumbing arranged around them.

For the mechanics of expressing the rules themselves, see
[writing rules](../how-to/writing-rules.md).


## concepts/yaml-compilation.md

# YAML Compilation

The [Five Primitives](./five-primitives.md) page describes *what* you author
in Fathom's YAML. The
[Runtime & Working Memory](./runtime-and-working-memory.md) page describes
*what happens* once the engine is running. This page is about the pipeline
in between: how a YAML file becomes the CLIPS source string that clipspy's
`env.build()` consumes.

## The pipeline in one picture

Compilation is three stages — **parse**, **validate**, **compile** —
with two models in between:

```
YAML text
  │  yaml.safe_load()       (parse)
  ▼
Python dicts / lists
  │  Pydantic validation    (validate)
  ▼
TemplateDefinition / RuleDefinition / ModuleDefinition / FunctionDefinition
  │  Compiler.compile_*     (compile)
  ▼
CLIPS source string
  │  env.build(...)
  ▼
Construct live in the CLIPS environment
```

Parsing is boring: `yaml.safe_load` turns text into nested Python
containers. Validation is where the interesting work happens — Pydantic
rejects malformed or unsafe input *before* any CLIPS source is generated.
Compilation is a string transform: each validated model goes through a
matching `Compiler.compile_*` method that returns a CLIPS construct as
text. The compiler never talks to CLIPS directly; the engine is what
hands the output to `env.build()`.

## Why Pydantic sits in the middle

Fathom emits CLIPS source as text. clipspy's `env.build()` takes a source
string and parses it — there is no structured builder API for assembling
a `deftemplate` piece by piece. Every name and every slot value Fathom
writes into that string has to be safe.

The Pydantic layer is where that safety is enforced. Two regexes in
`src/fathom/models.py` define the safe grammar:

- `_CLIPS_IDENT_RE = r"^[A-Za-z_][A-Za-z0-9_\-]*$"` — all identifiers
  (template names, rule names, module names, function names, slot names,
  and the `AssertSpec.template` field) must match this pattern.
- `_SLOT_VAR_RE = r"^\?[A-Za-z_][A-Za-z0-9_\-]*$"` — LHS bind variables
  and `?var` references inside `AssertSpec.slots` must match this pattern.

Two helpers apply them. `_validate_clips_ident(name, kind)` is called
from field validators on every identifier field — `TemplateDefinition.name`,
`RuleDefinition.name`, `ModuleDefinition.name`, `FunctionDefinition.name`,
`RulesetDefinition.ruleset`/`module`, and `AssertSpec.template`.
`_validate_slot_value(value)` inspects each value in `AssertSpec.slots`:
values starting with `?` must match `_SLOT_VAR_RE`; values starting with
`(` must be balanced s-expressions; plain string values are accepted
provided they contain no NUL bytes.

Invalid input produces a structured Pydantic error pointing at the
offending field. Valid input produces models the compiler can emit
without further escaping of names — only slot string *literals* need
quoting, because those are the only place user-supplied text lands
inside a CLIPS double-quoted string.

## What each YAML construct compiles to

Each top-level model has a dedicated `compile_*` method on `Compiler`.

`TemplateDefinition` → a `deftemplate` scoped to `MAIN`:

```clips
(deftemplate MAIN::session
    (slot session_id (type STRING))
    (slot user_role  (type SYMBOL) (allowed-symbols admin user guest)))
```

Emitted by `compile_template`. Templates always live in `MAIN` so every
module can see them via `(import MAIN ?ALL)`. Slot type and
allowed-value directives come from `_CLIPS_TYPE_MAP` and
`_CLIPS_ALLOWED_MAP`; string defaults are quoted and escaped.

`ModuleDefinition` → a `defmodule` that imports everything from `MAIN`:

```clips
(defmodule governance (import MAIN ?ALL))
```

Emitted by `compile_module`. `ModuleDefinition` also carries a `priority`
integer, which is neither serialized into the CLIPS source nor read by the
runtime: focus order comes from the `focus_order:` key, or from declaration
order when that key is absent (see [Module](../reference/yaml/module.md)).

`RuleDefinition` → a `defrule` qualified by its owning module:

```clips
(defrule governance::deny-low-clearance
    (declare (salience -10))
    (session (user_role ?role))
    (test (eq ?role guest))
    =>
    (assert (__fathom_decision (action deny) (reason "guest role") ...)))
```

Emitted by `compile_rule(defn, module)`. A `(declare (salience N))` clause
appears only when `salience != 0`. LHS and RHS rendering are covered below.

`FunctionDefinition` → one or more `deffunction` constructs:

```clips
(deffunction MAIN::classification-rank (?level)
    (switch ?level
        (case public then 0)
        (case secret then 1)
        (default -1)))
```

Emitted by `compile_function`. The `type` field is a literal of
`"classification" | "raw"`. Classification functions come
from a `HierarchyDefinition` as a family of deffunctions (`<hier>-rank`,
`<hier>-below`, `<hier>-meets-or-exceeds`, `<hier>-within-scope`) plus
backward-compatible unscoped shims for the first hierarchy loaded.
Raw functions return `defn.body` verbatim — the escape hatch for
hand-written CLIPS. Temporal operators (`changed_within`,
`count_exceeds`, etc.) are not declared via `FunctionDefinition`; they
are Engine-registered Python externals under the reserved `fathom-`
prefix.

## The LHS: fact patterns and condition entries

Every entry in a rule's `when` list is a `FactPattern`: one template name,
an optional `alias`, and a list of `ConditionEntry` items. The compiler
emits one parenthesized pattern per `FactPattern` on the rule's LHS.

A `ConditionEntry` has four shapes, enforced by the `model_validator` on
the model:

1. **`slot` + `expression`** — constrain a slot with an operator
   (`equals(admin)`, `greater_than(3)`, and so on).
2. **`slot` + `bind`** — capture the slot's value into a `?var` that peer
   conditions and the RHS can reference.
3. **`slot` + `bind` + `expression`** — bind and constrain in one step.
4. **`test` alone** — an already-parenthesized CLIPS expression, emitted
   verbatim as a `(test …)` conditional element.

`Compiler._compile_fact_pattern` walks the list into two buckets: slot
constraints (concatenated inside the fact pattern) and test CEs (appended
*after* all fact patterns, because CLIPS semantics require tests to
reference variables already bound by earlier patterns).
`_compile_condition` produces the per-slot string, including the
`?bind&<expr>` form when a condition both binds and constrains.

Shape 4 is the primary escape hatch on the LHS. A `test` entry lets a
rule call any CLIPS-visible function, including host-language callables
registered via `Engine.register_function`. See
[Registering a Python function](../how-to/register-function.md).

## The RHS: asserts and decisions

Every `AssertSpec` in `then.asserts` becomes one `(assert …)` form on the
rule's RHS. For each spec, the compiler emits:

```clips
(assert (<template> (<slot> <value>) (<slot> <value>) ...))
```

Slot values go through `Compiler._emit_slot_value`, which is a three-way
switch mirroring `_validate_slot_value`:

- Starts with `?` → emitted verbatim as a variable reference.
- Starts with `(` → emitted verbatim as an s-expression.
- Otherwise → emitted as a quoted, escaped CLIPS string literal.

This is why `AssertSpec.slots` is typed `dict[str, str]` even though the
materialized fact can hold ints, symbols, and floats at runtime: at
compile time the value *is* a fragment of CLIPS source.

If `then.action` is set, `_compile_action` prepends an `__fathom_decision`
assert before any user asserts. `action` is emitted as a SYMBOL
(unquoted); `reason` becomes a quoted literal, or a `(str-cat …)`
expression interpolating LHS binds when it contains `{variable}`
placeholders. The engine reads `__fathom_decision` facts back at the end
of `evaluate()` to determine the winning decision — see
[Runtime & Working Memory](./runtime-and-working-memory.md). Assert-only
rules (no `action`) skip the decision block entirely.

## Safety: why the validators matter

Because Fathom builds CLIPS source by string concatenation, a malicious
or accidental identifier like `"foo) (deftemplate evil"` would break out
of the enclosing construct. Substituted into `compile_template`, it would
produce two top-level constructs where the author wrote one:

```clips
(deftemplate MAIN::foo) (deftemplate evil
    (slot ...))
```

The Pydantic layer blocks this before the compiler runs.
`_validate_clips_ident` rejects anything outside
`[A-Za-z_][A-Za-z0-9_-]*`, and `_validate_slot_value` ensures that a
slot value starting with `(` is a balanced s-expression — so an attacker
cannot smuggle a `))` that terminates the surrounding assert form.

The general rule: **if it ends up as raw text in CLIPS source without
quoting, it goes through `_validate_clips_ident` or
`_validate_slot_value` first**. Identifier validation on
`AssertSpec.template` and every slot key inside `AssertSpec.slots`
closes the loop on the RHS.

## The raw escape hatch

Some things the YAML surface does not express directly — complex
deffunctions, custom conditional elements, CLIPS-side state machinery.
Two escape hatches exist, and both sit on the far side of the safety
validators on purpose.

`FunctionDefinition(type="raw", body=...)` emits arbitrary CLIPS
function source:

```yaml
functions:
  - name: my-helper
    type: raw
    params: ["?x"]
    body: |
      (deffunction MAIN::my-helper (?x)
          (+ ?x 1))
```

`compile_function` returns `defn.body` verbatim when `type == "raw"`.
The `params` field is informational in this path; the author owns the
entire deffunction source.

`ConditionEntry(test="(my-fn ?x)")` emits an arbitrary `(test …)` CE on
the LHS:

```yaml
rules:
  - name: flag-risky
    when:
      - template: session
        conditions:
          - slot: session_id
            bind: "?sid"
          - test: "(risk-score-exceeds ?sid 0.9)"
    then:
      action: deny
      reason: risky session
```

This is how you reach anything registered via `Engine.register_function`
or any CLIPS built-in outside Fathom's operator allow-list. The `test`
field is only checked for being a non-empty parenthesized expression;
the contents are not parsed. That is the point — it is the escape hatch
— and raw passthrough sidesteps the per-operator safety net. With the
hatch comes the author's responsibility for the CLIPS it emits.

## Loading order

Compiled constructs are loaded into the CLIPS environment in a fixed
order — templates, modules, functions, rules — reflecting the dependency
graph between primitives. See
[Runtime & Working Memory](./runtime-and-working-memory.md) for why this
order is the only one that works.

## Related reading

- [Five Primitives](./five-primitives.md) — what each YAML construct means
  before compilation.
- [Runtime & Working Memory](./runtime-and-working-memory.md) — what the
  compiled constructs do once loaded.
- [Registering a Python function](../how-to/register-function.md) — the
  companion to the `test` escape hatch on the LHS.


## concepts/audit-attestation.md

# Audit & Attestation

The [Five Primitives](./five-primitives.md) page describes what rules look like
and how they compile. The [Runtime & Working Memory](./runtime-and-working-memory.md)
page describes what happens when you call `evaluate()`. This page is about what
Fathom writes down *afterwards* — the record of each decision, and the optional
cryptographic signature that turns that record into something you can show a
third party months later.

Two separate mechanisms share this page because they solve two halves of the
same problem:

- **The audit log** answers "what did the engine decide, on what inputs,
  citing which rules?" for every evaluation. It's a local, append-only record.
- **Attestation** answers "prove it." An Ed25519 signature over the decision
  and input digest lets an off-site verifier confirm the record is genuine
  without trusting the box that produced it.

The audit log is always available (default sink is a no-op). Attestation is
opt-in — you construct the engine with a key.

## Why a decision engine keeps records

Fathom is deterministic in a specific, limited sense: given the same rule
pack, the same working memory **in the same order**, and the same module
focus stack, it produces the same decision. Order matters because the winning
decision is last-write-wins — two rules at equal salience are resolved by
CLIPS activation order, which follows assertion order. The same fact *set*
supplied in a different order can therefore produce a different decision;
salience is how you make a rule's precedence explicit rather than incidental.

That determinism is only useful if you can reconstruct what happened on a
specific call weeks later — which facts were present, which rules fired,
what the final decision was.

Stateless policy engines can get away with "replay the request" — the input
fully determines the output. Fathom can't, because [working memory persists
across evaluations](./runtime-and-working-memory.md). The fact that caused a
deny today was asserted by a request three hours ago. Without a record
written at decision time, that context is gone.

The audit log is that record. Attestation adds one property on top: a
signature bound to the decision and inputs, so the record can survive
leaving the box it was written on.

## Audit log shape

Every successful evaluation produces one `AuditRecord`
(`src/fathom/models.py`):

```python
class AuditRecord(BaseModel):
    timestamp: str
    session_id: str
    input_facts: list[dict[str, Any]] | None = None
    modules_traversed: list[str]
    rules_fired: list[str]
    decision: str | None
    reason: str | None
    duration_us: int
    metadata: dict[str, str] = Field(default_factory=dict)
    asserted_facts: list[AssertedFact] | None = None
    match_evidence: list[MatchEvidence] | None = None
    attestation_token: str | None = None
```

Field by field:

- **`timestamp`** — UTC ISO-8601, set inside `AuditLog.record()` via
  `datetime.now(UTC).isoformat()`. Not taken from the caller, so clients
  can't back-date entries.
- **`session_id`** — the engine's session identifier. Lets you stitch
  evaluations together when reconstructing what a single agent did.
- **`input_facts`** — the working memory the decision was computed from,
  snapshotted by the engine *before* inference so rule-asserted facts are
  not folded in. Each entry is `{"template": <name>, "slots": {...}}`.
  Populated only at `log: full`; `log: summary` omits it. A caller
  constructing an `AuditRecord` by hand may still pass its own list.
- **`modules_traversed`** / **`rules_fired`** — copied from
  `EvaluationResult.module_trace` and `rule_trace`. The modules active
  during inference and the fully-qualified `module::rule` names in fire
  order.
- **`decision`** / **`reason`** — the `action` and `reason` read off the
  last `__fathom_decision` fact. `None` if no rule asserted a decision.
- **`duration_us`** — microseconds spent in the inference loop.
- **`metadata`** — arbitrary string key/value pairs propagated from the
  decision's rule.
- **`asserted_facts`** — populated only when at least one loaded rule
  declares an RHS `asserts` block (see below).
- **`match_evidence`** — which facts, with which slot values, made each
  rule fire. `None` unless the engine was built with `match_evidence=True`
  (see below).
- **`attestation_token`** — the JWT signed for this same evaluation, copied
  onto the record so an exported line can be checked without the caller's
  copy of the token. `None` on an engine constructed without an
  `attestation_service`.

Records are written one-per-line as JSON. JSON Lines is trivially grep-able,
`jq`-able, and concatenatable; it's what most log aggregators expect.
Append-only at the process level means a local attacker can truncate or
overwrite the file but not silently rewrite a past entry without touching
its bytes — detection lives at the filesystem boundary (log shipper,
immutable volume, or WORM bucket underneath).

## Audit sinks

`AuditSink` is a tiny `Protocol` with one method
(`src/fathom/audit.py`):

```python
@runtime_checkable
class AuditSink(Protocol):
    def write(self, record: AuditRecord) -> None: ...
```

Three implementations ship with Fathom:

- **`FileSink(path)`** — writes `record.model_dump_json() + "\n"` to the
  given file in append mode. The constructor creates parent directories and
  `touch`es the file, so pointing it at a fresh path Just Works.
- **`NullSink`** — `write()` is a no-op. This is the default when you
  construct an `Engine` without passing `audit_sink`.
- **`ChainedAttestationLog(path, service)`** (`fathom.chained_log`) — the
  same JSON Lines shape, but each line is signed and commits to the hash of
  the line before it, so deleting or reordering entries is detectable and
  not just discouraged. It needs the `attestation` extra and a signing
  service:

  ```python
  from fathom import Engine
  from fathom.attestation import AttestationService
  from fathom.chained_log import ChainedAttestationLog

  log = ChainedAttestationLog("/var/log/fathom/audit.jsonl", AttestationService.generate_keypair())
  engine = Engine(audit_sink=log)
  ```

  Verify it later with `fathom verify-chain <log> --pubkey <log>.pub.pem`.

Anything satisfying the protocol is a valid sink. A production deployment
might write to S3, publish to Kafka, call out to syslog, or fan out to
several of those — none of which Fathom provides out of the box, but all of
which are ten lines of Python on top of the protocol.

## Default is off

```python
from fathom import Engine
from fathom.audit import FileSink

engine = Engine(audit_sink=FileSink("/var/log/fathom/audit.jsonl"))
```

Without that argument, `Engine.__init__` installs a `NullSink`:

```python
self._audit_log = AuditLog(audit_sink or NullSink())
```

Audit is opt-in for a reason: many embedding contexts — tests, notebooks,
short-lived agents — have no use for a durable log, and making file I/O
mandatory would turn every `evaluate()` into a write. Production passes a
real sink; everything else keeps working with zero ceremony.

## What gets recorded when

The recording happens inside `Engine.evaluate()`, under the engine's
re-entrant lock, so a concurrent caller cannot interleave its own facts into
the snapshots below. The sequence:

1. **Pre-snapshot user facts** — but only if `self._has_asserting_rules` is
   true. That flag is set at load time when any compiled rule declares a
   non-empty `asserts` block. If no loaded rule can assert new facts, the
   snapshot is skipped entirely — there's nothing to diff against.
2. **Snapshot input facts** — `self._snapshot_input_facts()` captures the
   caller-supplied working memory the decision is about to be computed over.
   It is taken only when an `attestation_service` is configured or the audit
   log is recording, because it costs a query per template and nothing else
   consumes it. This snapshot is what `input_hash` binds (see
   [Attestation as signed proof](#attestation-as-signed-proof)) and what
   `log: full` records.
3. **Run inference** — `self._evaluator.evaluate()` returns an
   `EvaluationResult` with `decision`, `reason`, `rule_trace`,
   `module_trace`, and `duration_us`, plus the effective log level.
4. **Sign, if configured** — if the engine was constructed with an
   `attestation_service`, call
   `sign(result, self._session_id, input_facts=...)` and store the returned
   JWT on `result.attestation_token`. The input facts are not optional here:
   `sign` refuses `None`, because a token signed without inputs binds
   nothing.
5. **Diff pre/post snapshots** — a second `_snapshot_user_facts()` call,
   differenced against the pre-snapshot, yields the facts the rules
   asserted during this evaluation. Order is preserved from the post
   snapshot; equality is keyed on `(template, sorted(slots.items()))`.
6. **Record** — `self._audit_log.record(result, session_id,
   input_facts=..., asserted_facts=..., log_level=...)` constructs the
   `AuditRecord` and hands it to the sink.
7. **Metrics** — `self._metrics.record_evaluation(...)` runs in a `finally`
   so metrics are updated even if recording raised.

Two things worth flagging:

- `asserted_facts` is `None` when no loaded rule has an `asserts` block,
  and also when asserting rules exist but none fired. An empty list is
  collapsed to `None`, so the record distinguishes "didn't try to capture
  this" from "captured nothing."
- Signing happens *before* the audit record is written, which is what lets
  the record carry the token: the JWT is set on the `EvaluationResult` in
  step 4 and copied onto `AuditRecord.attestation_token` in step 6. The
  caller gets the same token on the result to forward separately if it
  wants to.

## Match evidence: which facts fired the rule

`rules_fired` names the rules; it doesn't say what they matched. Over a
working memory of a hundred facts that leaves a deny unexplainable after the
fact — you know *deny-uncleared* fired, not which agent tripped it.

`Engine(match_evidence=True)` records the basis of every firing:

```python
engine = Engine.from_rules("policy/", match_evidence=True)
engine.assert_fact("agent", {"id": "alpha", "clearance": "secret"})
engine.assert_fact("agent", {"id": "bravo", "clearance": "none"})

result = engine.evaluate()
for firing in result.match_evidence:
    print(firing.rule)                    # "governance::deny-uncleared"
    for fact in firing.facts:
        print(fact.template, fact.slots)  # agent {'id': 'bravo', 'clearance': 'none'}
```

One `MatchEvidence` entry per firing — a rule that fires twice on different
facts appears twice — and one `AssertedFact` per condition element on the
rule's left-hand side, in the order the conditions were written. Rules with
no `then.action` are covered too, so an assert-only rule that quietly seeded
a fact still explains itself.

The same list is copied onto the `AuditRecord`, so evidence survives to the
sink and through JSON serialization.

### Why it is opt-in

clipspy exposes no accessor for the facts behind an activation, so the
evidence has to be compiled in: with the flag on, every condition element
gains a pattern-address binding and every rule gains an extra `assert` on
its right-hand side. That is a change to the generated CLIPS, not a Python
wrapper around it.

Leaving the flag off is therefore free rather than merely cheap — the
compiler emits byte-identical CLIPS to what it emitted before the feature
existed, the `__fathom_evidence` template is never built, and the evaluator
returns an empty list without touching working memory. `scripts/benchmark.py`
holds single-rule evaluation to the same `<100µs` target with the feature in
the tree.

Turn it on when you need to explain decisions after the fact — compliance
review, incident forensics, debugging a rule that fires on the wrong join.
Leave it off for steady-state enforcement.

## Attestation as signed proof

`AttestationService` (`src/fathom/attestation.py`) turns an evaluation into a
JWT signed with an Ed25519 key. Construct one of two ways:

```python
from fathom.attestation import AttestationService

# Ephemeral keypair — fine for tests, wrong for production.
service = AttestationService.generate_keypair()

# Stable key — load from secure storage at startup.
service = AttestationService.from_private_key_bytes(pem_bytes)
```

Pass it to the engine alongside (or instead of) a sink:

```python
engine = Engine(
    audit_sink=FileSink("/var/log/fathom/audit.jsonl"),
    attestation_service=service,
)
```

The algorithm is `EdDSA` (PyJWT's name for Ed25519-over-JWT). Ed25519 was
picked because signatures are 64 bytes, verification is fast, and the
public-key PEM is small enough to embed in a verifier image.

The payload is deliberately narrow:

```python
{
    "iss":        "fathom",
    "iat":        int(time.time()),
    "decision":   result.decision,
    "rule_trace": result.rule_trace,
    # input_facts is the pre-inference working-memory snapshot: a list of
    # {"template": <name>, "slots": {...}} entries, in template-registry
    # order and then working-memory order within each template.
    "input_hash": sha256(json.dumps(input_facts, sort_keys=True)).hexdigest(),
    "session_id": session_id,
}
```

What's **in** the signature: the decision, the rules that produced it, the
session, an issuance timestamp, and a hash of the caller-supplied input
facts. What's **not**: the facts themselves (they're hashed, not embedded),
the reason string, the metadata dict, and the evaluation duration — those
remain in the audit log but sit outside the signed envelope. The JWT
alone proves *what* was decided; to prove *why*, pair it with the
matching audit-log line.

## Verifying an attestation

```python
from fathom.attestation import verify_token

payload = verify_token(jwt_string, service.public_key)
```

`verify_token` re-decodes the JWT with `algorithms=["EdDSA"]` and the
supplied public key, returning the payload dict. Any failure — bad
signature, malformed token, wrong algorithm — raises `AttestationError`.

The public key can be serialised for distribution:

```python
pem = service.public_key_pem()  # PEM SubjectPublicKeyInfo bytes
```

Two fields in the payload are worth calling out:

- **`iat`** gives freshness. A verifier that has its own clock and a known
  signing-key issuance window can reject tokens from outside it without
  contacting the signer.
- **`input_hash`** binds the token to a specific input fact set. A verifier
  reconstructs the hash from the inputs it has and compares; a mismatch
  means someone changed either the facts or the token.

## Threat model

What audit + attestation *do* protect against:

- **Disputes about what was decided.** A signed `decision` and `rule_trace`
  pin down the answer and the rules that produced it.
- **Tampering with the decision in an exported log.** Each line carries the
  JWT signed for that evaluation, and the payload commits to `decision`,
  `rule_trace`, `session_id`, `iat`, and `input_hash`. Change any of those
  in the line and it no longer matches the claims `verify_token` returns;
  re-signing needs the private key.

  What this does **not** cover: `reason`, `metadata`, `duration_us`,
  `input_facts`, and `asserted_facts` sit outside the signed payload, so a
  line whose token verifies is not thereby proof that those fields are
  untouched. Nor does a per-line signature detect a line being *deleted* —
  for that you need order and continuity, which is what
  `fathom.chained_log.ChainedAttestationLog` and `fathom verify-chain`
  provide: each entry commits to the hash of the one before it, so a
  removed or reordered entry breaks the chain.
- **Input substitution.** The `input_hash` commits the token to a specific
  set of facts — template name and slot values both. Swap a fact, change a
  slot, or move a fact to a different template, and the hash stops matching.

  Two limits worth knowing. The snapshot is grouped by template (registry
  order) and then by working-memory order within each template, so
  reordering facts *of the same template* changes the hash, while
  interleaving facts of *different* templates differently does not. And
  since cross-template order can change the decision (last-write-wins,
  above) while leaving the hash alone, `input_hash` is not by itself proof
  that a given fact set could only have produced that decision.

What they *don't* protect against:

- **A compromised engine.** If the process producing audit records is
  controlled by an attacker, it simply never calls `AuditLog.record()`, or
  signs a fabricated result. Fathom cannot attest to its own integrity;
  that's the job of whatever loads the binary.
- **Private-key theft.** Ed25519 is only as strong as the secrecy of the
  signing key. Key custody is out of scope.
- **Side channels.** Nothing here prevents an observer from inferring
  decisions from timing, cache behaviour, or downstream effects.

## Declaring attestation on a rule

`ThenBlock` carries an `attestation: bool` field
(`src/fathom/models.py`). It is compiled into the `__fathom_decision`
fact's `attestation` slot — the `TRUE`/`FALSE` value is visible to
anything reading the decision fact and surfaces through the audit log's
decision chain. It is **not** a switch that turns JWT signing on or off:
the engine decides whether to sign based on whether an
`attestation_service` was passed to `Engine(...)`, not on the flag in the
rule. Think of the rule-level `attestation` field as declarative
metadata — "this rule claims its decisions should be attested" — that
downstream consumers (audit readers, policy linters) can act on.

## How they fit together

Audit is the always-on local story: every evaluation gets one
`AuditRecord`, written synchronously to whatever sink the engine was given.
`NullSink` by default, `FileSink` for development, anything that satisfies
the `AuditSink` protocol in production.

Attestation is the optional portable story: construct the engine with an
`AttestationService` and each `EvaluationResult` comes back carrying an
Ed25519-signed JWT. The token travels independently of the log; the log
keeps the full context; together they give a downstream auditor everything
they need.

See [Runtime & Working Memory](./runtime-and-working-memory.md) for the
evaluation loop these records describe, and
[Writing Rules](../how-to/writing-rules.md) for the YAML-level `attestation`
flag on a rule's `then` block.


## concepts/not-in-v1.md

# CLIPS Features Not In v1

Fathom is not "CLIPS with YAML." It's a curated subset of CLIPS with an
opinionated authoring surface — the [Five Primitives](./five-primitives.md) —
and most of CLIPS's surface area is deliberately left out. This page lists
what's missing and why, so if you come from a CLIPS background and find
yourself hunting for a feature that isn't there, you can stop hunting.

There are two lists below. The first is the short, canonical list from
the original v1 design — three features that are explicitly deferred. The second is
longer and less formal: parts of CLIPS that simply aren't plumbed through the
YAML grammar or the Python API. Those aren't *forbidden* — a determined
embedder can still hand-build raw CLIPS constructs — but they sit outside the
Pydantic safety layer that makes authored rules reviewable.

## Explicitly deferred (per the original v1 design)

These three are called out by name in the design document's
"Explicitly Not in v1" section.

### COOL — the CLIPS Object System

CLIPS ships a full object layer — `defclass`, `definstances`,
`defmessage-handler`, slot inheritance, message dispatch. It's powerful,
and it's a different mental model from the rule-and-fact model Fathom is
built around. The design note is blunt: the OOP layer "adds massive surface
area," and the use cases it covers are covered well enough by Templates plus
Functions.

What to use instead: model your domain with `TemplateDefinition` slots and
express behavior with rules that match those templates. If you want shared
computation, lift it into a `FunctionDefinition`.

### Backward chaining

CLIPS supports goal-driven reasoning — "does this fact follow from what we
know?" — in addition to the forward chaining Fathom uses. Fathom's target
problems (governance, routing, classification) are natural fits for forward
chaining: you have a situation, you want to know what decision applies.
The design note marks backward chaining as a v2 consideration.

`Engine.__init__` used to accept an `experimental_backward_chaining` flag
that had no behavioural effect. It was **removed in 1.0** rather than
shipped as a permanent no-op that reads like a feature: passing it now
raises `TypeError`. There is no replacement flag — backward chaining
remains a v2 consideration.

What to use instead: frame the question as forward chaining. Assert the
facts that describe the situation, `evaluate()`, and read the decision.

### Generic functions and message handlers

CLIPS's `defgeneric` / `defmethod` lets you dispatch a function call to
different implementations based on argument types — the procedural equivalent
of method resolution in an OO language. The design note calls it
"over-engineering for the current problem space," and that's accurate: the
rule pack is already a dispatch layer, and `deffunction` plus clear naming
covers the rest.

What to use instead: author distinct `FunctionDefinition` instances with
clear names, or do the type branching inside a single function body.

## Not exposed in the YAML grammar

These aren't in the design doc's deferred list, but they aren't surfaced
in the authored YAML or the Pydantic models either. They're omitted by
absence rather than by policy.

### `defglobal` — global variables

CLIPS `defglobal` lets you declare module-scoped mutable variables
(`?*max-retries*`, for instance) that rules can read and write. Fathom has
no top-level YAML key for globals, and no `GlobalDefinition` in
`models.py`. The design choice is that all interesting state should live
in working memory — as typed facts — where it's visible to the audit log,
queryable, and versioned through the normal assert/retract path.

What to use instead: model what you'd put in a global as a small
single-instance template (e.g. `config` with the knobs as slots), assert one
fact at session start, and match against it in rules that need it.

### `deffacts` — initial facts blocks

CLIPS `deffacts` lets a construct file declare facts that get asserted
when the environment is reset. Fathom's rule packs have no `facts:` section
that auto-asserts on engine start — facts enter working memory through
`Engine.assert_fact()`, the REST/gRPC fact endpoints, or the RHS of a rule.
The compiler never emits `deffacts`.

What to use instead: if you need a consistent set of starting facts, assert
them from your application's bootstrap code right after constructing the
engine, or drive them from a rule that fires once under a startup focus.

### Logical CEs (truth maintenance)

CLIPS supports `(logical ...)` on the LHS of a rule: facts asserted by that
rule's RHS are *logically dependent* on the matched LHS facts, and when a
supporting fact is retracted, the derived facts are retracted with it.
Fathom's `ConditionEntry` doesn't model `logical`, and the compiler doesn't
emit it. Truth maintenance is useful but carries subtle semantics, and
getting it wrong is hard to debug.

What to use instead: keep dependencies explicit. If a derived fact should
go away when an input fact does, retract it yourself — either from a rule
that watches for the input's absence, or from application code after the
next `evaluate()`.

### Conflict-resolution strategy configuration

CLIPS exposes several strategies for picking which activation fires next
when multiple rules are ready: `depth`, `breadth`, `lex`, `mea`, `random`,
`simplicity`, `complexity`. Fathom uses the CLIPS default (`depth`) and does
not expose a configuration knob. The [Runtime & Working Memory](./runtime-and-working-memory.md)
page explains how salience and the module focus stack give you enough
ordering control for the problem classes Fathom targets.

What to use instead: set explicit `salience` on rules that must fire in a
particular order, and use modules plus focus to partition evaluation into
phases.

### Runtime agenda inspection

CLIPS provides `(get-agenda)`, `(refresh-agenda)`, and similar functions to
inspect or manipulate the agenda — the queue of rule activations — at
runtime. Fathom's Python API doesn't expose these, and there's no YAML way
to ask "what's about to fire?" from inside a rule pack.

What to use instead: inspect the `rule_trace` and `module_trace` fields on
the `EvaluationResult` after the fact. That tells you what fired and in
which module, which is what most agenda questions are really asking.

### Pattern-network and debug introspection

CLIPS has `(watch rules)`, `(watch facts)`, `(dribble-on)`, and a suite of
debug hooks that dump RETE activity to stderr. Fathom doesn't expose these
as YAML features or Python API, because the audit log and the
`rule_trace` / `module_trace` fields on `EvaluationResult` cover the same
need with structured output that a host application can parse.

What to use instead: read the audit log for decisions, and the evaluation
traces for the firing sequence.

### FuzzyCLIPS and temporal CLIPS extensions

These are third-party CLIPS extensions (fuzzy-set reasoning; temporal
operators). They aren't part of base CLIPS, `clipspy` doesn't ship them, and
Fathom hasn't re-implemented them.

What to use instead: encode uncertainty or time explicitly as slot values
(confidence scores, timestamps) and reason about them with ordinary
conditions and custom functions.

## The raw escape hatch

The YAML grammar is a safety layer, not a cage. When you genuinely need
CLIPS expressiveness that the authored surface doesn't reach, Fathom offers
three documented escape hatches:

- **`FunctionDefinition(type="raw", body=...)`** — write a CLIPS
  `deffunction` body verbatim. Useful for CLIPS built-ins that Fathom's
  expression operators don't cover, or for multi-line CLIPS logic that
  would be awkward to shoehorn into a structured function.
- **`ConditionEntry(test=...)`** — emit a raw `(test ...)` conditional
  element on the rule LHS. This is the most common use of the escape hatch:
  calling a custom Python function you've registered.
- **`Engine.register_function(name, fn)`** — expose a Python callable as a
  CLIPS external function, callable from rule RHS actions or from `test`
  conditional elements. See
  [Register a function](../how-to/register-function.md) for the recipe.

The [YAML Compilation](./yaml-compilation.md) page describes the compiler's
raw-passthrough paths in more detail. The rule of thumb: prefer the
structured YAML surface, and reach for raw only when the alternative would
be contorted.

## What's shipped vs what's deferred

| Feature | Status |
|---|---|
| Templates (`deftemplate`) | Shipped |
| Facts (assert/retract/query) | Shipped |
| Rules (`defrule`) with salience | Shipped |
| Modules + focus stack | Shipped |
| Functions (`deffunction`) — structured + raw | Shipped |
| Custom Python functions via `register_function` | Shipped |
| Audit log + attestation | Shipped |
| COOL (`defclass`, message handlers) | Not in v1 |
| Backward chaining | v2 consideration |
| Generic functions (`defgeneric`, `defmethod`) | Not in v1 |
| `defglobal` | Not exposed |
| `deffacts` | Not exposed |
| Logical CEs / truth maintenance | Not exposed |
| Conflict-resolution strategy config | Uses CLIPS default (`depth`) |
| Agenda inspection at runtime | Not exposed |
| `watch` / `dribble` debug hooks | Not exposed (use `rule_trace`) |
| FuzzyCLIPS / temporal extensions | Not shipped |

## Versioning

v1 is the Phase 3 milestone on the Fathom roadmap. "v2 considerations" —
notably backward chaining — are possibilities, not commitments, and the
design document doesn't promise dates. If a feature on this page matters
to you, open an issue describing the use case; that's the input the v2
scoping will work from.


## reference/index.md

# Reference

Every public surface of Fathom is documented here.

Most pages under this tab are generated from source — the SDK, REST, gRPC, MCP,
CLI, and rule-pack references are rewritten by `make docs-gen`, so hand edits to
them are overwritten on the next build. The YAML pages and
[Configuration](configuration.md) are hand-written and cite the files they
describe in their frontmatter.

Which of these surfaces the project promises to keep working is
[VERSIONING.md](https://github.com/KrakenNet/fathom/blob/main/VERSIONING.md).

## Deployment

- [Configuration](configuration.md) — every `FATHOM_*` variable, gRPC TLS, and
  the server token scopes

## SDKs

- [Python SDK](python-sdk/index.md)
- [Go SDK](go-sdk/index.md)
- [TypeScript SDK](typescript-sdk/index.md)

## APIs

- [REST](rest/index.md) · [Try It](rest/try.md)
- [gRPC](grpc/index.md)
- [MCP Tools](mcp/index.md)

## YAML

- [Schemas](yaml/index.md)

## Tooling

- [CLI](cli/index.md)
- [VSCode snippets + schemas](tooling/vscode/index.md)

## Rule Packs

- [OWASP Agentic](rule-packs/owasp-agentic.md)
- [NIST 800-53](rule-packs/nist-800-53.md)
- [HIPAA](rule-packs/hipaa.md)
- [CMMC](rule-packs/cmmc.md)


## reference/configuration.md

# Configuration

Fathom as a library takes its configuration through `Engine(...)` arguments and
needs no environment. Everything below configures the **servers** — the REST
app, the gRPC server, and the CLI's client commands.

Each variable is read from the process environment. Where a variable is read
per request rather than once at import, the table says so: those can be changed
without a restart.

## All variables

| Variable | Read by | Default | Effect |
|---|---|---|---|
| `FATHOM_API_TOKEN` | REST, gRPC | *(none — required)* | The data-plane bearer token. Every `/v1/*` route and every RPC compares the presented `Authorization: Bearer …` against it with `hmac.compare_digest`. Unset means server mode refuses to authenticate anything. Read per verification. |
| `FATHOM_ADMIN_TOKEN` | REST, gRPC | *(unset)* | Scopes the reload surface. When set, `POST /v1/rules/reload` and the `Reload` RPC accept **only** this token and the data-plane token stops working for them. When unset, reload falls back to `FATHOM_API_TOKEN`. |
| `FATHOM_RULESET_ROOT` | REST | *(none — required)* | Directory that every caller-supplied ruleset path is jailed under. A request naming a path outside it is rejected with 400; an unset root is a 500, never an unjailed read. |
| `FATHOM_RULESET_PUBKEY_PATH` | REST | *(none)* | PEM path of the public key that signed rulesets are verified against. Required at startup unless the dev escape below is engaged — a missing or unreadable key is a startup failure, not a warning. |
| `FATHOM_ALLOW_UNSIGNED_RULESETS` | REST | `0` | Set to `1` **and** build the app with `require_signature=False` to accept unsigned reload payloads. Both are required; either alone keeps signature checking on. Logs a warning on every boot that uses it. |
| `FATHOM_MAX_REQUEST_BYTES` | REST | `2097152` (2 MB) | Body cap on the data-plane routes, enforced on bytes actually received rather than `Content-Length`. `/v1/evaluate` runs a quadratic join over the facts it is given, so this bounds server CPU, not just memory. A non-numeric or non-positive value falls back to the default. Read per request. |
| `FATHOM_MAX_RELOAD_BYTES` | REST, gRPC | `5242880` (5 MB) | Body cap on the reload route specifically, which the data-plane cap skips. gRPC's own receive limit is set below this. Same fallback behaviour. Read per request. |
| `FATHOM_EXPOSE_DOCS` | REST | `0` | Set to `1` to serve `/docs`, `/redoc`, and `/openapi.json`. Off by default because they publish route and schema names to unauthenticated callers. Read once at import. |
| `FATHOM_GRPC_TLS_CERT` | gRPC | *(none)* | PEM certificate path. Required with `FATHOM_GRPC_TLS_KEY` unless insecure mode is opted into. |
| `FATHOM_GRPC_TLS_KEY` | gRPC | *(none)* | PEM private-key path, paired with the certificate above. |
| `FATHOM_GRPC_ALLOW_INSECURE` | gRPC | `0` | Set to `1` to bind a plaintext port when no TLS pair is configured. See the warning below. |
| `FATHOM_METRICS` | Engine | `0` | Set to `1` to enable Prometheus metrics collection without passing `enabled=True` to `MetricsCollector`. No-ops when `prometheus_client` is not installed, so setting it is safe on an install without the `metrics` extra. |
| `FATHOM_TOKEN` | CLI | *(unset)* | Default bearer token for CLI commands that call a server. `--token` overrides it. |

## gRPC TLS

**The gRPC server refuses to start without TLS.** `serve()` binds a secure port
only when both `FATHOM_GRPC_TLS_CERT` and `FATHOM_GRPC_TLS_KEY` name readable
PEM files:

```bash
export FATHOM_GRPC_TLS_CERT=/etc/fathom/tls/server.crt
export FATHOM_GRPC_TLS_KEY=/etc/fathom/tls/server.key
export FATHOM_API_TOKEN="$(cat /run/secrets/fathom-api-token)"
```

```python
from fathom.integrations.grpc_server import serve

server = serve(port=50051)
server.wait_for_termination()
```

With neither the pair nor an explicit opt-in, `serve()` raises `RuntimeError`
naming the three variables. That is deliberate: the bearer token travels in an
RPC metadata header, so an insecure port hands every token to any passive
observer on the path.

The opt-in exists for local development and for deployments that terminate TLS
in a sidecar on the same host:

```bash
export FATHOM_GRPC_ALLOW_INSECURE=1   # plaintext port; token sent in the clear
```

Do not set it on a port reachable from another host. If a service mesh already
terminates TLS, keep the plaintext listener bound to loopback.

Client-certificate authentication (mTLS) is not wired up: the server passes a
single key pair to `grpc.ssl_server_credentials` and does not request a client
certificate. Callers are authenticated by bearer token only.

## Server tokens

Both server surfaces authenticate the same way — a bearer token compared in
constant time — and both distinguish two scopes:

- **Data plane** (`FATHOM_API_TOKEN`): evaluate, assert, query, retract.
- **Admin** (`FATHOM_ADMIN_TOKEN`): ruleset reload.

Setting only `FATHOM_API_TOKEN` means one token does both. Setting both means
a leaked data-plane token cannot swap the policy the server enforces, which is
the split worth having in production. See
[Hot-reloading rulesets](../how-to/hot-reload.md) for the rest of the reload
security model.

## What is not configurable by environment

- **Rule paths and engine options** — `Engine(...)` arguments: audit sink,
  attestation service, session id, evaluation `run_limit`, `match_evidence`.
- **Ports and worker counts** — `serve(port=…, max_workers=…)` for gRPC, and
  the ASGI server's own flags for REST (`uvicorn --port`).
- **Per-rule behaviour** — salience, module focus, and log level are properties
  of the ruleset, not of the deployment.


## reference/python-sdk/index.md

# Python SDK Reference

Generated from `fathom.__all__` at docs-build time via mkdocstrings.

## Public symbols

- [`AssertSpec`](assertspec.md)
- [`AssertedFact`](assertedfact.md)
- [`AttestationError`](attestationerror.md)
- [`AttestationService`](attestationservice.md)
- [`AuditLog`](auditlog.md)
- [`AuditRecord`](auditrecord.md)
- [`AuditSink`](auditsink.md)
- [`ChainedAttestationLog`](chainedattestationlog.md)
- [`CompilationError`](compilationerror.md)
- [`Engine`](engine.md)
- [`EvaluationError`](evaluationerror.md)
- [`EvaluationLimitError`](evaluationlimiterror.md)
- [`EvaluationResult`](evaluationresult.md)
- [`FactStore`](factstore.md)
- [`FathomError`](fathomerror.md)
- [`FileSink`](filesink.md)
- [`FleetConnectionError`](fleetconnectionerror.md)
- [`FleetEngine`](fleetengine.md)
- [`FleetError`](fleeterror.md)
- [`InMemoryFactStore`](inmemoryfactstore.md)
- [`NullSink`](nullsink.md)
- [`ScopeError`](scopeerror.md)
- [`ValidationError`](validationerror.md)
- [`verify_chain`](verify_chain.md)
- [`verify_token`](verify_token.md)


## reference/go-sdk/index.md

# Go SDK

Module path: `github.com/KrakenNet/fathom-go`

- **Generated reference:** [`fathom-go.md`](fathom-go.md)
- **Source:** [`packages/fathom-go`](https://github.com/KrakenNet/fathom/tree/master/packages/fathom-go)
- **pkg.go.dev:** [pkg.go.dev/github.com/KrakenNet/fathom-go](https://pkg.go.dev/github.com/KrakenNet/fathom-go)


## reference/typescript-sdk/index.md

# TypeScript SDK

Package path: `packages/fathom-ts`

- **Generated reference:** see module pages alongside this index.
- **Source:** [`packages/fathom-ts/src`](https://github.com/KrakenNet/fathom/tree/master/packages/fathom-ts/src)


## reference/rest/index.md

# REST API

Fathom exposes the engine over HTTP via FastAPI. The canonical schema is
exported at [`openapi.json`](openapi.json) and is regenerated on every
build.

## Quick links

- **Interactive try-it:** [Swagger UI](try.md)
- **Raw schema:** [`openapi.json`](openapi.json)
- **Postman:** *Import → OpenAPI* pointed at `openapi.json`.
- **Insomnia:** use *Import from URL* pointed at `openapi.json`.

## Reference

<swagger-ui src="./openapi.json"/>


## reference/rest/try.md

# Try It — REST API

<swagger-ui src="./openapi.json"/>


## reference/grpc/index.md

# gRPC API

Fathom's gRPC service is defined in
[`protos/fathom.proto`](https://github.com/KrakenNet/fathom/blob/master/protos/fathom.proto).

- **Generated reference:** [`fathom.md`](fathom.md)
- **Raw `.proto`:** [`fathom.proto`](fathom.proto)

The Go SDK's typed client wraps this service — see
[Go SDK reference](../go-sdk/index.md).


## reference/mcp/index.md

# MCP Tool Manifest

Raw manifest: [`manifest.json`](manifest.json)

| Tool | Description |
|---|---|
| [`fathom.assert_fact`](assert_fact.md) | Assert a fact into working memory |
| [`fathom.evaluate`](evaluate.md) | Run forward-chain evaluation |
| [`fathom.query`](query.md) | Query working memory |
| [`fathom.retract`](retract.md) | Retract facts from working memory |


## reference/yaml/index.md

# YAML Reference

Fathom's YAML authoring surface has one JSON Schema per construct.
Schemas are regenerated from `fathom.models` on every docs build.

## Per-construct reference

| Construct | Page |
|---|---|
| Template | [Template](template.md) |
| Rule | [Rule](rule.md) |
| Module | [Module](module.md) |
| Function | [Function](function.md) |
| Fact | [Fact](fact.md) |

## Downloads

| Construct | Schema |
|---|---|
| Template | [`template.schema.json`](schemas/template.schema.json) |
| Rule | [`rule.schema.json`](schemas/rule.schema.json) |
| Module | [`module.schema.json`](schemas/module.schema.json) |
| Function | [`function.schema.json`](schemas/function.schema.json) |
| Hierarchy | [`schemas/hierarchy.schema.json`](schemas/hierarchy.schema.json) |

See [VSCode tooling](../tooling/vscode/index.md) for editor setup.


## reference/yaml/template.md

# Template

A **template** declares the shape of a fact: its name and the typed slots
that facts of that kind carry. Templates compile to CLIPS `deftemplate`
constructs and live in the `MAIN` module regardless of which rule module
references them. For the conceptual role of templates among Fathom's five
primitives, see [Five Primitives](../../concepts/five-primitives.md).

## Top-level fields — `TemplateDefinition`

| Field         | Type                              | Default     | Required | Description                                                                                          |
|---------------|-----------------------------------|-------------|----------|------------------------------------------------------------------------------------------------------|
| `name`        | `str`                             | —           | yes      | CLIPS identifier. Must match `^[A-Za-z_][A-Za-z0-9_\-]*$`. Emitted as `(deftemplate MAIN::<name> …)`. |
| `description` | `str`                             | `""`        | no       | Author-facing prose. Not emitted to CLIPS.                                                           |
| `slots`       | `list[SlotDefinition]`            | —           | yes      | One or more slot definitions. Empty list is accepted by Pydantic but rejected at compile time.       |
| `ttl`         | `int \| None`                     | `None`      | no       | Fact-expiry metadata (seconds). Not emitted to CLIPS; reserved for runtime fact-expiry.              |
| `scope`       | `Literal["session", "fleet"]`     | `"session"` | no       | Authoring hint consumed by the fleet layer. Not emitted to CLIPS.                                    |

The `name` field is validated by `_name_must_be_clips_ident` at model
construction and re-checked (for emptiness) by `compile_template`.

## Slot fields — `SlotDefinition`

| Field            | Type                              | Default | Required | Description                                                                                                                       |
|------------------|-----------------------------------|---------|----------|-----------------------------------------------------------------------------------------------------------------------------------|
| `name`           | `str`                             | —       | yes      | Slot identifier. No regex validator is applied in the current model; the compiler emits it verbatim into `(slot <name> …)`.       |
| `type`           | `SlotType`                        | —       | yes      | One of `string`, `symbol`, `float`, `integer`. See [SlotType](#slottype-enum).                                                    |
| `required`       | `bool`                            | `False` | no       | If True, FactManager rejects asserts that omit this slot with a ValidationError (src/fathom/facts.py:274-283). Not emitted to CLIPS; the rule-RHS assert path bypasses this check because it doesn't run through FactManager._validate. |
| `allowed_values` | `list[str] \| None`               | `None`  | no       | Emitted only for `string` and `symbol` slots (see [`_CLIPS_ALLOWED_MAP`](#slottype-enum)). Silently ignored for numeric slots.    |
| `default`        | `str \| float \| int \| None`     | `None`  | no       | Emitted as `(default <value>)`. String defaults are CLIPS-quoted and escaped; numeric defaults are emitted raw.                   |

## SlotType enum

`SlotType` is a `StrEnum` with four members. The compiler maps each to a
CLIPS type keyword via `_CLIPS_TYPE_MAP` and — for the two symbolic
types — to an allowed-values directive via `_CLIPS_ALLOWED_MAP`.

| YAML value | CLIPS type keyword | Allowed-values directive |
|------------|--------------------|--------------------------|
| `string`   | `STRING`           | `allowed-strings`        |
| `symbol`   | `SYMBOL`           | `allowed-symbols`        |
| `float`    | `FLOAT`            | *(none — silently dropped)* |
| `integer`  | `INTEGER`          | *(none — silently dropped)* |

Source: `src/fathom/compiler.py` lines 29–40.

## CLIPS emission

`compile_template` (in `src/fathom/compiler.py`) emits a multi-line
`deftemplate` form. Every template is scoped to the `MAIN` module;
per-module scoping happens on `defrule`, not on `deftemplate`.

### YAML input

```yaml
templates:
  - name: access-request
    description: An agent's request to perform an action on a resource.
    slots:
      - name: subject
        type: symbol
      - name: action
        type: string
        allowed_values: [read, write, delete]
      - name: amount
        type: integer
        default: 0
```

### CLIPS output

```
(deftemplate MAIN::access-request
    (slot subject (type SYMBOL))
    (slot action (type STRING) (allowed-strings "read" "write" "delete"))
    (slot amount (type INTEGER) (default 0)))
```

### Emission rules

1. First line: `(deftemplate MAIN::<name>`.
2. Each slot is indented four spaces and wrapped as
   `(slot <name> <parts>)`.
3. Slot parts are emitted in this fixed order: `(type <CLIPS_TYPE>)`,
   `(<allowed-directive> <values>)` (string/symbol only), `(default <value>)`.
4. For `string` slots, each `allowed_values` entry and any string default
   is run through `_escape_clips_string` (backslash first, then
   double-quote) and wrapped in `"…"`. `symbol` allowed values are emitted
   unquoted.
5. Numeric defaults (`int`, `float`) are emitted via `str(slot.default)`
   with no quoting.
6. The template closes with `)` on its own line.

## Validators

Pydantic-level rejections (raised at `TemplateDefinition(...)` or during
YAML load):

- Any key not listed in the tables above, on either model — both
  `TemplateDefinition` and `SlotDefinition` are `extra="forbid"`, so a
  misspelled field is an error rather than a silently dropped line.
- `TemplateDefinition.name` that does not match
  `^[A-Za-z_][A-Za-z0-9_\-]*$` → `ValueError` from
  `_name_must_be_clips_ident`.
- `SlotDefinition.name` that does not match the same pattern → `ValueError`
  from `_validate_clips_ident`.
- On a `symbol` slot, an `allowed_values` entry that is not a CLIPS
  identifier → `ValueError`. Symbol allowed values are emitted **unquoted**,
  so an unchecked entry closes the `deftemplate` and appends constructs of
  its own.
- On a `symbol` slot, a string `default` that is not a CLIPS identifier →
  `ValueError`, for the same reason.
- On an `integer` or `float` slot, a string `default` of any kind →
  `ValueError`: the value is interpolated verbatim, so it has to be numeric.

`string` slots are exempt from the last three: their values are escaped and
quoted on the way out, so no content in them can escape the construct.

Compile-time rejections (raised by `Compiler.compile_template`):

- `name` that is an empty string → `CompilationError` with
  `construct="template:<empty>"`.
- `slots` is an empty list → `CompilationError` with
  `construct="template:<name>"`. Note: the `slots` field has no Pydantic
  `min_length=1` constraint, so an empty list passes model validation
  and only fails at compile time.

## What is not emitted

These YAML fields are accepted by the model but do not appear in the
compiled CLIPS `deftemplate`:

- `TemplateDefinition.description` — metadata only.
- `TemplateDefinition.ttl` — reserved for runtime fact-expiry; has no
  CLIPS counterpart. A template with `ttl: 300` emits the same
  `deftemplate` as one without.
- `TemplateDefinition.scope` — consumed by the fleet layer, not the
  compiler.
- `SlotDefinition.required` — the flag itself is not emitted to CLIPS.
  Runtime enforcement happens in `FactManager._check_required`
  (`src/fathom/facts.py:274-283`) on the SDK and REST paths, but it is
  not checked on the rule-RHS assert path.
- `SlotDefinition.allowed_values` on `float` or `integer` slots —
  silently dropped because these types have no entry in
  `_CLIPS_ALLOWED_MAP`.

If you need one of these to influence runtime behavior today, enforce it
in a rule (for example, an explicit `allowed-values`-style test CE for a
numeric slot) rather than relying on the template to do so.

## See also

- [Five Primitives](../../concepts/five-primitives.md) — conceptual
  overview of templates, facts, rules, modules, and functions.
- [YAML Compilation](../../concepts/yaml-compilation.md) — how YAML
  documents are loaded and compiled to CLIPS source.
- [Audit & Attestation](../../concepts/audit-attestation.md) — how
  asserted-fact slots surface in audit records.


## reference/yaml/rule.md

# 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](../../concepts/five-primitives.md);
for how salience and last-write-wins interact at evaluation time see
[Runtime & Working Memory](../../concepts/runtime-and-working-memory.md).

## 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`](#thenblock-fields) 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

```yaml
- slot: role
  expression: equals(admin)
```

Emits a CLIPS slot constraint via `_compile_condition`. See
[Supported operators](#supported-operators-in-expression).

### Shape 2 — slot + bind (no expression)

```yaml
- slot: subject_id
  bind: ?sid
```

`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

```yaml
- test: (my-fn ?sid)
```

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.

```yaml
- slot: amount
  bind: ?amt
  expression: greater_than(100)
  test: (policy-allows ?amt)
```

### What the validator rejects

- Empty entry (no `expression`, `bind`, or `test`).
- `slot` set but neither `expression` nor `bind` provided.
- `slot` set alongside a standalone `test` with no `expression`/`bind` —
  the slot would have no effect; drop it or add an `expression`/`bind`.
- `bind` that does not start with `?`.
- `test` that is empty or not parenthesized.

## Supported operators in `expression`

### Grammar

An `expression` is exactly one operator application:

```
expression := operator "(" argument ")"
operator   := [A-Za-z_][A-Za-z0-9_]*
```

**Lexing.** The operator is everything before the *first* `(`; the argument
is everything between that `(` and the *final* `)` of the string. The
expression must end with `)`.

**Balance.** For every operator except `contains` and `matches`, the
parentheses in the argument must balance and may close only on that final
character. This is what stops an argument from closing the pattern early and
smuggling extra conditional elements into the generated `defrule`
(`equals(a1)) (admin (level 9)` is rejected). Text inside a double-quoted
CLIPS string does not count toward the balance, and `\` escapes the next
character.

`contains` and `matches` are exempt: the compiler emits their argument as an
escaped, quoted CLIPS string (`(str-index "…" ?v)` and
`(fathom-matches ?v "…")`), so it cannot break out whatever it holds. Bare
parentheses are therefore legal in those two — `matches([)])` and
`contains(a :-) b)` are valid — which matters because `(` and `)` are
literal inside a regex character class.

**Argument forms.** An argument is one of:

- a **cross-fact reference**, `$alias.field`, resolved to the CLIPS variable
  `?alias-field`. The alias must be declared as `alias:` on another pattern
  in the same rule (see **Aliases** below);
- a **list**, `[a, b, c]`, for `in` and `not_in` only;
- a **literal**, everything else.

**How a literal is emitted** depends on the declared type of the slot it is
compared against, which is why compiling a rule file in isolation can differ
from compiling it as part of a ruleset:

| Slot type | Emitted as | Example |
|---|---|---|
| `string` | quoted CLIPS string, escaped | `equals(a@b.com)` → `(id "a@b.com")` |
| `symbol`, `integer`, `float`, `boolean` | bare CLIPS token | `equals(admin)` → `(role admin)` |

Compiling without the templates is not a mistake CLIPS reliably catches.
`equals` and `not_in` emit a pattern constraint, so an unquoted literal
against a `string` slot is rejected at load time (`[CSTRNCHK1]`) —
but `not_equals` and `in` emit a `:(...)` predicate, which CLIPS does not
type-check, and a symbol never equals a string: the rule builds clean and
then decides every fact the wrong way. Always compile against the
templates — `fathom compile` on a ruleset directory, or on a rule file
whose sibling `templates/` directory is present, does this for you.

**Aliases.** `alias:` on a fact pattern names it for `$alias.field`
references. An alias must start with `$` followed by a CLIPS identifier, may
not be `$p<number>` (that namespace is generated for unaliased patterns),
and may not be reused by two patterns in the same rule. Referencing an alias
no pattern declares is a compile error — CLIPS would otherwise accept the
rule and match everything, since the reference would be the variable's first
occurrence and so bind rather than constrain.

Compiling a reference makes the *aliased* pattern mention the join variable
too, adding a slot constraint if the pattern had none on that slot:

```yaml
when:
  - template: access_request
    alias: $req
    conditions:
      - slot: mode
        expression: equals(read)
  - template: resource
    conditions:
      - slot: id
        expression: equals($req.object_id)   # joins on ?req-object_id
```

```clips
(access_request (mode "read") (object_id ?req-object_id))
(resource (id ?req-object_id))
```

Only one variable may bind a slot, so when the aliased slot already binds
one the compiler reuses it where it can and falls back to an equality
`(test ...)` where it cannot — a `bind:` you declared is never renamed.

**Stability.** The operator set and this grammar are covered by the 1.0
compatibility promise. The generated CLIPS *variable names*
(`?s_<index>_<slot>`, `?<alias>-<slot>`) are an implementation detail and
may change in a minor release — do not depend on them from a `test:` CE;
use `bind:` to name a variable you need.

Source: `_compile_condition` in `src/fathom/compiler.py` and
`_validate_expression` in `src/fathom/models.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`, `schema_frequency_exceeds` |

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](../../how-to/writing-rules.md).

## `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](../../concepts/audit-attestation.md). |
| `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](../../concepts/runtime-and-working-memory.md).

## 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

```yaml
- 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_0_amount&:(> ?s_0_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 when `salience == 0`.
- Test CEs appear after all pattern CEs, in source order across
  patterns.
- `reason` with `{placeholder}` placeholders becomes `(str-cat …)`;
  literal reasons are emitted as plain quoted strings.
- `notify` is always quoted — empty list emits `""`.
- `metadata` is empty-string when `{}`, otherwise
  `json.dumps(metadata, sort_keys=True)`.
- The `__fathom_decision` assert precedes user asserts in document
  order (AC-1.3).
- When `action` is `None`, 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 `name` or invalid CLIPS identifier — `ValueError` /
  `CompilationError`.
- Empty `when` — `CompilationError` from `compile_rule`.
- Unsupported operator in an `expression` — `CompilationError` from
  `_compile_condition`.
- `AssertSpec.template` or slot key that is not a valid CLIPS
  identifier — `ValueError`.
- Slot value with unbalanced parens, malformed `?var`, or embedded
  `\x00` — `ValueError` from `_validate_slot_value`.
- `ThenBlock` with neither `action` nor a non-empty `assert` list —
  `ValueError` from `_require_action_or_asserts`.
- `ConditionEntry` rejections listed under
  [What the validator rejects](#what-the-validator-rejects).

## See also

- [Five Primitives](../../concepts/five-primitives.md)
- [YAML Compilation](../../concepts/yaml-compilation.md)
- [Runtime & Working Memory](../../concepts/runtime-and-working-memory.md)
- [Writing rules](../../how-to/writing-rules.md)
- [Template reference](./template.md)


## reference/yaml/module.md

# Module

A **module** is a CLIPS namespace for rules. Every rule lives in exactly
one module, and Fathom's deterministic ordering is driven by the order
those modules are pushed onto the CLIPS focus stack. For the role of
modules among Fathom's five primitives, see
[Five Primitives](../../concepts/five-primitives.md); for the focus-stack
mechanics at evaluation time, see
[Runtime & Working Memory](../../concepts/runtime-and-working-memory.md).

## Top-level fields — `ModuleDefinition`

| Field         | Type    | Default | Required | Description                                                                                                                  |
|---------------|---------|---------|----------|------------------------------------------------------------------------------------------------------------------------------|
| `name`        | `str`   | —       | yes      | CLIPS identifier. Must match `^[A-Za-z_][A-Za-z0-9_\-]*$`. Emitted as `(defmodule <name> …)`.                                |
| `description` | `str`   | `""`    | no       | Author-facing prose. Not emitted to CLIPS.                                                                                    |
| `priority`    | `int`   | `0`     | no       | Author-facing metadata. **Not emitted to CLIPS** and **not used to drive the focus stack** in the current runtime (see below). |

The `name` field is validated by `_name_must_be_clips_ident` at model
construction and re-checked (for emptiness) by `compile_module`.

## CLIPS emission

`compile_module` (in `src/fathom/compiler.py`) emits a single-line
`defmodule` form. Every non-`MAIN` module imports `?ALL` from `MAIN` so
that templates declared in `MAIN` — including the built-in
`__fathom_decision` decision template — are visible to rules in the
module.

### YAML input

```yaml
modules:
  - name: access-control
    description: Core access decisions.
    priority: 100
```

### CLIPS output

```
(defmodule access-control (import MAIN ?ALL))
```

### Emission rules

1. The emitted form is exactly
   `(defmodule <name> (import MAIN ?ALL))` — one line, no variations
   for `description` or `priority`.
2. `description` is never emitted. It exists for authoring only.
3. `priority` is never emitted as a CLIPS construct attribute. CLIPS has
   no module-level priority concept; module ordering is done by the
   focus stack, not by attributes on `defmodule`.
4. An empty `name` at compile time raises `CompilationError` with
   `construct="module:<empty>"`.

## Priority and the focus stack

`ModuleDefinition.priority` is a stored integer (default `0`) that the
runtime **does not read** when setting focus. Focus order is driven by
the explicit `focus_order:` list at the top of the module YAML file (or
by a later `Engine.set_focus(...)` call). When no `focus_order:` is
given anywhere in the pack, `load_modules` falls back to the order the
modules were declared in — see
[When `focus_order` is omitted](#when-focus_order-is-omitted).

At evaluation time, `_setup_focus_stack` in `src/fathom/evaluator.py`
emits a single `(focus ...)` eval listing the modules in the order they
appear in `focus_order` — `(focus A B C)` gives A the focus first and
queues B and C behind it. `priority` is never consulted during this step.

Every module in the list runs: CLIPS drains the first module's agenda,
then moves to the next. A decision in an earlier module does **not**
short-circuit the ones after it. Because the evaluator is
last-write-wins, the module you list **last** is the one whose decision
the caller sees — so a guardrail module belongs at the end of
`focus_order`, not the start.

What `priority` is actually used for today:

- It is surfaced by `fathom inspect` as metadata for each loaded
  module (see `src/fathom/cli.py`).
- It is preserved on the `ModuleDefinition` in the engine's
  `module_registry` for external tooling.

See [Runtime & Working Memory](../../concepts/runtime-and-working-memory.md)
for the full focus-stack mechanics.

### When `focus_order` is omitted

A pack that declares modules and no `focus_order:` is focused on every
module *that call* declared, in declaration order: files in load order,
and within a file the order the `modules:` list is written.
`Engine.load_modules` applies this once, after every module file is
parsed, and only when that call parsed no `focus_order:` of its own — a
`focus_order:` in any of its files wins outright, and a partial one is
read as the author excluding a module on purpose.

Focus is engine-wide and **cumulative across calls**: whatever a call
contributes is appended to the order already in place, never substituted
for it. Loading a second rule pack therefore leaves the first one
enforced. `Engine.set_focus(...)` is the exception and is documented as
such — it replaces the whole order, which is how you narrow focus
deliberately.

The fallback exists because omitting `focus_order:` is not a request for
a different order; it is the absence of a request. CLIPS drains only the
agenda of the module that holds the focus, so with no focus set at all,
rules scoped to a declared module never fire and the caller silently
receives the default decision — a wrong answer rather than an error.

Declaration order is not a substitute for stating the order you want.
It is stable, but it moves when you add a module or rename a file, and
because the evaluator is last-write-wins, the module that lands last
writes the decision. Any pack whose modules must run in a particular
sequence should say so with `focus_order:`.

### Declaring focus order

```yaml
modules:
  - name: classification
    description: Label-derivation rules.
  - name: access-control
    description: Core access decisions.
    priority: 100   # metadata only — does not affect ordering

focus_order:
  - classification
  - access-control
```

Here `classification` runs first because it appears first in
`focus_order`, and `access-control` runs after it — so `access-control`
writes the final decision. The `priority: 100` is informational and does
not reorder anything.

## The MAIN module

`MAIN` is created implicitly by Fathom the first time `load_modules` is
called, via `(defmodule MAIN (export ?ALL))` (see `src/fathom/engine.py`).
Authors do not declare `MAIN` in YAML. All `deftemplate` constructs —
including the built-in `__fathom_decision` template that Fathom
installs at engine startup — live in `MAIN`, and every non-`MAIN`
module imports from it. Rules are scoped to a module by the `module:`
key on the enclosing `RulesetDefinition`; see [Rule](./rule.md) for the
rule emission.

## Validators

Pydantic-level rejections (raised at `ModuleDefinition(...)` or during
YAML load):

- `name` that does not match `^[A-Za-z_][A-Za-z0-9_\-]*$` →
  `ValueError` from `_name_must_be_clips_ident`.

Compile-time rejections (raised by `Compiler.compile_module`):

- `name` that is an empty string → `CompilationError` with
  `construct="module:<empty>"`.

Duplicate module names are rejected by `parse_module_file` (at YAML
load) and again by `Engine.load_modules` (across files), both as
`CompilationError`.

## What is not emitted

These YAML fields are accepted by the model but never appear in the
compiled CLIPS `defmodule`:

- `ModuleDefinition.description` — metadata only.
- `ModuleDefinition.priority` — metadata only; does not influence
  focus-stack ordering in the current runtime. Ordering comes from
  `focus_order:`, or from declaration order when that key is absent.

## See also

- [Five Primitives](../../concepts/five-primitives.md) — conceptual
  overview of templates, facts, rules, modules, and functions.
- [Runtime & Working Memory](../../concepts/runtime-and-working-memory.md)
  — focus-stack mechanics and the evaluation loop.
- [Rule](./rule.md) — how rules are scoped to a module via
  `RulesetDefinition.module`.
- [Template](./template.md) — why templates live in `MAIN`.


## reference/yaml/function.md

# Function

A **function** exposes a CLIPS `deffunction` that rules may call from
either the LHS (inside a `test:` escape hatch) or the RHS. Fathom
recognizes two function subtypes: `classification` (emitted as a
family of rank/below/meets-or-exceeds/within-scope deffunctions driven
by a `HierarchyDefinition`) and `raw` (the author-supplied CLIPS
source is emitted verbatim). Temporal operators (`changed_within`,
`count_exceeds`, `rate_exceeds`, `last_n`, `distinct_count`,
`sequence_detected`, `schema_frequency_exceeds`) are served by
Engine-registered Python externals
under the reserved `fathom-` prefix, **not** by `FunctionDefinition`.
For conceptual context see
[Five Primitives](../../concepts/five-primitives.md); for exposing
Python callables instead of authoring CLIPS, see
[Register a Python function](../../how-to/register-function.md).

## Top-level fields — `FunctionDefinition`

| Field           | Type                                             | Default            | Required | Description                                                                                                                                                            |
|-----------------|--------------------------------------------------|--------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name`          | `str`                                            | —                  | yes      | CLIPS identifier. Must match `^[A-Za-z_][A-Za-z0-9_\-]*$` (validated by `_name_must_be_clips_ident`). For `raw` functions this is authoring metadata only — the emitted CLIPS name comes from `body`. For `classification` the hierarchy name drives the emitted function names (see below). |
| `description`   | `str`                                            | `""`               | no       | Author-facing prose. Not emitted to CLIPS.                                                                                                                             |
| `params`        | `list[str]`                                      | —                  | yes      | Parameter names. Used when authoring `raw` bodies; the classification path ignores `params` and generates its own `(?level)` / `(?a ?b)` signatures from the hierarchy. |
| `hierarchy_ref` | `str \| None`                                    | `None`             | no       | **Required** when `type == "classification"`. Names the hierarchy whose `levels` drive emission. A trailing `.yaml` is stripped: `hier_name = defn.hierarchy_ref.rsplit(".", 1)[0]`. Ignored for `raw`. |
| `type`          | `Literal["classification", "raw"]`               | `"classification"` | no       | Selects the emission strategy. Default is `classification`.                                                                                                            |
| `body`          | `str \| None`                                    | `None`             | no       | Raw CLIPS source. **Required** when `type == "raw"`. Ignored for `classification`.                                                                                     |

Emission is dispatched in `compile_function` in
`src/fathom/compiler.py`. Empty `name`, missing `body` for a `raw`
function, missing `hierarchy_ref` for a `classification` function, and
a `hierarchy_ref` that does not resolve to a registered hierarchy all
raise `CompilationError`.

## Type subtypes

### `classification`

A classification function expands to a **family** of deffunctions
named by the hierarchy — not by `FunctionDefinition.name`. The
authoring `name` is a handle for the YAML bundle; the emitted CLIPS
identifiers all begin with the hierarchy name (e.g. `clearance-rank`,
`clearance-below`). The expansion, from
`_compile_classification_functions`, is:

- `(deffunction MAIN::<hier>-rank (?level) (switch ?level (case LEVEL then INDEX) … (default -1)))` — one `case` per level, in declaration order; out-of-hierarchy inputs return `-1`.
- `(deffunction MAIN::<hier>-below (?a ?b) (< (<hier>-rank ?a) (<hier>-rank ?b)))`.
- `(deffunction MAIN::<hier>-meets-or-exceeds (?a ?b) (>= (<hier>-rank ?a) (<hier>-rank ?b)))`.
- `(deffunction MAIN::<hier>-within-scope (?a ?b) (and (>= (<hier>-rank ?a) 0) (>= (<hier>-rank ?b) 0)))` — both inputs must be in-hierarchy.

The compiler also emits **unscoped backward-compat shims** —
`below`, `meets-or-exceeds`, `within-scope` — that delegate to the
scoped versions. These shims are emitted **only for the first loaded
hierarchy** (tracked via `self._first_hierarchy_name`); subsequent
hierarchies add their scoped deffunctions but do not overwrite the
unscoped shims.

Compilation failures:

- Missing `hierarchy_ref` → `CompilationError`.
- `hierarchy_ref` not registered in the bundle → `CompilationError`.

### `raw`

`compile_function` returns `defn.body` as-is. There is no `(deffunction
…)` wrapping, no parameter binding from `params`, and no escaping — the
YAML author supplies a complete, parenthesis-balanced CLIPS source
string, which is inserted into the compiled program verbatim.

Compilation failures:

- Missing `body` → `CompilationError`.

### Temporal operators (no `FunctionDefinition` needed)

Temporal operators (`changed_within`, `count_exceeds`, `rate_exceeds`,
`last_n`, `distinct_count`, `sequence_detected`,
`schema_frequency_exceeds`) are served by Engine-registered Python
externals under the reserved `fathom-` prefix, not by compiled CLIPS
deffunctions. Use them directly in rule
conditions; **no `FunctionDefinition` declaration is required or
accepted** — `type: temporal` was removed in 0.4.0 (raises a Pydantic
validation error if encountered in YAML). See
[Register a Python function](../../how-to/register-function.md) for
the external-function path and
[Not in v1](../../concepts/not-in-v1.md) for the broader roadmap.

## `HierarchyDefinition` fields

Classification functions consume a `HierarchyDefinition` from the same
YAML bundle.

| Field          | Type               | Default | Required | Description                                                                                                                                       |
|----------------|--------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| `name`         | `str`              | —       | yes      | Hierarchy name. Drives the emitted deffunction prefix (`<name>-rank`, `<name>-below`, …).                                                          |
| `levels`       | `list[str]`        | —       | yes      | Ordered lowest-to-highest. `rank` returns the 0-based index of a level; `below` / `meets-or-exceeds` compare ranks; out-of-hierarchy inputs rank `-1`. |
| `compartments` | `list[str] \| None`| `None`  | no       | Accepted by the model but **not** consumed by `_compile_classification_functions` today. Reserved for future compartment-aware emission.           |

## Worked example — `classification`

### YAML input

```yaml
hierarchies:
  - name: clearance
    levels: [unclassified, confidential, secret, top-secret]

functions:
  - name: clearance-check
    type: classification
    params: [a, b]
    hierarchy_ref: clearance
```

### CLIPS output

```
(deffunction MAIN::clearance-rank (?level)
    (switch ?level
        (case unclassified then 0)
        (case confidential then 1)
        (case secret then 2)
        (case top-secret then 3)
        (default -1)))

(deffunction MAIN::clearance-below (?a ?b)
    (< (clearance-rank ?a) (clearance-rank ?b)))

(deffunction MAIN::clearance-meets-or-exceeds (?a ?b)
    (>= (clearance-rank ?a) (clearance-rank ?b)))

(deffunction MAIN::clearance-within-scope (?a ?b)
    (and (>= (clearance-rank ?a) 0) (>= (clearance-rank ?b) 0)))

(deffunction MAIN::below (?a ?b)
    (clearance-below ?a ?b))

(deffunction MAIN::meets-or-exceeds (?a ?b)
    (clearance-meets-or-exceeds ?a ?b))

(deffunction MAIN::within-scope (?a ?b)
    (clearance-within-scope ?a ?b))
```

Notes:

- The unscoped `below` / `meets-or-exceeds` / `within-scope` shims
  appear because `clearance` is the **first** hierarchy loaded. A
  second hierarchy would emit only its scoped deffunctions; it would
  not shadow the shims.
- `FunctionDefinition.name` (`clearance-check`) and `params` (`[a, b]`)
  are not reflected in the output — the classification path drives
  emission purely from the hierarchy.
- Indentation is four spaces; function definitions are joined by blank
  lines.

## Worked example — `raw`

### YAML input

```yaml
functions:
  - name: double
    type: raw
    params: [x]
    body: "(deffunction MAIN::double (?x) (* ?x 2))"
```

### CLIPS output

```
(deffunction MAIN::double (?x) (* ?x 2))
```

The `body` string is emitted exactly — `params` is authoring metadata
only, and the deffunction's parameter list comes from whatever the
`body` declares.

Naming convention: the `fathom-` prefix is **reserved** for
Engine-registered builtins (`fathom-matches`, `fathom-count-exceeds`,
`fathom-rate-exceeds`, `fathom-changed-within`, `fathom-last-n`,
`fathom-distinct-count`, `fathom-sequence-detected`,
`fathom-parse-compartments`, `fathom-has-compartment`,
`fathom-compartments-superset`, `fathom-dominates`). Python callers of
`Engine.register_function` are rejected with `ValueError` if the
requested name starts with `fathom-`. YAML `raw` bodies are not
re-checked by the compiler, but avoid the prefix in authored CLIPS to
prevent collisions with the runtime's own bindings.

## Validators — what is rejected

| Condition                                            | Where raised                   | Error              |
|------------------------------------------------------|--------------------------------|--------------------|
| `name` that is not a valid CLIPS identifier          | Pydantic validator             | `ValueError`       |
| Empty `name` at compile time                         | `compile_function`             | `CompilationError` |
| `type: raw` with `body is None`                      | `compile_function`             | `CompilationError` |
| `type: classification` without `hierarchy_ref`       | `compile_function`             | `CompilationError` |
| `type: classification` with unknown `hierarchy_ref`  | `compile_function`             | `CompilationError` |
| Any key not defined on the model, on `FunctionDefinition` or `HierarchyDefinition` | Pydantic (`extra="forbid"`) | `ValidationError` |
| `HierarchyDefinition.name` that is not a valid CLIPS identifier | Pydantic validator | `ValueError` |
| A `levels` or `compartments` member that is not a valid CLIPS identifier | Pydantic validator | `ValueError` |

## What is not emitted

- `description` — author-facing only.
- `HierarchyDefinition.compartments` — accepted by the model, unused by
  today's classification emission.
- `FunctionDefinition.name` and `params` — ignored by the
  `classification` path (the hierarchy drives names; parameter
  signatures are fixed).
- `FunctionDefinition.params` — informational for `raw` (the emitted
  signature comes from `body`).

## See also

- [Five Primitives](../../concepts/five-primitives.md)
- [YAML Compilation](../../concepts/yaml-compilation.md)
- [Register a Python function](../../how-to/register-function.md)
- [Rule reference](./rule.md) — `test:` escape hatch for calling these functions from the LHS.
- [Template reference](./template.md)


## reference/yaml/fact.md

# Fact

A **fact** is a concrete instance of a template — the pair
`(template_name, data_dict)` — living in CLIPS working memory. Unlike
templates, rules, modules, and functions, facts are **not declared in
YAML**: they are asserted at runtime through the SDK, the REST API, or
as a side effect of a rule firing. All facts live under the `MAIN`
module (templates are compiled there; see
[Template reference](./template.md)). For conceptual context see
[Five Primitives](../../concepts/five-primitives.md); for working-memory
mechanics see
[Runtime & Working Memory](../../concepts/runtime-and-working-memory.md).

## Three entry paths

Facts enter working memory through exactly three surfaces. The first
two share the `FactManager` validation chain; the third (rule RHS)
bypasses it — see each subsection.

### REST — `POST /v1/facts`

`AssertFactRequest` body (`src/fathom/models.py`):

```json
{
  "session_id": "abc-123",
  "template": "access-request",
  "data": {
    "subject": "alice",
    "action": "read",
    "amount": 0
  }
}
```

The endpoint (`src/fathom/integrations/rest.py` lines 547–564) requires
an existing session created via `POST /v1/evaluate`; an unknown
`session_id` returns **404 session not found**. Companion endpoints
`POST /v1/query` and `DELETE /v1/facts` accept `session_id`,
`template`, and an optional `filter` dict with the same semantics as
the SDK.

### SDK — `Engine.assert_fact` / `Engine.assert_facts`

Single (`src/fathom/engine.py` line 1309):

```python
engine.assert_fact("access-request", {"subject": "alice", "action": "read"})
```

Atomic batch (`src/fathom/engine.py` line 1365) — every fact is
validated first; if any fails, **none** are asserted:

```python
engine.assert_facts([
    ("access-request", {"subject": "alice", "action": "read"}),
    ("access-request", {"subject": "bob",   "action": "write"}),
])
```

`Engine.assert_fact` raises `ScopeError` when the referenced template's
`scope` is `"fleet"` — fleet-scoped facts must go through
`FleetEngine.assert_fact`.

### Rule RHS — `then.assert`

Rules may assert facts as a side effect of firing. The YAML surface is
the `assert:` key on a `ThenBlock`, with one `AssertSpec` entry per
fact; slot values are CLIPS source text (literals, `?var` bindings,
balanced s-expressions) — **not** Python data. See the
[Rule reference](./rule.md) for `AssertSpec` and `ThenBlock` shape.

Rule-RHS asserts **do not pass through `FactManager._validate`** — the
compiler emits `(assert (<template> (<slot> <value>) …))` directly into
the CLIPS RHS, and CLIPS itself enforces the deftemplate's type and
allowed-value constraints. The FactManager's Python-level checks do
not run for this path.

## `FactInput` — embedded in `EvaluateRequest`

`FactInput` (`src/fathom/models.py`) is the `(template, data)` shape
used inside `EvaluateRequest.facts[]` — the enclosing request carries
the `session_id`:

```json
{
  "ruleset": "access",
  "session_id": "abc-123",
  "facts": [
    {"template": "access-request", "data": {"subject": "alice", "action": "read"}}
  ]
}
```

Each `FactInput` is asserted into the session before rules fire.

## Validation chain

`FactManager._validate` (`src/fathom/facts.py` lines 200–228) runs seven
steps, in order. The first to fail raises `ValidationError` and aborts
the assertion.

1. **Template registered.** If the template name is not in the
   engine's registry, raise `ValidationError("Unknown template 'X'")`.
2. **Unknown-slot check** (`_check_unknown_slots`, lines 230–243). Any
   key in `data` that is not a declared slot is rejected. The error
   sorts the unknown set and suggests the closest known slot via
   `difflib.get_close_matches(..., n=1)`.
3. **Apply defaults** (`_apply_defaults`, lines 245–253). Every slot
   with a non-`None` `SlotDefinition.default` missing from `data` gets
   the default copied in.
4. **Required check** (`_check_required`, lines 274–283). Slots marked
   `required: true` still missing after defaults raise
   `ValidationError("Missing required slot(s) […] in template 'X'")`.
   **Note:** [Template reference](./template.md) documents
   `SlotDefinition.required` as "not currently enforced" — that is
   accurate for the compiler and CLIPS emission, but `FactManager`
   enforces it at assertion time on the SDK and REST paths.
5. **Type coercion** (`_coerce_types`, lines 255–272):
   - `INTEGER` slot with a `float` value where `value == int(value)` →
     coerced to `int` (excluding `bool`).
   - `STRING` slot with a non-`str` value → coerced via `str(value)`.
   `FLOAT` slots accept `int` without coercion (type check allows
   both). `SYMBOL` slots are wrapped later — see
   [CLIPS coercion](#clips-coercion).
6. **Type check** (`_check_types`, lines 285–320). Validates against
   `_PYTHON_TYPE_MAP` (facts.py lines 18–23):

   | `SlotType` | Accepted Python types |
   |------------|------------------------|
   | `STRING`   | `str`                 |
   | `SYMBOL`   | `str`                 |
   | `FLOAT`    | `float`, `int`        |
   | `INTEGER`  | `int`                 |

   `bool` is **explicitly rejected** for `INTEGER` and `FLOAT` slots
   (facts.py lines 296–312) even though Python's `bool` is a subclass
   of `int` — the check runs before the `isinstance` comparison.
7. **Allowed-values check** (`_check_allowed_values`, lines 322–337).
   When `SlotDefinition.allowed_values` is set, the value is coerced
   with `str(value)` and compared against the list. Comparison is by
   string equality, so `allowed_values: ["1", "2"]` accepts
   `data = {"n": 1}` after stringification.

### Example — unknown-slot rejection

```python
engine.assert_fact("access-request", {"subjects": "alice"})
# ValidationError: Unknown slot(s) ['subjects'] in template
# 'access-request'. Did you mean 'subject'?
```

The suggestion is computed from the first unknown slot in sorted order.

## CLIPS coercion

After `_validate` returns, `_coerce_for_clips` (`src/fathom/facts.py`
lines 185–196) walks the validated data once more: for every slot
whose `type` is `SYMBOL` and whose value is still a plain `str`, the
value is wrapped in `clips.Symbol(value)` before being passed to
`template.assert_fact(**coerced)`. All other values pass through
unchanged.

## Query semantics

`Engine.query(template, fact_filter=None) -> list[dict]` forwards to
`FactManager.query` (`src/fathom/facts.py` lines 76–105). Returns one
dict per matching fact keyed by slot name. `clips.Symbol` values are
stringified on readout (facts.py lines 96–98), so callers receive plain
`str` for `SYMBOL` slots. When `fact_filter` is a non-empty dict, a
fact matches only when **every** filter key satisfies
`row.get(k) == v` (facts.py lines 100–104) — equality is Python `==`,
values are not coerced. A `None` or empty filter returns all facts.
Unknown template raises `ValidationError("Unknown template 'X'")`.
`Engine.count` is `len(query(...))` (facts.py lines 107–113).

```python
engine.query("access-request", {"subject": "alice"})
# [{"subject": "alice", "action": "read", "amount": 0}, ...]
```

## Retract semantics

`Engine.retract(template, fact_filter=None) -> int` forwards to
`FactManager.retract` (`src/fathom/facts.py` lines 115–147). Filter
semantics match `query`: a `None`/empty filter retracts **all** facts
of the template. Matches are collected first, then retracted, to avoid
mutating the CLIPS fact list during iteration. Returns the retracted
count. Accessible via `DELETE /v1/facts`
(`src/fathom/integrations/rest.py` lines 404–420), which takes a
`RetractFactsRequest` and returns
`RetractFactsResponse(retracted_count)`.

```python
removed = engine.retract("access-request", {"subject": "alice"})
```

`Engine.clear_facts()` retracts every user fact in the registry;
`__fathom_decision` and `initial-fact` are untouched.

## TTL and expiration

Fact expiration is configured **at runtime** via the Python API only —
there is no YAML surface for per-fact TTL. `Engine.load_templates`
forwards `TemplateDefinition.ttl` to `FactManager.set_ttl` (engine.py
lines 511–512; facts.py lines 41–42), which stores a per-template TTL.
Assertion timestamps are captured in `_fact_timestamps` keyed by CLIPS
fact index. `FactManager.cleanup_expired()` (facts.py lines 166–181)
retracts facts whose stored timestamp plus TTL is in the past and
returns the count retracted. Cleanup is not automatic — callers invoke
it explicitly.

## What is not surfaced

- **Internal decision facts.** Rules assert a `__fathom_decision` fact
  to carry the outcome; the template is built by `Engine.__init__`
  (engine.py lines 51–60, 198) and deliberately kept out of
  `_template_registry`, so it never appears in `query`, `retract`, or
  the audit fact-snapshot.
- **`initial-fact`.** Asserted by `env.reset()`; excluded by the same
  registry-gated mechanism.
- **Compile-time YAML documents.** Templates, modules, functions, and
  rules are CLIPS constructs, not facts.

## See also

- [Template reference](./template.md) — slot types, defaults,
  allowed-values.
- [Rule reference](./rule.md) — `then.assert` and `AssertSpec` shape
  for rule-RHS assertions.
- [Runtime & Working Memory](../../concepts/runtime-and-working-memory.md)
  — the evaluation loop and fact lifetime.
- [Five Primitives](../../concepts/five-primitives.md)


## reference/cli/index.md

# CLI Reference

| Command | |
|---|---|
| [`fathom bench`](bench.md) | |
| [`fathom compile`](compile.md) | |
| [`fathom convert rego`](convert-rego.md) | |
| [`fathom convert to-rego`](convert-to-rego.md) | |
| [`fathom info`](info.md) | |
| [`fathom repl`](repl.md) | |
| [`fathom status`](status.md) | |
| [`fathom test`](test.md) | |
| [`fathom validate`](validate.md) | |
| [`fathom verify-artifact`](verify-artifact.md) | |
| [`fathom verify-chain`](verify-chain.md) | |


## reference/tooling/vscode/index.md

# VSCode Tooling

## Snippets

Download [`fathom.code-snippets`](fathom.code-snippets) and drop it
into `.vscode/` at your repo root. Available prefixes:

- `fathom-template` — template skeleton
- `fathom-rule` — rule skeleton
- `fathom-module` — module skeleton
- `fathom-function` — function skeleton
- `fathom-schema` — `yaml-language-server` schema association header

## JSON Schema association

Add to your workspace `.vscode/settings.json`:

```json
{
  "yaml.schemas": {
    "https://fathom-rules.dev/reference/yaml/schemas/rule.schema.json": "rules/*.yaml",
    "https://fathom-rules.dev/reference/yaml/schemas/template.schema.json": "templates/*.yaml",
    "https://fathom-rules.dev/reference/yaml/schemas/module.schema.json": "modules/*.yaml",
    "https://fathom-rules.dev/reference/yaml/schemas/function.schema.json": "functions/*.yaml"
  }
}
```

Or add the `yaml-language-server` header to the top of any YAML file:

```yaml
# yaml-language-server: $schema=https://fathom-rules.dev/reference/yaml/schemas/rule.schema.json
```


## reference/rule-packs/owasp-agentic.md

# Rule Pack: `owasp-agentic`

OWASP LLM Top 10 agentic safety rule pack.

**Pack version:** `1.0`  
**Rule count:** 4  
**Modules:** `owasp`  
**Templates:** `agent_input`, `agent_output`, `tool_call`

## Rules

| Name | Salience | Action | Reason | Source |
|---|---|---|---|---|
| `detect-prompt-injection` | 100 | `escalate` | Possible prompt injection detected in agent input | `src/fathom/rule_packs/owasp_agentic/rules/owasp_rules.yaml` |
| `deny-excessive-agency-exec` | 50 | `deny` | Tool call is in the dangerous tools list (LLM04: Excessive Agency) | `src/fathom/rule_packs/owasp_agentic/rules/owasp_rules.yaml` |
| `flag-insecure-output-ssn` | 90 | `escalate` | Agent output may contain SSN pattern (LLM06: Insecure Output) | `src/fathom/rule_packs/owasp_agentic/rules/owasp_rules.yaml` |
| `flag-insecure-output-email` | 80 | `escalate` | Agent output may contain email address (LLM06: Insecure Output) | `src/fathom/rule_packs/owasp_agentic/rules/owasp_rules.yaml` |


## reference/rule-packs/nist-800-53.md

# Rule Pack: `nist-800-53`

NIST SP 800-53 security and privacy controls rule pack.

**Pack version:** `1.0`  
**Rule count:** 10  
**Modules:** `nist`  
**Templates:** `access_request`, `audit_event`, `data_transfer`

## Rules

| Name | Salience | Action | Reason | Source |
|---|---|---|---|---|
| `access-enforcement` | 50 | `deny` | Access denied: subject clearance insufficient for resource (AC-3) | `src/fathom/rule_packs/nist_800_53/rules/ac_rules.yaml` |
| `info-flow-enforcement` | 50 | `deny` | Data transfer blocked: classified data cannot flow to external destination (AC-4) | `src/fathom/rule_packs/nist_800_53/rules/ac_rules.yaml` |
| `least-privilege` | 50 | `deny` | Privileged action requires explicit context justification (AC-6) | `src/fathom/rule_packs/nist_800_53/rules/ac_rules.yaml` |
| `remote-access` | 100 | `escalate` | Remote privileged access requires additional authorization (AC-17) | `src/fathom/rule_packs/nist_800_53/rules/ac_rules.yaml` |
| `audit-events` | 90 | `escalate` | Auditable event has unknown outcome — requires resolution (AU-2) | `src/fathom/rule_packs/nist_800_53/rules/au_rules.yaml` |
| `audit-content` | 50 | `deny` | Audit record missing required subject field (AU-3) | `src/fathom/rule_packs/nist_800_53/rules/au_rules.yaml` |
| `audit-review-analysis` | 80 | `escalate` | Failed privileged action requires review and analysis (AU-6) | `src/fathom/rule_packs/nist_800_53/rules/au_rules.yaml` |
| `audit-generation` | 50 | `deny` | Audit generation requires resource identification for data events (AU-12) | `src/fathom/rule_packs/nist_800_53/rules/au_rules.yaml` |
| `boundary-protection` | 50 | `deny` | Data transfer to external boundary requires secure protocol (SC-7) | `src/fathom/rule_packs/nist_800_53/rules/sc_rules.yaml` |
| `transmission-confidentiality` | 50 | `deny` | Classified data requires secure transmission protocol (SC-8) | `src/fathom/rule_packs/nist_800_53/rules/sc_rules.yaml` |


## reference/rule-packs/hipaa.md

# Rule Pack: `hipaa`

HIPAA Privacy and Security Rule rule pack.

**Pack version:** `1.0`  
**Rule count:** 3  
**Modules:** `hipaa`  
**Templates:** `data_transfer`, `phi_policy`

## Rules

| Name | Salience | Action | Reason | Source |
|---|---|---|---|---|
| `minimum-necessary` | 50 | `deny` | PHI access denied: minimum necessary justification required (164.502(b)) | `src/fathom/rule_packs/hipaa/rules/hipaa_rules.yaml` |
| `transmission-security` | 50 | `deny` | PHI transfer blocked: transmission must be encrypted (164.312(e)(1)) | `src/fathom/rule_packs/hipaa/rules/hipaa_rules.yaml` |
| `breach-trigger` | 200 | `escalate` | Bulk PHI access detected: more than 10 reads in 300s, breach notification evaluation required (164.402) | `src/fathom/rule_packs/hipaa/rules/hipaa_rules.yaml` |


## reference/rule-packs/cmmc.md

# Rule Pack: `cmmc`

CMMC Level 2 Cybersecurity Maturity Model Certification rule pack.

**Pack version:** `1.0`  
**Rule count:** 6  
**Modules:** `cmmc`  
**Templates:** `cui_policy`

## Rules

| Name | Salience | Action | Reason | Source |
|---|---|---|---|---|
| `ac-l2-authorized-access` | 50 | `deny` | CUI access denied: authorized access justification required (AC.L2-3.1.1) | `src/fathom/rule_packs/cmmc/rules/cmmc_rules.yaml` |
| `ac-l2-cui-flow` | 50 | `deny` | CUI transfer blocked: controlled information cannot flow to external destination (AC.L2-3.1.3) | `src/fathom/rule_packs/cmmc/rules/cmmc_rules.yaml` |
| `ac-l2-least-privilege` | 50 | `deny` | Privileged CUI action requires explicit justification (AC.L2-3.1.5) | `src/fathom/rule_packs/cmmc/rules/cmmc_rules.yaml` |
| `au-l2-audit-records` | 100 | `escalate` | Audit record incomplete: outcome must be recorded for CUI events (AU.L2-3.3.1) | `src/fathom/rule_packs/cmmc/rules/cmmc_rules.yaml` |
| `au-l2-traceability` | 50 | `deny` | Audit traceability failed: subject identity required for all CUI actions (AU.L2-3.3.2) | `src/fathom/rule_packs/cmmc/rules/cmmc_rules.yaml` |
| `ir-l2-incident-handling` | 200 | `escalate` | Bulk CUI access detected: more than 20 reads in 600s, incident handling evaluation required (IR.L2-3.6.1) | `src/fathom/rule_packs/cmmc/rules/cmmc_rules.yaml` |


## reference/rule-packs/schema-denoising.md

# Rule Pack: `schema-denoising`

Schema-denoising rule pack: frequency-tau promotion over an extraction stream.

**Pack version:** `1.0`  
**Rule count:** 3  
**Modules:** `schema_denoising`  
**Templates:** `aligned_fact`, `candidate_schema`, `extracted_fact`, `stable_schema`

## Rules

| Name | Salience | Action | Reason | Source |
|---|---|---|---|---|
| `observe-candidate-schema` | 100 | `` |  | `src/fathom/rule_packs/schema_denoising/rules/schema_rules.yaml` |
| `promote-stable-schema` | 90 | `` |  | `src/fathom/rule_packs/schema_denoising/rules/schema_rules.yaml` |
| `align-fact-to-stable-schema` | 80 | `` |  | `src/fathom/rule_packs/schema_denoising/rules/schema_rules.yaml` |


## reference/rule-packs/conflict-detection.md

# Rule Pack: `conflict-detection`

Conflict-detection rule pack: contradictions in the fact layer, as facts.

**Pack version:** `1.0`  
**Rule count:** 3  
**Modules:** `conflict_detection`  
**Templates:** `claim`, `conflict`, `mutual_exclusion`, `subsumes`

## Rules

| Name | Salience | Action | Reason | Source |
|---|---|---|---|---|
| `detect-mutual-exclusion` | 100 | `` |  | `src/fathom/rule_packs/conflict_detection/rules/conflict_rules.yaml` |
| `detect-temporal-conflict` | 90 | `` |  | `src/fathom/rule_packs/conflict_detection/rules/conflict_rules.yaml` |
| `detect-granularity-conflict` | 80 | `` |  | `src/fathom/rule_packs/conflict_detection/rules/conflict_rules.yaml` |


## reference/planned-integrations.md

# Planned Integrations

This page catalogs integrations that are **not** production-ready: scaffolded
SDKs, stub applications, and adapter surfaces named in the original v1
design that are not implemented. For shipped integrations, see the dedicated reference
pages ([Python SDK](./python-sdk/index.md), [REST API](./rest/index.md),
[gRPC API](./grpc/index.md), [MCP Tools](./mcp/index.md),
[CLI](./cli/index.md), [VSCode Tooling](./tooling/vscode/index.md),
[Rule Packs](./rule-packs/owasp-agentic.md)).

Each entry declares a **Status** of one of:

- **Shipped** — in-tree, tested, documented, and reachable from a release artifact.
- **Partial** — in-tree with working code but missing tests, packaging, or CI coverage.
- **Planned** — named in the original v1 design with no implementation in the source tree.

## Go SDK — `packages/fathom-go/`

**Status:** Partial.

**Location:** `packages/fathom-go/` — a hand-written REST client plus
generated gRPC bindings. Package contents: `client.go` (181 lines),
`client_test.go` (818 lines), `grpc_test.go` (230 lines, build-tagged
`integration`), `tools.go`, `go.mod`, `go.sum`, `Makefile`, and
`proto/` with `fathom.pb.go` + `fathom_grpc.pb.go`. `go.mod` declares
`module github.com/KrakenNet/fathom-go` at `go 1.25.0`.

**What works today:**

- `NewClient(baseURL, opts...)` constructor at `client.go:39-48`, with
  functional options `WithBearerToken` (`client.go:25-27`) and
  `WithHTTPClient` (`client.go:31-33`).
- Four request/response pairs covering the REST surface: `Evaluate`,
  `AssertFact`, `Query`, `Retract` (`client.go:74-144`).
- Shared transport at `client.go:148-180`: JSON marshal/unmarshal,
  `Content-Type: application/json`, optional `Authorization: Bearer
  <token>` header, and error surfacing on any non-2xx status with the
  server body embedded in the returned error.
- Unit tests in `client_test.go` exercise the REST surface against
  `httptest` servers.
- Generated gRPC stubs live in `packages/fathom-go/proto/` (built from
  `protos/fathom.proto`); `grpc_test.go` is a `-tags=integration` test
  that spawns the Python gRPC server and dials it via those stubs.

**What is missing:**

- **No released module.** Consumers must vendor the package from a local
  clone; nothing is published to a Go proxy. Tracked as issue
  [#41](https://github.com/KrakenNet/fathom/issues/41).

The Go suite **is** wired into CI:
`.github/workflows/go-ci.yml` runs `go vet`, `go build`, and
`go test ./...` on every pull request, with a second `integration` job
that spins up the Python gRPC server and runs `go test -tags integration
./...`. A `verify-grpc` step also fails the build if the generated
bindings drift from `protos/fathom.proto`.

**How to use today:** Clone the monorepo, `go get` against the local path
(or add a `replace` directive), and point the client at a running REST
server. For the current public API surface, see the generated reference
at [Go SDK](./go-sdk/fathom-go.md).

## TypeScript SDK — `packages/fathom-ts/`

**Status:** Partial.

**Location:** `packages/fathom-ts/` — published identity
`@fathom-rules/sdk`, versioned in lockstep with the engine (release-please
writes `package.json` from the same tag, and `scripts/check_version_sync.py`
fails the build if the two drift). Source lives in
`src/client.ts` (215 lines), `src/errors.ts` (77 lines), and
`src/index.ts` (26 lines). Vitest suites in `test/client.test.ts` and
`test/errors.test.ts`.

**What works today:** A hand-written `FathomClient` plus a typed error
hierarchy. The package ships with 34
vitest tests passing (15 in `test/client.test.ts`, 19 in
`test/errors.test.ts`), and the typedoc reference is generated into
`docs/reference/typescript-sdk/` by the `docs` npm script in
`package.json`. The suite is wired into CI: `.github/workflows/ts-ci.yml`
runs typecheck, build and vitest as the required `ts-test` check on every
pull request.

**There is no generated client.** `src/generated/` used to hold one, and
this page used to describe it as working. It was generated once, in April
2026, from a copy of the spec at the repo root that was frozen at API
version 0.3.0 — two endpoints behind the live one — and nothing in the
package ever imported it. Its `generate` script emitted zero files against
the pinned `@hey-api/openapi-ts`, so it could not be refreshed either.
Both the dead tree and the stale root spec are gone; the single spec is
[`docs/reference/rest/openapi.json`](./rest/openapi.json), regenerated by
`scripts/export_openapi.py` and held to the running app by
`tests/test_scripts/test_export_openapi.py`.

**What is missing:**

- **The client covers 4 of the 11 documented endpoints** — `/v1/evaluate`,
  `/v1/facts` (assert and retract) and `/v1/query`. `/v1/compile`,
  `/v1/templates`, `/v1/modules`, `/v1/rules`, `/v1/rules/reload`,
  `/v1/status`, `/v1/data/{path}` and `/health` have no method on
  `FathomClient`.
- **No published npm release.** `repository.url` in `package.json` points
  at the monorepo; no `dist/` is published. Tracked as issue
  [#40](https://github.com/KrakenNet/fathom/issues/40).

**How to use today:** Clone the monorepo, `pnpm install` in
`packages/fathom-ts/`, and import from the local workspace path. The
generated API reference lives at
[TypeScript SDK](./typescript-sdk/index.md).

## Visual Rule Editor

**Status:** Planned.

The original v1 design named a browser-based rule editor. A React scaffold
lived at `packages/fathom-editor/` and was removed: six component stubs, no
tests, no backend wiring, and a CI job whose only assertion was that the tree
still compiled. It never round-tripped against a live Fathom server, and
Policy Studio (below) had meanwhile shipped
a working browser UI over a real engine. Building the editor out is tracked as
issue [#43](https://github.com/KrakenNet/fathom/issues/43); the scaffold is in
git history if it is ever the right starting point.

## Framework adapters

The original v1 design listed four framework adapters. All four are now shipped.

| Adapter                    | Status | Location |
|----------------------------|--------------------|-----------------|
| LangChain callback handler | **Shipped** | `src/fathom/integrations/langchain.py` |
| CrewAI before-tool-call hook | **Shipped** | `src/fathom/integrations/crewai.py` |
| OpenAI Agents SDK tool guardrail | **Shipped** | `src/fathom/integrations/openai_agents.py` |
| Google ADK before-tool callback | **Shipped** | `src/fathom/integrations/google_adk.py` |

Each adapter follows the same pattern: intercept tool calls, then evaluate a
`tool_request` fact against the policy through `Engine.evaluate_once`, which
asserts the fact, runs, and withdraws it again so one call cannot be decided
on the previous call's working memory.

The guard is **allowlist-only**: it permits the call when — and only when —
the decision is exactly `allow`. Every other outcome (`deny`, `escalate`,
`route`, `scope`, a missing decision, or any value a future release adds)
raises `PolicyViolation` — one class, defined in `fathom.integrations` and
re-exported by every adapter, so one `except` covers all four — or returns
an error dict for ADK. A denylist of
known-bad decisions would fail open on anything it had not heard of.
Install via `pip install fathom-rules[langchain]`, `fathom-rules[crewai]`,
`fathom-rules[openai-agents]`, or `fathom-rules[google-adk]`.

## Policy Studio — `packages/fathom-studio/`

**Status:** Partial.

**Location:** `packages/fathom-studio/` — package identity `fathom-studio` at
`0.1.0`, a uv workspace member of this repo. It depends on `fathom-rules`
like any other consumer; the engine wheel ships no Studio code.

**What works today:** A browser UI over a real engine — five views (Reasoning
Bench, Live Wire, Rules, Templates, Audit) served as a zero-build React SPA,
a JSON backend under `/studio/api`, server-rendered HTMX panels, and the
production REST app mounted in the same process under `/api`. Nine demo
scenarios ship as package data, held byte-identical to the repo's
`examples/0N-*` directories by a test. Its pytest suite runs in CI: root
`testpaths` includes `packages/fathom-studio/tests`, so the required `test`
job covers it.

**What is missing:**

- **No published release.** `fathom-studio` is not on PyPI and no workflow
  builds or publishes it; run it from a checkout with `uv run fathom-studio`.
- **No stability promise.** The `/studio/api/*` routes are unversioned and
  explicitly excluded from
  [VERSIONING.md](https://github.com/KrakenNet/fathom/blob/main/VERSIONING.md).
- **In-memory audit only.** The Audit view's chain is process-local, capped
  at 200 records, and signed with a keypair minted at startup. The durable
  equivalent is `fathom.chained_log.ChainedAttestationLog`.

**How to use today:** [Running Policy Studio](../how-to/policy-studio.md).

## Known blockers

- **Proto ↔ `go.mod` path alignment** — previously flagged as
  `REVIEW.md` M2 (proto declared `github.com/KrakenNet/fathom/gen/go/fathom/v1`
  while `go.mod` declared `github.com/KrakenNet/fathom-go`, which would
  have broken `protoc` output). Resolved at HEAD:
  `protos/fathom.proto:12` now declares
  `go_package = "github.com/KrakenNet/fathom-go/proto;fathomv1"`, matching
  `packages/fathom-go/go.mod:1`. Generated bindings now live in
  `packages/fathom-go/proto/{fathom.pb.go,fathom_grpc.pb.go}`.
- **Every in-tree package is now covered by CI.** The Python suite
  (`.github/workflows/ci.yml`, which also covers the Studio), the Go suite
  (`.github/workflows/go-ci.yml`, unit + `-tags integration`) and the
  TypeScript suite (`.github/workflows/ts-ci.yml`, the required `ts-test`
  check, closing issue
  [#39](https://github.com/KrakenNet/fathom/issues/39)) all run on every
  pull request.

## See also

- [Python SDK](./python-sdk/index.md) — the reference implementation; all
  shipped adapters (including LangChain) live here.
- [REST API](./rest/index.md) — the wire protocol the Go and TypeScript
  SDKs target.
- [gRPC API](./grpc/index.md) — the proto surface, which the Go SDK now
  implements through the generated bindings in `packages/fathom-go/proto/`.
- [Go SDK](./go-sdk/fathom-go.md) — gomarkdoc output for the clients
  described above.
- [TypeScript SDK](./typescript-sdk/index.md) — typedoc output for
  `@fathom-rules/sdk`.

