@lexq/cli 1.0.1 → 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
@@ -269,7 +269,7 @@ function registerAuthCommands(program) {
269
269
 
270
270
  Example:
271
271
  $ lexq auth whoami
272
- { "tenantId": "abc-123", "userId": "...", "role": "ADMIN", "apiKey": "lexq_us_****abcd" }
272
+ { "tenantId": "abc-123", "userId": "...", "role": "ADMIN", "tenantTimezone": "Asia/Seoul", "apiKey": "lexq_us_****abcd" }
273
273
  `
274
274
  ).action(async () => {
275
275
  try {
@@ -310,9 +310,10 @@ function registerStatusCommand(program) {
310
310
 
311
311
  Example:
312
312
  $ lexq status
313
- { "status": "ok", "latencyMs": 142, "tenantId": "abc-123", "role": "ADMIN" }
313
+ { "status": "ok", "latencyMs": 142, "tenantId": "abc-123", "role": "ADMIN", "tenantTimezone": "Asia/Seoul" }
314
314
 
315
315
  Use this to verify your API key is valid and the LexQ API is reachable.
316
+ tenantTimezone is the zone every yyyy-MM-dd date option is read in.
316
317
  `
317
318
  ).action(async () => {
318
319
  const globalOpts = program.opts();
@@ -326,7 +327,8 @@ function registerStatusCommand(program) {
326
327
  status: "ok",
327
328
  latencyMs: Date.now() - startTime,
328
329
  tenantId: info.tenantId,
329
- role: info.role
330
+ role: info.role,
331
+ tenantTimezone: info.tenantTimezone
330
332
  });
331
333
  } catch (error) {
332
334
  printError(error);
@@ -357,6 +359,22 @@ var MAX_SNAPSHOT_BYTES = 131072;
357
359
  // src/commands/groups.ts
358
360
  import "commander";
359
361
  import dedent3 from "dedent";
362
+
363
+ // src/lib/dates.ts
364
+ var ORG_DAY = "yyyy-MM-dd, a whole calendar day in the organization time zone (tenantTimezone in whoami), not UTC";
365
+ var START_DAY = `Start date, ${ORG_DAY}. Inclusive.`;
366
+ var END_DAY = `End date, ${ORG_DAY}. Inclusive: the whole day counts.`;
367
+ var WINDOW_START_DAY = `Window start date, ${ORG_DAY}. Inclusive.`;
368
+ var WINDOW_END_DAY = `Window end date, ${ORG_DAY}. Inclusive: the whole day counts.`;
369
+ var INSTANT = "ISO-8601 instant with Z or an offset, e.g. 2026-10-01T00:00:00Z or 2026-10-01T09:00:00+09:00. A bare date or a time without an offset is rejected.";
370
+ var STATS_DEFAULT_WINDOW_DAYS = 30;
371
+ var STATS_MAX_WINDOW_DAYS = 31;
372
+ var FAILURE_LOG_LOOKBACK_DAYS = 7;
373
+ function utcMinute(instant) {
374
+ return instant ? `${new Date(instant).toISOString().substring(0, 16)}Z` : "\u2013";
375
+ }
376
+
377
+ // src/commands/groups.ts
360
378
  function registerGroupCommands(program) {
361
379
  const groups = program.command("groups").description("Manage policy groups").addHelpText(
362
380
  "after",
@@ -396,7 +414,7 @@ function registerGroupCommands(program) {
396
414
  g.status,
397
415
  String(g.priority),
398
416
  g.currentVersionName ?? "\u2013",
399
- g.updatedAt.substring(0, 10)
417
+ utcMinute(g.updatedAt)
400
418
  ]),
401
419
  { truncate: 24 }
402
420
  );
@@ -714,7 +732,7 @@ function registerVersionCommands(program) {
714
732
  v.status,
715
733
  v.commitMessage ?? "\u2013",
716
734
  v.snapshotHash ? v.snapshotHash.substring(0, 8) : "\u2013",
717
- v.createdAt.substring(0, 10)
735
+ utcMinute(v.createdAt)
718
736
  ]),
719
737
  { truncate: 24 }
720
738
  );
@@ -747,7 +765,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
747
765
  process.exit(1);
748
766
  }
749
767
  });
750
- versions.command("create").description("Create a new draft version").requiredOption("--group-id <groupId>", "Policy group ID").option("--commit-message <message>", "Commit message").option("--effective-from <date>", "Effective from (ISO datetime)").option("--effective-to <date>", "Effective to (ISO datetime)").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
768
+ versions.command("create").description("Create a new draft version").requiredOption("--group-id <groupId>", "Policy group 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(
751
769
  "after",
752
770
  dedent4`
753
771
 
@@ -762,8 +780,8 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
762
780
 
763
781
  Fields:
764
782
  commitMessage string Version description (optional, max 255 chars)
765
- effectiveFrom datetime Start of effective period (optional, ISO-8601)
766
- effectiveTo datetime End of effective period (optional, auto-expires)
783
+ effectiveFrom instant Start of effective period (optional, with Z or an offset)
784
+ effectiveTo instant End of effective period (optional, auto-expires)
767
785
  `
768
786
  ).action(async (opts) => {
769
787
  try {
@@ -786,14 +804,18 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
786
804
  process.exit(1);
787
805
  }
788
806
  });
789
- 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 <date>", "Effective from (ISO datetime)").option("--effective-to <date>", "Effective to (ISO datetime)").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(
790
808
  "after",
791
809
  dedent4`
792
810
 
793
811
  Only DRAFT versions can be updated. Published (ACTIVE) versions are immutable.
794
812
 
795
- Example:
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.
815
+
816
+ Examples:
796
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
797
819
  `
798
820
  ).action(async (opts) => {
799
821
  try {
@@ -889,6 +911,8 @@ function buildUpdateBody(opts) {
889
911
  if (opts.commitMessage) body.commitMessage = opts.commitMessage;
890
912
  if (opts.effectiveFrom) body.effectiveFrom = opts.effectiveFrom;
891
913
  if (opts.effectiveTo) body.effectiveTo = opts.effectiveTo;
914
+ if (opts.clearEffectiveFrom) body.clearEffectiveFrom = true;
915
+ if (opts.clearEffectiveTo) body.clearEffectiveTo = true;
892
916
  return body;
893
917
  }
894
918
 
@@ -1620,6 +1644,7 @@ function registerDeployCommands(program) {
1620
1644
 
1621
1645
  Locks the version permanently. Rules cannot be modified after publishing.
1622
1646
  A snapshot hash is generated for integrity verification.
1647
+ A version whose effective end has already passed cannot be published (P-058).
1623
1648
 
1624
1649
  Example:
1625
1650
  $ lexq deploy publish --group-id <gid> --version-id <vid> --memo "Validated via dry-run"
@@ -1650,6 +1675,8 @@ function registerDeployCommands(program) {
1650
1675
  dedent7`
1651
1676
 
1652
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.
1653
1680
 
1654
1681
  Example:
1655
1682
  $ lexq deploy live --group-id <gid> --version-id <vid> --memo "Go live — v3"
@@ -1676,7 +1703,8 @@ function registerDeployCommands(program) {
1676
1703
  dedent7`
1677
1704
 
1678
1705
  Reverts to the version that was live before the current one.
1679
- 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).
1680
1708
 
1681
1709
  Example:
1682
1710
  $ lexq deploy rollback --group-id <gid> --memo "High error rate" --force
@@ -1742,7 +1770,7 @@ function registerDeployCommands(program) {
1742
1770
  process.exit(1);
1743
1771
  }
1744
1772
  });
1745
- deploy.command("history").description("List deployment history").option("--group-id <groupId>", "Filter by policy group").option("--types <types>", "Filter by types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
1773
+ deploy.command("history").description("List deployment history").option("--group-id <groupId>", "Filter by policy group").option("--types <types>", "Filter by types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)").option("--start-date <date>", START_DAY).option("--end-date <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
1746
1774
  "after",
1747
1775
  dedent7`
1748
1776
 
@@ -1774,7 +1802,7 @@ function registerDeployCommands(program) {
1774
1802
  d.policyGroupName,
1775
1803
  d.versionNo != null ? `v${d.versionNo}` : "\u2013",
1776
1804
  d.deployedByName,
1777
- d.deployedAt.substring(0, 16)
1805
+ utcMinute(d.deployedAt)
1778
1806
  ]),
1779
1807
  { truncate: 20 }
1780
1808
  );
@@ -1836,7 +1864,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1836
1864
  d.groupName,
1837
1865
  d.groupStatus,
1838
1866
  d.currentVersionName ?? "\u2013",
1839
- d.lastDeployedAt?.substring(0, 16) ?? "\u2013"
1867
+ utcMinute(d.lastDeployedAt)
1840
1868
  ])
1841
1869
  );
1842
1870
  } else {
@@ -1903,6 +1931,8 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1903
1931
 
1904
1932
  The version must be ACTIVE with a future effective start date; the system
1905
1933
  deploys it automatically at that time (within one scheduler tick, ≤60s).
1934
+ That time is the version's effectiveFrom, set on the DRAFT before publishing;
1935
+ this command takes no time of its own.
1906
1936
  One pending schedule per group. Manual deploy/rollback/undeploy, starting
1907
1937
  an A/B test, or archiving the group cancels the pending schedule.
1908
1938
 
@@ -1994,9 +2024,9 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1994
2024
  s.status,
1995
2025
  s.policyGroupName ?? s.policyGroupId.substring(0, 8),
1996
2026
  s.versionNo != null ? `v${s.versionNo}` : "\u2013",
1997
- s.scheduledFor.substring(0, 16),
2027
+ utcMinute(s.scheduledFor),
1998
2028
  s.scheduledByName,
1999
- s.status === "EXECUTED" ? s.executedAt?.substring(0, 16) ?? "\u2013" : s.status === "CANCELED" ? s.canceledReason ?? "\u2013" : s.status === "FAILED" ? s.failedReason ?? "\u2013" : "\u2013"
2029
+ s.status === "EXECUTED" ? utcMinute(s.executedAt) : s.status === "CANCELED" ? s.canceledReason ?? "\u2013" : s.status === "FAILED" ? s.failedReason ?? "\u2013" : "\u2013"
2000
2030
  ]),
2001
2031
  { truncate: 20 }
2002
2032
  );
@@ -2177,7 +2207,7 @@ function registerAnalyticsCommands(program) {
2177
2207
  Use --format table for summary view.
2178
2208
  `
2179
2209
  );
2180
- 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(
2181
2211
  "after",
2182
2212
  dedent8`
2183
2213
 
@@ -2196,13 +2226,19 @@ function registerAnalyticsCommands(program) {
2196
2226
  "maxRecords": 1000
2197
2227
  }
2198
2228
  }'
2199
-
2229
+
2230
+ $ lexq analytics simulation start --file simulation.json
2231
+
2200
2232
  Dataset types: HISTORICAL, UPLOADED, MANUAL
2201
2233
  Dataset sources: EXECUTION_LOGS, S3_BUCKET, REQUEST_BODY
2234
+ HISTORICAL from / to are yyyy-MM-dd, whole days in the organization time zone, both inclusive.
2202
2235
  `
2203
2236
  ).action(async (opts) => {
2204
2237
  try {
2205
2238
  const globalOpts = program.opts();
2239
+ if (!opts.json && !opts.file) {
2240
+ throw new Error("Request body is required. Use --json or --file.");
2241
+ }
2206
2242
  const body = resolveBody(opts);
2207
2243
  const data = await apiRequest("POST", "analytics/simulations", {
2208
2244
  apiKey: globalOpts.apiKey,
@@ -2315,7 +2351,7 @@ function registerAnalyticsCommands(program) {
2315
2351
  process.exit(1);
2316
2352
  }
2317
2353
  });
2318
- sim.command("list").description("List simulation history").option("--status <status>", "Filter by status (PENDING, RUNNING, COMPLETED, FAILED, CANCELED)").option("--from <date>", "Start date (yyyy-MM-dd)").option("--to <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
2354
+ sim.command("list").description("List simulation history").option("--status <status>", "Filter by status (PENDING, RUNNING, COMPLETED, FAILED, CANCELED)").option("--from <date>", START_DAY).option("--to <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
2319
2355
  try {
2320
2356
  const globalOpts = program.opts();
2321
2357
  const format = globalOpts.format ?? "json";
@@ -2344,7 +2380,7 @@ function registerAnalyticsCommands(program) {
2344
2380
  s.status,
2345
2381
  `${s.matchRate.toFixed(1)}%`,
2346
2382
  String(s.totalRecords),
2347
- s.createdAt.substring(0, 16)
2383
+ utcMinute(s.createdAt)
2348
2384
  ]),
2349
2385
  { truncate: 20 }
2350
2386
  );
@@ -2553,7 +2589,7 @@ function registerProfileCommands(program) {
2553
2589
  program.command("profile <groupId>").description("Per-rule latency profile with relative slow-rule flags").option(
2554
2590
  "--rule <ruleId>",
2555
2591
  `Single-rule detail (distributions + ${LATENCY_WINDOW_MILLIS / 1e3}s window series)`
2556
- ).option("--version <versionId>", "Version to inspect (default: live version)").option(
2592
+ ).option("--version-id <versionId>", "Version to inspect (default: live version)").option(
2557
2593
  "--from <instant>",
2558
2594
  `Window start, ISO-8601 instant (default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago)`
2559
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(
@@ -2569,7 +2605,7 @@ function registerProfileCommands(program) {
2569
2605
  Examples:
2570
2606
  $ lexq profile <groupId>
2571
2607
  $ lexq profile <groupId> --cache MISS --from 2026-07-01T00:00:00Z
2572
- $ lexq profile <groupId> --rule <ruleId> --version <versionId>
2608
+ $ lexq profile <groupId> --rule <ruleId> --version-id <versionId>
2573
2609
  `
2574
2610
  ).action(async (groupId, opts) => {
2575
2611
  try {
@@ -2579,7 +2615,7 @@ function registerProfileCommands(program) {
2579
2615
  throw new Error(`--cache must be one of: ${ProfileCacheState.join(" | ")}`);
2580
2616
  }
2581
2617
  const params = {};
2582
- if (opts.version) params.versionId = opts.version;
2618
+ if (opts.versionId) params.versionId = opts.versionId;
2583
2619
  if (opts.from) params.from = opts.from;
2584
2620
  if (opts.to) params.to = opts.to;
2585
2621
  if (opts.cache) params.cacheState = opts.cache;
@@ -2665,7 +2701,7 @@ function registerHistoryCommands(program) {
2665
2701
  Statuses: SUCCESS, NO_MATCH, ERROR, TIMEOUT
2666
2702
  `
2667
2703
  );
2668
- history.command("list").description("List execution history").option("--trace-id <traceId>", "Filter by trace ID").option("--group-id <groupId>", "Filter by policy group").option("--version-id <versionId>", "Filter by version").option("--status <status>", "Filter by status (SUCCESS, NO_MATCH, ERROR, TIMEOUT)").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
2704
+ history.command("list").description("List execution history").option("--trace-id <traceId>", "Filter by trace ID").option("--group-id <groupId>", "Filter by policy group").option("--version-id <versionId>", "Filter by version").option("--status <status>", "Filter by status (SUCCESS, NO_MATCH, ERROR, TIMEOUT)").option("--start-date <date>", START_DAY).option("--end-date <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
2669
2705
  "after",
2670
2706
  dedent10`
2671
2707
 
@@ -2705,7 +2741,7 @@ function registerHistoryCommands(program) {
2705
2741
  h.status,
2706
2742
  h.isMatched ? "\u2713" : "\u2717",
2707
2743
  `${h.latencyMs}ms`,
2708
- h.createdAt.substring(0, 16)
2744
+ utcMinute(h.createdAt)
2709
2745
  ]),
2710
2746
  { truncate: 20 }
2711
2747
  );
@@ -2745,15 +2781,19 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2745
2781
  process.exit(1);
2746
2782
  }
2747
2783
  });
2748
- history.command("stats").description("Get execution statistics").option("--group-id <groupId>", "Filter by policy group").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").addHelpText(
2784
+ history.command("stats").description("Get execution statistics").option("--group-id <groupId>", "Filter by policy group").option("--start-date <date>", START_DAY).option("--end-date <date>", END_DAY).addHelpText(
2749
2785
  "after",
2750
2786
  dedent10`
2751
2787
 
2752
2788
  Shows total executions, success/no-match/failure counts, success rate, and avg latency.
2753
2789
 
2790
+ Without dates the window is the last ${STATS_DEFAULT_WINDOW_DAYS} days through today, in the organization
2791
+ time zone. At most ${STATS_MAX_WINDOW_DAYS} days per call; a longer range is rejected with AN-039.
2792
+ --start-date alone runs through today.
2793
+
2754
2794
  Example:
2755
2795
  $ lexq history stats --format table
2756
- $ lexq history stats --group-id <gid> --start-date 2026-04-01
2796
+ $ lexq history stats --group-id <gid> --start-date 2026-04-01 --end-date 2026-04-30
2757
2797
  `
2758
2798
  ).action(async (opts) => {
2759
2799
  try {
@@ -2840,7 +2880,7 @@ function registerReplayCommands(program) {
2840
2880
  process.exit(1);
2841
2881
  }
2842
2882
  });
2843
- replay.command("start").description("Submit a window replay job (blast radius)").requiredOption("--version-id <versionId>", "Candidate version to re-evaluate against").requiredOption("--from <date>", "Window start date (yyyy-MM-dd)").requiredOption("--to <date>", "Window end date (yyyy-MM-dd)").option("--max-records <number>", `Sample cap (hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`).addHelpText(
2883
+ replay.command("start").description("Submit a window replay job (blast radius)").requiredOption("--version-id <versionId>", "Candidate version to re-evaluate against").requiredOption("--from <date>", WINDOW_START_DAY).requiredOption("--to <date>", WINDOW_END_DAY).option("--max-records <number>", `Sample cap (hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`).addHelpText(
2844
2884
  "after",
2845
2885
  dedent11`
2846
2886
 
@@ -2884,7 +2924,7 @@ function registerReplayCommands(program) {
2884
2924
  });
2885
2925
  if (format === "table") {
2886
2926
  printTable(
2887
- ["Job", "Version", "Window", "Status", "Progress", "Changed", "At"],
2927
+ ["Job", "Version", "Window (org time zone)", "Status", "Progress", "Changed", "At"],
2888
2928
  data.content.map((j) => [
2889
2929
  j.jobId.substring(0, 12),
2890
2930
  j.candidateVersionName ?? "\u2013",
@@ -2892,7 +2932,7 @@ function registerReplayCommands(program) {
2892
2932
  j.status,
2893
2933
  `${j.progress}%`,
2894
2934
  `${j.changedCount}${j.capped ? " (capped)" : ""}`,
2895
- j.createdAt.substring(0, 16)
2935
+ utcMinute(j.createdAt)
2896
2936
  ]),
2897
2937
  { truncate: 24 }
2898
2938
  );
@@ -2998,7 +3038,7 @@ function registerProvenanceCommands(program) {
2998
3038
  process.exit(1);
2999
3039
  }
3000
3040
  });
3001
- provenance.command("reveal-audits").description("List PII reveal audits (who revealed what, when)").option("--trace-id <traceId>", "Filter by trace ID (exact)").option("--fact-key <factKey>", "Filter by fact key (partial, case-insensitive)").option("--revealed-by <operatorId>", "Filter by operator ID (exact)").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
3041
+ provenance.command("reveal-audits").description("List PII reveal audits (who revealed what, when)").option("--trace-id <traceId>", "Filter by trace ID (exact)").option("--fact-key <factKey>", "Filter by fact key (partial, case-insensitive)").option("--revealed-by <operatorId>", "Filter by operator ID (exact)").option("--start-date <date>", START_DAY).option("--end-date <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
3002
3042
  "after",
3003
3043
  dedent12`
3004
3044
 
@@ -3032,7 +3072,7 @@ function registerProvenanceCommands(program) {
3032
3072
  printTable(
3033
3073
  ["Revealed At", "By", "Fact Key", "Trace"],
3034
3074
  data.content.map((a) => [
3035
- a.revealedAt.substring(0, 16),
3075
+ utcMinute(a.revealedAt),
3036
3076
  a.revealedByName,
3037
3077
  a.factKey,
3038
3078
  a.traceId.substring(0, 12)
@@ -3070,10 +3110,13 @@ function registerLogCommands(program) {
3070
3110
  Statuses: PENDING (needs attention), RESOLVED, IGNORED
3071
3111
  `
3072
3112
  );
3073
- logs.command("list").description("List failure logs").option("--task-type <taskType>", `Filter by task type (${TaskType.join(", ")})`).option("--status <status>", "Filter by status (PENDING, RESOLVED, IGNORED)").option("--keyword <keyword>", "Search keyword").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
3113
+ logs.command("list").description("List failure logs").option("--task-type <taskType>", `Filter by task type (${TaskType.join(", ")})`).option("--status <status>", "Filter by status (PENDING, RESOLVED, IGNORED)").option("--keyword <keyword>", "Search keyword").option("--start-date <date>", START_DAY).option("--end-date <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
3074
3114
  "after",
3075
3115
  dedent13`
3076
3116
 
3117
+ Without dates the window is ${FAILURE_LOG_LOOKBACK_DAYS} days back through today, in the organization
3118
+ time zone. Pass --start-date to look further back.
3119
+
3077
3120
  Examples:
3078
3121
  $ lexq logs list --status PENDING --format table
3079
3122
  $ lexq logs list --task-type PLATFORM_WEBHOOK
@@ -3104,7 +3147,7 @@ function registerLogCommands(program) {
3104
3147
  l.taskType,
3105
3148
  l.status,
3106
3149
  l.errorMessage?.substring(0, 24) ?? "\u2013",
3107
- l.createdAt.substring(0, 16)
3150
+ utcMinute(l.createdAt)
3108
3151
  ]),
3109
3152
  { truncate: 24 }
3110
3153
  );
@@ -3502,7 +3545,7 @@ function registerStatusTools(server, callApi) {
3502
3545
  {
3503
3546
  title: "Who Am I",
3504
3547
  annotations: { readOnlyHint: true },
3505
- description: "Show current authentication info (tenant ID, user ID, role).",
3548
+ description: 'Show current authentication info (tenant ID, user ID, role) and tenantTimezone, the organization time zone that every yyyy-MM-dd date filter uses. Work out "today" or "yesterday" in that zone, not in UTC.',
3506
3549
  inputSchema: z.object({})
3507
3550
  },
3508
3551
  async () => callApi("GET", "whoami")
@@ -3700,8 +3743,8 @@ function registerVersionTools(server, callApi) {
3700
3743
  inputSchema: z3.object({
3701
3744
  groupId: z3.string().uuid().describe("Policy group ID"),
3702
3745
  commitMessage: z3.string().optional().describe("Commit message describing this version"),
3703
- effectiveFrom: z3.string().optional().describe("Effective start date (ISO 8601)"),
3704
- effectiveTo: z3.string().optional().describe("Effective end date (ISO 8601)")
3746
+ effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT}`),
3747
+ effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT}`)
3705
3748
  })
3706
3749
  },
3707
3750
  async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/versions`, { body })
@@ -3711,13 +3754,15 @@ function registerVersionTools(server, callApi) {
3711
3754
  {
3712
3755
  title: "Update Policy Version",
3713
3756
  annotations: { readOnlyHint: false, destructiveHint: true },
3714
- description: "Update a DRAFT version. Only DRAFT versions can be modified. Only provided fields are changed.",
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.",
3715
3758
  inputSchema: z3.object({
3716
3759
  groupId: z3.string().uuid().describe("Policy group ID"),
3717
3760
  versionId: z3.string().uuid().describe("Version ID"),
3718
3761
  commitMessage: z3.string().optional().describe("New commit message"),
3719
- effectiveFrom: z3.string().optional().describe("New effective start date"),
3720
- effectiveTo: z3.string().optional().describe("New effective end date")
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.")
3721
3766
  })
3722
3767
  },
3723
3768
  async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
@@ -4060,7 +4105,7 @@ function registerFactTools(server, callApi) {
4060
4105
  {
4061
4106
  title: "List Unregistered Facts",
4062
4107
  annotations: { readOnlyHint: true },
4063
- 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.",
4064
4109
  inputSchema: z5.object({
4065
4110
  groupId: z5.string().uuid().describe("Policy group ID"),
4066
4111
  versionId: z5.string().uuid().describe("Version ID")
@@ -4093,7 +4138,7 @@ function registerDeployTools(server, callApi) {
4093
4138
  {
4094
4139
  title: "Publish Version",
4095
4140
  annotations: { readOnlyHint: false, destructiveHint: false },
4096
- 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.`,
4097
4142
  inputSchema: z6.object({
4098
4143
  groupId: z6.string().uuid().describe("Policy group ID"),
4099
4144
  versionId: z6.string().uuid().describe("Version ID to publish"),
@@ -4107,7 +4152,7 @@ function registerDeployTools(server, callApi) {
4107
4152
  {
4108
4153
  title: "Deploy to Live",
4109
4154
  annotations: { readOnlyHint: false, destructiveHint: true },
4110
- 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.",
4111
4156
  inputSchema: z6.object({
4112
4157
  groupId: z6.string().uuid().describe("Policy group ID"),
4113
4158
  versionId: z6.string().uuid().describe("Version ID to deploy"),
@@ -4123,7 +4168,7 @@ function registerDeployTools(server, callApi) {
4123
4168
  {
4124
4169
  title: "Rollback Deployment",
4125
4170
  annotations: { readOnlyHint: false, destructiveHint: true },
4126
- 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).",
4127
4172
  inputSchema: z6.object({
4128
4173
  groupId: z6.string().uuid().describe("Policy group ID"),
4129
4174
  memo: z6.string().min(1).describe("Rollback reason (required)")
@@ -4153,7 +4198,7 @@ function registerDeployTools(server, callApi) {
4153
4198
  {
4154
4199
  title: "Schedule Deployment",
4155
4200
  annotations: { readOnlyHint: false, destructiveHint: false },
4156
- description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it. The snapshot hash is sealed at scheduling and re-verified at execution (fail-closed).",
4201
+ description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). The time is the version's effectiveFrom, set on the DRAFT before publishing; this call takes no time of its own. One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it. The snapshot hash is sealed at scheduling and re-verified at execution (fail-closed).",
4157
4202
  inputSchema: z6.object({
4158
4203
  groupId: z6.string().uuid().describe("Policy group ID"),
4159
4204
  versionId: z6.string().uuid().describe("ACTIVE version ID with a future effective start date"),
@@ -4200,8 +4245,8 @@ function registerDeployTools(server, callApi) {
4200
4245
  size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
4201
4246
  groupId: z6.string().uuid().optional().describe("Filter by group ID"),
4202
4247
  types: z6.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
4203
- startDate: z6.string().optional().describe("Start date (yyyy-MM-dd)"),
4204
- endDate: z6.string().optional().describe("End date (yyyy-MM-dd)")
4248
+ startDate: z6.string().optional().describe(START_DAY),
4249
+ endDate: z6.string().optional().describe(END_DAY)
4205
4250
  })
4206
4251
  },
4207
4252
  async ({ page, size, groupId, types, startDate, endDate }) => {
@@ -4345,7 +4390,8 @@ function registerAnalyticsTools(server, callApi) {
4345
4390
  Start an Impact Simulation against historical, uploaded, or inline data.
4346
4391
 
4347
4392
  dataset.type and dataset.source are BOTH required, and must be paired:
4348
- HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd)
4393
+ HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
4394
+ in the organization time zone, both inclusive)
4349
4395
  UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
4350
4396
  MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
4351
4397
 
@@ -4400,8 +4446,8 @@ function registerAnalyticsTools(server, callApi) {
4400
4446
  page: z7.number().int().min(0).default(0).describe("Page number"),
4401
4447
  size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
4402
4448
  status: z7.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
4403
- from: z7.string().optional().describe("Start date (yyyy-MM-dd)"),
4404
- to: z7.string().optional().describe("End date (yyyy-MM-dd)")
4449
+ from: z7.string().optional().describe(START_DAY),
4450
+ to: z7.string().optional().describe(END_DAY)
4405
4451
  })
4406
4452
  },
4407
4453
  async ({ page, size, status, from, to }) => {
@@ -4579,8 +4625,8 @@ function registerReplayTools(server, callApi) {
4579
4625
  description: "Submit an async job that replays a date window of past executions against a candidate version and measures the blast radius (how many decisions change). Billed per replayed record (REPLAY metric); VIEWER role cannot submit. Poll with lexq_replay_status.",
4580
4626
  inputSchema: z9.object({
4581
4627
  candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against"),
4582
- from: z9.string().describe("Window start date (yyyy-MM-dd)"),
4583
- to: z9.string().describe("Window end date (yyyy-MM-dd)"),
4628
+ from: z9.string().describe(WINDOW_START_DAY),
4629
+ to: z9.string().describe(WINDOW_END_DAY),
4584
4630
  maxRecords: z9.number().int().min(1).optional().describe(
4585
4631
  `Sample cap (server default applies; hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`
4586
4632
  )
@@ -4656,8 +4702,8 @@ function registerHistoryTools(server, callApi) {
4656
4702
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
4657
4703
  versionId: z10.string().uuid().optional().describe("Filter by version"),
4658
4704
  status: z10.enum(["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"]).optional().describe("Filter by execution status"),
4659
- startDate: z10.string().optional().describe("Start date (yyyy-MM-dd)"),
4660
- endDate: z10.string().optional().describe("End date (yyyy-MM-dd)")
4705
+ startDate: z10.string().optional().describe(START_DAY),
4706
+ endDate: z10.string().optional().describe(END_DAY)
4661
4707
  })
4662
4708
  },
4663
4709
  async ({ page, size, traceId, groupId, versionId, status, startDate, endDate }) => {
@@ -4688,11 +4734,11 @@ function registerHistoryTools(server, callApi) {
4688
4734
  {
4689
4735
  title: "Execution Statistics",
4690
4736
  annotations: { readOnlyHint: true },
4691
- description: "Get execution KPIs: total executions, success/failure counts, success rate, and average latency.",
4737
+ description: `Get execution KPIs: total executions, success/failure counts, success rate, and average latency. Without dates the window is the last ${STATS_DEFAULT_WINDOW_DAYS} days through today, in the organization time zone. At most ${STATS_MAX_WINDOW_DAYS} days per call; a longer range is rejected with AN-039. startDate alone runs through today.`,
4692
4738
  inputSchema: z10.object({
4693
4739
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
4694
- startDate: z10.string().optional().describe("Start date (yyyy-MM-dd)"),
4695
- endDate: z10.string().optional().describe("End date (yyyy-MM-dd)")
4740
+ startDate: z10.string().optional().describe(START_DAY),
4741
+ endDate: z10.string().optional().describe(END_DAY)
4696
4742
  })
4697
4743
  },
4698
4744
  async ({ groupId, startDate, endDate }) => {
@@ -4732,8 +4778,8 @@ function registerProvenanceTools(server, callApi) {
4732
4778
  traceId: z11.string().optional().describe("Filter by trace ID (exact match)"),
4733
4779
  revealedBy: z11.string().optional().describe("Filter by operator ID (exact match)"),
4734
4780
  factKey: z11.string().optional().describe("Filter by fact key (partial match, case-insensitive)"),
4735
- startDate: z11.string().optional().describe("Start date (yyyy-MM-dd)"),
4736
- endDate: z11.string().optional().describe("End date (yyyy-MM-dd)")
4781
+ startDate: z11.string().optional().describe(START_DAY),
4782
+ endDate: z11.string().optional().describe(END_DAY)
4737
4783
  })
4738
4784
  },
4739
4785
  async ({ page, size, traceId, revealedBy, factKey, startDate, endDate }) => {
@@ -4756,15 +4802,15 @@ function registerLogTools(server, callApi) {
4756
4802
  {
4757
4803
  title: "List Failure Logs",
4758
4804
  annotations: { readOnlyHint: true },
4759
- description: "List system failure logs from background tasks (platform event webhooks, scheduled deployments).",
4805
+ description: `List system failure logs from background tasks (platform event webhooks, scheduled deployments). Without dates the window is ${FAILURE_LOG_LOOKBACK_DAYS} days back through today, in the organization time zone; pass startDate to look further back.`,
4760
4806
  inputSchema: z12.object({
4761
4807
  page: z12.number().int().min(0).default(0).describe("Page number"),
4762
4808
  size: z12.number().int().min(1).max(100).default(20).describe("Page size"),
4763
4809
  taskType: z12.enum(TaskType).optional().describe("Task type"),
4764
4810
  status: z12.enum(FailureStatus).optional().describe("Log status"),
4765
4811
  keyword: z12.string().optional().describe("Search in refId, refSubId, errorMessage"),
4766
- startDate: z12.string().optional().describe("Start date (yyyy-MM-dd)"),
4767
- endDate: z12.string().optional().describe("End date (yyyy-MM-dd)")
4812
+ startDate: z12.string().optional().describe(START_DAY),
4813
+ endDate: z12.string().optional().describe(END_DAY)
4768
4814
  })
4769
4815
  },
4770
4816
  async ({ page, size, taskType, status, keyword, startDate, endDate }) => {
@@ -6,7 +6,7 @@ function registerStatusTools(server, callApi) {
6
6
  {
7
7
  title: "Who Am I",
8
8
  annotations: { readOnlyHint: true },
9
- description: "Show current authentication info (tenant ID, user ID, role).",
9
+ description: 'Show current authentication info (tenant ID, user ID, role) and tenantTimezone, the organization time zone that every yyyy-MM-dd date filter uses. Work out "today" or "yesterday" in that zone, not in UTC.',
10
10
  inputSchema: z.object({})
11
11
  },
12
12
  async () => callApi("GET", "whoami")
@@ -259,6 +259,17 @@ function formatUnregisteredFactWarning(meta) {
259
259
  ].join("\n");
260
260
  }
261
261
 
262
+ // src/lib/dates.ts
263
+ var ORG_DAY = "yyyy-MM-dd, a whole calendar day in the organization time zone (tenantTimezone in whoami), not UTC";
264
+ var START_DAY = `Start date, ${ORG_DAY}. Inclusive.`;
265
+ var END_DAY = `End date, ${ORG_DAY}. Inclusive: the whole day counts.`;
266
+ var WINDOW_START_DAY = `Window start date, ${ORG_DAY}. Inclusive.`;
267
+ var WINDOW_END_DAY = `Window end date, ${ORG_DAY}. Inclusive: the whole day counts.`;
268
+ var INSTANT = "ISO-8601 instant with Z or an offset, e.g. 2026-10-01T00:00:00Z or 2026-10-01T09:00:00+09:00. A bare date or a time without an offset is rejected.";
269
+ var STATS_DEFAULT_WINDOW_DAYS = 30;
270
+ var STATS_MAX_WINDOW_DAYS = 31;
271
+ var FAILURE_LOG_LOOKBACK_DAYS = 7;
272
+
262
273
  // src/mcp/tools/versions.ts
263
274
  function registerVersionTools(server, callApi) {
264
275
  server.registerTool(
@@ -299,8 +310,8 @@ function registerVersionTools(server, callApi) {
299
310
  inputSchema: z3.object({
300
311
  groupId: z3.string().uuid().describe("Policy group ID"),
301
312
  commitMessage: z3.string().optional().describe("Commit message describing this version"),
302
- effectiveFrom: z3.string().optional().describe("Effective start date (ISO 8601)"),
303
- effectiveTo: z3.string().optional().describe("Effective end date (ISO 8601)")
313
+ effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT}`),
314
+ effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT}`)
304
315
  })
305
316
  },
306
317
  async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/versions`, { body })
@@ -310,13 +321,15 @@ function registerVersionTools(server, callApi) {
310
321
  {
311
322
  title: "Update Policy Version",
312
323
  annotations: { readOnlyHint: false, destructiveHint: true },
313
- description: "Update a DRAFT version. Only DRAFT versions can be modified. Only provided fields are changed.",
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.",
314
325
  inputSchema: z3.object({
315
326
  groupId: z3.string().uuid().describe("Policy group ID"),
316
327
  versionId: z3.string().uuid().describe("Version ID"),
317
328
  commitMessage: z3.string().optional().describe("New commit message"),
318
- effectiveFrom: z3.string().optional().describe("New effective start date"),
319
- effectiveTo: z3.string().optional().describe("New effective end date")
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.")
320
333
  })
321
334
  },
322
335
  async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
@@ -663,7 +676,7 @@ function registerFactTools(server, callApi) {
663
676
  {
664
677
  title: "List Unregistered Facts",
665
678
  annotations: { readOnlyHint: true },
666
- 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.",
667
680
  inputSchema: z5.object({
668
681
  groupId: z5.string().uuid().describe("Policy group ID"),
669
682
  versionId: z5.string().uuid().describe("Version ID")
@@ -696,7 +709,7 @@ function registerDeployTools(server, callApi) {
696
709
  {
697
710
  title: "Publish Version",
698
711
  annotations: { readOnlyHint: false, destructiveHint: false },
699
- 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.`,
700
713
  inputSchema: z6.object({
701
714
  groupId: z6.string().uuid().describe("Policy group ID"),
702
715
  versionId: z6.string().uuid().describe("Version ID to publish"),
@@ -710,7 +723,7 @@ function registerDeployTools(server, callApi) {
710
723
  {
711
724
  title: "Deploy to Live",
712
725
  annotations: { readOnlyHint: false, destructiveHint: true },
713
- 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.",
714
727
  inputSchema: z6.object({
715
728
  groupId: z6.string().uuid().describe("Policy group ID"),
716
729
  versionId: z6.string().uuid().describe("Version ID to deploy"),
@@ -726,7 +739,7 @@ function registerDeployTools(server, callApi) {
726
739
  {
727
740
  title: "Rollback Deployment",
728
741
  annotations: { readOnlyHint: false, destructiveHint: true },
729
- 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).",
730
743
  inputSchema: z6.object({
731
744
  groupId: z6.string().uuid().describe("Policy group ID"),
732
745
  memo: z6.string().min(1).describe("Rollback reason (required)")
@@ -756,7 +769,7 @@ function registerDeployTools(server, callApi) {
756
769
  {
757
770
  title: "Schedule Deployment",
758
771
  annotations: { readOnlyHint: false, destructiveHint: false },
759
- description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it. The snapshot hash is sealed at scheduling and re-verified at execution (fail-closed).",
772
+ description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). The time is the version's effectiveFrom, set on the DRAFT before publishing; this call takes no time of its own. One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it. The snapshot hash is sealed at scheduling and re-verified at execution (fail-closed).",
760
773
  inputSchema: z6.object({
761
774
  groupId: z6.string().uuid().describe("Policy group ID"),
762
775
  versionId: z6.string().uuid().describe("ACTIVE version ID with a future effective start date"),
@@ -803,8 +816,8 @@ function registerDeployTools(server, callApi) {
803
816
  size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
804
817
  groupId: z6.string().uuid().optional().describe("Filter by group ID"),
805
818
  types: z6.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
806
- startDate: z6.string().optional().describe("Start date (yyyy-MM-dd)"),
807
- endDate: z6.string().optional().describe("End date (yyyy-MM-dd)")
819
+ startDate: z6.string().optional().describe(START_DAY),
820
+ endDate: z6.string().optional().describe(END_DAY)
808
821
  })
809
822
  },
810
823
  async ({ page, size, groupId, types, startDate, endDate }) => {
@@ -948,7 +961,8 @@ function registerAnalyticsTools(server, callApi) {
948
961
  Start an Impact Simulation against historical, uploaded, or inline data.
949
962
 
950
963
  dataset.type and dataset.source are BOTH required, and must be paired:
951
- HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd)
964
+ HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
965
+ in the organization time zone, both inclusive)
952
966
  UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
953
967
  MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
954
968
 
@@ -1003,8 +1017,8 @@ function registerAnalyticsTools(server, callApi) {
1003
1017
  page: z7.number().int().min(0).default(0).describe("Page number"),
1004
1018
  size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
1005
1019
  status: z7.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
1006
- from: z7.string().optional().describe("Start date (yyyy-MM-dd)"),
1007
- to: z7.string().optional().describe("End date (yyyy-MM-dd)")
1020
+ from: z7.string().optional().describe(START_DAY),
1021
+ to: z7.string().optional().describe(END_DAY)
1008
1022
  })
1009
1023
  },
1010
1024
  async ({ page, size, status, from, to }) => {
@@ -1182,8 +1196,8 @@ function registerReplayTools(server, callApi) {
1182
1196
  description: "Submit an async job that replays a date window of past executions against a candidate version and measures the blast radius (how many decisions change). Billed per replayed record (REPLAY metric); VIEWER role cannot submit. Poll with lexq_replay_status.",
1183
1197
  inputSchema: z9.object({
1184
1198
  candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against"),
1185
- from: z9.string().describe("Window start date (yyyy-MM-dd)"),
1186
- to: z9.string().describe("Window end date (yyyy-MM-dd)"),
1199
+ from: z9.string().describe(WINDOW_START_DAY),
1200
+ to: z9.string().describe(WINDOW_END_DAY),
1187
1201
  maxRecords: z9.number().int().min(1).optional().describe(
1188
1202
  `Sample cap (server default applies; hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`
1189
1203
  )
@@ -1259,8 +1273,8 @@ function registerHistoryTools(server, callApi) {
1259
1273
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
1260
1274
  versionId: z10.string().uuid().optional().describe("Filter by version"),
1261
1275
  status: z10.enum(["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"]).optional().describe("Filter by execution status"),
1262
- startDate: z10.string().optional().describe("Start date (yyyy-MM-dd)"),
1263
- endDate: z10.string().optional().describe("End date (yyyy-MM-dd)")
1276
+ startDate: z10.string().optional().describe(START_DAY),
1277
+ endDate: z10.string().optional().describe(END_DAY)
1264
1278
  })
1265
1279
  },
1266
1280
  async ({ page, size, traceId, groupId, versionId, status, startDate, endDate }) => {
@@ -1291,11 +1305,11 @@ function registerHistoryTools(server, callApi) {
1291
1305
  {
1292
1306
  title: "Execution Statistics",
1293
1307
  annotations: { readOnlyHint: true },
1294
- description: "Get execution KPIs: total executions, success/failure counts, success rate, and average latency.",
1308
+ description: `Get execution KPIs: total executions, success/failure counts, success rate, and average latency. Without dates the window is the last ${STATS_DEFAULT_WINDOW_DAYS} days through today, in the organization time zone. At most ${STATS_MAX_WINDOW_DAYS} days per call; a longer range is rejected with AN-039. startDate alone runs through today.`,
1295
1309
  inputSchema: z10.object({
1296
1310
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
1297
- startDate: z10.string().optional().describe("Start date (yyyy-MM-dd)"),
1298
- endDate: z10.string().optional().describe("End date (yyyy-MM-dd)")
1311
+ startDate: z10.string().optional().describe(START_DAY),
1312
+ endDate: z10.string().optional().describe(END_DAY)
1299
1313
  })
1300
1314
  },
1301
1315
  async ({ groupId, startDate, endDate }) => {
@@ -1335,8 +1349,8 @@ function registerProvenanceTools(server, callApi) {
1335
1349
  traceId: z11.string().optional().describe("Filter by trace ID (exact match)"),
1336
1350
  revealedBy: z11.string().optional().describe("Filter by operator ID (exact match)"),
1337
1351
  factKey: z11.string().optional().describe("Filter by fact key (partial match, case-insensitive)"),
1338
- startDate: z11.string().optional().describe("Start date (yyyy-MM-dd)"),
1339
- endDate: z11.string().optional().describe("End date (yyyy-MM-dd)")
1352
+ startDate: z11.string().optional().describe(START_DAY),
1353
+ endDate: z11.string().optional().describe(END_DAY)
1340
1354
  })
1341
1355
  },
1342
1356
  async ({ page, size, traceId, revealedBy, factKey, startDate, endDate }) => {
@@ -1359,15 +1373,15 @@ function registerLogTools(server, callApi) {
1359
1373
  {
1360
1374
  title: "List Failure Logs",
1361
1375
  annotations: { readOnlyHint: true },
1362
- description: "List system failure logs from background tasks (platform event webhooks, scheduled deployments).",
1376
+ description: `List system failure logs from background tasks (platform event webhooks, scheduled deployments). Without dates the window is ${FAILURE_LOG_LOOKBACK_DAYS} days back through today, in the organization time zone; pass startDate to look further back.`,
1363
1377
  inputSchema: z12.object({
1364
1378
  page: z12.number().int().min(0).default(0).describe("Page number"),
1365
1379
  size: z12.number().int().min(1).max(100).default(20).describe("Page size"),
1366
1380
  taskType: z12.enum(TaskType).optional().describe("Task type"),
1367
1381
  status: z12.enum(FailureStatus).optional().describe("Log status"),
1368
1382
  keyword: z12.string().optional().describe("Search in refId, refSubId, errorMessage"),
1369
- startDate: z12.string().optional().describe("Start date (yyyy-MM-dd)"),
1370
- endDate: z12.string().optional().describe("End date (yyyy-MM-dd)")
1383
+ startDate: z12.string().optional().describe(START_DAY),
1384
+ endDate: z12.string().optional().describe(END_DAY)
1371
1385
  })
1372
1386
  },
1373
1387
  async ({ page, size, taskType, status, keyword, startDate, endDate }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "1.0.1",
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",
@@ -23,8 +23,11 @@ Filter options:
23
23
  | `--group-id <id>` | Filter by policy group | `--group-id <gid>` |
24
24
  | `--version-id <id>` | Filter by version | `--version-id <vid>` |
25
25
  | `--status <status>` | Filter by status | `--status SUCCESS` |
26
- | `--start-date <date>` | Start date (ISO) | `--start-date 2025-01-01` |
27
- | `--end-date <date>` | End date (ISO) | `--end-date 2025-01-31` |
26
+ | `--start-date <date>` | First day, inclusive | `--start-date 2025-01-01` |
27
+ | `--end-date <date>` | Last day, inclusive | `--end-date 2025-01-31` |
28
+
29
+ Dates are `yyyy-MM-dd`, whole days in the organization time zone (`tenantTimezone` in
30
+ `lexq auth whoami`), not UTC.
28
31
 
29
32
  ### Execution Statuses
30
33
 
@@ -53,6 +56,10 @@ Returns full detail including `inputFacts`, `executionTraces`, and `decisionTrac
53
56
  lexq history stats
54
57
  ```
55
58
 
59
+ Without dates the window is the last 30 days through today, in the organization time zone. At most
60
+ 31 days per call; a longer range is rejected with `AN-039`. `--start-date` alone runs through
61
+ today.
62
+
56
63
  Returns KPI summary:
57
64
 
58
65
  ```json
@@ -98,8 +105,12 @@ Filter options:
98
105
  | `--task-type <type>` | Task type | `PLATFORM_WEBHOOK`, `SCHEDULED_DEPLOYMENT` |
99
106
  | `--status <status>` | Log status | `PENDING`, `RESOLVED`, `IGNORED` |
100
107
  | `--keyword <kw>` | Search in refId, refSubId, errorMessage | any string |
101
- | `--start-date <date>` | Start date (ISO) | `2025-01-01` |
102
- | `--end-date <date>` | End date (ISO) | `2025-01-31` |
108
+ | `--start-date <date>` | First day, inclusive | `2025-01-01` |
109
+ | `--end-date <date>` | Last day, inclusive | `2025-01-31` |
110
+
111
+ Without dates the list covers 7 days back through today, in the organization time zone. A failure
112
+ older than that is left out even while it is still `PENDING`. Pass an earlier `--start-date` to
113
+ see it.
103
114
 
104
115
  ### Get Failure Log Detail
105
116
 
@@ -242,12 +253,12 @@ lexq profile <groupId>
242
253
  lexq profile <groupId> --rule <ruleId>
243
254
 
244
255
  # Defaults: live version, last 24h, cache HIT
245
- lexq profile <groupId> --version <vid> --cache MISS \
256
+ lexq profile <groupId> --version-id <vid> --cache MISS \
246
257
  --from 2026-07-01T00:00:00Z --to 2026-08-01T00:00:00Z
247
258
  ```
248
259
 
249
260
  `--from` / `--to` are ISO-8601 **instants** here (`2026-07-01T00:00:00Z`), unlike `replay start`
250
- which takes `yyyy-MM-dd` dates.
261
+ which takes `yyyy-MM-dd` days in the organization time zone.
251
262
 
252
263
  `p50` / `p95` / `p99` are `null` when the sample is under 100 — the engine reports insufficient
253
264
  data rather than inventing a number. `n` is always present.
@@ -264,7 +275,7 @@ lexq history list --status ERROR --page 0 --size 10
264
275
  # 3. For a specific bad decision, get the deterministic why
265
276
  lexq provenance get --trace-id <traceId>
266
277
 
267
- # 4. Check for system failures
278
+ # 4. Check for system failures (covers 7 days back by default; add --start-date for older ones)
268
279
  lexq logs list --status PENDING --page 0 --size 10
269
280
 
270
281
  # 5. Inspect and mark
@@ -390,6 +390,10 @@ lexq logs bulk-action --ids "id4,id5" --action IGNORE
390
390
  lexq logs list --status PENDING --page 0 --size 10
391
391
  ```
392
392
 
393
+ Without `--start-date`, `logs list` covers 7 days back through today, in the organization time
394
+ zone. An empty list in step 5 means there is no PENDING failure in that window, not that there is
395
+ none at all. Add an earlier `--start-date` to steps 1 and 5 to see older ones.
396
+
393
397
  `RESOLVE` and `IGNORE` differ only in intent — both remove the log from PENDING. Use `RESOLVE`
394
398
  when the underlying problem was fixed, `IGNORE` when it does not need fixing.
395
399
 
@@ -153,7 +153,12 @@ Prefixes you will encounter through the CLI:
153
153
  1. **Fact keys start with a letter,** then letters, numbers, and underscores. Casing is yours to
154
154
  choose and keys are case-sensitive. These docs use `camelCase`: `paymentAmount`, `customerTier`.
155
155
  2. **IDs are UUIDs.** Always copy the full ID from list/create output — do not guess.
156
- 3. **Dates use ISO 8601.** Example: `2025-01-01T00:00:00Z`. Time zone is UTC.
156
+ 3. **Dates come in two shapes.** A date filter (`--start-date`, `--end-date`, `--from` / `--to` on
157
+ `replay start` and `simulation list`) takes `yyyy-MM-dd` and means a whole calendar day in the
158
+ organization time zone, not UTC. `lexq auth whoami` shows that zone as `tenantTimezone`. An
159
+ instant (`--effective-from`, `--effective-to`, `--from` / `--to` on `profile`) takes ISO 8601
160
+ with `Z` or an offset: `2026-10-01T00:00:00Z` or `2026-10-01T09:00:00+09:00`. Tables print
161
+ times in UTC with a trailing `Z`.
157
162
  4. **JSON bodies via `--json`.** Most create/update commands accept `--json '<body>'` for the request body.
158
163
  5. **File input via `--file`.** `analytics dry-run`, `dry-run-compare`, `simulation start` accept
159
164
  `--file path/to/body.json` as an alternative to `--json`. `analytics dataset upload` requires
@@ -251,6 +251,9 @@ lexq analytics simulation start --json '{
251
251
  | `MANUAL` | `REQUEST_BODY` | Provide `manualData` array in the request |
252
252
  | `UPLOADED` | `S3_BUCKET` | Reference an uploaded dataset by `path` |
253
253
 
254
+ `HISTORICAL` takes `from` / `to` as `yyyy-MM-dd`, whole days in the organization time zone, both
255
+ inclusive.
256
+
254
257
  ### File Upload Dataset
255
258
 
256
259
  ```bash