> ## Documentation Index
> Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Logic Engine

> How Terra evaluates conditional visibility using expression trees

# Logic Engine

> Show this section IF (State is WA OR CA) AND (Income \< \$50,000).

Government forms need complex conditional logic. The Logic Engine evaluates **expression trees** to determine which fields are visible based on current form values.

***

## Why Expression Trees?

Simple visibility rules like "show X when Y equals Z" don't scale. Real forms have conditions like:

* "Show disability section if age > 65 OR has disability"
* "Show income verification if employment = 'self-employed' AND income > \$50k"
* "Hide bank section if (payment method = 'check') OR (no bank account)"

We need **composable conditions**: rules that combine with AND/OR operators into arbitrarily deep trees.

```mermaid theme={null}
flowchart TD
    AND[AND Group]
    OR1[OR Group]
    Rule1[State = WA]
    Rule2[State = CA]
    Rule3[Income < 50000]

    AND --> OR1
    AND --> Rule3
    OR1 --> Rule1
    OR1 --> Rule2
```

This tree evaluates to: `(State = WA OR State = CA) AND (Income < 50000)`

***

## Data Structures

### LogicRule

A single comparison:

```typescript theme={null}
type LogicRule = {
  id: string;
  type: "rule";
  fieldId: string;           // Which field to check
  operator: LogicOperator;   // How to compare
  value?: string | number | boolean | array;  // What to compare against
};
```

### LogicGroup

A collection of conditions combined with AND or OR:

```typescript theme={null}
type LogicGroup = {
  id: string;
  type: "group";
  operator: "AND" | "OR";
  children: LogicCondition[];  // Rules or nested groups
};
```

### LogicCondition

The recursive union:

```typescript theme={null}
type LogicCondition = LogicRule | LogicGroup;
```

***

## Operators

The engine supports 18 operators:

| Operator       | Description           | Example                                 |
| -------------- | --------------------- | --------------------------------------- |
| `eq`           | Equals                | `income = 50000`                        |
| `neq`          | Not equals            | `status != "denied"`                    |
| `gt`           | Greater than          | `age > 18`                              |
| `gte`          | Greater than or equal | `income >= 30000`                       |
| `lt`           | Less than             | `household_size < 4`                    |
| `lte`          | Less than or equal    | `age <= 65`                             |
| `contains`     | String/array contains | `states contains "WA"`                  |
| `not_contains` | Does not contain      | `allergies not_contains "peanuts"`      |
| `starts_with`  | String starts with    | `zip starts_with "98"`                  |
| `ends_with`    | String ends with      | `email ends_with ".gov"`                |
| `in`           | Value in list         | `state in ["WA", "CA", "OR"]`           |
| `not_in`       | Value not in list     | `status not_in ["denied", "cancelled"]` |
| `exists`       | Has any value         | `phone exists`                          |
| `not_exists`   | Is empty              | `middle_name not_exists`                |

### Age Operators (for date fields)

Special operators that calculate age from a date:

| Operator   | Description          | Example                               |
| ---------- | -------------------- | ------------------------------------- |
| `minAge`   | At least X years old | `dob minAge 18` (must be 18+)         |
| `maxAge`   | At most X years old  | `dob maxAge 65` (must be 65 or under) |
| `underAge` | Under X years old    | `dob underAge 21` (show warning)      |
| `overAge`  | Over X years old     | `dob overAge 62` (show senior info)   |

***

## Evaluation

The main evaluation function:

```typescript theme={null}
// src/lib/logic-engine.ts
export function evaluateLogic(
  logic: LogicCondition | undefined | null,
  formData: FormData
): boolean {
  if (!logic) return true;  // No logic = always visible

  if (logic.type === "rule") {
    return evaluateRule(logic, formData);
  } else if (logic.type === "group") {
    return evaluateGroup(logic, formData);
  }

  return true;
}
```

### Rule Evaluation

```typescript theme={null}
function evaluateRule(rule: LogicRule, formData: FormData): boolean {
  const fieldValue = getFieldValue(formData, rule.fieldId);
  return evaluateOperator(rule.operator, fieldValue, rule.value);
}
```

### Group Evaluation

```typescript theme={null}
function evaluateGroup(group: LogicGroup, formData: FormData): boolean {
  if (group.children.length === 0) return true;

  if (group.operator === "AND") {
    // All children must be true
    return group.children.every((child) => evaluateLogic(child, formData));
  } else {
    // At least one child must be true
    return group.children.some((child) => evaluateLogic(child, formData));
  }
}
```

### Nested Path Support

Field IDs can reference nested data:

```typescript theme={null}
function getFieldValue(formData: FormData, fieldId: string): unknown {
  const parts = fieldId.split(".");
  let value = formData;

  for (const part of parts) {
    if (value === null || value === undefined) return undefined;
    value = value[part];
  }

  return value;
}
```

This allows rules like `address.state = "WA"`.

***

## Show vs Hide Actions

By default, logic shows fields when conditions are met. But sometimes you want the opposite:

```typescript theme={null}
{
  "id": "ssn",
  "type": "text",
  "label": { "en": "SSN" },
  "logic": {
    "type": "rule",
    "fieldId": "is-citizen",
    "operator": "eq",
    "value": "no"
  },
  "logicAction": "hide"  // Hide when NOT a citizen = show only for citizens
}
```

The `logicAction` property inverts the logic result:

* `"show"` (default): field visible when logic is true
* `"hide"`: field visible when logic is false

***

## Complete Example

A form section that shows income verification for high-earning self-employed applicants in certain states:

```json theme={null}
{
  "id": "income-verification",
  "type": "group",
  "label": { "en": "Income Verification" },
  "logic": {
    "id": "root",
    "type": "group",
    "operator": "AND",
    "children": [
      {
        "id": "state-check",
        "type": "group",
        "operator": "OR",
        "children": [
          {
            "id": "wa",
            "type": "rule",
            "fieldId": "address.state",
            "operator": "eq",
            "value": "WA"
          },
          {
            "id": "ca",
            "type": "rule",
            "fieldId": "address.state",
            "operator": "eq",
            "value": "CA"
          }
        ]
      },
      {
        "id": "self-employed",
        "type": "rule",
        "fieldId": "employment-type",
        "operator": "eq",
        "value": "self-employed"
      },
      {
        "id": "high-income",
        "type": "rule",
        "fieldId": "annual-income",
        "operator": "gte",
        "value": 50000
      }
    ]
  },
  "elements": [...]
}
```

This evaluates as:

```
(state = "WA" OR state = "CA") AND employment-type = "self-employed" AND annual-income >= 50000
```

***

## Legacy Visibility (Deprecated)

The old visibility format is still supported for backwards compatibility:

```typescript theme={null}
// Old format (deprecated)
{
  "visibility": {
    "when": "has-dependents",
    "is": "yes"
  }
}

// New format (preferred)
{
  "logic": {
    "type": "rule",
    "fieldId": "has-dependents",
    "operator": "eq",
    "value": "yes"
  }
}
```

The `evaluateElementVisibility` function handles both:

```typescript theme={null}
export function evaluateElementVisibility(element, formData): boolean {
  // Prefer new logic system
  if (element.logic) {
    const result = evaluateLogic(element.logic, formData);
    const action = element.logicAction || "show";
    return action === "show" ? result : !result;
  }

  // Fall back to legacy visibility
  if (element.visibility) {
    return evaluateVisibility(element.visibility, formData);
  }

  // No conditions = always visible
  return true;
}
```

***

## Helper Functions

Utilities for building logic programmatically:

```typescript theme={null}
import {
  createEqualsRule,
  createAndGroup,
  createOrGroup,
} from "@/lib/logic-engine";

// Simple rule
const isAdult = createEqualsRule("is-adult", "yes");

// Combine with AND
const canApply = createAndGroup([
  createEqualsRule("is-adult", "yes"),
  createEqualsRule("is-resident", "yes"),
]);

// Combine with OR
const qualifiesForDiscount = createOrGroup([
  createEqualsRule("is-senior", "yes"),
  createEqualsRule("is-veteran", "yes"),
  createEqualsRule("is-disabled", "yes"),
]);
```

***

## Form Builder UI

The Logic Builder component (in `src/components/form-builder/logic-builder.tsx`) provides a visual interface:

```mermaid theme={null}
flowchart LR
    subgraph UI["Logic Builder UI"]
        AddRule[+ Add Rule]
        AddGroup[+ Add Group]
        Rules[Rule List]
        Operator[AND/OR Toggle]
    end

    subgraph State["Zustand Store"]
        Schema[Form Schema]
        Logic[Element Logic]
    end

    UI --> State
    State --> Preview[Live Preview]
```

Users can:

* Add rules that compare field values
* Create nested groups with AND/OR
* See live preview of which fields would show

***

## Performance Considerations

The logic engine re-evaluates on every form value change. For most forms this is instant, but deep nesting could become slow.

**Optimizations applied:**

1. **Short-circuit evaluation** — AND stops at first false, OR stops at first true
2. **Direct property access** — no recursive tree walking for simple paths
3. **Type coercion caching** — number/string conversions memoized per render

**Worst case:** A form with 100 fields, each with 10-deep nested logic, re-evaluated 60x per second. In practice, forms have 20-50 fields with 2-3 level nesting.

***

## Edge Cases

### Empty Values

Empty inputs (`""`, `null`, `undefined`, `[]`) are handled consistently:

```typescript theme={null}
function isEmpty(value: unknown): boolean {
  if (value === null || value === undefined) return true;
  if (typeof value === "string" && value.trim() === "") return true;
  if (Array.isArray(value) && value.length === 0) return true;
  return false;
}
```

### Type Coercion

Comparisons handle type mismatches:

```typescript theme={null}
// "50000" == 50000 → true (loose equality for eq/neq)
// toNumber("50000") → 50000 (for numeric comparisons)
```

### Invalid Dates

Age operators handle malformed dates:

```typescript theme={null}
function calculateAge(dateValue: unknown): number | null {
  // Returns null for invalid dates
  // Logic rules return false when age is null
}
```

***

## Related

<CardGroup cols={2}>
  <Card title="Schema Design" icon="file-code" href="/core-systems/forms/schema-design">
    How fields store logic conditions
  </Card>

  <Card title="Form Builder" icon="hammer" href="/core-systems/forms/form-builder-internals">
    Visual interface for building logic
  </Card>
</CardGroup>
