@lexq/cli 1.1.1 → 1.1.3

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
@@ -280,7 +280,7 @@ pnpm enums:check # Generated enums match the engine contract
280
280
  pnpm prose:check # Numbers and code tables in prose match the contract
281
281
  pnpm ab-key:check # Tool descriptions still name the A/B traffic key
282
282
  pnpm surface:check # Nothing internal reached a public file
283
- pnpm registry:check # server.json agrees with package.json
283
+ pnpm registry:check # server.json agrees with package.json and src/mcp/server-info.json
284
284
  pnpm schemas:check # tools/list matches the recorded snapshot (needs a build)
285
285
  pnpm test:fact-key # Fact key grammar
286
286
  pnpm test:decimals # Metric precision display
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]
@@ -3539,6 +3562,20 @@ function formatUnregisteredFactWarning(meta) {
3539
3562
  ].join("\n");
3540
3563
  }
3541
3564
 
3565
+ // src/mcp/server-info.json
3566
+ var server_info_default = {
3567
+ title: "LexQ",
3568
+ description: "The Decision Operations Platform for engineering teams",
3569
+ websiteUrl: "https://lexq.io",
3570
+ icons: [
3571
+ {
3572
+ src: "https://lexq.io/logo.png",
3573
+ mimeType: "image/png",
3574
+ sizes: ["1024x1024"]
3575
+ }
3576
+ ]
3577
+ };
3578
+
3542
3579
  // src/mcp/tools/status.ts
3543
3580
  import { z } from "zod";
3544
3581
  function registerStatusTools(server, callApi) {
@@ -3562,7 +3599,7 @@ function registerGroupTools(server, callApi) {
3562
3599
  {
3563
3600
  title: "List Policy Groups",
3564
3601
  annotations: { readOnlyHint: true },
3565
- description: "List all policy groups (tenant-wide, priority ASC).",
3602
+ description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
3566
3603
  inputSchema: z2.object({})
3567
3604
  },
3568
3605
  async () => callApi("GET", "policy-groups")
@@ -3615,12 +3652,14 @@ function registerGroupTools(server, callApi) {
3615
3652
  {
3616
3653
  title: "Update Policy Group",
3617
3654
  annotations: { readOnlyHint: false, destructiveHint: true },
3618
- description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged.",
3655
+ description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
3619
3656
  inputSchema: z2.object({
3620
3657
  groupId: z2.string().uuid().describe("Policy group ID"),
3621
3658
  name: z2.string().optional().describe("New name"),
3622
3659
  description: z2.string().optional().describe("New description"),
3623
- status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe("Status (DISABLED = emergency stop)"),
3660
+ status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
3661
+ "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."
3662
+ ),
3624
3663
  activationGroup: z2.string().optional().describe(
3625
3664
  "Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
3626
3665
  ),
@@ -3638,7 +3677,7 @@ function registerGroupTools(server, callApi) {
3638
3677
  {
3639
3678
  title: "Delete Policy Group",
3640
3679
  annotations: { readOnlyHint: false, destructiveHint: true },
3641
- description: "Archive a policy group. Only non-live groups can be deleted. This is irreversible.",
3680
+ 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
3681
  inputSchema: z2.object({
3643
3682
  groupId: z2.string().uuid().describe("Policy group ID")
3644
3683
  })
@@ -4065,13 +4104,13 @@ function registerFactTools(server, callApi) {
4065
4104
  {
4066
4105
  title: "Update Fact Definition",
4067
4106
  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.",
4107
+ 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
4108
  inputSchema: z5.object({
4070
4109
  factId: z5.string().uuid().describe("Fact definition ID"),
4071
4110
  name: z5.string().optional().describe("Display name"),
4072
4111
  description: z5.string().optional().describe("Description"),
4073
4112
  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."
4113
+ "Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
4075
4114
  ),
4076
4115
  isRequired: z5.boolean().optional().describe("Required flag"),
4077
4116
  isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
@@ -4093,7 +4132,7 @@ function registerFactTools(server, callApi) {
4093
4132
  {
4094
4133
  title: "Delete Fact Definition",
4095
4134
  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.",
4135
+ 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
4136
  inputSchema: z5.object({
4098
4137
  factId: z5.string().uuid().describe("Fact definition ID")
4099
4138
  })
@@ -4405,7 +4444,8 @@ function registerAnalyticsTools(server, callApi) {
4405
4444
  UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
4406
4445
  MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
4407
4446
 
4408
- options.maxRecords: number (max 100000, default 10000)
4447
+ options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
4448
+ most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
4409
4449
  options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
4410
4450
  options.includeRuleStats: boolean
4411
4451
  options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
@@ -4885,7 +4925,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4885
4925
  {
4886
4926
  title: "List Webhook Subscriptions",
4887
4927
  annotations: { readOnlyHint: true },
4888
- description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications (publish, deploy, rollback, undeploy).",
4928
+ 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
4929
  inputSchema: z13.object({
4890
4930
  page: z13.number().int().min(0).default(0).describe("Page number"),
4891
4931
  size: z13.number().int().min(1).max(100).default(20).describe("Page size")
@@ -4913,7 +4953,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4913
4953
  {
4914
4954
  title: "Save Webhook Subscription",
4915
4955
  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": "..."}).',
4956
+ 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
4957
  inputSchema: z13.object({
4918
4958
  id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
4919
4959
  name: z13.string().min(1).describe("Subscription name (unique per tenant)"),
@@ -5017,6 +5057,7 @@ function registerAllTools(server, callApi) {
5017
5057
  registerDomainTemplateTools(server, callApi);
5018
5058
  registerWebhookSubscriptionTools(server, callApi);
5019
5059
  }
5060
+ var SERVER_INFO = { name: "lexq", ...server_info_default };
5020
5061
 
5021
5062
  // src/mcp/server.ts
5022
5063
  var __dirname = dirname(fileURLToPath(import.meta.url));
@@ -5033,7 +5074,7 @@ function startMcpServer() {
5033
5074
  const callApi = createCallApiFromConfig();
5034
5075
  serveStdio(
5035
5076
  () => {
5036
- const server = new McpServer({ name: "lexq", version });
5077
+ const server = new McpServer({ ...SERVER_INFO, version });
5037
5078
  registerAllTools(server, callApi);
5038
5079
  return server;
5039
5080
  },
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/server';
1
+ import { Implementation, McpServer } from '@modelcontextprotocol/server';
2
2
  import { LosslessNumber } from 'lossless-json';
3
3
  export { LosslessNumber } from 'lossless-json';
4
4
 
@@ -63,5 +63,14 @@ declare const isLosslessNumber: (value: unknown) => value is LosslessNumber;
63
63
  * @param callApi - API caller function (config-based for CLI, Bearer-based for HTTP)
64
64
  */
65
65
  declare function registerAllTools(server: McpServer, callApi: CallApi): void;
66
+ /**
67
+ * How the server introduces itself when a client connects: a title, a description and an
68
+ * icon the client can show, and a link to its website. A host serving these tools adds its
69
+ * own version.
70
+ *
71
+ * The values are kept in server-info.json rather than here because server.json lists them
72
+ * again for the MCP Registry, and `pnpm registry:check` has to read both without a build.
73
+ */
74
+ declare const SERVER_INFO: Omit<Implementation, 'version'>;
66
75
 
67
- export { ApiError, type CallApi, type McpToolResult, formatUnregisteredFactWarning, isLosslessNumber, paginationParams, parseJson, registerAllTools, stringifyJson };
76
+ export { ApiError, type CallApi, type McpToolResult, SERVER_INFO, formatUnregisteredFactWarning, isLosslessNumber, paginationParams, parseJson, registerAllTools, stringifyJson };
@@ -1,3 +1,17 @@
1
+ // src/mcp/server-info.json
2
+ var server_info_default = {
3
+ title: "LexQ",
4
+ description: "The Decision Operations Platform for engineering teams",
5
+ websiteUrl: "https://lexq.io",
6
+ icons: [
7
+ {
8
+ src: "https://lexq.io/logo.png",
9
+ mimeType: "image/png",
10
+ sizes: ["1024x1024"]
11
+ }
12
+ ]
13
+ };
14
+
1
15
  // src/mcp/tools/status.ts
2
16
  import { z } from "zod";
3
17
  function registerStatusTools(server, callApi) {
@@ -37,6 +51,8 @@ var WebhookPayloadFormat = ["GENERIC", "SLACK"];
37
51
  // src/types/constants.ts
38
52
  var MAX_ROUNDING_SCALE = 34;
39
53
  var REPLAY_WINDOW_MAX_RECORDS = 5e4;
54
+ var DEFAULT_SIMULATION_RECORDS = 1e3;
55
+ var MAX_SIMULATION_RECORDS = 1e5;
40
56
  var PROFILE_SAMPLE_PERMILLE = 10;
41
57
  var LATENCY_WINDOW_MILLIS = 6e4;
42
58
  var TAIL_MIN_OBS = 3;
@@ -58,7 +74,7 @@ function registerGroupTools(server, callApi) {
58
74
  {
59
75
  title: "List Policy Groups",
60
76
  annotations: { readOnlyHint: true },
61
- description: "List all policy groups (tenant-wide, priority ASC).",
77
+ description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
62
78
  inputSchema: z2.object({})
63
79
  },
64
80
  async () => callApi("GET", "policy-groups")
@@ -111,12 +127,14 @@ function registerGroupTools(server, callApi) {
111
127
  {
112
128
  title: "Update Policy Group",
113
129
  annotations: { readOnlyHint: false, destructiveHint: true },
114
- description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged.",
130
+ description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
115
131
  inputSchema: z2.object({
116
132
  groupId: z2.string().uuid().describe("Policy group ID"),
117
133
  name: z2.string().optional().describe("New name"),
118
134
  description: z2.string().optional().describe("New description"),
119
- status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe("Status (DISABLED = emergency stop)"),
135
+ status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
136
+ "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."
137
+ ),
120
138
  activationGroup: z2.string().optional().describe(
121
139
  "Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
122
140
  ),
@@ -134,7 +152,7 @@ function registerGroupTools(server, callApi) {
134
152
  {
135
153
  title: "Delete Policy Group",
136
154
  annotations: { readOnlyHint: false, destructiveHint: true },
137
- description: "Archive a policy group. Only non-live groups can be deleted. This is irreversible.",
155
+ 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
156
  inputSchema: z2.object({
139
157
  groupId: z2.string().uuid().describe("Policy group ID")
140
158
  })
@@ -634,13 +652,13 @@ function registerFactTools(server, callApi) {
634
652
  {
635
653
  title: "Update Fact Definition",
636
654
  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.",
655
+ 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
656
  inputSchema: z5.object({
639
657
  factId: z5.string().uuid().describe("Fact definition ID"),
640
658
  name: z5.string().optional().describe("Display name"),
641
659
  description: z5.string().optional().describe("Description"),
642
660
  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."
661
+ "Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
644
662
  ),
645
663
  isRequired: z5.boolean().optional().describe("Required flag"),
646
664
  isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
@@ -662,7 +680,7 @@ function registerFactTools(server, callApi) {
662
680
  {
663
681
  title: "Delete Fact Definition",
664
682
  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.",
683
+ 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
684
  inputSchema: z5.object({
667
685
  factId: z5.string().uuid().describe("Fact definition ID")
668
686
  })
@@ -974,7 +992,8 @@ function registerAnalyticsTools(server, callApi) {
974
992
  UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
975
993
  MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
976
994
 
977
- options.maxRecords: number (max 100000, default 10000)
995
+ options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
996
+ most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
978
997
  options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
979
998
  options.includeRuleStats: boolean
980
999
  options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
@@ -1454,7 +1473,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
1454
1473
  {
1455
1474
  title: "List Webhook Subscriptions",
1456
1475
  annotations: { readOnlyHint: true },
1457
- description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications (publish, deploy, rollback, undeploy).",
1476
+ 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
1477
  inputSchema: z13.object({
1459
1478
  page: z13.number().int().min(0).default(0).describe("Page number"),
1460
1479
  size: z13.number().int().min(1).max(100).default(20).describe("Page size")
@@ -1482,7 +1501,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
1482
1501
  {
1483
1502
  title: "Save Webhook Subscription",
1484
1503
  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": "..."}).',
1504
+ 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
1505
  inputSchema: z13.object({
1487
1506
  id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
1488
1507
  name: z13.string().min(1).describe("Subscription name (unique per tenant)"),
@@ -1586,9 +1605,11 @@ function registerAllTools(server, callApi) {
1586
1605
  registerDomainTemplateTools(server, callApi);
1587
1606
  registerWebhookSubscriptionTools(server, callApi);
1588
1607
  }
1608
+ var SERVER_INFO = { name: "lexq", ...server_info_default };
1589
1609
  export {
1590
1610
  ApiError,
1591
1611
  LosslessNumber,
1612
+ SERVER_INFO,
1592
1613
  formatUnregisteredFactWarning,
1593
1614
  isLosslessNumber,
1594
1615
  paginationParams,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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