@lexq/cli 2.1.0 → 2.1.1

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
@@ -387,11 +387,57 @@ var MAX_RULES_PER_VERSION = 100;
387
387
  var MAX_CONDITIONS_PER_RULE = 25;
388
388
  var MAX_CONDITION_DEPTH = 10;
389
389
  var MAX_SNAPSHOT_BYTES = 131072;
390
+ var STATS_DEFAULT_WINDOW_DAYS = 30;
391
+ var STATS_MAX_WINDOW_DAYS = 31;
392
+ var FAILURE_LOG_LOOKBACK_DAYS = 7;
393
+ var PURGE_AFTER_DAYS = 30;
394
+ var DESCRIPTION_MAX_LENGTH = 255;
395
+ var COMMIT_MESSAGE_MAX_LENGTH = 500;
396
+ var SCHEDULE_TICK_MAX_MILLIS = 6e4;
390
397
 
391
398
  // src/commands/groups.ts
392
399
  import "commander";
393
400
  import dedent3 from "dedent";
394
401
 
402
+ // src/types/enums.ts
403
+ var PolicyGroupStatus = ["ACTIVE", "DISABLED", "ARCHIVED"];
404
+ var LogicalOperator = ["AND", "OR"];
405
+ var MutateOperator = ["ASSIGN", "ADD", "SUB", "MUL", "DIV"];
406
+ var CalculationMethod = ["PERCENTAGE", "AMOUNT"];
407
+ var RoundingPolicy = [
408
+ "UP",
409
+ "DOWN",
410
+ "CEILING",
411
+ "FLOOR",
412
+ "HALF_UP",
413
+ "HALF_DOWN",
414
+ "HALF_EVEN"
415
+ ];
416
+ var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
417
+ var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
418
+ var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
419
+ var DeploymentType = ["DEPLOY", "ROLLBACK", "UNDEPLOY"];
420
+ var ApiExecutionStatus = ["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"];
421
+ var ProfileCacheState = ["HIT", "MISS"];
422
+ var SimulationStatus = ["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"];
423
+ var SimulationDatasetType = ["HISTORICAL", "UPLOADED", "MANUAL"];
424
+ var SimulationDatasetSource = ["EXECUTION_LOGS", "S3_BUCKET", "REQUEST_BODY"];
425
+ var SimulationMetricType = ["COUNT", "SUM", "AVG"];
426
+ var ExportFormat = ["CSV", "JSON"];
427
+ var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
428
+ var FailureAction = ["IGNORE", "RESOLVE"];
429
+ var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
430
+ var PlatformEventType = [
431
+ "VERSION_PUBLISHED",
432
+ "DEPLOYED",
433
+ "ROLLED_BACK",
434
+ "UNDEPLOYED",
435
+ "DEPLOY_SCHEDULED",
436
+ "DEPLOY_SCHEDULE_CANCELED"
437
+ ];
438
+ var WebhookPayloadFormat = ["GENERIC", "SLACK"];
439
+ var DeploymentScheduleStatus = ["PENDING", "EXECUTED", "CANCELED", "FAILED"];
440
+
395
441
  // src/lib/dates.ts
396
442
  var ORG_DAY = "yyyy-MM-dd, a whole calendar day in the organization time zone (tenantTimezone in whoami), not UTC";
397
443
  var START_DAY = `Start date, ${ORG_DAY}. Inclusive.`;
@@ -399,9 +445,6 @@ var END_DAY = `End date, ${ORG_DAY}. Inclusive: the whole day counts.`;
399
445
  var WINDOW_START_DAY = `Window start date, ${ORG_DAY}. Inclusive.`;
400
446
  var WINDOW_END_DAY = `Window end date, ${ORG_DAY}. Inclusive: the whole day counts.`;
401
447
  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.";
402
- var STATS_DEFAULT_WINDOW_DAYS = 30;
403
- var STATS_MAX_WINDOW_DAYS = 31;
404
- var FAILURE_LOG_LOOKBACK_DAYS = 7;
405
448
  function utcMinute(instant) {
406
449
  return instant ? `${new Date(instant).toISOString().substring(0, 16)}Z` : "\u2013";
407
450
  }
@@ -424,7 +467,7 @@ function registerGroupCommands(program) {
424
467
  delete Archive a group
425
468
  ab-test Manage A/B tests (start, stop, adjust)
426
469
 
427
- Statuses: ACTIVE, DISABLED, ARCHIVED
470
+ Statuses: ${PolicyGroupStatus.join(", ")}
428
471
  DISABLED stops production execution (403 P-009) and keeps the deployed version and any A/B test.
429
472
  Dry run, simulation, and replay still work. Set ACTIVE to resume.
430
473
  ARCHIVED means deleted. Only 'lexq groups delete' sets it, and it cannot be undone.
@@ -490,9 +533,9 @@ ${data.length} total`);
490
533
 
491
534
  Fields:
492
535
  name string Group name (required, unique per tenant)
493
- description string Description (optional, max 255 chars)
536
+ description string Description (optional, max ${DESCRIPTION_MAX_LENGTH} chars)
494
537
  activationGroup string Execution Group — conflict-resolution cluster key (optional)
495
- activationMode string NONE | EXCLUSIVE | MAX_N [default: NONE]
538
+ activationMode string ${ConflictResolutionMode.join(" | ")} [default: NONE]
496
539
  activationStrategy string HIGHEST_PRIORITY [default, and currently the only value]
497
540
  executionLimit number Max rules to fire in MAX_N mode (optional)
498
541
  status string ACTIVE | DISABLED [default: ACTIVE]
@@ -554,7 +597,7 @@ ${data.length} total`);
554
597
  It cannot be restored, and calls that change it (settings, versions, rules, starting an A/B
555
598
  test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever
556
599
  executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted
557
- after 30 days.
600
+ after ${PURGE_AFTER_DAYS} days.
558
601
 
559
602
  Use --force to skip the confirmation prompt.
560
603
  `
@@ -826,7 +869,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
826
869
  }'
827
870
 
828
871
  Fields:
829
- commitMessage string Version description (optional, max 255 chars)
872
+ commitMessage string Version description (optional, max ${COMMIT_MESSAGE_MAX_LENGTH} chars)
830
873
  effectiveFrom instant Start of effective period (optional, with Z or an offset)
831
874
  effectiveTo instant End of effective period (optional, auto-expires)
832
875
  `
@@ -1091,11 +1134,11 @@ ${data.length} total`);
1091
1134
  refVar is the percentage base and is optional — omit to use targetVar itself.
1092
1135
  It is only valid in PERCENTAGE × {ASSIGN, ADD, SUB}.
1093
1136
 
1094
- Value Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
1137
+ Value Types: ${ValueType.join(", ")}
1095
1138
 
1096
1139
  Mutex (optional — rule-level conflict resolution):
1097
1140
  mutexGroup string Logical grouping key (e.g., "best-discount")
1098
- mutexMode string NONE | EXCLUSIVE | MAX_N [default: NONE]
1141
+ mutexMode string ${ConflictResolutionMode.join(" | ")} [default: NONE]
1099
1142
  mutexStrategy string HIGHEST_PRIORITY (currently the only value)
1100
1143
  mutexLimit number Max rules to fire in MAX_N mode (required when MAX_N)
1101
1144
 
@@ -1304,27 +1347,6 @@ function escapeUnsafe(json) {
1304
1347
 
1305
1348
  // src/lib/export.ts
1306
1349
  import { writeFileSync as writeFileSync2 } from "fs";
1307
-
1308
- // src/types/enums.ts
1309
- var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
1310
- var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
1311
- var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
1312
- var ProfileCacheState = ["HIT", "MISS"];
1313
- var ExportFormat = ["CSV", "JSON"];
1314
- var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
1315
- var FailureAction = ["IGNORE", "RESOLVE"];
1316
- var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
1317
- var PlatformEventType = [
1318
- "VERSION_PUBLISHED",
1319
- "DEPLOYED",
1320
- "ROLLED_BACK",
1321
- "UNDEPLOYED",
1322
- "DEPLOY_SCHEDULED",
1323
- "DEPLOY_SCHEDULE_CANCELED"
1324
- ];
1325
- var WebhookPayloadFormat = ["GENERIC", "SLACK"];
1326
-
1327
- // src/lib/export.ts
1328
1350
  var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
1329
1351
  function parseFormat(value) {
1330
1352
  if (typeof value === "string" && EXPORT_FORMATS.includes(value)) {
@@ -1459,7 +1481,7 @@ ${data.length} unregistered`);
1459
1481
  process.exit(1);
1460
1482
  }
1461
1483
  });
1462
- facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (letters, numbers, underscores)").option("--name <n>", "Display name").option("--type <type>", "Value type: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER").option("--description <desc>", "Description").option("--required", "Mark as required", false).option("--pii", "Mark as PII \u2014 value is masked on every read surface", false).option("--allowed-values <csv>", "Comma-separated list of accepted values").option("--min <number>", "Lower bound, inclusive (numeric types only)").option("--max <number>", "Upper bound, inclusive (numeric types only)").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
1484
+ facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (letters, numbers, underscores)").option("--name <n>", "Display name").option("--type <type>", `Value type: ${ValueType.join(", ")}`).option("--description <desc>", "Description").option("--required", "Mark as required", false).option("--pii", "Mark as PII \u2014 value is masked on every read surface", false).option("--allowed-values <csv>", "Comma-separated list of accepted values").option("--min <number>", "Lower bound, inclusive (numeric types only)").option("--max <number>", "Upper bound, inclusive (numeric types only)").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
1463
1485
  "after",
1464
1486
  dedent6`
1465
1487
 
@@ -1479,7 +1501,7 @@ ${data.length} unregistered`);
1479
1501
  $ lexq facts create --key tier --name "Tier" --type STRING \
1480
1502
  --allowed-values "GOLD, SILVER, BRONZE"
1481
1503
 
1482
- Value Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
1504
+ Value Types: ${ValueType.join(", ")}
1483
1505
  Key Format: starts with a letter, then letters, numbers, and underscores.
1484
1506
  Casing is yours to choose (e.g., paymentAmount, payment_amount)
1485
1507
 
@@ -1508,7 +1530,7 @@ ${data.length} unregistered`);
1508
1530
  process.exit(1);
1509
1531
  }
1510
1532
  });
1511
- facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <n>", "Display name").option("--description <desc>", 'Description (pass "" to remove it)').option("--required", "Mark as required").option("--no-required", "Mark as not required").option("--pii", "Mark as PII (enables masking)").option("--no-pii", "Unmark as PII (disables masking \u2014 value becomes visible)").option("--type <type>", "Value type: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER").option("--allowed-values <csv>", "Comma-separated list of accepted values").option("--min <number>", "Lower bound, inclusive (numeric types only)").option("--max <number>", "Upper bound, inclusive (numeric types only)").option("--clear-value-domain", "Remove the value domain entirely").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
1533
+ facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <n>", "Display name").option("--description <desc>", 'Description (pass "" to remove it)').option("--required", "Mark as required").option("--no-required", "Mark as not required").option("--pii", "Mark as PII (enables masking)").option("--no-pii", "Unmark as PII (disables masking \u2014 value becomes visible)").option("--type <type>", `Value type: ${ValueType.join(", ")}`).option("--allowed-values <csv>", "Comma-separated list of accepted values").option("--min <number>", "Lower bound, inclusive (numeric types only)").option("--max <number>", "Upper bound, inclusive (numeric types only)").option("--clear-value-domain", "Remove the value domain entirely").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
1512
1534
  "after",
1513
1535
  dedent6`
1514
1536
 
@@ -1892,7 +1914,7 @@ function registerDeployCommands(program) {
1892
1914
  process.exit(1);
1893
1915
  }
1894
1916
  });
1895
- 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(
1917
+ deploy.command("history").description("List deployment history").option("--group-id <groupId>", "Filter by policy group").option("--types <types>", `Filter by types (comma-separated: ${DeploymentType.join(",")})`).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(
1896
1918
  "after",
1897
1919
  dedent7`
1898
1920
 
@@ -2052,7 +2074,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2052
2074
  dedent7`
2053
2075
 
2054
2076
  The version must be ACTIVE with a future effective start date; the system
2055
- deploys it automatically at that time (within one scheduler tick, ≤60s).
2077
+ deploys it automatically at that time (within one scheduler tick, ≤${SCHEDULE_TICK_MAX_MILLIS / 1e3}s).
2056
2078
  That time is the version's effectiveFrom, set on the DRAFT before publishing;
2057
2079
  this command takes no time of its own.
2058
2080
  One pending schedule per group. Manual deploy/rollback/undeploy, starting
@@ -2377,8 +2399,8 @@ function registerAnalyticsCommands(program) {
2377
2399
 
2378
2400
  $ lexq analytics simulation start --file simulation.json
2379
2401
 
2380
- Dataset types: HISTORICAL, UPLOADED, MANUAL
2381
- Dataset sources: EXECUTION_LOGS, S3_BUCKET, REQUEST_BODY
2402
+ Dataset types: ${SimulationDatasetType.join(", ")}
2403
+ Dataset sources: ${SimulationDatasetSource.join(", ")}
2382
2404
  HISTORICAL from / to are yyyy-MM-dd, whole days in the organization time zone, both inclusive.
2383
2405
  `
2384
2406
  ).action(async (opts) => {
@@ -2499,7 +2521,7 @@ function registerAnalyticsCommands(program) {
2499
2521
  process.exit(1);
2500
2522
  }
2501
2523
  });
2502
- 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) => {
2524
+ sim.command("list").description("List simulation history").option("--status <status>", `Filter by status (${SimulationStatus.join(", ")})`).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) => {
2503
2525
  try {
2504
2526
  const globalOpts = program.opts();
2505
2527
  const format = globalOpts.format ?? "json";
@@ -2703,6 +2725,15 @@ function resolveBody(opts) {
2703
2725
  // src/commands/profile.ts
2704
2726
  import "commander";
2705
2727
  import dedent9 from "dedent";
2728
+
2729
+ // src/lib/percentiles.ts
2730
+ var PERCENTILE_MIN_N = {
2731
+ p50: TAIL_MIN_OBS * 2,
2732
+ p95: TAIL_MIN_OBS * 20,
2733
+ p99: TAIL_MIN_OBS * 100
2734
+ };
2735
+
2736
+ // src/commands/profile.ts
2706
2737
  var ms = (nanos) => nanos == null ? "\u2013" : (nanos / 1e6).toFixed(2);
2707
2738
  var msWithUnit = (nanos) => nanos == null ? "\u2013" : `${(nanos / 1e6).toFixed(2)}ms`;
2708
2739
  function registerProfileCommands(program) {
@@ -2712,15 +2743,18 @@ function registerProfileCommands(program) {
2712
2743
  ).option("--version-id <versionId>", "Version to inspect (default: live version)").option(
2713
2744
  "--from <instant>",
2714
2745
  `Window start, ISO-8601 instant (default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago)`
2715
- ).option("--to <instant>", "Window end, ISO-8601 instant (default: now)").option("--cache <state>", "Cache dimension for the rule table: HIT | MISS (default: HIT)").addHelpText(
2746
+ ).option("--to <instant>", "Window end, ISO-8601 instant (default: now)").option(
2747
+ "--cache <state>",
2748
+ `Cache dimension for the rule table: ${ProfileCacheState.join(" | ")} (default: HIT)`
2749
+ ).addHelpText(
2716
2750
  "after",
2717
2751
  dedent9`
2718
2752
 
2719
2753
  Slow-rule judgment is relative only: flagged = p50 ≥ ${SLOW_MULTIPLIER}× the median of
2720
2754
  per-rule p50s within the group. Absolute ms thresholds are intentionally
2721
- not supported. Each percentile is withheld (–) unless n×(1−q) ≥ 3 —
2722
- p50 from n ≥ 6, p95 from n ≥ 60, p99 from n ≥ 300. TOTAL is recorded
2723
- on every call, rule detail from a deterministic 1% sample.
2755
+ not supported. Each percentile is withheld (–) unless n×(1−q) ≥ ${TAIL_MIN_OBS} —
2756
+ p50 from n ≥ ${PERCENTILE_MIN_N.p50}, p95 from n ≥ ${PERCENTILE_MIN_N.p95}, p99 from n ≥ ${PERCENTILE_MIN_N.p99}. TOTAL is recorded
2757
+ on every call, rule detail from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample.
2724
2758
 
2725
2759
  Examples:
2726
2760
  $ lexq profile <groupId>
@@ -2818,10 +2852,10 @@ function registerHistoryCommands(program) {
2818
2852
  get Get full execution detail (request facts, traces, decisions)
2819
2853
  stats Aggregate statistics (success rate, latency, counts)
2820
2854
 
2821
- Statuses: SUCCESS, NO_MATCH, ERROR, TIMEOUT
2855
+ Statuses: ${ApiExecutionStatus.join(", ")}
2822
2856
  `
2823
2857
  );
2824
- 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(
2858
+ 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 (${ApiExecutionStatus.join(", ")})`).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(
2825
2859
  "after",
2826
2860
  dedent10`
2827
2861
 
@@ -3214,6 +3248,18 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3214
3248
  // src/commands/logs.ts
3215
3249
  import "commander";
3216
3250
  import dedent13 from "dedent";
3251
+
3252
+ // src/lib/enum-notes.ts
3253
+ function withNotes(notes) {
3254
+ return Object.entries(notes).map(([value, note]) => note ? `${value} (${note})` : value);
3255
+ }
3256
+
3257
+ // src/commands/logs.ts
3258
+ var STATUS_NOTES = {
3259
+ PENDING: "needs attention",
3260
+ RESOLVED: "",
3261
+ IGNORED: ""
3262
+ };
3217
3263
  function registerLogCommands(program) {
3218
3264
  const logs = program.command("logs").description("Failure logs").addHelpText(
3219
3265
  "after",
@@ -3224,13 +3270,13 @@ function registerLogCommands(program) {
3224
3270
  Commands:
3225
3271
  list List failure logs with filters
3226
3272
  get Get failure log detail (includes the original payload)
3227
- action Process a single log (IGNORE, RESOLVE)
3273
+ action Process a single log (${FailureAction.join(", ")})
3228
3274
  bulk-action Process multiple logs at once
3229
3275
 
3230
- Statuses: PENDING (needs attention), RESOLVED, IGNORED
3276
+ Statuses: ${withNotes(STATUS_NOTES).join(", ")}
3231
3277
  `
3232
3278
  );
3233
- 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(
3279
+ logs.command("list").description("List failure logs").option("--task-type <taskType>", `Filter by task type (${TaskType.join(", ")})`).option("--status <status>", `Filter by status (${FailureStatus.join(", ")})`).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(
3234
3280
  "after",
3235
3281
  dedent13`
3236
3282
 
@@ -3286,7 +3332,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3286
3332
  dedent13`
3287
3333
 
3288
3334
  Includes the full payload that was used for the failed operation.
3289
- Use this to inspect what went wrong before deciding to IGNORE or RESOLVE.
3335
+ Use this to inspect what went wrong before deciding to ${FailureAction.join(" or ")}.
3290
3336
  `
3291
3337
  ).action(async (opts) => {
3292
3338
  try {
@@ -3303,7 +3349,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3303
3349
  process.exit(1);
3304
3350
  }
3305
3351
  });
3306
- logs.command("action").description("Process a failure log action (IGNORE, RESOLVE)").requiredOption("--id <logId>", "Log ID").requiredOption("--action <action>", "Action: IGNORE or RESOLVE").addHelpText(
3352
+ logs.command("action").description(`Process a failure log action (${FailureAction.join(", ")})`).requiredOption("--id <logId>", "Log ID").requiredOption("--action <action>", `Action: ${FailureAction.join(" or ")}`).addHelpText(
3307
3353
  "after",
3308
3354
  dedent13`
3309
3355
 
@@ -3334,7 +3380,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3334
3380
  process.exit(1);
3335
3381
  }
3336
3382
  });
3337
- logs.command("bulk-action").description("Bulk process failure logs").requiredOption("--ids <logIds>", "Comma-separated log IDs").requiredOption("--action <action>", "Action: IGNORE or RESOLVE").addHelpText(
3383
+ logs.command("bulk-action").description("Bulk process failure logs").requiredOption("--ids <logIds>", "Comma-separated log IDs").requiredOption("--action <action>", `Action: ${FailureAction.join(" or ")}`).addHelpText(
3338
3384
  "after",
3339
3385
  dedent13`
3340
3386
 
@@ -3365,6 +3411,10 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3365
3411
  // src/commands/webhook-subscriptions.ts
3366
3412
  import "commander";
3367
3413
  import dedent14 from "dedent";
3414
+ var FORMAT_NOTES = {
3415
+ GENERIC: "full JSON",
3416
+ SLACK: '{"text":"..."}'
3417
+ };
3368
3418
  function registerWebhookSubscriptionCommands(program) {
3369
3419
  const webhooks = program.command("webhook-subscriptions").description("Manage platform event webhook subscriptions").addHelpText(
3370
3420
  "after",
@@ -3467,7 +3517,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3467
3517
  name string Subscription name (required, unique per tenant)
3468
3518
  webhookUrl string Webhook endpoint URL (required)
3469
3519
  subscribedEvents string[] ${PlatformEventType.join(", ")}
3470
- payloadFormat string GENERIC (full JSON) or SLACK ({"text":"..."}) [default: GENERIC]
3520
+ payloadFormat string ${withNotes(FORMAT_NOTES).join(" or ")} [default: GENERIC]
3471
3521
  secret string HMAC-SHA256 signing secret (optional)
3472
3522
  isActive boolean Enable/disable (optional) [default: true]
3473
3523
  id string Provide to update, omit to create
@@ -3744,7 +3794,7 @@ function registerAnalyticsTools(server, callApi) {
3744
3794
  options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
3745
3795
  options.includeRuleStats: boolean
3746
3796
  options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
3747
- { "targetVariable": "<fact>", "aggregationType": "COUNT" | "SUM" | "AVG" }
3797
+ { "targetVariable": "<fact>", "aggregationType": ${SimulationMetricType.map((t) => `"${t}"`).join(" | ")} }
3748
3798
 
3749
3799
  Example (uploaded dataset):
3750
3800
  {
@@ -3790,7 +3840,7 @@ function registerAnalyticsTools(server, callApi) {
3790
3840
  inputSchema: z.object({
3791
3841
  page: z.number().int().min(0).default(0).describe("Page number"),
3792
3842
  size: z.number().int().min(1).max(100).default(20).describe("Page size"),
3793
- status: z.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
3843
+ status: z.enum(SimulationStatus).optional().describe("Filter by status"),
3794
3844
  from: z.string().optional().describe(START_DAY),
3795
3845
  to: z.string().optional().describe(END_DAY)
3796
3846
  })
@@ -4067,7 +4117,7 @@ function registerGroupTools(server, callApi) {
4067
4117
  groupId: z4.string().uuid().describe("Policy group ID"),
4068
4118
  name: z4.string().optional().describe("New name"),
4069
4119
  description: z4.string().optional().describe("New description"),
4070
- status: z4.enum(["ACTIVE", "DISABLED"]).optional().describe(
4120
+ status: z4.enum(PolicyGroupStatus).extract(["ACTIVE", "DISABLED"]).optional().describe(
4071
4121
  "DISABLED stops production execution of this group: execution calls return 403 (P-009), and a composite call that includes the group is rejected in full. The deployed version and any A/B test are kept, and dry run, Impact Simulation, and Decision Replay keep working, so a fix can be verified before setting ACTIVE again. To go back to the previous version, use lexq_deploy_rollback. To take the version out of production, use lexq_deploy_undeploy."
4072
4122
  ),
4073
4123
  activationGroup: z4.string().optional().describe(
@@ -4087,7 +4137,7 @@ function registerGroupTools(server, callApi) {
4087
4137
  {
4088
4138
  title: "Delete Policy Group",
4089
4139
  annotations: { readOnlyHint: false, destructiveHint: true },
4090
- description: "Delete (archive) a policy group. A deployed group cannot be deleted (P-020); undeploy it first with lexq_deploy_undeploy, which also ends a running A/B test. This cannot be undone. The group no longer appears in lexq_groups_list and its name can be reused right away. Calls that change it (settings, versions, rules, starting an A/B test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted after 30 days.",
4140
+ description: `Delete (archive) a policy group. A deployed group cannot be deleted (P-020); undeploy it first with lexq_deploy_undeploy, which also ends a running A/B test. This cannot be undone. The group no longer appears in lexq_groups_list and its name can be reused right away. Calls that change it (settings, versions, rules, starting an A/B test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted after ${PURGE_AFTER_DAYS} days.`,
4091
4141
  inputSchema: z4.object({
4092
4142
  groupId: z4.string().uuid().describe("Policy group ID")
4093
4143
  })
@@ -4121,7 +4171,9 @@ function registerGroupTools(server, callApi) {
4121
4171
  inputSchema: z4.object({
4122
4172
  groupId: z4.string().uuid().describe("Policy group ID"),
4123
4173
  testVersionId: z4.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
4124
- trafficRate: z4.number().int().min(1).max(99).describe("Traffic percentage routed to challenger (1-99)")
4174
+ trafficRate: z4.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
4175
+ `Traffic percentage routed to challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
4176
+ )
4125
4177
  })
4126
4178
  },
4127
4179
  async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/ab-test`, { body })
@@ -4146,7 +4198,9 @@ function registerGroupTools(server, callApi) {
4146
4198
  description: "Adjust traffic rate of a running A/B test.",
4147
4199
  inputSchema: z4.object({
4148
4200
  groupId: z4.string().uuid().describe("Policy group ID"),
4149
- trafficRate: z4.number().int().min(1).max(99).describe("New traffic percentage for challenger (1-99)")
4201
+ trafficRate: z4.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
4202
+ `New traffic percentage for challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
4203
+ )
4150
4204
  })
4151
4205
  },
4152
4206
  async ({ groupId, ...body }) => callApi("PATCH", `policy-groups/${groupId}/ab-test/traffic-rate`, { body })
@@ -4253,6 +4307,7 @@ function registerVersionTools(server, callApi) {
4253
4307
  // src/mcp/tools/rules.ts
4254
4308
  import { z as z6 } from "zod";
4255
4309
  import dedent17 from "dedent";
4310
+ var oneOf = (values) => values.map((v) => `"${v}"`).join("|");
4256
4311
  function registerRuleTools(server, callApi) {
4257
4312
  server.registerTool(
4258
4313
  "lexq_rules_list",
@@ -4302,8 +4357,8 @@ function registerRuleTools(server, callApi) {
4302
4357
  After saving, lexq_facts_unregistered lists any keys this version references but has not
4303
4358
  defined (non-blocking, version-wide) — use it to decide what to register.
4304
4359
 
4305
- Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
4306
- Value types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
4360
+ Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: ${oneOf(LogicalOperator)}, children: [...] }
4361
+ Value types: ${ValueType.join(", ")}
4307
4362
  Limits: a condition tree may hold at most ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes; GROUP nodes do not count)
4308
4363
  and nest at most ${MAX_CONDITION_DEPTH} levels below the root. A version holds at most ${MAX_RULES_PER_VERSION} rules.
4309
4364
 
@@ -4326,7 +4381,7 @@ function registerRuleTools(server, callApi) {
4326
4381
  Actions: [{ type, parameters }]
4327
4382
 
4328
4383
  Action parameter schemas:
4329
- - MUTATE_FACT: { targetVar: string, operator: "ASSIGN"|"ADD"|"SUB"|"MUL"|"DIV", method: "PERCENTAGE"|"AMOUNT", operand: number, refVar?: string, rounding?: RoundingOption }
4384
+ - MUTATE_FACT: { targetVar: string, operator: ${oneOf(MutateOperator)}, method: ${oneOf(CalculationMethod)}, operand: number, refVar?: string, rounding?: RoundingOption }
4330
4385
  targetVar is the fact this action reads and writes. It must be a registered fact
4331
4386
  (ACT-032 otherwise), and the request must send it (P-015 otherwise). Its value must be
4332
4387
  a number when the action runs.
@@ -4365,7 +4420,7 @@ function registerRuleTools(server, callApi) {
4365
4420
  writes, send that value as an input fact in a second request. If a rule should run
4366
4421
  whenever another one does, put its actions in that rule instead.
4367
4422
 
4368
- RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
4423
+ RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: ${oneOf(RoundingPolicy)} } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
4369
4424
  `,
4370
4425
  inputSchema: z6.object({
4371
4426
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -4496,7 +4551,7 @@ function registerFactTools(server, callApi) {
4496
4551
  {
4497
4552
  title: "Create Fact Definition",
4498
4553
  annotations: { readOnlyHint: false, destructiveHint: false },
4499
- description: "Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER.",
4554
+ description: `Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: ${VALUE_TYPES.join(", ")}.`,
4500
4555
  inputSchema: z7.object({
4501
4556
  key: z7.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
4502
4557
  name: z7.string().describe("Display name"),
@@ -4702,7 +4757,7 @@ function registerDeployTools(server, callApi) {
4702
4757
  {
4703
4758
  title: "List Scheduled Deployments",
4704
4759
  annotations: { readOnlyHint: true },
4705
- description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
4760
+ description: `List scheduled deployments across all groups (all statuses: ${DeploymentScheduleStatus.join(", ")}), newest first.`,
4706
4761
  inputSchema: z8.object({
4707
4762
  page: z8.number().int().min(0).default(0).describe("Page number"),
4708
4763
  size: z8.number().int().min(1).max(100).default(20).describe("Page size")
@@ -4720,7 +4775,7 @@ function registerDeployTools(server, callApi) {
4720
4775
  page: z8.number().int().min(0).default(0).describe("Page number"),
4721
4776
  size: z8.number().int().min(1).max(100).default(20).describe("Page size"),
4722
4777
  groupId: z8.string().uuid().optional().describe("Filter by group ID"),
4723
- types: z8.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
4778
+ types: z8.string().optional().describe(`Filter by deployment types (comma-separated: ${DeploymentType.join(",")})`),
4724
4779
  startDate: z8.string().optional().describe(START_DAY),
4725
4780
  endDate: z8.string().optional().describe(END_DAY)
4726
4781
  })
@@ -4802,7 +4857,7 @@ function registerProfileTools(server, callApi) {
4802
4857
  {
4803
4858
  title: "Group Latency Profile",
4804
4859
  annotations: { readOnlyHint: true },
4805
- description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 6, p95 n \u2265 60, p99 n \u2265 300 \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
4860
+ description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99} \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
4806
4861
  inputSchema: z9.object({
4807
4862
  groupId: z9.string().uuid().describe("Policy group ID"),
4808
4863
  versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
@@ -4810,7 +4865,7 @@ function registerProfileTools(server, callApi) {
4810
4865
  `Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
4811
4866
  ),
4812
4867
  to: z9.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
4813
- cacheState: z9.enum(["HIT", "MISS"]).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
4868
+ cacheState: z9.enum(ProfileCacheState).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
4814
4869
  })
4815
4870
  },
4816
4871
  async ({ groupId, versionId, from, to, cacheState }) => callApi("GET", `policy-groups/${groupId}/profile`, {
@@ -4822,7 +4877,7 @@ function registerProfileTools(server, callApi) {
4822
4877
  {
4823
4878
  title: "Rule Latency Detail",
4824
4879
  annotations: { readOnlyHint: true },
4825
- description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265 6, p95 n \u2265 60, p99 n \u2265 300). ` + RELATIVE_THRESHOLD,
4880
+ description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99}). ` + RELATIVE_THRESHOLD,
4826
4881
  inputSchema: z9.object({
4827
4882
  groupId: z9.string().uuid().describe("Policy group ID"),
4828
4883
  ruleId: z9.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
@@ -4939,7 +4994,7 @@ function registerHistoryTools(server, callApi) {
4939
4994
  traceId: z11.string().optional().describe("Filter by trace ID"),
4940
4995
  groupId: z11.string().uuid().optional().describe("Filter by policy group"),
4941
4996
  versionId: z11.string().uuid().optional().describe("Filter by version"),
4942
- status: z11.enum(["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"]).optional().describe("Filter by execution status"),
4997
+ status: z11.enum(ApiExecutionStatus).optional().describe("Filter by execution status"),
4943
4998
  startDate: z11.string().optional().describe(START_DAY),
4944
4999
  endDate: z11.string().optional().describe(END_DAY)
4945
5000
  })
@@ -5034,6 +5089,10 @@ function registerProvenanceTools(server, callApi) {
5034
5089
 
5035
5090
  // src/mcp/tools/logs.ts
5036
5091
  import { z as z13 } from "zod";
5092
+ var ACTION_NOTES = {
5093
+ RESOLVE: "mark as manually fixed",
5094
+ IGNORE: "skip intentionally"
5095
+ };
5037
5096
  function registerLogTools(server, callApi) {
5038
5097
  server.registerTool(
5039
5098
  "lexq_logs_list",
@@ -5078,7 +5137,7 @@ function registerLogTools(server, callApi) {
5078
5137
  {
5079
5138
  title: "Process Failure Log",
5080
5139
  annotations: { readOnlyHint: false, destructiveHint: false },
5081
- description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
5140
+ description: `Process a single failure log: ${withNotes(ACTION_NOTES).join(" or ")}.`,
5082
5141
  inputSchema: z13.object({
5083
5142
  logId: z13.string().uuid().describe("Failure log ID"),
5084
5143
  action: z13.enum(FailureAction).describe("Action to take")
@@ -5107,6 +5166,10 @@ function registerLogTools(server, callApi) {
5107
5166
 
5108
5167
  // src/mcp/tools/webhook-subscriptions.ts
5109
5168
  import { z as z14 } from "zod";
5169
+ var FORMAT_NOTES2 = {
5170
+ GENERIC: "full JSON",
5171
+ SLACK: '{"text": "..."}'
5172
+ };
5110
5173
  function registerWebhookSubscriptionTools(server, callApi) {
5111
5174
  server.registerTool(
5112
5175
  "lexq_webhook_subscriptions_list",
@@ -5141,7 +5204,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
5141
5204
  {
5142
5205
  title: "Save Webhook Subscription",
5143
5206
  annotations: { readOnlyHint: false, destructiveHint: true },
5144
- description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).`,
5207
+ description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: ${withNotes(FORMAT_NOTES2).join(", ")}.`,
5145
5208
  inputSchema: z14.object({
5146
5209
  id: z14.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
5147
5210
  name: z14.string().min(1).describe("Subscription name (unique per tenant)"),
@@ -31,9 +31,27 @@ function registerStatusTools(server, callApi) {
31
31
  import { z as z2 } from "zod";
32
32
 
33
33
  // src/types/enums.ts
34
+ var PolicyGroupStatus = ["ACTIVE", "DISABLED", "ARCHIVED"];
35
+ var LogicalOperator = ["AND", "OR"];
36
+ var MutateOperator = ["ASSIGN", "ADD", "SUB", "MUL", "DIV"];
37
+ var CalculationMethod = ["PERCENTAGE", "AMOUNT"];
38
+ var RoundingPolicy = [
39
+ "UP",
40
+ "DOWN",
41
+ "CEILING",
42
+ "FLOOR",
43
+ "HALF_UP",
44
+ "HALF_DOWN",
45
+ "HALF_EVEN"
46
+ ];
34
47
  var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
35
48
  var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
36
49
  var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
50
+ var DeploymentType = ["DEPLOY", "ROLLBACK", "UNDEPLOY"];
51
+ var ApiExecutionStatus = ["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"];
52
+ var ProfileCacheState = ["HIT", "MISS"];
53
+ var SimulationStatus = ["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"];
54
+ var SimulationMetricType = ["COUNT", "SUM", "AVG"];
37
55
  var ExportFormat = ["CSV", "JSON"];
38
56
  var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
39
57
  var FailureAction = ["IGNORE", "RESOLVE"];
@@ -47,9 +65,12 @@ var PlatformEventType = [
47
65
  "DEPLOY_SCHEDULE_CANCELED"
48
66
  ];
49
67
  var WebhookPayloadFormat = ["GENERIC", "SLACK"];
68
+ var DeploymentScheduleStatus = ["PENDING", "EXECUTED", "CANCELED", "FAILED"];
50
69
 
51
70
  // src/types/constants.ts
52
71
  var MAX_ROUNDING_SCALE = 34;
72
+ var MIN_TRAFFIC_RATE = 1;
73
+ var MAX_TRAFFIC_RATE = 99;
53
74
  var REPLAY_WINDOW_MAX_RECORDS = 5e4;
54
75
  var DEFAULT_SIMULATION_RECORDS = 1e3;
55
76
  var MAX_SIMULATION_RECORDS = 1e5;
@@ -66,6 +87,10 @@ var MAX_RULES_PER_VERSION = 100;
66
87
  var MAX_CONDITIONS_PER_RULE = 25;
67
88
  var MAX_CONDITION_DEPTH = 10;
68
89
  var MAX_SNAPSHOT_BYTES = 131072;
90
+ var STATS_DEFAULT_WINDOW_DAYS = 30;
91
+ var STATS_MAX_WINDOW_DAYS = 31;
92
+ var FAILURE_LOG_LOOKBACK_DAYS = 7;
93
+ var PURGE_AFTER_DAYS = 30;
69
94
 
70
95
  // src/mcp/tools/groups.ts
71
96
  function registerGroupTools(server, callApi) {
@@ -132,7 +157,7 @@ function registerGroupTools(server, callApi) {
132
157
  groupId: z2.string().uuid().describe("Policy group ID"),
133
158
  name: z2.string().optional().describe("New name"),
134
159
  description: z2.string().optional().describe("New description"),
135
- status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
160
+ status: z2.enum(PolicyGroupStatus).extract(["ACTIVE", "DISABLED"]).optional().describe(
136
161
  "DISABLED stops production execution of this group: execution calls return 403 (P-009), and a composite call that includes the group is rejected in full. The deployed version and any A/B test are kept, and dry run, Impact Simulation, and Decision Replay keep working, so a fix can be verified before setting ACTIVE again. To go back to the previous version, use lexq_deploy_rollback. To take the version out of production, use lexq_deploy_undeploy."
137
162
  ),
138
163
  activationGroup: z2.string().optional().describe(
@@ -152,7 +177,7 @@ function registerGroupTools(server, callApi) {
152
177
  {
153
178
  title: "Delete Policy Group",
154
179
  annotations: { readOnlyHint: false, destructiveHint: true },
155
- description: "Delete (archive) a policy group. A deployed group cannot be deleted (P-020); undeploy it first with lexq_deploy_undeploy, which also ends a running A/B test. This cannot be undone. The group no longer appears in lexq_groups_list and its name can be reused right away. Calls that change it (settings, versions, rules, starting an A/B test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted after 30 days.",
180
+ description: `Delete (archive) a policy group. A deployed group cannot be deleted (P-020); undeploy it first with lexq_deploy_undeploy, which also ends a running A/B test. This cannot be undone. The group no longer appears in lexq_groups_list and its name can be reused right away. Calls that change it (settings, versions, rules, starting an A/B test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted after ${PURGE_AFTER_DAYS} days.`,
156
181
  inputSchema: z2.object({
157
182
  groupId: z2.string().uuid().describe("Policy group ID")
158
183
  })
@@ -186,7 +211,9 @@ function registerGroupTools(server, callApi) {
186
211
  inputSchema: z2.object({
187
212
  groupId: z2.string().uuid().describe("Policy group ID"),
188
213
  testVersionId: z2.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
189
- trafficRate: z2.number().int().min(1).max(99).describe("Traffic percentage routed to challenger (1-99)")
214
+ trafficRate: z2.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
215
+ `Traffic percentage routed to challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
216
+ )
190
217
  })
191
218
  },
192
219
  async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/ab-test`, { body })
@@ -211,7 +238,9 @@ function registerGroupTools(server, callApi) {
211
238
  description: "Adjust traffic rate of a running A/B test.",
212
239
  inputSchema: z2.object({
213
240
  groupId: z2.string().uuid().describe("Policy group ID"),
214
- trafficRate: z2.number().int().min(1).max(99).describe("New traffic percentage for challenger (1-99)")
241
+ trafficRate: z2.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
242
+ `New traffic percentage for challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
243
+ )
215
244
  })
216
245
  },
217
246
  async ({ groupId, ...body }) => callApi("PATCH", `policy-groups/${groupId}/ab-test/traffic-rate`, { body })
@@ -324,9 +353,6 @@ var END_DAY = `End date, ${ORG_DAY}. Inclusive: the whole day counts.`;
324
353
  var WINDOW_START_DAY = `Window start date, ${ORG_DAY}. Inclusive.`;
325
354
  var WINDOW_END_DAY = `Window end date, ${ORG_DAY}. Inclusive: the whole day counts.`;
326
355
  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.";
327
- var STATS_DEFAULT_WINDOW_DAYS = 30;
328
- var STATS_MAX_WINDOW_DAYS = 31;
329
- var FAILURE_LOG_LOOKBACK_DAYS = 7;
330
356
 
331
357
  // src/mcp/tools/versions.ts
332
358
  function registerVersionTools(server, callApi) {
@@ -427,6 +453,7 @@ function registerVersionTools(server, callApi) {
427
453
  // src/mcp/tools/rules.ts
428
454
  import { z as z4 } from "zod";
429
455
  import dedent from "dedent";
456
+ var oneOf = (values) => values.map((v) => `"${v}"`).join("|");
430
457
  function registerRuleTools(server, callApi) {
431
458
  server.registerTool(
432
459
  "lexq_rules_list",
@@ -476,8 +503,8 @@ function registerRuleTools(server, callApi) {
476
503
  After saving, lexq_facts_unregistered lists any keys this version references but has not
477
504
  defined (non-blocking, version-wide) — use it to decide what to register.
478
505
 
479
- Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
480
- Value types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
506
+ Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: ${oneOf(LogicalOperator)}, children: [...] }
507
+ Value types: ${ValueType.join(", ")}
481
508
  Limits: a condition tree may hold at most ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes; GROUP nodes do not count)
482
509
  and nest at most ${MAX_CONDITION_DEPTH} levels below the root. A version holds at most ${MAX_RULES_PER_VERSION} rules.
483
510
 
@@ -500,7 +527,7 @@ function registerRuleTools(server, callApi) {
500
527
  Actions: [{ type, parameters }]
501
528
 
502
529
  Action parameter schemas:
503
- - MUTATE_FACT: { targetVar: string, operator: "ASSIGN"|"ADD"|"SUB"|"MUL"|"DIV", method: "PERCENTAGE"|"AMOUNT", operand: number, refVar?: string, rounding?: RoundingOption }
530
+ - MUTATE_FACT: { targetVar: string, operator: ${oneOf(MutateOperator)}, method: ${oneOf(CalculationMethod)}, operand: number, refVar?: string, rounding?: RoundingOption }
504
531
  targetVar is the fact this action reads and writes. It must be a registered fact
505
532
  (ACT-032 otherwise), and the request must send it (P-015 otherwise). Its value must be
506
533
  a number when the action runs.
@@ -539,7 +566,7 @@ function registerRuleTools(server, callApi) {
539
566
  writes, send that value as an input fact in a second request. If a rule should run
540
567
  whenever another one does, put its actions in that rule instead.
541
568
 
542
- RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
569
+ RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: ${oneOf(RoundingPolicy)} } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
543
570
  `,
544
571
  inputSchema: z4.object({
545
572
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -674,7 +701,7 @@ function registerFactTools(server, callApi) {
674
701
  {
675
702
  title: "Create Fact Definition",
676
703
  annotations: { readOnlyHint: false, destructiveHint: false },
677
- description: "Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER.",
704
+ description: `Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: ${VALUE_TYPES.join(", ")}.`,
678
705
  inputSchema: z5.object({
679
706
  key: z5.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
680
707
  name: z5.string().describe("Display name"),
@@ -880,7 +907,7 @@ function registerDeployTools(server, callApi) {
880
907
  {
881
908
  title: "List Scheduled Deployments",
882
909
  annotations: { readOnlyHint: true },
883
- description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
910
+ description: `List scheduled deployments across all groups (all statuses: ${DeploymentScheduleStatus.join(", ")}), newest first.`,
884
911
  inputSchema: z6.object({
885
912
  page: z6.number().int().min(0).default(0).describe("Page number"),
886
913
  size: z6.number().int().min(1).max(100).default(20).describe("Page size")
@@ -898,7 +925,7 @@ function registerDeployTools(server, callApi) {
898
925
  page: z6.number().int().min(0).default(0).describe("Page number"),
899
926
  size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
900
927
  groupId: z6.string().uuid().optional().describe("Filter by group ID"),
901
- types: z6.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
928
+ types: z6.string().optional().describe(`Filter by deployment types (comma-separated: ${DeploymentType.join(",")})`),
902
929
  startDate: z6.string().optional().describe(START_DAY),
903
930
  endDate: z6.string().optional().describe(END_DAY)
904
931
  })
@@ -1061,7 +1088,7 @@ function registerAnalyticsTools(server, callApi) {
1061
1088
  options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
1062
1089
  options.includeRuleStats: boolean
1063
1090
  options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
1064
- { "targetVariable": "<fact>", "aggregationType": "COUNT" | "SUM" | "AVG" }
1091
+ { "targetVariable": "<fact>", "aggregationType": ${SimulationMetricType.map((t) => `"${t}"`).join(" | ")} }
1065
1092
 
1066
1093
  Example (uploaded dataset):
1067
1094
  {
@@ -1107,7 +1134,7 @@ function registerAnalyticsTools(server, callApi) {
1107
1134
  inputSchema: z7.object({
1108
1135
  page: z7.number().int().min(0).default(0).describe("Page number"),
1109
1136
  size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
1110
- status: z7.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
1137
+ status: z7.enum(SimulationStatus).optional().describe("Filter by status"),
1111
1138
  from: z7.string().optional().describe(START_DAY),
1112
1139
  to: z7.string().optional().describe(END_DAY)
1113
1140
  })
@@ -1210,6 +1237,15 @@ function registerAnalyticsTools(server, callApi) {
1210
1237
 
1211
1238
  // src/mcp/tools/profile.ts
1212
1239
  import { z as z8 } from "zod";
1240
+
1241
+ // src/lib/percentiles.ts
1242
+ var PERCENTILE_MIN_N = {
1243
+ p50: TAIL_MIN_OBS * 2,
1244
+ p95: TAIL_MIN_OBS * 20,
1245
+ p99: TAIL_MIN_OBS * 100
1246
+ };
1247
+
1248
+ // src/mcp/tools/profile.ts
1213
1249
  var RELATIVE_THRESHOLD = `flagged = p50 \u2265 ${SLOW_MULTIPLIER}\xD7 median of per-rule p50s within the group; absolute thresholds are intentionally not supported.`;
1214
1250
  function profileParams(opts) {
1215
1251
  const params = {};
@@ -1225,7 +1261,7 @@ function registerProfileTools(server, callApi) {
1225
1261
  {
1226
1262
  title: "Group Latency Profile",
1227
1263
  annotations: { readOnlyHint: true },
1228
- description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 6, p95 n \u2265 60, p99 n \u2265 300 \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
1264
+ description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99} \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
1229
1265
  inputSchema: z8.object({
1230
1266
  groupId: z8.string().uuid().describe("Policy group ID"),
1231
1267
  versionId: z8.string().uuid().optional().describe("Version to inspect (default: live version)"),
@@ -1233,7 +1269,7 @@ function registerProfileTools(server, callApi) {
1233
1269
  `Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
1234
1270
  ),
1235
1271
  to: z8.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
1236
- cacheState: z8.enum(["HIT", "MISS"]).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
1272
+ cacheState: z8.enum(ProfileCacheState).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
1237
1273
  })
1238
1274
  },
1239
1275
  async ({ groupId, versionId, from, to, cacheState }) => callApi("GET", `policy-groups/${groupId}/profile`, {
@@ -1245,7 +1281,7 @@ function registerProfileTools(server, callApi) {
1245
1281
  {
1246
1282
  title: "Rule Latency Detail",
1247
1283
  annotations: { readOnlyHint: true },
1248
- description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265 6, p95 n \u2265 60, p99 n \u2265 300). ` + RELATIVE_THRESHOLD,
1284
+ description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99}). ` + RELATIVE_THRESHOLD,
1249
1285
  inputSchema: z8.object({
1250
1286
  groupId: z8.string().uuid().describe("Policy group ID"),
1251
1287
  ruleId: z8.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
@@ -1362,7 +1398,7 @@ function registerHistoryTools(server, callApi) {
1362
1398
  traceId: z10.string().optional().describe("Filter by trace ID"),
1363
1399
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
1364
1400
  versionId: z10.string().uuid().optional().describe("Filter by version"),
1365
- status: z10.enum(["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"]).optional().describe("Filter by execution status"),
1401
+ status: z10.enum(ApiExecutionStatus).optional().describe("Filter by execution status"),
1366
1402
  startDate: z10.string().optional().describe(START_DAY),
1367
1403
  endDate: z10.string().optional().describe(END_DAY)
1368
1404
  })
@@ -1457,6 +1493,17 @@ function registerProvenanceTools(server, callApi) {
1457
1493
 
1458
1494
  // src/mcp/tools/logs.ts
1459
1495
  import { z as z12 } from "zod";
1496
+
1497
+ // src/lib/enum-notes.ts
1498
+ function withNotes(notes) {
1499
+ return Object.entries(notes).map(([value, note]) => note ? `${value} (${note})` : value);
1500
+ }
1501
+
1502
+ // src/mcp/tools/logs.ts
1503
+ var ACTION_NOTES = {
1504
+ RESOLVE: "mark as manually fixed",
1505
+ IGNORE: "skip intentionally"
1506
+ };
1460
1507
  function registerLogTools(server, callApi) {
1461
1508
  server.registerTool(
1462
1509
  "lexq_logs_list",
@@ -1501,7 +1548,7 @@ function registerLogTools(server, callApi) {
1501
1548
  {
1502
1549
  title: "Process Failure Log",
1503
1550
  annotations: { readOnlyHint: false, destructiveHint: false },
1504
- description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
1551
+ description: `Process a single failure log: ${withNotes(ACTION_NOTES).join(" or ")}.`,
1505
1552
  inputSchema: z12.object({
1506
1553
  logId: z12.string().uuid().describe("Failure log ID"),
1507
1554
  action: z12.enum(FailureAction).describe("Action to take")
@@ -1530,6 +1577,10 @@ function registerLogTools(server, callApi) {
1530
1577
 
1531
1578
  // src/mcp/tools/webhook-subscriptions.ts
1532
1579
  import { z as z13 } from "zod";
1580
+ var FORMAT_NOTES = {
1581
+ GENERIC: "full JSON",
1582
+ SLACK: '{"text": "..."}'
1583
+ };
1533
1584
  function registerWebhookSubscriptionTools(server, callApi) {
1534
1585
  server.registerTool(
1535
1586
  "lexq_webhook_subscriptions_list",
@@ -1564,7 +1615,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
1564
1615
  {
1565
1616
  title: "Save Webhook Subscription",
1566
1617
  annotations: { readOnlyHint: false, destructiveHint: true },
1567
- description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).`,
1618
+ description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: ${withNotes(FORMAT_NOTES).join(", ")}.`,
1568
1619
  inputSchema: z13.object({
1569
1620
  id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
1570
1621
  name: z13.string().min(1).describe("Subscription name (unique per tenant)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "mcpName": "io.lexq/lexq",
5
5
  "description": "The Decision Operations Platform for engineering teams",
6
6
  "type": "module",