@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 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":"is_adult","value":true}}]
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 are `snake_case` and case-sensitive.
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), `bash tests/e2e.sh`.
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 (snake_case), value type, display name, and description. |
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":{"loyalty_tier":"PLATINUM","purchase_subtotal_usd":150}}'
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
- bash tests/e2e.sh # CLI E2E tests (requires API key)
239
- bash tests/test-engine-api.sh # Engine API integration tests
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": "customer_tier",
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": "payment_amount", "operator": "SUB", "method": "PERCENTAGE", "operand": 20 } }
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": "user_tags", "operator": "HAS_ANY",
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": "payment_amount", "operator": "SUB", "method": "PERCENTAGE", "operand": 25 } }
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 (user_id, user_tags) are auto-created.
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 (lowercase, 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(
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 customer_tier --name "Customer Tier" --type STRING
1275
- $ lexq facts create --key order_total --name "Order Total" --type NUMBER --required
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": "user_region",
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: lowercase letters, numbers, underscores only (e.g., payment_amount)
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": {"payment_amount": 150000, "customer_tier": "VIP"}}'
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": {"payment_amount": 100000, "customer_tier": "VIP"}
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": "user_tags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
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 += order_total × 5%" → { targetVar: "points", refVar: "order_total",
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 is_blocked fact.
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 must be lowercase with underscores (e.g. payment_amount). Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER.",
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(/^[a-z][a-z0-9_]*$/).describe("Variable key (snake_case)"),
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 payment_amount)
3985
- generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {fact_name}__delta key (signed difference)
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": { "payment_amount": 100000, "customer_tier": "VIP" } }
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. {"payment_amount":100000}'),
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
- user_id,payment_amount
4156
+ userId,paymentAmount
4151
4157
  user_001,150000
4152
4158
  user_002,50000
4153
4159
 
4154
- JSON example: [{"user_id":"user_001","payment_amount":150000}, {"user_id":"user_002","payment_amount":50000}]
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"),
@@ -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": "user_tags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
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 += order_total × 5%" → { targetVar: "points", refVar: "order_total",
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 is_blocked fact.
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 must be lowercase with underscores (e.g. payment_amount). Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER.",
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(/^[a-z][a-z0-9_]*$/).describe("Variable key (snake_case)"),
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 payment_amount)
778
- generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {fact_name}__delta key (signed difference)
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": { "payment_amount": 100000, "customer_tier": "VIP" } }
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. {"payment_amount":100000}'),
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
- user_id,payment_amount
948
+ userId,paymentAmount
944
949
  user_001,150000
945
950
  user_002,50000
946
951
 
947
- JSON example: [{"user_id":"user_001","payment_amount":150000}, {"user_id":"user_002","payment_amount":50000}]
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.50",
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
- `is_blocked` fact while its own rule stays `SELECTED`.
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 payment_amount --name "Payment Amount" --type NUMBER --required
23
- lexq facts create --key customer_tier --name "Customer Tier" --type STRING
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": "payment_amount",
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": "payment_amount",
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": "payment_amount", "operator": "GREATER_THAN_OR_EQUAL", "value": 100000, "valueType": "NUMBER" },
54
- { "type": "SINGLE", "field": "payment_amount", "operator": "LESS_THAN", "value": 500000, "valueType": "NUMBER" }
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": "payment_amount",
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": "payment_amount",
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": "payment_amount",
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":{"payment_amount":600000}}'
92
- # Expected: mutatedFacts.payment_amount = 480000, generatedVariables.payment_amount__delta = -120000
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":{"payment_amount":200000}}'
95
- # Expected: mutatedFacts.payment_amount = 180000, generatedVariables.payment_amount__delta = -20000
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 transaction_amount --name "Transaction Amount" --type NUMBER --required
117
- lexq facts create --key transaction_count_24h --name "Transactions in 24h" --type NUMBER
118
- lexq facts create --key country_code --name "Country Code" --type STRING
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": "transaction_amount", "operator": "GREATER_THAN", "value": 5000000, "valueType": "NUMBER" },
128
- { "type": "SINGLE", "field": "transaction_count_24h", "operator": "GREATER_THAN", "value": 10, "valueType": "NUMBER" }
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": "country_code",
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": { "transaction_amount": 10000000, "transaction_count_24h": 15, "country_code": "KR" }
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": "payment_amount",
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": { "payment_amount": 100000, "customer_tier": "VIP" }
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:** `total_points` must be present in the request facts. The engine is stateless — it does
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 purchase_amount --name "Purchase Amount" --type NUMBER --required
226
- lexq facts create --key is_first_purchase --name "First Purchase" --type BOOLEAN
227
- lexq facts create --key total_points --name "Total Points" --type NUMBER --required
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": "is_first_purchase",
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": "total_points",
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 purchase_amount = 1 point per 1000 KRW
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": "purchase_amount",
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": "total_points",
267
- "refVar": "purchase_amount",
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": { "purchase_amount": 50000, "is_first_purchase": true, "total_points": 0 }
279
+ "facts": { "purchaseAmount": 50000, "isFirstPurchase": true, "totalPoints": 0 }
280
280
  }'
281
281
  ```
282
282
 
283
- `total_points` and `refVar: purchase_amount` are two different facts — this is exactly what
284
- `refVar` exists for. Omitting it would compute `total_points += total_points × 0.1%`.
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 `total_points` and `total_points__delta` from `generatedVariables` in the response.
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": "customer_tier", "operator": "EQUALS", "value": "VIP", "valueType": "STRING"
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": "payment_amount",
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": "payment_amount", "operator": "GREATER_THAN_OR_EQUAL", "value": 50000, "valueType": "NUMBER"
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": "payment_amount",
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 lifetime_value --name "Lifetime Value" --type NUMBER --required
439
- lexq facts create --key signup_days --name "Days Since Signup" --type NUMBER --required
440
- lexq facts create --key support_tier --name "Support Tier" --type STRING
441
- lexq facts create --key beta_enabled --name "Beta Access" --type BOOLEAN
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": "lifetime_value",
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": "support_tier", "value": "PRIORITY" } }
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": "signup_days",
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": "beta_enabled", "value": true } }
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": { "lifetime_value": 1500000, "signup_days": 400 }
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": "payment_amount",
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": "customer_tier",
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": "payment_amount",
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": "user_tags",
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
- `(customer_tier = "VIP" AND payment_amount >= 100000) OR region IN ["KR", "JP"]`:
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": "customer_tier",
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": "payment_amount",
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": "loyalty_point",
222
- "refVar": "order_total",
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
- `loyalty_point += order_total × 5%` — two different facts. Omitting `refVar` would compute
231
- `loyalty_point += loyalty_point × 5%` instead.
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 `is_blocked` fact. Subsequent actions in 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 `is_blocked` from the response.
245
+ read `isBlocked` from the response.
246
246
 
247
247
  ### Action Example: 10% Discount via MUTATE_FACT
248
248
 
249
- Reduces `payment_amount` by 10%. `__delta` is auto-generated in `generatedVariables` (e.g.,
250
- `payment_amount__delta: -10000` for a 100,000 input).
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": "payment_amount",
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": "customer_tier", "operator": "EQUALS", "value": "VIP", "valueType": "STRING" },
305
- { "type": "SINGLE", "field": "payment_amount", "operator": "GREATER_THAN_OR_EQUAL", "value": 100000, "valueType": "NUMBER" }
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": "payment_amount",
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": "payment_amount",
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 (snake_case, case-sensitive)
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 use `snake_case`.** Example: `payment_amount`, `customer_tier`. Always lowercase.
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": "payment_amount",
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": "customer_tier",
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
- "payment_amount": 0,
54
- "customer_tier": ""
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
- "payment_amount": 150000,
71
- "customer_tier": "VIP"
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": { "payment_amount": 150000, "customer_tier": "VIP" }
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
- "payment_amount": 150000,
102
- "customer_tier": "VIP"
101
+ "paymentAmount": 150000,
102
+ "customerTier": "VIP"
103
103
  },
104
104
  "mutatedFacts": {
105
- "payment_amount": 135000,
106
- "customer_tier": "VIP"
105
+ "paymentAmount": 135000,
106
+ "customerTier": "VIP"
107
107
  },
108
108
  "generatedVariables": {
109
- "payment_amount__delta": -15000
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": "(customer_tier == 'VIP') && (payment_amount >= 100000)",
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 `payment_amount`)
143
- - `generatedVariables` — for every fact in `mutatedFacts`, a paired `{fact_name}__delta` key is auto-generated with the
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 `is_blocked` fact and its own rule stays
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": { "payment_amount": 150000, "customer_tier": "VIP" },
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": "payment_amount__delta",
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": "payment_amount__delta",
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": { "payment_amount": 150000, "customer_tier": "VIP" }
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": "payment_amount__delta", "aggregationType": "SUM" }
369
+ "metricConfig": { "targetVariable": "paymentAmount__delta", "aggregationType": "SUM" }
370
370
  }
371
371
  }'
372
372