@lexq/cli 1.0.2 → 1.1.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/AGENTS.md CHANGED
@@ -180,8 +180,12 @@ The MCP toolset mirrors the CLI command inventory one-to-one.
180
180
  - Releasing is two steps, in this order. Raise `version` in package.json inside the pull request, so a reviewer
181
181
  sees the number beside the change that justifies it. After it merges, push the matching `v<version>` tag — that
182
182
  tag is what publishes. A tag that disagrees with package.json is rejected before anything reaches npm.
183
- - Pick the number by what breaks. On 0.x a breaking change is a minor bump (0.1 → 0.2), not a patch. Raising the
184
- Node floor, a peer dependency major, or any change to what `tools/list` returns is breaking.
183
+ - Pick the number by what breaks. A change is breaking when a call that used to work now fails or does something
184
+ else. That includes removing or renaming a tool, command, option or parameter, changing a parameter's type,
185
+ adding a required parameter or making an optional one required, and narrowing the values a parameter accepts.
186
+ Raising the Node floor or moving a peer dependency to a new major version is breaking too. A breaking change
187
+ raises the major number. A new tool, command, option or optional parameter raises the minor, and anything else,
188
+ including a changed description, raises the patch.
185
189
  - Gates before commit: `pnpm typecheck` (zero errors), `pnpm lint` (zero warnings), `pnpm test:fact-key`,
186
190
  `pnpm test:decimals`.
187
191
  - Multi-line CLI help and MCP tool descriptions use `dedent` — template-literal indentation leaks into LLM context.
package/dist/index.js CHANGED
@@ -804,18 +804,18 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
804
804
  process.exit(1);
805
805
  }
806
806
  });
807
- versions.command("update").description("Update a draft version metadata").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--id <versionId>", "Policy version ID").option("--commit-message <message>", "Commit message").option("--effective-from <instant>", `Effective start, ${INSTANT}`).option("--effective-to <instant>", `Effective end, ${INSTANT}`).option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
807
+ versions.command("update").description("Update a draft version metadata").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--id <versionId>", "Policy version ID").option("--commit-message <message>", "Commit message").option("--effective-from <instant>", `Effective start, ${INSTANT}`).option("--effective-to <instant>", `Effective end, ${INSTANT}`).option("--clear-effective-from", "Remove the effective start").option("--clear-effective-to", "Remove the effective end").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
808
808
  "after",
809
809
  dedent4`
810
810
 
811
811
  Only DRAFT versions can be updated. Published (ACTIVE) versions are immutable.
812
812
 
813
- An omitted --commit-message keeps its value, but --effective-from and --effective-to
814
- are replaced together: one you omit is cleared. To keep the effective window, pass
815
- both current values (read them with lexq versions get).
813
+ Omitted options keep their current values. To remove an end of the effective
814
+ window, pass --clear-effective-from or --clear-effective-to instead of a value.
816
815
 
817
- Example:
816
+ Examples:
818
817
  $ lexq versions update --group-id <gid> --id <vid> --commit-message "Updated rules"
818
+ $ lexq versions update --group-id <gid> --id <vid> --clear-effective-to
819
819
  `
820
820
  ).action(async (opts) => {
821
821
  try {
@@ -911,6 +911,8 @@ function buildUpdateBody(opts) {
911
911
  if (opts.commitMessage) body.commitMessage = opts.commitMessage;
912
912
  if (opts.effectiveFrom) body.effectiveFrom = opts.effectiveFrom;
913
913
  if (opts.effectiveTo) body.effectiveTo = opts.effectiveTo;
914
+ if (opts.clearEffectiveFrom) body.clearEffectiveFrom = true;
915
+ if (opts.clearEffectiveTo) body.clearEffectiveTo = true;
914
916
  return body;
915
917
  }
916
918
 
@@ -1642,6 +1644,7 @@ function registerDeployCommands(program) {
1642
1644
 
1643
1645
  Locks the version permanently. Rules cannot be modified after publishing.
1644
1646
  A snapshot hash is generated for integrity verification.
1647
+ A version whose effective end has already passed cannot be published (P-058).
1645
1648
 
1646
1649
  Example:
1647
1650
  $ lexq deploy publish --group-id <gid> --version-id <vid> --memo "Validated via dry-run"
@@ -1672,6 +1675,8 @@ function registerDeployCommands(program) {
1672
1675
  dedent7`
1673
1676
 
1674
1677
  Takes effect immediately. The version starts receiving production traffic.
1678
+ A version whose effective start has not arrived (P-037) or whose effective end
1679
+ has passed (P-058) is rejected.
1675
1680
 
1676
1681
  Example:
1677
1682
  $ lexq deploy live --group-id <gid> --version-id <vid> --memo "Go live — v3"
@@ -1698,7 +1703,8 @@ function registerDeployCommands(program) {
1698
1703
  dedent7`
1699
1704
 
1700
1705
  Reverts to the version that was live before the current one.
1701
- Only available if the previous version is still ACTIVE.
1706
+ Only available if the previous version is still ACTIVE and its effective end
1707
+ has not passed (P-058).
1702
1708
 
1703
1709
  Example:
1704
1710
  $ lexq deploy rollback --group-id <gid> --memo "High error rate" --force
@@ -2201,7 +2207,7 @@ function registerAnalyticsCommands(program) {
2201
2207
  Use --format table for summary view.
2202
2208
  `
2203
2209
  );
2204
- sim.command("start").description("Start a new batch simulation").requiredOption("--json <body>", "Simulation request body as JSON").option("--file <path>", "Read request body from a JSON file").addHelpText(
2210
+ sim.command("start").description("Start a new batch simulation").option("--json <body>", "Simulation request body as JSON").option("--file <path>", "Read request body from a JSON file").addHelpText(
2205
2211
  "after",
2206
2212
  dedent8`
2207
2213
 
@@ -2220,7 +2226,9 @@ function registerAnalyticsCommands(program) {
2220
2226
  "maxRecords": 1000
2221
2227
  }
2222
2228
  }'
2223
-
2229
+
2230
+ $ lexq analytics simulation start --file simulation.json
2231
+
2224
2232
  Dataset types: HISTORICAL, UPLOADED, MANUAL
2225
2233
  Dataset sources: EXECUTION_LOGS, S3_BUCKET, REQUEST_BODY
2226
2234
  HISTORICAL from / to are yyyy-MM-dd, whole days in the organization time zone, both inclusive.
@@ -2228,6 +2236,9 @@ function registerAnalyticsCommands(program) {
2228
2236
  ).action(async (opts) => {
2229
2237
  try {
2230
2238
  const globalOpts = program.opts();
2239
+ if (!opts.json && !opts.file) {
2240
+ throw new Error("Request body is required. Use --json or --file.");
2241
+ }
2231
2242
  const body = resolveBody(opts);
2232
2243
  const data = await apiRequest("POST", "analytics/simulations", {
2233
2244
  apiKey: globalOpts.apiKey,
@@ -2578,7 +2589,7 @@ function registerProfileCommands(program) {
2578
2589
  program.command("profile <groupId>").description("Per-rule latency profile with relative slow-rule flags").option(
2579
2590
  "--rule <ruleId>",
2580
2591
  `Single-rule detail (distributions + ${LATENCY_WINDOW_MILLIS / 1e3}s window series)`
2581
- ).option("--version <versionId>", "Version to inspect (default: live version)").option(
2592
+ ).option("--version-id <versionId>", "Version to inspect (default: live version)").option(
2582
2593
  "--from <instant>",
2583
2594
  `Window start, ISO-8601 instant (default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago)`
2584
2595
  ).option("--to <instant>", "Window end, ISO-8601 instant (default: now)").option("--cache <state>", "Cache dimension for the rule table: HIT | MISS (default: HIT)").addHelpText(
@@ -2594,7 +2605,7 @@ function registerProfileCommands(program) {
2594
2605
  Examples:
2595
2606
  $ lexq profile <groupId>
2596
2607
  $ lexq profile <groupId> --cache MISS --from 2026-07-01T00:00:00Z
2597
- $ lexq profile <groupId> --rule <ruleId> --version <versionId>
2608
+ $ lexq profile <groupId> --rule <ruleId> --version-id <versionId>
2598
2609
  `
2599
2610
  ).action(async (groupId, opts) => {
2600
2611
  try {
@@ -2604,7 +2615,7 @@ function registerProfileCommands(program) {
2604
2615
  throw new Error(`--cache must be one of: ${ProfileCacheState.join(" | ")}`);
2605
2616
  }
2606
2617
  const params = {};
2607
- if (opts.version) params.versionId = opts.version;
2618
+ if (opts.versionId) params.versionId = opts.versionId;
2608
2619
  if (opts.from) params.from = opts.from;
2609
2620
  if (opts.to) params.to = opts.to;
2610
2621
  if (opts.cache) params.cacheState = opts.cache;
@@ -3743,13 +3754,15 @@ function registerVersionTools(server, callApi) {
3743
3754
  {
3744
3755
  title: "Update Policy Version",
3745
3756
  annotations: { readOnlyHint: false, destructiveHint: true },
3746
- description: "Update a DRAFT version. Only DRAFT versions can be modified. An omitted commitMessage keeps its value, but effectiveFrom and effectiveTo are replaced together: one you omit is cleared. To keep the effective window, send both current values (read them with lexq_versions_get).",
3757
+ description: "Update a DRAFT version. Only DRAFT versions can be modified. Omitted fields keep their current values. To remove an end of the effective window, set clearEffectiveFrom or clearEffectiveTo to true instead of sending a value.",
3747
3758
  inputSchema: z3.object({
3748
3759
  groupId: z3.string().uuid().describe("Policy group ID"),
3749
3760
  versionId: z3.string().uuid().describe("Version ID"),
3750
3761
  commitMessage: z3.string().optional().describe("New commit message"),
3751
- effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT} Omit it and the start is cleared.`),
3752
- effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT} Omit it and the end is cleared.`)
3762
+ effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
3763
+ effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
3764
+ clearEffectiveFrom: z3.boolean().optional().describe("Set to true to remove the effective start. Do not send effectiveFrom with it."),
3765
+ clearEffectiveTo: z3.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
3753
3766
  })
3754
3767
  },
3755
3768
  async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
@@ -4092,7 +4105,7 @@ function registerFactTools(server, callApi) {
4092
4105
  {
4093
4106
  title: "List Unregistered Facts",
4094
4107
  annotations: { readOnlyHint: true },
4095
- description: "List facts referenced by a version's rules but not yet defined (read-only \u2014 does not block publish/deploy, INV-4). Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
4108
+ description: "List facts referenced by a version's rules but not yet defined (read-only \u2014 does not block publish/deploy). Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
4096
4109
  inputSchema: z5.object({
4097
4110
  groupId: z5.string().uuid().describe("Policy group ID"),
4098
4111
  versionId: z5.string().uuid().describe("Version ID")
@@ -4125,7 +4138,7 @@ function registerDeployTools(server, callApi) {
4125
4138
  {
4126
4139
  title: "Publish Version",
4127
4140
  annotations: { readOnlyHint: false, destructiveHint: false },
4128
- description: `Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts referenced by rules do not block publishing (INV-4); call lexq_facts_unregistered first to review them. Publishing fails if the version's compiled rules exceed ${MAX_SNAPSHOT_BYTES / 1024} KB.`,
4141
+ description: `Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts referenced by rules do not block publishing; call lexq_facts_unregistered first to review them. Publishing fails if the version's compiled rules exceed ${MAX_SNAPSHOT_BYTES / 1024} KB, or if its effective end has already passed (P-058); move the end into the future with lexq_versions_update first.`,
4129
4142
  inputSchema: z6.object({
4130
4143
  groupId: z6.string().uuid().describe("Policy group ID"),
4131
4144
  versionId: z6.string().uuid().describe("Version ID to publish"),
@@ -4139,7 +4152,7 @@ function registerDeployTools(server, callApi) {
4139
4152
  {
4140
4153
  title: "Deploy to Live",
4141
4154
  annotations: { readOnlyHint: false, destructiveHint: true },
4142
- description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Versions whose effective start date has not arrived are rejected (P-037) \u2014 use lexq_deploy_schedule for those. Undefined facts do not block deployment (INV-4); use lexq_facts_unregistered to review what the version references but has not defined.",
4155
+ description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Versions whose effective start date has not arrived are rejected (P-037) \u2014 use lexq_deploy_schedule for those. Versions whose effective end has passed are rejected (P-058). Undefined facts do not block deployment; use lexq_facts_unregistered to review what the version references but has not defined.",
4143
4156
  inputSchema: z6.object({
4144
4157
  groupId: z6.string().uuid().describe("Policy group ID"),
4145
4158
  versionId: z6.string().uuid().describe("Version ID to deploy"),
@@ -4155,7 +4168,7 @@ function registerDeployTools(server, callApi) {
4155
4168
  {
4156
4169
  title: "Rollback Deployment",
4157
4170
  annotations: { readOnlyHint: false, destructiveHint: true },
4158
- description: "Rollback to the previous deployed version. Only available if there is a previous version.",
4171
+ 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).",
4159
4172
  inputSchema: z6.object({
4160
4173
  groupId: z6.string().uuid().describe("Policy group ID"),
4161
4174
  memo: z6.string().min(1).describe("Rollback reason (required)")
@@ -321,13 +321,15 @@ function registerVersionTools(server, callApi) {
321
321
  {
322
322
  title: "Update Policy Version",
323
323
  annotations: { readOnlyHint: false, destructiveHint: true },
324
- description: "Update a DRAFT version. Only DRAFT versions can be modified. An omitted commitMessage keeps its value, but effectiveFrom and effectiveTo are replaced together: one you omit is cleared. To keep the effective window, send both current values (read them with lexq_versions_get).",
324
+ description: "Update a DRAFT version. Only DRAFT versions can be modified. Omitted fields keep their current values. To remove an end of the effective window, set clearEffectiveFrom or clearEffectiveTo to true instead of sending a value.",
325
325
  inputSchema: z3.object({
326
326
  groupId: z3.string().uuid().describe("Policy group ID"),
327
327
  versionId: z3.string().uuid().describe("Version ID"),
328
328
  commitMessage: z3.string().optional().describe("New commit message"),
329
- effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT} Omit it and the start is cleared.`),
330
- effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT} Omit it and the end is cleared.`)
329
+ effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
330
+ effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
331
+ clearEffectiveFrom: z3.boolean().optional().describe("Set to true to remove the effective start. Do not send effectiveFrom with it."),
332
+ clearEffectiveTo: z3.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
331
333
  })
332
334
  },
333
335
  async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
@@ -674,7 +676,7 @@ function registerFactTools(server, callApi) {
674
676
  {
675
677
  title: "List Unregistered Facts",
676
678
  annotations: { readOnlyHint: true },
677
- description: "List facts referenced by a version's rules but not yet defined (read-only \u2014 does not block publish/deploy, INV-4). Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
679
+ description: "List facts referenced by a version's rules but not yet defined (read-only \u2014 does not block publish/deploy). Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
678
680
  inputSchema: z5.object({
679
681
  groupId: z5.string().uuid().describe("Policy group ID"),
680
682
  versionId: z5.string().uuid().describe("Version ID")
@@ -707,7 +709,7 @@ function registerDeployTools(server, callApi) {
707
709
  {
708
710
  title: "Publish Version",
709
711
  annotations: { readOnlyHint: false, destructiveHint: false },
710
- description: `Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts referenced by rules do not block publishing (INV-4); call lexq_facts_unregistered first to review them. Publishing fails if the version's compiled rules exceed ${MAX_SNAPSHOT_BYTES / 1024} KB.`,
712
+ description: `Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts referenced by rules do not block publishing; call lexq_facts_unregistered first to review them. Publishing fails if the version's compiled rules exceed ${MAX_SNAPSHOT_BYTES / 1024} KB, or if its effective end has already passed (P-058); move the end into the future with lexq_versions_update first.`,
711
713
  inputSchema: z6.object({
712
714
  groupId: z6.string().uuid().describe("Policy group ID"),
713
715
  versionId: z6.string().uuid().describe("Version ID to publish"),
@@ -721,7 +723,7 @@ function registerDeployTools(server, callApi) {
721
723
  {
722
724
  title: "Deploy to Live",
723
725
  annotations: { readOnlyHint: false, destructiveHint: true },
724
- description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Versions whose effective start date has not arrived are rejected (P-037) \u2014 use lexq_deploy_schedule for those. Undefined facts do not block deployment (INV-4); use lexq_facts_unregistered to review what the version references but has not defined.",
726
+ description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Versions whose effective start date has not arrived are rejected (P-037) \u2014 use lexq_deploy_schedule for those. Versions whose effective end has passed are rejected (P-058). Undefined facts do not block deployment; use lexq_facts_unregistered to review what the version references but has not defined.",
725
727
  inputSchema: z6.object({
726
728
  groupId: z6.string().uuid().describe("Policy group ID"),
727
729
  versionId: z6.string().uuid().describe("Version ID to deploy"),
@@ -737,7 +739,7 @@ function registerDeployTools(server, callApi) {
737
739
  {
738
740
  title: "Rollback Deployment",
739
741
  annotations: { readOnlyHint: false, destructiveHint: true },
740
- description: "Rollback to the previous deployed version. Only available if there is a previous version.",
742
+ 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).",
741
743
  inputSchema: z6.object({
742
744
  groupId: z6.string().uuid().describe("Policy group ID"),
743
745
  memo: z6.string().min(1).describe("Rollback reason (required)")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
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",
@@ -41,6 +41,7 @@
41
41
  "test:decimals": "pnpm build && node tests/exact-decimals.mjs",
42
42
  "test:fact-key": "node tests/fact-key.mjs",
43
43
  "test:export-format": "node tests/export-format.mjs",
44
+ "test:command-options": "node tests/command-options.mjs",
44
45
  "test:value-domain": "pnpm build && node tests/value-domain.mjs",
45
46
  "prepublishOnly": "pnpm build",
46
47
  "knip": "knip",
@@ -253,7 +253,7 @@ lexq profile <groupId>
253
253
  lexq profile <groupId> --rule <ruleId>
254
254
 
255
255
  # Defaults: live version, last 24h, cache HIT
256
- lexq profile <groupId> --version <vid> --cache MISS \
256
+ lexq profile <groupId> --version-id <vid> --cache MISS \
257
257
  --from 2026-07-01T00:00:00Z --to 2026-08-01T00:00:00Z
258
258
  ```
259
259