@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.
- package/AGENTS.md +34 -32
- package/CONTEXT.md +41 -47
- package/README.md +8 -6
- package/dist/index.js +156 -389
- package/dist/mcp/register.js +102 -167
- package/package.json +1 -1
- package/skills/lexq-execution/SKILL.md +120 -65
- package/skills/lexq-groups/SKILL.md +78 -38
- package/skills/lexq-recipes/SKILL.md +122 -149
- package/skills/lexq-rules/SKILL.md +154 -42
- package/skills/lexq-shared/SKILL.md +48 -28
- package/skills/lexq-simulation/SKILL.md +47 -27
|
@@ -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
|
|
8
|
-
|
|
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": "
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
|
77
|
-
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
|
|
83
|
-
|
|
|
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
|
|
143
|
-
|
|
144
|
-
| `MUTATE_FACT`
|
|
145
|
-
| `
|
|
146
|
-
| `
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
|
151
|
-
|
|
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
|
-
"
|
|
256
|
+
"targetVar": "payment_amount",
|
|
163
257
|
"method": "PERCENTAGE",
|
|
164
258
|
"operator": "SUB",
|
|
165
|
-
"
|
|
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
|
-
"
|
|
312
|
+
"targetVar": "payment_amount",
|
|
221
313
|
"method": "PERCENTAGE",
|
|
222
314
|
"operator": "SUB",
|
|
223
|
-
"
|
|
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
|
-
"
|
|
333
|
+
"targetVar": "payment_amount",
|
|
242
334
|
"method": "PERCENTAGE",
|
|
243
335
|
"operator": "SUB",
|
|
244
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
54
|
-
8. lexq
|
|
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
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
| `auth`
|
|
86
|
-
| `status`
|
|
87
|
-
| `groups`
|
|
88
|
-
| `versions`
|
|
89
|
-
| `rules`
|
|
90
|
-
| `facts`
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
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
|
-
"
|
|
128
|
+
"errorCode": "P-002"
|
|
124
129
|
}
|
|
125
130
|
```
|
|
126
131
|
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
|
132
|
-
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
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`.**
|
|
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
|
|
83
|
+
### Recommended: Always Use `--debug`
|
|
85
84
|
|
|
86
85
|
```bash
|
|
87
|
-
lexq analytics dry-run --version-id <vid> --debug --
|
|
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
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
|
149
|
-
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
|
|
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` |
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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 --
|
|
355
|
+
lexq analytics dry-run --version-id <vid> --debug --json '{
|
|
336
356
|
"facts": { "payment_amount": 150000, "customer_tier": "VIP" }
|
|
337
357
|
}'
|
|
338
358
|
|