@lexq/cli 2.0.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
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
  import { readFileSync as readFileSync4 } from "fs";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
- import { dirname as dirname2, join as join3 } from "path";
7
+ import { dirname as dirname2, join as join4 } from "path";
8
8
 
9
9
  // src/commands/auth.ts
10
10
  import "commander";
@@ -85,6 +85,11 @@ var ApiError = class extends Error {
85
85
  statusCode;
86
86
  errorCode;
87
87
  };
88
+ function describeForm(form) {
89
+ return [...form.entries()].map(
90
+ ([name, value]) => typeof value === "string" ? ` ${name}: ${value}` : ` ${name}: ${value.name} (${value.size} bytes, ${value.type})`
91
+ );
92
+ }
88
93
  async function doFetch(method, path, options) {
89
94
  const config = loadConfig();
90
95
  const baseUrl = options.baseUrl ?? config.baseUrl;
@@ -99,26 +104,32 @@ async function doFetch(method, path, options) {
99
104
  }
100
105
  }
101
106
  const headers = { "X-API-KEY": apiKey, Accept: "application/json" };
102
- if (options.body) headers["Content-Type"] = "application/json";
107
+ if (options.body && !options.form) headers["Content-Type"] = "application/json";
103
108
  if (options.dryRun) {
104
109
  const masked = apiKey.length > 8 ? apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length - 4) : "****";
105
110
  console.log(`${method} ${url.toString()}`);
106
111
  console.log("Headers:");
107
112
  console.log(` X-API-KEY: ${masked}`);
108
- console.log(` Content-Type: application/json`);
109
- if (options.body) {
113
+ console.log(` Content-Type: ${options.form ? "multipart/form-data" : "application/json"}`);
114
+ if (options.form) {
115
+ console.log("Body:");
116
+ for (const line of describeForm(options.form)) console.log(line);
117
+ } else if (options.body) {
110
118
  console.log("Body:");
111
119
  console.log(` ${stringifyJson(options.body, 2) ?? ""}`);
112
120
  }
113
121
  console.log("\n(Use without --dry-run to execute)");
114
122
  process.exit(0);
115
123
  }
116
- if (options.verbose) console.error(`\u2192 ${method} ${url.toString()}`);
124
+ if (options.verbose) {
125
+ console.error(`\u2192 ${method} ${url.toString()}`);
126
+ if (options.form) for (const line of describeForm(options.form)) console.error(line);
127
+ }
117
128
  const startTime = Date.now();
118
129
  const response = await fetch(url.toString(), {
119
130
  method,
120
131
  headers,
121
- body: options.body ? stringifyJson(options.body) : void 0
132
+ body: options.form ?? (options.body ? stringifyJson(options.body) : void 0)
122
133
  });
123
134
  if (options.verbose) {
124
135
  console.error(`\u2190 ${response.status} ${response.statusText} (${Date.now() - startTime}ms)`);
@@ -132,7 +143,7 @@ function isFileResponse(response) {
132
143
  return contentType.includes("text/csv") || contentType.includes("application/octet-stream");
133
144
  }
134
145
  function assertOk(response, json) {
135
- if (!response.ok || json.result !== "SUCCESS") {
146
+ if (json.result !== "SUCCESS") {
136
147
  throw new ApiError(
137
148
  response.status,
138
149
  json.errorCode,
@@ -140,12 +151,25 @@ function assertOk(response, json) {
140
151
  );
141
152
  }
142
153
  }
154
+ async function refusal(response) {
155
+ let json;
156
+ try {
157
+ json = parseJson(await response.text());
158
+ } catch {
159
+ }
160
+ return new ApiError(
161
+ response.status,
162
+ json?.errorCode ?? null,
163
+ json?.message ?? `Request failed with status ${response.status}`
164
+ );
165
+ }
143
166
  async function apiRequest(method, path, options = {}) {
144
167
  const { data } = await apiRequestWithMeta(method, path, options);
145
168
  return data;
146
169
  }
147
170
  async function apiRequestWithMeta(method, path, options = {}) {
148
171
  const response = await doFetch(method, path, options);
172
+ if (!response.ok) throw await refusal(response);
149
173
  if (isFileResponse(response)) {
150
174
  return { data: await response.text(), meta: null };
151
175
  }
@@ -349,6 +373,7 @@ var MAX_TRAFFIC_RATE = 99;
349
373
  var REPLAY_WINDOW_MAX_RECORDS = 5e4;
350
374
  var DEFAULT_SIMULATION_RECORDS = 1e3;
351
375
  var MAX_SIMULATION_RECORDS = 1e5;
376
+ var MAX_DATASET_SIZE = 10485760;
352
377
  var PROFILE_SAMPLE_PERMILLE = 10;
353
378
  var LATENCY_WINDOW_MILLIS = 6e4;
354
379
  var TAIL_MIN_OBS = 3;
@@ -362,11 +387,57 @@ var MAX_RULES_PER_VERSION = 100;
362
387
  var MAX_CONDITIONS_PER_RULE = 25;
363
388
  var MAX_CONDITION_DEPTH = 10;
364
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;
365
397
 
366
398
  // src/commands/groups.ts
367
399
  import "commander";
368
400
  import dedent3 from "dedent";
369
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
+
370
441
  // src/lib/dates.ts
371
442
  var ORG_DAY = "yyyy-MM-dd, a whole calendar day in the organization time zone (tenantTimezone in whoami), not UTC";
372
443
  var START_DAY = `Start date, ${ORG_DAY}. Inclusive.`;
@@ -374,9 +445,6 @@ var END_DAY = `End date, ${ORG_DAY}. Inclusive: the whole day counts.`;
374
445
  var WINDOW_START_DAY = `Window start date, ${ORG_DAY}. Inclusive.`;
375
446
  var WINDOW_END_DAY = `Window end date, ${ORG_DAY}. Inclusive: the whole day counts.`;
376
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.";
377
- var STATS_DEFAULT_WINDOW_DAYS = 30;
378
- var STATS_MAX_WINDOW_DAYS = 31;
379
- var FAILURE_LOG_LOOKBACK_DAYS = 7;
380
448
  function utcMinute(instant) {
381
449
  return instant ? `${new Date(instant).toISOString().substring(0, 16)}Z` : "\u2013";
382
450
  }
@@ -399,7 +467,7 @@ function registerGroupCommands(program) {
399
467
  delete Archive a group
400
468
  ab-test Manage A/B tests (start, stop, adjust)
401
469
 
402
- Statuses: ACTIVE, DISABLED, ARCHIVED
470
+ Statuses: ${PolicyGroupStatus.join(", ")}
403
471
  DISABLED stops production execution (403 P-009) and keeps the deployed version and any A/B test.
404
472
  Dry run, simulation, and replay still work. Set ACTIVE to resume.
405
473
  ARCHIVED means deleted. Only 'lexq groups delete' sets it, and it cannot be undone.
@@ -465,9 +533,9 @@ ${data.length} total`);
465
533
 
466
534
  Fields:
467
535
  name string Group name (required, unique per tenant)
468
- description string Description (optional, max 255 chars)
536
+ description string Description (optional, max ${DESCRIPTION_MAX_LENGTH} chars)
469
537
  activationGroup string Execution Group — conflict-resolution cluster key (optional)
470
- activationMode string NONE | EXCLUSIVE | MAX_N [default: NONE]
538
+ activationMode string ${ConflictResolutionMode.join(" | ")} [default: NONE]
471
539
  activationStrategy string HIGHEST_PRIORITY [default, and currently the only value]
472
540
  executionLimit number Max rules to fire in MAX_N mode (optional)
473
541
  status string ACTIVE | DISABLED [default: ACTIVE]
@@ -529,7 +597,7 @@ ${data.length} total`);
529
597
  It cannot be restored, and calls that change it (settings, versions, rules, starting an A/B
530
598
  test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever
531
599
  executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted
532
- after 30 days.
600
+ after ${PURGE_AFTER_DAYS} days.
533
601
 
534
602
  Use --force to skip the confirmation prompt.
535
603
  `
@@ -801,7 +869,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
801
869
  }'
802
870
 
803
871
  Fields:
804
- commitMessage string Version description (optional, max 255 chars)
872
+ commitMessage string Version description (optional, max ${COMMIT_MESSAGE_MAX_LENGTH} chars)
805
873
  effectiveFrom instant Start of effective period (optional, with Z or an offset)
806
874
  effectiveTo instant End of effective period (optional, auto-expires)
807
875
  `
@@ -1066,11 +1134,11 @@ ${data.length} total`);
1066
1134
  refVar is the percentage base and is optional — omit to use targetVar itself.
1067
1135
  It is only valid in PERCENTAGE × {ASSIGN, ADD, SUB}.
1068
1136
 
1069
- Value Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
1137
+ Value Types: ${ValueType.join(", ")}
1070
1138
 
1071
1139
  Mutex (optional — rule-level conflict resolution):
1072
1140
  mutexGroup string Logical grouping key (e.g., "best-discount")
1073
- mutexMode string NONE | EXCLUSIVE | MAX_N [default: NONE]
1141
+ mutexMode string ${ConflictResolutionMode.join(" | ")} [default: NONE]
1074
1142
  mutexStrategy string HIGHEST_PRIORITY (currently the only value)
1075
1143
  mutexLimit number Max rules to fire in MAX_N mode (required when MAX_N)
1076
1144
 
@@ -1279,27 +1347,6 @@ function escapeUnsafe(json) {
1279
1347
 
1280
1348
  // src/lib/export.ts
1281
1349
  import { writeFileSync as writeFileSync2 } from "fs";
1282
-
1283
- // src/types/enums.ts
1284
- var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
1285
- var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
1286
- var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
1287
- var ProfileCacheState = ["HIT", "MISS"];
1288
- var ExportFormat = ["CSV", "JSON"];
1289
- var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
1290
- var FailureAction = ["IGNORE", "RESOLVE"];
1291
- var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
1292
- var PlatformEventType = [
1293
- "VERSION_PUBLISHED",
1294
- "DEPLOYED",
1295
- "ROLLED_BACK",
1296
- "UNDEPLOYED",
1297
- "DEPLOY_SCHEDULED",
1298
- "DEPLOY_SCHEDULE_CANCELED"
1299
- ];
1300
- var WebhookPayloadFormat = ["GENERIC", "SLACK"];
1301
-
1302
- // src/lib/export.ts
1303
1350
  var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
1304
1351
  function parseFormat(value) {
1305
1352
  if (typeof value === "string" && EXPORT_FORMATS.includes(value)) {
@@ -1434,7 +1481,7 @@ ${data.length} unregistered`);
1434
1481
  process.exit(1);
1435
1482
  }
1436
1483
  });
1437
- 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(
1438
1485
  "after",
1439
1486
  dedent6`
1440
1487
 
@@ -1454,7 +1501,7 @@ ${data.length} unregistered`);
1454
1501
  $ lexq facts create --key tier --name "Tier" --type STRING \
1455
1502
  --allowed-values "GOLD, SILVER, BRONZE"
1456
1503
 
1457
- Value Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
1504
+ Value Types: ${ValueType.join(", ")}
1458
1505
  Key Format: starts with a letter, then letters, numbers, and underscores.
1459
1506
  Casing is yours to choose (e.g., paymentAmount, payment_amount)
1460
1507
 
@@ -1483,7 +1530,7 @@ ${data.length} unregistered`);
1483
1530
  process.exit(1);
1484
1531
  }
1485
1532
  });
1486
- 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(
1487
1534
  "after",
1488
1535
  dedent6`
1489
1536
 
@@ -1867,7 +1914,7 @@ function registerDeployCommands(program) {
1867
1914
  process.exit(1);
1868
1915
  }
1869
1916
  });
1870
- 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(
1871
1918
  "after",
1872
1919
  dedent7`
1873
1920
 
@@ -2027,7 +2074,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2027
2074
  dedent7`
2028
2075
 
2029
2076
  The version must be ACTIVE with a future effective start date; the system
2030
- 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).
2031
2078
  That time is the version's effectiveFrom, set on the DRAFT before publishing;
2032
2079
  this command takes no time of its own.
2033
2080
  One pending schedule per group. Manual deploy/rollback/undeploy, starting
@@ -2157,8 +2204,30 @@ async function warnUnregisteredFacts(globalOpts, groupId, versionId) {
2157
2204
 
2158
2205
  // src/commands/analytics.ts
2159
2206
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
2207
+ import { basename } from "path";
2160
2208
  import "commander";
2161
2209
  import dedent8 from "dedent";
2210
+
2211
+ // src/lib/dataset-upload.ts
2212
+ import { extname } from "path";
2213
+ var DATASET_CONTENT_TYPES = {
2214
+ ".csv": "text/csv",
2215
+ ".json": "application/json"
2216
+ };
2217
+ function datasetContentType(filename) {
2218
+ return DATASET_CONTENT_TYPES[extname(filename).toLowerCase()];
2219
+ }
2220
+ function uploadDataset(bytes, filename, options) {
2221
+ const form = new FormData();
2222
+ const type = datasetContentType(filename) ?? "application/octet-stream";
2223
+ form.append("file", new Blob([bytes], { type }), filename);
2224
+ return apiRequest("POST", "analytics/datasets/upload", {
2225
+ ...options,
2226
+ form
2227
+ });
2228
+ }
2229
+
2230
+ // src/commands/analytics.ts
2162
2231
  function registerAnalyticsCommands(program) {
2163
2232
  const analytics = program.command("analytics").description("Dry run, simulation, and requirements").addHelpText(
2164
2233
  "after",
@@ -2330,8 +2399,8 @@ function registerAnalyticsCommands(program) {
2330
2399
 
2331
2400
  $ lexq analytics simulation start --file simulation.json
2332
2401
 
2333
- Dataset types: HISTORICAL, UPLOADED, MANUAL
2334
- Dataset sources: EXECUTION_LOGS, S3_BUCKET, REQUEST_BODY
2402
+ Dataset types: ${SimulationDatasetType.join(", ")}
2403
+ Dataset sources: ${SimulationDatasetSource.join(", ")}
2335
2404
  HISTORICAL from / to are yyyy-MM-dd, whole days in the organization time zone, both inclusive.
2336
2405
  `
2337
2406
  ).action(async (opts) => {
@@ -2452,7 +2521,7 @@ function registerAnalyticsCommands(program) {
2452
2521
  process.exit(1);
2453
2522
  }
2454
2523
  });
2455
- 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) => {
2456
2525
  try {
2457
2526
  const globalOpts = program.opts();
2458
2527
  const format = globalOpts.format ?? "json";
@@ -2577,41 +2646,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2577
2646
  ).action(async (opts) => {
2578
2647
  try {
2579
2648
  const globalOpts = program.opts();
2580
- const config = loadConfig();
2581
- const baseUrl = globalOpts.baseUrl ?? config.baseUrl;
2582
- const apiKey = globalOpts.apiKey ?? config.apiKey;
2583
- if (!apiKey) {
2584
- throw new Error('Not authenticated. Run "lexq auth login" first.');
2585
- }
2586
2649
  const filePath = opts.file;
2587
- const fileBuffer = readFileSync2(filePath);
2588
- const fileName = filePath.split("/").pop() ?? "dataset";
2589
- const ext = fileName.split(".").pop()?.toLowerCase();
2590
- let contentType = "application/octet-stream";
2591
- if (ext === "csv") contentType = "text/csv";
2592
- else if (ext === "json") contentType = "application/json";
2593
- const blob = new Blob([fileBuffer], { type: contentType });
2594
- const formData = new FormData();
2595
- formData.append("file", blob, fileName);
2596
- const url = new URL(
2597
- "analytics/datasets/upload",
2598
- baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"
2599
- );
2600
- if (globalOpts.verbose) {
2601
- console.error(`\u2192 POST ${url.toString()}`);
2602
- console.error(` File: ${filePath} (${fileBuffer.length} bytes)`);
2603
- }
2604
- const response = await fetch(url.toString(), {
2605
- method: "POST",
2606
- headers: { "X-API-KEY": apiKey },
2607
- body: formData
2650
+ const result = await uploadDataset(readFileSync2(filePath), basename(filePath), {
2651
+ apiKey: globalOpts.apiKey,
2652
+ baseUrl: globalOpts.baseUrl,
2653
+ dryRun: globalOpts.dryRun,
2654
+ verbose: globalOpts.verbose
2608
2655
  });
2609
- if (!response.ok) {
2610
- const errorText = await response.text();
2611
- throw new Error(`Upload failed (${response.status}): ${errorText}`);
2612
- }
2613
- const envelope = await response.json();
2614
- const result = envelope.data;
2615
2656
  console.log(`\u2713 Dataset uploaded: ${result.path}`);
2616
2657
  console.log(` File: ${result.filename} (${result.size} bytes)`);
2617
2658
  printJson(result);
@@ -2684,6 +2725,15 @@ function resolveBody(opts) {
2684
2725
  // src/commands/profile.ts
2685
2726
  import "commander";
2686
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
2687
2737
  var ms = (nanos) => nanos == null ? "\u2013" : (nanos / 1e6).toFixed(2);
2688
2738
  var msWithUnit = (nanos) => nanos == null ? "\u2013" : `${(nanos / 1e6).toFixed(2)}ms`;
2689
2739
  function registerProfileCommands(program) {
@@ -2693,15 +2743,18 @@ function registerProfileCommands(program) {
2693
2743
  ).option("--version-id <versionId>", "Version to inspect (default: live version)").option(
2694
2744
  "--from <instant>",
2695
2745
  `Window start, ISO-8601 instant (default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago)`
2696
- ).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(
2697
2750
  "after",
2698
2751
  dedent9`
2699
2752
 
2700
2753
  Slow-rule judgment is relative only: flagged = p50 ≥ ${SLOW_MULTIPLIER}× the median of
2701
2754
  per-rule p50s within the group. Absolute ms thresholds are intentionally
2702
- not supported. Each percentile is withheld (–) unless n×(1−q) ≥ 3 —
2703
- p50 from n ≥ 6, p95 from n ≥ 60, p99 from n ≥ 300. TOTAL is recorded
2704
- 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.
2705
2758
 
2706
2759
  Examples:
2707
2760
  $ lexq profile <groupId>
@@ -2799,10 +2852,10 @@ function registerHistoryCommands(program) {
2799
2852
  get Get full execution detail (request facts, traces, decisions)
2800
2853
  stats Aggregate statistics (success rate, latency, counts)
2801
2854
 
2802
- Statuses: SUCCESS, NO_MATCH, ERROR, TIMEOUT
2855
+ Statuses: ${ApiExecutionStatus.join(", ")}
2803
2856
  `
2804
2857
  );
2805
- 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(
2806
2859
  "after",
2807
2860
  dedent10`
2808
2861
 
@@ -3195,6 +3248,18 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3195
3248
  // src/commands/logs.ts
3196
3249
  import "commander";
3197
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
+ };
3198
3263
  function registerLogCommands(program) {
3199
3264
  const logs = program.command("logs").description("Failure logs").addHelpText(
3200
3265
  "after",
@@ -3205,13 +3270,13 @@ function registerLogCommands(program) {
3205
3270
  Commands:
3206
3271
  list List failure logs with filters
3207
3272
  get Get failure log detail (includes the original payload)
3208
- action Process a single log (IGNORE, RESOLVE)
3273
+ action Process a single log (${FailureAction.join(", ")})
3209
3274
  bulk-action Process multiple logs at once
3210
3275
 
3211
- Statuses: PENDING (needs attention), RESOLVED, IGNORED
3276
+ Statuses: ${withNotes(STATUS_NOTES).join(", ")}
3212
3277
  `
3213
3278
  );
3214
- 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(
3215
3280
  "after",
3216
3281
  dedent13`
3217
3282
 
@@ -3267,7 +3332,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3267
3332
  dedent13`
3268
3333
 
3269
3334
  Includes the full payload that was used for the failed operation.
3270
- 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 ")}.
3271
3336
  `
3272
3337
  ).action(async (opts) => {
3273
3338
  try {
@@ -3284,7 +3349,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3284
3349
  process.exit(1);
3285
3350
  }
3286
3351
  });
3287
- 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(
3288
3353
  "after",
3289
3354
  dedent13`
3290
3355
 
@@ -3315,7 +3380,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3315
3380
  process.exit(1);
3316
3381
  }
3317
3382
  });
3318
- 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(
3319
3384
  "after",
3320
3385
  dedent13`
3321
3386
 
@@ -3346,6 +3411,10 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3346
3411
  // src/commands/webhook-subscriptions.ts
3347
3412
  import "commander";
3348
3413
  import dedent14 from "dedent";
3414
+ var FORMAT_NOTES = {
3415
+ GENERIC: "full JSON",
3416
+ SLACK: '{"text":"..."}'
3417
+ };
3349
3418
  function registerWebhookSubscriptionCommands(program) {
3350
3419
  const webhooks = program.command("webhook-subscriptions").description("Manage platform event webhook subscriptions").addHelpText(
3351
3420
  "after",
@@ -3448,7 +3517,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3448
3517
  name string Subscription name (required, unique per tenant)
3449
3518
  webhookUrl string Webhook endpoint URL (required)
3450
3519
  subscribedEvents string[] ${PlatformEventType.join(", ")}
3451
- payloadFormat string GENERIC (full JSON) or SLACK ({"text":"..."}) [default: GENERIC]
3520
+ payloadFormat string ${withNotes(FORMAT_NOTES).join(" or ")} [default: GENERIC]
3452
3521
  secret string HMAC-SHA256 signing secret (optional)
3453
3522
  isActive boolean Enable/disable (optional) [default: true]
3454
3523
  id string Provide to update, omit to create
@@ -3548,56 +3617,36 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3548
3617
 
3549
3618
  // src/commands/serve.ts
3550
3619
  import "commander";
3551
- import dedent18 from "dedent";
3620
+ import dedent19 from "dedent";
3552
3621
 
3553
3622
  // src/mcp/server.ts
3554
3623
  import { readFileSync as readFileSync3 } from "fs";
3555
- import { dirname, join as join2 } from "path";
3624
+ import { dirname, join as join3 } from "path";
3556
3625
  import { fileURLToPath } from "url";
3557
3626
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
3558
3627
  import { McpServer } from "@modelcontextprotocol/server";
3559
3628
 
3560
3629
  // src/mcp/tools/_shared.ts
3630
+ function localApiOptions() {
3631
+ const stored = loadConfig();
3632
+ return {
3633
+ baseUrl: process.env.PARTNER_BASE_URL ?? stored.baseUrl,
3634
+ apiKey: process.env.LEXQ_API_KEY ?? stored.apiKey
3635
+ };
3636
+ }
3561
3637
  function createCallApiFromConfig() {
3562
3638
  return async (method, path, opts) => {
3563
3639
  try {
3564
- const stored = loadConfig();
3565
- const config = {
3566
- ...stored,
3567
- baseUrl: process.env.PARTNER_BASE_URL ?? stored.baseUrl,
3568
- apiKey: process.env.LEXQ_API_KEY ?? stored.apiKey
3569
- };
3640
+ let form;
3570
3641
  if (opts?.upload) {
3571
- const url = new URL(
3572
- path,
3573
- config.baseUrl.endsWith("/") ? config.baseUrl : config.baseUrl + "/"
3574
- );
3575
- if (opts.params) {
3576
- for (const [key, value] of Object.entries(opts.params)) {
3577
- if (value !== void 0) url.searchParams.set(key, value);
3578
- }
3579
- }
3580
- if (!config.apiKey) {
3581
- throw new Error('Not authenticated. Run "lexq auth login" first.');
3582
- }
3642
+ form = new FormData();
3583
3643
  const blob = new Blob([opts.upload.content], { type: "text/plain" });
3584
- const formData = new FormData();
3585
- formData.append(opts.upload.fieldName, blob, opts.upload.filename);
3586
- const response = await fetch(url.toString(), {
3587
- method,
3588
- headers: { "X-API-KEY": config.apiKey },
3589
- body: formData
3590
- });
3591
- const data2 = parseJson(await response.text());
3592
- return { content: [{ type: "text", text: stringifyJson(data2, 2) ?? "" }] };
3644
+ form.append(opts.upload.fieldName, blob, opts.upload.filename);
3593
3645
  }
3594
- const clientOpts = {
3595
- apiKey: config.apiKey,
3596
- baseUrl: config.baseUrl
3597
- };
3598
3646
  const { data, meta } = await apiRequestWithMeta(method, path, {
3599
- ...clientOpts,
3647
+ ...localApiOptions(),
3600
3648
  body: opts?.body,
3649
+ form,
3601
3650
  params: opts?.params
3602
3651
  });
3603
3652
  const content = [
@@ -3640,124 +3689,457 @@ function formatUnregisteredFactWarning(meta) {
3640
3689
  ].join("\n");
3641
3690
  }
3642
3691
 
3643
- // src/mcp/server-info.json
3644
- var server_info_default = {
3645
- title: "LexQ",
3646
- description: "The Decision Operations Platform for engineering teams",
3647
- websiteUrl: "https://lexq.io",
3648
- icons: [
3649
- {
3650
- src: "https://lexq.io/logo.png",
3651
- mimeType: "image/png",
3652
- sizes: ["1024x1024"]
3653
- }
3654
- ]
3655
- };
3692
+ // src/mcp/local-tools.ts
3693
+ import { readFile, realpath, stat } from "fs/promises";
3694
+ import { homedir as homedir2 } from "os";
3695
+ import { basename as basename2, isAbsolute, join as join2 } from "path";
3696
+ import { z as z2 } from "zod";
3697
+ import dedent16 from "dedent";
3656
3698
 
3657
- // src/mcp/tools/status.ts
3699
+ // src/mcp/tools/analytics.ts
3658
3700
  import { z } from "zod";
3659
- function registerStatusTools(server, callApi) {
3701
+ import dedent15 from "dedent";
3702
+ function datasetBlock(path) {
3703
+ const dataset = { type: "UPLOADED", source: "S3_BUCKET", path };
3704
+ return {
3705
+ type: "text",
3706
+ text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
3707
+ };
3708
+ }
3709
+ function registerAnalyticsTools(server, callApi) {
3660
3710
  server.registerTool(
3661
- "lexq_whoami",
3711
+ "lexq_dry_run",
3662
3712
  {
3663
- title: "Who Am I",
3713
+ title: "Dry Run",
3664
3714
  annotations: { readOnlyHint: true },
3665
- 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.',
3666
- inputSchema: z.object({})
3715
+ description: dedent15`
3716
+ Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
3717
+
3718
+ Returns:
3719
+ inputFacts — normalized input facts
3720
+ mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates paymentAmount)
3721
+ generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {factName}__delta key (signed difference)
3722
+ executionTraces — per-rule match status
3723
+ decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
3724
+
3725
+ Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
3726
+ Always dry-run before publishing to validate rule behavior.`,
3727
+ inputSchema: z.object({
3728
+ versionId: z.string().uuid().describe("Policy version ID to test against"),
3729
+ facts: z.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
3730
+ includeDebugInfo: z.boolean().default(true).describe("Include execution and decision traces")
3731
+ })
3667
3732
  },
3668
- async () => callApi("GET", "whoami")
3733
+ async ({ versionId, facts, includeDebugInfo }) => {
3734
+ const parsedFacts = parseJson(facts);
3735
+ return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
3736
+ body: { facts: parsedFacts, includeDebugInfo }
3737
+ });
3738
+ }
3669
3739
  );
3670
- }
3671
-
3672
- // src/mcp/tools/groups.ts
3673
- import { z as z2 } from "zod";
3674
- function registerGroupTools(server, callApi) {
3675
3740
  server.registerTool(
3676
- "lexq_groups_list",
3741
+ "lexq_dry_run_compare",
3677
3742
  {
3678
- title: "List Policy Groups",
3743
+ title: "Dry Run Compare",
3679
3744
  annotations: { readOnlyHint: true },
3680
- description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
3681
- inputSchema: z2.object({})
3745
+ description: dedent15`
3746
+ Compare dry run results between two versions using the same input facts. Useful for validating changes.
3747
+
3748
+ Returns:
3749
+ resultA / resultB — full DryRunResponse for each version
3750
+ diff.mutatedDiff — changes in mutatedFacts between A and B (key → {before, after})
3751
+ diff.generatedDiff — changes in generatedVariables between A and B`,
3752
+ inputSchema: z.object({
3753
+ versionIdA: z.string().uuid().describe("Baseline version ID"),
3754
+ versionIdB: z.string().uuid().describe("Candidate version ID"),
3755
+ facts: z.string().describe("JSON string of facts object")
3756
+ })
3682
3757
  },
3683
- async () => callApi("GET", "policy-groups")
3758
+ async ({ versionIdA, versionIdB, facts }) => {
3759
+ const parsedFacts = parseJson(facts);
3760
+ return callApi("POST", "analytics/dry-run/compare", {
3761
+ body: { versionIdA, versionIdB, facts: parsedFacts }
3762
+ });
3763
+ }
3684
3764
  );
3685
3765
  server.registerTool(
3686
- "lexq_groups_get",
3766
+ "lexq_requirements",
3687
3767
  {
3688
- title: "Get Policy Group",
3768
+ title: "Analyze Requirements",
3689
3769
  annotations: { readOnlyHint: true },
3690
- description: "Get a single policy group by ID. Its currentVersionId and currentDeploymentId are what lexq_deploy_rollback expects, and previousVersionName is the version a rollback restores.",
3691
- inputSchema: z2.object({
3692
- groupId: z2.string().uuid().describe("Policy group ID")
3770
+ description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
3771
+ inputSchema: z.object({
3772
+ groupId: z.string().uuid().describe("Policy group ID"),
3773
+ versionId: z.string().uuid().describe("Version ID")
3693
3774
  })
3694
3775
  },
3695
- async ({ groupId }) => callApi("GET", `policy-groups/${groupId}`)
3776
+ async ({ groupId, versionId }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/requirements`)
3696
3777
  );
3697
3778
  server.registerTool(
3698
- "lexq_groups_create",
3779
+ "lexq_simulation_start",
3699
3780
  {
3700
- title: "Create Policy Group",
3781
+ title: "Start Simulation",
3701
3782
  annotations: { readOnlyHint: false, destructiveHint: false },
3702
- 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.`,
3703
- inputSchema: z2.object({
3704
- name: z2.string().describe("Group name (unique among non-ARCHIVED)"),
3705
- description: z2.string().optional().describe("Group description"),
3706
- activationMode: z2.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
3707
- activationStrategy: z2.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups when mode is EXCLUSIVE or MAX_N"),
3708
- executionLimit: z2.number().int().min(1).optional().describe(
3709
- "How many policy groups sharing this activationGroup may run (required when mode is MAX_N). Counts groups, not rules: every rule of a winning group runs."
3710
- ),
3711
- activationGroup: z2.string().optional().describe(
3712
- "Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
3713
- )
3783
+ description: dedent15`
3784
+ Start an Impact Simulation against historical, uploaded, or inline data.
3785
+
3786
+ dataset.type and dataset.source are BOTH required, and must be paired:
3787
+ HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
3788
+ in the organization time zone, both inclusive)
3789
+ UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
3790
+ MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
3791
+
3792
+ options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
3793
+ most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
3794
+ options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
3795
+ options.includeRuleStats: boolean
3796
+ options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
3797
+ { "targetVariable": "<fact>", "aggregationType": ${SimulationMetricType.map((t) => `"${t}"`).join(" | ")} }
3798
+
3799
+ Example (uploaded dataset):
3800
+ {
3801
+ "policyVersionId": "<uuid>",
3802
+ "dataset": { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<path from lexq_dataset_upload>" },
3803
+ "options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true, "maxRecords": 10000 }
3804
+ }
3805
+
3806
+ Example (historical):
3807
+ {
3808
+ "policyVersionId": "<uuid>",
3809
+ "dataset": { "type": "HISTORICAL", "source": "EXECUTION_LOGS", "from": "2026-01-01", "to": "2026-01-31" },
3810
+ "options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true }
3811
+ }
3812
+ `,
3813
+ inputSchema: z.object({
3814
+ body: z.string().describe("JSON string of SimulationRequest")
3714
3815
  })
3715
3816
  },
3716
- async (args) => {
3717
- const body = {
3718
- name: args.name
3719
- };
3720
- if (args.description !== void 0) body.description = args.description;
3721
- if (args.activationMode !== void 0) body.activationMode = args.activationMode;
3722
- if (args.activationStrategy !== void 0) body.activationStrategy = args.activationStrategy;
3723
- if (args.executionLimit !== void 0) body.executionLimit = args.executionLimit;
3724
- if (args.activationGroup !== void 0) body.activationGroup = args.activationGroup;
3725
- return callApi("POST", "policy-groups", { body });
3817
+ async ({ body }) => {
3818
+ const parsed = parseJson(body);
3819
+ return callApi("POST", "analytics/simulations", { body: parsed });
3726
3820
  }
3727
3821
  );
3728
3822
  server.registerTool(
3729
- "lexq_groups_update",
3823
+ "lexq_simulation_status",
3730
3824
  {
3731
- title: "Update Policy Group",
3732
- annotations: { readOnlyHint: false, destructiveHint: true },
3733
- description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
3734
- inputSchema: z2.object({
3735
- groupId: z2.string().uuid().describe("Policy group ID"),
3736
- name: z2.string().optional().describe("New name"),
3737
- description: z2.string().optional().describe("New description"),
3738
- status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
3739
- "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."
3740
- ),
3741
- activationGroup: z2.string().optional().describe(
3742
- "Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
3743
- ),
3744
- activationMode: z2.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
3745
- activationStrategy: z2.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups"),
3746
- executionLimit: z2.number().int().min(1).optional().describe(
3747
- "How many policy groups sharing this activationGroup may run. Counts groups, not rules."
3748
- )
3825
+ title: "Simulation Status",
3826
+ annotations: { readOnlyHint: true },
3827
+ description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
3828
+ inputSchema: z.object({
3829
+ simulationId: z.string().uuid().describe("Simulation ID")
3749
3830
  })
3750
3831
  },
3751
- async ({ groupId, ...body }) => callApi("PUT", `policy-groups/${groupId}`, { body })
3832
+ async ({ simulationId }) => callApi("GET", `analytics/simulations/${simulationId}`)
3752
3833
  );
3753
3834
  server.registerTool(
3754
- "lexq_groups_delete",
3835
+ "lexq_simulation_list",
3755
3836
  {
3756
- title: "Delete Policy Group",
3837
+ title: "List Simulations",
3838
+ annotations: { readOnlyHint: true },
3839
+ description: "List simulation history with optional filters.",
3840
+ inputSchema: z.object({
3841
+ page: z.number().int().min(0).default(0).describe("Page number"),
3842
+ size: z.number().int().min(1).max(100).default(20).describe("Page size"),
3843
+ status: z.enum(SimulationStatus).optional().describe("Filter by status"),
3844
+ from: z.string().optional().describe(START_DAY),
3845
+ to: z.string().optional().describe(END_DAY)
3846
+ })
3847
+ },
3848
+ async ({ page, size, status, from, to }) => {
3849
+ const params = paginationParams(page, size);
3850
+ if (status) params.status = status;
3851
+ if (from) params.from = from;
3852
+ if (to) params.to = to;
3853
+ return callApi("GET", "analytics/simulations", { params });
3854
+ }
3855
+ );
3856
+ server.registerTool(
3857
+ "lexq_simulation_cancel",
3858
+ {
3859
+ title: "Cancel Simulation",
3757
3860
  annotations: { readOnlyHint: false, destructiveHint: true },
3758
- 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.",
3861
+ description: "Cancel a running or pending simulation.",
3862
+ inputSchema: z.object({
3863
+ simulationId: z.string().uuid().describe("Simulation ID")
3864
+ })
3865
+ },
3866
+ async ({ simulationId }) => callApi("DELETE", `analytics/simulations/${simulationId}`)
3867
+ );
3868
+ server.registerTool(
3869
+ "lexq_simulation_export",
3870
+ {
3871
+ title: "Export Simulation",
3872
+ annotations: { readOnlyHint: true },
3873
+ description: "Export simulation results as JSON or CSV. Returns the raw data.",
3874
+ inputSchema: z.object({
3875
+ simulationId: z.string().uuid().describe("Simulation ID"),
3876
+ format: z.enum(EXPORT_FORMATS).default("json").describe("Export format")
3877
+ })
3878
+ },
3879
+ async ({ simulationId, format }) => callApi("GET", `analytics/simulations/${simulationId}/export`, {
3880
+ params: { format }
3881
+ })
3882
+ );
3883
+ server.registerTool(
3884
+ "lexq_dataset_upload",
3885
+ {
3886
+ title: "Upload Dataset",
3887
+ annotations: { readOnlyHint: false, destructiveHint: false },
3888
+ description: dedent15`
3889
+ Upload inline CSV or JSON content as a simulation dataset.
3890
+ The content is uploaded to S3 and a path is returned in the "path" field.
3891
+
3892
+ To use the returned path in lexq_simulation_start, set:
3893
+ dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
3894
+
3895
+ The content travels inside the tool call, so this suits small datasets, and the hosted server at
3896
+ mcp.lexq.io limits the size of a request. For a larger file, use lexq_dataset_upload_file on the
3897
+ local server (lexq serve --mcp), or upload it with the CLI or the console and pass the returned path.
3898
+
3899
+ CSV example:
3900
+ userId,paymentAmount
3901
+ user_001,150000
3902
+ user_002,50000
3903
+
3904
+ JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
3905
+ `,
3906
+ inputSchema: z.object({
3907
+ content: z.string().describe("CSV or JSON content as string"),
3908
+ filename: z.string().default("dataset.csv").describe("Filename with extension (.csv or .json)")
3909
+ })
3910
+ },
3911
+ async ({ content, filename }) => {
3912
+ const result = await callApi("POST", "analytics/datasets/upload", {
3913
+ upload: { content, filename, fieldName: "file" }
3914
+ });
3915
+ if (!result.isError) {
3916
+ try {
3917
+ const uploaded = parseJson(result.content[0]?.text ?? "{}");
3918
+ const path = uploaded.data?.path ?? uploaded.path;
3919
+ if (typeof path === "string" && path) result.content.push(datasetBlock(path));
3920
+ } catch {
3921
+ }
3922
+ }
3923
+ return result;
3924
+ }
3925
+ );
3926
+ server.registerTool(
3927
+ "lexq_dataset_template",
3928
+ {
3929
+ title: "Download Dataset Template",
3930
+ annotations: { readOnlyHint: true },
3931
+ description: "Generate a sample CSV or JSON template based on the required facts of a version. Use this to understand the expected data format before uploading a dataset.",
3932
+ inputSchema: z.object({
3933
+ groupId: z.string().uuid().describe("Policy group ID"),
3934
+ versionId: z.string().uuid().describe("Version ID"),
3935
+ format: z.enum(EXPORT_FORMATS).default("csv").describe("Template format")
3936
+ })
3937
+ },
3938
+ async ({ groupId, versionId, format }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/dataset-template`, {
3939
+ params: { format }
3940
+ })
3941
+ );
3942
+ }
3943
+
3944
+ // src/mcp/local-tools.ts
3945
+ var MAX_DATASET_MB = MAX_DATASET_SIZE / 1024 / 1024;
3946
+ var NOT_ABSOLUTE = "file must be an absolute path on this computer. A leading ~/ stands for the home directory.";
3947
+ var NOT_A_DATASET = "file must be a .csv or .json file, and a symbolic link must point to one.";
3948
+ var TOO_LARGE = `file is larger than ${MAX_DATASET_MB} MB.`;
3949
+ var UNREADABLE = "file does not exist, or is not a regular file this server can read.";
3950
+ var NOT_AN_ARRAY = "a .json file must hold an array of records.";
3951
+ function refuse(message) {
3952
+ return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
3953
+ }
3954
+ function startsWithArray(bytes) {
3955
+ let i = bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191 ? 3 : 0;
3956
+ while (bytes[i] === 32 || bytes[i] === 9 || bytes[i] === 10 || bytes[i] === 13) i++;
3957
+ return bytes[i] === 91;
3958
+ }
3959
+ async function readDatasetFile(file) {
3960
+ const named = file.startsWith("~/") ? join2(homedir2(), file.slice(2)) : file;
3961
+ if (!isAbsolute(named) || /^[\\/]{2}/.test(named)) return { error: NOT_ABSOLUTE };
3962
+ if (!datasetContentType(named)) return { error: NOT_A_DATASET };
3963
+ let real;
3964
+ try {
3965
+ real = await realpath(named);
3966
+ } catch {
3967
+ return { error: UNREADABLE };
3968
+ }
3969
+ const type = datasetContentType(real);
3970
+ if (!type) return { error: NOT_A_DATASET };
3971
+ try {
3972
+ const info = await stat(real);
3973
+ if (!info.isFile()) return { error: UNREADABLE };
3974
+ if (info.size > MAX_DATASET_SIZE) return { error: TOO_LARGE };
3975
+ const bytes = await readFile(real);
3976
+ if (type === "application/json" && !startsWithArray(bytes)) return { error: NOT_AN_ARRAY };
3977
+ return { bytes, filename: basename2(real) };
3978
+ } catch {
3979
+ return { error: UNREADABLE };
3980
+ }
3981
+ }
3982
+ function registerLocalTools(server) {
3983
+ server.registerTool(
3984
+ "lexq_dataset_upload_file",
3985
+ {
3986
+ title: "Upload Dataset File",
3987
+ annotations: { readOnlyHint: false, destructiveHint: false },
3988
+ description: dedent16`
3989
+ Upload a CSV or JSON file from this computer as a simulation dataset, up to ${MAX_DATASET_MB} MB.
3990
+ The server reads the file from disk, so its content never passes through the conversation.
3991
+ Only the local server (lexq serve --mcp) has this tool.
3992
+
3993
+ file: the absolute path of a .csv or .json file. A leading ~/ stands for the home directory.
3994
+ A symbolic link is followed, and the file it points to must also end in .csv or .json.
3995
+
3996
+ CSV: a header row of fact keys, then one row per record. JSON: an array of objects.
3997
+
3998
+ Returns the stored path and a ready-to-use dataset block for lexq_simulation_start:
3999
+ dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
4000
+ `,
3759
4001
  inputSchema: z2.object({
3760
- groupId: z2.string().uuid().describe("Policy group ID")
4002
+ file: z2.string().min(1).describe(
4003
+ "Absolute path of a .csv or .json file. A leading ~/ stands for the home directory."
4004
+ )
4005
+ })
4006
+ },
4007
+ async ({ file }) => {
4008
+ const read = await readDatasetFile(file);
4009
+ if ("error" in read) return refuse(read.error);
4010
+ try {
4011
+ const uploaded = await uploadDataset(read.bytes, read.filename, localApiOptions());
4012
+ return {
4013
+ content: [
4014
+ { type: "text", text: stringifyJson(uploaded, 2) ?? "" },
4015
+ datasetBlock(uploaded.path)
4016
+ ]
4017
+ };
4018
+ } catch (error) {
4019
+ return refuse(error instanceof Error ? error.message : String(error));
4020
+ }
4021
+ }
4022
+ );
4023
+ }
4024
+
4025
+ // src/mcp/server-info.json
4026
+ var server_info_default = {
4027
+ title: "LexQ",
4028
+ description: "The Decision Operations Platform for engineering teams",
4029
+ websiteUrl: "https://lexq.io",
4030
+ icons: [
4031
+ {
4032
+ src: "https://lexq.io/logo.png",
4033
+ mimeType: "image/png",
4034
+ sizes: ["1024x1024"]
4035
+ }
4036
+ ]
4037
+ };
4038
+
4039
+ // src/mcp/tools/status.ts
4040
+ import { z as z3 } from "zod";
4041
+ function registerStatusTools(server, callApi) {
4042
+ server.registerTool(
4043
+ "lexq_whoami",
4044
+ {
4045
+ title: "Who Am I",
4046
+ annotations: { readOnlyHint: true },
4047
+ 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.',
4048
+ inputSchema: z3.object({})
4049
+ },
4050
+ async () => callApi("GET", "whoami")
4051
+ );
4052
+ }
4053
+
4054
+ // src/mcp/tools/groups.ts
4055
+ import { z as z4 } from "zod";
4056
+ function registerGroupTools(server, callApi) {
4057
+ server.registerTool(
4058
+ "lexq_groups_list",
4059
+ {
4060
+ title: "List Policy Groups",
4061
+ annotations: { readOnlyHint: true },
4062
+ description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
4063
+ inputSchema: z4.object({})
4064
+ },
4065
+ async () => callApi("GET", "policy-groups")
4066
+ );
4067
+ server.registerTool(
4068
+ "lexq_groups_get",
4069
+ {
4070
+ title: "Get Policy Group",
4071
+ annotations: { readOnlyHint: true },
4072
+ description: "Get a single policy group by ID. Its currentVersionId and currentDeploymentId are what lexq_deploy_rollback expects, and previousVersionName is the version a rollback restores.",
4073
+ inputSchema: z4.object({
4074
+ groupId: z4.string().uuid().describe("Policy group ID")
4075
+ })
4076
+ },
4077
+ async ({ groupId }) => callApi("GET", `policy-groups/${groupId}`)
4078
+ );
4079
+ server.registerTool(
4080
+ "lexq_groups_create",
4081
+ {
4082
+ title: "Create Policy Group",
4083
+ annotations: { readOnlyHint: false, destructiveHint: false },
4084
+ 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.`,
4085
+ inputSchema: z4.object({
4086
+ name: z4.string().describe("Group name (unique among non-ARCHIVED)"),
4087
+ description: z4.string().optional().describe("Group description"),
4088
+ activationMode: z4.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
4089
+ activationStrategy: z4.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups when mode is EXCLUSIVE or MAX_N"),
4090
+ executionLimit: z4.number().int().min(1).optional().describe(
4091
+ "How many policy groups sharing this activationGroup may run (required when mode is MAX_N). Counts groups, not rules: every rule of a winning group runs."
4092
+ ),
4093
+ activationGroup: z4.string().optional().describe(
4094
+ "Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
4095
+ )
4096
+ })
4097
+ },
4098
+ async (args) => {
4099
+ const body = {
4100
+ name: args.name
4101
+ };
4102
+ if (args.description !== void 0) body.description = args.description;
4103
+ if (args.activationMode !== void 0) body.activationMode = args.activationMode;
4104
+ if (args.activationStrategy !== void 0) body.activationStrategy = args.activationStrategy;
4105
+ if (args.executionLimit !== void 0) body.executionLimit = args.executionLimit;
4106
+ if (args.activationGroup !== void 0) body.activationGroup = args.activationGroup;
4107
+ return callApi("POST", "policy-groups", { body });
4108
+ }
4109
+ );
4110
+ server.registerTool(
4111
+ "lexq_groups_update",
4112
+ {
4113
+ title: "Update Policy Group",
4114
+ annotations: { readOnlyHint: false, destructiveHint: true },
4115
+ description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
4116
+ inputSchema: z4.object({
4117
+ groupId: z4.string().uuid().describe("Policy group ID"),
4118
+ name: z4.string().optional().describe("New name"),
4119
+ description: z4.string().optional().describe("New description"),
4120
+ status: z4.enum(PolicyGroupStatus).extract(["ACTIVE", "DISABLED"]).optional().describe(
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."
4122
+ ),
4123
+ activationGroup: z4.string().optional().describe(
4124
+ "Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
4125
+ ),
4126
+ activationMode: z4.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
4127
+ activationStrategy: z4.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups"),
4128
+ executionLimit: z4.number().int().min(1).optional().describe(
4129
+ "How many policy groups sharing this activationGroup may run. Counts groups, not rules."
4130
+ )
4131
+ })
4132
+ },
4133
+ async ({ groupId, ...body }) => callApi("PUT", `policy-groups/${groupId}`, { body })
4134
+ );
4135
+ server.registerTool(
4136
+ "lexq_groups_delete",
4137
+ {
4138
+ title: "Delete Policy Group",
4139
+ annotations: { readOnlyHint: false, destructiveHint: true },
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.`,
4141
+ inputSchema: z4.object({
4142
+ groupId: z4.string().uuid().describe("Policy group ID")
3761
4143
  })
3762
4144
  },
3763
4145
  async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}`)
@@ -3768,8 +4150,8 @@ function registerGroupTools(server, callApi) {
3768
4150
  title: "Reorder Policy Groups",
3769
4151
  annotations: { readOnlyHint: false, destructiveHint: true },
3770
4152
  description: "Reorder policy groups by priority. Priority is tenant-wide and flat (1...N continuous); array index 0 = priority 1 (highest precedence). activationGroup is not affected \u2014 this only changes priority.",
3771
- inputSchema: z2.object({
3772
- groupIds: z2.array(z2.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
4153
+ inputSchema: z4.object({
4154
+ groupIds: z4.array(z4.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
3773
4155
  })
3774
4156
  },
3775
4157
  async ({ groupIds }) => {
@@ -3786,10 +4168,12 @@ function registerGroupTools(server, callApi) {
3786
4168
  title: "Start A/B Test",
3787
4169
  annotations: { readOnlyHint: false, destructiveHint: false },
3788
4170
  description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. The challenger must be a published version of the same group; a version from another group is rejected with P-016. The split is computed from context.trafficKey on each execution request; requests that omit it never reach the challenger and the test stays at 0%.",
3789
- inputSchema: z2.object({
3790
- groupId: z2.string().uuid().describe("Policy group ID"),
3791
- testVersionId: z2.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
3792
- trafficRate: z2.number().int().min(1).max(99).describe("Traffic percentage routed to challenger (1-99)")
4171
+ inputSchema: z4.object({
4172
+ groupId: z4.string().uuid().describe("Policy group ID"),
4173
+ testVersionId: z4.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
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
+ )
3793
4177
  })
3794
4178
  },
3795
4179
  async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/ab-test`, { body })
@@ -3800,8 +4184,8 @@ function registerGroupTools(server, callApi) {
3800
4184
  title: "Stop A/B Test",
3801
4185
  annotations: { readOnlyHint: false, destructiveHint: true },
3802
4186
  description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
3803
- inputSchema: z2.object({
3804
- groupId: z2.string().uuid().describe("Policy group ID")
4187
+ inputSchema: z4.object({
4188
+ groupId: z4.string().uuid().describe("Policy group ID")
3805
4189
  })
3806
4190
  },
3807
4191
  async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/ab-test`)
@@ -3812,9 +4196,11 @@ function registerGroupTools(server, callApi) {
3812
4196
  title: "Adjust A/B Test",
3813
4197
  annotations: { readOnlyHint: false, destructiveHint: true },
3814
4198
  description: "Adjust traffic rate of a running A/B test.",
3815
- inputSchema: z2.object({
3816
- groupId: z2.string().uuid().describe("Policy group ID"),
3817
- trafficRate: z2.number().int().min(1).max(99).describe("New traffic percentage for challenger (1-99)")
4199
+ inputSchema: z4.object({
4200
+ groupId: z4.string().uuid().describe("Policy group ID"),
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
+ )
3818
4204
  })
3819
4205
  },
3820
4206
  async ({ groupId, ...body }) => callApi("PATCH", `policy-groups/${groupId}/ab-test/traffic-rate`, { body })
@@ -3822,7 +4208,7 @@ function registerGroupTools(server, callApi) {
3822
4208
  }
3823
4209
 
3824
4210
  // src/mcp/tools/versions.ts
3825
- import { z as z3 } from "zod";
4211
+ import { z as z5 } from "zod";
3826
4212
  function registerVersionTools(server, callApi) {
3827
4213
  server.registerTool(
3828
4214
  "lexq_versions_list",
@@ -3830,10 +4216,10 @@ function registerVersionTools(server, callApi) {
3830
4216
  title: "List Policy Versions",
3831
4217
  annotations: { readOnlyHint: true },
3832
4218
  description: "List all versions of a policy group.",
3833
- inputSchema: z3.object({
3834
- groupId: z3.string().uuid().describe("Policy group ID"),
3835
- page: z3.number().int().min(0).default(0).describe("Page number"),
3836
- size: z3.number().int().min(1).max(100).default(20).describe("Page size")
4219
+ inputSchema: z5.object({
4220
+ groupId: z5.string().uuid().describe("Policy group ID"),
4221
+ page: z5.number().int().min(0).default(0).describe("Page number"),
4222
+ size: z5.number().int().min(1).max(100).default(20).describe("Page size")
3837
4223
  })
3838
4224
  },
3839
4225
  async ({ groupId, page, size }) => callApi("GET", `policy-groups/${groupId}/versions`, {
@@ -3846,9 +4232,9 @@ function registerVersionTools(server, callApi) {
3846
4232
  title: "Get Policy Version",
3847
4233
  annotations: { readOnlyHint: true },
3848
4234
  description: "Get a single version by ID, including its rules and fact requirements.",
3849
- inputSchema: z3.object({
3850
- groupId: z3.string().uuid().describe("Policy group ID"),
3851
- versionId: z3.string().uuid().describe("Version ID")
4235
+ inputSchema: z5.object({
4236
+ groupId: z5.string().uuid().describe("Policy group ID"),
4237
+ versionId: z5.string().uuid().describe("Version ID")
3852
4238
  })
3853
4239
  },
3854
4240
  async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}`)
@@ -3859,11 +4245,11 @@ function registerVersionTools(server, callApi) {
3859
4245
  title: "Create Policy Version",
3860
4246
  annotations: { readOnlyHint: false, destructiveHint: false },
3861
4247
  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.`,
3862
- inputSchema: z3.object({
3863
- groupId: z3.string().uuid().describe("Policy group ID"),
3864
- commitMessage: z3.string().optional().describe("Commit message describing this version"),
3865
- effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT}`),
3866
- effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT}`)
4248
+ inputSchema: z5.object({
4249
+ groupId: z5.string().uuid().describe("Policy group ID"),
4250
+ commitMessage: z5.string().optional().describe("Commit message describing this version"),
4251
+ effectiveFrom: z5.string().optional().describe(`Effective start, ${INSTANT}`),
4252
+ effectiveTo: z5.string().optional().describe(`Effective end, ${INSTANT}`)
3867
4253
  })
3868
4254
  },
3869
4255
  async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/versions`, { body })
@@ -3874,14 +4260,14 @@ function registerVersionTools(server, callApi) {
3874
4260
  title: "Update Policy Version",
3875
4261
  annotations: { readOnlyHint: false, destructiveHint: true },
3876
4262
  description: "Update a DRAFT version. Only DRAFT versions can be modified. Omitted fields keep their current values. To remove an end of the effective window, set clearEffectiveFrom or clearEffectiveTo to true instead of sending a value.",
3877
- inputSchema: z3.object({
3878
- groupId: z3.string().uuid().describe("Policy group ID"),
3879
- versionId: z3.string().uuid().describe("Version ID"),
3880
- commitMessage: z3.string().optional().describe("New commit message"),
3881
- effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
3882
- effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
3883
- clearEffectiveFrom: z3.boolean().optional().describe("Set to true to remove the effective start. Do not send effectiveFrom with it."),
3884
- clearEffectiveTo: z3.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
4263
+ inputSchema: z5.object({
4264
+ groupId: z5.string().uuid().describe("Policy group ID"),
4265
+ versionId: z5.string().uuid().describe("Version ID"),
4266
+ commitMessage: z5.string().optional().describe("New commit message"),
4267
+ effectiveFrom: z5.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
4268
+ effectiveTo: z5.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
4269
+ clearEffectiveFrom: z5.boolean().optional().describe("Set to true to remove the effective start. Do not send effectiveFrom with it."),
4270
+ clearEffectiveTo: z5.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
3885
4271
  })
3886
4272
  },
3887
4273
  async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
@@ -3892,9 +4278,9 @@ function registerVersionTools(server, callApi) {
3892
4278
  title: "Delete Policy Version",
3893
4279
  annotations: { readOnlyHint: false, destructiveHint: true },
3894
4280
  description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
3895
- inputSchema: z3.object({
3896
- groupId: z3.string().uuid().describe("Policy group ID"),
3897
- versionId: z3.string().uuid().describe("Version ID")
4281
+ inputSchema: z5.object({
4282
+ groupId: z5.string().uuid().describe("Policy group ID"),
4283
+ versionId: z5.string().uuid().describe("Version ID")
3898
4284
  })
3899
4285
  },
3900
4286
  async ({ groupId, versionId }) => callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}`)
@@ -3905,9 +4291,9 @@ function registerVersionTools(server, callApi) {
3905
4291
  title: "Clone Policy Version",
3906
4292
  annotations: { readOnlyHint: false, destructiveHint: false },
3907
4293
  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.`,
3908
- inputSchema: z3.object({
3909
- groupId: z3.string().uuid().describe("Policy group ID"),
3910
- versionId: z3.string().uuid().describe("Source version ID to clone")
4294
+ inputSchema: z5.object({
4295
+ groupId: z5.string().uuid().describe("Policy group ID"),
4296
+ versionId: z5.string().uuid().describe("Source version ID to clone")
3911
4297
  })
3912
4298
  },
3913
4299
  async ({ groupId, versionId }) => {
@@ -3919,8 +4305,9 @@ function registerVersionTools(server, callApi) {
3919
4305
  }
3920
4306
 
3921
4307
  // src/mcp/tools/rules.ts
3922
- import { z as z4 } from "zod";
3923
- import dedent15 from "dedent";
4308
+ import { z as z6 } from "zod";
4309
+ import dedent17 from "dedent";
4310
+ var oneOf = (values) => values.map((v) => `"${v}"`).join("|");
3924
4311
  function registerRuleTools(server, callApi) {
3925
4312
  server.registerTool(
3926
4313
  "lexq_rules_list",
@@ -3928,9 +4315,9 @@ function registerRuleTools(server, callApi) {
3928
4315
  title: "List Rules",
3929
4316
  annotations: { readOnlyHint: true },
3930
4317
  description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
3931
- inputSchema: z4.object({
3932
- groupId: z4.string().uuid().describe("Policy group ID"),
3933
- versionId: z4.string().uuid().describe("Version ID")
4318
+ inputSchema: z6.object({
4319
+ groupId: z6.string().uuid().describe("Policy group ID"),
4320
+ versionId: z6.string().uuid().describe("Version ID")
3934
4321
  })
3935
4322
  },
3936
4323
  async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/rules`)
@@ -3941,10 +4328,10 @@ function registerRuleTools(server, callApi) {
3941
4328
  title: "Get Rule Detail",
3942
4329
  annotations: { readOnlyHint: true },
3943
4330
  description: "Get full rule detail including condition tree and action definitions.",
3944
- inputSchema: z4.object({
3945
- groupId: z4.string().uuid().describe("Policy group ID"),
3946
- versionId: z4.string().uuid().describe("Version ID"),
3947
- ruleId: z4.string().uuid().describe("Rule ID")
4331
+ inputSchema: z6.object({
4332
+ groupId: z6.string().uuid().describe("Policy group ID"),
4333
+ versionId: z6.string().uuid().describe("Version ID"),
4334
+ ruleId: z6.string().uuid().describe("Rule ID")
3948
4335
  })
3949
4336
  },
3950
4337
  async ({ groupId, versionId, ruleId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`)
@@ -3954,7 +4341,7 @@ function registerRuleTools(server, callApi) {
3954
4341
  {
3955
4342
  title: "Create Rule",
3956
4343
  annotations: { readOnlyHint: false, destructiveHint: false },
3957
- description: dedent15`
4344
+ description: dedent17`
3958
4345
  Create a rule in a DRAFT version. Requires name, condition tree, and actions array. priority is auto-assigned (appended last); use lexq_rules_reorder to change order.
3959
4346
 
3960
4347
  Before creating rules with new fact keys, call lexq_facts_list to check existing facts.
@@ -3970,8 +4357,8 @@ function registerRuleTools(server, callApi) {
3970
4357
  After saving, lexq_facts_unregistered lists any keys this version references but has not
3971
4358
  defined (non-blocking, version-wide) — use it to decide what to register.
3972
4359
 
3973
- Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
3974
- 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(", ")}
3975
4362
  Limits: a condition tree may hold at most ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes; GROUP nodes do not count)
3976
4363
  and nest at most ${MAX_CONDITION_DEPTH} levels below the root. A version holds at most ${MAX_RULES_PER_VERSION} rules.
3977
4364
 
@@ -3994,7 +4381,7 @@ function registerRuleTools(server, callApi) {
3994
4381
  Actions: [{ type, parameters }]
3995
4382
 
3996
4383
  Action parameter schemas:
3997
- - 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 }
3998
4385
  targetVar is the fact this action reads and writes. It must be a registered fact
3999
4386
  (ACT-032 otherwise), and the request must send it (P-015 otherwise). Its value must be
4000
4387
  a number when the action runs.
@@ -4033,12 +4420,12 @@ function registerRuleTools(server, callApi) {
4033
4420
  writes, send that value as an input fact in a second request. If a rule should run
4034
4421
  whenever another one does, put its actions in that rule instead.
4035
4422
 
4036
- 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).
4037
4424
  `,
4038
- inputSchema: z4.object({
4039
- groupId: z4.string().uuid().describe("Policy group ID"),
4040
- versionId: z4.string().uuid().describe("Version ID"),
4041
- rule: z4.string().describe(
4425
+ inputSchema: z6.object({
4426
+ groupId: z6.string().uuid().describe("Policy group ID"),
4427
+ versionId: z6.string().uuid().describe("Version ID"),
4428
+ rule: z6.string().describe(
4042
4429
  "JSON string of CreateRuleRequest: { name, condition, actions, mutexGroup?, mutexMode?, mutexStrategy?, mutexLimit?, isEnabled? }"
4043
4430
  )
4044
4431
  })
@@ -4054,11 +4441,11 @@ function registerRuleTools(server, callApi) {
4054
4441
  title: "Update Rule",
4055
4442
  annotations: { readOnlyHint: false, destructiveHint: true },
4056
4443
  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. Saving re-checks every action in the rule, including ones you did not change, and fails with ACT-032 if any action's targetVar is not a registered fact.`,
4057
- inputSchema: z4.object({
4058
- groupId: z4.string().uuid().describe("Policy group ID"),
4059
- versionId: z4.string().uuid().describe("Version ID"),
4060
- ruleId: z4.string().uuid().describe("Rule ID"),
4061
- rule: z4.string().describe(
4444
+ inputSchema: z6.object({
4445
+ groupId: z6.string().uuid().describe("Policy group ID"),
4446
+ versionId: z6.string().uuid().describe("Version ID"),
4447
+ ruleId: z6.string().uuid().describe("Rule ID"),
4448
+ rule: z6.string().describe(
4062
4449
  "JSON string of UpdateRuleRequest: { name?, condition?, actions?, mutexGroup?, mutexMode?, mutexStrategy?, mutexLimit?, isEnabled? }"
4063
4450
  )
4064
4451
  })
@@ -4076,11 +4463,11 @@ function registerRuleTools(server, callApi) {
4076
4463
  title: "Delete Rule",
4077
4464
  annotations: { readOnlyHint: false, destructiveHint: true },
4078
4465
  description: "Delete a rule from a DRAFT version.",
4079
- inputSchema: z4.object({
4080
- groupId: z4.string().uuid().describe("Policy group ID"),
4081
- versionId: z4.string().uuid().describe("Version ID"),
4082
- ruleId: z4.string().uuid().describe("Rule ID"),
4083
- force: z4.boolean().default(false).describe("Skip confirmation (for the last rule in a version)")
4466
+ inputSchema: z6.object({
4467
+ groupId: z6.string().uuid().describe("Policy group ID"),
4468
+ versionId: z6.string().uuid().describe("Version ID"),
4469
+ ruleId: z6.string().uuid().describe("Rule ID"),
4470
+ force: z6.boolean().default(false).describe("Skip confirmation (for the last rule in a version)")
4084
4471
  })
4085
4472
  },
4086
4473
  async ({ groupId, versionId, ruleId, force }) => {
@@ -4097,10 +4484,10 @@ function registerRuleTools(server, callApi) {
4097
4484
  title: "Reorder Rules",
4098
4485
  annotations: { readOnlyHint: false, destructiveHint: true },
4099
4486
  description: "Reorder rules by specifying rule IDs in desired order. Priorities are assigned 1...N (1-based, continuous); array index 0 = priority 1 (highest precedence).",
4100
- inputSchema: z4.object({
4101
- groupId: z4.string().uuid().describe("Policy group ID"),
4102
- versionId: z4.string().uuid().describe("Version ID"),
4103
- ruleIds: z4.array(z4.string().uuid()).describe("Rule IDs in desired priority order")
4487
+ inputSchema: z6.object({
4488
+ groupId: z6.string().uuid().describe("Policy group ID"),
4489
+ versionId: z6.string().uuid().describe("Version ID"),
4490
+ ruleIds: z6.array(z6.string().uuid()).describe("Rule IDs in desired priority order")
4104
4491
  })
4105
4492
  },
4106
4493
  async ({ groupId, versionId, ruleIds }) => {
@@ -4119,11 +4506,11 @@ function registerRuleTools(server, callApi) {
4119
4506
  title: "Toggle Rule",
4120
4507
  annotations: { readOnlyHint: false, destructiveHint: true },
4121
4508
  description: "Enable or disable a rule without deleting it.",
4122
- inputSchema: z4.object({
4123
- groupId: z4.string().uuid().describe("Policy group ID"),
4124
- versionId: z4.string().uuid().describe("Version ID"),
4125
- ruleId: z4.string().uuid().describe("Rule ID"),
4126
- isEnabled: z4.boolean().describe("true to enable, false to disable")
4509
+ inputSchema: z6.object({
4510
+ groupId: z6.string().uuid().describe("Policy group ID"),
4511
+ versionId: z6.string().uuid().describe("Version ID"),
4512
+ ruleId: z6.string().uuid().describe("Rule ID"),
4513
+ isEnabled: z6.boolean().describe("true to enable, false to disable")
4127
4514
  })
4128
4515
  },
4129
4516
  async ({ groupId, versionId, ruleId, isEnabled }) => callApi("PATCH", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}/enabled`, {
@@ -4133,7 +4520,7 @@ function registerRuleTools(server, callApi) {
4133
4520
  }
4134
4521
 
4135
4522
  // src/mcp/tools/facts.ts
4136
- import { z as z5 } from "zod";
4523
+ import { z as z7 } from "zod";
4137
4524
 
4138
4525
  // src/types/facts.ts
4139
4526
  var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
@@ -4147,10 +4534,10 @@ function registerFactTools(server, callApi) {
4147
4534
  title: "List Fact Definitions",
4148
4535
  annotations: { readOnlyHint: true },
4149
4536
  description: "List all fact definitions (input variable schema). Shows key, type, required, and PII status. Always check this before creating rules.",
4150
- inputSchema: z5.object({
4151
- page: z5.number().int().min(0).default(0).describe("Page number"),
4152
- size: z5.number().int().min(1).max(100).default(20).describe("Page size"),
4153
- keyword: z5.string().optional().describe("Search keyword")
4537
+ inputSchema: z7.object({
4538
+ page: z7.number().int().min(0).default(0).describe("Page number"),
4539
+ size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
4540
+ keyword: z7.string().optional().describe("Search keyword")
4154
4541
  })
4155
4542
  },
4156
4543
  async ({ page, size, keyword }) => {
@@ -4164,22 +4551,22 @@ function registerFactTools(server, callApi) {
4164
4551
  {
4165
4552
  title: "Create Fact Definition",
4166
4553
  annotations: { readOnlyHint: false, destructiveHint: false },
4167
- 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.",
4168
- inputSchema: z5.object({
4169
- key: z5.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
4170
- name: z5.string().describe("Display name"),
4171
- type: z5.enum(VALUE_TYPES).describe("Value type"),
4172
- description: z5.string().optional().describe("Description"),
4173
- isRequired: z5.boolean().default(false).describe("Whether this fact is required for rule evaluation"),
4174
- isPii: z5.boolean().default(false).describe(
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(", ")}.`,
4555
+ inputSchema: z7.object({
4556
+ key: z7.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
4557
+ name: z7.string().describe("Display name"),
4558
+ type: z7.enum(VALUE_TYPES).describe("Value type"),
4559
+ description: z7.string().optional().describe("Description"),
4560
+ isRequired: z7.boolean().default(false).describe("Whether this fact is required for rule evaluation"),
4561
+ isPii: z7.boolean().default(false).describe(
4175
4562
  "Mark as PII \u2014 masked on every read surface, revealable only in the console (audited)"
4176
4563
  ),
4177
- valueDomain: z5.object({
4178
- allowedValues: z5.array(z5.union([z5.string(), z5.number()])).optional().describe(
4564
+ valueDomain: z7.object({
4565
+ allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
4179
4566
  "Accepted values. For list types the value domain applies to each element, not to the list as a whole."
4180
4567
  ),
4181
- min: z5.number().optional().describe("Lower bound, inclusive. Numeric types only."),
4182
- max: z5.number().optional().describe("Upper bound, inclusive. Numeric types only.")
4568
+ min: z7.number().optional().describe("Lower bound, inclusive. Numeric types only."),
4569
+ max: z7.number().optional().describe("Upper bound, inclusive. Numeric types only.")
4183
4570
  }).optional().describe(
4184
4571
  "Which values this fact accepts. Omit for no value domain. STRING and LIST_STRING take allowedValues only; NUMBER and LIST_NUMBER take all three; BOOLEAN takes none. Declaring it lets the rule editor offer a dropdown and rejects a rule literal that could never match."
4185
4572
  )
@@ -4193,21 +4580,21 @@ function registerFactTools(server, callApi) {
4193
4580
  title: "Update Fact Definition",
4194
4581
  annotations: { readOnlyHint: false, destructiveHint: true },
4195
4582
  description: "Update a fact definition. The key is immutable. The type can change only while no rule references the fact; if any does, the call fails with FD-007 and reports the count. Rules in deleted groups do not count. Only the fields you send are changed. System facts (isBlocked, blockReason) accept changes to name, description, PII, and the value domain; isRequired is ignored, and their type cannot change (FD-003).",
4196
- inputSchema: z5.object({
4197
- factId: z5.string().uuid().describe("Fact definition ID"),
4198
- name: z5.string().optional().describe("Display name"),
4199
- description: z5.string().optional().describe('Description. Send "" to remove it.'),
4200
- type: z5.enum(VALUE_TYPES).optional().describe(
4583
+ inputSchema: z7.object({
4584
+ factId: z7.string().uuid().describe("Fact definition ID"),
4585
+ name: z7.string().optional().describe("Display name"),
4586
+ description: z7.string().optional().describe('Description. Send "" to remove it.'),
4587
+ type: z7.enum(VALUE_TYPES).optional().describe(
4201
4588
  "Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
4202
4589
  ),
4203
- isRequired: z5.boolean().optional().describe("Required flag"),
4204
- isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
4205
- valueDomain: z5.object({
4206
- allowedValues: z5.array(z5.union([z5.string(), z5.number()])).optional().describe(
4590
+ isRequired: z7.boolean().optional().describe("Required flag"),
4591
+ isPii: z7.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
4592
+ valueDomain: z7.object({
4593
+ allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
4207
4594
  "Accepted values. For list types the value domain applies to each element, not to the list as a whole."
4208
4595
  ),
4209
- min: z5.number().optional().describe("Lower bound, inclusive. Numeric types only."),
4210
- max: z5.number().optional().describe("Upper bound, inclusive. Numeric types only.")
4596
+ min: z7.number().optional().describe("Lower bound, inclusive. Numeric types only."),
4597
+ max: z7.number().optional().describe("Upper bound, inclusive. Numeric types only.")
4211
4598
  }).optional().describe(
4212
4599
  "Which values this fact accepts. Three states: omit the field to leave the value domain alone, send {} to remove it, or send a populated object to replace it. Narrowing it never blocks the update and does not rewrite rules. When rules compare this fact with a value the new value domain excludes, the result ends with a warning that lists them by version: drafts and the versions currently deployed."
4213
4600
  )
@@ -4221,8 +4608,8 @@ function registerFactTools(server, callApi) {
4221
4608
  title: "Delete Fact Definition",
4222
4609
  annotations: { readOnlyHint: false, destructiveHint: true },
4223
4610
  description: "Delete a fact definition. System facts cannot be deleted. Neither can a fact that any rule references: that call fails with FD-006 and reports the count. Rules in deleted groups do not count. Remove the references first.",
4224
- inputSchema: z5.object({
4225
- factId: z5.string().uuid().describe("Fact definition ID")
4611
+ inputSchema: z7.object({
4612
+ factId: z7.string().uuid().describe("Fact definition ID")
4226
4613
  })
4227
4614
  },
4228
4615
  async ({ factId }) => callApi("DELETE", `schema/facts/${factId}`)
@@ -4233,7 +4620,7 @@ function registerFactTools(server, callApi) {
4233
4620
  title: "Get Action Runtime Fact Metadata",
4234
4621
  annotations: { readOnlyHint: true },
4235
4622
  description: "Retrieve runtime fact requirements per Action type. For each Action, shows which input facts must be present in the execution payload \u2014 e.g. MUTATE_FACT always requires its targetVar fact, plus refVar when one is specified. The factRequired flag describes the FACT, not the parameter: refVar is an optional parameter, but if you specify it the named fact must exist. A required fact absent at runtime throws \u2014 the engine never defaults to 0. These facts must be sent in the request (P-015 otherwise), even when an earlier action in the same rule writes them. SET_FACT requires no input fact: it is the only action whose targetVar the request does not have to send, and that targetVar must be a registered fact (ACT-032). Static data, safe to cache in-session.",
4236
- inputSchema: z5.object({})
4623
+ inputSchema: z7.object({})
4237
4624
  },
4238
4625
  async () => callApi("GET", "schema/action-metadata")
4239
4626
  );
@@ -4243,9 +4630,9 @@ function registerFactTools(server, callApi) {
4243
4630
  title: "List Unregistered Facts",
4244
4631
  annotations: { readOnlyHint: true },
4245
4632
  description: "List facts referenced by a version's rules but not yet defined (read-only). Keys that rules only read do not block publish/deploy; a rule whose action targetVar is not a registered fact is rejected at save and publish with ACT-032. Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
4246
- inputSchema: z5.object({
4247
- groupId: z5.string().uuid().describe("Policy group ID"),
4248
- versionId: z5.string().uuid().describe("Version ID")
4633
+ inputSchema: z7.object({
4634
+ groupId: z7.string().uuid().describe("Policy group ID"),
4635
+ versionId: z7.string().uuid().describe("Version ID")
4249
4636
  })
4250
4637
  },
4251
4638
  async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
@@ -4256,9 +4643,9 @@ function registerFactTools(server, callApi) {
4256
4643
  title: "Export Fact Catalog",
4257
4644
  annotations: { readOnlyHint: true },
4258
4645
  description: "Export the fact catalog. The two formats carry different things: CSV is the catalog as it stands, system facts included, for reading in a spreadsheet; JSON matches the shape that batch create accepts, so it can be fed straight back in, which is why it leaves out the fields that endpoint does not take. Returns the file contents as text.",
4259
- inputSchema: z5.object({
4260
- format: z5.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
4261
- keyword: z5.string().optional().describe("Filter by key or name")
4646
+ inputSchema: z7.object({
4647
+ format: z7.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
4648
+ keyword: z7.string().optional().describe("Filter by key or name")
4262
4649
  })
4263
4650
  },
4264
4651
  async ({ format, keyword }) => callApi("GET", "schema/facts/export", {
@@ -4268,8 +4655,8 @@ function registerFactTools(server, callApi) {
4268
4655
  }
4269
4656
 
4270
4657
  // src/mcp/tools/deploy.ts
4271
- import dedent16 from "dedent";
4272
- import { z as z6 } from "zod";
4658
+ import dedent18 from "dedent";
4659
+ import { z as z8 } from "zod";
4273
4660
  function registerDeployTools(server, callApi) {
4274
4661
  server.registerTool(
4275
4662
  "lexq_deploy_publish",
@@ -4277,10 +4664,10 @@ function registerDeployTools(server, callApi) {
4277
4664
  title: "Publish Version",
4278
4665
  annotations: { readOnlyHint: false, destructiveHint: false },
4279
4666
  description: `Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts that rules only read do not block publishing; call lexq_facts_unregistered first to review them. Publishing fails if an action's targetVar is not a registered fact (ACT-032), if the version's compiled rules exceed ${MAX_SNAPSHOT_BYTES / 1024} KB, or if its effective end has already passed (P-058); move the end into the future with lexq_versions_update first.`,
4280
- inputSchema: z6.object({
4281
- groupId: z6.string().uuid().describe("Policy group ID"),
4282
- versionId: z6.string().uuid().describe("Version ID to publish"),
4283
- memo: z6.string().min(1).describe("Publish memo (required)")
4667
+ inputSchema: z8.object({
4668
+ groupId: z8.string().uuid().describe("Policy group ID"),
4669
+ versionId: z8.string().uuid().describe("Version ID to publish"),
4670
+ memo: z8.string().min(1).describe("Publish memo (required)")
4284
4671
  })
4285
4672
  },
4286
4673
  async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/versions/${versionId}/publish`, { body: { memo } })
@@ -4291,10 +4678,10 @@ function registerDeployTools(server, callApi) {
4291
4678
  title: "Deploy to Live",
4292
4679
  annotations: { readOnlyHint: false, destructiveHint: true },
4293
4680
  description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Versions whose effective start date has not arrived are rejected (P-037) \u2014 use lexq_deploy_schedule for those. Versions whose effective end has passed are rejected (P-058). Undefined facts do not block deployment; use lexq_facts_unregistered to review what the version references but has not defined.",
4294
- inputSchema: z6.object({
4295
- groupId: z6.string().uuid().describe("Policy group ID"),
4296
- versionId: z6.string().uuid().describe("Version ID to deploy"),
4297
- memo: z6.string().min(1).describe("Deployment memo (required)")
4681
+ inputSchema: z8.object({
4682
+ groupId: z8.string().uuid().describe("Policy group ID"),
4683
+ versionId: z8.string().uuid().describe("Version ID to deploy"),
4684
+ memo: z8.string().min(1).describe("Deployment memo (required)")
4298
4685
  })
4299
4686
  },
4300
4687
  async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/deploy`, {
@@ -4306,16 +4693,16 @@ function registerDeployTools(server, callApi) {
4306
4693
  {
4307
4694
  title: "Rollback Deployment",
4308
4695
  annotations: { readOnlyHint: false, destructiveHint: true },
4309
- description: dedent16`
4696
+ description: dedent18`
4310
4697
  Roll the group back to the version that was live before the current one. Call lexq_groups_get first: currentVersionName is live now, previousVersionName is what a rollback restores, and a rollback is rejected while canRollback is false. A rollback needs a previous version that is still ACTIVE and whose effective end has not passed (P-058).
4311
4698
 
4312
4699
  Pass the currentVersionId and currentDeploymentId from that call as expectedCurrentVersionId and expectedCurrentDeploymentId. If anyone deployed, rolled back or undeployed in between, the rollback is rejected with 409 and nothing changes: P-062 if another version or no version is live, P-063 if the same version is live but from a newer deployment. Read the group again and check with the user before retrying.
4313
4700
  `,
4314
- inputSchema: z6.object({
4315
- groupId: z6.string().uuid().describe("Policy group ID"),
4316
- expectedCurrentVersionId: z6.string().uuid().describe("currentVersionId from your last read of the group"),
4317
- expectedCurrentDeploymentId: z6.string().uuid().describe("currentDeploymentId from your last read of the group"),
4318
- memo: z6.string().min(1).describe("Rollback reason (required)")
4701
+ inputSchema: z8.object({
4702
+ groupId: z8.string().uuid().describe("Policy group ID"),
4703
+ expectedCurrentVersionId: z8.string().uuid().describe("currentVersionId from your last read of the group"),
4704
+ expectedCurrentDeploymentId: z8.string().uuid().describe("currentDeploymentId from your last read of the group"),
4705
+ memo: z8.string().min(1).describe("Rollback reason (required)")
4319
4706
  })
4320
4707
  },
4321
4708
  async ({ groupId, expectedCurrentVersionId, expectedCurrentDeploymentId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
@@ -4328,372 +4715,133 @@ function registerDeployTools(server, callApi) {
4328
4715
  title: "Undeploy",
4329
4716
  annotations: { readOnlyHint: false, destructiveHint: true },
4330
4717
  description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
4331
- inputSchema: z6.object({
4332
- groupId: z6.string().uuid().describe("Policy group ID"),
4333
- memo: z6.string().min(1).describe("Undeploy reason (required)")
4718
+ inputSchema: z8.object({
4719
+ groupId: z8.string().uuid().describe("Policy group ID"),
4720
+ memo: z8.string().min(1).describe("Undeploy reason (required)")
4334
4721
  })
4335
4722
  },
4336
4723
  async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/undeploy`, {
4337
- body: { memo }
4338
- })
4339
- );
4340
- server.registerTool(
4341
- "lexq_deploy_schedule",
4342
- {
4343
- title: "Schedule Deployment",
4344
- annotations: { readOnlyHint: false, destructiveHint: false },
4345
- 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).",
4346
- inputSchema: z6.object({
4347
- groupId: z6.string().uuid().describe("Policy group ID"),
4348
- versionId: z6.string().uuid().describe("ACTIVE version ID with a future effective start date"),
4349
- memo: z6.string().min(1).describe("Schedule memo (required)")
4350
- })
4351
- },
4352
- async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/schedule`, {
4353
- body: { versionId, memo }
4354
- })
4355
- );
4356
- server.registerTool(
4357
- "lexq_deploy_unschedule",
4358
- {
4359
- title: "Cancel Scheduled Deployment",
4360
- annotations: { readOnlyHint: false, destructiveHint: true },
4361
- description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
4362
- inputSchema: z6.object({
4363
- groupId: z6.string().uuid().describe("Policy group ID")
4364
- })
4365
- },
4366
- async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/schedule`)
4367
- );
4368
- server.registerTool(
4369
- "lexq_deploy_schedules",
4370
- {
4371
- title: "List Scheduled Deployments",
4372
- annotations: { readOnlyHint: true },
4373
- description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
4374
- inputSchema: z6.object({
4375
- page: z6.number().int().min(0).default(0).describe("Page number"),
4376
- size: z6.number().int().min(1).max(100).default(20).describe("Page size")
4377
- })
4378
- },
4379
- async ({ page, size }) => callApi("GET", "policy-groups/schedules", { params: paginationParams(page, size) })
4380
- );
4381
- server.registerTool(
4382
- "lexq_deploy_history",
4383
- {
4384
- title: "Deployment History",
4385
- annotations: { readOnlyHint: true },
4386
- description: "List deployment history across all groups.",
4387
- inputSchema: z6.object({
4388
- page: z6.number().int().min(0).default(0).describe("Page number"),
4389
- size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
4390
- groupId: z6.string().uuid().optional().describe("Filter by group ID"),
4391
- types: z6.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
4392
- startDate: z6.string().optional().describe(START_DAY),
4393
- endDate: z6.string().optional().describe(END_DAY)
4394
- })
4395
- },
4396
- async ({ page, size, groupId, types, startDate, endDate }) => {
4397
- const params = paginationParams(page, size);
4398
- if (groupId) params.groupId = groupId;
4399
- if (types) params.types = types;
4400
- if (startDate) params.startDate = startDate;
4401
- if (endDate) params.endDate = endDate;
4402
- return callApi("GET", "deployments", { params });
4403
- }
4404
- );
4405
- server.registerTool(
4406
- "lexq_deploy_detail",
4407
- {
4408
- title: "Deployment Detail",
4409
- annotations: { readOnlyHint: true },
4410
- description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
4411
- inputSchema: z6.object({
4412
- deploymentId: z6.string().uuid().describe("Deployment ID")
4413
- })
4414
- },
4415
- async ({ deploymentId }) => callApi("GET", `deployments/${deploymentId}`)
4416
- );
4417
- server.registerTool(
4418
- "lexq_deploy_overview",
4419
- {
4420
- title: "Deployment Overview",
4421
- annotations: { readOnlyHint: true },
4422
- description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer. Each group also carries currentDeploymentId, previousVersionName (the version a rollback restores) and canRollback, as in lexq_groups_get.",
4423
- inputSchema: z6.object({})
4424
- },
4425
- async () => callApi("GET", "deployments/overview")
4426
- );
4427
- server.registerTool(
4428
- "lexq_deploy_deployable",
4429
- {
4430
- title: "List Deployable Versions",
4431
- annotations: { readOnlyHint: true },
4432
- description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
4433
- inputSchema: z6.object({
4434
- groupId: z6.string().uuid().describe("Policy group ID")
4435
- })
4436
- },
4437
- async ({ groupId }) => callApi("GET", `deployments/groups/${groupId}/deployable-versions`)
4438
- );
4439
- server.registerTool(
4440
- "lexq_deploy_diff",
4441
- {
4442
- title: "Deployment Diff",
4443
- annotations: { readOnlyHint: true },
4444
- description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
4445
- inputSchema: z6.object({
4446
- baseVersionId: z6.string().uuid().describe("Base version ID (typically the current live)"),
4447
- targetVersionId: z6.string().uuid().describe("Target version ID (the one you want to deploy)")
4448
- })
4449
- },
4450
- async ({ baseVersionId, targetVersionId }) => callApi("GET", "deployments/diff", {
4451
- params: { baseVersionId, targetVersionId }
4452
- })
4453
- );
4454
- }
4455
-
4456
- // src/mcp/tools/analytics.ts
4457
- import { z as z7 } from "zod";
4458
- import dedent17 from "dedent";
4459
- function registerAnalyticsTools(server, callApi) {
4460
- server.registerTool(
4461
- "lexq_dry_run",
4462
- {
4463
- title: "Dry Run",
4464
- annotations: { readOnlyHint: true },
4465
- description: dedent17`
4466
- Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
4467
-
4468
- Returns:
4469
- inputFacts — normalized input facts
4470
- mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates paymentAmount)
4471
- generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {factName}__delta key (signed difference)
4472
- executionTraces — per-rule match status
4473
- decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
4474
-
4475
- Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
4476
- Always dry-run before publishing to validate rule behavior.`,
4477
- inputSchema: z7.object({
4478
- versionId: z7.string().uuid().describe("Policy version ID to test against"),
4479
- facts: z7.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
4480
- includeDebugInfo: z7.boolean().default(true).describe("Include execution and decision traces")
4481
- })
4482
- },
4483
- async ({ versionId, facts, includeDebugInfo }) => {
4484
- const parsedFacts = parseJson(facts);
4485
- return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
4486
- body: { facts: parsedFacts, includeDebugInfo }
4487
- });
4488
- }
4489
- );
4490
- server.registerTool(
4491
- "lexq_dry_run_compare",
4492
- {
4493
- title: "Dry Run Compare",
4494
- annotations: { readOnlyHint: true },
4495
- description: dedent17`
4496
- Compare dry run results between two versions using the same input facts. Useful for validating changes.
4497
-
4498
- Returns:
4499
- resultA / resultB — full DryRunResponse for each version
4500
- diff.mutatedDiff — changes in mutatedFacts between A and B (key → {before, after})
4501
- diff.generatedDiff — changes in generatedVariables between A and B`,
4502
- inputSchema: z7.object({
4503
- versionIdA: z7.string().uuid().describe("Baseline version ID"),
4504
- versionIdB: z7.string().uuid().describe("Candidate version ID"),
4505
- facts: z7.string().describe("JSON string of facts object")
4506
- })
4507
- },
4508
- async ({ versionIdA, versionIdB, facts }) => {
4509
- const parsedFacts = parseJson(facts);
4510
- return callApi("POST", "analytics/dry-run/compare", {
4511
- body: { versionIdA, versionIdB, facts: parsedFacts }
4512
- });
4513
- }
4724
+ body: { memo }
4725
+ })
4514
4726
  );
4515
4727
  server.registerTool(
4516
- "lexq_requirements",
4728
+ "lexq_deploy_schedule",
4517
4729
  {
4518
- title: "Analyze Requirements",
4519
- annotations: { readOnlyHint: true },
4520
- description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
4521
- inputSchema: z7.object({
4522
- groupId: z7.string().uuid().describe("Policy group ID"),
4523
- versionId: z7.string().uuid().describe("Version ID")
4730
+ title: "Schedule Deployment",
4731
+ annotations: { readOnlyHint: false, destructiveHint: false },
4732
+ 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).",
4733
+ inputSchema: z8.object({
4734
+ groupId: z8.string().uuid().describe("Policy group ID"),
4735
+ versionId: z8.string().uuid().describe("ACTIVE version ID with a future effective start date"),
4736
+ memo: z8.string().min(1).describe("Schedule memo (required)")
4524
4737
  })
4525
4738
  },
4526
- async ({ groupId, versionId }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/requirements`)
4739
+ async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/schedule`, {
4740
+ body: { versionId, memo }
4741
+ })
4527
4742
  );
4528
4743
  server.registerTool(
4529
- "lexq_simulation_start",
4744
+ "lexq_deploy_unschedule",
4530
4745
  {
4531
- title: "Start Simulation",
4532
- annotations: { readOnlyHint: false, destructiveHint: false },
4533
- description: dedent17`
4534
- Start an Impact Simulation against historical, uploaded, or inline data.
4535
-
4536
- dataset.type and dataset.source are BOTH required, and must be paired:
4537
- HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
4538
- in the organization time zone, both inclusive)
4539
- UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
4540
- MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
4541
-
4542
- options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
4543
- most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
4544
- options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
4545
- options.includeRuleStats: boolean
4546
- options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
4547
- { "targetVariable": "<fact>", "aggregationType": "COUNT" | "SUM" | "AVG" }
4548
-
4549
- Example (uploaded dataset):
4550
- {
4551
- "policyVersionId": "<uuid>",
4552
- "dataset": { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<path from lexq_dataset_upload>" },
4553
- "options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true, "maxRecords": 10000 }
4554
- }
4555
-
4556
- Example (historical):
4557
- {
4558
- "policyVersionId": "<uuid>",
4559
- "dataset": { "type": "HISTORICAL", "source": "EXECUTION_LOGS", "from": "2026-01-01", "to": "2026-01-31" },
4560
- "options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true }
4561
- }
4562
- `,
4563
- inputSchema: z7.object({
4564
- body: z7.string().describe("JSON string of SimulationRequest")
4746
+ title: "Cancel Scheduled Deployment",
4747
+ annotations: { readOnlyHint: false, destructiveHint: true },
4748
+ description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
4749
+ inputSchema: z8.object({
4750
+ groupId: z8.string().uuid().describe("Policy group ID")
4565
4751
  })
4566
4752
  },
4567
- async ({ body }) => {
4568
- const parsed = parseJson(body);
4569
- return callApi("POST", "analytics/simulations", { body: parsed });
4570
- }
4753
+ async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/schedule`)
4571
4754
  );
4572
4755
  server.registerTool(
4573
- "lexq_simulation_status",
4756
+ "lexq_deploy_schedules",
4574
4757
  {
4575
- title: "Simulation Status",
4758
+ title: "List Scheduled Deployments",
4576
4759
  annotations: { readOnlyHint: true },
4577
- description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
4578
- inputSchema: z7.object({
4579
- simulationId: z7.string().uuid().describe("Simulation ID")
4760
+ description: `List scheduled deployments across all groups (all statuses: ${DeploymentScheduleStatus.join(", ")}), newest first.`,
4761
+ inputSchema: z8.object({
4762
+ page: z8.number().int().min(0).default(0).describe("Page number"),
4763
+ size: z8.number().int().min(1).max(100).default(20).describe("Page size")
4580
4764
  })
4581
4765
  },
4582
- async ({ simulationId }) => callApi("GET", `analytics/simulations/${simulationId}`)
4766
+ async ({ page, size }) => callApi("GET", "policy-groups/schedules", { params: paginationParams(page, size) })
4583
4767
  );
4584
4768
  server.registerTool(
4585
- "lexq_simulation_list",
4769
+ "lexq_deploy_history",
4586
4770
  {
4587
- title: "List Simulations",
4771
+ title: "Deployment History",
4588
4772
  annotations: { readOnlyHint: true },
4589
- description: "List simulation history with optional filters.",
4590
- inputSchema: z7.object({
4591
- page: z7.number().int().min(0).default(0).describe("Page number"),
4592
- size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
4593
- status: z7.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
4594
- from: z7.string().optional().describe(START_DAY),
4595
- to: z7.string().optional().describe(END_DAY)
4773
+ description: "List deployment history across all groups.",
4774
+ inputSchema: z8.object({
4775
+ page: z8.number().int().min(0).default(0).describe("Page number"),
4776
+ size: z8.number().int().min(1).max(100).default(20).describe("Page size"),
4777
+ groupId: z8.string().uuid().optional().describe("Filter by group ID"),
4778
+ types: z8.string().optional().describe(`Filter by deployment types (comma-separated: ${DeploymentType.join(",")})`),
4779
+ startDate: z8.string().optional().describe(START_DAY),
4780
+ endDate: z8.string().optional().describe(END_DAY)
4596
4781
  })
4597
4782
  },
4598
- async ({ page, size, status, from, to }) => {
4783
+ async ({ page, size, groupId, types, startDate, endDate }) => {
4599
4784
  const params = paginationParams(page, size);
4600
- if (status) params.status = status;
4601
- if (from) params.from = from;
4602
- if (to) params.to = to;
4603
- return callApi("GET", "analytics/simulations", { params });
4785
+ if (groupId) params.groupId = groupId;
4786
+ if (types) params.types = types;
4787
+ if (startDate) params.startDate = startDate;
4788
+ if (endDate) params.endDate = endDate;
4789
+ return callApi("GET", "deployments", { params });
4604
4790
  }
4605
4791
  );
4606
4792
  server.registerTool(
4607
- "lexq_simulation_cancel",
4793
+ "lexq_deploy_detail",
4608
4794
  {
4609
- title: "Cancel Simulation",
4610
- annotations: { readOnlyHint: false, destructiveHint: true },
4611
- description: "Cancel a running or pending simulation.",
4612
- inputSchema: z7.object({
4613
- simulationId: z7.string().uuid().describe("Simulation ID")
4795
+ title: "Deployment Detail",
4796
+ annotations: { readOnlyHint: true },
4797
+ description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
4798
+ inputSchema: z8.object({
4799
+ deploymentId: z8.string().uuid().describe("Deployment ID")
4614
4800
  })
4615
4801
  },
4616
- async ({ simulationId }) => callApi("DELETE", `analytics/simulations/${simulationId}`)
4802
+ async ({ deploymentId }) => callApi("GET", `deployments/${deploymentId}`)
4617
4803
  );
4618
4804
  server.registerTool(
4619
- "lexq_simulation_export",
4805
+ "lexq_deploy_overview",
4620
4806
  {
4621
- title: "Export Simulation",
4807
+ title: "Deployment Overview",
4622
4808
  annotations: { readOnlyHint: true },
4623
- description: "Export simulation results as JSON or CSV. Returns the raw data.",
4624
- inputSchema: z7.object({
4625
- simulationId: z7.string().uuid().describe("Simulation ID"),
4626
- format: z7.enum(EXPORT_FORMATS).default("json").describe("Export format")
4627
- })
4809
+ description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer. Each group also carries currentDeploymentId, previousVersionName (the version a rollback restores) and canRollback, as in lexq_groups_get.",
4810
+ inputSchema: z8.object({})
4628
4811
  },
4629
- async ({ simulationId, format }) => callApi("GET", `analytics/simulations/${simulationId}/export`, {
4630
- params: { format }
4631
- })
4812
+ async () => callApi("GET", "deployments/overview")
4632
4813
  );
4633
4814
  server.registerTool(
4634
- "lexq_dataset_upload",
4815
+ "lexq_deploy_deployable",
4635
4816
  {
4636
- title: "Upload Dataset",
4637
- annotations: { readOnlyHint: false, destructiveHint: false },
4638
- description: dedent17`
4639
- Upload inline CSV or JSON content as a simulation dataset.
4640
- The content is uploaded to S3 and a path is returned in the "path" field.
4641
-
4642
- To use the returned path in lexq_simulation_start, set:
4643
- dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
4644
-
4645
- CSV example:
4646
- userId,paymentAmount
4647
- user_001,150000
4648
- user_002,50000
4649
-
4650
- JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
4651
- `,
4652
- inputSchema: z7.object({
4653
- content: z7.string().describe("CSV or JSON content as string"),
4654
- filename: z7.string().default("dataset.csv").describe("Filename with extension (.csv or .json)")
4817
+ title: "List Deployable Versions",
4818
+ annotations: { readOnlyHint: true },
4819
+ description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
4820
+ inputSchema: z8.object({
4821
+ groupId: z8.string().uuid().describe("Policy group ID")
4655
4822
  })
4656
4823
  },
4657
- async ({ content, filename }) => {
4658
- const result = await callApi("POST", "analytics/datasets/upload", {
4659
- upload: { content, filename, fieldName: "file" }
4660
- });
4661
- if (!result.isError) {
4662
- try {
4663
- const uploaded = parseJson(result.content[0]?.text ?? "{}");
4664
- if (uploaded.path) {
4665
- const dataset = { type: "UPLOADED", source: "S3_BUCKET", path: uploaded.path };
4666
- result.content.push({
4667
- type: "text",
4668
- text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
4669
- });
4670
- }
4671
- } catch {
4672
- }
4673
- }
4674
- return result;
4675
- }
4824
+ async ({ groupId }) => callApi("GET", `deployments/groups/${groupId}/deployable-versions`)
4676
4825
  );
4677
4826
  server.registerTool(
4678
- "lexq_dataset_template",
4827
+ "lexq_deploy_diff",
4679
4828
  {
4680
- title: "Download Dataset Template",
4829
+ title: "Deployment Diff",
4681
4830
  annotations: { readOnlyHint: true },
4682
- description: "Generate a sample CSV or JSON template based on the required facts of a version. Use this to understand the expected data format before uploading a dataset.",
4683
- inputSchema: z7.object({
4684
- groupId: z7.string().uuid().describe("Policy group ID"),
4685
- versionId: z7.string().uuid().describe("Version ID"),
4686
- format: z7.enum(EXPORT_FORMATS).default("csv").describe("Template format")
4831
+ description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
4832
+ inputSchema: z8.object({
4833
+ baseVersionId: z8.string().uuid().describe("Base version ID (typically the current live)"),
4834
+ targetVersionId: z8.string().uuid().describe("Target version ID (the one you want to deploy)")
4687
4835
  })
4688
4836
  },
4689
- async ({ groupId, versionId, format }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/dataset-template`, {
4690
- params: { format }
4837
+ async ({ baseVersionId, targetVersionId }) => callApi("GET", "deployments/diff", {
4838
+ params: { baseVersionId, targetVersionId }
4691
4839
  })
4692
4840
  );
4693
4841
  }
4694
4842
 
4695
4843
  // src/mcp/tools/profile.ts
4696
- import { z as z8 } from "zod";
4844
+ import { z as z9 } from "zod";
4697
4845
  var RELATIVE_THRESHOLD = `flagged = p50 \u2265 ${SLOW_MULTIPLIER}\xD7 median of per-rule p50s within the group; absolute thresholds are intentionally not supported.`;
4698
4846
  function profileParams(opts) {
4699
4847
  const params = {};
@@ -4709,15 +4857,15 @@ function registerProfileTools(server, callApi) {
4709
4857
  {
4710
4858
  title: "Group Latency Profile",
4711
4859
  annotations: { readOnlyHint: true },
4712
- 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.`,
4713
- inputSchema: z8.object({
4714
- groupId: z8.string().uuid().describe("Policy group ID"),
4715
- versionId: z8.string().uuid().optional().describe("Version to inspect (default: live version)"),
4716
- from: z8.string().optional().describe(
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.`,
4861
+ inputSchema: z9.object({
4862
+ groupId: z9.string().uuid().describe("Policy group ID"),
4863
+ versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
4864
+ from: z9.string().optional().describe(
4717
4865
  `Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
4718
4866
  ),
4719
- to: z8.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
4720
- cacheState: z8.enum(["HIT", "MISS"]).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
4867
+ to: z9.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
4868
+ cacheState: z9.enum(ProfileCacheState).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
4721
4869
  })
4722
4870
  },
4723
4871
  async ({ groupId, versionId, from, to, cacheState }) => callApi("GET", `policy-groups/${groupId}/profile`, {
@@ -4729,15 +4877,15 @@ function registerProfileTools(server, callApi) {
4729
4877
  {
4730
4878
  title: "Rule Latency Detail",
4731
4879
  annotations: { readOnlyHint: true },
4732
- 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,
4733
- inputSchema: z8.object({
4734
- groupId: z8.string().uuid().describe("Policy group ID"),
4735
- ruleId: z8.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
4736
- versionId: z8.string().uuid().optional().describe("Version to inspect (default: live version)"),
4737
- from: z8.string().optional().describe(
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,
4881
+ inputSchema: z9.object({
4882
+ groupId: z9.string().uuid().describe("Policy group ID"),
4883
+ ruleId: z9.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
4884
+ versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
4885
+ from: z9.string().optional().describe(
4738
4886
  `Window start, ISO-8601 instant. Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
4739
4887
  ),
4740
- to: z8.string().optional().describe("Window end, ISO-8601 instant. Default: now")
4888
+ to: z9.string().optional().describe("Window end, ISO-8601 instant. Default: now")
4741
4889
  })
4742
4890
  },
4743
4891
  async ({ groupId, ruleId, versionId, from, to }) => callApi("GET", `policy-groups/${groupId}/profile/rules/${ruleId}`, {
@@ -4747,7 +4895,7 @@ function registerProfileTools(server, callApi) {
4747
4895
  }
4748
4896
 
4749
4897
  // src/mcp/tools/replay.ts
4750
- import { z as z9 } from "zod";
4898
+ import { z as z10 } from "zod";
4751
4899
  function registerReplayTools(server, callApi) {
4752
4900
  server.registerTool(
4753
4901
  "lexq_replay_decision",
@@ -4755,9 +4903,9 @@ function registerReplayTools(server, callApi) {
4755
4903
  title: "Replay a Decision",
4756
4904
  annotations: { readOnlyHint: true },
4757
4905
  description: "Re-evaluate a past execution (traceId) against a candidate version and return the decision diff (decisionChanged, effect changes, fired rules) plus a determinism verdict. Synchronous and free of charge (TPS throttle only). A replay sends no webhook, notification, or event: rule actions produce no outward effects.",
4758
- inputSchema: z9.object({
4759
- traceId: z9.string().describe("Trace ID of the past execution to replay"),
4760
- candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against")
4906
+ inputSchema: z10.object({
4907
+ traceId: z10.string().describe("Trace ID of the past execution to replay"),
4908
+ candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against")
4761
4909
  })
4762
4910
  },
4763
4911
  async ({ traceId, candidateVersionId }) => callApi("POST", "replay/decisions", { body: { traceId, candidateVersionId } })
@@ -4768,11 +4916,11 @@ function registerReplayTools(server, callApi) {
4768
4916
  title: "Start Window Replay (Blast Radius)",
4769
4917
  annotations: { readOnlyHint: false, destructiveHint: false },
4770
4918
  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.",
4771
- inputSchema: z9.object({
4772
- candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against"),
4773
- from: z9.string().describe(WINDOW_START_DAY),
4774
- to: z9.string().describe(WINDOW_END_DAY),
4775
- maxRecords: z9.number().int().min(1).optional().describe(
4919
+ inputSchema: z10.object({
4920
+ candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against"),
4921
+ from: z10.string().describe(WINDOW_START_DAY),
4922
+ to: z10.string().describe(WINDOW_END_DAY),
4923
+ maxRecords: z10.number().int().min(1).optional().describe(
4776
4924
  `Sample cap (server default applies; hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`
4777
4925
  )
4778
4926
  })
@@ -4785,8 +4933,8 @@ function registerReplayTools(server, callApi) {
4785
4933
  title: "Get Replay Job Status",
4786
4934
  annotations: { readOnlyHint: true },
4787
4935
  description: "Poll a window replay job. RUNNING shows progress 0\u2013100; COMPLETED fills summary and changedSamples; FAILED carries errorMessage. capped=true means the window exceeded the sample cap and only part was replayed.",
4788
- inputSchema: z9.object({
4789
- jobId: z9.string().describe("Replay job ID from lexq_replay_start")
4936
+ inputSchema: z10.object({
4937
+ jobId: z10.string().describe("Replay job ID from lexq_replay_start")
4790
4938
  })
4791
4939
  },
4792
4940
  async ({ jobId }) => callApi("GET", `replay/jobs/${jobId}`)
@@ -4797,9 +4945,9 @@ function registerReplayTools(server, callApi) {
4797
4945
  title: "List Replay Jobs",
4798
4946
  annotations: { readOnlyHint: true },
4799
4947
  description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
4800
- inputSchema: z9.object({
4801
- page: z9.number().int().min(0).default(0).describe("Page number"),
4802
- size: z9.number().int().min(1).max(100).default(20).describe("Page size")
4948
+ inputSchema: z10.object({
4949
+ page: z10.number().int().min(0).default(0).describe("Page number"),
4950
+ size: z10.number().int().min(1).max(100).default(20).describe("Page size")
4803
4951
  })
4804
4952
  },
4805
4953
  async ({ page, size }) => callApi("GET", "replay/jobs", { params: paginationParams(page, size) })
@@ -4810,8 +4958,8 @@ function registerReplayTools(server, callApi) {
4810
4958
  title: "Cancel Replay Job",
4811
4959
  annotations: { readOnlyHint: false, destructiveHint: true },
4812
4960
  description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
4813
- inputSchema: z9.object({
4814
- jobId: z9.string().describe("Replay job ID")
4961
+ inputSchema: z10.object({
4962
+ jobId: z10.string().describe("Replay job ID")
4815
4963
  })
4816
4964
  },
4817
4965
  async ({ jobId }) => callApi("POST", `replay/jobs/${jobId}/cancel`)
@@ -4822,9 +4970,9 @@ function registerReplayTools(server, callApi) {
4822
4970
  title: "Export Replay Result",
4823
4971
  annotations: { readOnlyHint: true },
4824
4972
  description: "Export a COMPLETED window replay job. A running job is rejected \u2014 a partial result reads as the whole thing on the receiving end. The two formats carry different things: CSV holds the effect blast radius, whose columns are fixed; JSON holds the changed samples and action parameters, which nest and whose keys differ per tenant. JSON is not a superset of CSV. Returns the file contents as text.",
4825
- inputSchema: z9.object({
4826
- jobId: z9.string().describe("Replay job ID"),
4827
- format: z9.enum(EXPORT_FORMATS).default("json").describe("Export format")
4973
+ inputSchema: z10.object({
4974
+ jobId: z10.string().describe("Replay job ID"),
4975
+ format: z10.enum(EXPORT_FORMATS).default("json").describe("Export format")
4828
4976
  })
4829
4977
  },
4830
4978
  async ({ jobId, format }) => callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } })
@@ -4832,7 +4980,7 @@ function registerReplayTools(server, callApi) {
4832
4980
  }
4833
4981
 
4834
4982
  // src/mcp/tools/history.ts
4835
- import { z as z10 } from "zod";
4983
+ import { z as z11 } from "zod";
4836
4984
  function registerHistoryTools(server, callApi) {
4837
4985
  server.registerTool(
4838
4986
  "lexq_history_list",
@@ -4840,15 +4988,15 @@ function registerHistoryTools(server, callApi) {
4840
4988
  title: "List Execution History",
4841
4989
  annotations: { readOnlyHint: true },
4842
4990
  description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
4843
- inputSchema: z10.object({
4844
- page: z10.number().int().min(0).default(0).describe("Page number"),
4845
- size: z10.number().int().min(1).max(100).default(20).describe("Page size"),
4846
- traceId: z10.string().optional().describe("Filter by trace ID"),
4847
- groupId: z10.string().uuid().optional().describe("Filter by policy group"),
4848
- versionId: z10.string().uuid().optional().describe("Filter by version"),
4849
- status: z10.enum(["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"]).optional().describe("Filter by execution status"),
4850
- startDate: z10.string().optional().describe(START_DAY),
4851
- endDate: z10.string().optional().describe(END_DAY)
4991
+ inputSchema: z11.object({
4992
+ page: z11.number().int().min(0).default(0).describe("Page number"),
4993
+ size: z11.number().int().min(1).max(100).default(20).describe("Page size"),
4994
+ traceId: z11.string().optional().describe("Filter by trace ID"),
4995
+ groupId: z11.string().uuid().optional().describe("Filter by policy group"),
4996
+ versionId: z11.string().uuid().optional().describe("Filter by version"),
4997
+ status: z11.enum(ApiExecutionStatus).optional().describe("Filter by execution status"),
4998
+ startDate: z11.string().optional().describe(START_DAY),
4999
+ endDate: z11.string().optional().describe(END_DAY)
4852
5000
  })
4853
5001
  },
4854
5002
  async ({ page, size, traceId, groupId, versionId, status, startDate, endDate }) => {
@@ -4868,8 +5016,8 @@ function registerHistoryTools(server, callApi) {
4868
5016
  title: "Get Execution Detail",
4869
5017
  annotations: { readOnlyHint: true },
4870
5018
  description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
4871
- inputSchema: z10.object({
4872
- traceId: z10.string().describe("Trace ID from execution history")
5019
+ inputSchema: z11.object({
5020
+ traceId: z11.string().describe("Trace ID from execution history")
4873
5021
  })
4874
5022
  },
4875
5023
  async ({ traceId }) => callApi("GET", `execution/history/${traceId}`)
@@ -4880,10 +5028,10 @@ function registerHistoryTools(server, callApi) {
4880
5028
  title: "Execution Statistics",
4881
5029
  annotations: { readOnlyHint: true },
4882
5030
  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.`,
4883
- inputSchema: z10.object({
4884
- groupId: z10.string().uuid().optional().describe("Filter by policy group"),
4885
- startDate: z10.string().optional().describe(START_DAY),
4886
- endDate: z10.string().optional().describe(END_DAY)
5031
+ inputSchema: z11.object({
5032
+ groupId: z11.string().uuid().optional().describe("Filter by policy group"),
5033
+ startDate: z11.string().optional().describe(START_DAY),
5034
+ endDate: z11.string().optional().describe(END_DAY)
4887
5035
  })
4888
5036
  },
4889
5037
  async ({ groupId, startDate, endDate }) => {
@@ -4897,7 +5045,7 @@ function registerHistoryTools(server, callApi) {
4897
5045
  }
4898
5046
 
4899
5047
  // src/mcp/tools/provenance.ts
4900
- import { z as z11 } from "zod";
5048
+ import { z as z12 } from "zod";
4901
5049
  function registerProvenanceTools(server, callApi) {
4902
5050
  server.registerTool(
4903
5051
  "lexq_provenance_get",
@@ -4905,8 +5053,8 @@ function registerProvenanceTools(server, callApi) {
4905
5053
  title: "Get Decision Provenance",
4906
5054
  annotations: { readOnlyHint: true },
4907
5055
  description: "Get the lineage of a single decision: what was decided, deterministic why per rule, input facts (PII facts are masked as \u2022\u2022\u2022\u2022\u2022\u2022 with maskedKeys listing them \u2014 values are revealable only in the console, audited), the authored/published/deployed responsibility chain, and the rule snapshot fingerprint.",
4908
- inputSchema: z11.object({
4909
- traceId: z11.string().describe("Trace ID of the execution")
5056
+ inputSchema: z12.object({
5057
+ traceId: z12.string().describe("Trace ID of the execution")
4910
5058
  })
4911
5059
  },
4912
5060
  async ({ traceId }) => callApi("GET", `provenance/${traceId}`)
@@ -4917,14 +5065,14 @@ function registerProvenanceTools(server, callApi) {
4917
5065
  title: "List PII Reveal Audits",
4918
5066
  annotations: { readOnlyHint: true },
4919
5067
  description: "List the PII reveal audit ledger \u2014 who revealed which fact of which trace, and when. Metadata only; revealed values are never stored or returned. Use for monthly access-log inspection and SIEM collection.",
4920
- inputSchema: z11.object({
4921
- page: z11.number().int().min(0).default(0).describe("Page number"),
4922
- size: z11.number().int().min(1).max(100).default(20).describe("Page size"),
4923
- traceId: z11.string().optional().describe("Filter by trace ID (exact match)"),
4924
- revealedBy: z11.string().optional().describe("Filter by operator ID (exact match)"),
4925
- factKey: z11.string().optional().describe("Filter by fact key (partial match, case-insensitive)"),
4926
- startDate: z11.string().optional().describe(START_DAY),
4927
- endDate: z11.string().optional().describe(END_DAY)
5068
+ inputSchema: z12.object({
5069
+ page: z12.number().int().min(0).default(0).describe("Page number"),
5070
+ size: z12.number().int().min(1).max(100).default(20).describe("Page size"),
5071
+ traceId: z12.string().optional().describe("Filter by trace ID (exact match)"),
5072
+ revealedBy: z12.string().optional().describe("Filter by operator ID (exact match)"),
5073
+ factKey: z12.string().optional().describe("Filter by fact key (partial match, case-insensitive)"),
5074
+ startDate: z12.string().optional().describe(START_DAY),
5075
+ endDate: z12.string().optional().describe(END_DAY)
4928
5076
  })
4929
5077
  },
4930
5078
  async ({ page, size, traceId, revealedBy, factKey, startDate, endDate }) => {
@@ -4940,7 +5088,11 @@ function registerProvenanceTools(server, callApi) {
4940
5088
  }
4941
5089
 
4942
5090
  // src/mcp/tools/logs.ts
4943
- import { z as z12 } from "zod";
5091
+ import { z as z13 } from "zod";
5092
+ var ACTION_NOTES = {
5093
+ RESOLVE: "mark as manually fixed",
5094
+ IGNORE: "skip intentionally"
5095
+ };
4944
5096
  function registerLogTools(server, callApi) {
4945
5097
  server.registerTool(
4946
5098
  "lexq_logs_list",
@@ -4948,14 +5100,14 @@ function registerLogTools(server, callApi) {
4948
5100
  title: "List Failure Logs",
4949
5101
  annotations: { readOnlyHint: true },
4950
5102
  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.`,
4951
- inputSchema: z12.object({
4952
- page: z12.number().int().min(0).default(0).describe("Page number"),
4953
- size: z12.number().int().min(1).max(100).default(20).describe("Page size"),
4954
- taskType: z12.enum(TaskType).optional().describe("Task type"),
4955
- status: z12.enum(FailureStatus).optional().describe("Log status"),
4956
- keyword: z12.string().optional().describe("Search in refId, refSubId, errorMessage"),
4957
- startDate: z12.string().optional().describe(START_DAY),
4958
- endDate: z12.string().optional().describe(END_DAY)
5103
+ inputSchema: z13.object({
5104
+ page: z13.number().int().min(0).default(0).describe("Page number"),
5105
+ size: z13.number().int().min(1).max(100).default(20).describe("Page size"),
5106
+ taskType: z13.enum(TaskType).optional().describe("Task type"),
5107
+ status: z13.enum(FailureStatus).optional().describe("Log status"),
5108
+ keyword: z13.string().optional().describe("Search in refId, refSubId, errorMessage"),
5109
+ startDate: z13.string().optional().describe(START_DAY),
5110
+ endDate: z13.string().optional().describe(END_DAY)
4959
5111
  })
4960
5112
  },
4961
5113
  async ({ page, size, taskType, status, keyword, startDate, endDate }) => {
@@ -4974,8 +5126,8 @@ function registerLogTools(server, callApi) {
4974
5126
  title: "Get Failure Log",
4975
5127
  annotations: { readOnlyHint: true },
4976
5128
  description: "Get failure log detail by ID.",
4977
- inputSchema: z12.object({
4978
- logId: z12.string().uuid().describe("Failure log ID")
5129
+ inputSchema: z13.object({
5130
+ logId: z13.string().uuid().describe("Failure log ID")
4979
5131
  })
4980
5132
  },
4981
5133
  async ({ logId }) => callApi("GET", `failure-logs/${logId}`)
@@ -4985,10 +5137,10 @@ function registerLogTools(server, callApi) {
4985
5137
  {
4986
5138
  title: "Process Failure Log",
4987
5139
  annotations: { readOnlyHint: false, destructiveHint: false },
4988
- description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
4989
- inputSchema: z12.object({
4990
- logId: z12.string().uuid().describe("Failure log ID"),
4991
- action: z12.enum(FailureAction).describe("Action to take")
5140
+ description: `Process a single failure log: ${withNotes(ACTION_NOTES).join(" or ")}.`,
5141
+ inputSchema: z13.object({
5142
+ logId: z13.string().uuid().describe("Failure log ID"),
5143
+ action: z13.enum(FailureAction).describe("Action to take")
4992
5144
  })
4993
5145
  },
4994
5146
  async ({ logId, action }) => callApi("POST", `failure-logs/${logId}/actions`, {
@@ -5001,9 +5153,9 @@ function registerLogTools(server, callApi) {
5001
5153
  title: "Bulk Process Failure Logs",
5002
5154
  annotations: { readOnlyHint: false, destructiveHint: false },
5003
5155
  description: "Process multiple failure logs at once. Provide an array of log IDs and the action.",
5004
- inputSchema: z12.object({
5005
- logIds: z12.array(z12.string().uuid()).describe("Array of failure log IDs"),
5006
- action: z12.enum(FailureAction).describe("Action to apply to all logs")
5156
+ inputSchema: z13.object({
5157
+ logIds: z13.array(z13.string().uuid()).describe("Array of failure log IDs"),
5158
+ action: z13.enum(FailureAction).describe("Action to apply to all logs")
5007
5159
  })
5008
5160
  },
5009
5161
  async ({ logIds, action }) => callApi("POST", "failure-logs/bulk-actions", {
@@ -5013,7 +5165,11 @@ function registerLogTools(server, callApi) {
5013
5165
  }
5014
5166
 
5015
5167
  // src/mcp/tools/webhook-subscriptions.ts
5016
- import { z as z13 } from "zod";
5168
+ import { z as z14 } from "zod";
5169
+ var FORMAT_NOTES2 = {
5170
+ GENERIC: "full JSON",
5171
+ SLACK: '{"text": "..."}'
5172
+ };
5017
5173
  function registerWebhookSubscriptionTools(server, callApi) {
5018
5174
  server.registerTool(
5019
5175
  "lexq_webhook_subscriptions_list",
@@ -5021,9 +5177,9 @@ function registerWebhookSubscriptionTools(server, callApi) {
5021
5177
  title: "List Webhook Subscriptions",
5022
5178
  annotations: { readOnlyHint: true },
5023
5179
  description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications: publish, deploy, rollback, and undeploy, plus a deployment being scheduled or a schedule being canceled.",
5024
- inputSchema: z13.object({
5025
- page: z13.number().int().min(0).default(0).describe("Page number"),
5026
- size: z13.number().int().min(1).max(100).default(20).describe("Page size")
5180
+ inputSchema: z14.object({
5181
+ page: z14.number().int().min(0).default(0).describe("Page number"),
5182
+ size: z14.number().int().min(1).max(100).default(20).describe("Page size")
5027
5183
  })
5028
5184
  },
5029
5185
  async ({ page, size }) => {
@@ -5037,8 +5193,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
5037
5193
  title: "Get Webhook Subscription",
5038
5194
  annotations: { readOnlyHint: true },
5039
5195
  description: "Get webhook subscription detail by ID.",
5040
- inputSchema: z13.object({
5041
- id: z13.string().uuid().describe("Webhook subscription ID")
5196
+ inputSchema: z14.object({
5197
+ id: z14.string().uuid().describe("Webhook subscription ID")
5042
5198
  })
5043
5199
  },
5044
5200
  async ({ id }) => callApi("GET", `webhook-subscriptions/${id}`)
@@ -5048,15 +5204,15 @@ function registerWebhookSubscriptionTools(server, callApi) {
5048
5204
  {
5049
5205
  title: "Save Webhook Subscription",
5050
5206
  annotations: { readOnlyHint: false, destructiveHint: true },
5051
- description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).`,
5052
- inputSchema: z13.object({
5053
- id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
5054
- name: z13.string().min(1).describe("Subscription name (unique per tenant)"),
5055
- webhookUrl: z13.string().url().describe("Webhook endpoint URL"),
5056
- subscribedEvents: z13.array(z13.enum(PlatformEventType)).min(1).describe("Events to subscribe to"),
5057
- payloadFormat: z13.enum(WebhookPayloadFormat).optional().default("GENERIC").describe("Payload format"),
5058
- secret: z13.string().optional().describe("HMAC-SHA256 signing secret"),
5059
- isActive: z13.boolean().optional().default(true).describe("Whether the subscription is active")
5207
+ description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: ${withNotes(FORMAT_NOTES2).join(", ")}.`,
5208
+ inputSchema: z14.object({
5209
+ id: z14.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
5210
+ name: z14.string().min(1).describe("Subscription name (unique per tenant)"),
5211
+ webhookUrl: z14.string().url().describe("Webhook endpoint URL"),
5212
+ subscribedEvents: z14.array(z14.enum(PlatformEventType)).min(1).describe("Events to subscribe to"),
5213
+ payloadFormat: z14.enum(WebhookPayloadFormat).optional().default("GENERIC").describe("Payload format"),
5214
+ secret: z14.string().optional().describe("HMAC-SHA256 signing secret"),
5215
+ isActive: z14.boolean().optional().default(true).describe("Whether the subscription is active")
5060
5216
  })
5061
5217
  },
5062
5218
  async ({ ...body }) => callApi("POST", "webhook-subscriptions", { body })
@@ -5067,8 +5223,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
5067
5223
  title: "Delete Webhook Subscription",
5068
5224
  annotations: { readOnlyHint: false, destructiveHint: true },
5069
5225
  description: "Delete a webhook subscription by ID.",
5070
- inputSchema: z13.object({
5071
- id: z13.string().uuid().describe("Webhook subscription ID")
5226
+ inputSchema: z14.object({
5227
+ id: z14.string().uuid().describe("Webhook subscription ID")
5072
5228
  })
5073
5229
  },
5074
5230
  async ({ id }) => callApi("DELETE", `webhook-subscriptions/${id}`)
@@ -5079,8 +5235,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
5079
5235
  title: "Test Webhook Subscription",
5080
5236
  annotations: { readOnlyHint: false, destructiveHint: false },
5081
5237
  description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
5082
- inputSchema: z13.object({
5083
- id: z13.string().uuid().describe("Webhook subscription ID")
5238
+ inputSchema: z14.object({
5239
+ id: z14.string().uuid().describe("Webhook subscription ID")
5084
5240
  })
5085
5241
  },
5086
5242
  async ({ id }) => callApi("POST", `webhook-subscriptions/${id}/test`)
@@ -5088,7 +5244,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
5088
5244
  }
5089
5245
 
5090
5246
  // src/mcp/tools/domain-templates.ts
5091
- import { z as z14 } from "zod";
5247
+ import { z as z15 } from "zod";
5092
5248
  function registerDomainTemplateTools(server, callApi) {
5093
5249
  server.registerTool(
5094
5250
  "lexq_domain_templates_list",
@@ -5096,7 +5252,7 @@ function registerDomainTemplateTools(server, callApi) {
5096
5252
  title: "List Domain Templates",
5097
5253
  annotations: { readOnlyHint: true },
5098
5254
  description: "List all domain templates. A domain template is a starter pack of fact definitions and sample rules for a common decision, such as checkout pricing (ECOMMERCE) or transfer approval (FINTECH). Each entry reports its key, whether it can be applied (isAvailable), and how many facts and sample rules it provisions. Call this before preview or apply to see which templates exist.",
5099
- inputSchema: z14.object({})
5255
+ inputSchema: z15.object({})
5100
5256
  },
5101
5257
  async () => callApi("GET", "domain-templates")
5102
5258
  );
@@ -5106,8 +5262,8 @@ function registerDomainTemplateTools(server, callApi) {
5106
5262
  title: "Preview Domain Template",
5107
5263
  annotations: { readOnlyHint: true },
5108
5264
  description: "Preview exactly what a domain template will provision before applying it: the fact definitions it registers (an input with a value domain carries its allowed values or range), the sample rules it creates with a description of each, and an apply plan. This is a read-only dry run; nothing is created. factConflicts lists keys this tenant already has with a different type, or with different allowed values or a different range on an input fact. While it is not empty, apply fails with P-061 and creates nothing: delete those facts or change them to match the template, then apply. plan is null for a template that cannot be applied yet.",
5109
- inputSchema: z14.object({
5110
- template: z14.string().describe(
5265
+ inputSchema: z15.object({
5266
+ template: z15.string().describe(
5111
5267
  "Domain template key (e.g. ECOMMERCE). Use lexq_domain_templates_list to see the keys."
5112
5268
  )
5113
5269
  })
@@ -5120,9 +5276,9 @@ function registerDomainTemplateTools(server, callApi) {
5120
5276
  title: "Apply Domain Template",
5121
5277
  annotations: { readOnlyHint: false, destructiveHint: false },
5122
5278
  description: "Apply a domain template to the current tenant. Creates the template's fact definitions and a new policy group pre-populated with its sample rules as a DRAFT version. Apply never overwrites existing schema: a key that already exists with the same definition is kept as it is. Run lexq_domain_templates_preview first. If its factConflicts is not empty, apply fails with P-061 and creates nothing until those facts are deleted or changed to match the template. A template whose isAvailable is false cannot be applied.",
5123
- inputSchema: z14.object({
5124
- template: z14.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
5125
- customName: z14.string().optional().describe(
5279
+ inputSchema: z15.object({
5280
+ template: z15.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
5281
+ customName: z15.string().optional().describe(
5126
5282
  "Optional custom name for the policy group that gets created. If omitted, the template's default name is used."
5127
5283
  )
5128
5284
  })
@@ -5158,7 +5314,7 @@ var SERVER_INFO = { name: "lexq", ...server_info_default };
5158
5314
  var __dirname = dirname(fileURLToPath(import.meta.url));
5159
5315
  function getVersion() {
5160
5316
  try {
5161
- const pkg = JSON.parse(readFileSync3(join2(__dirname, "..", "package.json"), "utf-8"));
5317
+ const pkg = JSON.parse(readFileSync3(join3(__dirname, "..", "package.json"), "utf-8"));
5162
5318
  return pkg.version;
5163
5319
  } catch {
5164
5320
  return "0.1.0";
@@ -5171,6 +5327,7 @@ function startMcpServer() {
5171
5327
  () => {
5172
5328
  const server = new McpServer({ ...SERVER_INFO, version });
5173
5329
  registerAllTools(server, callApi);
5330
+ registerLocalTools(server);
5174
5331
  return server;
5175
5332
  },
5176
5333
  {
@@ -5188,7 +5345,7 @@ function startMcpServer() {
5188
5345
  function registerServeCommand(program) {
5189
5346
  program.command("serve").description("Start LexQ as a server for AI agent integrations").option("--mcp", "Start as MCP (Model Context Protocol) server over stdio").addHelpText(
5190
5347
  "after",
5191
- dedent18`
5348
+ dedent19`
5192
5349
 
5193
5350
  Example:
5194
5351
  $ lexq serve --mcp
@@ -5219,11 +5376,11 @@ function registerServeCommand(program) {
5219
5376
 
5220
5377
  // src/commands/domain-templates.ts
5221
5378
  import "commander";
5222
- import dedent19 from "dedent";
5379
+ import dedent20 from "dedent";
5223
5380
  function registerDomainTemplateCommands(program) {
5224
5381
  const templates = program.command("domain-templates").description("Browse and apply domain templates").addHelpText(
5225
5382
  "after",
5226
- dedent19`
5383
+ dedent20`
5227
5384
 
5228
5385
  A domain template is an industry-specific starter pack of fact
5229
5386
  definitions and sample rules. Applying one provisions a ready-to-use
@@ -5271,7 +5428,7 @@ function registerDomainTemplateCommands(program) {
5271
5428
  });
5272
5429
  templates.command("preview").description("Preview what a domain template provisions").requiredOption("--template <key>", "Domain template key (e.g. ECOMMERCE)").addHelpText(
5273
5430
  "after",
5274
- dedent19`
5431
+ dedent20`
5275
5432
 
5276
5433
  Read-only dry run — shows the fact definitions and sample rules the
5277
5434
  template will create. Nothing is provisioned.
@@ -5300,7 +5457,7 @@ function registerDomainTemplateCommands(program) {
5300
5457
  });
5301
5458
  templates.command("apply").description("Apply a domain template to the current tenant").requiredOption("--template <key>", "Domain template key (e.g. ECOMMERCE)").option("--name <n>", "Custom name for the policy group that gets created").option("--force", "Skip confirmation prompt").addHelpText(
5302
5459
  "after",
5303
- dedent19`
5460
+ dedent20`
5304
5461
 
5305
5462
  Creates the template's fact definitions and a new DRAFT policy group
5306
5463
  populated with its sample rules. A fact that already exists with the
@@ -5354,7 +5511,7 @@ function registerDomainTemplateCommands(program) {
5354
5511
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
5355
5512
  function getVersion2() {
5356
5513
  try {
5357
- const pkg = JSON.parse(readFileSync4(join3(__dirname2, "..", "package.json"), "utf-8"));
5514
+ const pkg = JSON.parse(readFileSync4(join4(__dirname2, "..", "package.json"), "utf-8"));
5358
5515
  return pkg.version;
5359
5516
  } catch {
5360
5517
  return "0.0.0";