@lexq/cli 1.1.1 → 1.1.2

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
@@ -5,7 +5,7 @@
5
5
  ## What is this?
6
6
 
7
7
  LexQ CLI (`@lexq/cli`, binary: `lexq`) manages a policy execution engine. Policies are business rules (if-then) that
8
- evaluate input facts and produce actions (discounts, blocks, notifications, etc.).
8
+ evaluate input facts and produce actions: change a number such as a discount, set a fact, or record a rejection.
9
9
 
10
10
  The CLI also doubles as an **MCP server** — run `lexq serve --mcp` to expose the full command inventory as MCP tools to
11
11
  any MCP-compatible AI client.
@@ -106,7 +106,7 @@ lexq deploy live --group-id $GID --version-id $VID --memo "Initial deploy"
106
106
 
107
107
  ## Example: Subscribe to Deployment Notifications
108
108
 
109
- Webhook subscriptions fire on platform events — separate from rule-level webhooks.
109
+ Webhook subscriptions fire on deployment lifecycle events.
110
110
 
111
111
  ```bash
112
112
  # Subscribe to Slack for deployment events
package/CONTEXT.md CHANGED
@@ -64,11 +64,11 @@ Tenant
64
64
 
65
65
  ### Policy Group Status
66
66
 
67
- | Value | Description |
68
- |------------|------------------------------------|
69
- | `ACTIVE` | Normal operation |
70
- | `DISABLED` | Execution blocked (emergency stop) |
71
- | `ARCHIVED` | Permanently removed |
67
+ | Value | Description |
68
+ |------------|---------------------------------------------------------------------------------------------------------------------|
69
+ | `ACTIVE` | Normal operation |
70
+ | `DISABLED` | Production execution rejected (`P-009`); dry run, simulation, and replay still work |
71
+ | `ARCHIVED` | Deleted. Kept for audit if it was ever executed, deployed, or replayed; otherwise permanently deleted after 30 days |
72
72
 
73
73
  ### Policy Version Status
74
74
 
package/README.md CHANGED
@@ -48,7 +48,7 @@ Requires **Node.js 22+**.
48
48
  ```bash
49
49
  # 1. Authenticate
50
50
  lexq auth login
51
- # Enter your API key (create one at console.lexq.io → Management → API Keys)
51
+ # Enter your API key (create one with the Manage scope at console.lexq.io → Management → API Keys)
52
52
 
53
53
  # 2. Verify
54
54
  lexq auth whoami
package/dist/index.js CHANGED
@@ -163,6 +163,10 @@ import Table from "cli-table3";
163
163
  function printJson(data) {
164
164
  console.log(stringifyJson(data, 2));
165
165
  }
166
+ function groupLabel(name, archivedAt, fallback = "\u2013") {
167
+ const label = name ?? fallback;
168
+ return archivedAt ? `(deleted) ${label}` : label;
169
+ }
166
170
  function printTable(headers, rows, options) {
167
171
  const table = new Table({
168
172
  head: headers,
@@ -227,7 +231,8 @@ function registerAuthCommands(program) {
227
231
  whoami Verify authentication and show account info
228
232
 
229
233
  Getting Started:
230
- 1. Get your API key from the LexQ Console (Settings → API Keys)
234
+ 1. Create an API key with the Manage scope in the LexQ Console (Management → API Keys).
235
+ An "Execute only" key is for the execution API and does not work with this CLI.
231
236
  2. Run: lexq auth login
232
237
  3. Verify: lexq auth whoami
233
238
  `
@@ -269,7 +274,7 @@ function registerAuthCommands(program) {
269
274
 
270
275
  Example:
271
276
  $ lexq auth whoami
272
- { "tenantId": "abc-123", "userId": "...", "role": "ADMIN", "tenantTimezone": "Asia/Seoul", "apiKey": "lexq_us_****abcd" }
277
+ { "tenantId": "abc-123", "userId": "...", "role": "API_ADMIN", "tenantTimezone": "Asia/Seoul", "apiKey": "lexq****abcd", "baseUrl": "https://api.lexq.io/api/v1/partners" }
273
278
  `
274
279
  ).action(async () => {
275
280
  try {
@@ -310,7 +315,7 @@ function registerStatusCommand(program) {
310
315
 
311
316
  Example:
312
317
  $ lexq status
313
- { "status": "ok", "latencyMs": 142, "tenantId": "abc-123", "role": "ADMIN", "tenantTimezone": "Asia/Seoul" }
318
+ { "status": "ok", "latencyMs": 142, "tenantId": "abc-123", "role": "API_ADMIN", "tenantTimezone": "Asia/Seoul" }
314
319
 
315
320
  Use this to verify your API key is valid and the LexQ API is reachable.
316
321
  tenantTimezone is the zone every yyyy-MM-dd date option is read in.
@@ -342,6 +347,8 @@ var MAX_ROUNDING_SCALE = 34;
342
347
  var MIN_TRAFFIC_RATE = 1;
343
348
  var MAX_TRAFFIC_RATE = 99;
344
349
  var REPLAY_WINDOW_MAX_RECORDS = 5e4;
350
+ var DEFAULT_SIMULATION_RECORDS = 1e3;
351
+ var MAX_SIMULATION_RECORDS = 1e5;
345
352
  var PROFILE_SAMPLE_PERMILLE = 10;
346
353
  var LATENCY_WINDOW_MILLIS = 6e4;
347
354
  var TAIL_MIN_OBS = 3;
@@ -384,18 +391,21 @@ function registerGroupCommands(program) {
384
391
  It controls deployment lifecycle, conflict resolution, and A/B testing.
385
392
 
386
393
  Commands:
387
- list List all policy groups
394
+ list List policy groups (deleted groups are not included)
388
395
  get Get group detail by ID
389
396
  create Create a new group
390
397
  update Update group settings
391
- reorder Change group priorities (drag & drag equivalent)
398
+ reorder Change group priorities (drag & drop equivalent)
392
399
  delete Archive a group
393
400
  ab-test Manage A/B tests (start, stop, adjust)
394
401
 
395
- Statuses: ACTIVE, DISABLED (emergency stop), ARCHIVED (soft delete)
402
+ Statuses: ACTIVE, DISABLED, ARCHIVED
403
+ DISABLED stops production execution (403 P-009) and keeps the deployed version and any A/B test.
404
+ Dry run, simulation, and replay still work. Set ACTIVE to resume.
405
+ ARCHIVED means deleted. Only 'lexq groups delete' sets it, and it cannot be undone.
396
406
  `
397
407
  );
398
- groups.command("list").description("List all policy groups").action(async () => {
408
+ groups.command("list").description("List policy groups (deleted groups are not included)").action(async () => {
399
409
  try {
400
410
  const globalOpts = program.opts();
401
411
  const format = globalOpts.format ?? "json";
@@ -490,6 +500,7 @@ ${data.length} total`);
490
500
  $ lexq groups update --id <groupId> --json '{"description": "Updated", "status": "DISABLED"}'
491
501
 
492
502
  All fields are optional — only provided fields are updated. priority is not settable here — use 'lexq groups reorder'.
503
+ A deleted group cannot be updated (409 P-059).
493
504
  `
494
505
  ).action(async (opts) => {
495
506
  try {
@@ -512,7 +523,15 @@ ${data.length} total`);
512
523
  "after",
513
524
  dedent3`
514
525
 
515
- This archives the group (soft delete). Use --force to skip the confirmation prompt.
526
+ A deployed group cannot be deleted (P-020). Undeploy it first, which also ends a running A/B test.
527
+
528
+ A deleted group no longer appears in 'lexq groups list', and its name can be reused right away.
529
+ It cannot be restored, and calls that change it (settings, versions, rules, starting an A/B
530
+ test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever
531
+ executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted
532
+ after 30 days.
533
+
534
+ Use --force to skip the confirmation prompt.
516
535
  `
517
536
  ).action(async (opts) => {
518
537
  try {
@@ -1801,7 +1820,7 @@ function registerDeployCommands(program) {
1801
1820
  data.content.map((d) => [
1802
1821
  d.id.substring(0, 8),
1803
1822
  d.deploymentType,
1804
- d.policyGroupName,
1823
+ groupLabel(d.policyGroupName, d.policyGroupArchivedAt),
1805
1824
  d.versionNo != null ? `v${d.versionNo}` : "\u2013",
1806
1825
  d.deployedByName,
1807
1826
  utcMinute(d.deployedAt)
@@ -2024,7 +2043,11 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2024
2043
  ["Status", "Group", "Version", "Scheduled For", "By", "Result"],
2025
2044
  data.content.map((s) => [
2026
2045
  s.status,
2027
- s.policyGroupName ?? s.policyGroupId.substring(0, 8),
2046
+ groupLabel(
2047
+ s.policyGroupName,
2048
+ s.policyGroupArchivedAt,
2049
+ s.policyGroupId.substring(0, 8)
2050
+ ),
2028
2051
  s.versionNo != null ? `v${s.versionNo}` : "\u2013",
2029
2052
  utcMinute(s.scheduledFor),
2030
2053
  s.scheduledByName,
@@ -2377,7 +2400,7 @@ function registerAnalyticsCommands(program) {
2377
2400
  ["ID", "Group", "Target", "Status", "Match", "Records", "Created"],
2378
2401
  data.content.map((s) => [
2379
2402
  s.simulationId.substring(0, 8),
2380
- s.policyGroupName,
2403
+ groupLabel(s.policyGroupName, s.policyGroupArchivedAt),
2381
2404
  s.targetVersionName ?? "\u2013",
2382
2405
  s.status,
2383
2406
  `${s.matchRate.toFixed(1)}%`,
@@ -2738,12 +2761,12 @@ function registerHistoryCommands(program) {
2738
2761
  ["Trace", "Group", "Version", "Status", "Matched", "Latency", "At"],
2739
2762
  data.content.map((h) => [
2740
2763
  h.traceId.substring(0, 12),
2741
- h.policyGroupName ?? "\u2013",
2764
+ groupLabel(h.policyGroupName, h.policyGroupArchivedAt),
2742
2765
  h.policyVersionNo != null ? `v${h.policyVersionNo}` : "\u2013",
2743
2766
  h.status,
2744
2767
  h.isMatched ? "\u2713" : "\u2717",
2745
2768
  `${h.latencyMs}ms`,
2746
- utcMinute(h.createdAt)
2769
+ utcMinute(h.executedAt)
2747
2770
  ]),
2748
2771
  { truncate: 20 }
2749
2772
  );
@@ -3252,8 +3275,8 @@ function registerWebhookSubscriptionCommands(program) {
3252
3275
  "after",
3253
3276
  dedent14`
3254
3277
 
3255
- Receive notifications when deployment lifecycle events occur
3256
- (publish, deploy, rollback, undeploy).
3278
+ Receive notifications for deployment lifecycle events: publish, deploy, rollback, and
3279
+ undeploy, plus a deployment being scheduled or a schedule being canceled.
3257
3280
 
3258
3281
  Commands:
3259
3282
  list List all webhook subscriptions
@@ -3262,8 +3285,8 @@ function registerWebhookSubscriptionCommands(program) {
3262
3285
  delete Delete a subscription
3263
3286
  test Send a test event to verify connectivity
3264
3287
 
3265
- This is separate from Integrations (rule action webhooks).
3266
- Webhook subscriptions are for platform-level event notifications.
3288
+ Rule actions never call external systems. These subscriptions are how LexQ pushes deployment
3289
+ events to you.
3267
3290
  `
3268
3291
  );
3269
3292
  webhooks.command("list").description("List webhook subscriptions").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
@@ -3348,7 +3371,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3348
3371
  Fields:
3349
3372
  name string Subscription name (required, unique per tenant)
3350
3373
  webhookUrl string Webhook endpoint URL (required)
3351
- subscribedEvents string[] VERSION_PUBLISHED, DEPLOYED, ROLLED_BACK, UNDEPLOYED
3374
+ subscribedEvents string[] ${PlatformEventType.join(", ")}
3352
3375
  payloadFormat string GENERIC (full JSON) or SLACK ({"text":"..."}) [default: GENERIC]
3353
3376
  secret string HMAC-SHA256 signing secret (optional)
3354
3377
  isActive boolean Enable/disable (optional) [default: true]
@@ -3562,7 +3585,7 @@ function registerGroupTools(server, callApi) {
3562
3585
  {
3563
3586
  title: "List Policy Groups",
3564
3587
  annotations: { readOnlyHint: true },
3565
- description: "List all policy groups (tenant-wide, priority ASC).",
3588
+ description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
3566
3589
  inputSchema: z2.object({})
3567
3590
  },
3568
3591
  async () => callApi("GET", "policy-groups")
@@ -3615,12 +3638,14 @@ function registerGroupTools(server, callApi) {
3615
3638
  {
3616
3639
  title: "Update Policy Group",
3617
3640
  annotations: { readOnlyHint: false, destructiveHint: true },
3618
- description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged.",
3641
+ description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
3619
3642
  inputSchema: z2.object({
3620
3643
  groupId: z2.string().uuid().describe("Policy group ID"),
3621
3644
  name: z2.string().optional().describe("New name"),
3622
3645
  description: z2.string().optional().describe("New description"),
3623
- status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe("Status (DISABLED = emergency stop)"),
3646
+ status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
3647
+ "DISABLED stops production execution of this group: execution calls return 403 (P-009), and a composite call that includes the group is rejected in full. The deployed version and any A/B test are kept, and dry run, Impact Simulation, and Decision Replay keep working, so a fix can be verified before setting ACTIVE again. To go back to the previous version, use lexq_deploy_rollback. To take the version out of production, use lexq_deploy_undeploy."
3648
+ ),
3624
3649
  activationGroup: z2.string().optional().describe(
3625
3650
  "Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
3626
3651
  ),
@@ -3638,7 +3663,7 @@ function registerGroupTools(server, callApi) {
3638
3663
  {
3639
3664
  title: "Delete Policy Group",
3640
3665
  annotations: { readOnlyHint: false, destructiveHint: true },
3641
- description: "Archive a policy group. Only non-live groups can be deleted. This is irreversible.",
3666
+ description: "Delete (archive) a policy group. A deployed group cannot be deleted (P-020); undeploy it first with lexq_deploy_undeploy, which also ends a running A/B test. This cannot be undone. The group no longer appears in lexq_groups_list and its name can be reused right away. Calls that change it (settings, versions, rules, starting an A/B test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted after 30 days.",
3642
3667
  inputSchema: z2.object({
3643
3668
  groupId: z2.string().uuid().describe("Policy group ID")
3644
3669
  })
@@ -4065,13 +4090,13 @@ function registerFactTools(server, callApi) {
4065
4090
  {
4066
4091
  title: "Update Fact Definition",
4067
4092
  annotations: { readOnlyHint: false, destructiveHint: true },
4068
- 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.",
4093
+ 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. Rules in deleted groups do not count. Only the fields you send are changed. System facts accept name, description, and PII only.",
4069
4094
  inputSchema: z5.object({
4070
4095
  factId: z5.string().uuid().describe("Fact definition ID"),
4071
4096
  name: z5.string().optional().describe("Display name"),
4072
4097
  description: z5.string().optional().describe("Description"),
4073
4098
  type: z5.enum(VALUE_TYPES).optional().describe(
4074
- "Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule references the fact."
4099
+ "Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
4075
4100
  ),
4076
4101
  isRequired: z5.boolean().optional().describe("Required flag"),
4077
4102
  isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
@@ -4093,7 +4118,7 @@ function registerFactTools(server, callApi) {
4093
4118
  {
4094
4119
  title: "Delete Fact Definition",
4095
4120
  annotations: { readOnlyHint: false, destructiveHint: true },
4096
- 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.",
4121
+ 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. Rules in deleted groups do not count. Remove the references first.",
4097
4122
  inputSchema: z5.object({
4098
4123
  factId: z5.string().uuid().describe("Fact definition ID")
4099
4124
  })
@@ -4405,7 +4430,8 @@ function registerAnalyticsTools(server, callApi) {
4405
4430
  UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
4406
4431
  MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
4407
4432
 
4408
- options.maxRecords: number (max 100000, default 10000)
4433
+ options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
4434
+ most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
4409
4435
  options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
4410
4436
  options.includeRuleStats: boolean
4411
4437
  options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
@@ -4885,7 +4911,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4885
4911
  {
4886
4912
  title: "List Webhook Subscriptions",
4887
4913
  annotations: { readOnlyHint: true },
4888
- description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications (publish, deploy, rollback, undeploy).",
4914
+ description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications: publish, deploy, rollback, and undeploy, plus a deployment being scheduled or a schedule being canceled.",
4889
4915
  inputSchema: z13.object({
4890
4916
  page: z13.number().int().min(0).default(0).describe("Page number"),
4891
4917
  size: z13.number().int().min(1).max(100).default(20).describe("Page size")
@@ -4913,7 +4939,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4913
4939
  {
4914
4940
  title: "Save Webhook Subscription",
4915
4941
  annotations: { readOnlyHint: false, destructiveHint: true },
4916
- description: 'Create or update a webhook subscription. Omit id to create, provide id to update. Events: VERSION_PUBLISHED, DEPLOYED, ROLLED_BACK, UNDEPLOYED. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).',
4942
+ description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).`,
4917
4943
  inputSchema: z13.object({
4918
4944
  id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
4919
4945
  name: z13.string().min(1).describe("Subscription name (unique per tenant)"),
@@ -37,6 +37,8 @@ var WebhookPayloadFormat = ["GENERIC", "SLACK"];
37
37
  // src/types/constants.ts
38
38
  var MAX_ROUNDING_SCALE = 34;
39
39
  var REPLAY_WINDOW_MAX_RECORDS = 5e4;
40
+ var DEFAULT_SIMULATION_RECORDS = 1e3;
41
+ var MAX_SIMULATION_RECORDS = 1e5;
40
42
  var PROFILE_SAMPLE_PERMILLE = 10;
41
43
  var LATENCY_WINDOW_MILLIS = 6e4;
42
44
  var TAIL_MIN_OBS = 3;
@@ -58,7 +60,7 @@ function registerGroupTools(server, callApi) {
58
60
  {
59
61
  title: "List Policy Groups",
60
62
  annotations: { readOnlyHint: true },
61
- description: "List all policy groups (tenant-wide, priority ASC).",
63
+ description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
62
64
  inputSchema: z2.object({})
63
65
  },
64
66
  async () => callApi("GET", "policy-groups")
@@ -111,12 +113,14 @@ function registerGroupTools(server, callApi) {
111
113
  {
112
114
  title: "Update Policy Group",
113
115
  annotations: { readOnlyHint: false, destructiveHint: true },
114
- description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged.",
116
+ description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
115
117
  inputSchema: z2.object({
116
118
  groupId: z2.string().uuid().describe("Policy group ID"),
117
119
  name: z2.string().optional().describe("New name"),
118
120
  description: z2.string().optional().describe("New description"),
119
- status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe("Status (DISABLED = emergency stop)"),
121
+ status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
122
+ "DISABLED stops production execution of this group: execution calls return 403 (P-009), and a composite call that includes the group is rejected in full. The deployed version and any A/B test are kept, and dry run, Impact Simulation, and Decision Replay keep working, so a fix can be verified before setting ACTIVE again. To go back to the previous version, use lexq_deploy_rollback. To take the version out of production, use lexq_deploy_undeploy."
123
+ ),
120
124
  activationGroup: z2.string().optional().describe(
121
125
  "Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
122
126
  ),
@@ -134,7 +138,7 @@ function registerGroupTools(server, callApi) {
134
138
  {
135
139
  title: "Delete Policy Group",
136
140
  annotations: { readOnlyHint: false, destructiveHint: true },
137
- description: "Archive a policy group. Only non-live groups can be deleted. This is irreversible.",
141
+ description: "Delete (archive) a policy group. A deployed group cannot be deleted (P-020); undeploy it first with lexq_deploy_undeploy, which also ends a running A/B test. This cannot be undone. The group no longer appears in lexq_groups_list and its name can be reused right away. Calls that change it (settings, versions, rules, starting an A/B test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted after 30 days.",
138
142
  inputSchema: z2.object({
139
143
  groupId: z2.string().uuid().describe("Policy group ID")
140
144
  })
@@ -634,13 +638,13 @@ function registerFactTools(server, callApi) {
634
638
  {
635
639
  title: "Update Fact Definition",
636
640
  annotations: { readOnlyHint: false, destructiveHint: true },
637
- 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.",
641
+ 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. Rules in deleted groups do not count. Only the fields you send are changed. System facts accept name, description, and PII only.",
638
642
  inputSchema: z5.object({
639
643
  factId: z5.string().uuid().describe("Fact definition ID"),
640
644
  name: z5.string().optional().describe("Display name"),
641
645
  description: z5.string().optional().describe("Description"),
642
646
  type: z5.enum(VALUE_TYPES).optional().describe(
643
- "Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule references the fact."
647
+ "Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
644
648
  ),
645
649
  isRequired: z5.boolean().optional().describe("Required flag"),
646
650
  isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
@@ -662,7 +666,7 @@ function registerFactTools(server, callApi) {
662
666
  {
663
667
  title: "Delete Fact Definition",
664
668
  annotations: { readOnlyHint: false, destructiveHint: true },
665
- 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.",
669
+ 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. Rules in deleted groups do not count. Remove the references first.",
666
670
  inputSchema: z5.object({
667
671
  factId: z5.string().uuid().describe("Fact definition ID")
668
672
  })
@@ -974,7 +978,8 @@ function registerAnalyticsTools(server, callApi) {
974
978
  UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
975
979
  MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
976
980
 
977
- options.maxRecords: number (max 100000, default 10000)
981
+ options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
982
+ most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
978
983
  options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
979
984
  options.includeRuleStats: boolean
980
985
  options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
@@ -1454,7 +1459,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
1454
1459
  {
1455
1460
  title: "List Webhook Subscriptions",
1456
1461
  annotations: { readOnlyHint: true },
1457
- description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications (publish, deploy, rollback, undeploy).",
1462
+ description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications: publish, deploy, rollback, and undeploy, plus a deployment being scheduled or a schedule being canceled.",
1458
1463
  inputSchema: z13.object({
1459
1464
  page: z13.number().int().min(0).default(0).describe("Page number"),
1460
1465
  size: z13.number().int().min(1).max(100).default(20).describe("Page size")
@@ -1482,7 +1487,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
1482
1487
  {
1483
1488
  title: "Save Webhook Subscription",
1484
1489
  annotations: { readOnlyHint: false, destructiveHint: true },
1485
- description: 'Create or update a webhook subscription. Omit id to create, provide id to update. Events: VERSION_PUBLISHED, DEPLOYED, ROLLED_BACK, UNDEPLOYED. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).',
1490
+ description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).`,
1486
1491
  inputSchema: z13.object({
1487
1492
  id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
1488
1493
  name: z13.string().min(1).describe("Subscription name (unique per tenant)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "mcpName": "io.lexq/lexq",
5
5
  "description": "LexQ CLI — manage policies, simulate rules, and deploy from the terminal. Built for humans and AI agents.",
6
6
  "type": "module",
@@ -17,9 +17,24 @@ controls conflict resolution when multiple groups interact.
17
17
  | `DISABLED` | `ARCHIVED` | **no** |
18
18
 
19
19
  - **ACTIVE** — executions are processed.
20
- - **DISABLED** — all executions are blocked. Use for emergency stop.
20
+ - **DISABLED** — production execution stops. Execution calls return `P-009` (403), and a composite call that includes
21
+ the group is rejected in full. The live version and any A/B test are kept, and dry run, Impact Simulation, and
22
+ Decision Replay keep working, so you can verify a fix before setting `ACTIVE` again.
21
23
  - **ARCHIVED** — removed from execution. Reached only through `lexq groups delete`, not by setting `status` directly.
22
24
 
25
+ ### Disable, Roll Back, or Undeploy?
26
+
27
+ | Situation | Use | Callers get |
28
+ |-----------------------------------------------------------------------|------------------------|------------------------------------------------------------------------|
29
+ | The previously deployed version is correct | `lexq deploy rollback` | Decisions from the previous version |
30
+ | This version has to leave production | `lexq deploy undeploy` | `P-019` (400). Composite calls skip the group. A running A/B test ends |
31
+ | Stop now without knowing the cause, then resume with the same version | `status: DISABLED` | `P-009` (403). Composite calls that include the group are rejected |
32
+
33
+ Do not disable a group for a planned end, such as a promotion with a known end date. Set the version's `effectiveTo`
34
+ instead. When that time passes, the group switches to its newest `ACTIVE` version that is still in effect, or is
35
+ undeployed if there is none. Composite calls keep working either way. A disabled group rejects every composite call
36
+ that includes it.
37
+
23
38
  ## CRUD Commands
24
39
 
25
40
  ### List Groups
@@ -93,20 +108,31 @@ lexq groups delete --id <groupId>
93
108
  lexq groups delete --id <groupId> --force
94
109
  ```
95
110
 
96
- **`delete` archives — it does not erase.** The group becomes `ARCHIVED` and stops executing;
97
- versions, rules, and history are retained for audit. Deleting an already-archived group is a
98
- no-op.
111
+ **`delete` archives — it does not erase right away.** The group becomes `ARCHIVED` and stops
112
+ executing. If it was ever executed, deployed, or replayed, it is kept for audit, along with its
113
+ versions and rules. Otherwise it is permanently deleted after 30 days, together with its
114
+ versions, rules, simulations, and schedules. Deleting an already-archived group is a no-op.
99
115
 
100
- Two conditions block it:
101
-
102
- | Condition | What to do first |
103
- |------------------------|-----------------------------------------------------|
104
- | The group is deployed | `lexq deploy undeploy --group-id <gid>` |
105
- | An A/B test is running | `lexq groups ab-test stop --group-id <gid> --force` |
116
+ A deployed group cannot be deleted (`P-020`). Undeploy it first with
117
+ `lexq deploy undeploy --group-id <gid> --memo "<reason>"`, which also ends a running A/B test.
106
118
 
107
119
  Archiving cancels any pending scheduled deployment and closes the priority gap — remaining groups
108
120
  are renumbered 1…N.
109
121
 
122
+ A deleted group no longer appears in `lexq groups list` and cannot be restored or changed.
123
+ Changing its settings, versions, or rules, starting an A/B test, and publishing, deploying,
124
+ rolling back, or scheduling a deployment all return `409` `P-059`. To start over, create a new
125
+ group.
126
+
127
+ **The name can be reused right away.** Records keep the group's ID and the name it had, so after
128
+ a new group takes the name, deployment history, scheduled deployments, execution history,
129
+ simulations, and replays can list two groups under that name. These records carry
130
+ `policyGroupArchivedAt`, the time the group was deleted (`null` if it has not been). In the
131
+ tables of `lexq deploy history`, `deploy schedules`, `history list`, and
132
+ `analytics simulation list` (`--format table`), a deleted group's name shows as
133
+ `(deleted) <name>`. To see one group's deployments or executions only, pass its ID to
134
+ `lexq deploy history --group-id` or `lexq history list --group-id`.
135
+
110
136
  ## Conflict Resolution
111
137
 
112
138
  ### Activation Mode (across groups in the same `activationGroup`)
@@ -246,4 +272,5 @@ lets you reuse the existing group instead of handling the error.
246
272
  lexq groups update --id <groupId> --json '{"status": "DISABLED"}'
247
273
  ```
248
274
 
249
- This immediately stops all executions for the group. No undeploy needed.
275
+ This immediately stops production execution for the group. Dry run, Impact Simulation, and Decision Replay keep
276
+ working. No undeploy needed.
@@ -19,6 +19,9 @@ lexq auth login
19
19
  lexq auth whoami
20
20
  ```
21
21
 
22
+ Create the key with the **Manage** scope in the Console (Management → API Keys). An **Execute only** key is
23
+ for calling the execution API from your application and does not work with this CLI.
24
+
22
25
  The API key is passed as `X-API-KEY` header on every request. You can also override per-command:
23
26
 
24
27
  ```bash