@lexq/cli 1.0.0 → 1.0.2

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
@@ -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);
@@ -347,10 +349,32 @@ var SLOW_MULTIPLIER = 10;
347
349
  var MIN_SAMPLES = 100;
348
350
  var MIN_COHORT_SIZE = 3;
349
351
  var PROFILE_DEFAULT_WINDOW_HOURS = 24;
352
+ var MAX_GROUPS_PER_TENANT = 20;
353
+ var MAX_DRAFT_VERSIONS_PER_GROUP = 20;
354
+ var MAX_RULES_PER_VERSION = 100;
355
+ var MAX_CONDITIONS_PER_RULE = 25;
356
+ var MAX_CONDITION_DEPTH = 10;
357
+ var MAX_SNAPSHOT_BYTES = 131072;
350
358
 
351
359
  // src/commands/groups.ts
352
360
  import "commander";
353
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
354
378
  function registerGroupCommands(program) {
355
379
  const groups = program.command("groups").description("Manage policy groups").addHelpText(
356
380
  "after",
@@ -390,7 +414,7 @@ function registerGroupCommands(program) {
390
414
  g.status,
391
415
  String(g.priority),
392
416
  g.currentVersionName ?? "\u2013",
393
- g.updatedAt.substring(0, 10)
417
+ utcMinute(g.updatedAt)
394
418
  ]),
395
419
  { truncate: 24 }
396
420
  );
@@ -708,7 +732,7 @@ function registerVersionCommands(program) {
708
732
  v.status,
709
733
  v.commitMessage ?? "\u2013",
710
734
  v.snapshotHash ? v.snapshotHash.substring(0, 8) : "\u2013",
711
- v.createdAt.substring(0, 10)
735
+ utcMinute(v.createdAt)
712
736
  ]),
713
737
  { truncate: 24 }
714
738
  );
@@ -741,7 +765,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
741
765
  process.exit(1);
742
766
  }
743
767
  });
744
- 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(
745
769
  "after",
746
770
  dedent4`
747
771
 
@@ -756,8 +780,8 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
756
780
 
757
781
  Fields:
758
782
  commitMessage string Version description (optional, max 255 chars)
759
- effectiveFrom datetime Start of effective period (optional, ISO-8601)
760
- 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)
761
785
  `
762
786
  ).action(async (opts) => {
763
787
  try {
@@ -780,12 +804,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
780
804
  process.exit(1);
781
805
  }
782
806
  });
783
- 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("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
784
808
  "after",
785
809
  dedent4`
786
810
 
787
811
  Only DRAFT versions can be updated. Published (ACTIVE) versions are immutable.
788
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).
816
+
789
817
  Example:
790
818
  $ lexq versions update --group-id <gid> --id <vid> --commit-message "Updated rules"
791
819
  `
@@ -1736,7 +1764,7 @@ function registerDeployCommands(program) {
1736
1764
  process.exit(1);
1737
1765
  }
1738
1766
  });
1739
- 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(
1767
+ 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(
1740
1768
  "after",
1741
1769
  dedent7`
1742
1770
 
@@ -1768,7 +1796,7 @@ function registerDeployCommands(program) {
1768
1796
  d.policyGroupName,
1769
1797
  d.versionNo != null ? `v${d.versionNo}` : "\u2013",
1770
1798
  d.deployedByName,
1771
- d.deployedAt.substring(0, 16)
1799
+ utcMinute(d.deployedAt)
1772
1800
  ]),
1773
1801
  { truncate: 20 }
1774
1802
  );
@@ -1830,7 +1858,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1830
1858
  d.groupName,
1831
1859
  d.groupStatus,
1832
1860
  d.currentVersionName ?? "\u2013",
1833
- d.lastDeployedAt?.substring(0, 16) ?? "\u2013"
1861
+ utcMinute(d.lastDeployedAt)
1834
1862
  ])
1835
1863
  );
1836
1864
  } else {
@@ -1897,6 +1925,8 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1897
1925
 
1898
1926
  The version must be ACTIVE with a future effective start date; the system
1899
1927
  deploys it automatically at that time (within one scheduler tick, ≤60s).
1928
+ That time is the version's effectiveFrom, set on the DRAFT before publishing;
1929
+ this command takes no time of its own.
1900
1930
  One pending schedule per group. Manual deploy/rollback/undeploy, starting
1901
1931
  an A/B test, or archiving the group cancels the pending schedule.
1902
1932
 
@@ -1988,9 +2018,9 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1988
2018
  s.status,
1989
2019
  s.policyGroupName ?? s.policyGroupId.substring(0, 8),
1990
2020
  s.versionNo != null ? `v${s.versionNo}` : "\u2013",
1991
- s.scheduledFor.substring(0, 16),
2021
+ utcMinute(s.scheduledFor),
1992
2022
  s.scheduledByName,
1993
- s.status === "EXECUTED" ? s.executedAt?.substring(0, 16) ?? "\u2013" : s.status === "CANCELED" ? s.canceledReason ?? "\u2013" : s.status === "FAILED" ? s.failedReason ?? "\u2013" : "\u2013"
2023
+ s.status === "EXECUTED" ? utcMinute(s.executedAt) : s.status === "CANCELED" ? s.canceledReason ?? "\u2013" : s.status === "FAILED" ? s.failedReason ?? "\u2013" : "\u2013"
1994
2024
  ]),
1995
2025
  { truncate: 20 }
1996
2026
  );
@@ -2193,6 +2223,7 @@ function registerAnalyticsCommands(program) {
2193
2223
 
2194
2224
  Dataset types: HISTORICAL, UPLOADED, MANUAL
2195
2225
  Dataset sources: EXECUTION_LOGS, S3_BUCKET, REQUEST_BODY
2226
+ HISTORICAL from / to are yyyy-MM-dd, whole days in the organization time zone, both inclusive.
2196
2227
  `
2197
2228
  ).action(async (opts) => {
2198
2229
  try {
@@ -2309,7 +2340,7 @@ function registerAnalyticsCommands(program) {
2309
2340
  process.exit(1);
2310
2341
  }
2311
2342
  });
2312
- 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) => {
2343
+ 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) => {
2313
2344
  try {
2314
2345
  const globalOpts = program.opts();
2315
2346
  const format = globalOpts.format ?? "json";
@@ -2338,7 +2369,7 @@ function registerAnalyticsCommands(program) {
2338
2369
  s.status,
2339
2370
  `${s.matchRate.toFixed(1)}%`,
2340
2371
  String(s.totalRecords),
2341
- s.createdAt.substring(0, 16)
2372
+ utcMinute(s.createdAt)
2342
2373
  ]),
2343
2374
  { truncate: 20 }
2344
2375
  );
@@ -2659,7 +2690,7 @@ function registerHistoryCommands(program) {
2659
2690
  Statuses: SUCCESS, NO_MATCH, ERROR, TIMEOUT
2660
2691
  `
2661
2692
  );
2662
- 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(
2693
+ 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(
2663
2694
  "after",
2664
2695
  dedent10`
2665
2696
 
@@ -2699,7 +2730,7 @@ function registerHistoryCommands(program) {
2699
2730
  h.status,
2700
2731
  h.isMatched ? "\u2713" : "\u2717",
2701
2732
  `${h.latencyMs}ms`,
2702
- h.createdAt.substring(0, 16)
2733
+ utcMinute(h.createdAt)
2703
2734
  ]),
2704
2735
  { truncate: 20 }
2705
2736
  );
@@ -2739,15 +2770,19 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2739
2770
  process.exit(1);
2740
2771
  }
2741
2772
  });
2742
- 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(
2773
+ 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(
2743
2774
  "after",
2744
2775
  dedent10`
2745
2776
 
2746
2777
  Shows total executions, success/no-match/failure counts, success rate, and avg latency.
2747
2778
 
2779
+ Without dates the window is the last ${STATS_DEFAULT_WINDOW_DAYS} days through today, in the organization
2780
+ time zone. At most ${STATS_MAX_WINDOW_DAYS} days per call; a longer range is rejected with AN-039.
2781
+ --start-date alone runs through today.
2782
+
2748
2783
  Example:
2749
2784
  $ lexq history stats --format table
2750
- $ lexq history stats --group-id <gid> --start-date 2026-04-01
2785
+ $ lexq history stats --group-id <gid> --start-date 2026-04-01 --end-date 2026-04-30
2751
2786
  `
2752
2787
  ).action(async (opts) => {
2753
2788
  try {
@@ -2834,7 +2869,7 @@ function registerReplayCommands(program) {
2834
2869
  process.exit(1);
2835
2870
  }
2836
2871
  });
2837
- 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(
2872
+ 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(
2838
2873
  "after",
2839
2874
  dedent11`
2840
2875
 
@@ -2878,7 +2913,7 @@ function registerReplayCommands(program) {
2878
2913
  });
2879
2914
  if (format === "table") {
2880
2915
  printTable(
2881
- ["Job", "Version", "Window", "Status", "Progress", "Changed", "At"],
2916
+ ["Job", "Version", "Window (org time zone)", "Status", "Progress", "Changed", "At"],
2882
2917
  data.content.map((j) => [
2883
2918
  j.jobId.substring(0, 12),
2884
2919
  j.candidateVersionName ?? "\u2013",
@@ -2886,7 +2921,7 @@ function registerReplayCommands(program) {
2886
2921
  j.status,
2887
2922
  `${j.progress}%`,
2888
2923
  `${j.changedCount}${j.capped ? " (capped)" : ""}`,
2889
- j.createdAt.substring(0, 16)
2924
+ utcMinute(j.createdAt)
2890
2925
  ]),
2891
2926
  { truncate: 24 }
2892
2927
  );
@@ -2992,7 +3027,7 @@ function registerProvenanceCommands(program) {
2992
3027
  process.exit(1);
2993
3028
  }
2994
3029
  });
2995
- 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(
3030
+ 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(
2996
3031
  "after",
2997
3032
  dedent12`
2998
3033
 
@@ -3026,7 +3061,7 @@ function registerProvenanceCommands(program) {
3026
3061
  printTable(
3027
3062
  ["Revealed At", "By", "Fact Key", "Trace"],
3028
3063
  data.content.map((a) => [
3029
- a.revealedAt.substring(0, 16),
3064
+ utcMinute(a.revealedAt),
3030
3065
  a.revealedByName,
3031
3066
  a.factKey,
3032
3067
  a.traceId.substring(0, 12)
@@ -3064,10 +3099,13 @@ function registerLogCommands(program) {
3064
3099
  Statuses: PENDING (needs attention), RESOLVED, IGNORED
3065
3100
  `
3066
3101
  );
3067
- 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(
3102
+ 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(
3068
3103
  "after",
3069
3104
  dedent13`
3070
3105
 
3106
+ Without dates the window is ${FAILURE_LOG_LOOKBACK_DAYS} days back through today, in the organization
3107
+ time zone. Pass --start-date to look further back.
3108
+
3071
3109
  Examples:
3072
3110
  $ lexq logs list --status PENDING --format table
3073
3111
  $ lexq logs list --task-type PLATFORM_WEBHOOK
@@ -3098,7 +3136,7 @@ function registerLogCommands(program) {
3098
3136
  l.taskType,
3099
3137
  l.status,
3100
3138
  l.errorMessage?.substring(0, 24) ?? "\u2013",
3101
- l.createdAt.substring(0, 16)
3139
+ utcMinute(l.createdAt)
3102
3140
  ]),
3103
3141
  { truncate: 24 }
3104
3142
  );
@@ -3496,7 +3534,7 @@ function registerStatusTools(server, callApi) {
3496
3534
  {
3497
3535
  title: "Who Am I",
3498
3536
  annotations: { readOnlyHint: true },
3499
- description: "Show current authentication info (tenant ID, user ID, role).",
3537
+ 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.',
3500
3538
  inputSchema: z.object({})
3501
3539
  },
3502
3540
  async () => callApi("GET", "whoami")
@@ -3533,7 +3571,7 @@ function registerGroupTools(server, callApi) {
3533
3571
  {
3534
3572
  title: "Create Policy Group",
3535
3573
  annotations: { readOnlyHint: false, destructiveHint: false },
3536
- description: "Create a new policy group. Requires name. Priority is auto-assigned (appended last, tenant-wide); use lexq_groups_reorder to change order. Optionally set conflict resolution, activation group, and description. Policy groups that share an activationGroup form a cluster and must share the same activationMode / activationStrategy / executionLimit; executionLimit is how many of those groups run, not how many rules.",
3574
+ description: `Create a new policy group. Requires name. Priority is auto-assigned (appended last, tenant-wide); use lexq_groups_reorder to change order. Optionally set conflict resolution, activation group, and description. Policy groups that share an activationGroup form a cluster and must share the same activationMode / activationStrategy / executionLimit; executionLimit is how many of those groups run, not how many rules. A tenant can have at most ${MAX_GROUPS_PER_TENANT} groups that are not ARCHIVED; archive groups you no longer use to free a slot.`,
3537
3575
  inputSchema: z2.object({
3538
3576
  name: z2.string().describe("Group name (unique among non-ARCHIVED)"),
3539
3577
  description: z2.string().optional().describe("Group description"),
@@ -3690,12 +3728,12 @@ function registerVersionTools(server, callApi) {
3690
3728
  {
3691
3729
  title: "Create Policy Version",
3692
3730
  annotations: { readOnlyHint: false, destructiveHint: false },
3693
- description: "Create a new DRAFT version in a policy group. Optionally provide a commit message and effective date range.",
3731
+ description: `Create a new DRAFT version in a policy group. Optionally provide a commit message and effective date range. A group can hold at most ${MAX_DRAFT_VERSIONS_PER_GROUP} DRAFT versions at a time; published versions do not count. Publish or delete drafts to make room.`,
3694
3732
  inputSchema: z3.object({
3695
3733
  groupId: z3.string().uuid().describe("Policy group ID"),
3696
3734
  commitMessage: z3.string().optional().describe("Commit message describing this version"),
3697
- effectiveFrom: z3.string().optional().describe("Effective start date (ISO 8601)"),
3698
- effectiveTo: z3.string().optional().describe("Effective end date (ISO 8601)")
3735
+ effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT}`),
3736
+ effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT}`)
3699
3737
  })
3700
3738
  },
3701
3739
  async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/versions`, { body })
@@ -3705,13 +3743,13 @@ function registerVersionTools(server, callApi) {
3705
3743
  {
3706
3744
  title: "Update Policy Version",
3707
3745
  annotations: { readOnlyHint: false, destructiveHint: true },
3708
- description: "Update a DRAFT version. Only DRAFT versions can be modified. Only provided fields are changed.",
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).",
3709
3747
  inputSchema: z3.object({
3710
3748
  groupId: z3.string().uuid().describe("Policy group ID"),
3711
3749
  versionId: z3.string().uuid().describe("Version ID"),
3712
3750
  commitMessage: z3.string().optional().describe("New commit message"),
3713
- effectiveFrom: z3.string().optional().describe("New effective start date"),
3714
- effectiveTo: z3.string().optional().describe("New effective end date")
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.`)
3715
3753
  })
3716
3754
  },
3717
3755
  async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
@@ -3734,7 +3772,7 @@ function registerVersionTools(server, callApi) {
3734
3772
  {
3735
3773
  title: "Clone Policy Version",
3736
3774
  annotations: { readOnlyHint: false, destructiveHint: false },
3737
- description: "Clone an existing version to create a new DRAFT. Useful when the source version is already published.",
3775
+ description: `Clone an existing version to create a new DRAFT. Useful when the source version is already published. The new DRAFT counts toward the limit of ${MAX_DRAFT_VERSIONS_PER_GROUP} DRAFT versions per group, and a source with more than ${MAX_RULES_PER_VERSION} rules cannot be cloned.`,
3738
3776
  inputSchema: z3.object({
3739
3777
  groupId: z3.string().uuid().describe("Policy group ID"),
3740
3778
  versionId: z3.string().uuid().describe("Source version ID to clone")
@@ -3797,7 +3835,9 @@ function registerRuleTools(server, callApi) {
3797
3835
 
3798
3836
  Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
3799
3837
  Value types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
3800
-
3838
+ Limits: a condition tree may hold at most ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes; GROUP nodes do not count)
3839
+ and nest at most ${MAX_CONDITION_DEPTH} levels below the root. A version holds at most ${MAX_RULES_PER_VERSION} rules.
3840
+
3801
3841
  Operators are constrained by the LEFT fact's type (from lexq_facts_list). Using one outside
3802
3842
  its type is rejected by the server — check the fact type before choosing an operator.
3803
3843
  - STRING fact: EQUALS, NOT_EQUALS, CONTAINS, IN, NOT_IN
@@ -3863,7 +3903,7 @@ function registerRuleTools(server, callApi) {
3863
3903
  {
3864
3904
  title: "Update Rule",
3865
3905
  annotations: { readOnlyHint: false, destructiveHint: true },
3866
- description: "Update an existing rule in a DRAFT version. Only provided fields are changed.",
3906
+ description: `Update an existing rule in a DRAFT version. Only provided fields are changed. A new condition tree must stay within ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes) and ${MAX_CONDITION_DEPTH} levels of nesting below the root.`,
3867
3907
  inputSchema: z4.object({
3868
3908
  groupId: z4.string().uuid().describe("Policy group ID"),
3869
3909
  versionId: z4.string().uuid().describe("Version ID"),
@@ -4085,7 +4125,7 @@ function registerDeployTools(server, callApi) {
4085
4125
  {
4086
4126
  title: "Publish Version",
4087
4127
  annotations: { readOnlyHint: false, destructiveHint: false },
4088
- 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.",
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.`,
4089
4129
  inputSchema: z6.object({
4090
4130
  groupId: z6.string().uuid().describe("Policy group ID"),
4091
4131
  versionId: z6.string().uuid().describe("Version ID to publish"),
@@ -4145,7 +4185,7 @@ function registerDeployTools(server, callApi) {
4145
4185
  {
4146
4186
  title: "Schedule Deployment",
4147
4187
  annotations: { readOnlyHint: false, destructiveHint: false },
4148
- 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).",
4188
+ 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).",
4149
4189
  inputSchema: z6.object({
4150
4190
  groupId: z6.string().uuid().describe("Policy group ID"),
4151
4191
  versionId: z6.string().uuid().describe("ACTIVE version ID with a future effective start date"),
@@ -4192,8 +4232,8 @@ function registerDeployTools(server, callApi) {
4192
4232
  size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
4193
4233
  groupId: z6.string().uuid().optional().describe("Filter by group ID"),
4194
4234
  types: z6.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
4195
- startDate: z6.string().optional().describe("Start date (yyyy-MM-dd)"),
4196
- endDate: z6.string().optional().describe("End date (yyyy-MM-dd)")
4235
+ startDate: z6.string().optional().describe(START_DAY),
4236
+ endDate: z6.string().optional().describe(END_DAY)
4197
4237
  })
4198
4238
  },
4199
4239
  async ({ page, size, groupId, types, startDate, endDate }) => {
@@ -4337,7 +4377,8 @@ function registerAnalyticsTools(server, callApi) {
4337
4377
  Start an Impact Simulation against historical, uploaded, or inline data.
4338
4378
 
4339
4379
  dataset.type and dataset.source are BOTH required, and must be paired:
4340
- HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd)
4380
+ HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
4381
+ in the organization time zone, both inclusive)
4341
4382
  UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
4342
4383
  MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
4343
4384
 
@@ -4392,8 +4433,8 @@ function registerAnalyticsTools(server, callApi) {
4392
4433
  page: z7.number().int().min(0).default(0).describe("Page number"),
4393
4434
  size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
4394
4435
  status: z7.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
4395
- from: z7.string().optional().describe("Start date (yyyy-MM-dd)"),
4396
- to: z7.string().optional().describe("End date (yyyy-MM-dd)")
4436
+ from: z7.string().optional().describe(START_DAY),
4437
+ to: z7.string().optional().describe(END_DAY)
4397
4438
  })
4398
4439
  },
4399
4440
  async ({ page, size, status, from, to }) => {
@@ -4571,8 +4612,8 @@ function registerReplayTools(server, callApi) {
4571
4612
  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.",
4572
4613
  inputSchema: z9.object({
4573
4614
  candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against"),
4574
- from: z9.string().describe("Window start date (yyyy-MM-dd)"),
4575
- to: z9.string().describe("Window end date (yyyy-MM-dd)"),
4615
+ from: z9.string().describe(WINDOW_START_DAY),
4616
+ to: z9.string().describe(WINDOW_END_DAY),
4576
4617
  maxRecords: z9.number().int().min(1).optional().describe(
4577
4618
  `Sample cap (server default applies; hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`
4578
4619
  )
@@ -4648,8 +4689,8 @@ function registerHistoryTools(server, callApi) {
4648
4689
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
4649
4690
  versionId: z10.string().uuid().optional().describe("Filter by version"),
4650
4691
  status: z10.enum(["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"]).optional().describe("Filter by execution status"),
4651
- startDate: z10.string().optional().describe("Start date (yyyy-MM-dd)"),
4652
- endDate: z10.string().optional().describe("End date (yyyy-MM-dd)")
4692
+ startDate: z10.string().optional().describe(START_DAY),
4693
+ endDate: z10.string().optional().describe(END_DAY)
4653
4694
  })
4654
4695
  },
4655
4696
  async ({ page, size, traceId, groupId, versionId, status, startDate, endDate }) => {
@@ -4680,11 +4721,11 @@ function registerHistoryTools(server, callApi) {
4680
4721
  {
4681
4722
  title: "Execution Statistics",
4682
4723
  annotations: { readOnlyHint: true },
4683
- description: "Get execution KPIs: total executions, success/failure counts, success rate, and average latency.",
4724
+ 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.`,
4684
4725
  inputSchema: z10.object({
4685
4726
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
4686
- startDate: z10.string().optional().describe("Start date (yyyy-MM-dd)"),
4687
- endDate: z10.string().optional().describe("End date (yyyy-MM-dd)")
4727
+ startDate: z10.string().optional().describe(START_DAY),
4728
+ endDate: z10.string().optional().describe(END_DAY)
4688
4729
  })
4689
4730
  },
4690
4731
  async ({ groupId, startDate, endDate }) => {
@@ -4724,8 +4765,8 @@ function registerProvenanceTools(server, callApi) {
4724
4765
  traceId: z11.string().optional().describe("Filter by trace ID (exact match)"),
4725
4766
  revealedBy: z11.string().optional().describe("Filter by operator ID (exact match)"),
4726
4767
  factKey: z11.string().optional().describe("Filter by fact key (partial match, case-insensitive)"),
4727
- startDate: z11.string().optional().describe("Start date (yyyy-MM-dd)"),
4728
- endDate: z11.string().optional().describe("End date (yyyy-MM-dd)")
4768
+ startDate: z11.string().optional().describe(START_DAY),
4769
+ endDate: z11.string().optional().describe(END_DAY)
4729
4770
  })
4730
4771
  },
4731
4772
  async ({ page, size, traceId, revealedBy, factKey, startDate, endDate }) => {
@@ -4748,15 +4789,15 @@ function registerLogTools(server, callApi) {
4748
4789
  {
4749
4790
  title: "List Failure Logs",
4750
4791
  annotations: { readOnlyHint: true },
4751
- description: "List system failure logs from background tasks (platform event webhooks, scheduled deployments).",
4792
+ 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.`,
4752
4793
  inputSchema: z12.object({
4753
4794
  page: z12.number().int().min(0).default(0).describe("Page number"),
4754
4795
  size: z12.number().int().min(1).max(100).default(20).describe("Page size"),
4755
4796
  taskType: z12.enum(TaskType).optional().describe("Task type"),
4756
4797
  status: z12.enum(FailureStatus).optional().describe("Log status"),
4757
4798
  keyword: z12.string().optional().describe("Search in refId, refSubId, errorMessage"),
4758
- startDate: z12.string().optional().describe("Start date (yyyy-MM-dd)"),
4759
- endDate: z12.string().optional().describe("End date (yyyy-MM-dd)")
4799
+ startDate: z12.string().optional().describe(START_DAY),
4800
+ endDate: z12.string().optional().describe(END_DAY)
4760
4801
  })
4761
4802
  },
4762
4803
  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")
@@ -34,6 +34,23 @@ var PlatformEventType = [
34
34
  ];
35
35
  var WebhookPayloadFormat = ["GENERIC", "SLACK"];
36
36
 
37
+ // src/types/constants.ts
38
+ var MAX_ROUNDING_SCALE = 34;
39
+ var REPLAY_WINDOW_MAX_RECORDS = 5e4;
40
+ var PROFILE_SAMPLE_PERMILLE = 10;
41
+ var LATENCY_WINDOW_MILLIS = 6e4;
42
+ var TAIL_MIN_OBS = 3;
43
+ var SLOW_MULTIPLIER = 10;
44
+ var MIN_SAMPLES = 100;
45
+ var MIN_COHORT_SIZE = 3;
46
+ var PROFILE_DEFAULT_WINDOW_HOURS = 24;
47
+ var MAX_GROUPS_PER_TENANT = 20;
48
+ var MAX_DRAFT_VERSIONS_PER_GROUP = 20;
49
+ var MAX_RULES_PER_VERSION = 100;
50
+ var MAX_CONDITIONS_PER_RULE = 25;
51
+ var MAX_CONDITION_DEPTH = 10;
52
+ var MAX_SNAPSHOT_BYTES = 131072;
53
+
37
54
  // src/mcp/tools/groups.ts
38
55
  function registerGroupTools(server, callApi) {
39
56
  server.registerTool(
@@ -63,7 +80,7 @@ function registerGroupTools(server, callApi) {
63
80
  {
64
81
  title: "Create Policy Group",
65
82
  annotations: { readOnlyHint: false, destructiveHint: false },
66
- description: "Create a new policy group. Requires name. Priority is auto-assigned (appended last, tenant-wide); use lexq_groups_reorder to change order. Optionally set conflict resolution, activation group, and description. Policy groups that share an activationGroup form a cluster and must share the same activationMode / activationStrategy / executionLimit; executionLimit is how many of those groups run, not how many rules.",
83
+ description: `Create a new policy group. Requires name. Priority is auto-assigned (appended last, tenant-wide); use lexq_groups_reorder to change order. Optionally set conflict resolution, activation group, and description. Policy groups that share an activationGroup form a cluster and must share the same activationMode / activationStrategy / executionLimit; executionLimit is how many of those groups run, not how many rules. A tenant can have at most ${MAX_GROUPS_PER_TENANT} groups that are not ARCHIVED; archive groups you no longer use to free a slot.`,
67
84
  inputSchema: z2.object({
68
85
  name: z2.string().describe("Group name (unique among non-ARCHIVED)"),
69
86
  description: z2.string().optional().describe("Group description"),
@@ -242,6 +259,17 @@ function formatUnregisteredFactWarning(meta) {
242
259
  ].join("\n");
243
260
  }
244
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
+
245
273
  // src/mcp/tools/versions.ts
246
274
  function registerVersionTools(server, callApi) {
247
275
  server.registerTool(
@@ -278,12 +306,12 @@ function registerVersionTools(server, callApi) {
278
306
  {
279
307
  title: "Create Policy Version",
280
308
  annotations: { readOnlyHint: false, destructiveHint: false },
281
- description: "Create a new DRAFT version in a policy group. Optionally provide a commit message and effective date range.",
309
+ description: `Create a new DRAFT version in a policy group. Optionally provide a commit message and effective date range. A group can hold at most ${MAX_DRAFT_VERSIONS_PER_GROUP} DRAFT versions at a time; published versions do not count. Publish or delete drafts to make room.`,
282
310
  inputSchema: z3.object({
283
311
  groupId: z3.string().uuid().describe("Policy group ID"),
284
312
  commitMessage: z3.string().optional().describe("Commit message describing this version"),
285
- effectiveFrom: z3.string().optional().describe("Effective start date (ISO 8601)"),
286
- 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}`)
287
315
  })
288
316
  },
289
317
  async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/versions`, { body })
@@ -293,13 +321,13 @@ function registerVersionTools(server, callApi) {
293
321
  {
294
322
  title: "Update Policy Version",
295
323
  annotations: { readOnlyHint: false, destructiveHint: true },
296
- 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. 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).",
297
325
  inputSchema: z3.object({
298
326
  groupId: z3.string().uuid().describe("Policy group ID"),
299
327
  versionId: z3.string().uuid().describe("Version ID"),
300
328
  commitMessage: z3.string().optional().describe("New commit message"),
301
- effectiveFrom: z3.string().optional().describe("New effective start date"),
302
- effectiveTo: z3.string().optional().describe("New effective end date")
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.`)
303
331
  })
304
332
  },
305
333
  async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
@@ -322,7 +350,7 @@ function registerVersionTools(server, callApi) {
322
350
  {
323
351
  title: "Clone Policy Version",
324
352
  annotations: { readOnlyHint: false, destructiveHint: false },
325
- description: "Clone an existing version to create a new DRAFT. Useful when the source version is already published.",
353
+ description: `Clone an existing version to create a new DRAFT. Useful when the source version is already published. The new DRAFT counts toward the limit of ${MAX_DRAFT_VERSIONS_PER_GROUP} DRAFT versions per group, and a source with more than ${MAX_RULES_PER_VERSION} rules cannot be cloned.`,
326
354
  inputSchema: z3.object({
327
355
  groupId: z3.string().uuid().describe("Policy group ID"),
328
356
  versionId: z3.string().uuid().describe("Source version ID to clone")
@@ -336,17 +364,6 @@ function registerVersionTools(server, callApi) {
336
364
  );
337
365
  }
338
366
 
339
- // src/types/constants.ts
340
- var MAX_ROUNDING_SCALE = 34;
341
- var REPLAY_WINDOW_MAX_RECORDS = 5e4;
342
- var PROFILE_SAMPLE_PERMILLE = 10;
343
- var LATENCY_WINDOW_MILLIS = 6e4;
344
- var TAIL_MIN_OBS = 3;
345
- var SLOW_MULTIPLIER = 10;
346
- var MIN_SAMPLES = 100;
347
- var MIN_COHORT_SIZE = 3;
348
- var PROFILE_DEFAULT_WINDOW_HOURS = 24;
349
-
350
367
  // src/mcp/tools/rules.ts
351
368
  import { z as z4 } from "zod";
352
369
  import dedent from "dedent";
@@ -396,7 +413,9 @@ function registerRuleTools(server, callApi) {
396
413
 
397
414
  Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
398
415
  Value types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
399
-
416
+ Limits: a condition tree may hold at most ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes; GROUP nodes do not count)
417
+ and nest at most ${MAX_CONDITION_DEPTH} levels below the root. A version holds at most ${MAX_RULES_PER_VERSION} rules.
418
+
400
419
  Operators are constrained by the LEFT fact's type (from lexq_facts_list). Using one outside
401
420
  its type is rejected by the server — check the fact type before choosing an operator.
402
421
  - STRING fact: EQUALS, NOT_EQUALS, CONTAINS, IN, NOT_IN
@@ -462,7 +481,7 @@ function registerRuleTools(server, callApi) {
462
481
  {
463
482
  title: "Update Rule",
464
483
  annotations: { readOnlyHint: false, destructiveHint: true },
465
- description: "Update an existing rule in a DRAFT version. Only provided fields are changed.",
484
+ description: `Update an existing rule in a DRAFT version. Only provided fields are changed. A new condition tree must stay within ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes) and ${MAX_CONDITION_DEPTH} levels of nesting below the root.`,
466
485
  inputSchema: z4.object({
467
486
  groupId: z4.string().uuid().describe("Policy group ID"),
468
487
  versionId: z4.string().uuid().describe("Version ID"),
@@ -688,7 +707,7 @@ function registerDeployTools(server, callApi) {
688
707
  {
689
708
  title: "Publish Version",
690
709
  annotations: { readOnlyHint: false, destructiveHint: false },
691
- 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.",
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.`,
692
711
  inputSchema: z6.object({
693
712
  groupId: z6.string().uuid().describe("Policy group ID"),
694
713
  versionId: z6.string().uuid().describe("Version ID to publish"),
@@ -748,7 +767,7 @@ function registerDeployTools(server, callApi) {
748
767
  {
749
768
  title: "Schedule Deployment",
750
769
  annotations: { readOnlyHint: false, destructiveHint: false },
751
- 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).",
770
+ 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).",
752
771
  inputSchema: z6.object({
753
772
  groupId: z6.string().uuid().describe("Policy group ID"),
754
773
  versionId: z6.string().uuid().describe("ACTIVE version ID with a future effective start date"),
@@ -795,8 +814,8 @@ function registerDeployTools(server, callApi) {
795
814
  size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
796
815
  groupId: z6.string().uuid().optional().describe("Filter by group ID"),
797
816
  types: z6.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
798
- startDate: z6.string().optional().describe("Start date (yyyy-MM-dd)"),
799
- endDate: z6.string().optional().describe("End date (yyyy-MM-dd)")
817
+ startDate: z6.string().optional().describe(START_DAY),
818
+ endDate: z6.string().optional().describe(END_DAY)
800
819
  })
801
820
  },
802
821
  async ({ page, size, groupId, types, startDate, endDate }) => {
@@ -940,7 +959,8 @@ function registerAnalyticsTools(server, callApi) {
940
959
  Start an Impact Simulation against historical, uploaded, or inline data.
941
960
 
942
961
  dataset.type and dataset.source are BOTH required, and must be paired:
943
- HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd)
962
+ HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
963
+ in the organization time zone, both inclusive)
944
964
  UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
945
965
  MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
946
966
 
@@ -995,8 +1015,8 @@ function registerAnalyticsTools(server, callApi) {
995
1015
  page: z7.number().int().min(0).default(0).describe("Page number"),
996
1016
  size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
997
1017
  status: z7.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
998
- from: z7.string().optional().describe("Start date (yyyy-MM-dd)"),
999
- to: z7.string().optional().describe("End date (yyyy-MM-dd)")
1018
+ from: z7.string().optional().describe(START_DAY),
1019
+ to: z7.string().optional().describe(END_DAY)
1000
1020
  })
1001
1021
  },
1002
1022
  async ({ page, size, status, from, to }) => {
@@ -1174,8 +1194,8 @@ function registerReplayTools(server, callApi) {
1174
1194
  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.",
1175
1195
  inputSchema: z9.object({
1176
1196
  candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against"),
1177
- from: z9.string().describe("Window start date (yyyy-MM-dd)"),
1178
- to: z9.string().describe("Window end date (yyyy-MM-dd)"),
1197
+ from: z9.string().describe(WINDOW_START_DAY),
1198
+ to: z9.string().describe(WINDOW_END_DAY),
1179
1199
  maxRecords: z9.number().int().min(1).optional().describe(
1180
1200
  `Sample cap (server default applies; hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`
1181
1201
  )
@@ -1251,8 +1271,8 @@ function registerHistoryTools(server, callApi) {
1251
1271
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
1252
1272
  versionId: z10.string().uuid().optional().describe("Filter by version"),
1253
1273
  status: z10.enum(["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"]).optional().describe("Filter by execution status"),
1254
- startDate: z10.string().optional().describe("Start date (yyyy-MM-dd)"),
1255
- endDate: z10.string().optional().describe("End date (yyyy-MM-dd)")
1274
+ startDate: z10.string().optional().describe(START_DAY),
1275
+ endDate: z10.string().optional().describe(END_DAY)
1256
1276
  })
1257
1277
  },
1258
1278
  async ({ page, size, traceId, groupId, versionId, status, startDate, endDate }) => {
@@ -1283,11 +1303,11 @@ function registerHistoryTools(server, callApi) {
1283
1303
  {
1284
1304
  title: "Execution Statistics",
1285
1305
  annotations: { readOnlyHint: true },
1286
- description: "Get execution KPIs: total executions, success/failure counts, success rate, and average latency.",
1306
+ 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.`,
1287
1307
  inputSchema: z10.object({
1288
1308
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
1289
- startDate: z10.string().optional().describe("Start date (yyyy-MM-dd)"),
1290
- endDate: z10.string().optional().describe("End date (yyyy-MM-dd)")
1309
+ startDate: z10.string().optional().describe(START_DAY),
1310
+ endDate: z10.string().optional().describe(END_DAY)
1291
1311
  })
1292
1312
  },
1293
1313
  async ({ groupId, startDate, endDate }) => {
@@ -1327,8 +1347,8 @@ function registerProvenanceTools(server, callApi) {
1327
1347
  traceId: z11.string().optional().describe("Filter by trace ID (exact match)"),
1328
1348
  revealedBy: z11.string().optional().describe("Filter by operator ID (exact match)"),
1329
1349
  factKey: z11.string().optional().describe("Filter by fact key (partial match, case-insensitive)"),
1330
- startDate: z11.string().optional().describe("Start date (yyyy-MM-dd)"),
1331
- endDate: z11.string().optional().describe("End date (yyyy-MM-dd)")
1350
+ startDate: z11.string().optional().describe(START_DAY),
1351
+ endDate: z11.string().optional().describe(END_DAY)
1332
1352
  })
1333
1353
  },
1334
1354
  async ({ page, size, traceId, revealedBy, factKey, startDate, endDate }) => {
@@ -1351,15 +1371,15 @@ function registerLogTools(server, callApi) {
1351
1371
  {
1352
1372
  title: "List Failure Logs",
1353
1373
  annotations: { readOnlyHint: true },
1354
- description: "List system failure logs from background tasks (platform event webhooks, scheduled deployments).",
1374
+ 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.`,
1355
1375
  inputSchema: z12.object({
1356
1376
  page: z12.number().int().min(0).default(0).describe("Page number"),
1357
1377
  size: z12.number().int().min(1).max(100).default(20).describe("Page size"),
1358
1378
  taskType: z12.enum(TaskType).optional().describe("Task type"),
1359
1379
  status: z12.enum(FailureStatus).optional().describe("Log status"),
1360
1380
  keyword: z12.string().optional().describe("Search in refId, refSubId, errorMessage"),
1361
- startDate: z12.string().optional().describe("Start date (yyyy-MM-dd)"),
1362
- endDate: z12.string().optional().describe("End date (yyyy-MM-dd)")
1381
+ startDate: z12.string().optional().describe(START_DAY),
1382
+ endDate: z12.string().optional().describe(END_DAY)
1363
1383
  })
1364
1384
  },
1365
1385
  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.0",
3
+ "version": "1.0.2",
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",
@@ -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
 
@@ -247,7 +258,7 @@ lexq profile <groupId> --version <vid> --cache MISS \
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