@lexq/cli 0.1.50 → 0.1.51
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 +5 -3
- package/CONTEXT.md +1 -1
- package/README.md +3 -3
- package/dist/index.js +29 -23
- package/dist/mcp/register.js +16 -11
- package/package.json +2 -1
- package/skills/lexq-execution/SKILL.md +1 -1
- package/skills/lexq-recipes/SKILL.md +49 -49
- package/skills/lexq-rules/SKILL.md +21 -21
- package/skills/lexq-shared/SKILL.md +2 -1
- package/skills/lexq-simulation/SKILL.md +21 -21
package/AGENTS.md
CHANGED
|
@@ -93,7 +93,7 @@ lexq facts create --key age --name "User Age" --type NUMBER
|
|
|
93
93
|
lexq rules create --group-id $GID --version-id $VID --json '{
|
|
94
94
|
"name": "Adult Check",
|
|
95
95
|
"condition": {"type":"SINGLE","field":"age","operator":"GREATER_THAN_OR_EQUAL","value":18,"valueType":"NUMBER"},
|
|
96
|
-
"actions": [{"type":"SET_FACT","parameters":{"targetVar":"
|
|
96
|
+
"actions": [{"type":"SET_FACT","parameters":{"targetVar":"isAdult","value":true}}]
|
|
97
97
|
}'
|
|
98
98
|
|
|
99
99
|
# 5. Test
|
|
@@ -151,7 +151,8 @@ The MCP toolset mirrors the CLI command inventory one-to-one.
|
|
|
151
151
|
- Every response is an envelope: the success branch carries `data`; the failure branch carries `errorCode` +
|
|
152
152
|
`message`. Surface both — never a bare "request failed".
|
|
153
153
|
- Pagination is asymmetric: requests send `page`/`size`, responses return `pageNo`/`pageSize`. Pages are 0-indexed.
|
|
154
|
-
- Fact keys
|
|
154
|
+
- Fact keys start with a letter, then letters, numbers, and underscores. Casing is yours to choose
|
|
155
|
+
and keys are case-sensitive. These docs use `camelCase`.
|
|
155
156
|
- Actions never call external systems. The engine mutates facts and records the decision — read the response and
|
|
156
157
|
act on it yourself. Deployment lifecycle webhooks (`lexq webhook-subscriptions`) are the one push channel.
|
|
157
158
|
|
|
@@ -175,6 +176,7 @@ The MCP toolset mirrors the CLI command inventory one-to-one.
|
|
|
175
176
|
the single api-client layer.
|
|
176
177
|
- The hosted MCP server consumes this package from npm. Publish (`v*` tag) **before** redeploying it — CI cannot
|
|
177
178
|
catch that ordering.
|
|
178
|
-
- Gates before commit: `pnpm typecheck` (zero errors), `pnpm lint` (zero warnings), `
|
|
179
|
+
- Gates before commit: `pnpm typecheck` (zero errors), `pnpm lint` (zero warnings), `pnpm test:fact-key`,
|
|
180
|
+
`pnpm test:decimals`.
|
|
179
181
|
- Multi-line CLI help and MCP tool descriptions use `dedent` — template-literal indentation leaks into LLM context.
|
|
180
182
|
- Types mirror engine DTOs exactly; never invent response shapes.
|
package/CONTEXT.md
CHANGED
|
@@ -142,7 +142,7 @@ implemented).
|
|
|
142
142
|
| **Policy Version** | An immutable snapshot of rules. Only DRAFT versions can be modified. |
|
|
143
143
|
| **Policy Rule** | A condition → actions pair. Evaluated in priority order within a version. |
|
|
144
144
|
| **Fact** | An input variable passed during execution. Declared via Fact Definitions with key, type, and name. |
|
|
145
|
-
| **Fact Definition** | Schema declaration for a fact — its key
|
|
145
|
+
| **Fact Definition** | Schema declaration for a fact — its key, value type, display name, and description. |
|
|
146
146
|
| **Dry Run** | Single-input test execution. Returns which rules matched, what actions would fire, and decision traces. |
|
|
147
147
|
| **Simulation** | Batch test replaying historical executions against a version. Compares with a baseline. |
|
|
148
148
|
| **Activation Group** | A logical grouping of Policy Groups for cross-group conflict resolution. |
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ lexq domain-templates apply --template ECOMMERCE
|
|
|
60
60
|
|
|
61
61
|
# 4. Test a rule against your data before shipping
|
|
62
62
|
lexq analytics dry-run --version-id <VERSION_ID> --debug \
|
|
63
|
-
--json '{"facts":{"
|
|
63
|
+
--json '{"facts":{"loyaltyTier":"PLATINUM","purchaseSubtotalUsd":150}}'
|
|
64
64
|
|
|
65
65
|
# 5. Deploy
|
|
66
66
|
lexq deploy publish --group-id <GROUP_ID> --version-id <VERSION_ID> --memo "v1"
|
|
@@ -235,8 +235,8 @@ pnpm start -- groups list
|
|
|
235
235
|
```bash
|
|
236
236
|
pnpm typecheck # Type check
|
|
237
237
|
pnpm lint # ESLint
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
pnpm test:fact-key # Fact key grammar
|
|
239
|
+
pnpm test:decimals # Metric precision display
|
|
240
240
|
```
|
|
241
241
|
|
|
242
242
|
## License
|
package/dist/index.js
CHANGED
|
@@ -954,13 +954,13 @@ ${data.length} total`);
|
|
|
954
954
|
"name": "VIP 20% Discount",
|
|
955
955
|
"condition": {
|
|
956
956
|
"type": "SINGLE",
|
|
957
|
-
"field": "
|
|
957
|
+
"field": "customerTier",
|
|
958
958
|
"operator": "EQUALS",
|
|
959
959
|
"value": "VIP",
|
|
960
960
|
"valueType": "STRING"
|
|
961
961
|
},
|
|
962
962
|
"actions": [
|
|
963
|
-
{ "type": "MUTATE_FACT", "parameters": { "targetVar": "
|
|
963
|
+
{ "type": "MUTATE_FACT", "parameters": { "targetVar": "paymentAmount", "operator": "SUB", "method": "PERCENTAGE", "operand": 20 } }
|
|
964
964
|
]
|
|
965
965
|
}'
|
|
966
966
|
|
|
@@ -980,7 +980,7 @@ ${data.length} total`);
|
|
|
980
980
|
HAS_NONE fact has none of the given values
|
|
981
981
|
|
|
982
982
|
Example:
|
|
983
|
-
{ "type": "SINGLE", "field": "
|
|
983
|
+
{ "type": "SINGLE", "field": "userTags", "operator": "HAS_ANY",
|
|
984
984
|
"value": ["VIP", "GOLD"], "valueType": "LIST_STRING" }
|
|
985
985
|
|
|
986
986
|
CONTAINS is substring match on STRING facts, not list membership.
|
|
@@ -1036,7 +1036,7 @@ ${data.length} total`);
|
|
|
1036
1036
|
$ lexq rules update --group-id <gid> --version-id <vid> --id <rid> --json '{
|
|
1037
1037
|
"name": "VIP 25% Discount",
|
|
1038
1038
|
"actions": [
|
|
1039
|
-
{ "type": "MUTATE_FACT", "parameters": { "targetVar": "
|
|
1039
|
+
{ "type": "MUTATE_FACT", "parameters": { "targetVar": "paymentAmount", "operator": "SUB", "method": "PERCENTAGE", "operand": 25 } }
|
|
1040
1040
|
]
|
|
1041
1041
|
}'
|
|
1042
1042
|
`
|
|
@@ -1180,7 +1180,7 @@ function registerFactCommands(program) {
|
|
|
1180
1180
|
delete Remove a fact definition
|
|
1181
1181
|
action-metadata Show action runtime fact metadata
|
|
1182
1182
|
|
|
1183
|
-
System facts (
|
|
1183
|
+
System facts (userId, userTags) are auto-created.
|
|
1184
1184
|
`
|
|
1185
1185
|
);
|
|
1186
1186
|
facts.command("list").description("List fact definitions").option("--keyword <keyword>", "Filter by keyword").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
|
|
@@ -1266,16 +1266,16 @@ ${data.length} unregistered`);
|
|
|
1266
1266
|
process.exit(1);
|
|
1267
1267
|
}
|
|
1268
1268
|
});
|
|
1269
|
-
facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (
|
|
1269
|
+
facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (letters, numbers, underscores)").option("--name <n>", "Display name").option("--type <type>", "Value type: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER").option("--description <desc>", "Description").option("--required", "Mark as required", false).option("--pii", "Mark as PII \u2014 value is masked on every read surface", false).option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
|
|
1270
1270
|
"after",
|
|
1271
1271
|
dedent6`
|
|
1272
1272
|
|
|
1273
1273
|
Examples:
|
|
1274
|
-
$ lexq facts create --key
|
|
1275
|
-
$ lexq facts create --key
|
|
1274
|
+
$ lexq facts create --key customerTier --name "Customer Tier" --type STRING
|
|
1275
|
+
$ lexq facts create --key orderTotal --name "Order Total" --type NUMBER --required
|
|
1276
1276
|
|
|
1277
1277
|
$ lexq facts create --json '{
|
|
1278
|
-
"key": "
|
|
1278
|
+
"key": "userRegion",
|
|
1279
1279
|
"name": "User Region",
|
|
1280
1280
|
"type": "STRING",
|
|
1281
1281
|
"description": "ISO country code",
|
|
@@ -1283,7 +1283,8 @@ ${data.length} unregistered`);
|
|
|
1283
1283
|
}'
|
|
1284
1284
|
|
|
1285
1285
|
Value Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
|
|
1286
|
-
Key Format:
|
|
1286
|
+
Key Format: starts with a letter, then letters, numbers, and underscores.
|
|
1287
|
+
Casing is yours to choose (e.g., paymentAmount, payment_amount)
|
|
1287
1288
|
`
|
|
1288
1289
|
).action(async (opts) => {
|
|
1289
1290
|
try {
|
|
@@ -1868,7 +1869,7 @@ function registerAnalyticsCommands(program) {
|
|
|
1868
1869
|
|
|
1869
1870
|
Examples:
|
|
1870
1871
|
$ lexq analytics dry-run --version-id <vid> --debug \\
|
|
1871
|
-
--json '{"facts": {"
|
|
1872
|
+
--json '{"facts": {"paymentAmount": 150000, "customerTier": "VIP"}}'
|
|
1872
1873
|
|
|
1873
1874
|
$ lexq analytics dry-run --version-id <vid> --file test-input.json
|
|
1874
1875
|
|
|
@@ -1907,7 +1908,7 @@ function registerAnalyticsCommands(program) {
|
|
|
1907
1908
|
$ lexq analytics dry-run-compare --json '{
|
|
1908
1909
|
"versionIdA": "<version-a-id>",
|
|
1909
1910
|
"versionIdB": "<version-b-id>",
|
|
1910
|
-
"facts": {"
|
|
1911
|
+
"facts": {"paymentAmount": 100000, "customerTier": "VIP"}
|
|
1911
1912
|
}'
|
|
1912
1913
|
|
|
1913
1914
|
Shows side-by-side which rules matched and what actions fired for each version.
|
|
@@ -3585,7 +3586,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3585
3586
|
- HAS_ANY: fact has at least one of the given values
|
|
3586
3587
|
- HAS_ALL: fact has all of the given values
|
|
3587
3588
|
- HAS_NONE: fact has none of the given values
|
|
3588
|
-
Example: { "type": "SINGLE", "field": "
|
|
3589
|
+
Example: { "type": "SINGLE", "field": "userTags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
|
|
3589
3590
|
|
|
3590
3591
|
Do NOT use CONTAINS on a list fact — CONTAINS is substring match on STRING facts only.
|
|
3591
3592
|
IN is the mirror of HAS_*: IN takes a scalar fact with a list value; HAS_* takes lists on both sides.
|
|
@@ -3603,7 +3604,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3603
3604
|
refVar is the base for percentage calculation and is OPTIONAL — omit it to use targetVar
|
|
3604
3605
|
itself. It is only meaningful in PERCENTAGE × {ASSIGN, ADD, SUB}; specifying it in any
|
|
3605
3606
|
other cell is an error. Use it when the base differs from the target, e.g.
|
|
3606
|
-
"points +=
|
|
3607
|
+
"points += orderTotal × 5%" → { targetVar: "points", refVar: "orderTotal",
|
|
3607
3608
|
operator: "ADD", method: "PERCENTAGE", operand: 5 }.
|
|
3608
3609
|
operator × method matrix:
|
|
3609
3610
|
ASSIGN targetVar = operand | targetVar = refVar × operand/100
|
|
@@ -3617,7 +3618,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3617
3618
|
— this is the only action that does. MUTATE_FACT requires the target to already exist.
|
|
3618
3619
|
- BLOCK: { reason: string } Records a rejection decision. It does NOT halt rule execution —
|
|
3619
3620
|
subsequent actions and subsequent winning rules still run. Enforcement is the caller's
|
|
3620
|
-
responsibility; the decision surfaces as the
|
|
3621
|
+
responsibility; the decision surfaces as the isBlocked fact.
|
|
3621
3622
|
|
|
3622
3623
|
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), 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).
|
|
3623
3624
|
`,
|
|
@@ -3716,6 +3717,11 @@ function registerRuleTools(server, callApi) {
|
|
|
3716
3717
|
|
|
3717
3718
|
// src/mcp/tools/facts.ts
|
|
3718
3719
|
import { z as z4 } from "zod";
|
|
3720
|
+
|
|
3721
|
+
// src/types/facts.ts
|
|
3722
|
+
var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
3723
|
+
|
|
3724
|
+
// src/mcp/tools/facts.ts
|
|
3719
3725
|
function registerFactTools(server, callApi) {
|
|
3720
3726
|
server.registerTool(
|
|
3721
3727
|
"lexq_facts_list",
|
|
@@ -3738,9 +3744,9 @@ function registerFactTools(server, callApi) {
|
|
|
3738
3744
|
"lexq_facts_create",
|
|
3739
3745
|
{
|
|
3740
3746
|
title: "Create Fact Definition",
|
|
3741
|
-
description: "Register a new input variable. Key
|
|
3747
|
+
description: "Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER.",
|
|
3742
3748
|
inputSchema: {
|
|
3743
|
-
key: z4.string().regex(
|
|
3749
|
+
key: z4.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
3744
3750
|
name: z4.string().describe("Display name"),
|
|
3745
3751
|
type: z4.enum(["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"]).describe("Value type"),
|
|
3746
3752
|
description: z4.string().optional().describe("Description"),
|
|
@@ -3981,16 +3987,16 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3981
3987
|
|
|
3982
3988
|
Returns:
|
|
3983
3989
|
inputFacts — normalized input facts
|
|
3984
|
-
mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates
|
|
3985
|
-
generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {
|
|
3990
|
+
mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates paymentAmount)
|
|
3991
|
+
generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {factName}__delta key (signed difference)
|
|
3986
3992
|
executionTraces — per-rule match status
|
|
3987
3993
|
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
3988
3994
|
|
|
3989
|
-
Example input: { "facts": { "
|
|
3995
|
+
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
3990
3996
|
Always dry-run before publishing to validate rule behavior.`,
|
|
3991
3997
|
inputSchema: {
|
|
3992
3998
|
versionId: z6.string().uuid().describe("Policy version ID to test against"),
|
|
3993
|
-
facts: z6.string().describe('JSON string of facts object, e.g. {"
|
|
3999
|
+
facts: z6.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
|
|
3994
4000
|
includeDebugInfo: z6.boolean().default(true).describe("Include execution and decision traces")
|
|
3995
4001
|
}
|
|
3996
4002
|
},
|
|
@@ -4147,11 +4153,11 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
4147
4153
|
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
4148
4154
|
|
|
4149
4155
|
CSV example:
|
|
4150
|
-
|
|
4156
|
+
userId,paymentAmount
|
|
4151
4157
|
user_001,150000
|
|
4152
4158
|
user_002,50000
|
|
4153
4159
|
|
|
4154
|
-
JSON example: [{"
|
|
4160
|
+
JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
|
|
4155
4161
|
`,
|
|
4156
4162
|
inputSchema: {
|
|
4157
4163
|
content: z6.string().describe("CSV or JSON content as string"),
|
package/dist/mcp/register.js
CHANGED
|
@@ -378,7 +378,7 @@ function registerRuleTools(server, callApi) {
|
|
|
378
378
|
- HAS_ANY: fact has at least one of the given values
|
|
379
379
|
- HAS_ALL: fact has all of the given values
|
|
380
380
|
- HAS_NONE: fact has none of the given values
|
|
381
|
-
Example: { "type": "SINGLE", "field": "
|
|
381
|
+
Example: { "type": "SINGLE", "field": "userTags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
|
|
382
382
|
|
|
383
383
|
Do NOT use CONTAINS on a list fact — CONTAINS is substring match on STRING facts only.
|
|
384
384
|
IN is the mirror of HAS_*: IN takes a scalar fact with a list value; HAS_* takes lists on both sides.
|
|
@@ -396,7 +396,7 @@ function registerRuleTools(server, callApi) {
|
|
|
396
396
|
refVar is the base for percentage calculation and is OPTIONAL — omit it to use targetVar
|
|
397
397
|
itself. It is only meaningful in PERCENTAGE × {ASSIGN, ADD, SUB}; specifying it in any
|
|
398
398
|
other cell is an error. Use it when the base differs from the target, e.g.
|
|
399
|
-
"points +=
|
|
399
|
+
"points += orderTotal × 5%" → { targetVar: "points", refVar: "orderTotal",
|
|
400
400
|
operator: "ADD", method: "PERCENTAGE", operand: 5 }.
|
|
401
401
|
operator × method matrix:
|
|
402
402
|
ASSIGN targetVar = operand | targetVar = refVar × operand/100
|
|
@@ -410,7 +410,7 @@ function registerRuleTools(server, callApi) {
|
|
|
410
410
|
— this is the only action that does. MUTATE_FACT requires the target to already exist.
|
|
411
411
|
- BLOCK: { reason: string } Records a rejection decision. It does NOT halt rule execution —
|
|
412
412
|
subsequent actions and subsequent winning rules still run. Enforcement is the caller's
|
|
413
|
-
responsibility; the decision surfaces as the
|
|
413
|
+
responsibility; the decision surfaces as the isBlocked fact.
|
|
414
414
|
|
|
415
415
|
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), 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).
|
|
416
416
|
`,
|
|
@@ -509,6 +509,11 @@ function registerRuleTools(server, callApi) {
|
|
|
509
509
|
|
|
510
510
|
// src/mcp/tools/facts.ts
|
|
511
511
|
import { z as z4 } from "zod";
|
|
512
|
+
|
|
513
|
+
// src/types/facts.ts
|
|
514
|
+
var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
515
|
+
|
|
516
|
+
// src/mcp/tools/facts.ts
|
|
512
517
|
function registerFactTools(server, callApi) {
|
|
513
518
|
server.registerTool(
|
|
514
519
|
"lexq_facts_list",
|
|
@@ -531,9 +536,9 @@ function registerFactTools(server, callApi) {
|
|
|
531
536
|
"lexq_facts_create",
|
|
532
537
|
{
|
|
533
538
|
title: "Create Fact Definition",
|
|
534
|
-
description: "Register a new input variable. Key
|
|
539
|
+
description: "Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER.",
|
|
535
540
|
inputSchema: {
|
|
536
|
-
key: z4.string().regex(
|
|
541
|
+
key: z4.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
537
542
|
name: z4.string().describe("Display name"),
|
|
538
543
|
type: z4.enum(["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"]).describe("Value type"),
|
|
539
544
|
description: z4.string().optional().describe("Description"),
|
|
@@ -774,16 +779,16 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
774
779
|
|
|
775
780
|
Returns:
|
|
776
781
|
inputFacts — normalized input facts
|
|
777
|
-
mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates
|
|
778
|
-
generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {
|
|
782
|
+
mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates paymentAmount)
|
|
783
|
+
generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {factName}__delta key (signed difference)
|
|
779
784
|
executionTraces — per-rule match status
|
|
780
785
|
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
781
786
|
|
|
782
|
-
Example input: { "facts": { "
|
|
787
|
+
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
783
788
|
Always dry-run before publishing to validate rule behavior.`,
|
|
784
789
|
inputSchema: {
|
|
785
790
|
versionId: z6.string().uuid().describe("Policy version ID to test against"),
|
|
786
|
-
facts: z6.string().describe('JSON string of facts object, e.g. {"
|
|
791
|
+
facts: z6.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
|
|
787
792
|
includeDebugInfo: z6.boolean().default(true).describe("Include execution and decision traces")
|
|
788
793
|
}
|
|
789
794
|
},
|
|
@@ -940,11 +945,11 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
940
945
|
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
941
946
|
|
|
942
947
|
CSV example:
|
|
943
|
-
|
|
948
|
+
userId,paymentAmount
|
|
944
949
|
user_001,150000
|
|
945
950
|
user_002,50000
|
|
946
951
|
|
|
947
|
-
JSON example: [{"
|
|
952
|
+
JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
|
|
948
953
|
`,
|
|
949
954
|
inputSchema: {
|
|
950
955
|
content: z6.string().describe("CSV or JSON content as string"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexq/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.51",
|
|
4
4
|
"description": "LexQ CLI — manage policies, simulate rules, and deploy from the terminal. Built for humans and AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"constants:check": "node scripts/check-constants.mjs",
|
|
33
33
|
"start": "node dist/index.js",
|
|
34
34
|
"test:decimals": "pnpm build && node tests/exact-decimals.mjs",
|
|
35
|
+
"test:fact-key": "node tests/fact-key.mjs",
|
|
35
36
|
"prepublishOnly": "pnpm build",
|
|
36
37
|
"knip": "knip",
|
|
37
38
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
@@ -191,7 +191,7 @@ Returns the decision, per-rule reason codes (`FINAL_WINNER`, `CONDITION_MISMATCH
|
|
|
191
191
|
`GROUP_LIMIT_REACHED`, …), and the sealed version snapshot hash.
|
|
192
192
|
|
|
193
193
|
A rule that lost conflict resolution has status `BLOCKED` — unrelated to the `BLOCK` action, which writes the
|
|
194
|
-
`
|
|
194
|
+
`isBlocked` fact while its own rule stays `SELECTED`.
|
|
195
195
|
|
|
196
196
|
### PII Reveal Audit
|
|
197
197
|
|
|
@@ -19,15 +19,15 @@ lexq versions create --group-id <gid> --json '{"commitMessage": "Initial tiered
|
|
|
19
19
|
# → Save the version ID
|
|
20
20
|
|
|
21
21
|
# 3. Register facts (skip if already exist)
|
|
22
|
-
lexq facts create --key
|
|
23
|
-
lexq facts create --key
|
|
22
|
+
lexq facts create --key paymentAmount --name "Payment Amount" --type NUMBER --required
|
|
23
|
+
lexq facts create --key customerTier --name "Customer Tier" --type STRING
|
|
24
24
|
|
|
25
25
|
# 4. Add rules — creation order becomes priority order (first created = priority 1 = highest)
|
|
26
26
|
lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
27
27
|
"name": "Premium Tier - 20%",
|
|
28
28
|
"condition": {
|
|
29
29
|
"type": "SINGLE",
|
|
30
|
-
"field": "
|
|
30
|
+
"field": "paymentAmount",
|
|
31
31
|
"operator": "GREATER_THAN_OR_EQUAL",
|
|
32
32
|
"value": 500000,
|
|
33
33
|
"valueType": "NUMBER"
|
|
@@ -35,7 +35,7 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
35
35
|
"actions": [{
|
|
36
36
|
"type": "MUTATE_FACT",
|
|
37
37
|
"parameters": {
|
|
38
|
-
"targetVar": "
|
|
38
|
+
"targetVar": "paymentAmount",
|
|
39
39
|
"method": "PERCENTAGE",
|
|
40
40
|
"operator": "SUB",
|
|
41
41
|
"operand": 20,
|
|
@@ -50,14 +50,14 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
50
50
|
"type": "GROUP",
|
|
51
51
|
"operator": "AND",
|
|
52
52
|
"children": [
|
|
53
|
-
{ "type": "SINGLE", "field": "
|
|
54
|
-
{ "type": "SINGLE", "field": "
|
|
53
|
+
{ "type": "SINGLE", "field": "paymentAmount", "operator": "GREATER_THAN_OR_EQUAL", "value": 100000, "valueType": "NUMBER" },
|
|
54
|
+
{ "type": "SINGLE", "field": "paymentAmount", "operator": "LESS_THAN", "value": 500000, "valueType": "NUMBER" }
|
|
55
55
|
]
|
|
56
56
|
},
|
|
57
57
|
"actions": [{
|
|
58
58
|
"type": "MUTATE_FACT",
|
|
59
59
|
"parameters": {
|
|
60
|
-
"targetVar": "
|
|
60
|
+
"targetVar": "paymentAmount",
|
|
61
61
|
"method": "PERCENTAGE",
|
|
62
62
|
"operator": "SUB",
|
|
63
63
|
"operand": 10,
|
|
@@ -70,7 +70,7 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
70
70
|
"name": "Base Tier - 5%",
|
|
71
71
|
"condition": {
|
|
72
72
|
"type": "SINGLE",
|
|
73
|
-
"field": "
|
|
73
|
+
"field": "paymentAmount",
|
|
74
74
|
"operator": "GREATER_THAN_OR_EQUAL",
|
|
75
75
|
"value": 30000,
|
|
76
76
|
"valueType": "NUMBER"
|
|
@@ -78,7 +78,7 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
78
78
|
"actions": [{
|
|
79
79
|
"type": "MUTATE_FACT",
|
|
80
80
|
"parameters": {
|
|
81
|
-
"targetVar": "
|
|
81
|
+
"targetVar": "paymentAmount",
|
|
82
82
|
"method": "PERCENTAGE",
|
|
83
83
|
"operator": "SUB",
|
|
84
84
|
"operand": 5,
|
|
@@ -88,11 +88,11 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
88
88
|
}'
|
|
89
89
|
|
|
90
90
|
# 5. Validate
|
|
91
|
-
lexq analytics dry-run --version-id <vid> --debug --json '{"facts":{"
|
|
92
|
-
# Expected: mutatedFacts.
|
|
91
|
+
lexq analytics dry-run --version-id <vid> --debug --json '{"facts":{"paymentAmount":600000}}'
|
|
92
|
+
# Expected: mutatedFacts.paymentAmount = 480000, generatedVariables.paymentAmount__delta = -120000
|
|
93
93
|
|
|
94
|
-
lexq analytics dry-run --version-id <vid> --debug --json '{"facts":{"
|
|
95
|
-
# Expected: mutatedFacts.
|
|
94
|
+
lexq analytics dry-run --version-id <vid> --debug --json '{"facts":{"paymentAmount":200000}}'
|
|
95
|
+
# Expected: mutatedFacts.paymentAmount = 180000, generatedVariables.paymentAmount__delta = -20000
|
|
96
96
|
|
|
97
97
|
# 6. Deploy
|
|
98
98
|
lexq deploy publish --group-id <gid> --version-id <vid> --memo "Tiered discount v1"
|
|
@@ -113,9 +113,9 @@ lexq groups create --json '{
|
|
|
113
113
|
|
|
114
114
|
lexq versions create --group-id <gid> --json '{"commitMessage": "Fraud rules v1"}'
|
|
115
115
|
|
|
116
|
-
lexq facts create --key
|
|
117
|
-
lexq facts create --key
|
|
118
|
-
lexq facts create --key
|
|
116
|
+
lexq facts create --key transactionAmount --name "Transaction Amount" --type NUMBER --required
|
|
117
|
+
lexq facts create --key transactionCount24h --name "Transactions in 24h" --type NUMBER
|
|
118
|
+
lexq facts create --key countryCode --name "Country Code" --type STRING
|
|
119
119
|
|
|
120
120
|
# High-value + high-frequency
|
|
121
121
|
lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
@@ -124,8 +124,8 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
124
124
|
"type": "GROUP",
|
|
125
125
|
"operator": "AND",
|
|
126
126
|
"children": [
|
|
127
|
-
{ "type": "SINGLE", "field": "
|
|
128
|
-
{ "type": "SINGLE", "field": "
|
|
127
|
+
{ "type": "SINGLE", "field": "transactionAmount", "operator": "GREATER_THAN", "value": 5000000, "valueType": "NUMBER" },
|
|
128
|
+
{ "type": "SINGLE", "field": "transactionCount24h", "operator": "GREATER_THAN", "value": 10, "valueType": "NUMBER" }
|
|
129
129
|
]
|
|
130
130
|
},
|
|
131
131
|
"actions": [
|
|
@@ -138,7 +138,7 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
138
138
|
"name": "Sanctioned Country Block",
|
|
139
139
|
"condition": {
|
|
140
140
|
"type": "SINGLE",
|
|
141
|
-
"field": "
|
|
141
|
+
"field": "countryCode",
|
|
142
142
|
"operator": "IN",
|
|
143
143
|
"value": ["XX", "YY", "ZZ"],
|
|
144
144
|
"valueType": "LIST_STRING"
|
|
@@ -150,7 +150,7 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
150
150
|
|
|
151
151
|
# Validate
|
|
152
152
|
lexq analytics dry-run --version-id <vid> --debug --json '{
|
|
153
|
-
"facts": { "
|
|
153
|
+
"facts": { "transactionAmount": 10000000, "transactionCount24h": 15, "countryCode": "KR" }
|
|
154
154
|
}'
|
|
155
155
|
```
|
|
156
156
|
|
|
@@ -172,7 +172,7 @@ lexq rules update --group-id <gid> --version-id <v2id> --id <ruleId> --json '{
|
|
|
172
172
|
"actions": [{
|
|
173
173
|
"type": "MUTATE_FACT",
|
|
174
174
|
"parameters": {
|
|
175
|
-
"targetVar": "
|
|
175
|
+
"targetVar": "paymentAmount",
|
|
176
176
|
"method": "PERCENTAGE",
|
|
177
177
|
"operator": "SUB",
|
|
178
178
|
"operand": 15,
|
|
@@ -183,7 +183,7 @@ lexq rules update --group-id <gid> --version-id <v2id> --id <ruleId> --json '{
|
|
|
183
183
|
|
|
184
184
|
# 3. Validate with dry-run
|
|
185
185
|
lexq analytics dry-run --version-id <v2id> --debug --json '{
|
|
186
|
-
"facts": { "
|
|
186
|
+
"facts": { "paymentAmount": 100000, "customerTier": "VIP" }
|
|
187
187
|
}'
|
|
188
188
|
|
|
189
189
|
# 4. Publish v2
|
|
@@ -210,7 +210,7 @@ lexq groups ab-test stop --group-id <gid> --force
|
|
|
210
210
|
|
|
211
211
|
**Goal:** Award loyalty points based on purchase behavior.
|
|
212
212
|
|
|
213
|
-
**Note:** `
|
|
213
|
+
**Note:** `totalPoints` must be present in the request facts. The engine is stateless — it does
|
|
214
214
|
not read your database. Send the customer's current balance (or `0` for a new customer) and apply
|
|
215
215
|
the returned value yourself. `MUTATE_FACT` throws if the target fact is absent.
|
|
216
216
|
|
|
@@ -222,16 +222,16 @@ lexq groups create --json '{
|
|
|
222
222
|
|
|
223
223
|
lexq versions create --group-id <gid> --json '{"commitMessage": "Points program v1"}'
|
|
224
224
|
|
|
225
|
-
lexq facts create --key
|
|
226
|
-
lexq facts create --key
|
|
227
|
-
lexq facts create --key
|
|
225
|
+
lexq facts create --key purchaseAmount --name "Purchase Amount" --type NUMBER --required
|
|
226
|
+
lexq facts create --key isFirstPurchase --name "First Purchase" --type BOOLEAN
|
|
227
|
+
lexq facts create --key totalPoints --name "Total Points" --type NUMBER --required
|
|
228
228
|
|
|
229
229
|
# Bonus points for first purchase (fixed 200)
|
|
230
230
|
lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
231
231
|
"name": "First Purchase Bonus Points",
|
|
232
232
|
"condition": {
|
|
233
233
|
"type": "SINGLE",
|
|
234
|
-
"field": "
|
|
234
|
+
"field": "isFirstPurchase",
|
|
235
235
|
"operator": "EQUALS",
|
|
236
236
|
"value": true,
|
|
237
237
|
"valueType": "BOOLEAN"
|
|
@@ -240,7 +240,7 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
240
240
|
{
|
|
241
241
|
"type": "MUTATE_FACT",
|
|
242
242
|
"parameters": {
|
|
243
|
-
"targetVar": "
|
|
243
|
+
"targetVar": "totalPoints",
|
|
244
244
|
"operator": "ADD",
|
|
245
245
|
"method": "AMOUNT",
|
|
246
246
|
"operand": 200
|
|
@@ -249,12 +249,12 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
249
249
|
]
|
|
250
250
|
}'
|
|
251
251
|
|
|
252
|
-
# Standard points: 0.1% of
|
|
252
|
+
# Standard points: 0.1% of purchaseAmount = 1 point per 1000 KRW
|
|
253
253
|
lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
254
254
|
"name": "Standard Purchase Points",
|
|
255
255
|
"condition": {
|
|
256
256
|
"type": "SINGLE",
|
|
257
|
-
"field": "
|
|
257
|
+
"field": "purchaseAmount",
|
|
258
258
|
"operator": "GREATER_THAN_OR_EQUAL",
|
|
259
259
|
"value": 1000,
|
|
260
260
|
"valueType": "NUMBER"
|
|
@@ -263,8 +263,8 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
263
263
|
{
|
|
264
264
|
"type": "MUTATE_FACT",
|
|
265
265
|
"parameters": {
|
|
266
|
-
"targetVar": "
|
|
267
|
-
"refVar": "
|
|
266
|
+
"targetVar": "totalPoints",
|
|
267
|
+
"refVar": "purchaseAmount",
|
|
268
268
|
"operator": "ADD",
|
|
269
269
|
"method": "PERCENTAGE",
|
|
270
270
|
"operand": 0.1,
|
|
@@ -276,14 +276,14 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
276
276
|
|
|
277
277
|
# Verify — new customer, 50,000 KRW purchase → 200 bonus + 50 standard = 250
|
|
278
278
|
lexq analytics dry-run --version-id <vid> --debug --json '{
|
|
279
|
-
"facts": { "
|
|
279
|
+
"facts": { "purchaseAmount": 50000, "isFirstPurchase": true, "totalPoints": 0 }
|
|
280
280
|
}'
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
-
`
|
|
284
|
-
`refVar` exists for. Omitting it would compute `
|
|
283
|
+
`totalPoints` and `refVar: purchaseAmount` are two different facts — this is exactly what
|
|
284
|
+
`refVar` exists for. Omitting it would compute `totalPoints += totalPoints × 0.1%`.
|
|
285
285
|
|
|
286
|
-
Read `
|
|
286
|
+
Read `totalPoints` and `totalPoints__delta` from `generatedVariables` in the response.
|
|
287
287
|
|
|
288
288
|
---
|
|
289
289
|
|
|
@@ -298,12 +298,12 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
298
298
|
"mutexMode": "EXCLUSIVE",
|
|
299
299
|
"mutexStrategy": "HIGHEST_PRIORITY",
|
|
300
300
|
"condition": {
|
|
301
|
-
"type": "SINGLE", "field": "
|
|
301
|
+
"type": "SINGLE", "field": "customerTier", "operator": "EQUALS", "value": "VIP", "valueType": "STRING"
|
|
302
302
|
},
|
|
303
303
|
"actions": [{
|
|
304
304
|
"type": "MUTATE_FACT",
|
|
305
305
|
"parameters": {
|
|
306
|
-
"targetVar": "
|
|
306
|
+
"targetVar": "paymentAmount",
|
|
307
307
|
"method": "PERCENTAGE",
|
|
308
308
|
"operator": "SUB",
|
|
309
309
|
"operand": 20,
|
|
@@ -318,12 +318,12 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
318
318
|
"mutexMode": "EXCLUSIVE",
|
|
319
319
|
"mutexStrategy": "HIGHEST_PRIORITY",
|
|
320
320
|
"condition": {
|
|
321
|
-
"type": "SINGLE", "field": "
|
|
321
|
+
"type": "SINGLE", "field": "paymentAmount", "operator": "GREATER_THAN_OR_EQUAL", "value": 50000, "valueType": "NUMBER"
|
|
322
322
|
},
|
|
323
323
|
"actions": [{
|
|
324
324
|
"type": "MUTATE_FACT",
|
|
325
325
|
"parameters": {
|
|
326
|
-
"targetVar": "
|
|
326
|
+
"targetVar": "paymentAmount",
|
|
327
327
|
"method": "PERCENTAGE",
|
|
328
328
|
"operator": "SUB",
|
|
329
329
|
"operand": 15,
|
|
@@ -435,20 +435,20 @@ boolean or string flags are simpler and easier to query.
|
|
|
435
435
|
lexq groups create --json '{"name": "segmentation", "activationMode": "NONE"}'
|
|
436
436
|
lexq versions create --group-id <gid> --json '{"commitMessage": "Segments v1"}'
|
|
437
437
|
|
|
438
|
-
lexq facts create --key
|
|
439
|
-
lexq facts create --key
|
|
440
|
-
lexq facts create --key
|
|
441
|
-
lexq facts create --key
|
|
438
|
+
lexq facts create --key lifetimeValue --name "Lifetime Value" --type NUMBER --required
|
|
439
|
+
lexq facts create --key signupDays --name "Days Since Signup" --type NUMBER --required
|
|
440
|
+
lexq facts create --key supportTier --name "Support Tier" --type STRING
|
|
441
|
+
lexq facts create --key betaEnabled --name "Beta Access" --type BOOLEAN
|
|
442
442
|
|
|
443
443
|
# High-value customer → priority support
|
|
444
444
|
lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
445
445
|
"name": "High Value Segment",
|
|
446
446
|
"condition": {
|
|
447
|
-
"type": "SINGLE", "field": "
|
|
447
|
+
"type": "SINGLE", "field": "lifetimeValue",
|
|
448
448
|
"operator": "GREATER_THAN_OR_EQUAL", "value": 1000000, "valueType": "NUMBER"
|
|
449
449
|
},
|
|
450
450
|
"actions": [
|
|
451
|
-
{ "type": "SET_FACT", "parameters": { "targetVar": "
|
|
451
|
+
{ "type": "SET_FACT", "parameters": { "targetVar": "supportTier", "value": "PRIORITY" } }
|
|
452
452
|
]
|
|
453
453
|
}'
|
|
454
454
|
|
|
@@ -456,16 +456,16 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
456
456
|
lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
457
457
|
"name": "Veteran Beta Access",
|
|
458
458
|
"condition": {
|
|
459
|
-
"type": "SINGLE", "field": "
|
|
459
|
+
"type": "SINGLE", "field": "signupDays",
|
|
460
460
|
"operator": "GREATER_THAN", "value": 365, "valueType": "NUMBER"
|
|
461
461
|
},
|
|
462
462
|
"actions": [
|
|
463
|
-
{ "type": "SET_FACT", "parameters": { "targetVar": "
|
|
463
|
+
{ "type": "SET_FACT", "parameters": { "targetVar": "betaEnabled", "value": true } }
|
|
464
464
|
]
|
|
465
465
|
}'
|
|
466
466
|
|
|
467
467
|
lexq analytics dry-run --version-id <vid> --debug --json '{
|
|
468
|
-
"facts": { "
|
|
468
|
+
"facts": { "lifetimeValue": 1500000, "signupDays": 400 }
|
|
469
469
|
}'
|
|
470
470
|
```
|
|
471
471
|
|
|
@@ -39,7 +39,7 @@ Conditions use a tree structure with two node types: `SINGLE` and `GROUP`.
|
|
|
39
39
|
```json
|
|
40
40
|
{
|
|
41
41
|
"type": "SINGLE",
|
|
42
|
-
"field": "
|
|
42
|
+
"field": "paymentAmount",
|
|
43
43
|
"operator": "GREATER_THAN_OR_EQUAL",
|
|
44
44
|
"value": 100000,
|
|
45
45
|
"valueType": "NUMBER"
|
|
@@ -55,14 +55,14 @@ Conditions use a tree structure with two node types: `SINGLE` and `GROUP`.
|
|
|
55
55
|
"children": [
|
|
56
56
|
{
|
|
57
57
|
"type": "SINGLE",
|
|
58
|
-
"field": "
|
|
58
|
+
"field": "customerTier",
|
|
59
59
|
"operator": "EQUALS",
|
|
60
60
|
"value": "VIP",
|
|
61
61
|
"valueType": "STRING"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"type": "SINGLE",
|
|
65
|
-
"field": "
|
|
65
|
+
"field": "paymentAmount",
|
|
66
66
|
"operator": "GREATER_THAN",
|
|
67
67
|
"value": 50000,
|
|
68
68
|
"valueType": "NUMBER"
|
|
@@ -109,7 +109,7 @@ by the server — check `lexq facts list` before choosing.
|
|
|
109
109
|
|
|
110
110
|
// list fact, list value
|
|
111
111
|
{
|
|
112
|
-
"field": "
|
|
112
|
+
"field": "userTags",
|
|
113
113
|
"operator": "HAS_ANY",
|
|
114
114
|
"value": [
|
|
115
115
|
"VIP",
|
|
@@ -133,7 +133,7 @@ Do **not** use `CONTAINS` on a list fact — that idiom works in some rule engin
|
|
|
133
133
|
|
|
134
134
|
### Nested Conditions Example
|
|
135
135
|
|
|
136
|
-
`(
|
|
136
|
+
`(customerTier = "VIP" AND paymentAmount >= 100000) OR region IN ["KR", "JP"]`:
|
|
137
137
|
|
|
138
138
|
```json
|
|
139
139
|
{
|
|
@@ -146,14 +146,14 @@ Do **not** use `CONTAINS` on a list fact — that idiom works in some rule engin
|
|
|
146
146
|
"children": [
|
|
147
147
|
{
|
|
148
148
|
"type": "SINGLE",
|
|
149
|
-
"field": "
|
|
149
|
+
"field": "customerTier",
|
|
150
150
|
"operator": "EQUALS",
|
|
151
151
|
"value": "VIP",
|
|
152
152
|
"valueType": "STRING"
|
|
153
153
|
},
|
|
154
154
|
{
|
|
155
155
|
"type": "SINGLE",
|
|
156
|
-
"field": "
|
|
156
|
+
"field": "paymentAmount",
|
|
157
157
|
"operator": "GREATER_THAN_OR_EQUAL",
|
|
158
158
|
"value": 100000,
|
|
159
159
|
"valueType": "NUMBER"
|
|
@@ -218,8 +218,8 @@ Use `refVar` when the base differs from the target:
|
|
|
218
218
|
{
|
|
219
219
|
"type": "MUTATE_FACT",
|
|
220
220
|
"parameters": {
|
|
221
|
-
"targetVar": "
|
|
222
|
-
"refVar": "
|
|
221
|
+
"targetVar": "loyaltyPoint",
|
|
222
|
+
"refVar": "orderTotal",
|
|
223
223
|
"operator": "ADD",
|
|
224
224
|
"method": "PERCENTAGE",
|
|
225
225
|
"operand": 5
|
|
@@ -227,8 +227,8 @@ Use `refVar` when the base differs from the target:
|
|
|
227
227
|
}
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
-
`
|
|
231
|
-
`
|
|
230
|
+
`loyaltyPoint += orderTotal × 5%` — two different facts. Omitting `refVar` would compute
|
|
231
|
+
`loyaltyPoint += loyaltyPoint × 5%` instead.
|
|
232
232
|
|
|
233
233
|
**Ranges are not constrained.** Negative operands and percentages above 100 are valid — refunds
|
|
234
234
|
(`-5`), surcharges (`150`), risk scores, and game points all need them.
|
|
@@ -240,20 +240,20 @@ present as a number and throws otherwise. "Make something that wasn't there" is
|
|
|
240
240
|
|
|
241
241
|
### `BLOCK` does not halt execution
|
|
242
242
|
|
|
243
|
-
`BLOCK` records a rejection decision by writing the `
|
|
243
|
+
`BLOCK` records a rejection decision by writing the `isBlocked` fact. Subsequent actions in the
|
|
244
244
|
same rule and subsequent winning rules still run. Enforcement is the caller's responsibility —
|
|
245
|
-
read `
|
|
245
|
+
read `isBlocked` from the response.
|
|
246
246
|
|
|
247
247
|
### Action Example: 10% Discount via MUTATE_FACT
|
|
248
248
|
|
|
249
|
-
Reduces `
|
|
250
|
-
`
|
|
249
|
+
Reduces `paymentAmount` by 10%. `__delta` is auto-generated in `generatedVariables` (e.g.,
|
|
250
|
+
`paymentAmount__delta: -10000` for a 100,000 input).
|
|
251
251
|
|
|
252
252
|
```json
|
|
253
253
|
{
|
|
254
254
|
"type": "MUTATE_FACT",
|
|
255
255
|
"parameters": {
|
|
256
|
-
"targetVar": "
|
|
256
|
+
"targetVar": "paymentAmount",
|
|
257
257
|
"method": "PERCENTAGE",
|
|
258
258
|
"operator": "SUB",
|
|
259
259
|
"operand": 10,
|
|
@@ -301,15 +301,15 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
|
|
|
301
301
|
"type": "GROUP",
|
|
302
302
|
"operator": "AND",
|
|
303
303
|
"children": [
|
|
304
|
-
{ "type": "SINGLE", "field": "
|
|
305
|
-
{ "type": "SINGLE", "field": "
|
|
304
|
+
{ "type": "SINGLE", "field": "customerTier", "operator": "EQUALS", "value": "VIP", "valueType": "STRING" },
|
|
305
|
+
{ "type": "SINGLE", "field": "paymentAmount", "operator": "GREATER_THAN_OR_EQUAL", "value": 100000, "valueType": "NUMBER" }
|
|
306
306
|
]
|
|
307
307
|
},
|
|
308
308
|
"actions": [
|
|
309
309
|
{
|
|
310
310
|
"type": "MUTATE_FACT",
|
|
311
311
|
"parameters": {
|
|
312
|
-
"targetVar": "
|
|
312
|
+
"targetVar": "paymentAmount",
|
|
313
313
|
"method": "PERCENTAGE",
|
|
314
314
|
"operator": "SUB",
|
|
315
315
|
"operand": 10,
|
|
@@ -330,7 +330,7 @@ lexq rules update --group-id <gid> --version-id <vid> --id <ruleId> --json '{
|
|
|
330
330
|
{
|
|
331
331
|
"type": "MUTATE_FACT",
|
|
332
332
|
"parameters": {
|
|
333
|
-
"targetVar": "
|
|
333
|
+
"targetVar": "paymentAmount",
|
|
334
334
|
"method": "PERCENTAGE",
|
|
335
335
|
"operator": "SUB",
|
|
336
336
|
"operand": 15,
|
|
@@ -415,6 +415,6 @@ Before creating rules, always:
|
|
|
415
415
|
|
|
416
416
|
1. **Check available facts:** `lexq facts list`
|
|
417
417
|
2. **Confirm the version is DRAFT:** `lexq versions get --group-id <gid> --id <vid>` → status must be `DRAFT`
|
|
418
|
-
3. **Use exact fact keys** from the fact definitions (
|
|
418
|
+
3. **Use exact fact keys** from the fact definitions (case-sensitive)
|
|
419
419
|
4. **Match value types** — a fact defined as `NUMBER` must receive numeric values, not strings
|
|
420
420
|
5. **Match the operator to the fact type** — list-typed facts accept only `HAS_ANY` / `HAS_ALL` / `HAS_NONE`
|
|
@@ -149,7 +149,8 @@ Prefixes you will encounter through the CLI:
|
|
|
149
149
|
|
|
150
150
|
## Important Conventions
|
|
151
151
|
|
|
152
|
-
1. **Fact keys
|
|
152
|
+
1. **Fact keys start with a letter,** then letters, numbers, and underscores. Casing is yours to
|
|
153
|
+
choose and keys are case-sensitive. These docs use `camelCase`: `paymentAmount`, `customerTier`.
|
|
153
154
|
2. **IDs are UUIDs.** Always copy the full ID from list/create output — do not guess.
|
|
154
155
|
3. **Dates use ISO 8601.** Example: `2025-01-01T00:00:00Z`. Time zone is UTC.
|
|
155
156
|
4. **JSON bodies via `--json`.** Most create/update commands accept `--json '<body>'` for the request body.
|
|
@@ -31,7 +31,7 @@ Response includes:
|
|
|
31
31
|
"versionNo": 3,
|
|
32
32
|
"requiredFacts": [
|
|
33
33
|
{
|
|
34
|
-
"key": "
|
|
34
|
+
"key": "paymentAmount",
|
|
35
35
|
"type": "NUMBER",
|
|
36
36
|
"required": true,
|
|
37
37
|
"usedBy": [
|
|
@@ -40,7 +40,7 @@ Response includes:
|
|
|
40
40
|
]
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
|
-
"key": "
|
|
43
|
+
"key": "customerTier",
|
|
44
44
|
"type": "STRING",
|
|
45
45
|
"required": true,
|
|
46
46
|
"usedBy": [
|
|
@@ -50,8 +50,8 @@ Response includes:
|
|
|
50
50
|
],
|
|
51
51
|
"exampleRequest": {
|
|
52
52
|
"facts": {
|
|
53
|
-
"
|
|
54
|
-
"
|
|
53
|
+
"paymentAmount": 0,
|
|
54
|
+
"customerTier": ""
|
|
55
55
|
},
|
|
56
56
|
"context": {}
|
|
57
57
|
}
|
|
@@ -67,8 +67,8 @@ Test a single set of facts against a version:
|
|
|
67
67
|
```bash
|
|
68
68
|
lexq analytics dry-run --version-id <vid> --json '{
|
|
69
69
|
"facts": {
|
|
70
|
-
"
|
|
71
|
-
"
|
|
70
|
+
"paymentAmount": 150000,
|
|
71
|
+
"customerTier": "VIP"
|
|
72
72
|
}
|
|
73
73
|
}'
|
|
74
74
|
```
|
|
@@ -84,7 +84,7 @@ lexq analytics dry-run --version-id <vid> --json '{
|
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
86
|
lexq analytics dry-run --version-id <vid> --debug --json '{
|
|
87
|
-
"facts": { "
|
|
87
|
+
"facts": { "paymentAmount": 150000, "customerTier": "VIP" }
|
|
88
88
|
}'
|
|
89
89
|
```
|
|
90
90
|
|
|
@@ -98,22 +98,22 @@ external to mock.
|
|
|
98
98
|
"result": "SUCCESS",
|
|
99
99
|
"data": {
|
|
100
100
|
"inputFacts": {
|
|
101
|
-
"
|
|
102
|
-
"
|
|
101
|
+
"paymentAmount": 150000,
|
|
102
|
+
"customerTier": "VIP"
|
|
103
103
|
},
|
|
104
104
|
"mutatedFacts": {
|
|
105
|
-
"
|
|
106
|
-
"
|
|
105
|
+
"paymentAmount": 135000,
|
|
106
|
+
"customerTier": "VIP"
|
|
107
107
|
},
|
|
108
108
|
"generatedVariables": {
|
|
109
|
-
"
|
|
109
|
+
"paymentAmount__delta": -15000
|
|
110
110
|
},
|
|
111
111
|
"executionTraces": [
|
|
112
112
|
{
|
|
113
113
|
"ruleId": "...",
|
|
114
114
|
"ruleName": "VIP 10% Discount",
|
|
115
115
|
"matched": true,
|
|
116
|
-
"matchExpression": "(
|
|
116
|
+
"matchExpression": "(customerTier == 'VIP') && (paymentAmount >= 100000)",
|
|
117
117
|
"generatedActions": [
|
|
118
118
|
{
|
|
119
119
|
"type": "MUTATE_FACT",
|
|
@@ -139,8 +139,8 @@ external to mock.
|
|
|
139
139
|
}
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
- `mutatedFacts` — input facts changed by rule actions (e.g., `MUTATE_FACT` reduces `
|
|
143
|
-
- `generatedVariables` — for every fact in `mutatedFacts`, a paired `{
|
|
142
|
+
- `mutatedFacts` — input facts changed by rule actions (e.g., `MUTATE_FACT` reduces `paymentAmount`)
|
|
143
|
+
- `generatedVariables` — for every fact in `mutatedFacts`, a paired `{factName}__delta` key is auto-generated with the
|
|
144
144
|
signed difference (negative = decrease, positive = increase)
|
|
145
145
|
|
|
146
146
|
### Reading Decision Traces
|
|
@@ -154,7 +154,7 @@ Each trace carries a `status` (what happened) and a `reasonCode` (why).
|
|
|
154
154
|
| `BLOCKED` | Matched but lost conflict resolution — see `reasonCode` for which round and why |
|
|
155
155
|
| `ERROR` | Action execution failed |
|
|
156
156
|
|
|
157
|
-
`BLOCKED` is unrelated to the `BLOCK` action. A `BLOCK` action writes the `
|
|
157
|
+
`BLOCKED` is unrelated to the `BLOCK` action. A `BLOCK` action writes the `isBlocked` fact and its own rule stays
|
|
158
158
|
`SELECTED`; `BLOCKED` means the rule was dropped by activation-group or mutex competition.
|
|
159
159
|
|
|
160
160
|
### Reading Reason Codes
|
|
@@ -197,7 +197,7 @@ Compare how two versions evaluate the same input:
|
|
|
197
197
|
|
|
198
198
|
```bash
|
|
199
199
|
lexq analytics dry-run-compare --json '{
|
|
200
|
-
"facts": { "
|
|
200
|
+
"facts": { "paymentAmount": 150000, "customerTier": "VIP" },
|
|
201
201
|
"versionIdA": "<baselineVersionId>",
|
|
202
202
|
"versionIdB": "<candidateVersionId>"
|
|
203
203
|
}'
|
|
@@ -225,7 +225,7 @@ lexq analytics simulation start --json '{
|
|
|
225
225
|
"maxRecords": 10000,
|
|
226
226
|
"baselinePolicyVersionId": "<currentLiveVersionId>",
|
|
227
227
|
"metricConfig": {
|
|
228
|
-
"targetVariable": "
|
|
228
|
+
"targetVariable": "paymentAmount__delta",
|
|
229
229
|
"aggregationType": "SUM"
|
|
230
230
|
}
|
|
231
231
|
}
|
|
@@ -316,7 +316,7 @@ lexq analytics simulation export --id <simulationId> --format csv --output resul
|
|
|
316
316
|
"matchRate": 0.85
|
|
317
317
|
},
|
|
318
318
|
"metricSummary": {
|
|
319
|
-
"targetVariable": "
|
|
319
|
+
"targetVariable": "paymentAmount__delta",
|
|
320
320
|
"aggregationType": "SUM",
|
|
321
321
|
"baselineValue": 5000000,
|
|
322
322
|
"simulatedValue": 4500000,
|
|
@@ -353,7 +353,7 @@ lexq analytics requirements --group-id <gid> --version-id <vid>
|
|
|
353
353
|
|
|
354
354
|
# 2. Dry-run with representative inputs
|
|
355
355
|
lexq analytics dry-run --version-id <vid> --debug --json '{
|
|
356
|
-
"facts": { "
|
|
356
|
+
"facts": { "paymentAmount": 150000, "customerTier": "VIP" }
|
|
357
357
|
}'
|
|
358
358
|
|
|
359
359
|
# 3. If dry-run looks good, publish
|
|
@@ -366,7 +366,7 @@ lexq analytics simulation start --json '{
|
|
|
366
366
|
"options": {
|
|
367
367
|
"baselinePolicyVersionId": "<currentLiveVersionId>",
|
|
368
368
|
"includeRuleStats": true,
|
|
369
|
-
"metricConfig": { "targetVariable": "
|
|
369
|
+
"metricConfig": { "targetVariable": "paymentAmount__delta", "aggregationType": "SUM" }
|
|
370
370
|
}
|
|
371
371
|
}'
|
|
372
372
|
|