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

# 정책 룰

> 룰은 정책의 if-then 로직을 정의합니다. 조건 문법, 액션 타입, 충돌 해소, decision trace를 알아보세요.

## 정책 룰이란?

**정책 룰(Policy Rule)** 은 조건 → 액션의 쌍입니다. 입력 facts가 조건을 만족하면 룰의 액션이 실행됩니다. 룰은 **우선순위 순서** (0이 가장 높은 우선순위)로 평가됩니다.

```
IF 조건이 매칭되면 → THEN 액션을 실행
```

각 룰은 특정 정책 버전에 속하며, 이름 / 우선순위 / 조건(트리 구조 로직) / 하나 이상의 액션을 가집니다.

## 조건 문법

조건은 `SINGLE`과 `GROUP` 두 노드 타입의 트리 구조입니다.

<Warning>
  LexQ는 `type: "SINGLE"` / `type: "GROUP"`, `field`, `operator`, `value`, `valueType` 필드를 사용하는 자체 condition DTO 포맷을 사용합니다. Console 내부 폼 표현(다른 필드명 사용 가능)과 혼동하지 마세요. **API 또는 CLI 호출 시에는 항상 엔진 포맷을 사용해야 합니다.**
</Warning>

### SINGLE 조건

단일 fact를 값과 비교하는 leaf 노드:

```json theme={null}
{
  "type": "SINGLE",
  "field": "payment_amount",
  "operator": "GREATER_THAN_OR_EQUAL",
  "value": 100000,
  "valueType": "NUMBER"
}
```

### GROUP 조건

자식 조건을 `AND` 또는 `OR`로 결합하는 branch 노드:

```json theme={null}
{
  "type": "GROUP",
  "operator": "AND",
  "children": [
    { "type": "SINGLE", "field": "customer_tier", "operator": "EQUALS", "value": "VIP", "valueType": "STRING" },
    { "type": "SINGLE", "field": "payment_amount", "operator": "GREATER_THAN", "value": 50000, "valueType": "NUMBER" }
  ]
}
```

### 연산자

| 연산자                     | 호환 타입          | 설명              |
| ----------------------- | -------------- | --------------- |
| `EQUALS`                | All            | 정확히 일치          |
| `NOT_EQUALS`            | All            | 부정              |
| `GREATER_THAN`          | NUMBER         | `>`             |
| `GREATER_THAN_OR_EQUAL` | NUMBER         | `>=`            |
| `LESS_THAN`             | NUMBER         | `<`             |
| `LESS_THAN_OR_EQUAL`    | NUMBER         | `<=`            |
| `CONTAINS`              | STRING         | 부분 문자열 일치       |
| `IN`                    | STRING, NUMBER | 값이 제공된 리스트에 포함  |
| `NOT_IN`                | STRING, NUMBER | 값이 제공된 리스트에 미포함 |

<Info>
  `IN` / `NOT_IN`의 경우 **호환 타입** 열은 검사 대상 *fact*의 타입(`STRING` 또는 `NUMBER`)을 가리킵니다. 제공하는 `value`는 리스트 자체이며, 그 `valueType`은 `LIST_STRING` 또는 `LIST_NUMBER`입니다.
</Info>

### 값 타입

| 타입            | JSON 값         | 예시               |
| ------------- | -------------- | ---------------- |
| `STRING`      | `"string"`     | `"VIP"`          |
| `NUMBER`      | `number`       | `100000`         |
| `BOOLEAN`     | `true / false` | `true`           |
| `LIST_STRING` | `["a", "b"]`   | `["KR", "US"]`   |
| `LIST_NUMBER` | `[1, 2]`       | `[10000, 20000]` |

### 중첩 조건 예시

`(customer_tier = "VIP" AND payment_amount >= 100000) OR region IN ["KR", "JP"]`

```json theme={null}
{
  "type": "GROUP",
  "operator": "OR",
  "children": [
    {
      "type": "GROUP",
      "operator": "AND",
      "children": [
        { "type": "SINGLE", "field": "customer_tier", "operator": "EQUALS", "value": "VIP", "valueType": "STRING" },
        { "type": "SINGLE", "field": "payment_amount", "operator": "GREATER_THAN_OR_EQUAL", "value": 100000, "valueType": "NUMBER" }
      ]
    },
    { "type": "SINGLE", "field": "region", "operator": "IN", "value": ["KR", "JP"], "valueType": "LIST_STRING" }
  ]
}
```

<Info>
  위 예시에서 `customer_tier`, `region`, `payment_amount`는 사용자 정의 fact로 [Fact Definitions](/ko/guides/fact-definitions)에 등록하는 것이 좋습니다. 등록 전에 참조해도 되며 — LexQ는 미등록 키를 거부하지 않고 비차단 경고로 알립니다 — 등록하면 타입 검증과 requirements analyzer가 활성화됩니다. 기본 제공 시스템 fact는 `user_id`, `user_tags` 두 개뿐입니다.
</Info>

## 액션 타입

LexQ 엔진의 액션은 **도메인 중립 원시(primitive)** 입니다. 엔진은 숫자와 구조만 봅니다 — 커머스 / 핀테크 / 보험 / 특정 비즈니스 모델을 가정하지 않습니다. 도메인 의미는 fact 이름과 integration 페이로드에 담깁니다.

| 타입                  | 설명                               | 주요 파라미터                                                                              |
| ------------------- | -------------------------------- | ------------------------------------------------------------------------------------ |
| `MUTATE_FACT`       | 산술 연산자로 숫자 fact를 in-place 변경     | `refVar`, `operator`, `method`, `rate` (PERCENTAGE 일 때) 또는 `value`, `rounding` (선택)  |
| `INCREMENT_FACT`    | 계산된 양만큼 숫자 fact를 누적 증가           | `targetVar`, `method`, `refVar`, `rate` (PERCENTAGE 일 때) 또는 `value`, `rounding` (선택) |
| `EMIT_EVENT`        | 외부 integration에 이벤트 발행           | `integrationId`, `eventPayload`                                                      |
| `EMIT_NOTIFICATION` | integration을 통해 알림 발송            | `integrationId`, `targetVar`, `notificationPayload`                                  |
| `EMIT_WEBHOOK`      | 외부 URL 또는 webhook integration 호출 | `url`, `method`, `payloadTemplate` (선택)                                              |
| `BLOCK`             | 요청 차단                            | `reason`                                                                             |
| `SET_FACT`          | fact를 고정 값으로 설정                  | `key`, `value`                                                                       |
| `ADD_TAG`           | 리스트 fact에 태그 추가                  | `tag`, `targetVar`                                                                   |

### MUTATE\_FACT — Percentage

`payment_amount`를 10% 감소:

```json theme={null}
{
  "type": "MUTATE_FACT",
  "parameters": {
    "refVar": "payment_amount",
    "operator": "SUB",
    "method": "PERCENTAGE",
    "rate": 10
  }
}
```

`payment_amount`가 200,000이면 → 180,000으로 감소. 변화량(`-20,000`)은 `generatedVariables`의 `payment_amount__delta`로 노출됩니다.

#### 연산자(Operator)

| 연산자      | AMOUNT            | PERCENTAGE                       |
| -------- | ----------------- | -------------------------------- |
| `ASSIGN` | `refVar = value`  | `refVar = refVar × rate / 100`   |
| `ADD`    | `refVar += value` | `refVar += refVar × rate / 100`  |
| `SUB`    | `refVar -= value` | `refVar -= refVar × rate / 100`  |
| `MUL`    | `refVar *= value` | `refVar *= (rate / 100 + 1)`     |
| `DIV`    | `refVar /= value` | invalid — 역수 rate를 사용한 `MUL`로 대체 |

<Warning>
  `DIV` + `PERCENTAGE` 조합은 DRAFT 저장 시점에 거부됩니다. `DIV` + `value=0`도 마찬가지입니다.
</Warning>

### MUTATE\_FACT — Fixed Amount

```json theme={null}
{
  "type": "MUTATE_FACT",
  "parameters": {
    "refVar": "payment_amount",
    "operator": "SUB",
    "method": "AMOUNT",
    "value": 5000
  }
}
```

### MUTATE\_FACT — 반올림(Rounding)

기본적으로 calculator 출력은 무손실(lossless) 정밀도로 보존됩니다. 고정 자릿수가 필요할 때만 선택적 `rounding` 필드를 사용하세요.

```json theme={null}
{
  "type": "MUTATE_FACT",
  "parameters": {
    "refVar": "payment_amount",
    "operator": "SUB",
    "method": "PERCENTAGE",
    "rate": 13.7,
    "rounding": { "scale": 0, "mode": "FLOOR" }
  }
}
```

`rounding.scale`은 `[0, 16]` 범위의 정수. `rounding.mode`는 `HALF_UP`(default), `HALF_DOWN`, `HALF_EVEN`, `FLOOR`, `CEILING`, `DOWN`, `UP` 중 하나. `rounding`을 생략하면 무손실 정밀도가 유지됩니다 — 가능하면 엔진이 아니라 다운스트림(예: 통화 표시 시점)에서 반올림하세요.

### INCREMENT\_FACT — Percentage

`payment_amount`의 1%를 `total_point`에 적립:

```json theme={null}
{
  "type": "INCREMENT_FACT",
  "parameters": {
    "targetVar": "total_point",
    "refVar": "payment_amount",
    "method": "PERCENTAGE",
    "rate": 1
  }
}
```

`payment_amount`가 100,000이면 → `total_point`가 1,000 증가. 증분량은 `generatedVariables`의 `total_point__delta`로 노출됩니다.

<Info>
  `INCREMENT_FACT`는 엔진 내부 fact 변경만 수행합니다. 외부 시스템(예: 포인트 서비스) 동기화가 필요하면 동일 룰 안에서 `[INCREMENT_FACT, EMIT_EVENT]` 체인으로 구성하세요. 엔진은 `INCREMENT_FACT` 대신 외부 시스템을 호출하지 않습니다 — 이는 엔진 상태와 외부 상태 사이의 audit-grade 분리를 보존하기 위함입니다.
</Info>

### INCREMENT\_FACT — Fixed Amount

```json theme={null}
{
  "type": "INCREMENT_FACT",
  "parameters": {
    "targetVar": "total_point",
    "method": "AMOUNT",
    "value": 500
  }
}
```

### EMIT\_EVENT

외부 integration에 generic 이벤트 페이로드를 발행합니다. 엔진은 `integrationId`와 비어있지 않은 `eventPayload` 맵의 존재만 검증합니다 — 페이로드 구조는 integration provider의 책임입니다.

```json theme={null}
{
  "type": "EMIT_EVENT",
  "parameters": {
    "integrationId": "<your-integration-id>",
    "eventPayload": {
      "couponId": "WELCOME_VIP_2026"
    }
  }
}
```

도메인 전용 키(`couponId`, `ticketId`, `claimId` 등)는 integration provider가 라우팅합니다. 이로써 `EMIT_EVENT`는 진정한 generic primitive가 됩니다 — 쿠폰 발행, 티켓 생성, 보험 청구 제출 등 어떤 이벤트성 외부 호출에도 사용할 수 있습니다.

### EMIT\_NOTIFICATION

외부 integration을 통해 알림을 발송합니다.

```json theme={null}
{
  "type": "EMIT_NOTIFICATION",
  "parameters": {
    "integrationId": "<your-notification-integration-id>",
    "targetVar": "phone_number",
    "notificationPayload": {
      "channel": "SMS",
      "templateId": "ORDER_CONFIRM_001",
      "variables": { "order_id": "order_id" }
    }
  }
}
```

`targetVar`는 수신자 fact를 지정합니다 (예: `phone_number`, `email`, `device_token`). `notificationPayload`는 채널, 템플릿, 변수 매핑을 integration provider에 전달 — 엔진은 이 키들을 해석하지 않습니다.

### BLOCK — 요청 차단

```json theme={null}
{
  "type": "BLOCK",
  "parameters": { "reason": "Suspected fraud" }
}
```

### SET\_FACT — Output Variable

<Warning>
  `SET_FACT`는 **리터럴 값 setter**이지 expression evaluator가 아닙니다. 출력의 키에 고정 값을 할당합니다. 산술 표현식이나 다른 fact 참조는 지원하지 않습니다.
</Warning>

```json theme={null}
{
  "type": "SET_FACT",
  "parameters": { "key": "risk_level", "value": "HIGH" }
}
```

### ADD\_TAG

```json theme={null}
{
  "type": "ADD_TAG",
  "parameters": {
    "tag": "VIP_VERIFIED",
    "targetVar": "user_tags"
  }
}
```

리스트 fact에 태그를 추가합니다. 중복은 자동으로 방지됩니다. `targetVar`를 생략하면 `user_tags`가 기본값입니다.

### EMIT\_WEBHOOK — Basic

```json theme={null}
{
  "type": "EMIT_WEBHOOK",
  "parameters": {
    "url": "https://api.example.com/webhooks/orders",
    "method": "POST"
  }
}
```

`payloadTemplate` 없이 사용하면 엔진은 모든 input/output facts를 요청 본문으로 전송합니다 (시스템 변수 제외).

### EMIT\_WEBHOOK — With Payload Template

`payloadTemplate`로 요청 본문을 커스터마이징하세요. 템플릿 변수는 실행 시점에 치환됩니다.

```json theme={null}
{
  "type": "EMIT_WEBHOOK",
  "parameters": {
    "url": "<integration-id-or-direct-url>",
    "method": "POST",
    "payloadTemplate": {
      "text": "🔔 Rule {{ruleName}} fired\nCustomer: {{fact.customer_tier}}\nAmount: {{output.payment_amount}}"
    }
  }
}
```

#### 사용 가능한 템플릿 변수

| 변수               | 설명               | 예시 값                                          |
| ---------------- | ---------------- | --------------------------------------------- |
| `{{fact.xxx}}`   | 입력 fact 값        | `{{fact.payment_amount}}` → `150000`          |
| `{{output.xxx}}` | 출력 변수 (액션 후)     | `{{output.payment_amount__delta}}` → `-20000` |
| `{{timestamp}}`  | 실행 시각 (ISO-8601) | `2026-04-14T05:30:00Z`                        |
| `{{ruleName}}`   | 매칭된 룰 이름         | `VIP 20% Discount`                            |
| `{{groupName}}`  | 정책 그룹 이름         | `Payment Policy`                              |
| `{{versionNo}}`  | 실행된 버전 번호        | `5`                                           |
| `{{xxx}}`        | 직접 fact 조회 (단축형) | `{{customer_tier}}` → `VIP`                   |

<Info>
  **Slack**은 `{"text": "..."}`, **Discord**는 `{"content": "..."}` 형식이 필요합니다. 각 플랫폼이 기대하는 형식에 맞춰 `payloadTemplate`을 사용하세요.
</Info>

<Warning>
  `{{fact.xxx}}`와 `{{output.xxx}}`는 동일한 데이터 맵을 참조합니다. 이전 액션(예: `MUTATE_FACT`)이 `payment_amount`를 변경하면, `{{fact.payment_amount}}`와 `{{output.payment_amount}}` 모두 변경된 값을 반영합니다.
</Warning>

## 생성 변수 (Generated Variables)

변경된 facts와 별도로, 엔진은 액션별 변화 정보를 `generatedVariables`로 노출합니다:

| 패턴                   | 의미                                           | 생성 액션            |
| -------------------- | -------------------------------------------- | ---------------- |
| `{refVar}__delta`    | `refVar`의 순변화량 (`mutated - original`, 부호 포함) | `MUTATE_FACT`    |
| `{targetVar}__delta` | `targetVar`의 순증분량 (항상 0 이상)                  | `INCREMENT_FACT` |

한 룰에서 여러 액션이 같은 fact를 변경하면 `__delta`는 누적 변화량을 보고합니다. 액션별 스냅샷은 audit drill-down용으로 `executionTraces`에 보관됩니다.

## Mutex — 룰 단위 충돌 해소

한 버전 내에서 룰들은 **mutex 그룹**에 속할 수 있으며, 이를 통해 매칭되는 룰 중 몇 개를 발화시킬지 제어합니다.

| 필드              | 설명                                               |
| --------------- | ------------------------------------------------ |
| `mutexGroup`    | 관련 룰을 그룹핑하는 문자열 키 (예: `"best-discount"`)         |
| `mutexMode`     | `NONE`(default), `EXCLUSIVE`, 또는 `MAX_N`         |
| `mutexStrategy` | `FIRST_MATCH`, `HIGHEST_PRIORITY`, `MAX_BENEFIT` |
| `mutexLimit`    | 이 그룹에서 발화 가능한 최대 룰 수 (`MAX_N` 모드용)               |

`mutexMode`가 `EXCLUSIVE`이면 winning 룰 하나만 발화합니다. `MAX_N`이면 우선순위 순서로 최대 `mutexLimit`개의 룰이 발화합니다. 발화하지 못한 다른 매칭 룰은 **Decision Trace**에 status `BLOCKED` + reason `MUTEX_PRIORITY_LOST` 또는 `MUTEX_LIMIT_REACHED`로 기록됩니다 ([Decision Trace](#decision-trace) 참조).

## Decision Trace

룰 평가 결과는 모두 **decision trace 엔트리**로 기록되어 *해당 룰이 발화했는지 / 왜 그렇게 결정됐는지*를 설명합니다. 이는 LexQ의 audit-grade 추론 영역의 핵심입니다 — 모든 결과는 특정 status와 reason code로 추적할 수 있습니다.

decision trace 엔트리는 두 분류 필드를 가집니다:

* **`status`** — 결과의 상위 분류
* **`reasonCode`** — 그 분류 안에서의 구체적 사유

### DecisionStatus

| Status         | 의미                                          |
| -------------- | ------------------------------------------- |
| `SELECTED`     | 룰이 발화하고 액션이 실행됨                             |
| `NO_MATCH`     | 룰의 condition 이 false 로 평가됨                  |
| `NOT_SELECTED` | 경쟁 이전 사전 필터링 (예: effective date — 향후 지원 예정) |
| `BLOCKED`      | mutex 또는 activation 그룹 경쟁에서 탈락              |
| `ERROR`        | 평가 도중 액션 또는 엔진 오류 발생                        |

### DecisionReasonCode

| ReasonCode               | 의미                                                           |
| ------------------------ | ------------------------------------------------------------ |
| `FINAL_WINNER`           | 이 평가에서 선택된 룰                                                 |
| `EFFECTIVE_DATE_INVALID` | 룰의 유효 기간 `[from, to]` 범위 밖 (향후 지원 예정)                        |
| `CONDITION_MISMATCH`     | 룰의 condition 트리가 입력 facts와 불일치                               |
| `MUTEX_PRIORITY_LOST`    | EXCLUSIVE mutex (limit=1) — 같은 그룹의 다른 룰이 더 높은 우선순위 또는 점수     |
| `MUTEX_LIMIT_REACHED`    | MAX\_N mutex (limit>1) — `mutexLimit` 이 더 높은 우선순위 룰들로 이미 채워짐 |
| `GROUP_PRIORITY_LOST`    | EXCLUSIVE activation 그룹 (limit=1) — 다른 룰이 priority 로 승리      |
| `GROUP_LIMIT_REACHED`    | MAX\_N activation 그룹 (limit>1) — `executionLimit` 이 이미 채워짐   |
| `ACTION_ERROR`           | 이 룰의 액션 executor 가 실행 중 throw                                |
| `ENGINE_ERROR`           | 엔진 자체가 룰 처리 전/도중에 실패                                         |

### Status × ReasonCode 매핑

| Status         | 가능한 ReasonCode                                                                             |
| -------------- | ------------------------------------------------------------------------------------------ |
| `SELECTED`     | `FINAL_WINNER`                                                                             |
| `NO_MATCH`     | `CONDITION_MISMATCH`                                                                       |
| `NOT_SELECTED` | `EFFECTIVE_DATE_INVALID` (향후 지원 예정)                                                        |
| `BLOCKED`      | `MUTEX_PRIORITY_LOST`, `MUTEX_LIMIT_REACHED`, `GROUP_PRIORITY_LOST`, `GROUP_LIMIT_REACHED` |
| `ERROR`        | `ACTION_ERROR`, `ENGINE_ERROR`                                                             |

<Info>
  "왜 내 룰이 발화하지 않았지?"를 디버깅할 때 decision trace는 두 단계로 답을 줍니다 — `status`는 어떤 분류의 필터링이 룰을 제거했는지 알려주고, `reasonCode`는 정확히 어떤 검증이 룰을 거부했는지 알려줍니다.
</Info>

## 완전한 룰 예시

```json theme={null}
{
  "name": "VIP 20% Discount",
  "priority": 0,
  "condition": {
    "type": "GROUP",
    "operator": "AND",
    "children": [
      { "type": "SINGLE", "field": "customer_tier", "operator": "EQUALS", "value": "VIP", "valueType": "STRING" },
      { "type": "SINGLE", "field": "payment_amount", "operator": "GREATER_THAN_OR_EQUAL", "value": 100000, "valueType": "NUMBER" }
    ]
  },
  "actions": [
    { "type": "MUTATE_FACT", "parameters": { "refVar": "payment_amount", "operator": "SUB", "method": "PERCENTAGE", "rate": 20 } },
    { "type": "SET_FACT", "parameters": { "key": "discount_applied", "value": true } }
  ],
  "mutexGroup": "best-discount",
  "mutexMode": "EXCLUSIVE",
  "mutexStrategy": "HIGHEST_PRIORITY",
  "isEnabled": true
}
```

## 다음 단계

<CardGroup cols={2}>
  <Card title="Fact Definitions" icon="database" href="/ko/guides/fact-definitions">
    룰이 기대하는 입력 변수를 정의하세요.
  </Card>

  <Card title="Dry Run" icon="flask-vial" href="/ko/guides/dry-run">
    배포 전에 룰을 테스트하세요.
  </Card>
</CardGroup>
