@lexq/cli 1.1.7 → 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
@@ -1773,7 +1773,13 @@ function registerDeployCommands(program) {
1773
1773
  process.exit(1);
1774
1774
  }
1775
1775
  });
1776
- 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(
1777
1783
  "after",
1778
1784
  dedent7`
1779
1785
 
@@ -1781,8 +1787,20 @@ function registerDeployCommands(program) {
1781
1787
  Only available if the previous version is still ACTIVE and its effective end
1782
1788
  has not passed (P-058).
1783
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
+
1784
1799
  Example:
1785
- $ 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
1786
1804
  `
1787
1805
  ).action(async (opts) => {
1788
1806
  try {
@@ -1802,7 +1820,11 @@ function registerDeployCommands(program) {
1802
1820
  baseUrl: globalOpts.baseUrl,
1803
1821
  dryRun: globalOpts.dryRun,
1804
1822
  verbose: globalOpts.verbose,
1805
- body: { memo: opts.memo }
1823
+ body: {
1824
+ memo: opts.memo,
1825
+ expectedCurrentVersionId: opts.expectedCurrentVersionId,
1826
+ expectedCurrentDeploymentId: opts.expectedCurrentDeploymentId
1827
+ }
1806
1828
  });
1807
1829
  console.log(`\u2713 Group ${opts.groupId} rolled back.`);
1808
1830
  } catch (error) {
@@ -2159,7 +2181,7 @@ function registerAnalyticsCommands(program) {
2159
2181
  dedent8`
2160
2182
 
2161
2183
  Examples:
2162
- $ lexq analytics dry-run --version-id <vid> --debug \\
2184
+ $ lexq analytics dry-run --version-id <vid> --debug \
2163
2185
  --json '{"facts": {"paymentAmount": 150000, "customerTier": "VIP"}}'
2164
2186
 
2165
2187
  $ lexq analytics dry-run --version-id <vid> --file test-input.json
@@ -3526,7 +3548,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3526
3548
 
3527
3549
  // src/commands/serve.ts
3528
3550
  import "commander";
3529
- import dedent17 from "dedent";
3551
+ import dedent18 from "dedent";
3530
3552
 
3531
3553
  // src/mcp/server.ts
3532
3554
  import { readFileSync as readFileSync3 } from "fs";
@@ -3665,7 +3687,7 @@ function registerGroupTools(server, callApi) {
3665
3687
  {
3666
3688
  title: "Get Policy Group",
3667
3689
  annotations: { readOnlyHint: true },
3668
- 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.",
3669
3691
  inputSchema: z2.object({
3670
3692
  groupId: z2.string().uuid().describe("Policy group ID")
3671
3693
  })
@@ -4246,6 +4268,7 @@ function registerFactTools(server, callApi) {
4246
4268
  }
4247
4269
 
4248
4270
  // src/mcp/tools/deploy.ts
4271
+ import dedent16 from "dedent";
4249
4272
  import { z as z6 } from "zod";
4250
4273
  function registerDeployTools(server, callApi) {
4251
4274
  server.registerTool(
@@ -4283,14 +4306,20 @@ function registerDeployTools(server, callApi) {
4283
4306
  {
4284
4307
  title: "Rollback Deployment",
4285
4308
  annotations: { readOnlyHint: false, destructiveHint: true },
4286
- 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
+ `,
4287
4314
  inputSchema: z6.object({
4288
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"),
4289
4318
  memo: z6.string().min(1).describe("Rollback reason (required)")
4290
4319
  })
4291
4320
  },
4292
- async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
4293
- body: { memo }
4321
+ async ({ groupId, expectedCurrentVersionId, expectedCurrentDeploymentId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
4322
+ body: { memo, expectedCurrentVersionId, expectedCurrentDeploymentId }
4294
4323
  })
4295
4324
  );
4296
4325
  server.registerTool(
@@ -4390,7 +4419,7 @@ function registerDeployTools(server, callApi) {
4390
4419
  {
4391
4420
  title: "Deployment Overview",
4392
4421
  annotations: { readOnlyHint: true },
4393
- 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.",
4394
4423
  inputSchema: z6.object({})
4395
4424
  },
4396
4425
  async () => callApi("GET", "deployments/overview")
@@ -4426,14 +4455,14 @@ function registerDeployTools(server, callApi) {
4426
4455
 
4427
4456
  // src/mcp/tools/analytics.ts
4428
4457
  import { z as z7 } from "zod";
4429
- import dedent16 from "dedent";
4458
+ import dedent17 from "dedent";
4430
4459
  function registerAnalyticsTools(server, callApi) {
4431
4460
  server.registerTool(
4432
4461
  "lexq_dry_run",
4433
4462
  {
4434
4463
  title: "Dry Run",
4435
4464
  annotations: { readOnlyHint: true },
4436
- description: dedent16`
4465
+ description: dedent17`
4437
4466
  Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
4438
4467
 
4439
4468
  Returns:
@@ -4463,7 +4492,7 @@ function registerAnalyticsTools(server, callApi) {
4463
4492
  {
4464
4493
  title: "Dry Run Compare",
4465
4494
  annotations: { readOnlyHint: true },
4466
- description: dedent16`
4495
+ description: dedent17`
4467
4496
  Compare dry run results between two versions using the same input facts. Useful for validating changes.
4468
4497
 
4469
4498
  Returns:
@@ -4501,7 +4530,7 @@ function registerAnalyticsTools(server, callApi) {
4501
4530
  {
4502
4531
  title: "Start Simulation",
4503
4532
  annotations: { readOnlyHint: false, destructiveHint: false },
4504
- description: dedent16`
4533
+ description: dedent17`
4505
4534
  Start an Impact Simulation against historical, uploaded, or inline data.
4506
4535
 
4507
4536
  dataset.type and dataset.source are BOTH required, and must be paired:
@@ -4606,7 +4635,7 @@ function registerAnalyticsTools(server, callApi) {
4606
4635
  {
4607
4636
  title: "Upload Dataset",
4608
4637
  annotations: { readOnlyHint: false, destructiveHint: false },
4609
- description: dedent16`
4638
+ description: dedent17`
4610
4639
  Upload inline CSV or JSON content as a simulation dataset.
4611
4640
  The content is uploaded to S3 and a path is returned in the "path" field.
4612
4641
 
@@ -5159,7 +5188,7 @@ function startMcpServer() {
5159
5188
  function registerServeCommand(program) {
5160
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(
5161
5190
  "after",
5162
- dedent17`
5191
+ dedent18`
5163
5192
 
5164
5193
  Example:
5165
5194
  $ lexq serve --mcp
@@ -5190,11 +5219,11 @@ function registerServeCommand(program) {
5190
5219
 
5191
5220
  // src/commands/domain-templates.ts
5192
5221
  import "commander";
5193
- import dedent18 from "dedent";
5222
+ import dedent19 from "dedent";
5194
5223
  function registerDomainTemplateCommands(program) {
5195
5224
  const templates = program.command("domain-templates").description("Browse and apply domain templates").addHelpText(
5196
5225
  "after",
5197
- dedent18`
5226
+ dedent19`
5198
5227
 
5199
5228
  A domain template is an industry-specific starter pack of fact
5200
5229
  definitions and sample rules. Applying one provisions a ready-to-use
@@ -5242,7 +5271,7 @@ function registerDomainTemplateCommands(program) {
5242
5271
  });
5243
5272
  templates.command("preview").description("Preview what a domain template provisions").requiredOption("--template <key>", "Domain template key (e.g. ECOMMERCE)").addHelpText(
5244
5273
  "after",
5245
- dedent18`
5274
+ dedent19`
5246
5275
 
5247
5276
  Read-only dry run — shows the fact definitions and sample rules the
5248
5277
  template will create. Nothing is provisioned.
@@ -5271,7 +5300,7 @@ function registerDomainTemplateCommands(program) {
5271
5300
  });
5272
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(
5273
5302
  "after",
5274
- dedent18`
5303
+ dedent19`
5275
5304
 
5276
5305
  Creates the template's fact definitions and a new DRAFT policy group
5277
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
  })
@@ -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.7",
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",
@@ -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