@lexq/cli 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CONTEXT.md +1 -1
- package/README.md +4 -3
- package/dist/index.js +320 -350
- package/dist/mcp/register.d.ts +7 -1
- package/dist/mcp/register.js +337 -320
- package/package.json +2 -1
- package/skills/lexq-execution/SKILL.md +8 -20
- package/skills/lexq-recipes/SKILL.md +9 -13
- package/skills/lexq-shared/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -387,11 +387,56 @@ var MAX_RULES_PER_VERSION = 100;
|
|
|
387
387
|
var MAX_CONDITIONS_PER_RULE = 25;
|
|
388
388
|
var MAX_CONDITION_DEPTH = 10;
|
|
389
389
|
var MAX_SNAPSHOT_BYTES = 131072;
|
|
390
|
+
var STATS_DEFAULT_WINDOW_DAYS = 30;
|
|
391
|
+
var STATS_MAX_WINDOW_DAYS = 31;
|
|
392
|
+
var FAILURE_LOG_LOOKBACK_DAYS = 7;
|
|
393
|
+
var PURGE_AFTER_DAYS = 30;
|
|
394
|
+
var DESCRIPTION_MAX_LENGTH = 255;
|
|
395
|
+
var COMMIT_MESSAGE_MAX_LENGTH = 500;
|
|
396
|
+
var SCHEDULE_TICK_MAX_MILLIS = 6e4;
|
|
390
397
|
|
|
391
398
|
// src/commands/groups.ts
|
|
392
399
|
import "commander";
|
|
393
400
|
import dedent3 from "dedent";
|
|
394
401
|
|
|
402
|
+
// src/types/enums.ts
|
|
403
|
+
var PolicyGroupStatus = ["ACTIVE", "DISABLED", "ARCHIVED"];
|
|
404
|
+
var LogicalOperator = ["AND", "OR"];
|
|
405
|
+
var MutateOperator = ["ASSIGN", "ADD", "SUB", "MUL", "DIV"];
|
|
406
|
+
var CalculationMethod = ["PERCENTAGE", "AMOUNT"];
|
|
407
|
+
var RoundingPolicy = [
|
|
408
|
+
"UP",
|
|
409
|
+
"DOWN",
|
|
410
|
+
"CEILING",
|
|
411
|
+
"FLOOR",
|
|
412
|
+
"HALF_UP",
|
|
413
|
+
"HALF_DOWN",
|
|
414
|
+
"HALF_EVEN"
|
|
415
|
+
];
|
|
416
|
+
var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
417
|
+
var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
|
|
418
|
+
var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
|
|
419
|
+
var DeploymentType = ["DEPLOY", "ROLLBACK", "UNDEPLOY"];
|
|
420
|
+
var ApiExecutionStatus = ["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"];
|
|
421
|
+
var ProfileCacheState = ["HIT", "MISS"];
|
|
422
|
+
var SimulationStatus = ["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"];
|
|
423
|
+
var SimulationDatasetType = ["HISTORICAL", "UPLOADED", "MANUAL"];
|
|
424
|
+
var SimulationDatasetSource = ["EXECUTION_LOGS", "S3_BUCKET", "REQUEST_BODY"];
|
|
425
|
+
var SimulationMetricType = ["COUNT", "SUM", "AVG"];
|
|
426
|
+
var ExportFormat = ["CSV", "JSON"];
|
|
427
|
+
var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
|
|
428
|
+
var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
|
|
429
|
+
var PlatformEventType = [
|
|
430
|
+
"VERSION_PUBLISHED",
|
|
431
|
+
"DEPLOYED",
|
|
432
|
+
"ROLLED_BACK",
|
|
433
|
+
"UNDEPLOYED",
|
|
434
|
+
"DEPLOY_SCHEDULED",
|
|
435
|
+
"DEPLOY_SCHEDULE_CANCELED"
|
|
436
|
+
];
|
|
437
|
+
var WebhookPayloadFormat = ["GENERIC", "SLACK"];
|
|
438
|
+
var DeploymentScheduleStatus = ["PENDING", "EXECUTED", "CANCELED", "FAILED"];
|
|
439
|
+
|
|
395
440
|
// src/lib/dates.ts
|
|
396
441
|
var ORG_DAY = "yyyy-MM-dd, a whole calendar day in the organization time zone (tenantTimezone in whoami), not UTC";
|
|
397
442
|
var START_DAY = `Start date, ${ORG_DAY}. Inclusive.`;
|
|
@@ -399,9 +444,6 @@ var END_DAY = `End date, ${ORG_DAY}. Inclusive: the whole day counts.`;
|
|
|
399
444
|
var WINDOW_START_DAY = `Window start date, ${ORG_DAY}. Inclusive.`;
|
|
400
445
|
var WINDOW_END_DAY = `Window end date, ${ORG_DAY}. Inclusive: the whole day counts.`;
|
|
401
446
|
var INSTANT = "ISO-8601 instant with Z or an offset, e.g. 2026-10-01T00:00:00Z or 2026-10-01T09:00:00+09:00. A bare date or a time without an offset is rejected.";
|
|
402
|
-
var STATS_DEFAULT_WINDOW_DAYS = 30;
|
|
403
|
-
var STATS_MAX_WINDOW_DAYS = 31;
|
|
404
|
-
var FAILURE_LOG_LOOKBACK_DAYS = 7;
|
|
405
447
|
function utcMinute(instant) {
|
|
406
448
|
return instant ? `${new Date(instant).toISOString().substring(0, 16)}Z` : "\u2013";
|
|
407
449
|
}
|
|
@@ -424,7 +466,7 @@ function registerGroupCommands(program) {
|
|
|
424
466
|
delete Archive a group
|
|
425
467
|
ab-test Manage A/B tests (start, stop, adjust)
|
|
426
468
|
|
|
427
|
-
Statuses:
|
|
469
|
+
Statuses: ${PolicyGroupStatus.join(", ")}
|
|
428
470
|
DISABLED stops production execution (403 P-009) and keeps the deployed version and any A/B test.
|
|
429
471
|
Dry run, simulation, and replay still work. Set ACTIVE to resume.
|
|
430
472
|
ARCHIVED means deleted. Only 'lexq groups delete' sets it, and it cannot be undone.
|
|
@@ -490,9 +532,9 @@ ${data.length} total`);
|
|
|
490
532
|
|
|
491
533
|
Fields:
|
|
492
534
|
name string Group name (required, unique per tenant)
|
|
493
|
-
description string Description (optional, max
|
|
535
|
+
description string Description (optional, max ${DESCRIPTION_MAX_LENGTH} chars)
|
|
494
536
|
activationGroup string Execution Group — conflict-resolution cluster key (optional)
|
|
495
|
-
activationMode string
|
|
537
|
+
activationMode string ${ConflictResolutionMode.join(" | ")} [default: NONE]
|
|
496
538
|
activationStrategy string HIGHEST_PRIORITY [default, and currently the only value]
|
|
497
539
|
executionLimit number Max rules to fire in MAX_N mode (optional)
|
|
498
540
|
status string ACTIVE | DISABLED [default: ACTIVE]
|
|
@@ -554,7 +596,7 @@ ${data.length} total`);
|
|
|
554
596
|
It cannot be restored, and calls that change it (settings, versions, rules, starting an A/B
|
|
555
597
|
test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever
|
|
556
598
|
executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted
|
|
557
|
-
after
|
|
599
|
+
after ${PURGE_AFTER_DAYS} days.
|
|
558
600
|
|
|
559
601
|
Use --force to skip the confirmation prompt.
|
|
560
602
|
`
|
|
@@ -826,7 +868,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
826
868
|
}'
|
|
827
869
|
|
|
828
870
|
Fields:
|
|
829
|
-
commitMessage string Version description (optional, max
|
|
871
|
+
commitMessage string Version description (optional, max ${COMMIT_MESSAGE_MAX_LENGTH} chars)
|
|
830
872
|
effectiveFrom instant Start of effective period (optional, with Z or an offset)
|
|
831
873
|
effectiveTo instant End of effective period (optional, auto-expires)
|
|
832
874
|
`
|
|
@@ -1091,11 +1133,11 @@ ${data.length} total`);
|
|
|
1091
1133
|
refVar is the percentage base and is optional — omit to use targetVar itself.
|
|
1092
1134
|
It is only valid in PERCENTAGE × {ASSIGN, ADD, SUB}.
|
|
1093
1135
|
|
|
1094
|
-
Value Types:
|
|
1136
|
+
Value Types: ${ValueType.join(", ")}
|
|
1095
1137
|
|
|
1096
1138
|
Mutex (optional — rule-level conflict resolution):
|
|
1097
1139
|
mutexGroup string Logical grouping key (e.g., "best-discount")
|
|
1098
|
-
mutexMode string
|
|
1140
|
+
mutexMode string ${ConflictResolutionMode.join(" | ")} [default: NONE]
|
|
1099
1141
|
mutexStrategy string HIGHEST_PRIORITY (currently the only value)
|
|
1100
1142
|
mutexLimit number Max rules to fire in MAX_N mode (required when MAX_N)
|
|
1101
1143
|
|
|
@@ -1304,27 +1346,6 @@ function escapeUnsafe(json) {
|
|
|
1304
1346
|
|
|
1305
1347
|
// src/lib/export.ts
|
|
1306
1348
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
1307
|
-
|
|
1308
|
-
// src/types/enums.ts
|
|
1309
|
-
var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
1310
|
-
var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
|
|
1311
|
-
var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
|
|
1312
|
-
var ProfileCacheState = ["HIT", "MISS"];
|
|
1313
|
-
var ExportFormat = ["CSV", "JSON"];
|
|
1314
|
-
var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
|
|
1315
|
-
var FailureAction = ["IGNORE", "RESOLVE"];
|
|
1316
|
-
var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
|
|
1317
|
-
var PlatformEventType = [
|
|
1318
|
-
"VERSION_PUBLISHED",
|
|
1319
|
-
"DEPLOYED",
|
|
1320
|
-
"ROLLED_BACK",
|
|
1321
|
-
"UNDEPLOYED",
|
|
1322
|
-
"DEPLOY_SCHEDULED",
|
|
1323
|
-
"DEPLOY_SCHEDULE_CANCELED"
|
|
1324
|
-
];
|
|
1325
|
-
var WebhookPayloadFormat = ["GENERIC", "SLACK"];
|
|
1326
|
-
|
|
1327
|
-
// src/lib/export.ts
|
|
1328
1349
|
var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
|
|
1329
1350
|
function parseFormat(value) {
|
|
1330
1351
|
if (typeof value === "string" && EXPORT_FORMATS.includes(value)) {
|
|
@@ -1459,7 +1480,7 @@ ${data.length} unregistered`);
|
|
|
1459
1480
|
process.exit(1);
|
|
1460
1481
|
}
|
|
1461
1482
|
});
|
|
1462
|
-
facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (letters, numbers, underscores)").option("--name <n>", "Display name").option("--type <type>",
|
|
1483
|
+
facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (letters, numbers, underscores)").option("--name <n>", "Display name").option("--type <type>", `Value type: ${ValueType.join(", ")}`).option("--description <desc>", "Description").option("--required", "Mark as required", false).option("--pii", "Mark as PII \u2014 value is masked on every read surface", false).option("--allowed-values <csv>", "Comma-separated list of accepted values").option("--min <number>", "Lower bound, inclusive (numeric types only)").option("--max <number>", "Upper bound, inclusive (numeric types only)").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
|
|
1463
1484
|
"after",
|
|
1464
1485
|
dedent6`
|
|
1465
1486
|
|
|
@@ -1479,7 +1500,7 @@ ${data.length} unregistered`);
|
|
|
1479
1500
|
$ lexq facts create --key tier --name "Tier" --type STRING \
|
|
1480
1501
|
--allowed-values "GOLD, SILVER, BRONZE"
|
|
1481
1502
|
|
|
1482
|
-
Value Types:
|
|
1503
|
+
Value Types: ${ValueType.join(", ")}
|
|
1483
1504
|
Key Format: starts with a letter, then letters, numbers, and underscores.
|
|
1484
1505
|
Casing is yours to choose (e.g., paymentAmount, payment_amount)
|
|
1485
1506
|
|
|
@@ -1508,7 +1529,7 @@ ${data.length} unregistered`);
|
|
|
1508
1529
|
process.exit(1);
|
|
1509
1530
|
}
|
|
1510
1531
|
});
|
|
1511
|
-
facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <n>", "Display name").option("--description <desc>", 'Description (pass "" to remove it)').option("--required", "Mark as required").option("--no-required", "Mark as not required").option("--pii", "Mark as PII (enables masking)").option("--no-pii", "Unmark as PII (disables masking \u2014 value becomes visible)").option("--type <type>",
|
|
1532
|
+
facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <n>", "Display name").option("--description <desc>", 'Description (pass "" to remove it)').option("--required", "Mark as required").option("--no-required", "Mark as not required").option("--pii", "Mark as PII (enables masking)").option("--no-pii", "Unmark as PII (disables masking \u2014 value becomes visible)").option("--type <type>", `Value type: ${ValueType.join(", ")}`).option("--allowed-values <csv>", "Comma-separated list of accepted values").option("--min <number>", "Lower bound, inclusive (numeric types only)").option("--max <number>", "Upper bound, inclusive (numeric types only)").option("--clear-value-domain", "Remove the value domain entirely").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
|
|
1512
1533
|
"after",
|
|
1513
1534
|
dedent6`
|
|
1514
1535
|
|
|
@@ -1892,7 +1913,7 @@ function registerDeployCommands(program) {
|
|
|
1892
1913
|
process.exit(1);
|
|
1893
1914
|
}
|
|
1894
1915
|
});
|
|
1895
|
-
deploy.command("history").description("List deployment history").option("--group-id <groupId>", "Filter by policy group").option("--types <types>",
|
|
1916
|
+
deploy.command("history").description("List deployment history").option("--group-id <groupId>", "Filter by policy group").option("--types <types>", `Filter by types (comma-separated: ${DeploymentType.join(",")})`).option("--start-date <date>", START_DAY).option("--end-date <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
|
|
1896
1917
|
"after",
|
|
1897
1918
|
dedent7`
|
|
1898
1919
|
|
|
@@ -2052,7 +2073,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2052
2073
|
dedent7`
|
|
2053
2074
|
|
|
2054
2075
|
The version must be ACTIVE with a future effective start date; the system
|
|
2055
|
-
deploys it automatically at that time (within one scheduler tick,
|
|
2076
|
+
deploys it automatically at that time (within one scheduler tick, ≤${SCHEDULE_TICK_MAX_MILLIS / 1e3}s).
|
|
2056
2077
|
That time is the version's effectiveFrom, set on the DRAFT before publishing;
|
|
2057
2078
|
this command takes no time of its own.
|
|
2058
2079
|
One pending schedule per group. Manual deploy/rollback/undeploy, starting
|
|
@@ -2377,8 +2398,8 @@ function registerAnalyticsCommands(program) {
|
|
|
2377
2398
|
|
|
2378
2399
|
$ lexq analytics simulation start --file simulation.json
|
|
2379
2400
|
|
|
2380
|
-
Dataset types:
|
|
2381
|
-
Dataset sources:
|
|
2401
|
+
Dataset types: ${SimulationDatasetType.join(", ")}
|
|
2402
|
+
Dataset sources: ${SimulationDatasetSource.join(", ")}
|
|
2382
2403
|
HISTORICAL from / to are yyyy-MM-dd, whole days in the organization time zone, both inclusive.
|
|
2383
2404
|
`
|
|
2384
2405
|
).action(async (opts) => {
|
|
@@ -2499,7 +2520,7 @@ function registerAnalyticsCommands(program) {
|
|
|
2499
2520
|
process.exit(1);
|
|
2500
2521
|
}
|
|
2501
2522
|
});
|
|
2502
|
-
sim.command("list").description("List simulation history").option("--status <status>",
|
|
2523
|
+
sim.command("list").description("List simulation history").option("--status <status>", `Filter by status (${SimulationStatus.join(", ")})`).option("--from <date>", START_DAY).option("--to <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
|
|
2503
2524
|
try {
|
|
2504
2525
|
const globalOpts = program.opts();
|
|
2505
2526
|
const format = globalOpts.format ?? "json";
|
|
@@ -2586,17 +2607,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2586
2607
|
).action(async (opts) => {
|
|
2587
2608
|
try {
|
|
2588
2609
|
const globalOpts = program.opts();
|
|
2589
|
-
await runExport(
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
verbose: globalOpts.verbose,
|
|
2597
|
-
output: opts.output
|
|
2598
|
-
}
|
|
2599
|
-
);
|
|
2610
|
+
await runExport(`analytics/simulations/${opts.id}/export`, parseFormat(opts.as), {
|
|
2611
|
+
apiKey: globalOpts.apiKey,
|
|
2612
|
+
baseUrl: globalOpts.baseUrl,
|
|
2613
|
+
dryRun: globalOpts.dryRun,
|
|
2614
|
+
verbose: globalOpts.verbose,
|
|
2615
|
+
output: opts.output
|
|
2616
|
+
});
|
|
2600
2617
|
} catch (error) {
|
|
2601
2618
|
printError(error);
|
|
2602
2619
|
process.exit(1);
|
|
@@ -2703,6 +2720,15 @@ function resolveBody(opts) {
|
|
|
2703
2720
|
// src/commands/profile.ts
|
|
2704
2721
|
import "commander";
|
|
2705
2722
|
import dedent9 from "dedent";
|
|
2723
|
+
|
|
2724
|
+
// src/lib/percentiles.ts
|
|
2725
|
+
var PERCENTILE_MIN_N = {
|
|
2726
|
+
p50: TAIL_MIN_OBS * 2,
|
|
2727
|
+
p95: TAIL_MIN_OBS * 20,
|
|
2728
|
+
p99: TAIL_MIN_OBS * 100
|
|
2729
|
+
};
|
|
2730
|
+
|
|
2731
|
+
// src/commands/profile.ts
|
|
2706
2732
|
var ms = (nanos) => nanos == null ? "\u2013" : (nanos / 1e6).toFixed(2);
|
|
2707
2733
|
var msWithUnit = (nanos) => nanos == null ? "\u2013" : `${(nanos / 1e6).toFixed(2)}ms`;
|
|
2708
2734
|
function registerProfileCommands(program) {
|
|
@@ -2712,15 +2738,18 @@ function registerProfileCommands(program) {
|
|
|
2712
2738
|
).option("--version-id <versionId>", "Version to inspect (default: live version)").option(
|
|
2713
2739
|
"--from <instant>",
|
|
2714
2740
|
`Window start, ISO-8601 instant (default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago)`
|
|
2715
|
-
).option("--to <instant>", "Window end, ISO-8601 instant (default: now)").option(
|
|
2741
|
+
).option("--to <instant>", "Window end, ISO-8601 instant (default: now)").option(
|
|
2742
|
+
"--cache <state>",
|
|
2743
|
+
`Cache dimension for the rule table: ${ProfileCacheState.join(" | ")} (default: HIT)`
|
|
2744
|
+
).addHelpText(
|
|
2716
2745
|
"after",
|
|
2717
2746
|
dedent9`
|
|
2718
2747
|
|
|
2719
2748
|
Slow-rule judgment is relative only: flagged = p50 ≥ ${SLOW_MULTIPLIER}× the median of
|
|
2720
2749
|
per-rule p50s within the group. Absolute ms thresholds are intentionally
|
|
2721
|
-
not supported. Each percentile is withheld (–) unless n×(1−q) ≥
|
|
2722
|
-
p50 from n ≥
|
|
2723
|
-
on every call, rule detail from a deterministic
|
|
2750
|
+
not supported. Each percentile is withheld (–) unless n×(1−q) ≥ ${TAIL_MIN_OBS} —
|
|
2751
|
+
p50 from n ≥ ${PERCENTILE_MIN_N.p50}, p95 from n ≥ ${PERCENTILE_MIN_N.p95}, p99 from n ≥ ${PERCENTILE_MIN_N.p99}. TOTAL is recorded
|
|
2752
|
+
on every call, rule detail from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample.
|
|
2724
2753
|
|
|
2725
2754
|
Examples:
|
|
2726
2755
|
$ lexq profile <groupId>
|
|
@@ -2818,10 +2847,10 @@ function registerHistoryCommands(program) {
|
|
|
2818
2847
|
get Get full execution detail (request facts, traces, decisions)
|
|
2819
2848
|
stats Aggregate statistics (success rate, latency, counts)
|
|
2820
2849
|
|
|
2821
|
-
Statuses:
|
|
2850
|
+
Statuses: ${ApiExecutionStatus.join(", ")}
|
|
2822
2851
|
`
|
|
2823
2852
|
);
|
|
2824
|
-
history.command("list").description("List execution history").option("--trace-id <traceId>", "Filter by trace ID").option("--group-id <groupId>", "Filter by policy group").option("--version-id <versionId>", "Filter by version").option("--status <status>",
|
|
2853
|
+
history.command("list").description("List execution history").option("--trace-id <traceId>", "Filter by trace ID").option("--group-id <groupId>", "Filter by policy group").option("--version-id <versionId>", "Filter by version").option("--status <status>", `Filter by status (${ApiExecutionStatus.join(", ")})`).option("--start-date <date>", START_DAY).option("--end-date <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
|
|
2825
2854
|
"after",
|
|
2826
2855
|
dedent10`
|
|
2827
2856
|
|
|
@@ -3214,6 +3243,22 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3214
3243
|
// src/commands/logs.ts
|
|
3215
3244
|
import "commander";
|
|
3216
3245
|
import dedent13 from "dedent";
|
|
3246
|
+
|
|
3247
|
+
// src/lib/enum-notes.ts
|
|
3248
|
+
function withNotes(notes) {
|
|
3249
|
+
return Object.entries(notes).map(([value, note]) => note ? `${value} (${note})` : value);
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
// src/lib/console.ts
|
|
3253
|
+
var CONSOLE_URL = "https://console.lexq.io";
|
|
3254
|
+
var FAILURE_LOG_CONSOLE_NOTE = `Logs can be read here but not resolved or ignored. An organization admin does that in the LexQ console: Management \u2192 Failure Logs (${CONSOLE_URL}/management/failure-logs).`;
|
|
3255
|
+
|
|
3256
|
+
// src/commands/logs.ts
|
|
3257
|
+
var STATUS_NOTES = {
|
|
3258
|
+
PENDING: "needs attention",
|
|
3259
|
+
RESOLVED: "",
|
|
3260
|
+
IGNORED: ""
|
|
3261
|
+
};
|
|
3217
3262
|
function registerLogCommands(program) {
|
|
3218
3263
|
const logs = program.command("logs").description("Failure logs").addHelpText(
|
|
3219
3264
|
"after",
|
|
@@ -3224,13 +3269,13 @@ function registerLogCommands(program) {
|
|
|
3224
3269
|
Commands:
|
|
3225
3270
|
list List failure logs with filters
|
|
3226
3271
|
get Get failure log detail (includes the original payload)
|
|
3227
|
-
action Process a single log (IGNORE, RESOLVE)
|
|
3228
|
-
bulk-action Process multiple logs at once
|
|
3229
3272
|
|
|
3230
|
-
|
|
3273
|
+
${FAILURE_LOG_CONSOLE_NOTE}
|
|
3274
|
+
|
|
3275
|
+
Statuses: ${withNotes(STATUS_NOTES).join(", ")}
|
|
3231
3276
|
`
|
|
3232
3277
|
);
|
|
3233
|
-
logs.command("list").description("List failure logs").option("--task-type <taskType>", `Filter by task type (${TaskType.join(", ")})`).option("--status <status>",
|
|
3278
|
+
logs.command("list").description("List failure logs").option("--task-type <taskType>", `Filter by task type (${TaskType.join(", ")})`).option("--status <status>", `Filter by status (${FailureStatus.join(", ")})`).option("--keyword <keyword>", "Search keyword").option("--start-date <date>", START_DAY).option("--end-date <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
|
|
3234
3279
|
"after",
|
|
3235
3280
|
dedent13`
|
|
3236
3281
|
|
|
@@ -3286,7 +3331,8 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3286
3331
|
dedent13`
|
|
3287
3332
|
|
|
3288
3333
|
Includes the full payload that was used for the failed operation.
|
|
3289
|
-
Use this to inspect what went wrong
|
|
3334
|
+
Use this to inspect what went wrong. Resolving or ignoring a log happens in the
|
|
3335
|
+
console; see lexq logs --help.
|
|
3290
3336
|
`
|
|
3291
3337
|
).action(async (opts) => {
|
|
3292
3338
|
try {
|
|
@@ -3303,68 +3349,15 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3303
3349
|
process.exit(1);
|
|
3304
3350
|
}
|
|
3305
3351
|
});
|
|
3306
|
-
logs.command("action").description("Process a failure log action (IGNORE, RESOLVE)").requiredOption("--id <logId>", "Log ID").requiredOption("--action <action>", "Action: IGNORE or RESOLVE").addHelpText(
|
|
3307
|
-
"after",
|
|
3308
|
-
dedent13`
|
|
3309
|
-
|
|
3310
|
-
Actions:
|
|
3311
|
-
IGNORE Mark as intentionally skipped (won't appear in PENDING)
|
|
3312
|
-
RESOLVE Mark as manually resolved (e.g., fixed via external system)
|
|
3313
|
-
|
|
3314
|
-
Example:
|
|
3315
|
-
$ lexq logs action --id <logId> --action RESOLVE
|
|
3316
|
-
`
|
|
3317
|
-
).action(async (opts) => {
|
|
3318
|
-
try {
|
|
3319
|
-
const globalOpts = program.opts();
|
|
3320
|
-
const data = await apiRequest(
|
|
3321
|
-
"POST",
|
|
3322
|
-
`failure-logs/${opts.id}/actions`,
|
|
3323
|
-
{
|
|
3324
|
-
apiKey: globalOpts.apiKey,
|
|
3325
|
-
baseUrl: globalOpts.baseUrl,
|
|
3326
|
-
dryRun: globalOpts.dryRun,
|
|
3327
|
-
verbose: globalOpts.verbose,
|
|
3328
|
-
params: { action: opts.action }
|
|
3329
|
-
}
|
|
3330
|
-
);
|
|
3331
|
-
printJson(data);
|
|
3332
|
-
} catch (error) {
|
|
3333
|
-
printError(error);
|
|
3334
|
-
process.exit(1);
|
|
3335
|
-
}
|
|
3336
|
-
});
|
|
3337
|
-
logs.command("bulk-action").description("Bulk process failure logs").requiredOption("--ids <logIds>", "Comma-separated log IDs").requiredOption("--action <action>", "Action: IGNORE or RESOLVE").addHelpText(
|
|
3338
|
-
"after",
|
|
3339
|
-
dedent13`
|
|
3340
|
-
|
|
3341
|
-
Processes each log individually. Failures are skipped with a warning.
|
|
3342
|
-
|
|
3343
|
-
Example:
|
|
3344
|
-
$ lexq logs bulk-action --ids "id1,id2,id3" --action RESOLVE
|
|
3345
|
-
`
|
|
3346
|
-
).action(async (opts) => {
|
|
3347
|
-
try {
|
|
3348
|
-
const globalOpts = program.opts();
|
|
3349
|
-
const logIds = opts.ids.split(",").map((id) => id.trim());
|
|
3350
|
-
const data = await apiRequest("POST", "failure-logs/bulk-actions", {
|
|
3351
|
-
apiKey: globalOpts.apiKey,
|
|
3352
|
-
baseUrl: globalOpts.baseUrl,
|
|
3353
|
-
dryRun: globalOpts.dryRun,
|
|
3354
|
-
verbose: globalOpts.verbose,
|
|
3355
|
-
body: { logIds, action: opts.action }
|
|
3356
|
-
});
|
|
3357
|
-
printJson(data);
|
|
3358
|
-
} catch (error) {
|
|
3359
|
-
printError(error);
|
|
3360
|
-
process.exit(1);
|
|
3361
|
-
}
|
|
3362
|
-
});
|
|
3363
3352
|
}
|
|
3364
3353
|
|
|
3365
3354
|
// src/commands/webhook-subscriptions.ts
|
|
3366
3355
|
import "commander";
|
|
3367
3356
|
import dedent14 from "dedent";
|
|
3357
|
+
var FORMAT_NOTES = {
|
|
3358
|
+
GENERIC: "full JSON",
|
|
3359
|
+
SLACK: '{"text":"..."}'
|
|
3360
|
+
};
|
|
3368
3361
|
function registerWebhookSubscriptionCommands(program) {
|
|
3369
3362
|
const webhooks = program.command("webhook-subscriptions").description("Manage platform event webhook subscriptions").addHelpText(
|
|
3370
3363
|
"after",
|
|
@@ -3467,7 +3460,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3467
3460
|
name string Subscription name (required, unique per tenant)
|
|
3468
3461
|
webhookUrl string Webhook endpoint URL (required)
|
|
3469
3462
|
subscribedEvents string[] ${PlatformEventType.join(", ")}
|
|
3470
|
-
payloadFormat string
|
|
3463
|
+
payloadFormat string ${withNotes(FORMAT_NOTES).join(" or ")} [default: GENERIC]
|
|
3471
3464
|
secret string HMAC-SHA256 signing secret (optional)
|
|
3472
3465
|
isActive boolean Enable/disable (optional) [default: true]
|
|
3473
3466
|
id string Provide to update, omit to create
|
|
@@ -3577,6 +3570,21 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
|
3577
3570
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
3578
3571
|
|
|
3579
3572
|
// src/mcp/tools/_shared.ts
|
|
3573
|
+
var READ_ONLY = {
|
|
3574
|
+
readOnlyHint: true,
|
|
3575
|
+
destructiveHint: false,
|
|
3576
|
+
openWorldHint: false
|
|
3577
|
+
};
|
|
3578
|
+
var ADDITIVE = {
|
|
3579
|
+
readOnlyHint: false,
|
|
3580
|
+
destructiveHint: false,
|
|
3581
|
+
openWorldHint: false
|
|
3582
|
+
};
|
|
3583
|
+
var DESTRUCTIVE = {
|
|
3584
|
+
readOnlyHint: false,
|
|
3585
|
+
destructiveHint: true,
|
|
3586
|
+
openWorldHint: false
|
|
3587
|
+
};
|
|
3580
3588
|
function localApiOptions() {
|
|
3581
3589
|
const stored = loadConfig();
|
|
3582
3590
|
return {
|
|
@@ -3599,9 +3607,8 @@ function createCallApiFromConfig() {
|
|
|
3599
3607
|
form,
|
|
3600
3608
|
params: opts?.params
|
|
3601
3609
|
});
|
|
3602
|
-
const
|
|
3603
|
-
|
|
3604
|
-
];
|
|
3610
|
+
const text = typeof data === "string" ? data : stringifyJson(data, 2) ?? "";
|
|
3611
|
+
const content = [{ type: "text", text }];
|
|
3605
3612
|
const warning = formatUnregisteredFactWarning(meta);
|
|
3606
3613
|
if (warning) content.push({ type: "text", text: warning });
|
|
3607
3614
|
const domainWarning = formatValueDomainMismatchWarning(meta);
|
|
@@ -3622,6 +3629,20 @@ function paginationParams(page, size) {
|
|
|
3622
3629
|
if (size !== void 0) params.size = String(size);
|
|
3623
3630
|
return params;
|
|
3624
3631
|
}
|
|
3632
|
+
var MAX_EXPORT_CHARS = 1e5;
|
|
3633
|
+
var formatCount = (count) => count.toLocaleString("en-US");
|
|
3634
|
+
var EXPORT_LIMIT_NOTE = `An export longer than ${formatCount(MAX_EXPORT_CHARS)} characters is refused.`;
|
|
3635
|
+
function limitExport(result, command, narrower) {
|
|
3636
|
+
if (result.isError) return result;
|
|
3637
|
+
const length = result.content.reduce((sum, block) => sum + block.text.length, 0);
|
|
3638
|
+
if (length <= MAX_EXPORT_CHARS) return result;
|
|
3639
|
+
const text = [
|
|
3640
|
+
`The export is ${formatCount(length)} characters long, and this tool returns at most ${formatCount(MAX_EXPORT_CHARS)}.`,
|
|
3641
|
+
narrower,
|
|
3642
|
+
`To get the whole file, download it from the LexQ console (${CONSOLE_URL}) or with \`${command}\`.`
|
|
3643
|
+
].filter(Boolean).join(" ");
|
|
3644
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
3645
|
+
}
|
|
3625
3646
|
function formatUnregisteredFactWarning(meta) {
|
|
3626
3647
|
const facts = meta?.unregisteredFacts ?? [];
|
|
3627
3648
|
if (facts.length === 0) return null;
|
|
@@ -3633,9 +3654,9 @@ function formatUnregisteredFactWarning(meta) {
|
|
|
3633
3654
|
return ` \u2022 ${f.key} (ambiguous) \u2014 pick a type (${choices}), then lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: <chosen> })`;
|
|
3634
3655
|
});
|
|
3635
3656
|
return [
|
|
3636
|
-
`\u26A0 The saved rule references ${facts.length} fact(s) not defined in the schema. The rule was saved
|
|
3657
|
+
`\u26A0 The saved rule references ${facts.length} fact(s) not defined in the schema. The rule was saved, but the engine does not check request values for these keys against a type or value domain, and lexq_requirements lists them with type UNKNOWN.`,
|
|
3637
3658
|
...lines,
|
|
3638
|
-
`Register them
|
|
3659
|
+
`Register them before the version is published to have those values checked, or call lexq_facts_unregistered to review the version's full list.`
|
|
3639
3660
|
].join("\n");
|
|
3640
3661
|
}
|
|
3641
3662
|
|
|
@@ -3661,7 +3682,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3661
3682
|
"lexq_dry_run",
|
|
3662
3683
|
{
|
|
3663
3684
|
title: "Dry Run",
|
|
3664
|
-
annotations:
|
|
3685
|
+
annotations: READ_ONLY,
|
|
3665
3686
|
description: dedent15`
|
|
3666
3687
|
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
|
|
3667
3688
|
|
|
@@ -3672,8 +3693,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3672
3693
|
executionTraces — per-rule match status
|
|
3673
3694
|
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
3674
3695
|
|
|
3675
|
-
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
3676
|
-
Always dry-run before publishing to validate rule behavior.`,
|
|
3696
|
+
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }`,
|
|
3677
3697
|
inputSchema: z.object({
|
|
3678
3698
|
versionId: z.string().uuid().describe("Policy version ID to test against"),
|
|
3679
3699
|
facts: z.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
|
|
@@ -3691,7 +3711,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3691
3711
|
"lexq_dry_run_compare",
|
|
3692
3712
|
{
|
|
3693
3713
|
title: "Dry Run Compare",
|
|
3694
|
-
annotations:
|
|
3714
|
+
annotations: READ_ONLY,
|
|
3695
3715
|
description: dedent15`
|
|
3696
3716
|
Compare dry run results between two versions using the same input facts. Useful for validating changes.
|
|
3697
3717
|
|
|
@@ -3716,7 +3736,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3716
3736
|
"lexq_requirements",
|
|
3717
3737
|
{
|
|
3718
3738
|
title: "Analyze Requirements",
|
|
3719
|
-
annotations:
|
|
3739
|
+
annotations: READ_ONLY,
|
|
3720
3740
|
description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
|
|
3721
3741
|
inputSchema: z.object({
|
|
3722
3742
|
groupId: z.string().uuid().describe("Policy group ID"),
|
|
@@ -3729,10 +3749,13 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3729
3749
|
"lexq_simulation_start",
|
|
3730
3750
|
{
|
|
3731
3751
|
title: "Start Simulation",
|
|
3732
|
-
annotations:
|
|
3752
|
+
annotations: DESTRUCTIVE,
|
|
3733
3753
|
description: dedent15`
|
|
3734
3754
|
Start an Impact Simulation against historical, uploaded, or inline data.
|
|
3735
3755
|
|
|
3756
|
+
Starting checks the plan's simulation usage limit. The records a simulation processes
|
|
3757
|
+
count toward that usage when it completes or is canceled.
|
|
3758
|
+
|
|
3736
3759
|
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
3737
3760
|
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
3738
3761
|
in the organization time zone, both inclusive)
|
|
@@ -3744,7 +3767,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3744
3767
|
options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
|
|
3745
3768
|
options.includeRuleStats: boolean
|
|
3746
3769
|
options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
|
|
3747
|
-
{ "targetVariable": "<fact>", "aggregationType":
|
|
3770
|
+
{ "targetVariable": "<fact>", "aggregationType": ${SimulationMetricType.map((t) => `"${t}"`).join(" | ")} }
|
|
3748
3771
|
|
|
3749
3772
|
Example (uploaded dataset):
|
|
3750
3773
|
{
|
|
@@ -3773,7 +3796,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3773
3796
|
"lexq_simulation_status",
|
|
3774
3797
|
{
|
|
3775
3798
|
title: "Simulation Status",
|
|
3776
|
-
annotations:
|
|
3799
|
+
annotations: READ_ONLY,
|
|
3777
3800
|
description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
|
|
3778
3801
|
inputSchema: z.object({
|
|
3779
3802
|
simulationId: z.string().uuid().describe("Simulation ID")
|
|
@@ -3785,12 +3808,12 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3785
3808
|
"lexq_simulation_list",
|
|
3786
3809
|
{
|
|
3787
3810
|
title: "List Simulations",
|
|
3788
|
-
annotations:
|
|
3811
|
+
annotations: READ_ONLY,
|
|
3789
3812
|
description: "List simulation history with optional filters.",
|
|
3790
3813
|
inputSchema: z.object({
|
|
3791
3814
|
page: z.number().int().min(0).default(0).describe("Page number"),
|
|
3792
3815
|
size: z.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
3793
|
-
status: z.enum(
|
|
3816
|
+
status: z.enum(SimulationStatus).optional().describe("Filter by status"),
|
|
3794
3817
|
from: z.string().optional().describe(START_DAY),
|
|
3795
3818
|
to: z.string().optional().describe(END_DAY)
|
|
3796
3819
|
})
|
|
@@ -3807,7 +3830,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3807
3830
|
"lexq_simulation_cancel",
|
|
3808
3831
|
{
|
|
3809
3832
|
title: "Cancel Simulation",
|
|
3810
|
-
annotations:
|
|
3833
|
+
annotations: DESTRUCTIVE,
|
|
3811
3834
|
description: "Cancel a running or pending simulation.",
|
|
3812
3835
|
inputSchema: z.object({
|
|
3813
3836
|
simulationId: z.string().uuid().describe("Simulation ID")
|
|
@@ -3819,22 +3842,25 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3819
3842
|
"lexq_simulation_export",
|
|
3820
3843
|
{
|
|
3821
3844
|
title: "Export Simulation",
|
|
3822
|
-
annotations:
|
|
3823
|
-
description:
|
|
3845
|
+
annotations: READ_ONLY,
|
|
3846
|
+
description: `Export simulation results as JSON or CSV. Returns the raw data. ${EXPORT_LIMIT_NOTE}`,
|
|
3824
3847
|
inputSchema: z.object({
|
|
3825
3848
|
simulationId: z.string().uuid().describe("Simulation ID"),
|
|
3826
3849
|
format: z.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
3827
3850
|
})
|
|
3828
3851
|
},
|
|
3829
|
-
async ({ simulationId, format }) =>
|
|
3830
|
-
|
|
3831
|
-
|
|
3852
|
+
async ({ simulationId, format }) => limitExport(
|
|
3853
|
+
await callApi("GET", `analytics/simulations/${simulationId}/export`, {
|
|
3854
|
+
params: { format }
|
|
3855
|
+
}),
|
|
3856
|
+
`lexq analytics simulation export --id ${simulationId} --as ${format} --output <file>`
|
|
3857
|
+
)
|
|
3832
3858
|
);
|
|
3833
3859
|
server.registerTool(
|
|
3834
3860
|
"lexq_dataset_upload",
|
|
3835
3861
|
{
|
|
3836
3862
|
title: "Upload Dataset",
|
|
3837
|
-
annotations:
|
|
3863
|
+
annotations: ADDITIVE,
|
|
3838
3864
|
description: dedent15`
|
|
3839
3865
|
Upload inline CSV or JSON content as a simulation dataset.
|
|
3840
3866
|
The content is uploaded to S3 and a path is returned in the "path" field.
|
|
@@ -3877,7 +3903,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3877
3903
|
"lexq_dataset_template",
|
|
3878
3904
|
{
|
|
3879
3905
|
title: "Download Dataset Template",
|
|
3880
|
-
annotations:
|
|
3906
|
+
annotations: READ_ONLY,
|
|
3881
3907
|
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.",
|
|
3882
3908
|
inputSchema: z.object({
|
|
3883
3909
|
groupId: z.string().uuid().describe("Policy group ID"),
|
|
@@ -3934,7 +3960,7 @@ function registerLocalTools(server) {
|
|
|
3934
3960
|
"lexq_dataset_upload_file",
|
|
3935
3961
|
{
|
|
3936
3962
|
title: "Upload Dataset File",
|
|
3937
|
-
annotations:
|
|
3963
|
+
annotations: ADDITIVE,
|
|
3938
3964
|
description: dedent16`
|
|
3939
3965
|
Upload a CSV or JSON file from this computer as a simulation dataset, up to ${MAX_DATASET_MB} MB.
|
|
3940
3966
|
The server reads the file from disk, so its content never passes through the conversation.
|
|
@@ -3993,7 +4019,7 @@ function registerStatusTools(server, callApi) {
|
|
|
3993
4019
|
"lexq_whoami",
|
|
3994
4020
|
{
|
|
3995
4021
|
title: "Who Am I",
|
|
3996
|
-
annotations:
|
|
4022
|
+
annotations: READ_ONLY,
|
|
3997
4023
|
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.',
|
|
3998
4024
|
inputSchema: z3.object({})
|
|
3999
4025
|
},
|
|
@@ -4008,7 +4034,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4008
4034
|
"lexq_groups_list",
|
|
4009
4035
|
{
|
|
4010
4036
|
title: "List Policy Groups",
|
|
4011
|
-
annotations:
|
|
4037
|
+
annotations: READ_ONLY,
|
|
4012
4038
|
description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
|
|
4013
4039
|
inputSchema: z4.object({})
|
|
4014
4040
|
},
|
|
@@ -4018,7 +4044,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4018
4044
|
"lexq_groups_get",
|
|
4019
4045
|
{
|
|
4020
4046
|
title: "Get Policy Group",
|
|
4021
|
-
annotations:
|
|
4047
|
+
annotations: READ_ONLY,
|
|
4022
4048
|
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.",
|
|
4023
4049
|
inputSchema: z4.object({
|
|
4024
4050
|
groupId: z4.string().uuid().describe("Policy group ID")
|
|
@@ -4030,7 +4056,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4030
4056
|
"lexq_groups_create",
|
|
4031
4057
|
{
|
|
4032
4058
|
title: "Create Policy Group",
|
|
4033
|
-
annotations:
|
|
4059
|
+
annotations: ADDITIVE,
|
|
4034
4060
|
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.`,
|
|
4035
4061
|
inputSchema: z4.object({
|
|
4036
4062
|
name: z4.string().describe("Group name (unique among non-ARCHIVED)"),
|
|
@@ -4061,13 +4087,13 @@ function registerGroupTools(server, callApi) {
|
|
|
4061
4087
|
"lexq_groups_update",
|
|
4062
4088
|
{
|
|
4063
4089
|
title: "Update Policy Group",
|
|
4064
|
-
annotations:
|
|
4090
|
+
annotations: DESTRUCTIVE,
|
|
4065
4091
|
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
|
|
4066
4092
|
inputSchema: z4.object({
|
|
4067
4093
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
4068
4094
|
name: z4.string().optional().describe("New name"),
|
|
4069
4095
|
description: z4.string().optional().describe("New description"),
|
|
4070
|
-
status: z4.enum(["ACTIVE", "DISABLED"]).optional().describe(
|
|
4096
|
+
status: z4.enum(PolicyGroupStatus).extract(["ACTIVE", "DISABLED"]).optional().describe(
|
|
4071
4097
|
"DISABLED stops production execution of this group: execution calls return 403 (P-009), and a composite call that includes the group is rejected in full. The deployed version and any A/B test are kept, and dry run, Impact Simulation, and Decision Replay keep working, so a fix can be verified before setting ACTIVE again. To go back to the previous version, use lexq_deploy_rollback. To take the version out of production, use lexq_deploy_undeploy."
|
|
4072
4098
|
),
|
|
4073
4099
|
activationGroup: z4.string().optional().describe(
|
|
@@ -4086,8 +4112,8 @@ function registerGroupTools(server, callApi) {
|
|
|
4086
4112
|
"lexq_groups_delete",
|
|
4087
4113
|
{
|
|
4088
4114
|
title: "Delete Policy Group",
|
|
4089
|
-
annotations:
|
|
4090
|
-
description:
|
|
4115
|
+
annotations: DESTRUCTIVE,
|
|
4116
|
+
description: `Delete (archive) a policy group. A deployed group cannot be deleted (P-020); undeploy it first with lexq_deploy_undeploy, which also ends a running A/B test. This cannot be undone. The group no longer appears in lexq_groups_list and its name can be reused right away. Calls that change it (settings, versions, rules, starting an A/B test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted after ${PURGE_AFTER_DAYS} days.`,
|
|
4091
4117
|
inputSchema: z4.object({
|
|
4092
4118
|
groupId: z4.string().uuid().describe("Policy group ID")
|
|
4093
4119
|
})
|
|
@@ -4098,7 +4124,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4098
4124
|
"lexq_groups_reorder",
|
|
4099
4125
|
{
|
|
4100
4126
|
title: "Reorder Policy Groups",
|
|
4101
|
-
annotations:
|
|
4127
|
+
annotations: DESTRUCTIVE,
|
|
4102
4128
|
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.",
|
|
4103
4129
|
inputSchema: z4.object({
|
|
4104
4130
|
groupIds: z4.array(z4.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
|
|
@@ -4116,12 +4142,14 @@ function registerGroupTools(server, callApi) {
|
|
|
4116
4142
|
"lexq_ab_test_start",
|
|
4117
4143
|
{
|
|
4118
4144
|
title: "Start A/B Test",
|
|
4119
|
-
annotations:
|
|
4120
|
-
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%.",
|
|
4145
|
+
annotations: DESTRUCTIVE,
|
|
4146
|
+
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%. Starting cancels the group's pending scheduled deployment. Starting while a test is already running replaces its challenger and traffic rate.",
|
|
4121
4147
|
inputSchema: z4.object({
|
|
4122
4148
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
4123
4149
|
testVersionId: z4.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
|
|
4124
|
-
trafficRate: z4.number().int().min(
|
|
4150
|
+
trafficRate: z4.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
|
|
4151
|
+
`Traffic percentage routed to challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
|
|
4152
|
+
)
|
|
4125
4153
|
})
|
|
4126
4154
|
},
|
|
4127
4155
|
async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/ab-test`, { body })
|
|
@@ -4130,7 +4158,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4130
4158
|
"lexq_ab_test_stop",
|
|
4131
4159
|
{
|
|
4132
4160
|
title: "Stop A/B Test",
|
|
4133
|
-
annotations:
|
|
4161
|
+
annotations: DESTRUCTIVE,
|
|
4134
4162
|
description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
|
|
4135
4163
|
inputSchema: z4.object({
|
|
4136
4164
|
groupId: z4.string().uuid().describe("Policy group ID")
|
|
@@ -4142,11 +4170,13 @@ function registerGroupTools(server, callApi) {
|
|
|
4142
4170
|
"lexq_ab_test_adjust",
|
|
4143
4171
|
{
|
|
4144
4172
|
title: "Adjust A/B Test",
|
|
4145
|
-
annotations:
|
|
4173
|
+
annotations: DESTRUCTIVE,
|
|
4146
4174
|
description: "Adjust traffic rate of a running A/B test.",
|
|
4147
4175
|
inputSchema: z4.object({
|
|
4148
4176
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
4149
|
-
trafficRate: z4.number().int().min(
|
|
4177
|
+
trafficRate: z4.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
|
|
4178
|
+
`New traffic percentage for challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
|
|
4179
|
+
)
|
|
4150
4180
|
})
|
|
4151
4181
|
},
|
|
4152
4182
|
async ({ groupId, ...body }) => callApi("PATCH", `policy-groups/${groupId}/ab-test/traffic-rate`, { body })
|
|
@@ -4160,7 +4190,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4160
4190
|
"lexq_versions_list",
|
|
4161
4191
|
{
|
|
4162
4192
|
title: "List Policy Versions",
|
|
4163
|
-
annotations:
|
|
4193
|
+
annotations: READ_ONLY,
|
|
4164
4194
|
description: "List all versions of a policy group.",
|
|
4165
4195
|
inputSchema: z5.object({
|
|
4166
4196
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4176,7 +4206,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4176
4206
|
"lexq_versions_get",
|
|
4177
4207
|
{
|
|
4178
4208
|
title: "Get Policy Version",
|
|
4179
|
-
annotations:
|
|
4209
|
+
annotations: READ_ONLY,
|
|
4180
4210
|
description: "Get a single version by ID, including its rules and fact requirements.",
|
|
4181
4211
|
inputSchema: z5.object({
|
|
4182
4212
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4189,7 +4219,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4189
4219
|
"lexq_versions_create",
|
|
4190
4220
|
{
|
|
4191
4221
|
title: "Create Policy Version",
|
|
4192
|
-
annotations:
|
|
4222
|
+
annotations: ADDITIVE,
|
|
4193
4223
|
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.`,
|
|
4194
4224
|
inputSchema: z5.object({
|
|
4195
4225
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4204,7 +4234,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4204
4234
|
"lexq_versions_update",
|
|
4205
4235
|
{
|
|
4206
4236
|
title: "Update Policy Version",
|
|
4207
|
-
annotations:
|
|
4237
|
+
annotations: DESTRUCTIVE,
|
|
4208
4238
|
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.",
|
|
4209
4239
|
inputSchema: z5.object({
|
|
4210
4240
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4212,7 +4242,9 @@ function registerVersionTools(server, callApi) {
|
|
|
4212
4242
|
commitMessage: z5.string().optional().describe("New commit message"),
|
|
4213
4243
|
effectiveFrom: z5.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
|
|
4214
4244
|
effectiveTo: z5.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
|
|
4215
|
-
clearEffectiveFrom: z5.boolean().optional().describe(
|
|
4245
|
+
clearEffectiveFrom: z5.boolean().optional().describe(
|
|
4246
|
+
"Set to true to remove the effective start. Do not send effectiveFrom with it."
|
|
4247
|
+
),
|
|
4216
4248
|
clearEffectiveTo: z5.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
|
|
4217
4249
|
})
|
|
4218
4250
|
},
|
|
@@ -4222,7 +4254,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4222
4254
|
"lexq_versions_delete",
|
|
4223
4255
|
{
|
|
4224
4256
|
title: "Delete Policy Version",
|
|
4225
|
-
annotations:
|
|
4257
|
+
annotations: DESTRUCTIVE,
|
|
4226
4258
|
description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
|
|
4227
4259
|
inputSchema: z5.object({
|
|
4228
4260
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4235,7 +4267,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4235
4267
|
"lexq_versions_clone",
|
|
4236
4268
|
{
|
|
4237
4269
|
title: "Clone Policy Version",
|
|
4238
|
-
annotations:
|
|
4270
|
+
annotations: ADDITIVE,
|
|
4239
4271
|
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.`,
|
|
4240
4272
|
inputSchema: z5.object({
|
|
4241
4273
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4253,12 +4285,13 @@ function registerVersionTools(server, callApi) {
|
|
|
4253
4285
|
// src/mcp/tools/rules.ts
|
|
4254
4286
|
import { z as z6 } from "zod";
|
|
4255
4287
|
import dedent17 from "dedent";
|
|
4288
|
+
var oneOf = (values) => values.map((v) => `"${v}"`).join("|");
|
|
4256
4289
|
function registerRuleTools(server, callApi) {
|
|
4257
4290
|
server.registerTool(
|
|
4258
4291
|
"lexq_rules_list",
|
|
4259
4292
|
{
|
|
4260
4293
|
title: "List Rules",
|
|
4261
|
-
annotations:
|
|
4294
|
+
annotations: READ_ONLY,
|
|
4262
4295
|
description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
|
|
4263
4296
|
inputSchema: z6.object({
|
|
4264
4297
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4271,7 +4304,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4271
4304
|
"lexq_rules_get",
|
|
4272
4305
|
{
|
|
4273
4306
|
title: "Get Rule Detail",
|
|
4274
|
-
annotations:
|
|
4307
|
+
annotations: READ_ONLY,
|
|
4275
4308
|
description: "Get full rule detail including condition tree and action definitions.",
|
|
4276
4309
|
inputSchema: z6.object({
|
|
4277
4310
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4285,87 +4318,39 @@ function registerRuleTools(server, callApi) {
|
|
|
4285
4318
|
"lexq_rules_create",
|
|
4286
4319
|
{
|
|
4287
4320
|
title: "Create Rule",
|
|
4288
|
-
annotations:
|
|
4321
|
+
annotations: ADDITIVE,
|
|
4289
4322
|
description: dedent17`
|
|
4290
|
-
Create a rule in a DRAFT version.
|
|
4291
|
-
|
|
4292
|
-
Before creating rules with new fact keys, call lexq_facts_list to check existing facts.
|
|
4293
|
-
If a required key is missing, ask the user to confirm the type, isRequired, and description
|
|
4294
|
-
before calling lexq_facts_create — registering facts enables type validation, Console UI
|
|
4295
|
-
autocomplete, and the dry-run requirements analyzer.
|
|
4296
|
-
|
|
4297
|
-
An action's targetVar (SET_FACT, MUTATE_FACT) must be a registered fact. Saving or
|
|
4298
|
-
publishing a rule whose targetVar is not registered fails with ACT-032, so register it
|
|
4299
|
-
with lexq_facts_create first. Keys a rule only reads (condition fields, refVar) may stay
|
|
4300
|
-
unregistered.
|
|
4301
|
-
|
|
4302
|
-
After saving, lexq_facts_unregistered lists any keys this version references but has not
|
|
4303
|
-
defined (non-blocking, version-wide) — use it to decide what to register.
|
|
4304
|
-
|
|
4305
|
-
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
|
|
4306
|
-
Value types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
|
|
4307
|
-
Limits: a condition tree may hold at most ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes; GROUP nodes do not count)
|
|
4308
|
-
and nest at most ${MAX_CONDITION_DEPTH} levels below the root. A version holds at most ${MAX_RULES_PER_VERSION} rules.
|
|
4309
|
-
|
|
4310
|
-
Operators are constrained by the LEFT fact's type (from lexq_facts_list). Using one outside
|
|
4311
|
-
its type is rejected by the server — check the fact type before choosing an operator.
|
|
4312
|
-
- STRING fact: EQUALS, NOT_EQUALS, CONTAINS, IN, NOT_IN
|
|
4313
|
-
- NUMBER fact: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN
|
|
4314
|
-
- BOOLEAN fact: EQUALS, NOT_EQUALS
|
|
4315
|
-
- LIST_* fact: HAS_ANY, HAS_ALL, HAS_NONE (only these)
|
|
4316
|
-
|
|
4317
|
-
HAS_* query list-typed facts. Value is always an array whose element type matches the fact:
|
|
4318
|
-
- HAS_ANY: fact has at least one of the given values
|
|
4319
|
-
- HAS_ALL: fact has all of the given values
|
|
4320
|
-
- HAS_NONE: fact has none of the given values
|
|
4321
|
-
Example: { "type": "SINGLE", "field": "userTags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
|
|
4323
|
+
Create a rule in a DRAFT version. It goes last in priority order; lexq_rules_reorder changes the order.
|
|
4322
4324
|
|
|
4323
|
-
|
|
4324
|
-
IN is the mirror of HAS_*: IN takes a scalar fact with a list value; HAS_* takes lists on both sides.
|
|
4325
|
+
An action's targetVar must be a registered fact (ACT-032 otherwise). Keys a rule only reads (condition fields, refVar) may stay unregistered.
|
|
4325
4326
|
|
|
4326
|
-
|
|
4327
|
+
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: ${oneOf(LogicalOperator)}, children: [...] }
|
|
4328
|
+
Value types: ${ValueType.join(", ")}
|
|
4329
|
+
Limits: ${MAX_CONDITIONS_PER_RULE} SINGLE conditions per rule (GROUP nodes do not count), ${MAX_CONDITION_DEPTH} levels of nesting below the root, ${MAX_RULES_PER_VERSION} rules per version.
|
|
4327
4330
|
|
|
4328
|
-
|
|
4329
|
-
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
registered fact (ACT-032 otherwise). The request does not have to send it: SET_FACT is
|
|
4352
|
-
the only action whose targetVar may be absent from the input facts. MUTATE_FACT requires
|
|
4353
|
-
its target to be present in the input facts.
|
|
4354
|
-
- BLOCK: { reason?: string } Records a rejection decision. It does NOT halt rule execution —
|
|
4355
|
-
subsequent actions and subsequent winning rules still run. Enforcement is the caller's
|
|
4356
|
-
responsibility; the decision surfaces as isBlocked (true) and blockReason (reason, or
|
|
4357
|
-
"Policy Violation" when omitted).
|
|
4358
|
-
Only BLOCK writes those two keys: saving or publishing a rule whose SET_FACT or
|
|
4359
|
-
MUTATE_FACT targets either one fails with ACT-033.
|
|
4360
|
-
|
|
4361
|
-
Evaluation order: every rule's condition is checked against the input facts before any
|
|
4362
|
-
action runs, so a condition never sees a value an action writes in the same request. Then
|
|
4363
|
-
the actions of the winning rules run in priority order. Each action works on the values
|
|
4364
|
-
the actions before it left, in its own rule or an earlier one. To test a value an action
|
|
4365
|
-
writes, send that value as an input fact in a second request. If a rule should run
|
|
4366
|
-
whenever another one does, put its actions in that rule instead.
|
|
4367
|
-
|
|
4368
|
-
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
|
|
4331
|
+
Operators that work on each fact type:
|
|
4332
|
+
- STRING: EQUALS, NOT_EQUALS, CONTAINS (substring), IN, NOT_IN
|
|
4333
|
+
- NUMBER: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN
|
|
4334
|
+
- BOOLEAN: EQUALS, NOT_EQUALS
|
|
4335
|
+
- LIST_STRING, LIST_NUMBER: HAS_ANY, HAS_ALL, HAS_NONE
|
|
4336
|
+
IN and NOT_IN test whether the fact's value is in the array. HAS_ANY, HAS_ALL and HAS_NONE match when the fact's list holds at least one, all, or none of the array's values. The array holds values of the fact's type, or for a list fact, of its element type.
|
|
4337
|
+
Example: { "type": "SINGLE", "field": "userTags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
|
|
4338
|
+
|
|
4339
|
+
Actions: [{ type, parameters }]
|
|
4340
|
+
- MUTATE_FACT: { targetVar, operator: ${oneOf(MutateOperator)}, method: ${oneOf(CalculationMethod)}, operand: number, refVar?, rounding? }
|
|
4341
|
+
Reads and writes targetVar. The request must send it (P-015 otherwise), and it must hold a number when the action runs. operand may be negative, and a percent may be over 100.
|
|
4342
|
+
AMOUNT PERCENTAGE (base = refVar, or targetVar when omitted)
|
|
4343
|
+
ASSIGN targetVar = operand targetVar = base × operand/100
|
|
4344
|
+
ADD targetVar += operand targetVar += base × operand/100
|
|
4345
|
+
SUB targetVar -= operand targetVar -= base × operand/100
|
|
4346
|
+
MUL targetVar *= operand targetVar *= (operand/100 + 1)
|
|
4347
|
+
DIV targetVar /= operand invalid
|
|
4348
|
+
refVar is allowed only with PERCENTAGE and ASSIGN, ADD or SUB. DIV with AMOUNT needs a nonzero operand.
|
|
4349
|
+
rounding: { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: ${oneOf(RoundingPolicy)} }. mode defaults to HALF_UP. Without rounding, the result keeps full precision.
|
|
4350
|
+
- SET_FACT: { targetVar, value: string|number|boolean }. The request does not have to send targetVar.
|
|
4351
|
+
- BLOCK: { reason? } sets isBlocked to true and blockReason to the reason, or "Policy Violation" without one. It does not stop later actions or rules; the caller enforces the block. Only BLOCK writes these two keys: a SET_FACT or MUTATE_FACT targeting either is rejected (ACT-033).
|
|
4352
|
+
|
|
4353
|
+
Every condition is checked against the input facts before any action runs, so a condition never sees a value an action writes in the same request. The winning rules' actions then run in priority order, each on the values the actions before it left.
|
|
4369
4354
|
`,
|
|
4370
4355
|
inputSchema: z6.object({
|
|
4371
4356
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4384,7 +4369,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4384
4369
|
"lexq_rules_update",
|
|
4385
4370
|
{
|
|
4386
4371
|
title: "Update Rule",
|
|
4387
|
-
annotations:
|
|
4372
|
+
annotations: DESTRUCTIVE,
|
|
4388
4373
|
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.`,
|
|
4389
4374
|
inputSchema: z6.object({
|
|
4390
4375
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4406,28 +4391,21 @@ function registerRuleTools(server, callApi) {
|
|
|
4406
4391
|
"lexq_rules_delete",
|
|
4407
4392
|
{
|
|
4408
4393
|
title: "Delete Rule",
|
|
4409
|
-
annotations:
|
|
4394
|
+
annotations: DESTRUCTIVE,
|
|
4410
4395
|
description: "Delete a rule from a DRAFT version.",
|
|
4411
4396
|
inputSchema: z6.object({
|
|
4412
4397
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4413
4398
|
versionId: z6.string().uuid().describe("Version ID"),
|
|
4414
|
-
ruleId: z6.string().uuid().describe("Rule ID")
|
|
4415
|
-
force: z6.boolean().default(false).describe("Skip confirmation (for the last rule in a version)")
|
|
4399
|
+
ruleId: z6.string().uuid().describe("Rule ID")
|
|
4416
4400
|
})
|
|
4417
4401
|
},
|
|
4418
|
-
async ({ groupId, versionId, ruleId
|
|
4419
|
-
const params = {};
|
|
4420
|
-
if (force) params.force = "true";
|
|
4421
|
-
return callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`, {
|
|
4422
|
-
params
|
|
4423
|
-
});
|
|
4424
|
-
}
|
|
4402
|
+
async ({ groupId, versionId, ruleId }) => callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`)
|
|
4425
4403
|
);
|
|
4426
4404
|
server.registerTool(
|
|
4427
4405
|
"lexq_rules_reorder",
|
|
4428
4406
|
{
|
|
4429
4407
|
title: "Reorder Rules",
|
|
4430
|
-
annotations:
|
|
4408
|
+
annotations: DESTRUCTIVE,
|
|
4431
4409
|
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).",
|
|
4432
4410
|
inputSchema: z6.object({
|
|
4433
4411
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4449,7 +4427,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4449
4427
|
"lexq_rules_toggle",
|
|
4450
4428
|
{
|
|
4451
4429
|
title: "Toggle Rule",
|
|
4452
|
-
annotations:
|
|
4430
|
+
annotations: DESTRUCTIVE,
|
|
4453
4431
|
description: "Enable or disable a rule without deleting it.",
|
|
4454
4432
|
inputSchema: z6.object({
|
|
4455
4433
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4472,13 +4450,14 @@ var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
|
4472
4450
|
|
|
4473
4451
|
// src/mcp/tools/facts.ts
|
|
4474
4452
|
var VALUE_TYPES = ValueType;
|
|
4453
|
+
var IS_REQUIRED_NOTE = "isRequired is stored on the definition only; the engine does not read it.";
|
|
4475
4454
|
function registerFactTools(server, callApi) {
|
|
4476
4455
|
server.registerTool(
|
|
4477
4456
|
"lexq_facts_list",
|
|
4478
4457
|
{
|
|
4479
4458
|
title: "List Fact Definitions",
|
|
4480
|
-
annotations:
|
|
4481
|
-
description:
|
|
4459
|
+
annotations: READ_ONLY,
|
|
4460
|
+
description: `List all fact definitions (input variable schema): key, name, type, description, PII and system flags, and value domain. ${IS_REQUIRED_NOTE}`,
|
|
4482
4461
|
inputSchema: z7.object({
|
|
4483
4462
|
page: z7.number().int().min(0).default(0).describe("Page number"),
|
|
4484
4463
|
size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
@@ -4495,14 +4474,14 @@ function registerFactTools(server, callApi) {
|
|
|
4495
4474
|
"lexq_facts_create",
|
|
4496
4475
|
{
|
|
4497
4476
|
title: "Create Fact Definition",
|
|
4498
|
-
annotations:
|
|
4499
|
-
description:
|
|
4477
|
+
annotations: ADDITIVE,
|
|
4478
|
+
description: `Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: ${VALUE_TYPES.join(", ")}.`,
|
|
4500
4479
|
inputSchema: z7.object({
|
|
4501
4480
|
key: z7.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
4502
4481
|
name: z7.string().describe("Display name"),
|
|
4503
4482
|
type: z7.enum(VALUE_TYPES).describe("Value type"),
|
|
4504
4483
|
description: z7.string().optional().describe("Description"),
|
|
4505
|
-
isRequired: z7.boolean().default(false).describe(
|
|
4484
|
+
isRequired: z7.boolean().default(false).describe(IS_REQUIRED_NOTE),
|
|
4506
4485
|
isPii: z7.boolean().default(false).describe(
|
|
4507
4486
|
"Mark as PII \u2014 masked on every read surface, revealable only in the console (audited)"
|
|
4508
4487
|
),
|
|
@@ -4523,7 +4502,7 @@ function registerFactTools(server, callApi) {
|
|
|
4523
4502
|
"lexq_facts_update",
|
|
4524
4503
|
{
|
|
4525
4504
|
title: "Update Fact Definition",
|
|
4526
|
-
annotations:
|
|
4505
|
+
annotations: DESTRUCTIVE,
|
|
4527
4506
|
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).",
|
|
4528
4507
|
inputSchema: z7.object({
|
|
4529
4508
|
factId: z7.string().uuid().describe("Fact definition ID"),
|
|
@@ -4532,7 +4511,7 @@ function registerFactTools(server, callApi) {
|
|
|
4532
4511
|
type: z7.enum(VALUE_TYPES).optional().describe(
|
|
4533
4512
|
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
|
|
4534
4513
|
),
|
|
4535
|
-
isRequired: z7.boolean().optional().describe(
|
|
4514
|
+
isRequired: z7.boolean().optional().describe(IS_REQUIRED_NOTE),
|
|
4536
4515
|
isPii: z7.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
|
|
4537
4516
|
valueDomain: z7.object({
|
|
4538
4517
|
allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
|
|
@@ -4551,7 +4530,7 @@ function registerFactTools(server, callApi) {
|
|
|
4551
4530
|
"lexq_facts_delete",
|
|
4552
4531
|
{
|
|
4553
4532
|
title: "Delete Fact Definition",
|
|
4554
|
-
annotations:
|
|
4533
|
+
annotations: DESTRUCTIVE,
|
|
4555
4534
|
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.",
|
|
4556
4535
|
inputSchema: z7.object({
|
|
4557
4536
|
factId: z7.string().uuid().describe("Fact definition ID")
|
|
@@ -4563,7 +4542,7 @@ function registerFactTools(server, callApi) {
|
|
|
4563
4542
|
"lexq_facts_action_metadata",
|
|
4564
4543
|
{
|
|
4565
4544
|
title: "Get Action Runtime Fact Metadata",
|
|
4566
|
-
annotations:
|
|
4545
|
+
annotations: READ_ONLY,
|
|
4567
4546
|
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.",
|
|
4568
4547
|
inputSchema: z7.object({})
|
|
4569
4548
|
},
|
|
@@ -4573,8 +4552,8 @@ function registerFactTools(server, callApi) {
|
|
|
4573
4552
|
"lexq_facts_unregistered",
|
|
4574
4553
|
{
|
|
4575
4554
|
title: "List Unregistered Facts",
|
|
4576
|
-
annotations:
|
|
4577
|
-
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).
|
|
4555
|
+
annotations: READ_ONLY,
|
|
4556
|
+
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). Registering one with lexq_facts_create before the version is published makes the engine check request values for it against its type (P-060) and any value domain (P-052).",
|
|
4578
4557
|
inputSchema: z7.object({
|
|
4579
4558
|
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
4580
4559
|
versionId: z7.string().uuid().describe("Version ID")
|
|
@@ -4586,16 +4565,20 @@ function registerFactTools(server, callApi) {
|
|
|
4586
4565
|
"lexq_facts_export",
|
|
4587
4566
|
{
|
|
4588
4567
|
title: "Export Fact Catalog",
|
|
4589
|
-
annotations:
|
|
4590
|
-
description:
|
|
4568
|
+
annotations: READ_ONLY,
|
|
4569
|
+
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, without value domains; JSON lists each definition with only the fields a new definition takes (key, name, type, description, isRequired, isPii, valueDomain), without IDs. Returns the file contents as text. ${EXPORT_LIMIT_NOTE}`,
|
|
4591
4570
|
inputSchema: z7.object({
|
|
4592
4571
|
format: z7.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
|
|
4593
4572
|
keyword: z7.string().optional().describe("Filter by key or name")
|
|
4594
4573
|
})
|
|
4595
4574
|
},
|
|
4596
|
-
async ({ format, keyword }) =>
|
|
4597
|
-
|
|
4598
|
-
|
|
4575
|
+
async ({ format, keyword }) => limitExport(
|
|
4576
|
+
await callApi("GET", "schema/facts/export", {
|
|
4577
|
+
params: keyword ? { format, keyword } : { format }
|
|
4578
|
+
}),
|
|
4579
|
+
`lexq facts export --as ${format} --output <file>`,
|
|
4580
|
+
"Pass keyword to export fewer facts."
|
|
4581
|
+
)
|
|
4599
4582
|
);
|
|
4600
4583
|
}
|
|
4601
4584
|
|
|
@@ -4607,7 +4590,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4607
4590
|
"lexq_deploy_publish",
|
|
4608
4591
|
{
|
|
4609
4592
|
title: "Publish Version",
|
|
4610
|
-
annotations:
|
|
4593
|
+
annotations: DESTRUCTIVE,
|
|
4611
4594
|
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.`,
|
|
4612
4595
|
inputSchema: z8.object({
|
|
4613
4596
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4621,7 +4604,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4621
4604
|
"lexq_deploy_live",
|
|
4622
4605
|
{
|
|
4623
4606
|
title: "Deploy to Live",
|
|
4624
|
-
annotations:
|
|
4607
|
+
annotations: DESTRUCTIVE,
|
|
4625
4608
|
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.",
|
|
4626
4609
|
inputSchema: z8.object({
|
|
4627
4610
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4637,7 +4620,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4637
4620
|
"lexq_deploy_rollback",
|
|
4638
4621
|
{
|
|
4639
4622
|
title: "Rollback Deployment",
|
|
4640
|
-
annotations:
|
|
4623
|
+
annotations: DESTRUCTIVE,
|
|
4641
4624
|
description: dedent18`
|
|
4642
4625
|
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).
|
|
4643
4626
|
|
|
@@ -4658,7 +4641,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4658
4641
|
"lexq_deploy_undeploy",
|
|
4659
4642
|
{
|
|
4660
4643
|
title: "Undeploy",
|
|
4661
|
-
annotations:
|
|
4644
|
+
annotations: DESTRUCTIVE,
|
|
4662
4645
|
description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
|
|
4663
4646
|
inputSchema: z8.object({
|
|
4664
4647
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4673,7 +4656,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4673
4656
|
"lexq_deploy_schedule",
|
|
4674
4657
|
{
|
|
4675
4658
|
title: "Schedule Deployment",
|
|
4676
|
-
annotations:
|
|
4659
|
+
annotations: DESTRUCTIVE,
|
|
4677
4660
|
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).",
|
|
4678
4661
|
inputSchema: z8.object({
|
|
4679
4662
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4689,7 +4672,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4689
4672
|
"lexq_deploy_unschedule",
|
|
4690
4673
|
{
|
|
4691
4674
|
title: "Cancel Scheduled Deployment",
|
|
4692
|
-
annotations:
|
|
4675
|
+
annotations: DESTRUCTIVE,
|
|
4693
4676
|
description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
|
|
4694
4677
|
inputSchema: z8.object({
|
|
4695
4678
|
groupId: z8.string().uuid().describe("Policy group ID")
|
|
@@ -4701,8 +4684,8 @@ function registerDeployTools(server, callApi) {
|
|
|
4701
4684
|
"lexq_deploy_schedules",
|
|
4702
4685
|
{
|
|
4703
4686
|
title: "List Scheduled Deployments",
|
|
4704
|
-
annotations:
|
|
4705
|
-
description:
|
|
4687
|
+
annotations: READ_ONLY,
|
|
4688
|
+
description: `List scheduled deployments across all groups (all statuses: ${DeploymentScheduleStatus.join(", ")}), newest first.`,
|
|
4706
4689
|
inputSchema: z8.object({
|
|
4707
4690
|
page: z8.number().int().min(0).default(0).describe("Page number"),
|
|
4708
4691
|
size: z8.number().int().min(1).max(100).default(20).describe("Page size")
|
|
@@ -4714,13 +4697,13 @@ function registerDeployTools(server, callApi) {
|
|
|
4714
4697
|
"lexq_deploy_history",
|
|
4715
4698
|
{
|
|
4716
4699
|
title: "Deployment History",
|
|
4717
|
-
annotations:
|
|
4700
|
+
annotations: READ_ONLY,
|
|
4718
4701
|
description: "List deployment history across all groups.",
|
|
4719
4702
|
inputSchema: z8.object({
|
|
4720
4703
|
page: z8.number().int().min(0).default(0).describe("Page number"),
|
|
4721
4704
|
size: z8.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
4722
4705
|
groupId: z8.string().uuid().optional().describe("Filter by group ID"),
|
|
4723
|
-
types: z8.string().optional().describe(
|
|
4706
|
+
types: z8.string().optional().describe(`Filter by deployment types (comma-separated: ${DeploymentType.join(",")})`),
|
|
4724
4707
|
startDate: z8.string().optional().describe(START_DAY),
|
|
4725
4708
|
endDate: z8.string().optional().describe(END_DAY)
|
|
4726
4709
|
})
|
|
@@ -4738,7 +4721,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4738
4721
|
"lexq_deploy_detail",
|
|
4739
4722
|
{
|
|
4740
4723
|
title: "Deployment Detail",
|
|
4741
|
-
annotations:
|
|
4724
|
+
annotations: READ_ONLY,
|
|
4742
4725
|
description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
|
|
4743
4726
|
inputSchema: z8.object({
|
|
4744
4727
|
deploymentId: z8.string().uuid().describe("Deployment ID")
|
|
@@ -4750,7 +4733,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4750
4733
|
"lexq_deploy_overview",
|
|
4751
4734
|
{
|
|
4752
4735
|
title: "Deployment Overview",
|
|
4753
|
-
annotations:
|
|
4736
|
+
annotations: READ_ONLY,
|
|
4754
4737
|
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.",
|
|
4755
4738
|
inputSchema: z8.object({})
|
|
4756
4739
|
},
|
|
@@ -4760,7 +4743,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4760
4743
|
"lexq_deploy_deployable",
|
|
4761
4744
|
{
|
|
4762
4745
|
title: "List Deployable Versions",
|
|
4763
|
-
annotations:
|
|
4746
|
+
annotations: READ_ONLY,
|
|
4764
4747
|
description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
|
|
4765
4748
|
inputSchema: z8.object({
|
|
4766
4749
|
groupId: z8.string().uuid().describe("Policy group ID")
|
|
@@ -4772,7 +4755,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4772
4755
|
"lexq_deploy_diff",
|
|
4773
4756
|
{
|
|
4774
4757
|
title: "Deployment Diff",
|
|
4775
|
-
annotations:
|
|
4758
|
+
annotations: READ_ONLY,
|
|
4776
4759
|
description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
|
|
4777
4760
|
inputSchema: z8.object({
|
|
4778
4761
|
baseVersionId: z8.string().uuid().describe("Base version ID (typically the current live)"),
|
|
@@ -4801,8 +4784,8 @@ function registerProfileTools(server, callApi) {
|
|
|
4801
4784
|
"lexq_profile_overview",
|
|
4802
4785
|
{
|
|
4803
4786
|
title: "Group Latency Profile",
|
|
4804
|
-
annotations:
|
|
4805
|
-
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265
|
|
4787
|
+
annotations: READ_ONLY,
|
|
4788
|
+
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99} \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
|
|
4806
4789
|
inputSchema: z9.object({
|
|
4807
4790
|
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
4808
4791
|
versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
@@ -4810,7 +4793,7 @@ function registerProfileTools(server, callApi) {
|
|
|
4810
4793
|
`Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
4811
4794
|
),
|
|
4812
4795
|
to: z9.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
|
|
4813
|
-
cacheState: z9.enum(
|
|
4796
|
+
cacheState: z9.enum(ProfileCacheState).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
|
|
4814
4797
|
})
|
|
4815
4798
|
},
|
|
4816
4799
|
async ({ groupId, versionId, from, to, cacheState }) => callApi("GET", `policy-groups/${groupId}/profile`, {
|
|
@@ -4821,8 +4804,8 @@ function registerProfileTools(server, callApi) {
|
|
|
4821
4804
|
"lexq_profile_rule",
|
|
4822
4805
|
{
|
|
4823
4806
|
title: "Rule Latency Detail",
|
|
4824
|
-
annotations:
|
|
4825
|
-
description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265
|
|
4807
|
+
annotations: READ_ONLY,
|
|
4808
|
+
description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99}). ` + RELATIVE_THRESHOLD,
|
|
4826
4809
|
inputSchema: z9.object({
|
|
4827
4810
|
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
4828
4811
|
ruleId: z9.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
|
|
@@ -4846,7 +4829,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4846
4829
|
"lexq_replay_decision",
|
|
4847
4830
|
{
|
|
4848
4831
|
title: "Replay a Decision",
|
|
4849
|
-
annotations:
|
|
4832
|
+
annotations: READ_ONLY,
|
|
4850
4833
|
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.",
|
|
4851
4834
|
inputSchema: z10.object({
|
|
4852
4835
|
traceId: z10.string().describe("Trace ID of the past execution to replay"),
|
|
@@ -4859,7 +4842,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4859
4842
|
"lexq_replay_start",
|
|
4860
4843
|
{
|
|
4861
4844
|
title: "Start Window Replay (Blast Radius)",
|
|
4862
|
-
annotations:
|
|
4845
|
+
annotations: DESTRUCTIVE,
|
|
4863
4846
|
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.",
|
|
4864
4847
|
inputSchema: z10.object({
|
|
4865
4848
|
candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against"),
|
|
@@ -4876,7 +4859,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4876
4859
|
"lexq_replay_status",
|
|
4877
4860
|
{
|
|
4878
4861
|
title: "Get Replay Job Status",
|
|
4879
|
-
annotations:
|
|
4862
|
+
annotations: READ_ONLY,
|
|
4880
4863
|
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.",
|
|
4881
4864
|
inputSchema: z10.object({
|
|
4882
4865
|
jobId: z10.string().describe("Replay job ID from lexq_replay_start")
|
|
@@ -4888,7 +4871,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4888
4871
|
"lexq_replay_list",
|
|
4889
4872
|
{
|
|
4890
4873
|
title: "List Replay Jobs",
|
|
4891
|
-
annotations:
|
|
4874
|
+
annotations: READ_ONLY,
|
|
4892
4875
|
description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
|
|
4893
4876
|
inputSchema: z10.object({
|
|
4894
4877
|
page: z10.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -4901,7 +4884,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4901
4884
|
"lexq_replay_cancel",
|
|
4902
4885
|
{
|
|
4903
4886
|
title: "Cancel Replay Job",
|
|
4904
|
-
annotations:
|
|
4887
|
+
annotations: DESTRUCTIVE,
|
|
4905
4888
|
description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
|
|
4906
4889
|
inputSchema: z10.object({
|
|
4907
4890
|
jobId: z10.string().describe("Replay job ID")
|
|
@@ -4913,14 +4896,17 @@ function registerReplayTools(server, callApi) {
|
|
|
4913
4896
|
"lexq_replay_export",
|
|
4914
4897
|
{
|
|
4915
4898
|
title: "Export Replay Result",
|
|
4916
|
-
annotations:
|
|
4917
|
-
description:
|
|
4899
|
+
annotations: READ_ONLY,
|
|
4900
|
+
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. ${EXPORT_LIMIT_NOTE}`,
|
|
4918
4901
|
inputSchema: z10.object({
|
|
4919
4902
|
jobId: z10.string().describe("Replay job ID"),
|
|
4920
4903
|
format: z10.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
4921
4904
|
})
|
|
4922
4905
|
},
|
|
4923
|
-
async ({ jobId, format }) =>
|
|
4906
|
+
async ({ jobId, format }) => limitExport(
|
|
4907
|
+
await callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } }),
|
|
4908
|
+
`lexq replay export --id ${jobId} --as ${format} --output <file>`
|
|
4909
|
+
)
|
|
4924
4910
|
);
|
|
4925
4911
|
}
|
|
4926
4912
|
|
|
@@ -4931,7 +4917,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
4931
4917
|
"lexq_history_list",
|
|
4932
4918
|
{
|
|
4933
4919
|
title: "List Execution History",
|
|
4934
|
-
annotations:
|
|
4920
|
+
annotations: READ_ONLY,
|
|
4935
4921
|
description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
|
|
4936
4922
|
inputSchema: z11.object({
|
|
4937
4923
|
page: z11.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -4939,7 +4925,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
4939
4925
|
traceId: z11.string().optional().describe("Filter by trace ID"),
|
|
4940
4926
|
groupId: z11.string().uuid().optional().describe("Filter by policy group"),
|
|
4941
4927
|
versionId: z11.string().uuid().optional().describe("Filter by version"),
|
|
4942
|
-
status: z11.enum(
|
|
4928
|
+
status: z11.enum(ApiExecutionStatus).optional().describe("Filter by execution status"),
|
|
4943
4929
|
startDate: z11.string().optional().describe(START_DAY),
|
|
4944
4930
|
endDate: z11.string().optional().describe(END_DAY)
|
|
4945
4931
|
})
|
|
@@ -4959,7 +4945,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
4959
4945
|
"lexq_history_get",
|
|
4960
4946
|
{
|
|
4961
4947
|
title: "Get Execution Detail",
|
|
4962
|
-
annotations:
|
|
4948
|
+
annotations: READ_ONLY,
|
|
4963
4949
|
description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
|
|
4964
4950
|
inputSchema: z11.object({
|
|
4965
4951
|
traceId: z11.string().describe("Trace ID from execution history")
|
|
@@ -4971,7 +4957,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
4971
4957
|
"lexq_history_stats",
|
|
4972
4958
|
{
|
|
4973
4959
|
title: "Execution Statistics",
|
|
4974
|
-
annotations:
|
|
4960
|
+
annotations: READ_ONLY,
|
|
4975
4961
|
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.`,
|
|
4976
4962
|
inputSchema: z11.object({
|
|
4977
4963
|
groupId: z11.string().uuid().optional().describe("Filter by policy group"),
|
|
@@ -4996,7 +4982,7 @@ function registerProvenanceTools(server, callApi) {
|
|
|
4996
4982
|
"lexq_provenance_get",
|
|
4997
4983
|
{
|
|
4998
4984
|
title: "Get Decision Provenance",
|
|
4999
|
-
annotations:
|
|
4985
|
+
annotations: READ_ONLY,
|
|
5000
4986
|
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.",
|
|
5001
4987
|
inputSchema: z12.object({
|
|
5002
4988
|
traceId: z12.string().describe("Trace ID of the execution")
|
|
@@ -5008,7 +4994,7 @@ function registerProvenanceTools(server, callApi) {
|
|
|
5008
4994
|
"lexq_pii_reveals_list",
|
|
5009
4995
|
{
|
|
5010
4996
|
title: "List PII Reveal Audits",
|
|
5011
|
-
annotations:
|
|
4997
|
+
annotations: READ_ONLY,
|
|
5012
4998
|
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.",
|
|
5013
4999
|
inputSchema: z12.object({
|
|
5014
5000
|
page: z12.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -5039,8 +5025,8 @@ function registerLogTools(server, callApi) {
|
|
|
5039
5025
|
"lexq_logs_list",
|
|
5040
5026
|
{
|
|
5041
5027
|
title: "List Failure Logs",
|
|
5042
|
-
annotations:
|
|
5043
|
-
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
|
|
5028
|
+
annotations: READ_ONLY,
|
|
5029
|
+
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. ${FAILURE_LOG_CONSOLE_NOTE}`,
|
|
5044
5030
|
inputSchema: z13.object({
|
|
5045
5031
|
page: z13.number().int().min(0).default(0).describe("Page number"),
|
|
5046
5032
|
size: z13.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
@@ -5065,54 +5051,28 @@ function registerLogTools(server, callApi) {
|
|
|
5065
5051
|
"lexq_logs_get",
|
|
5066
5052
|
{
|
|
5067
5053
|
title: "Get Failure Log",
|
|
5068
|
-
annotations:
|
|
5069
|
-
description:
|
|
5054
|
+
annotations: READ_ONLY,
|
|
5055
|
+
description: `Get failure log detail by ID. ${FAILURE_LOG_CONSOLE_NOTE}`,
|
|
5070
5056
|
inputSchema: z13.object({
|
|
5071
5057
|
logId: z13.string().uuid().describe("Failure log ID")
|
|
5072
5058
|
})
|
|
5073
5059
|
},
|
|
5074
5060
|
async ({ logId }) => callApi("GET", `failure-logs/${logId}`)
|
|
5075
5061
|
);
|
|
5076
|
-
server.registerTool(
|
|
5077
|
-
"lexq_logs_action",
|
|
5078
|
-
{
|
|
5079
|
-
title: "Process Failure Log",
|
|
5080
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5081
|
-
description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
|
|
5082
|
-
inputSchema: z13.object({
|
|
5083
|
-
logId: z13.string().uuid().describe("Failure log ID"),
|
|
5084
|
-
action: z13.enum(FailureAction).describe("Action to take")
|
|
5085
|
-
})
|
|
5086
|
-
},
|
|
5087
|
-
async ({ logId, action }) => callApi("POST", `failure-logs/${logId}/actions`, {
|
|
5088
|
-
params: { action }
|
|
5089
|
-
})
|
|
5090
|
-
);
|
|
5091
|
-
server.registerTool(
|
|
5092
|
-
"lexq_logs_bulk_action",
|
|
5093
|
-
{
|
|
5094
|
-
title: "Bulk Process Failure Logs",
|
|
5095
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5096
|
-
description: "Process multiple failure logs at once. Provide an array of log IDs and the action.",
|
|
5097
|
-
inputSchema: z13.object({
|
|
5098
|
-
logIds: z13.array(z13.string().uuid()).describe("Array of failure log IDs"),
|
|
5099
|
-
action: z13.enum(FailureAction).describe("Action to apply to all logs")
|
|
5100
|
-
})
|
|
5101
|
-
},
|
|
5102
|
-
async ({ logIds, action }) => callApi("POST", "failure-logs/bulk-actions", {
|
|
5103
|
-
body: { logIds, action }
|
|
5104
|
-
})
|
|
5105
|
-
);
|
|
5106
5062
|
}
|
|
5107
5063
|
|
|
5108
5064
|
// src/mcp/tools/webhook-subscriptions.ts
|
|
5109
5065
|
import { z as z14 } from "zod";
|
|
5066
|
+
var FORMAT_NOTES2 = {
|
|
5067
|
+
GENERIC: "full JSON",
|
|
5068
|
+
SLACK: '{"text": "..."}'
|
|
5069
|
+
};
|
|
5110
5070
|
function registerWebhookSubscriptionTools(server, callApi) {
|
|
5111
5071
|
server.registerTool(
|
|
5112
5072
|
"lexq_webhook_subscriptions_list",
|
|
5113
5073
|
{
|
|
5114
5074
|
title: "List Webhook Subscriptions",
|
|
5115
|
-
annotations:
|
|
5075
|
+
annotations: READ_ONLY,
|
|
5116
5076
|
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.",
|
|
5117
5077
|
inputSchema: z14.object({
|
|
5118
5078
|
page: z14.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -5128,7 +5088,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5128
5088
|
"lexq_webhook_subscriptions_get",
|
|
5129
5089
|
{
|
|
5130
5090
|
title: "Get Webhook Subscription",
|
|
5131
|
-
annotations:
|
|
5091
|
+
annotations: READ_ONLY,
|
|
5132
5092
|
description: "Get webhook subscription detail by ID.",
|
|
5133
5093
|
inputSchema: z14.object({
|
|
5134
5094
|
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
@@ -5140,8 +5100,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5140
5100
|
"lexq_webhook_subscriptions_save",
|
|
5141
5101
|
{
|
|
5142
5102
|
title: "Save Webhook Subscription",
|
|
5143
|
-
annotations: {
|
|
5144
|
-
description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats:
|
|
5103
|
+
annotations: { ...DESTRUCTIVE, openWorldHint: true },
|
|
5104
|
+
description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: ${withNotes(FORMAT_NOTES2).join(", ")}.`,
|
|
5145
5105
|
inputSchema: z14.object({
|
|
5146
5106
|
id: z14.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
|
|
5147
5107
|
name: z14.string().min(1).describe("Subscription name (unique per tenant)"),
|
|
@@ -5158,7 +5118,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5158
5118
|
"lexq_webhook_subscriptions_delete",
|
|
5159
5119
|
{
|
|
5160
5120
|
title: "Delete Webhook Subscription",
|
|
5161
|
-
annotations:
|
|
5121
|
+
annotations: DESTRUCTIVE,
|
|
5162
5122
|
description: "Delete a webhook subscription by ID.",
|
|
5163
5123
|
inputSchema: z14.object({
|
|
5164
5124
|
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
@@ -5170,7 +5130,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5170
5130
|
"lexq_webhook_subscriptions_test",
|
|
5171
5131
|
{
|
|
5172
5132
|
title: "Test Webhook Subscription",
|
|
5173
|
-
annotations: {
|
|
5133
|
+
annotations: { ...DESTRUCTIVE, openWorldHint: true },
|
|
5174
5134
|
description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
|
|
5175
5135
|
inputSchema: z14.object({
|
|
5176
5136
|
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
@@ -5187,7 +5147,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5187
5147
|
"lexq_domain_templates_list",
|
|
5188
5148
|
{
|
|
5189
5149
|
title: "List Domain Templates",
|
|
5190
|
-
annotations:
|
|
5150
|
+
annotations: READ_ONLY,
|
|
5191
5151
|
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.",
|
|
5192
5152
|
inputSchema: z15.object({})
|
|
5193
5153
|
},
|
|
@@ -5197,7 +5157,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5197
5157
|
"lexq_domain_templates_preview",
|
|
5198
5158
|
{
|
|
5199
5159
|
title: "Preview Domain Template",
|
|
5200
|
-
annotations:
|
|
5160
|
+
annotations: READ_ONLY,
|
|
5201
5161
|
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.",
|
|
5202
5162
|
inputSchema: z15.object({
|
|
5203
5163
|
template: z15.string().describe(
|
|
@@ -5211,7 +5171,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5211
5171
|
"lexq_domain_templates_apply",
|
|
5212
5172
|
{
|
|
5213
5173
|
title: "Apply Domain Template",
|
|
5214
|
-
annotations:
|
|
5174
|
+
annotations: ADDITIVE,
|
|
5215
5175
|
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.",
|
|
5216
5176
|
inputSchema: z15.object({
|
|
5217
5177
|
template: z15.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
|
|
@@ -5246,6 +5206,13 @@ function registerAllTools(server, callApi) {
|
|
|
5246
5206
|
registerWebhookSubscriptionTools(server, callApi);
|
|
5247
5207
|
}
|
|
5248
5208
|
var SERVER_INFO = { name: "lexq", ...server_info_default };
|
|
5209
|
+
var SERVER_INSTRUCTIONS = [
|
|
5210
|
+
"LexQ evaluates business rules: a policy group holds versions, a version holds rules, and rules read input facts.",
|
|
5211
|
+
"- Date filters (yyyy-MM-dd) are whole days in the organization's time zone, not UTC. Call lexq_whoami for tenantTimezone before working out the current date or a date range.",
|
|
5212
|
+
"- Before creating rules, call lexq_facts_list to see which facts exist and their types. When a rule needs a fact that is not defined, confirm its type and description with the user before calling lexq_facts_create.",
|
|
5213
|
+
"- Before lexq_deploy_publish, run lexq_dry_run on the version with representative facts. A published version cannot go back to draft.",
|
|
5214
|
+
`- For anything these tools cannot do, point the user to the LexQ console at ${CONSOLE_URL}.`
|
|
5215
|
+
].join("\n");
|
|
5249
5216
|
|
|
5250
5217
|
// src/mcp/server.ts
|
|
5251
5218
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -5262,7 +5229,10 @@ function startMcpServer() {
|
|
|
5262
5229
|
const callApi = createCallApiFromConfig();
|
|
5263
5230
|
serveStdio(
|
|
5264
5231
|
() => {
|
|
5265
|
-
const server = new McpServer(
|
|
5232
|
+
const server = new McpServer(
|
|
5233
|
+
{ ...SERVER_INFO, version },
|
|
5234
|
+
{ instructions: SERVER_INSTRUCTIONS }
|
|
5235
|
+
);
|
|
5266
5236
|
registerAllTools(server, callApi);
|
|
5267
5237
|
registerLocalTools(server);
|
|
5268
5238
|
return server;
|