@lexq/cli 0.1.39 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,15 +4,18 @@
4
4
 
5
5
  ## Overview
6
6
 
7
- A **Policy Rule** is a condition → actions pair within a version. Rules are evaluated in priority order (0 = highest).
8
- When a rule's condition matches the input facts, its actions fire.
7
+ A **Policy Rule** is a condition → actions pair within a version. Rules are evaluated in priority
8
+ order **lower number wins**, and priorities are 1-based (1 is highest). When a rule's condition
9
+ matches the input facts, its actions fire.
10
+
11
+ `priority` is assigned by the server, not by you. New rules are appended last. Use
12
+ `lexq rules reorder` to change the order.
9
13
 
10
14
  ## Rule Structure
11
15
 
12
16
  ```json
13
17
  {
14
18
  "name": "VIP 10% Discount",
15
- "priority": 0,
16
19
  "condition": {
17
20
  ...
18
21
  },
@@ -21,7 +24,7 @@ When a rule's condition matches the input facts, its actions fire.
21
24
  ],
22
25
  "mutexGroup": null,
23
26
  "mutexMode": "NONE",
24
- "mutexStrategy": "FIRST_MATCH",
27
+ "mutexStrategy": "HIGHEST_PRIORITY",
25
28
  "mutexLimit": null,
26
29
  "isEnabled": true
27
30
  }
@@ -70,17 +73,53 @@ Conditions use a tree structure with two node types: `SINGLE` and `GROUP`.
70
73
 
71
74
  ### Operators
72
75
 
73
- | Operator | Types | Description |
74
- |-------------------------|----------------|-----------------------------------|
75
- | `EQUALS` | all | Exact match |
76
- | `NOT_EQUALS` | all | Negation |
77
- | `GREATER_THAN` | NUMBER | `>` |
78
- | `GREATER_THAN_OR_EQUAL` | NUMBER | `>=` |
79
- | `LESS_THAN` | NUMBER | `<` |
80
- | `LESS_THAN_OR_EQUAL` | NUMBER | `<=` |
81
- | `CONTAINS` | STRING | Substring match |
82
- | `IN` | STRING, NUMBER | Value is in the provided list |
83
- | `NOT_IN` | STRING, NUMBER | Value is not in the provided list |
76
+ Operators are constrained by the **left fact's type**. Using one outside its type is rejected
77
+ by the server — check `lexq facts list` before choosing.
78
+
79
+ | Fact type | Allowed operators |
80
+ |-------------------------------|--------------------------------------------------------------------------------------------------------------------|
81
+ | `STRING` | `EQUALS`, `NOT_EQUALS`, `CONTAINS`, `IN`, `NOT_IN` |
82
+ | `NUMBER` | `EQUALS`, `NOT_EQUALS`, `GREATER_THAN`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN`, `LESS_THAN_OR_EQUAL`, `IN`, `NOT_IN` |
83
+ | `BOOLEAN` | `EQUALS`, `NOT_EQUALS` |
84
+ | `LIST_STRING` / `LIST_NUMBER` | `HAS_ANY`, `HAS_ALL`, `HAS_NONE` |
85
+
86
+ | Operator | Description | `value` |
87
+ |-------------------------------------------------------------------------------|------------------------------------------------------------|---------|
88
+ | `EQUALS` / `NOT_EQUALS` | Exact match / negation | scalar |
89
+ | `GREATER_THAN` / `GREATER_THAN_OR_EQUAL` / `LESS_THAN` / `LESS_THAN_OR_EQUAL` | Numeric comparison | scalar |
90
+ | `CONTAINS` | **Substring** match on a STRING fact — not list membership | scalar |
91
+ | `IN` / `NOT_IN` | Scalar fact is (not) in the given list | array |
92
+ | `HAS_ANY` | List fact has **at least one** of the given values | array |
93
+ | `HAS_ALL` | List fact has **all** of the given values | array |
94
+ | `HAS_NONE` | List fact has **none** of the given values | array |
95
+
96
+ **`IN` vs `HAS_*` — mirrors of each other.** This is the most common mistake here:
97
+
98
+ ```json
99
+ // scalar fact, list value
100
+ {
101
+ "field": "region",
102
+ "operator": "IN",
103
+ "value": [
104
+ "KR",
105
+ "JP"
106
+ ],
107
+ "valueType": "LIST_STRING"
108
+ }
109
+
110
+ // list fact, list value
111
+ {
112
+ "field": "user_tags",
113
+ "operator": "HAS_ANY",
114
+ "value": [
115
+ "VIP",
116
+ "GOLD"
117
+ ],
118
+ "valueType": "LIST_STRING"
119
+ }
120
+ ```
121
+
122
+ Do **not** use `CONTAINS` on a list fact — that idiom works in some rule engines but is rejected here.
84
123
 
85
124
  ### Value Types
86
125
 
@@ -139,16 +178,71 @@ Conditions use a tree structure with two node types: `SINGLE` and `GROUP`.
139
178
 
140
179
  Each rule can have multiple actions. Actions fire sequentially.
141
180
 
142
- | Type | Description | Key Parameters |
143
- |---------------------|----------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
144
- | `MUTATE_FACT` | Mutate a fact value (arithmetic) | `refVar`, `method` (PERCENTAGE/AMOUNT), `operator` (ADD/SUB/MUL/DIV), `rate` or `value`, `rounding` |
145
- | `INCREMENT_FACT` | Increment a fact (cumulative add) | `targetVar`, `refVar`, `method`, `value` or `rate`, `rounding` |
146
- | `EMIT_EVENT` | Emit an event to an external integration (coupons, etc.) | `integrationId`, `eventPayload` (Map) |
147
- | `BLOCK` | Block the transaction | `reason`, `code` |
148
- | `EMIT_NOTIFICATION` | Send a notification | `integrationId`, `target`, `notificationPayload` (Map) |
149
- | `EMIT_WEBHOOK` | Call an external URL | `url`, `payloadTemplate` |
150
- | `SET_FACT` | Set a fact value (literal assignment) | `key`, `value` |
151
- | `ADD_TAG` | Add a tag to the result | `tag` |
181
+ | Type | Description | Key Parameters |
182
+ |---------------|--------------------------------------------|----------------------------------------------------------------------|
183
+ | `MUTATE_FACT` | Arithmetic change to a numeric fact | `targetVar`, `operator`, `method`, `operand`, `refVar?`, `rounding?` |
184
+ | `SET_FACT` | Assign a literal value creates if absent | `targetVar`, `value` |
185
+ | `BLOCK` | Record a rejection decision | `reason` |
186
+
187
+ ### `MUTATE_FACT` parameters
188
+
189
+ | Parameter | Required | Meaning |
190
+ |-------------|----------|-------------------------------------------------------------------------------|
191
+ | `targetVar` | always | The fact this action **reads and writes**. Must already exist as a number. |
192
+ | `operator` | always | `ASSIGN` \| `ADD` \| `SUB` \| `MUL` \| `DIV` |
193
+ | `method` | always | `PERCENTAGE` \| `AMOUNT` — dictates the unit of `operand` |
194
+ | `operand` | always | The arithmetic operand. Percent when PERCENTAGE, absolute amount when AMOUNT. |
195
+ | `refVar` | optional | Base for percentage calculation. Omit to use `targetVar` itself. |
196
+ | `rounding` | optional | `{ scale: 0..16, mode?: HALF_UP \| ... }`. Omit for lossless full precision. |
197
+
198
+ **operator × method matrix**
199
+
200
+ | operator | `AMOUNT` | `PERCENTAGE` |
201
+ |----------|------------------------|-------------------------------------|
202
+ | `ASSIGN` | `targetVar = operand` | `targetVar = refVar × operand/100` |
203
+ | `ADD` | `targetVar += operand` | `targetVar += refVar × operand/100` |
204
+ | `SUB` | `targetVar -= operand` | `targetVar -= refVar × operand/100` |
205
+ | `MUL` | `targetVar *= operand` | `targetVar *= (operand/100 + 1)` |
206
+ | `DIV` | `targetVar /= operand` | **invalid** |
207
+
208
+ `DIV` + `PERCENTAGE` is rejected — use `MUL` with the inverse. `DIV` + `AMOUNT` requires
209
+ `operand !== 0`.
210
+
211
+ **`refVar` is only meaningful in `PERCENTAGE` × {`ASSIGN`, `ADD`, `SUB`}.** Specifying it in any
212
+ other cell is an error, not a silent no-op. `AMOUNT` has no base concept, and `MUL` × `PERCENTAGE`
213
+ is a multiplier shorthand that does not read a base.
214
+
215
+ Use `refVar` when the base differs from the target:
216
+
217
+ ```json
218
+ {
219
+ "type": "MUTATE_FACT",
220
+ "parameters": {
221
+ "targetVar": "loyalty_point",
222
+ "refVar": "order_total",
223
+ "operator": "ADD",
224
+ "method": "PERCENTAGE",
225
+ "operand": 5
226
+ }
227
+ }
228
+ ```
229
+
230
+ `loyalty_point += order_total × 5%` — two different facts. Omitting `refVar` would compute
231
+ `loyalty_point += loyalty_point × 5%` instead.
232
+
233
+ **Ranges are not constrained.** Negative operands and percentages above 100 are valid — refunds
234
+ (`-5`), surcharges (`150`), risk scores, and game points all need them.
235
+
236
+ ### `SET_FACT` vs `MUTATE_FACT`
237
+
238
+ `SET_FACT` creates the fact if it does not exist. `MUTATE_FACT` requires the target to already be
239
+ present as a number and throws otherwise. "Make something that wasn't there" is `SET_FACT`'s job.
240
+
241
+ ### `BLOCK` does not halt execution
242
+
243
+ `BLOCK` records a rejection decision by writing the `is_blocked` fact. Subsequent actions in the
244
+ same rule and subsequent winning rules still run. Enforcement is the caller's responsibility —
245
+ read `is_blocked` from the response.
152
246
 
153
247
  ### Action Example: 10% Discount via MUTATE_FACT
154
248
 
@@ -159,10 +253,10 @@ Reduces `payment_amount` by 10%. `__delta` is auto-generated in `generatedVariab
159
253
  {
160
254
  "type": "MUTATE_FACT",
161
255
  "parameters": {
162
- "refVar": "payment_amount",
256
+ "targetVar": "payment_amount",
163
257
  "method": "PERCENTAGE",
164
258
  "operator": "SUB",
165
- "rate": 10,
259
+ "operand": 10,
166
260
  "rounding": {
167
261
  "mode": "HALF_UP",
168
262
  "scale": 0
@@ -177,8 +271,7 @@ Reduces `payment_amount` by 10%. `__delta` is auto-generated in `generatedVariab
177
271
  {
178
272
  "type": "BLOCK",
179
273
  "parameters": {
180
- "reason": "Suspected fraud",
181
- "code": "FRAUD_DETECTED"
274
+ "reason": "Suspected fraud"
182
275
  }
183
276
  }
184
277
  ```
@@ -204,7 +297,6 @@ lexq rules get --group-id <gid> --version-id <vid> --id <ruleId>
204
297
  ```bash
205
298
  lexq rules create --group-id <gid> --version-id <vid> --json '{
206
299
  "name": "VIP 10% Discount",
207
- "priority": 0,
208
300
  "condition": {
209
301
  "type": "GROUP",
210
302
  "operator": "AND",
@@ -217,10 +309,10 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
217
309
  {
218
310
  "type": "MUTATE_FACT",
219
311
  "parameters": {
220
- "refVar": "payment_amount",
312
+ "targetVar": "payment_amount",
221
313
  "method": "PERCENTAGE",
222
314
  "operator": "SUB",
223
- "rate": 10,
315
+ "operand": 10,
224
316
  "rounding": { "mode": "HALF_UP", "scale": 0 }
225
317
  }
226
318
  }
@@ -238,10 +330,10 @@ lexq rules update --group-id <gid> --version-id <vid> --id <ruleId> --json '{
238
330
  {
239
331
  "type": "MUTATE_FACT",
240
332
  "parameters": {
241
- "refVar": "payment_amount",
333
+ "targetVar": "payment_amount",
242
334
  "method": "PERCENTAGE",
243
335
  "operator": "SUB",
244
- "rate": 15,
336
+ "operand": 15,
245
337
  "rounding": { "mode": "HALF_UP", "scale": 0 }
246
338
  }
247
339
  }
@@ -258,7 +350,8 @@ lexq rules delete --group-id <gid> --version-id <vid> --id <ruleId> --force
258
350
 
259
351
  ### Reorder Rules
260
352
 
261
- Pass rule IDs in desired priority order (index 0 = highest priority):
353
+ Pass rule IDs in desired order. The server assigns priority 1, 2, 3, the first ID becomes
354
+ priority 1 (highest):
262
355
 
263
356
  ```bash
264
357
  lexq rules reorder --group-id <gid> --version-id <vid> \
@@ -278,26 +371,44 @@ lexq rules toggle --group-id <gid> --version-id <vid> --id <ruleId> --enabled fa
278
371
 
279
372
  Within a single version, rules can belong to a `mutexGroup` to limit how many fire.
280
373
 
281
- | mutexMode | Behavior |
282
- |-------------|-----------------------------------------------|
283
- | `NONE` | All matching rules fire (default) |
284
- | `EXCLUSIVE` | Only one rule per mutex group fires |
285
- | `MAX_N` | Up to `mutexLimit` rules per mutex group fire |
374
+ | mutexMode | Behavior |
375
+ |-------------|---------------------------------------------------------------|
376
+ | `NONE` | All matching rules fire (default) |
377
+ | `EXCLUSIVE` | Only one rule per mutex group fires |
378
+ | `MAX_N` | Up to `mutexLimit` rules fire. Omit it and the server sets 2. |
286
379
 
287
380
  ```bash
288
381
  lexq rules create --group-id <gid> --version-id <vid> --json '{
289
382
  "name": "Discount A",
290
- "priority": 0,
291
383
  "mutexGroup": "discounts",
292
384
  "mutexMode": "EXCLUSIVE",
293
385
  "mutexStrategy": "HIGHEST_PRIORITY",
294
386
  "condition": { ... },
295
387
  "actions": [ ... ]
296
388
  }'
389
+
390
+ # Top two of the group fire
391
+ lexq rules create --group-id <gid> --version-id <vid> --json '{
392
+ "name": "Stackable Promo A",
393
+ "mutexGroup": "promos",
394
+ "mutexMode": "MAX_N",
395
+ "mutexLimit": 2,
396
+ "mutexStrategy": "HIGHEST_PRIORITY",
397
+ "condition": { ... },
398
+ "actions": [ ... ]
399
+ }'
297
400
  ```
298
401
 
299
402
  **Constraint:** All rules in the same `mutexGroup` must use identical `mutexMode` and `mutexStrategy`.
300
403
 
404
+ **`mutexGroup` is what turns mutex on.** Sending `mutexMode` without a `mutexGroup` is silently normalized to `NONE` —
405
+ there is no group to be exclusive within. Conversely, setting a `mutexGroup` without a `mutexMode` defaults to
406
+ `EXCLUSIVE`.
407
+
408
+ `mutexStrategy` currently accepts only `HIGHEST_PRIORITY` — the rule with the lowest priority
409
+ number in the group wins. `mutexMode: MAX_N` requires `mutexLimit`; omit it and the server
410
+ sets 2.
411
+
301
412
  ## Pre-Create Checklist
302
413
 
303
414
  Before creating rules, always:
@@ -305,4 +416,5 @@ Before creating rules, always:
305
416
  1. **Check available facts:** `lexq facts list`
306
417
  2. **Confirm the version is DRAFT:** `lexq versions get --group-id <gid> --id <vid>` → status must be `DRAFT`
307
418
  3. **Use exact fact keys** from the fact definitions (snake_case, case-sensitive)
308
- 4. **Match value types** — a fact defined as `NUMBER` must receive numeric values, not strings
419
+ 4. **Match value types** — a fact defined as `NUMBER` must receive numeric values, not strings
420
+ 5. **Match the operator to the fact type** — list-typed facts accept only `HAS_ANY` / `HAS_ALL` / `HAS_NONE`
@@ -35,7 +35,7 @@ lexq groups list --api-key lexq_us_override_key
35
35
  | **Policy Version** | A snapshot of rules within a group. Follows DRAFT → ACTIVE → ARCHIVED lifecycle. | Git branch / commit |
36
36
  | **Policy Rule** | A condition + actions pair within a version. Evaluated in priority order. | if-then statement |
37
37
  | **Fact Definition** | Input schema — declares available variables and their types (STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER). | Function parameter |
38
- | **Deployment** | Promotes a PUBLISHED version to live traffic. Supports rollback. | Production release |
38
+ | **Deployment** | Promotes an ACTIVE version to live traffic. Supports rollback and scheduled arming. | Production release |
39
39
  | **Dry Run** | Tests a single input against a DRAFT or ACTIVE version without side effects. | Unit test |
40
40
  | **Simulation** | Batch-tests a version against historical execution data. Compares with a baseline. | Integration test suite |
41
41
 
@@ -50,15 +50,15 @@ This is the typical lifecycle. **Always follow this order:**
50
50
  4. lexq rules create → Add rules with conditions + actions
51
51
  5. lexq analytics dry-run → Test with sample facts (validate before publish)
52
52
  6. lexq deploy publish → DRAFT → ACTIVE (locks the version)
53
- 7. lexq deploy live Deploy ACTIVE version to production
54
- 8. lexq analytics simulation Run batch comparison against baseline (optional)
53
+ 7. lexq analytics simulation Batch-test against historical data with a baseline (optional)
54
+ 8. lexq deploy live Deploy ACTIVE version to production
55
55
  ```
56
56
 
57
57
  ### Critical Ordering Constraints
58
58
 
59
59
  - **Cannot create rules without a DRAFT version.** Create the version first.
60
60
  - **Cannot publish without at least one rule.** Add rules before publishing.
61
- - **Cannot modify a published version.** Clone it to create a new DRAFT if changes are needed.
61
+ **Cannot modify an ACTIVE version.** Clone it to create a new DRAFT if changes are needed.
62
62
  - **Cannot deploy a DRAFT version.** Must publish first (DRAFT → ACTIVE).
63
63
  - **Always run `lexq analytics dry-run` before publishing.** This is your safety net.
64
64
 
@@ -80,19 +80,24 @@ Every command accepts these flags:
80
80
 
81
81
  ## Command Groups
82
82
 
83
- | Group | Commands | Description |
84
- |----------------|--------------------------------------------------------------------------------------------|---------------------------------|
85
- | `auth` | `login`, `logout`, `whoami` | Authentication |
86
- | `status` | (root) | API health check |
87
- | `groups` | `list`, `get`, `create`, `update`, `delete` + `ab-test start\|stop\|adjust` | Policy group CRUD + A/B testing |
88
- | `versions` | `list`, `get`, `create`, `update`, `delete`, `clone` | Version CRUD |
89
- | `rules` | `list`, `get`, `create`, `update`, `delete`, `reorder`, `toggle` | Rule CRUD |
90
- | `facts` | `list`, `create`, `update`, `delete` | Fact definition CRUD |
91
- | `deploy` | `publish`, `live`, `rollback`, `undeploy`, `history`, `detail`, `overview` | Deployment lifecycle |
92
- | `analytics` | `dry-run`, `dry-run-compare`, `requirements`, `simulation start/status/list/cancel/export` | Testing & analysis |
93
- | `history` | `list`, `get`, `stats` | Execution history |
94
- | `integrations` | `list`, `get`, `save`, `delete`, `config-spec` | External integrations |
95
- | `logs` | `list`, `get`, `action`, `bulk-action` | Failure log management |
83
+ | Group | Commands | Description |
84
+ |-------------------------|----------------------------------------------------------------------------------------------------------------------------|---------------------------------|
85
+ | `auth` | `login`, `logout`, `whoami` | Authentication |
86
+ | `status` | (root) | API health check |
87
+ | `groups` | `list`, `get`, `create`, `update`, `delete`, `reorder` + `ab-test start\|stop\|adjust` | Policy group CRUD + A/B testing |
88
+ | `versions` | `list`, `get`, `create`, `update`, `delete`, `clone` | Version CRUD |
89
+ | `rules` | `list`, `get`, `create`, `update`, `delete`, `reorder`, `toggle` | Rule CRUD |
90
+ | `facts` | `list`, `create`, `update`, `delete` | Fact definition CRUD |
91
+ | `domain-templates` | `list`, `preview`, `apply` | Industry starter packs |
92
+ | `deploy` | `publish`, `live`, `rollback`, `undeploy`, `history`, `detail`, `overview`, `schedule`, `unschedule`, `schedules` | Deployment lifecycle |
93
+ | `analytics` | `dry-run`, `dry-run-compare`, `requirements`, `simulation start\|status\|list\|cancel\|export`, `dataset upload\|template` | Testing & analysis |
94
+ | `profile` | (root, takes `<groupId>`) | Per-rule latency profile |
95
+ | `history` | `list`, `get`, `stats` | Execution history |
96
+ | `replay` | `decision`, `start`, `list`, `get`, `cancel` | Decision replay |
97
+ | `provenance` | `get`, `reveal-audits` | Decision provenance + PII audit |
98
+ | `logs` | `list`, `get`, `action`, `bulk-action` | Failure log management |
99
+ | `webhook-subscriptions` | `list`, `get`, `save`, `delete`, `test` | Platform event webhooks |
100
+ | `serve` | `--mcp` | MCP server over stdio |
96
101
 
97
102
  ## Pagination
98
103
 
@@ -120,19 +125,27 @@ API errors return:
120
125
  {
121
126
  "result": "ERROR",
122
127
  "message": "Policy version not found.",
123
- "code": "ENTITY_NOT_FOUND"
128
+ "errorCode": "P-002"
124
129
  }
125
130
  ```
126
131
 
127
- **Common error codes and what to do:**
132
+ `errorCode` is a stable identifier of the form `<domain>-<number>`. `message` is the
133
+ human-readable explanation and is localized by `Accept-Language`. **Branch on `errorCode`, never
134
+ on `message`.**
128
135
 
129
- | Code | Meaning | Action |
130
- |--------------------|--------------------------------|---------------------------------------------------------------------------------|
131
- | `ENTITY_NOT_FOUND` | Resource doesn't exist | Verify the ID. Run the corresponding `list` command. |
132
- | `INVALID_INPUT` | Validation failed | Check required fields. Run `lexq analytics requirements` for fact requirements. |
133
- | `CANNOT_MODIFY` | Version is not DRAFT | Clone the version to create a new DRAFT: `lexq versions clone` |
134
- | `EMPTY_RULES` | Publish attempted with 0 rules | Add at least one rule before publishing. |
135
- | `UNAUTHORIZED` | Invalid or missing API key | Run `lexq auth login` with a valid key. |
136
+ Prefixes you will encounter through the CLI:
137
+
138
+ | Prefix | Domain | Example cause |
139
+ |--------|-----------------------|------------------------------------------------|
140
+ | `C-` | Common validation | Malformed or missing request field |
141
+ | `A-` | Auth | Invalid or missing API key |
142
+ | `P-` | Policy group/version | Not found, wrong lifecycle state, already live |
143
+ | `ACT-` | Action parameters | Missing or invalid action parameter |
144
+ | `FD-` | Fact definitions | Duplicate key, system fact immutable |
145
+ | `AN-` | Analytics | Dry-run or simulation failure |
146
+ | `FL-` | Failure logs | Log not found |
147
+ | `WH-` | Webhook subscriptions | Invalid URL, delivery failure |
148
+ | `I-` | Idempotency | Duplicate idempotency key |
136
149
 
137
150
  ## Important Conventions
138
151
 
@@ -140,6 +153,13 @@ API errors return:
140
153
  2. **IDs are UUIDs.** Always copy the full ID from list/create output — do not guess.
141
154
  3. **Dates use ISO 8601.** Example: `2025-01-01T00:00:00Z`. Time zone is UTC.
142
155
  4. **JSON bodies via `--json`.** Most create/update commands accept `--json '<body>'` for the request body.
143
- 5. **File input via `--file`.** Analytics commands accept `--file path/to/body.json` as an alternative to `--json`.
156
+ 5. **File input via `--file`.** `analytics dry-run`, `dry-run-compare`, `simulation start` accept
157
+ `--file path/to/body.json` as an alternative to `--json`. `analytics dataset upload` requires
158
+ `--file` (CSV or JSON). `simulation export` and `dataset template` write output with `--output`.
144
159
  6. **Confirmation prompts.** Destructive operations (delete, cancel, undeploy) prompt for confirmation. Use `--force` to
145
- skip in automation.
160
+ skip in automation.
161
+
162
+ ## Skill Formatting
163
+
164
+ Catalog-style skills (`lexq-recipes`) separate independent items with `---`. Narrative skills do
165
+ not use it at all. Never mix both in one file.
@@ -78,17 +78,19 @@ lexq analytics dry-run --version-id <vid> --json '{
78
78
  | Flag | Description | Default |
79
79
  |-----------------|-----------------------------------------------------|---------|
80
80
  | `--debug` | Include execution traces (which rules matched, why) | `false` |
81
- | `--mock` | Mock external calls (webhooks, integrations) | `false` |
82
81
  | `--file <path>` | Read request body from file instead of `--json` | — |
83
82
 
84
- ### Recommended: Always Use `--debug --mock`
83
+ ### Recommended: Always Use `--debug`
85
84
 
86
85
  ```bash
87
- lexq analytics dry-run --version-id <vid> --debug --mock --json '{
86
+ lexq analytics dry-run --version-id <vid> --debug --json '{
88
87
  "facts": { "payment_amount": 150000, "customer_tier": "VIP" }
89
88
  }'
90
89
  ```
91
90
 
91
+ Dry run has no side effects — actions only mutate the fact map in memory. There is nothing
92
+ external to mock.
93
+
92
94
  ### Response Structure
93
95
 
94
96
  ```json
@@ -111,7 +113,7 @@ lexq analytics dry-run --version-id <vid> --debug --mock --json '{
111
113
  "ruleId": "...",
112
114
  "ruleName": "VIP 10% Discount",
113
115
  "matched": true,
114
- "matchExpression": "(customer_tier == VIP AND payment_amount >= 100000)",
116
+ "matchExpression": "(customer_tier == 'VIP') && (payment_amount >= 100000)",
115
117
  "generatedActions": [
116
118
  {
117
119
  "type": "MUTATE_FACT",
@@ -143,26 +145,51 @@ lexq analytics dry-run --version-id <vid> --debug --mock --json '{
143
145
 
144
146
  ### Reading Decision Traces
145
147
 
146
- | Status | Meaning |
147
- |-----------------|---------------------------------------------|
148
- | `SELECTED` | Rule matched and its actions fired |
149
- | `NO_MATCH` | Condition did not match the input |
150
- | `NOT_SELECTED` | Matched but excluded by conflict resolution |
151
- | `BLOCKED_MUTEX` | Blocked by mutex group constraint |
152
- | `LOST_PRIORITY` | Lost to a higher-priority rule |
153
- | `DROPPED_LIMIT` | Execution limit reached |
154
- | `ERROR` | Rule evaluation failed |
148
+ Each trace carries a `status` (what happened) and a `reasonCode` (why).
149
+
150
+ | Status | Meaning |
151
+ |------------|---------------------------------------------------------------------------------|
152
+ | `SELECTED` | Rule matched and its actions fired |
153
+ | `NO_MATCH` | Condition did not match, or could not be evaluated |
154
+ | `BLOCKED` | Matched but lost conflict resolution — see `reasonCode` for which round and why |
155
+ | `ERROR` | Action execution failed |
156
+
157
+ `BLOCKED` is unrelated to the `BLOCK` action. A `BLOCK` action writes the `is_blocked` fact and its own rule stays
158
+ `SELECTED`; `BLOCKED` means the rule was dropped by activation-group or mutex competition.
155
159
 
156
160
  ### Reading Reason Codes
157
161
 
158
162
  | Code | Meaning |
159
163
  |-----------------------|----------------------------------------------------------|
160
164
  | `FINAL_WINNER` | Successfully executed |
161
- | `CONDITION_MISMATCH` | Input facts didn't satisfy the condition |
165
+ | `CONDITION_MISMATCH` | Condition not satisfied, or could not be evaluated |
162
166
  | `MUTEX_PRIORITY_LOST` | Another rule in the same mutex group had higher priority |
163
167
  | `MUTEX_LIMIT_REACHED` | Mutex group's max rules already fired |
168
+ | `GROUP_PRIORITY_LOST` | Another group in the same activation group won |
164
169
  | `GROUP_LIMIT_REACHED` | Group's `executionLimit` reached |
165
- | `ACTION_ERROR` | Action execution failed (e.g., webhook timeout) |
170
+ | `ACTION_ERROR` | Action execution failed (e.g. required fact absent) |
171
+ | `ENGINE_ERROR` | Internal engine failure |
172
+
173
+ A version outside its effective date range is filtered **before** evaluation — no trace is produced for it at all. If a
174
+ rule you expect never appears in `decisionTraces`, check the version's `effectiveFrom` / `effectiveTo` with
175
+ `lexq versions get`.
176
+
177
+ #### `reasonDetail` on unevaluable conditions
178
+
179
+ `CONDITION_MISMATCH` covers two different things, distinguished by `reasonDetail`:
180
+
181
+ - **empty** — the condition was evaluated and did not match
182
+ - **`Evaluation error: <code>`** — the condition could not be evaluated at all
183
+
184
+ | Code | Meaning |
185
+ |-------------------------|--------------------------------------------------------------------|
186
+ | `FACT_NOT_PROVIDED` | The rule references a fact absent from the request |
187
+ | `FACT_TYPE_MISMATCH` | The fact's runtime type does not match the condition |
188
+ | `UNSUPPORTED_FACT_TYPE` | Operator not valid for the fact's type (e.g. `CONTAINS` on a list) |
189
+ | `MALFORMED_RULE` | The stored rule is structurally invalid |
190
+
191
+ A rule from another group referencing facts you did not send yields `FACT_NOT_PROVIDED` — this
192
+ is normal, not an error.
166
193
 
167
194
  ## 3. Dry Run Compare
168
195
 
@@ -218,19 +245,19 @@ lexq analytics simulation start --json '{
218
245
  ```bash
219
246
  # 1. Download template (optional)
220
247
  lexq analytics dataset template \
221
- --group-id --version-id --format csv --output template.csv
248
+ --group-id <gid> --version-id <vid> --format csv --output template.csv
222
249
 
223
250
  # 2. Upload dataset
224
251
  lexq analytics dataset upload --file ./my-data.csv
225
- # → path: datasets//a1b2c3d4e5f6.csv
252
+ # → path: datasets/<tenantId>/a1b2c3d4e5f6.csv
226
253
 
227
254
  # 3. Start simulation with uploaded path
228
255
  lexq analytics simulation start --json '{
229
- "policyVersionId": "",
256
+ "policyVersionId": "<vid>",
230
257
  "dataset": {
231
258
  "type": "UPLOADED",
232
259
  "source": "S3_BUCKET",
233
- "path": "datasets//a1b2c3d4e5f6.csv"
260
+ "path": "<path returned by dataset upload>"
234
261
  },
235
262
  "options": { "includeRuleStats": true, "maxRecords": 10000 }
236
263
  }'
@@ -239,13 +266,6 @@ lexq analytics simulation start --json '{
239
266
  **CSV format:** Header row with fact keys, data rows with values. Types auto-detected.
240
267
  **JSON format:** Array of objects `[{"key": "value"}, ...]`
241
268
 
242
- ### MCP — Dataset Tools
243
-
244
- ```
245
- lexq_dataset_template → Get sample CSV/JSON based on version's required facts
246
- lexq_dataset_upload → Upload inline CSV/JSON content to S3, returns path
247
- ```
248
-
249
269
  ### Check Status (Poll)
250
270
 
251
271
  ```bash
@@ -332,7 +352,7 @@ lexq analytics simulation export --id <simulationId> --format csv --output resul
332
352
  lexq analytics requirements --group-id <gid> --version-id <vid>
333
353
 
334
354
  # 2. Dry-run with representative inputs
335
- lexq analytics dry-run --version-id <vid> --debug --mock --json '{
355
+ lexq analytics dry-run --version-id <vid> --debug --json '{
336
356
  "facts": { "payment_amount": 150000, "customer_tier": "VIP" }
337
357
  }'
338
358