@lexq/cli 1.1.6 → 2.0.0

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/dist/index.js CHANGED
@@ -608,7 +608,10 @@ ${data.length} total`);
608
608
  The remaining traffic continues to the current live version.
609
609
  `
610
610
  );
611
- abTest.command("start").description("Start an A/B test").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Challenger version ID").requiredOption(
611
+ abTest.command("start").description("Start an A/B test").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption(
612
+ "--version-id <versionId>",
613
+ "Challenger version ID (a published version of this group)"
614
+ ).requiredOption(
612
615
  "--traffic-rate <rate>",
613
616
  `Traffic rate for challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
614
617
  ).addHelpText(
@@ -1770,7 +1773,13 @@ function registerDeployCommands(program) {
1770
1773
  process.exit(1);
1771
1774
  }
1772
1775
  });
1773
- deploy.command("rollback").description("Rollback to the previous deployed version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--memo <memo>", "Rollback reason").option("--force", "Skip confirmation prompt").addHelpText(
1776
+ deploy.command("rollback").description("Rollback to the previous deployed version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--memo <memo>", "Rollback reason").option(
1777
+ "--expected-current-version-id <versionId>",
1778
+ "currentVersionId from your last read of the group"
1779
+ ).option(
1780
+ "--expected-current-deployment-id <deploymentId>",
1781
+ "currentDeploymentId from your last read of the group"
1782
+ ).option("--force", "Skip confirmation prompt").addHelpText(
1774
1783
  "after",
1775
1784
  dedent7`
1776
1785
 
@@ -1778,8 +1787,20 @@ function registerDeployCommands(program) {
1778
1787
  Only available if the previous version is still ACTIVE and its effective end
1779
1788
  has not passed (P-058).
1780
1789
 
1790
+ Run lexq groups get first: currentVersionName is live now and
1791
+ previousVersionName is what a rollback restores. Pass the group's
1792
+ currentVersionId and currentDeploymentId with the two --expected-* options.
1793
+ The server checks only the values you pass. If anyone deployed, rolled back
1794
+ or undeployed in between, the rollback is rejected with 409 and nothing
1795
+ changes: P-062 if another version or no version is live, P-063 if the same
1796
+ version is live but from a newer deployment. Read the group again before
1797
+ retrying.
1798
+
1781
1799
  Example:
1782
- $ lexq deploy rollback --group-id <gid> --memo "High error rate" --force
1800
+ $ lexq groups get --id <gid>
1801
+ $ lexq deploy rollback --group-id <gid> --memo "High error rate" \
1802
+ --expected-current-version-id <currentVersionId> \
1803
+ --expected-current-deployment-id <currentDeploymentId> --force
1783
1804
  `
1784
1805
  ).action(async (opts) => {
1785
1806
  try {
@@ -1799,7 +1820,11 @@ function registerDeployCommands(program) {
1799
1820
  baseUrl: globalOpts.baseUrl,
1800
1821
  dryRun: globalOpts.dryRun,
1801
1822
  verbose: globalOpts.verbose,
1802
- body: { memo: opts.memo }
1823
+ body: {
1824
+ memo: opts.memo,
1825
+ expectedCurrentVersionId: opts.expectedCurrentVersionId,
1826
+ expectedCurrentDeploymentId: opts.expectedCurrentDeploymentId
1827
+ }
1803
1828
  });
1804
1829
  console.log(`\u2713 Group ${opts.groupId} rolled back.`);
1805
1830
  } catch (error) {
@@ -2156,7 +2181,7 @@ function registerAnalyticsCommands(program) {
2156
2181
  dedent8`
2157
2182
 
2158
2183
  Examples:
2159
- $ lexq analytics dry-run --version-id <vid> --debug \\
2184
+ $ lexq analytics dry-run --version-id <vid> --debug \
2160
2185
  --json '{"facts": {"paymentAmount": 150000, "customerTier": "VIP"}}'
2161
2186
 
2162
2187
  $ lexq analytics dry-run --version-id <vid> --file test-input.json
@@ -3523,7 +3548,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3523
3548
 
3524
3549
  // src/commands/serve.ts
3525
3550
  import "commander";
3526
- import dedent17 from "dedent";
3551
+ import dedent18 from "dedent";
3527
3552
 
3528
3553
  // src/mcp/server.ts
3529
3554
  import { readFileSync as readFileSync3 } from "fs";
@@ -3662,7 +3687,7 @@ function registerGroupTools(server, callApi) {
3662
3687
  {
3663
3688
  title: "Get Policy Group",
3664
3689
  annotations: { readOnlyHint: true },
3665
- description: "Get a single policy group by ID.",
3690
+ description: "Get a single policy group by ID. Its currentVersionId and currentDeploymentId are what lexq_deploy_rollback expects, and previousVersionName is the version a rollback restores.",
3666
3691
  inputSchema: z2.object({
3667
3692
  groupId: z2.string().uuid().describe("Policy group ID")
3668
3693
  })
@@ -3760,10 +3785,10 @@ function registerGroupTools(server, callApi) {
3760
3785
  {
3761
3786
  title: "Start A/B Test",
3762
3787
  annotations: { readOnlyHint: false, destructiveHint: false },
3763
- 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%.",
3788
+ description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. The challenger must be a published version of the same group; a version from another group is rejected with P-016. 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%.",
3764
3789
  inputSchema: z2.object({
3765
3790
  groupId: z2.string().uuid().describe("Policy group ID"),
3766
- testVersionId: z2.string().uuid().describe("Challenger version ID to test"),
3791
+ testVersionId: z2.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
3767
3792
  trafficRate: z2.number().int().min(1).max(99).describe("Traffic percentage routed to challenger (1-99)")
3768
3793
  })
3769
3794
  },
@@ -4243,6 +4268,7 @@ function registerFactTools(server, callApi) {
4243
4268
  }
4244
4269
 
4245
4270
  // src/mcp/tools/deploy.ts
4271
+ import dedent16 from "dedent";
4246
4272
  import { z as z6 } from "zod";
4247
4273
  function registerDeployTools(server, callApi) {
4248
4274
  server.registerTool(
@@ -4280,14 +4306,20 @@ function registerDeployTools(server, callApi) {
4280
4306
  {
4281
4307
  title: "Rollback Deployment",
4282
4308
  annotations: { readOnlyHint: false, destructiveHint: true },
4283
- description: "Rollback to the previous deployed version. Only available if there is a previous version that is still ACTIVE and whose effective end has not passed (P-058).",
4309
+ description: dedent16`
4310
+ Roll the group back to the version that was live before the current one. Call lexq_groups_get first: currentVersionName is live now, previousVersionName is what a rollback restores, and a rollback is rejected while canRollback is false. A rollback needs a previous version that is still ACTIVE and whose effective end has not passed (P-058).
4311
+
4312
+ Pass the currentVersionId and currentDeploymentId from that call as expectedCurrentVersionId and expectedCurrentDeploymentId. If anyone deployed, rolled back or undeployed in between, the rollback is rejected with 409 and nothing changes: P-062 if another version or no version is live, P-063 if the same version is live but from a newer deployment. Read the group again and check with the user before retrying.
4313
+ `,
4284
4314
  inputSchema: z6.object({
4285
4315
  groupId: z6.string().uuid().describe("Policy group ID"),
4316
+ expectedCurrentVersionId: z6.string().uuid().describe("currentVersionId from your last read of the group"),
4317
+ expectedCurrentDeploymentId: z6.string().uuid().describe("currentDeploymentId from your last read of the group"),
4286
4318
  memo: z6.string().min(1).describe("Rollback reason (required)")
4287
4319
  })
4288
4320
  },
4289
- async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
4290
- body: { memo }
4321
+ async ({ groupId, expectedCurrentVersionId, expectedCurrentDeploymentId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
4322
+ body: { memo, expectedCurrentVersionId, expectedCurrentDeploymentId }
4291
4323
  })
4292
4324
  );
4293
4325
  server.registerTool(
@@ -4387,7 +4419,7 @@ function registerDeployTools(server, callApi) {
4387
4419
  {
4388
4420
  title: "Deployment Overview",
4389
4421
  annotations: { readOnlyHint: true },
4390
- description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer.",
4422
+ description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer. Each group also carries currentDeploymentId, previousVersionName (the version a rollback restores) and canRollback, as in lexq_groups_get.",
4391
4423
  inputSchema: z6.object({})
4392
4424
  },
4393
4425
  async () => callApi("GET", "deployments/overview")
@@ -4423,14 +4455,14 @@ function registerDeployTools(server, callApi) {
4423
4455
 
4424
4456
  // src/mcp/tools/analytics.ts
4425
4457
  import { z as z7 } from "zod";
4426
- import dedent16 from "dedent";
4458
+ import dedent17 from "dedent";
4427
4459
  function registerAnalyticsTools(server, callApi) {
4428
4460
  server.registerTool(
4429
4461
  "lexq_dry_run",
4430
4462
  {
4431
4463
  title: "Dry Run",
4432
4464
  annotations: { readOnlyHint: true },
4433
- description: dedent16`
4465
+ description: dedent17`
4434
4466
  Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
4435
4467
 
4436
4468
  Returns:
@@ -4460,7 +4492,7 @@ function registerAnalyticsTools(server, callApi) {
4460
4492
  {
4461
4493
  title: "Dry Run Compare",
4462
4494
  annotations: { readOnlyHint: true },
4463
- description: dedent16`
4495
+ description: dedent17`
4464
4496
  Compare dry run results between two versions using the same input facts. Useful for validating changes.
4465
4497
 
4466
4498
  Returns:
@@ -4498,7 +4530,7 @@ function registerAnalyticsTools(server, callApi) {
4498
4530
  {
4499
4531
  title: "Start Simulation",
4500
4532
  annotations: { readOnlyHint: false, destructiveHint: false },
4501
- description: dedent16`
4533
+ description: dedent17`
4502
4534
  Start an Impact Simulation against historical, uploaded, or inline data.
4503
4535
 
4504
4536
  dataset.type and dataset.source are BOTH required, and must be paired:
@@ -4603,7 +4635,7 @@ function registerAnalyticsTools(server, callApi) {
4603
4635
  {
4604
4636
  title: "Upload Dataset",
4605
4637
  annotations: { readOnlyHint: false, destructiveHint: false },
4606
- description: dedent16`
4638
+ description: dedent17`
4607
4639
  Upload inline CSV or JSON content as a simulation dataset.
4608
4640
  The content is uploaded to S3 and a path is returned in the "path" field.
4609
4641
 
@@ -5156,7 +5188,7 @@ function startMcpServer() {
5156
5188
  function registerServeCommand(program) {
5157
5189
  program.command("serve").description("Start LexQ as a server for AI agent integrations").option("--mcp", "Start as MCP (Model Context Protocol) server over stdio").addHelpText(
5158
5190
  "after",
5159
- dedent17`
5191
+ dedent18`
5160
5192
 
5161
5193
  Example:
5162
5194
  $ lexq serve --mcp
@@ -5187,11 +5219,11 @@ function registerServeCommand(program) {
5187
5219
 
5188
5220
  // src/commands/domain-templates.ts
5189
5221
  import "commander";
5190
- import dedent18 from "dedent";
5222
+ import dedent19 from "dedent";
5191
5223
  function registerDomainTemplateCommands(program) {
5192
5224
  const templates = program.command("domain-templates").description("Browse and apply domain templates").addHelpText(
5193
5225
  "after",
5194
- dedent18`
5226
+ dedent19`
5195
5227
 
5196
5228
  A domain template is an industry-specific starter pack of fact
5197
5229
  definitions and sample rules. Applying one provisions a ready-to-use
@@ -5239,7 +5271,7 @@ function registerDomainTemplateCommands(program) {
5239
5271
  });
5240
5272
  templates.command("preview").description("Preview what a domain template provisions").requiredOption("--template <key>", "Domain template key (e.g. ECOMMERCE)").addHelpText(
5241
5273
  "after",
5242
- dedent18`
5274
+ dedent19`
5243
5275
 
5244
5276
  Read-only dry run — shows the fact definitions and sample rules the
5245
5277
  template will create. Nothing is provisioned.
@@ -5268,7 +5300,7 @@ function registerDomainTemplateCommands(program) {
5268
5300
  });
5269
5301
  templates.command("apply").description("Apply a domain template to the current tenant").requiredOption("--template <key>", "Domain template key (e.g. ECOMMERCE)").option("--name <n>", "Custom name for the policy group that gets created").option("--force", "Skip confirmation prompt").addHelpText(
5270
5302
  "after",
5271
- dedent18`
5303
+ dedent19`
5272
5304
 
5273
5305
  Creates the template's fact definitions and a new DRAFT policy group
5274
5306
  populated with its sample rules. A fact that already exists with the
@@ -84,7 +84,7 @@ function registerGroupTools(server, callApi) {
84
84
  {
85
85
  title: "Get Policy Group",
86
86
  annotations: { readOnlyHint: true },
87
- description: "Get a single policy group by ID.",
87
+ description: "Get a single policy group by ID. Its currentVersionId and currentDeploymentId are what lexq_deploy_rollback expects, and previousVersionName is the version a rollback restores.",
88
88
  inputSchema: z2.object({
89
89
  groupId: z2.string().uuid().describe("Policy group ID")
90
90
  })
@@ -182,10 +182,10 @@ function registerGroupTools(server, callApi) {
182
182
  {
183
183
  title: "Start A/B Test",
184
184
  annotations: { readOnlyHint: false, destructiveHint: false },
185
- 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%.",
185
+ description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. The challenger must be a published version of the same group; a version from another group is rejected with P-016. 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%.",
186
186
  inputSchema: z2.object({
187
187
  groupId: z2.string().uuid().describe("Policy group ID"),
188
- testVersionId: z2.string().uuid().describe("Challenger version ID to test"),
188
+ testVersionId: z2.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
189
189
  trafficRate: z2.number().int().min(1).max(99).describe("Traffic percentage routed to challenger (1-99)")
190
190
  })
191
191
  },
@@ -778,6 +778,7 @@ function registerFactTools(server, callApi) {
778
778
  }
779
779
 
780
780
  // src/mcp/tools/deploy.ts
781
+ import dedent2 from "dedent";
781
782
  import { z as z6 } from "zod";
782
783
  function registerDeployTools(server, callApi) {
783
784
  server.registerTool(
@@ -815,14 +816,20 @@ function registerDeployTools(server, callApi) {
815
816
  {
816
817
  title: "Rollback Deployment",
817
818
  annotations: { readOnlyHint: false, destructiveHint: true },
818
- description: "Rollback to the previous deployed version. Only available if there is a previous version that is still ACTIVE and whose effective end has not passed (P-058).",
819
+ description: dedent2`
820
+ Roll the group back to the version that was live before the current one. Call lexq_groups_get first: currentVersionName is live now, previousVersionName is what a rollback restores, and a rollback is rejected while canRollback is false. A rollback needs a previous version that is still ACTIVE and whose effective end has not passed (P-058).
821
+
822
+ Pass the currentVersionId and currentDeploymentId from that call as expectedCurrentVersionId and expectedCurrentDeploymentId. If anyone deployed, rolled back or undeployed in between, the rollback is rejected with 409 and nothing changes: P-062 if another version or no version is live, P-063 if the same version is live but from a newer deployment. Read the group again and check with the user before retrying.
823
+ `,
819
824
  inputSchema: z6.object({
820
825
  groupId: z6.string().uuid().describe("Policy group ID"),
826
+ expectedCurrentVersionId: z6.string().uuid().describe("currentVersionId from your last read of the group"),
827
+ expectedCurrentDeploymentId: z6.string().uuid().describe("currentDeploymentId from your last read of the group"),
821
828
  memo: z6.string().min(1).describe("Rollback reason (required)")
822
829
  })
823
830
  },
824
- async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
825
- body: { memo }
831
+ async ({ groupId, expectedCurrentVersionId, expectedCurrentDeploymentId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
832
+ body: { memo, expectedCurrentVersionId, expectedCurrentDeploymentId }
826
833
  })
827
834
  );
828
835
  server.registerTool(
@@ -922,7 +929,7 @@ function registerDeployTools(server, callApi) {
922
929
  {
923
930
  title: "Deployment Overview",
924
931
  annotations: { readOnlyHint: true },
925
- description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer.",
932
+ description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer. Each group also carries currentDeploymentId, previousVersionName (the version a rollback restores) and canRollback, as in lexq_groups_get.",
926
933
  inputSchema: z6.object({})
927
934
  },
928
935
  async () => callApi("GET", "deployments/overview")
@@ -958,14 +965,14 @@ function registerDeployTools(server, callApi) {
958
965
 
959
966
  // src/mcp/tools/analytics.ts
960
967
  import { z as z7 } from "zod";
961
- import dedent2 from "dedent";
968
+ import dedent3 from "dedent";
962
969
  function registerAnalyticsTools(server, callApi) {
963
970
  server.registerTool(
964
971
  "lexq_dry_run",
965
972
  {
966
973
  title: "Dry Run",
967
974
  annotations: { readOnlyHint: true },
968
- description: dedent2`
975
+ description: dedent3`
969
976
  Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
970
977
 
971
978
  Returns:
@@ -995,7 +1002,7 @@ function registerAnalyticsTools(server, callApi) {
995
1002
  {
996
1003
  title: "Dry Run Compare",
997
1004
  annotations: { readOnlyHint: true },
998
- description: dedent2`
1005
+ description: dedent3`
999
1006
  Compare dry run results between two versions using the same input facts. Useful for validating changes.
1000
1007
 
1001
1008
  Returns:
@@ -1033,7 +1040,7 @@ function registerAnalyticsTools(server, callApi) {
1033
1040
  {
1034
1041
  title: "Start Simulation",
1035
1042
  annotations: { readOnlyHint: false, destructiveHint: false },
1036
- description: dedent2`
1043
+ description: dedent3`
1037
1044
  Start an Impact Simulation against historical, uploaded, or inline data.
1038
1045
 
1039
1046
  dataset.type and dataset.source are BOTH required, and must be paired:
@@ -1138,7 +1145,7 @@ function registerAnalyticsTools(server, callApi) {
1138
1145
  {
1139
1146
  title: "Upload Dataset",
1140
1147
  annotations: { readOnlyHint: false, destructiveHint: false },
1141
- description: dedent2`
1148
+ description: dedent3`
1142
1149
  Upload inline CSV or JSON content as a simulation dataset.
1143
1150
  The content is uploaded to S3 and a path is returned in the "path" field.
1144
1151
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "1.1.6",
3
+ "version": "2.0.0",
4
4
  "mcpName": "io.lexq/lexq",
5
- "description": "LexQ CLI — manage policies, simulate rules, and deploy from the terminal. Built for humans and AI agents.",
5
+ "description": "The Decision Operations Platform for engineering teams",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "lexq": "./dist/index.js"
@@ -44,6 +44,7 @@
44
44
  "test:command-options": "node tests/command-options.mjs",
45
45
  "test:value-domain": "pnpm build && node tests/value-domain.mjs",
46
46
  "test:fact-update": "pnpm build && node tests/fact-update.mjs",
47
+ "test:rollback": "pnpm build && node tests/rollback.mjs",
47
48
  "prepublishOnly": "pnpm build",
48
49
  "knip": "knip",
49
50
  "format": "prettier --write \"src/**/*.ts\"",
@@ -57,7 +58,11 @@
57
58
  "ai-agent",
58
59
  "agent-skills",
59
60
  "simulation",
60
- "business-rules"
61
+ "business-rules",
62
+ "developer-tools",
63
+ "decision-management",
64
+ "mcp",
65
+ "mcp-server"
61
66
  ],
62
67
  "author": "LexQ, LLC",
63
68
  "license": "Apache-2.0",
@@ -220,8 +220,9 @@ lexq groups ab-test start --group-id <groupId> --version-id <activeVersionId> --
220
220
 
221
221
  `--traffic-rate` is the percentage (1–99) of traffic routed to the test version.
222
222
 
223
- The test version must be `ACTIVE` (published) and different from the live version. Starting a test cancels any pending
224
- scheduled deployment for the group.
223
+ The test version must be a version of the same group, `ACTIVE` (published), and different from the live version. A
224
+ version from another group is rejected with `P-016`. Starting a test cancels any pending scheduled deployment for the
225
+ group.
225
226
 
226
227
  ### Adjust Traffic
227
228
 
@@ -352,17 +352,24 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
352
352
  **Goal:** Something went wrong in production — revert to the previous version.
353
353
 
354
354
  ```bash
355
- # 1. Check current state
356
- lexq deploy overview
355
+ # 1. Read the group: currentVersionName is live now, previousVersionName is what a
356
+ # rollback restores, and a rollback is rejected while canRollback is false
357
+ lexq groups get --id <gid>
357
358
 
358
- # 2. Rollback
359
- lexq deploy rollback --group-id <gid> --memo "Reverting due to increased error rate"
359
+ # 2. Roll back with the IDs from step 1
360
+ lexq deploy rollback --group-id <gid> --memo "Reverting due to increased error rate" \
361
+ --expected-current-version-id <currentVersionId> \
362
+ --expected-current-deployment-id <currentDeploymentId>
360
363
 
361
364
  # 3. Verify
362
365
  lexq deploy overview
363
366
  lexq history stats
364
367
  ```
365
368
 
369
+ If anyone deployed, rolled back or undeployed after step 1, step 2 is rejected with `409` and
370
+ nothing changes: `P-062` if another version or no version is live, `P-063` if the same version is
371
+ live but from a newer deployment. Start again from step 1.
372
+
366
373
  ---
367
374
 
368
375
  ## Recipe 7: Monitoring + Failure Triage