@lexq/cli 0.1.52 → 0.1.54

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
@@ -129,8 +129,9 @@ Place this file in the project root. The IDE will auto-discover it.
129
129
 
130
130
  ### Claude Code
131
131
 
132
- Reads this file via the root `CLAUDE.md` pointer. No separate context file this document and `skills/` are the whole
133
- guide.
132
+ Reads this file through the root `CLAUDE.md`, which is a one-line import rather than a copy. There is no
133
+ Claude-specific document to keep in sync. The CLI guide is this file plus `skills/`; `CONTEXT.md` covers platform
134
+ architecture separately and ships alongside.
134
135
 
135
136
  ### Gemini CLI
136
137
 
package/dist/index.js CHANGED
@@ -2359,6 +2359,7 @@ import "commander";
2359
2359
  import dedent9 from "dedent";
2360
2360
 
2361
2361
  // src/types/enums.ts
2362
+ var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
2362
2363
  var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
2363
2364
  var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
2364
2365
  var ProfileCacheState = ["HIT", "MISS"];
@@ -3405,7 +3406,7 @@ function registerGroupTools(server, callApi) {
3405
3406
  "lexq_ab_test_start",
3406
3407
  {
3407
3408
  title: "Start A/B Test",
3408
- description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate.",
3409
+ description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. The split is computed from context.trafficKey on each execution request; requests that omit it never reach the challenger and the test stays at 0%.",
3409
3410
  inputSchema: {
3410
3411
  groupId: z.string().uuid().describe("Policy group ID"),
3411
3412
  testVersionId: z.string().uuid().describe("Challenger version ID to test"),
@@ -3722,6 +3723,7 @@ import { z as z4 } from "zod";
3722
3723
  var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
3723
3724
 
3724
3725
  // src/mcp/tools/facts.ts
3726
+ var VALUE_TYPES = ValueType;
3725
3727
  function registerFactTools(server, callApi) {
3726
3728
  server.registerTool(
3727
3729
  "lexq_facts_list",
@@ -3748,7 +3750,7 @@ function registerFactTools(server, callApi) {
3748
3750
  inputSchema: {
3749
3751
  key: z4.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
3750
3752
  name: z4.string().describe("Display name"),
3751
- type: z4.enum(["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"]).describe("Value type"),
3753
+ type: z4.enum(VALUE_TYPES).describe("Value type"),
3752
3754
  description: z4.string().optional().describe("Description"),
3753
3755
  isRequired: z4.boolean().default(false).describe("Whether this fact is required for rule evaluation"),
3754
3756
  isPii: z4.boolean().default(false).describe(
@@ -3762,11 +3764,14 @@ function registerFactTools(server, callApi) {
3762
3764
  "lexq_facts_update",
3763
3765
  {
3764
3766
  title: "Update Fact Definition",
3765
- description: "Update a fact definition. Key and type cannot be changed. Only provided fields are updated. System facts only allow name, description, and PII changes.",
3767
+ description: "Update a fact definition. The key is immutable. The type can change only while no rule references the fact; if any does, the call fails with FD-007 and reports the count. Only the fields you send are changed. System facts accept name, description, and PII only.",
3766
3768
  inputSchema: {
3767
3769
  factId: z4.string().uuid().describe("Fact definition ID"),
3768
3770
  name: z4.string().optional().describe("Display name"),
3769
3771
  description: z4.string().optional().describe("Description"),
3772
+ type: z4.enum(VALUE_TYPES).optional().describe(
3773
+ "Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule references the fact."
3774
+ ),
3770
3775
  isRequired: z4.boolean().optional().describe("Required flag"),
3771
3776
  isPii: z4.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)")
3772
3777
  }
@@ -3777,7 +3782,7 @@ function registerFactTools(server, callApi) {
3777
3782
  "lexq_facts_delete",
3778
3783
  {
3779
3784
  title: "Delete Fact Definition",
3780
- description: "Delete a fact definition. System facts cannot be deleted.",
3785
+ description: "Delete a fact definition. System facts cannot be deleted. Neither can a fact that any rule references: that call fails with FD-006 and reports the count. Remove the references first.",
3781
3786
  inputSchema: {
3782
3787
  factId: z4.string().uuid().describe("Fact definition ID")
3783
3788
  }
@@ -15,6 +15,7 @@ function registerStatusTools(server, callApi) {
15
15
  import { z } from "zod";
16
16
 
17
17
  // src/types/enums.ts
18
+ var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
18
19
  var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
19
20
  var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
20
21
  var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
@@ -128,7 +129,7 @@ function registerGroupTools(server, callApi) {
128
129
  "lexq_ab_test_start",
129
130
  {
130
131
  title: "Start A/B Test",
131
- description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate.",
132
+ description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. The split is computed from context.trafficKey on each execution request; requests that omit it never reach the challenger and the test stays at 0%.",
132
133
  inputSchema: {
133
134
  groupId: z.string().uuid().describe("Policy group ID"),
134
135
  testVersionId: z.string().uuid().describe("Challenger version ID to test"),
@@ -514,6 +515,7 @@ import { z as z4 } from "zod";
514
515
  var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
515
516
 
516
517
  // src/mcp/tools/facts.ts
518
+ var VALUE_TYPES = ValueType;
517
519
  function registerFactTools(server, callApi) {
518
520
  server.registerTool(
519
521
  "lexq_facts_list",
@@ -540,7 +542,7 @@ function registerFactTools(server, callApi) {
540
542
  inputSchema: {
541
543
  key: z4.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
542
544
  name: z4.string().describe("Display name"),
543
- type: z4.enum(["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"]).describe("Value type"),
545
+ type: z4.enum(VALUE_TYPES).describe("Value type"),
544
546
  description: z4.string().optional().describe("Description"),
545
547
  isRequired: z4.boolean().default(false).describe("Whether this fact is required for rule evaluation"),
546
548
  isPii: z4.boolean().default(false).describe(
@@ -554,11 +556,14 @@ function registerFactTools(server, callApi) {
554
556
  "lexq_facts_update",
555
557
  {
556
558
  title: "Update Fact Definition",
557
- description: "Update a fact definition. Key and type cannot be changed. Only provided fields are updated. System facts only allow name, description, and PII changes.",
559
+ description: "Update a fact definition. The key is immutable. The type can change only while no rule references the fact; if any does, the call fails with FD-007 and reports the count. Only the fields you send are changed. System facts accept name, description, and PII only.",
558
560
  inputSchema: {
559
561
  factId: z4.string().uuid().describe("Fact definition ID"),
560
562
  name: z4.string().optional().describe("Display name"),
561
563
  description: z4.string().optional().describe("Description"),
564
+ type: z4.enum(VALUE_TYPES).optional().describe(
565
+ "Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule references the fact."
566
+ ),
562
567
  isRequired: z4.boolean().optional().describe("Required flag"),
563
568
  isPii: z4.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)")
564
569
  }
@@ -569,7 +574,7 @@ function registerFactTools(server, callApi) {
569
574
  "lexq_facts_delete",
570
575
  {
571
576
  title: "Delete Fact Definition",
572
- description: "Delete a fact definition. System facts cannot be deleted.",
577
+ description: "Delete a fact definition. System facts cannot be deleted. Neither can a fact that any rule references: that call fails with FD-006 and reports the count. Remove the references first.",
573
578
  inputSchema: {
574
579
  factId: z4.string().uuid().describe("Fact definition ID")
575
580
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
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": {
@@ -31,6 +31,7 @@
31
31
  "enums:check": "node scripts/gen-enums.mjs --check",
32
32
  "constants:check": "node scripts/check-constants.mjs",
33
33
  "surface:check": "node scripts/check-public-surface.mjs",
34
+ "ab-key:check": "node scripts/check-ab-key.mjs",
34
35
  "start": "node dist/index.js",
35
36
  "test:decimals": "pnpm build && node tests/exact-decimals.mjs",
36
37
  "test:fact-key": "node tests/fact-key.mjs",
@@ -159,6 +159,33 @@ lexq groups create --json '{
159
159
 
160
160
  Split traffic between the current live version and a test version.
161
161
 
162
+ ### Traffic Identity Key
163
+
164
+ **Every execution request has to carry `context.trafficKey`.** That value is what the split is
165
+ computed from: the server hashes it together with the group ID and assigns the request to a
166
+ bucket, so the same key value always reaches the same version for that group.
167
+
168
+ Send it in `context`, not in `facts`. Routing never reads `facts`.
169
+
170
+ ```json
171
+ {
172
+ "facts": { "paymentAmount": 150000, "customerTier": "VIP" },
173
+ "context": { "trafficKey": "customer-8f3a21" }
174
+ }
175
+ ```
176
+
177
+ If the key is missing or empty, no request is ever assigned to the test version. The test still
178
+ reports as running and execution logs keep accumulating, so **the failure does not surface in the
179
+ response** — only a server-side warning records it. This is the most common reason a test sits
180
+ at 0%.
181
+
182
+ Pick a value that is stable per experiment unit (a customer ID, a merchant ID, a session ID) and
183
+ opaque — the whole `context` object is stored on the execution ledger, so send a hash or a
184
+ surrogate key rather than an email address. A value that changes every request, such as a request
185
+ ID, scatters one user across both versions and destroys the comparison.
186
+
187
+ Batch calls read `sharedContext.trafficKey` instead, and the whole batch runs on one version.
188
+
162
189
  ### Start A/B Test
163
190
 
164
191
  ```bash
@@ -191,11 +218,12 @@ This reverts all traffic to the main version. The test version remains ACTIVE bu
191
218
  ```
192
219
  1. Create two versions (v1 live, v2 DRAFT with changes)
193
220
  2. Publish v2: lexq deploy publish --group-id <gid> --version-id <v2id>
194
- 3. Start A/B: lexq groups ab-test start --group-id <gid> --version-id <v2id> --traffic-rate 10
195
- 4. Monitor: lexq history stats (compare metrics)
196
- 5. Adjust traffic gradually: 10% 30% → 50%
197
- 6. Promote winner: lexq deploy live --group-id <gid> --version-id <v2id>
198
- 7. Stop test: lexq groups ab-test stop --group-id <gid> --force
221
+ 3. Confirm your execution requests send context.trafficKey
222
+ 4. Start A/B: lexq groups ab-test start --group-id <gid> --version-id <v2id> --traffic-rate 10
223
+ 5. Monitor: lexq history stats (compare metrics)
224
+ 6. Adjust traffic gradually: 10% 30% 50%
225
+ 7. Promote winner: lexq deploy live --group-id <gid> --version-id <v2id>
226
+ 8. Stop test: lexq groups ab-test stop --group-id <gid> --force
199
227
  ```
200
228
 
201
229
  ## Common Patterns
@@ -190,6 +190,7 @@ lexq analytics dry-run --version-id <v2id> --debug --json '{
190
190
  lexq deploy publish --group-id <gid> --version-id <v2id> --memo "15% discount test"
191
191
 
192
192
  # 5. Start A/B test at 10% traffic
193
+ # Your execution requests must carry context.trafficKey, or the test stays at 0%.
193
194
  lexq groups ab-test start --group-id <gid> --version-id <v2id> --traffic-rate 10
194
195
 
195
196
  # 6. Monitor (check execution stats periodically)
@@ -141,7 +141,7 @@ Prefixes you will encounter through the CLI:
141
141
  | `A-` | Auth | Invalid or missing API key |
142
142
  | `P-` | Policy group/version | Not found, wrong lifecycle state, already live |
143
143
  | `ACT-` | Action parameters | Missing or invalid action parameter |
144
- | `FD-` | Fact definitions | Duplicate key, system fact immutable |
144
+ | `FD-` | Fact definitions | Duplicate key, system fact immutable, fact still referenced by rules |
145
145
  | `AN-` | Analytics | Dry-run or simulation failure |
146
146
  | `FL-` | Failure logs | Log not found |
147
147
  | `WH-` | Webhook subscriptions | Invalid URL, delivery failure |
@@ -60,6 +60,11 @@ Response includes:
60
60
 
61
61
  **Always run this before dry-run.** Copy `exampleRequest.facts` as your starting template and fill in real values.
62
62
 
63
+ `exampleRequest.context` comes back empty, and it stays empty even when the group has an A/B test
64
+ running. Live execution requests against such a group also need `context.trafficKey` or the test
65
+ version receives no traffic — see the A/B testing section of the groups skill. Dry-run itself is
66
+ unaffected: it always evaluates the version you name, so routing never applies here.
67
+
63
68
  ## 2. Dry Run (Single Input)
64
69
 
65
70
  Test a single set of facts against a version: