@lexq/cli 0.1.49 → 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 +73 -36
- package/dist/mcp/register.js +40 -18
- package/package.json +3 -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
|
@@ -315,6 +315,19 @@ function registerStatusCommand(program) {
|
|
|
315
315
|
});
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
// src/types/constants.ts
|
|
319
|
+
var MAX_ROUNDING_SCALE = 34;
|
|
320
|
+
var MIN_TRAFFIC_RATE = 1;
|
|
321
|
+
var MAX_TRAFFIC_RATE = 99;
|
|
322
|
+
var REPLAY_WINDOW_MAX_RECORDS = 5e4;
|
|
323
|
+
var PROFILE_SAMPLE_PERMILLE = 10;
|
|
324
|
+
var LATENCY_WINDOW_MILLIS = 6e4;
|
|
325
|
+
var TAIL_MIN_OBS = 3;
|
|
326
|
+
var SLOW_MULTIPLIER = 10;
|
|
327
|
+
var MIN_SAMPLES = 100;
|
|
328
|
+
var MIN_COHORT_SIZE = 3;
|
|
329
|
+
var PROFILE_DEFAULT_WINDOW_HOURS = 24;
|
|
330
|
+
|
|
318
331
|
// src/commands/groups.ts
|
|
319
332
|
import "commander";
|
|
320
333
|
import dedent3 from "dedent";
|
|
@@ -528,11 +541,14 @@ ${data.length} total`);
|
|
|
528
541
|
stop Stop the test and revert to 100% live version
|
|
529
542
|
adjust Change the traffic percentage
|
|
530
543
|
|
|
531
|
-
The traffic rate (
|
|
544
|
+
The traffic rate (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE}) determines what percentage goes to the challenger.
|
|
532
545
|
The remaining traffic continues to the current live version.
|
|
533
546
|
`
|
|
534
547
|
);
|
|
535
|
-
abTest.command("start").description("Start an A/B test").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Challenger version ID").requiredOption(
|
|
548
|
+
abTest.command("start").description("Start an A/B test").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Challenger version ID").requiredOption(
|
|
549
|
+
"--traffic-rate <rate>",
|
|
550
|
+
`Traffic rate for challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
|
|
551
|
+
).addHelpText(
|
|
536
552
|
"after",
|
|
537
553
|
dedent3`
|
|
538
554
|
|
|
@@ -590,7 +606,10 @@ ${data.length} total`);
|
|
|
590
606
|
process.exit(1);
|
|
591
607
|
}
|
|
592
608
|
});
|
|
593
|
-
abTest.command("adjust").description("Adjust A/B test traffic rate").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption(
|
|
609
|
+
abTest.command("adjust").description("Adjust A/B test traffic rate").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption(
|
|
610
|
+
"--traffic-rate <rate>",
|
|
611
|
+
`New traffic rate (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
|
|
612
|
+
).addHelpText(
|
|
594
613
|
"after",
|
|
595
614
|
dedent3`
|
|
596
615
|
|
|
@@ -935,13 +954,13 @@ ${data.length} total`);
|
|
|
935
954
|
"name": "VIP 20% Discount",
|
|
936
955
|
"condition": {
|
|
937
956
|
"type": "SINGLE",
|
|
938
|
-
"field": "
|
|
957
|
+
"field": "customerTier",
|
|
939
958
|
"operator": "EQUALS",
|
|
940
959
|
"value": "VIP",
|
|
941
960
|
"valueType": "STRING"
|
|
942
961
|
},
|
|
943
962
|
"actions": [
|
|
944
|
-
{ "type": "MUTATE_FACT", "parameters": { "targetVar": "
|
|
963
|
+
{ "type": "MUTATE_FACT", "parameters": { "targetVar": "paymentAmount", "operator": "SUB", "method": "PERCENTAGE", "operand": 20 } }
|
|
945
964
|
]
|
|
946
965
|
}'
|
|
947
966
|
|
|
@@ -961,7 +980,7 @@ ${data.length} total`);
|
|
|
961
980
|
HAS_NONE fact has none of the given values
|
|
962
981
|
|
|
963
982
|
Example:
|
|
964
|
-
{ "type": "SINGLE", "field": "
|
|
983
|
+
{ "type": "SINGLE", "field": "userTags", "operator": "HAS_ANY",
|
|
965
984
|
"value": ["VIP", "GOLD"], "valueType": "LIST_STRING" }
|
|
966
985
|
|
|
967
986
|
CONTAINS is substring match on STRING facts, not list membership.
|
|
@@ -1017,7 +1036,7 @@ ${data.length} total`);
|
|
|
1017
1036
|
$ lexq rules update --group-id <gid> --version-id <vid> --id <rid> --json '{
|
|
1018
1037
|
"name": "VIP 25% Discount",
|
|
1019
1038
|
"actions": [
|
|
1020
|
-
{ "type": "MUTATE_FACT", "parameters": { "targetVar": "
|
|
1039
|
+
{ "type": "MUTATE_FACT", "parameters": { "targetVar": "paymentAmount", "operator": "SUB", "method": "PERCENTAGE", "operand": 25 } }
|
|
1021
1040
|
]
|
|
1022
1041
|
}'
|
|
1023
1042
|
`
|
|
@@ -1161,7 +1180,7 @@ function registerFactCommands(program) {
|
|
|
1161
1180
|
delete Remove a fact definition
|
|
1162
1181
|
action-metadata Show action runtime fact metadata
|
|
1163
1182
|
|
|
1164
|
-
System facts (
|
|
1183
|
+
System facts (userId, userTags) are auto-created.
|
|
1165
1184
|
`
|
|
1166
1185
|
);
|
|
1167
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) => {
|
|
@@ -1247,16 +1266,16 @@ ${data.length} unregistered`);
|
|
|
1247
1266
|
process.exit(1);
|
|
1248
1267
|
}
|
|
1249
1268
|
});
|
|
1250
|
-
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(
|
|
1251
1270
|
"after",
|
|
1252
1271
|
dedent6`
|
|
1253
1272
|
|
|
1254
1273
|
Examples:
|
|
1255
|
-
$ lexq facts create --key
|
|
1256
|
-
$ 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
|
|
1257
1276
|
|
|
1258
1277
|
$ lexq facts create --json '{
|
|
1259
|
-
"key": "
|
|
1278
|
+
"key": "userRegion",
|
|
1260
1279
|
"name": "User Region",
|
|
1261
1280
|
"type": "STRING",
|
|
1262
1281
|
"description": "ISO country code",
|
|
@@ -1264,7 +1283,8 @@ ${data.length} unregistered`);
|
|
|
1264
1283
|
}'
|
|
1265
1284
|
|
|
1266
1285
|
Value Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
|
|
1267
|
-
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)
|
|
1268
1288
|
`
|
|
1269
1289
|
).action(async (opts) => {
|
|
1270
1290
|
try {
|
|
@@ -1849,7 +1869,7 @@ function registerAnalyticsCommands(program) {
|
|
|
1849
1869
|
|
|
1850
1870
|
Examples:
|
|
1851
1871
|
$ lexq analytics dry-run --version-id <vid> --debug \\
|
|
1852
|
-
--json '{"facts": {"
|
|
1872
|
+
--json '{"facts": {"paymentAmount": 150000, "customerTier": "VIP"}}'
|
|
1853
1873
|
|
|
1854
1874
|
$ lexq analytics dry-run --version-id <vid> --file test-input.json
|
|
1855
1875
|
|
|
@@ -1888,7 +1908,7 @@ function registerAnalyticsCommands(program) {
|
|
|
1888
1908
|
$ lexq analytics dry-run-compare --json '{
|
|
1889
1909
|
"versionIdA": "<version-a-id>",
|
|
1890
1910
|
"versionIdB": "<version-b-id>",
|
|
1891
|
-
"facts": {"
|
|
1911
|
+
"facts": {"paymentAmount": 100000, "customerTier": "VIP"}
|
|
1892
1912
|
}'
|
|
1893
1913
|
|
|
1894
1914
|
Shows side-by-side which rules matched and what actions fired for each version.
|
|
@@ -2359,11 +2379,17 @@ var WebhookPayloadFormat = ["GENERIC", "SLACK"];
|
|
|
2359
2379
|
var ms = (nanos) => nanos == null ? "\u2013" : (nanos / 1e6).toFixed(2);
|
|
2360
2380
|
var msWithUnit = (nanos) => nanos == null ? "\u2013" : `${(nanos / 1e6).toFixed(2)}ms`;
|
|
2361
2381
|
function registerProfileCommands(program) {
|
|
2362
|
-
program.command("profile <groupId>").description("Per-rule latency profile with relative slow-rule flags").option(
|
|
2382
|
+
program.command("profile <groupId>").description("Per-rule latency profile with relative slow-rule flags").option(
|
|
2383
|
+
"--rule <ruleId>",
|
|
2384
|
+
`Single-rule detail (distributions + ${LATENCY_WINDOW_MILLIS / 1e3}s window series)`
|
|
2385
|
+
).option("--version <versionId>", "Version to inspect (default: live version)").option(
|
|
2386
|
+
"--from <instant>",
|
|
2387
|
+
`Window start, ISO-8601 instant (default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago)`
|
|
2388
|
+
).option("--to <instant>", "Window end, ISO-8601 instant (default: now)").option("--cache <state>", "Cache dimension for the rule table: HIT | MISS (default: HIT)").addHelpText(
|
|
2363
2389
|
"after",
|
|
2364
2390
|
dedent9`
|
|
2365
2391
|
|
|
2366
|
-
Slow-rule judgment is relative only: flagged = p50 ≥
|
|
2392
|
+
Slow-rule judgment is relative only: flagged = p50 ≥ ${SLOW_MULTIPLIER}× the median of
|
|
2367
2393
|
per-rule p50s within the group. Absolute ms thresholds are intentionally
|
|
2368
2394
|
not supported. Each percentile is withheld (–) unless n×(1−q) ≥ 3 —
|
|
2369
2395
|
p50 from n ≥ 6, p95 from n ≥ 60, p99 from n ≥ 300. TOTAL is recorded
|
|
@@ -2643,7 +2669,7 @@ function registerReplayCommands(program) {
|
|
|
2643
2669
|
process.exit(1);
|
|
2644
2670
|
}
|
|
2645
2671
|
});
|
|
2646
|
-
replay.command("start").description("Submit a window replay job (blast radius)").requiredOption("--version-id <versionId>", "Candidate version to re-evaluate against").requiredOption("--from <date>", "Window start date (yyyy-MM-dd)").requiredOption("--to <date>", "Window end date (yyyy-MM-dd)").option("--max-records <number>",
|
|
2672
|
+
replay.command("start").description("Submit a window replay job (blast radius)").requiredOption("--version-id <versionId>", "Candidate version to re-evaluate against").requiredOption("--from <date>", "Window start date (yyyy-MM-dd)").requiredOption("--to <date>", "Window end date (yyyy-MM-dd)").option("--max-records <number>", `Sample cap (hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`).addHelpText(
|
|
2647
2673
|
"after",
|
|
2648
2674
|
dedent11`
|
|
2649
2675
|
|
|
@@ -3560,7 +3586,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3560
3586
|
- HAS_ANY: fact has at least one of the given values
|
|
3561
3587
|
- HAS_ALL: fact has all of the given values
|
|
3562
3588
|
- HAS_NONE: fact has none of the given values
|
|
3563
|
-
Example: { "type": "SINGLE", "field": "
|
|
3589
|
+
Example: { "type": "SINGLE", "field": "userTags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
|
|
3564
3590
|
|
|
3565
3591
|
Do NOT use CONTAINS on a list fact — CONTAINS is substring match on STRING facts only.
|
|
3566
3592
|
IN is the mirror of HAS_*: IN takes a scalar fact with a list value; HAS_* takes lists on both sides.
|
|
@@ -3578,7 +3604,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3578
3604
|
refVar is the base for percentage calculation and is OPTIONAL — omit it to use targetVar
|
|
3579
3605
|
itself. It is only meaningful in PERCENTAGE × {ASSIGN, ADD, SUB}; specifying it in any
|
|
3580
3606
|
other cell is an error. Use it when the base differs from the target, e.g.
|
|
3581
|
-
"points +=
|
|
3607
|
+
"points += orderTotal × 5%" → { targetVar: "points", refVar: "orderTotal",
|
|
3582
3608
|
operator: "ADD", method: "PERCENTAGE", operand: 5 }.
|
|
3583
3609
|
operator × method matrix:
|
|
3584
3610
|
ASSIGN targetVar = operand | targetVar = refVar × operand/100
|
|
@@ -3592,9 +3618,9 @@ function registerRuleTools(server, callApi) {
|
|
|
3592
3618
|
— this is the only action that does. MUTATE_FACT requires the target to already exist.
|
|
3593
3619
|
- BLOCK: { reason: string } Records a rejection decision. It does NOT halt rule execution —
|
|
3594
3620
|
subsequent actions and subsequent winning rules still run. Enforcement is the caller's
|
|
3595
|
-
responsibility; the decision surfaces as the
|
|
3621
|
+
responsibility; the decision surfaces as the isBlocked fact.
|
|
3596
3622
|
|
|
3597
|
-
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0
|
|
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).
|
|
3598
3624
|
`,
|
|
3599
3625
|
inputSchema: {
|
|
3600
3626
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
@@ -3691,6 +3717,11 @@ function registerRuleTools(server, callApi) {
|
|
|
3691
3717
|
|
|
3692
3718
|
// src/mcp/tools/facts.ts
|
|
3693
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
|
|
3694
3725
|
function registerFactTools(server, callApi) {
|
|
3695
3726
|
server.registerTool(
|
|
3696
3727
|
"lexq_facts_list",
|
|
@@ -3713,9 +3744,9 @@ function registerFactTools(server, callApi) {
|
|
|
3713
3744
|
"lexq_facts_create",
|
|
3714
3745
|
{
|
|
3715
3746
|
title: "Create Fact Definition",
|
|
3716
|
-
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.",
|
|
3717
3748
|
inputSchema: {
|
|
3718
|
-
key: z4.string().regex(
|
|
3749
|
+
key: z4.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
3719
3750
|
name: z4.string().describe("Display name"),
|
|
3720
3751
|
type: z4.enum(["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"]).describe("Value type"),
|
|
3721
3752
|
description: z4.string().optional().describe("Description"),
|
|
@@ -3956,16 +3987,16 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3956
3987
|
|
|
3957
3988
|
Returns:
|
|
3958
3989
|
inputFacts — normalized input facts
|
|
3959
|
-
mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates
|
|
3960
|
-
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)
|
|
3961
3992
|
executionTraces — per-rule match status
|
|
3962
3993
|
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
3963
3994
|
|
|
3964
|
-
Example input: { "facts": { "
|
|
3995
|
+
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
3965
3996
|
Always dry-run before publishing to validate rule behavior.`,
|
|
3966
3997
|
inputSchema: {
|
|
3967
3998
|
versionId: z6.string().uuid().describe("Policy version ID to test against"),
|
|
3968
|
-
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}'),
|
|
3969
4000
|
includeDebugInfo: z6.boolean().default(true).describe("Include execution and decision traces")
|
|
3970
4001
|
}
|
|
3971
4002
|
},
|
|
@@ -4122,11 +4153,11 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
4122
4153
|
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
4123
4154
|
|
|
4124
4155
|
CSV example:
|
|
4125
|
-
|
|
4156
|
+
userId,paymentAmount
|
|
4126
4157
|
user_001,150000
|
|
4127
4158
|
user_002,50000
|
|
4128
4159
|
|
|
4129
|
-
JSON example: [{"
|
|
4160
|
+
JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
|
|
4130
4161
|
`,
|
|
4131
4162
|
inputSchema: {
|
|
4132
4163
|
content: z6.string().describe("CSV or JSON content as string"),
|
|
@@ -4172,7 +4203,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
4172
4203
|
|
|
4173
4204
|
// src/mcp/tools/profile.ts
|
|
4174
4205
|
import { z as z7 } from "zod";
|
|
4175
|
-
var RELATIVE_THRESHOLD =
|
|
4206
|
+
var RELATIVE_THRESHOLD = `flagged = p50 \u2265 ${SLOW_MULTIPLIER}\xD7 median of per-rule p50s within the group; absolute thresholds are intentionally not supported.`;
|
|
4176
4207
|
function profileParams(opts) {
|
|
4177
4208
|
const params = {};
|
|
4178
4209
|
if (opts.versionId) params.versionId = opts.versionId;
|
|
@@ -4186,11 +4217,13 @@ function registerProfileTools(server, callApi) {
|
|
|
4186
4217
|
"lexq_profile_overview",
|
|
4187
4218
|
{
|
|
4188
4219
|
title: "Group Latency Profile",
|
|
4189
|
-
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD +
|
|
4220
|
+
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 6, p95 n \u2265 60, p99 n \u2265 300 \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
|
|
4190
4221
|
inputSchema: {
|
|
4191
4222
|
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
4192
4223
|
versionId: z7.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
4193
|
-
from: z7.string().optional().describe(
|
|
4224
|
+
from: z7.string().optional().describe(
|
|
4225
|
+
`Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
4226
|
+
),
|
|
4194
4227
|
to: z7.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
|
|
4195
4228
|
cacheState: z7.enum(["HIT", "MISS"]).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
|
|
4196
4229
|
}
|
|
@@ -4203,12 +4236,14 @@ function registerProfileTools(server, callApi) {
|
|
|
4203
4236
|
"lexq_profile_rule",
|
|
4204
4237
|
{
|
|
4205
4238
|
title: "Rule Latency Detail",
|
|
4206
|
-
description:
|
|
4239
|
+
description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265 6, p95 n \u2265 60, p99 n \u2265 300). ` + RELATIVE_THRESHOLD,
|
|
4207
4240
|
inputSchema: {
|
|
4208
4241
|
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
4209
4242
|
ruleId: z7.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
|
|
4210
4243
|
versionId: z7.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
4211
|
-
from: z7.string().optional().describe(
|
|
4244
|
+
from: z7.string().optional().describe(
|
|
4245
|
+
`Window start, ISO-8601 instant. Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
4246
|
+
),
|
|
4212
4247
|
to: z7.string().optional().describe("Window end, ISO-8601 instant. Default: now")
|
|
4213
4248
|
}
|
|
4214
4249
|
},
|
|
@@ -4242,7 +4277,9 @@ function registerReplayTools(server, callApi) {
|
|
|
4242
4277
|
candidateVersionId: z8.string().uuid().describe("Version to re-evaluate against"),
|
|
4243
4278
|
from: z8.string().describe("Window start date (yyyy-MM-dd)"),
|
|
4244
4279
|
to: z8.string().describe("Window end date (yyyy-MM-dd)"),
|
|
4245
|
-
maxRecords: z8.number().int().min(1).optional().describe(
|
|
4280
|
+
maxRecords: z8.number().int().min(1).optional().describe(
|
|
4281
|
+
`Sample cap (server default applies; hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`
|
|
4282
|
+
)
|
|
4246
4283
|
}
|
|
4247
4284
|
},
|
|
4248
4285
|
async ({ candidateVersionId, from, to, maxRecords }) => callApi("POST", "replay/jobs", { body: { candidateVersionId, from, to, maxRecords } })
|
package/dist/mcp/register.js
CHANGED
|
@@ -309,6 +309,17 @@ function registerVersionTools(server, callApi) {
|
|
|
309
309
|
);
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
// src/types/constants.ts
|
|
313
|
+
var MAX_ROUNDING_SCALE = 34;
|
|
314
|
+
var REPLAY_WINDOW_MAX_RECORDS = 5e4;
|
|
315
|
+
var PROFILE_SAMPLE_PERMILLE = 10;
|
|
316
|
+
var LATENCY_WINDOW_MILLIS = 6e4;
|
|
317
|
+
var TAIL_MIN_OBS = 3;
|
|
318
|
+
var SLOW_MULTIPLIER = 10;
|
|
319
|
+
var MIN_SAMPLES = 100;
|
|
320
|
+
var MIN_COHORT_SIZE = 3;
|
|
321
|
+
var PROFILE_DEFAULT_WINDOW_HOURS = 24;
|
|
322
|
+
|
|
312
323
|
// src/mcp/tools/rules.ts
|
|
313
324
|
import { z as z3 } from "zod";
|
|
314
325
|
import dedent from "dedent";
|
|
@@ -367,7 +378,7 @@ function registerRuleTools(server, callApi) {
|
|
|
367
378
|
- HAS_ANY: fact has at least one of the given values
|
|
368
379
|
- HAS_ALL: fact has all of the given values
|
|
369
380
|
- HAS_NONE: fact has none of the given values
|
|
370
|
-
Example: { "type": "SINGLE", "field": "
|
|
381
|
+
Example: { "type": "SINGLE", "field": "userTags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
|
|
371
382
|
|
|
372
383
|
Do NOT use CONTAINS on a list fact — CONTAINS is substring match on STRING facts only.
|
|
373
384
|
IN is the mirror of HAS_*: IN takes a scalar fact with a list value; HAS_* takes lists on both sides.
|
|
@@ -385,7 +396,7 @@ function registerRuleTools(server, callApi) {
|
|
|
385
396
|
refVar is the base for percentage calculation and is OPTIONAL — omit it to use targetVar
|
|
386
397
|
itself. It is only meaningful in PERCENTAGE × {ASSIGN, ADD, SUB}; specifying it in any
|
|
387
398
|
other cell is an error. Use it when the base differs from the target, e.g.
|
|
388
|
-
"points +=
|
|
399
|
+
"points += orderTotal × 5%" → { targetVar: "points", refVar: "orderTotal",
|
|
389
400
|
operator: "ADD", method: "PERCENTAGE", operand: 5 }.
|
|
390
401
|
operator × method matrix:
|
|
391
402
|
ASSIGN targetVar = operand | targetVar = refVar × operand/100
|
|
@@ -399,9 +410,9 @@ function registerRuleTools(server, callApi) {
|
|
|
399
410
|
— this is the only action that does. MUTATE_FACT requires the target to already exist.
|
|
400
411
|
- BLOCK: { reason: string } Records a rejection decision. It does NOT halt rule execution —
|
|
401
412
|
subsequent actions and subsequent winning rules still run. Enforcement is the caller's
|
|
402
|
-
responsibility; the decision surfaces as the
|
|
413
|
+
responsibility; the decision surfaces as the isBlocked fact.
|
|
403
414
|
|
|
404
|
-
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0
|
|
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).
|
|
405
416
|
`,
|
|
406
417
|
inputSchema: {
|
|
407
418
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
@@ -498,6 +509,11 @@ function registerRuleTools(server, callApi) {
|
|
|
498
509
|
|
|
499
510
|
// src/mcp/tools/facts.ts
|
|
500
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
|
|
501
517
|
function registerFactTools(server, callApi) {
|
|
502
518
|
server.registerTool(
|
|
503
519
|
"lexq_facts_list",
|
|
@@ -520,9 +536,9 @@ function registerFactTools(server, callApi) {
|
|
|
520
536
|
"lexq_facts_create",
|
|
521
537
|
{
|
|
522
538
|
title: "Create Fact Definition",
|
|
523
|
-
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.",
|
|
524
540
|
inputSchema: {
|
|
525
|
-
key: z4.string().regex(
|
|
541
|
+
key: z4.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
526
542
|
name: z4.string().describe("Display name"),
|
|
527
543
|
type: z4.enum(["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"]).describe("Value type"),
|
|
528
544
|
description: z4.string().optional().describe("Description"),
|
|
@@ -763,16 +779,16 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
763
779
|
|
|
764
780
|
Returns:
|
|
765
781
|
inputFacts — normalized input facts
|
|
766
|
-
mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates
|
|
767
|
-
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)
|
|
768
784
|
executionTraces — per-rule match status
|
|
769
785
|
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
770
786
|
|
|
771
|
-
Example input: { "facts": { "
|
|
787
|
+
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
772
788
|
Always dry-run before publishing to validate rule behavior.`,
|
|
773
789
|
inputSchema: {
|
|
774
790
|
versionId: z6.string().uuid().describe("Policy version ID to test against"),
|
|
775
|
-
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}'),
|
|
776
792
|
includeDebugInfo: z6.boolean().default(true).describe("Include execution and decision traces")
|
|
777
793
|
}
|
|
778
794
|
},
|
|
@@ -929,11 +945,11 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
929
945
|
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
930
946
|
|
|
931
947
|
CSV example:
|
|
932
|
-
|
|
948
|
+
userId,paymentAmount
|
|
933
949
|
user_001,150000
|
|
934
950
|
user_002,50000
|
|
935
951
|
|
|
936
|
-
JSON example: [{"
|
|
952
|
+
JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
|
|
937
953
|
`,
|
|
938
954
|
inputSchema: {
|
|
939
955
|
content: z6.string().describe("CSV or JSON content as string"),
|
|
@@ -979,7 +995,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
979
995
|
|
|
980
996
|
// src/mcp/tools/profile.ts
|
|
981
997
|
import { z as z7 } from "zod";
|
|
982
|
-
var RELATIVE_THRESHOLD =
|
|
998
|
+
var RELATIVE_THRESHOLD = `flagged = p50 \u2265 ${SLOW_MULTIPLIER}\xD7 median of per-rule p50s within the group; absolute thresholds are intentionally not supported.`;
|
|
983
999
|
function profileParams(opts) {
|
|
984
1000
|
const params = {};
|
|
985
1001
|
if (opts.versionId) params.versionId = opts.versionId;
|
|
@@ -993,11 +1009,13 @@ function registerProfileTools(server, callApi) {
|
|
|
993
1009
|
"lexq_profile_overview",
|
|
994
1010
|
{
|
|
995
1011
|
title: "Group Latency Profile",
|
|
996
|
-
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD +
|
|
1012
|
+
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 6, p95 n \u2265 60, p99 n \u2265 300 \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
|
|
997
1013
|
inputSchema: {
|
|
998
1014
|
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
999
1015
|
versionId: z7.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
1000
|
-
from: z7.string().optional().describe(
|
|
1016
|
+
from: z7.string().optional().describe(
|
|
1017
|
+
`Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
1018
|
+
),
|
|
1001
1019
|
to: z7.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
|
|
1002
1020
|
cacheState: z7.enum(["HIT", "MISS"]).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
|
|
1003
1021
|
}
|
|
@@ -1010,12 +1028,14 @@ function registerProfileTools(server, callApi) {
|
|
|
1010
1028
|
"lexq_profile_rule",
|
|
1011
1029
|
{
|
|
1012
1030
|
title: "Rule Latency Detail",
|
|
1013
|
-
description:
|
|
1031
|
+
description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265 6, p95 n \u2265 60, p99 n \u2265 300). ` + RELATIVE_THRESHOLD,
|
|
1014
1032
|
inputSchema: {
|
|
1015
1033
|
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
1016
1034
|
ruleId: z7.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
|
|
1017
1035
|
versionId: z7.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
1018
|
-
from: z7.string().optional().describe(
|
|
1036
|
+
from: z7.string().optional().describe(
|
|
1037
|
+
`Window start, ISO-8601 instant. Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
1038
|
+
),
|
|
1019
1039
|
to: z7.string().optional().describe("Window end, ISO-8601 instant. Default: now")
|
|
1020
1040
|
}
|
|
1021
1041
|
},
|
|
@@ -1049,7 +1069,9 @@ function registerReplayTools(server, callApi) {
|
|
|
1049
1069
|
candidateVersionId: z8.string().uuid().describe("Version to re-evaluate against"),
|
|
1050
1070
|
from: z8.string().describe("Window start date (yyyy-MM-dd)"),
|
|
1051
1071
|
to: z8.string().describe("Window end date (yyyy-MM-dd)"),
|
|
1052
|
-
maxRecords: z8.number().int().min(1).optional().describe(
|
|
1072
|
+
maxRecords: z8.number().int().min(1).optional().describe(
|
|
1073
|
+
`Sample cap (server default applies; hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`
|
|
1074
|
+
)
|
|
1053
1075
|
}
|
|
1054
1076
|
},
|
|
1055
1077
|
async ({ candidateVersionId, from, to, maxRecords }) => callApi("POST", "replay/jobs", { body: { candidateVersionId, from, to, maxRecords } })
|
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": {
|
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
30
|
"enums": "node scripts/gen-enums.mjs",
|
|
31
31
|
"enums:check": "node scripts/gen-enums.mjs --check",
|
|
32
|
+
"constants:check": "node scripts/check-constants.mjs",
|
|
32
33
|
"start": "node dist/index.js",
|
|
33
34
|
"test:decimals": "pnpm build && node tests/exact-decimals.mjs",
|
|
35
|
+
"test:fact-key": "node tests/fact-key.mjs",
|
|
34
36
|
"prepublishOnly": "pnpm build",
|
|
35
37
|
"knip": "knip",
|
|
36
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
|
|