@lexq/cli 0.1.39 → 0.1.40
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/dist/index.js +39 -6
- package/dist/mcp/register.js +18 -3
- package/package.json +1 -1
- package/skills/lexq-recipes/SKILL.md +71 -2
- package/skills/lexq-rules/SKILL.md +56 -14
- package/skills/lexq-simulation/SKILL.md +38 -18
package/dist/index.js
CHANGED
|
@@ -933,9 +933,27 @@ ${data.length} total`);
|
|
|
933
933
|
]
|
|
934
934
|
}'
|
|
935
935
|
|
|
936
|
-
Condition Operators:
|
|
937
|
-
EQUALS, NOT_EQUALS,
|
|
938
|
-
|
|
936
|
+
Condition Operators (by fact type):
|
|
937
|
+
STRING EQUALS, NOT_EQUALS, CONTAINS, IN, NOT_IN
|
|
938
|
+
NUMBER EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL,
|
|
939
|
+
LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN
|
|
940
|
+
BOOLEAN EQUALS, NOT_EQUALS
|
|
941
|
+
LIST_STRING HAS_ANY, HAS_ALL, HAS_NONE
|
|
942
|
+
LIST_NUMBER HAS_ANY, HAS_ALL, HAS_NONE
|
|
943
|
+
|
|
944
|
+
Using an operator outside its fact type is rejected by the server.
|
|
945
|
+
|
|
946
|
+
List-typed facts (HAS_*) — the value is always an array:
|
|
947
|
+
HAS_ANY fact has at least one of the given values
|
|
948
|
+
HAS_ALL fact has all of the given values
|
|
949
|
+
HAS_NONE fact has none of the given values
|
|
950
|
+
|
|
951
|
+
Example:
|
|
952
|
+
{ "type": "SINGLE", "field": "user_tags", "operator": "HAS_ANY",
|
|
953
|
+
"value": ["VIP", "GOLD"], "valueType": "LIST_STRING" }
|
|
954
|
+
|
|
955
|
+
CONTAINS is substring match on STRING facts, not list membership.
|
|
956
|
+
IN is the mirror of HAS_*: scalar fact, list value.
|
|
939
957
|
|
|
940
958
|
Action Types:
|
|
941
959
|
MUTATE_FACT, INCREMENT_FACT, EMIT_EVENT, BLOCK, EMIT_NOTIFICATION, EMIT_WEBHOOK, SET_FACT, ADD_TAG
|
|
@@ -3712,8 +3730,23 @@ function registerRuleTools(server, callApi) {
|
|
|
3712
3730
|
defined (non-blocking, version-wide) — use it to decide what to register.
|
|
3713
3731
|
|
|
3714
3732
|
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
|
|
3715
|
-
Operators: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, IN, NOT_IN
|
|
3716
3733
|
Value types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
|
|
3734
|
+
|
|
3735
|
+
Operators are constrained by the LEFT fact's type (from lexq_facts_list). Using one outside
|
|
3736
|
+
its type is rejected by the server — check the fact type before choosing an operator.
|
|
3737
|
+
- STRING fact: EQUALS, NOT_EQUALS, CONTAINS, IN, NOT_IN
|
|
3738
|
+
- NUMBER fact: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN
|
|
3739
|
+
- BOOLEAN fact: EQUALS, NOT_EQUALS
|
|
3740
|
+
- LIST_* fact: HAS_ANY, HAS_ALL, HAS_NONE (only these)
|
|
3741
|
+
|
|
3742
|
+
HAS_* query list-typed facts. Value is always an array whose element type matches the fact:
|
|
3743
|
+
- HAS_ANY: fact has at least one of the given values
|
|
3744
|
+
- HAS_ALL: fact has all of the given values
|
|
3745
|
+
- HAS_NONE: fact has none of the given values
|
|
3746
|
+
Example: { "type": "SINGLE", "field": "user_tags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
|
|
3747
|
+
|
|
3748
|
+
Do NOT use CONTAINS on a list fact — CONTAINS is substring match on STRING facts only.
|
|
3749
|
+
IN is the mirror of HAS_*: IN takes a scalar fact with a list value; HAS_* takes lists on both sides.
|
|
3717
3750
|
|
|
3718
3751
|
Actions: [{ type, parameters }]
|
|
3719
3752
|
|
|
@@ -3722,14 +3755,14 @@ function registerRuleTools(server, callApi) {
|
|
|
3722
3755
|
- INCREMENT_FACT: { targetVar: string, method: "PERCENTAGE"|"AMOUNT", refVar?: string (required when PERCENTAGE), rate?: number (when PERCENTAGE), value?: number (when AMOUNT), rounding?: RoundingOption } targetVar (accumulation target) must exist at execution; refVar (PERCENTAGE source) must exist when method is PERCENTAGE. Each is supplied as an input fact or written by a prior action in this rule — a missing required fact throws (no 0 default). Note: external system call (e.g. point system sync) is NOT a primitive responsibility. Compose [INCREMENT_FACT, EMIT_EVENT] chain instead.
|
|
3723
3756
|
- EMIT_EVENT: { integrationId: uuid, eventPayload: object (Map<string,unknown>, ≥1 entry) } eventPayload is passed through to the integration provider as-is. Domain-specific keys (couponId, ticketId, etc.) are routed by the provider, not validated by the engine.
|
|
3724
3757
|
- BLOCK: { reason: string }
|
|
3725
|
-
- EMIT_NOTIFICATION: { integrationId: uuid, targetVar: string, notificationPayload: object (Map<string,unknown>, ≥1 entry) } targetVar identifies the recipient fact (e.g. phone_number / email / device_token).
|
|
3758
|
+
- EMIT_NOTIFICATION: { integrationId: uuid, targetVar: string, notificationPayload: object (Map<string,unknown>, ≥1 entry) } targetVar identifies the recipient fact (e.g. phone_number / email / device_token) and is REQUIRED — the named fact must be present in the request or the action throws. (Contrast with ADD_TAG, where targetVar is an optional write target that is created if absent.)
|
|
3726
3759
|
- EMIT_WEBHOOK: { url: string, method: "POST", payloadTemplate?: object } payloadTemplate is optional. Without it, all facts are sent as-is. With it, the object is sent as the HTTP body with {{variables}} replaced at execution time. Variables: {{fact.xxx}}, {{output.xxx}}, {{timestamp}}, {{ruleName}}, {{groupName}}, {{versionNo}}, {{xxx}} (shorthand).
|
|
3727
3760
|
Platform examples:
|
|
3728
3761
|
Slack: { "text": "Rule {{ruleName}} fired — {{fact.customer_tier}}" }
|
|
3729
3762
|
Discord: { "content": "Rule {{ruleName}} fired — {{fact.customer_tier}}" }
|
|
3730
3763
|
Generic: { "event": "rule_matched", "rule": "{{ruleName}}", "amount": "{{output.payment_amount}}" }
|
|
3731
3764
|
- SET_FACT: { key: string, value: string|number|boolean }
|
|
3732
|
-
- ADD_TAG: { tag: string, targetVar
|
|
3765
|
+
- ADD_TAG: { tag: string, targetVar?: string (defaults to "user_tags") } Appends tag to a LIST_STRING fact, creating it if absent. Adding an existing tag is a no-op (idempotent). Read tags back with HAS_ANY / HAS_ALL / HAS_NONE.
|
|
3733
3766
|
|
|
3734
3767
|
RoundingOption (optional, MUTATE_FACT / INCREMENT_FACT only): { scale: integer (0..16), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
|
|
3735
3768
|
`,
|
package/dist/mcp/register.js
CHANGED
|
@@ -323,8 +323,23 @@ function registerRuleTools(server, callApi) {
|
|
|
323
323
|
defined (non-blocking, version-wide) — use it to decide what to register.
|
|
324
324
|
|
|
325
325
|
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
|
|
326
|
-
Operators: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, IN, NOT_IN
|
|
327
326
|
Value types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
|
|
327
|
+
|
|
328
|
+
Operators are constrained by the LEFT fact's type (from lexq_facts_list). Using one outside
|
|
329
|
+
its type is rejected by the server — check the fact type before choosing an operator.
|
|
330
|
+
- STRING fact: EQUALS, NOT_EQUALS, CONTAINS, IN, NOT_IN
|
|
331
|
+
- NUMBER fact: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN
|
|
332
|
+
- BOOLEAN fact: EQUALS, NOT_EQUALS
|
|
333
|
+
- LIST_* fact: HAS_ANY, HAS_ALL, HAS_NONE (only these)
|
|
334
|
+
|
|
335
|
+
HAS_* query list-typed facts. Value is always an array whose element type matches the fact:
|
|
336
|
+
- HAS_ANY: fact has at least one of the given values
|
|
337
|
+
- HAS_ALL: fact has all of the given values
|
|
338
|
+
- HAS_NONE: fact has none of the given values
|
|
339
|
+
Example: { "type": "SINGLE", "field": "user_tags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
|
|
340
|
+
|
|
341
|
+
Do NOT use CONTAINS on a list fact — CONTAINS is substring match on STRING facts only.
|
|
342
|
+
IN is the mirror of HAS_*: IN takes a scalar fact with a list value; HAS_* takes lists on both sides.
|
|
328
343
|
|
|
329
344
|
Actions: [{ type, parameters }]
|
|
330
345
|
|
|
@@ -333,14 +348,14 @@ function registerRuleTools(server, callApi) {
|
|
|
333
348
|
- INCREMENT_FACT: { targetVar: string, method: "PERCENTAGE"|"AMOUNT", refVar?: string (required when PERCENTAGE), rate?: number (when PERCENTAGE), value?: number (when AMOUNT), rounding?: RoundingOption } targetVar (accumulation target) must exist at execution; refVar (PERCENTAGE source) must exist when method is PERCENTAGE. Each is supplied as an input fact or written by a prior action in this rule — a missing required fact throws (no 0 default). Note: external system call (e.g. point system sync) is NOT a primitive responsibility. Compose [INCREMENT_FACT, EMIT_EVENT] chain instead.
|
|
334
349
|
- EMIT_EVENT: { integrationId: uuid, eventPayload: object (Map<string,unknown>, ≥1 entry) } eventPayload is passed through to the integration provider as-is. Domain-specific keys (couponId, ticketId, etc.) are routed by the provider, not validated by the engine.
|
|
335
350
|
- BLOCK: { reason: string }
|
|
336
|
-
- EMIT_NOTIFICATION: { integrationId: uuid, targetVar: string, notificationPayload: object (Map<string,unknown>, ≥1 entry) } targetVar identifies the recipient fact (e.g. phone_number / email / device_token).
|
|
351
|
+
- EMIT_NOTIFICATION: { integrationId: uuid, targetVar: string, notificationPayload: object (Map<string,unknown>, ≥1 entry) } targetVar identifies the recipient fact (e.g. phone_number / email / device_token) and is REQUIRED — the named fact must be present in the request or the action throws. (Contrast with ADD_TAG, where targetVar is an optional write target that is created if absent.)
|
|
337
352
|
- EMIT_WEBHOOK: { url: string, method: "POST", payloadTemplate?: object } payloadTemplate is optional. Without it, all facts are sent as-is. With it, the object is sent as the HTTP body with {{variables}} replaced at execution time. Variables: {{fact.xxx}}, {{output.xxx}}, {{timestamp}}, {{ruleName}}, {{groupName}}, {{versionNo}}, {{xxx}} (shorthand).
|
|
338
353
|
Platform examples:
|
|
339
354
|
Slack: { "text": "Rule {{ruleName}} fired — {{fact.customer_tier}}" }
|
|
340
355
|
Discord: { "content": "Rule {{ruleName}} fired — {{fact.customer_tier}}" }
|
|
341
356
|
Generic: { "event": "rule_matched", "rule": "{{ruleName}}", "amount": "{{output.payment_amount}}" }
|
|
342
357
|
- SET_FACT: { key: string, value: string|number|boolean }
|
|
343
|
-
- ADD_TAG: { tag: string, targetVar
|
|
358
|
+
- ADD_TAG: { tag: string, targetVar?: string (defaults to "user_tags") } Appends tag to a LIST_STRING fact, creating it if absent. Adding an existing tag is a no-op (idempotent). Read tags back with HAS_ANY / HAS_ALL / HAS_NONE.
|
|
344
359
|
|
|
345
360
|
RoundingOption (optional, MUTATE_FACT / INCREMENT_FACT only): { scale: integer (0..16), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
|
|
346
361
|
`,
|
package/package.json
CHANGED
|
@@ -256,7 +256,7 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
256
256
|
"type": "EMIT_NOTIFICATION",
|
|
257
257
|
"parameters": {
|
|
258
258
|
"integrationId": "<notification-integration-uuid>",
|
|
259
|
-
"
|
|
259
|
+
"targetVar": "user_id",
|
|
260
260
|
"notificationPayload": {
|
|
261
261
|
"channel": "PUSH",
|
|
262
262
|
"templateId": "welcome_points"
|
|
@@ -502,4 +502,73 @@ lexq analytics simulation start --json '{
|
|
|
502
502
|
|
|
503
503
|
# 6. If simulation passes, deploy
|
|
504
504
|
lexq deploy live --group-id <gid> --version-id <newVid> --memo "Migration complete"
|
|
505
|
-
```
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## Recipe 11: Tag-Based Segmentation
|
|
510
|
+
|
|
511
|
+
**Goal:** Write tags in one rule, branch on them in another.
|
|
512
|
+
|
|
513
|
+
`user_tags` is a `LIST_STRING` fact seeded automatically for every tenant. `ADD_TAG` appends to
|
|
514
|
+
it; `HAS_ANY` / `HAS_ALL` / `HAS_NONE` read it.
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
# Tags are written by earlier rules (or supplied as input facts).
|
|
518
|
+
lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
519
|
+
"name": "Tag High-Value Customers",
|
|
520
|
+
"condition": {
|
|
521
|
+
"type": "SINGLE", "field": "lifetime_value", "operator": "GREATER_THAN_OR_EQUAL",
|
|
522
|
+
"value": 1000000, "valueType": "NUMBER"
|
|
523
|
+
},
|
|
524
|
+
"actions": [
|
|
525
|
+
{ "type": "ADD_TAG", "parameters": { "tag": "high_value" } }
|
|
526
|
+
]
|
|
527
|
+
}'
|
|
528
|
+
|
|
529
|
+
# Branch on any one of several tags
|
|
530
|
+
lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
531
|
+
"name": "Priority Support for VIP or High Value",
|
|
532
|
+
"condition": {
|
|
533
|
+
"type": "SINGLE", "field": "user_tags", "operator": "HAS_ANY",
|
|
534
|
+
"value": ["VIP", "high_value"], "valueType": "LIST_STRING"
|
|
535
|
+
},
|
|
536
|
+
"actions": [{ "type": "SET_FACT", "parameters": { "key": "support_tier", "value": "PRIORITY" } }]
|
|
537
|
+
}'
|
|
538
|
+
|
|
539
|
+
# Require every tag
|
|
540
|
+
lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
541
|
+
"name": "Beta Feature for Verified VIP",
|
|
542
|
+
"condition": {
|
|
543
|
+
"type": "SINGLE", "field": "user_tags", "operator": "HAS_ALL",
|
|
544
|
+
"value": ["VIP", "verified"], "valueType": "LIST_STRING"
|
|
545
|
+
},
|
|
546
|
+
"actions": [{ "type": "SET_FACT", "parameters": { "key": "beta_enabled", "value": true } }]
|
|
547
|
+
}'
|
|
548
|
+
|
|
549
|
+
# Exclude tagged users
|
|
550
|
+
lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
551
|
+
"name": "Promo Excludes Fraud Review",
|
|
552
|
+
"condition": {
|
|
553
|
+
"type": "SINGLE", "field": "user_tags", "operator": "HAS_NONE",
|
|
554
|
+
"value": ["fraud_review", "suspended"], "valueType": "LIST_STRING"
|
|
555
|
+
},
|
|
556
|
+
"actions": [{ "type": "MUTATE_FACT", "parameters": {
|
|
557
|
+
"refVar": "payment_amount", "method": "PERCENTAGE", "operator": "SUB", "rate": 5,
|
|
558
|
+
"rounding": { "mode": "HALF_UP", "scale": 0 }
|
|
559
|
+
}}]
|
|
560
|
+
}'
|
|
561
|
+
|
|
562
|
+
# Verify
|
|
563
|
+
lexq analytics dry-run --version-id <vid> --debug --mock --json '{
|
|
564
|
+
"facts": { "user_tags": ["VIP", "verified"], "payment_amount": 100000, "lifetime_value": 500000 }
|
|
565
|
+
}'
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**Notes**
|
|
569
|
+
|
|
570
|
+
- The `value` is always an array, even for a single tag: `"value": ["VIP"]`.
|
|
571
|
+
- `ADD_TAG` writes to `user_tags` by default. Pass `targetVar` to append to a different `LIST_STRING` fact. The list is
|
|
572
|
+
created if absent, and re-adding an existing tag is a no-op.
|
|
573
|
+
- An empty array makes `HAS_ALL` and `HAS_NONE` always true — they never look at the fact.
|
|
574
|
+
- Rules fire in priority order, so a tag written by rule 0 is visible to rule 1.
|
|
@@ -70,17 +70,53 @@ Conditions use a tree structure with two node types: `SINGLE` and `GROUP`.
|
|
|
70
70
|
|
|
71
71
|
### Operators
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
|
77
|
-
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
|
|
83
|
-
|
|
|
73
|
+
Operators are constrained by the **left fact's type**. Using one outside its type is rejected
|
|
74
|
+
by the server — check `lexq facts list` before choosing.
|
|
75
|
+
|
|
76
|
+
| Fact type | Allowed operators |
|
|
77
|
+
|-------------------------------|--------------------------------------------------------------------------------------------------------------------|
|
|
78
|
+
| `STRING` | `EQUALS`, `NOT_EQUALS`, `CONTAINS`, `IN`, `NOT_IN` |
|
|
79
|
+
| `NUMBER` | `EQUALS`, `NOT_EQUALS`, `GREATER_THAN`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN`, `LESS_THAN_OR_EQUAL`, `IN`, `NOT_IN` |
|
|
80
|
+
| `BOOLEAN` | `EQUALS`, `NOT_EQUALS` |
|
|
81
|
+
| `LIST_STRING` / `LIST_NUMBER` | `HAS_ANY`, `HAS_ALL`, `HAS_NONE` |
|
|
82
|
+
|
|
83
|
+
| Operator | Description | `value` |
|
|
84
|
+
|-------------------------------------------------------------------------------|------------------------------------------------------------|---------|
|
|
85
|
+
| `EQUALS` / `NOT_EQUALS` | Exact match / negation | scalar |
|
|
86
|
+
| `GREATER_THAN` / `GREATER_THAN_OR_EQUAL` / `LESS_THAN` / `LESS_THAN_OR_EQUAL` | Numeric comparison | scalar |
|
|
87
|
+
| `CONTAINS` | **Substring** match on a STRING fact — not list membership | scalar |
|
|
88
|
+
| `IN` / `NOT_IN` | Scalar fact is (not) in the given list | array |
|
|
89
|
+
| `HAS_ANY` | List fact has **at least one** of the given values | array |
|
|
90
|
+
| `HAS_ALL` | List fact has **all** of the given values | array |
|
|
91
|
+
| `HAS_NONE` | List fact has **none** of the given values | array |
|
|
92
|
+
|
|
93
|
+
**`IN` vs `HAS_*` — mirrors of each other.** This is the most common mistake here:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
// scalar fact, list value
|
|
97
|
+
{
|
|
98
|
+
"field": "region",
|
|
99
|
+
"operator": "IN",
|
|
100
|
+
"value": [
|
|
101
|
+
"KR",
|
|
102
|
+
"JP"
|
|
103
|
+
],
|
|
104
|
+
"valueType": "LIST_STRING"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// list fact, list value
|
|
108
|
+
{
|
|
109
|
+
"field": "user_tags",
|
|
110
|
+
"operator": "HAS_ANY",
|
|
111
|
+
"value": [
|
|
112
|
+
"VIP",
|
|
113
|
+
"GOLD"
|
|
114
|
+
],
|
|
115
|
+
"valueType": "LIST_STRING"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Do **not** use `CONTAINS` on a list fact — that idiom works in some rule engines but is rejected here.
|
|
84
120
|
|
|
85
121
|
### Value Types
|
|
86
122
|
|
|
@@ -145,10 +181,15 @@ Each rule can have multiple actions. Actions fire sequentially.
|
|
|
145
181
|
| `INCREMENT_FACT` | Increment a fact (cumulative add) | `targetVar`, `refVar`, `method`, `value` or `rate`, `rounding` |
|
|
146
182
|
| `EMIT_EVENT` | Emit an event to an external integration (coupons, etc.) | `integrationId`, `eventPayload` (Map) |
|
|
147
183
|
| `BLOCK` | Block the transaction | `reason`, `code` |
|
|
148
|
-
| `EMIT_NOTIFICATION` | Send a notification | `integrationId`, `
|
|
184
|
+
| `EMIT_NOTIFICATION` | Send a notification | `integrationId`, `targetVar`, `notificationPayload` (Map) |
|
|
149
185
|
| `EMIT_WEBHOOK` | Call an external URL | `url`, `payloadTemplate` |
|
|
150
186
|
| `SET_FACT` | Set a fact value (literal assignment) | `key`, `value` |
|
|
151
|
-
| `ADD_TAG` |
|
|
187
|
+
| `ADD_TAG` | Append a tag to a list fact | `tag`, `targetVar` (optional, default `user_tags`) |
|
|
188
|
+
|
|
189
|
+
**Two different `targetVar` meanings.** `EMIT_NOTIFICATION.targetVar` is a **read** — it names the
|
|
190
|
+
fact holding the recipient (`phone_number`, `email`, `device_token`), and the action throws if that
|
|
191
|
+
fact is absent from the request. `ADD_TAG.targetVar` is a **write** — the list is created if absent,
|
|
192
|
+
and adding a tag that is already present is a no-op.
|
|
152
193
|
|
|
153
194
|
### Action Example: 10% Discount via MUTATE_FACT
|
|
154
195
|
|
|
@@ -305,4 +346,5 @@ Before creating rules, always:
|
|
|
305
346
|
1. **Check available facts:** `lexq facts list`
|
|
306
347
|
2. **Confirm the version is DRAFT:** `lexq versions get --group-id <gid> --id <vid>` → status must be `DRAFT`
|
|
307
348
|
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
|
|
349
|
+
4. **Match value types** — a fact defined as `NUMBER` must receive numeric values, not strings
|
|
350
|
+
5. **Match the operator to the fact type** — list-typed facts accept only `HAS_ANY` / `HAS_ALL` / `HAS_NONE`
|
|
@@ -111,7 +111,7 @@ lexq analytics dry-run --version-id <vid> --debug --mock --json '{
|
|
|
111
111
|
"ruleId": "...",
|
|
112
112
|
"ruleName": "VIP 10% Discount",
|
|
113
113
|
"matched": true,
|
|
114
|
-
"matchExpression": "(customer_tier == VIP
|
|
114
|
+
"matchExpression": "(customer_tier == 'VIP') && (payment_amount >= 100000)",
|
|
115
115
|
"generatedActions": [
|
|
116
116
|
{
|
|
117
117
|
"type": "MUTATE_FACT",
|
|
@@ -143,26 +143,46 @@ lexq analytics dry-run --version-id <vid> --debug --mock --json '{
|
|
|
143
143
|
|
|
144
144
|
### Reading Decision Traces
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
|
149
|
-
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
| `ERROR`
|
|
146
|
+
Each trace carries a `status` (what happened) and a `reasonCode` (why).
|
|
147
|
+
|
|
148
|
+
| Status | Meaning |
|
|
149
|
+
|----------------|----------------------------------------------------|
|
|
150
|
+
| `SELECTED` | Rule matched and its actions fired |
|
|
151
|
+
| `NO_MATCH` | Condition did not match, or could not be evaluated |
|
|
152
|
+
| `NOT_SELECTED` | Matched but excluded by conflict resolution |
|
|
153
|
+
| `BLOCKED` | Blocked by a mutex group or group activation limit |
|
|
154
|
+
| `ERROR` | Action execution failed |
|
|
155
155
|
|
|
156
156
|
### Reading Reason Codes
|
|
157
157
|
|
|
158
|
-
| Code
|
|
159
|
-
|
|
160
|
-
| `FINAL_WINNER`
|
|
161
|
-
| `CONDITION_MISMATCH`
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
158
|
+
| Code | Meaning |
|
|
159
|
+
|--------------------------|----------------------------------------------------------|
|
|
160
|
+
| `FINAL_WINNER` | Successfully executed |
|
|
161
|
+
| `CONDITION_MISMATCH` | Condition not satisfied, or could not be evaluated |
|
|
162
|
+
| `EFFECTIVE_DATE_INVALID` | Outside the version's effective date range |
|
|
163
|
+
| `MUTEX_PRIORITY_LOST` | Another rule in the same mutex group had higher priority |
|
|
164
|
+
| `MUTEX_LIMIT_REACHED` | Mutex group's max rules already fired |
|
|
165
|
+
| `GROUP_PRIORITY_LOST` | Another group in the same activation group won |
|
|
166
|
+
| `GROUP_LIMIT_REACHED` | Group's `executionLimit` reached |
|
|
167
|
+
| `ACTION_ERROR` | Action execution failed (e.g., webhook timeout) |
|
|
168
|
+
| `ENGINE_ERROR` | Internal engine failure |
|
|
169
|
+
|
|
170
|
+
#### `reasonDetail` on unevaluable conditions
|
|
171
|
+
|
|
172
|
+
`CONDITION_MISMATCH` covers two different things, distinguished by `reasonDetail`:
|
|
173
|
+
|
|
174
|
+
- **empty** — the condition was evaluated and did not match
|
|
175
|
+
- **`Evaluation error: <code>`** — the condition could not be evaluated at all
|
|
176
|
+
|
|
177
|
+
| Code | Meaning |
|
|
178
|
+
|-------------------------|--------------------------------------------------------------------|
|
|
179
|
+
| `FACT_NOT_PROVIDED` | The rule references a fact absent from the request |
|
|
180
|
+
| `FACT_TYPE_MISMATCH` | The fact's runtime type does not match the condition |
|
|
181
|
+
| `UNSUPPORTED_FACT_TYPE` | Operator not valid for the fact's type (e.g. `CONTAINS` on a list) |
|
|
182
|
+
| `MALFORMED_RULE` | The stored rule is structurally invalid |
|
|
183
|
+
|
|
184
|
+
A rule from another group referencing facts you did not send yields `FACT_NOT_PROVIDED` — this
|
|
185
|
+
is normal, not an error.
|
|
166
186
|
|
|
167
187
|
## 3. Dry Run Compare
|
|
168
188
|
|