@lexq/cli 0.3.1 → 1.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/dist/index.js CHANGED
@@ -72,6 +72,7 @@ var survivesDoubleRoundTrip = (literal) => isSafeNumber(literal);
72
72
  var parseNumber = (value) => survivesDoubleRoundTrip(value) ? parseFloat(value) : new LosslessNumber(value);
73
73
  var parseJson = (text) => losslessParse(text, void 0, parseNumber);
74
74
  var stringifyJson = (value, space) => losslessStringify(value, void 0, space);
75
+ var isLosslessNumber = (value) => value instanceof LosslessNumber;
75
76
 
76
77
  // src/lib/api-client.ts
77
78
  var ApiError = class extends Error {
@@ -1338,7 +1339,7 @@ ${data.length} unregistered`);
1338
1339
  process.exit(1);
1339
1340
  }
1340
1341
  });
1341
- facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (letters, numbers, underscores)").option("--name <n>", "Display name").option("--type <type>", "Value type: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER").option("--description <desc>", "Description").option("--required", "Mark as required", false).option("--pii", "Mark as PII \u2014 value is masked on every read surface", false).option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
1342
+ facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (letters, numbers, underscores)").option("--name <n>", "Display name").option("--type <type>", "Value type: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER").option("--description <desc>", "Description").option("--required", "Mark as required", false).option("--pii", "Mark as PII \u2014 value is masked on every read surface", false).option("--allowed-values <csv>", "Comma-separated list of accepted values").option("--min <number>", "Lower bound, inclusive (numeric types only)").option("--max <number>", "Upper bound, inclusive (numeric types only)").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
1342
1343
  "after",
1343
1344
  dedent6`
1344
1345
 
@@ -1354,9 +1355,21 @@ ${data.length} unregistered`);
1354
1355
  "isRequired": false
1355
1356
  }'
1356
1357
 
1358
+ $ lexq facts create --key riskScore --name "Risk Score" --type NUMBER --min 0 --max 100
1359
+ $ lexq facts create --key tier --name "Tier" --type STRING \
1360
+ --allowed-values "GOLD, SILVER, BRONZE"
1361
+
1357
1362
  Value Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
1358
1363
  Key Format: starts with a letter, then letters, numbers, and underscores.
1359
1364
  Casing is yours to choose (e.g., paymentAmount, payment_amount)
1365
+
1366
+ Value Domain: --allowed-values, --min, and --max say which values the fact accepts.
1367
+ A rule can then only compare against those values, and the console turns
1368
+ the rule editor into a dropdown instead of a free text box.
1369
+ STRING types take --allowed-values. NUMBER types take all three.
1370
+ BOOLEAN takes none — it has only two values to begin with.
1371
+ For list types the constraint applies to each element.
1372
+ Leave them out for no constraint, which is how facts behaved before.
1360
1373
  `
1361
1374
  ).action(async (opts) => {
1362
1375
  try {
@@ -1375,14 +1388,25 @@ ${data.length} unregistered`);
1375
1388
  process.exit(1);
1376
1389
  }
1377
1390
  });
1378
- facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <n>", "Display name").option("--description <desc>", "Description").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("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
1391
+ facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <n>", "Display name").option("--description <desc>", "Description").option("--required", "Mark as required").option("--no-required", "Mark as not required").option("--pii", "Mark as PII (enables masking)").option("--no-pii", "Unmark as PII (disables masking \u2014 value becomes visible)").option("--type <type>", "Value type: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER").option("--allowed-values <csv>", "Comma-separated list of accepted values").option("--min <number>", "Lower bound, inclusive (numeric types only)").option("--max <number>", "Upper bound, inclusive (numeric types only)").option("--clear-value-domain", "Remove the value constraint entirely").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
1379
1392
  "after",
1380
1393
  dedent6`
1381
1394
 
1382
- System facts cannot be modified. Only display name, description, and required flag can be changed.
1395
+ System facts accept name, description, and the PII flag only.
1383
1396
 
1384
- Example:
1397
+ Examples:
1385
1398
  $ lexq facts update --id <factId> --name "Updated Name" --required
1399
+ $ lexq facts update --id <factId> --min 0 --max 100
1400
+ $ lexq facts update --id <factId> --clear-value-domain
1401
+
1402
+ Only the flags you pass are changed. Leaving out --allowed-values, --min, and --max
1403
+ leaves the constraint alone; --clear-value-domain removes it.
1404
+
1405
+ Narrowing a constraint does not rewrite rules that already reference this fact. They keep
1406
+ working, and the next time one is saved it is judged against the new constraint.
1407
+
1408
+ Pass --type when the accepted values are numbers and you are not also naming the type,
1409
+ so "10" is sent as a number rather than a string. --json settles it either way.
1386
1410
  `
1387
1411
  ).action(async (opts) => {
1388
1412
  try {
@@ -1496,16 +1520,66 @@ function buildCreateBody2(opts) {
1496
1520
  isPii: opts.pii === true
1497
1521
  };
1498
1522
  if (opts.description) body.description = opts.description;
1523
+ const domain = buildValueDomain(opts, opts.type);
1524
+ if (domain) body.valueDomain = domain;
1499
1525
  return body;
1500
1526
  }
1501
1527
  function buildUpdateBody2(opts) {
1502
1528
  const body = {};
1503
1529
  if (opts.name) body.name = opts.name;
1504
1530
  if (opts.description !== void 0) body.description = opts.description;
1531
+ if (opts.type) body.type = opts.type;
1505
1532
  if (typeof opts.required === "boolean") body.isRequired = opts.required;
1506
1533
  if (typeof opts.isPii === "boolean") body.isPii = opts.isPii;
1534
+ if (opts.clearValueDomain === true) {
1535
+ if (hasDomainFlag(opts)) {
1536
+ throw new Error(
1537
+ "--clear-value-domain cannot be combined with --allowed-values, --min, or --max."
1538
+ );
1539
+ }
1540
+ body.valueDomain = {};
1541
+ return body;
1542
+ }
1543
+ const domain = buildValueDomain(opts, opts.type);
1544
+ if (domain) body.valueDomain = domain;
1507
1545
  return body;
1508
1546
  }
1547
+ function hasDomainFlag(opts) {
1548
+ return opts.allowedValues !== void 0 || opts.min !== void 0 || opts.max !== void 0;
1549
+ }
1550
+ function parseNumericLiteral(text, flag) {
1551
+ let parsed;
1552
+ try {
1553
+ parsed = parseJson(text);
1554
+ } catch {
1555
+ throw new Error(`${flag} must be a number, got: ${text}`);
1556
+ }
1557
+ if (typeof parsed !== "number" && !isLosslessNumber(parsed)) {
1558
+ throw new Error(`${flag} must be a number, got: ${text}`);
1559
+ }
1560
+ return parsed;
1561
+ }
1562
+ function asDomainValue(text, numeric) {
1563
+ if (numeric === true) return parseNumericLiteral(text, "--allowed-values");
1564
+ if (numeric === false) return text;
1565
+ try {
1566
+ const parsed = parseJson(text);
1567
+ return typeof parsed === "number" || isLosslessNumber(parsed) ? parsed : text;
1568
+ } catch {
1569
+ return text;
1570
+ }
1571
+ }
1572
+ function buildValueDomain(opts, type) {
1573
+ if (!hasDomainFlag(opts)) return void 0;
1574
+ const numeric = type === void 0 ? void 0 : type === "NUMBER" || type === "LIST_NUMBER";
1575
+ const domain = {};
1576
+ if (opts.allowedValues !== void 0) {
1577
+ domain.allowedValues = opts.allowedValues.split(",").map((part) => part.trim()).filter((part) => part.length > 0).map((part) => asDomainValue(part, numeric));
1578
+ }
1579
+ if (opts.min !== void 0) domain.min = parseNumericLiteral(opts.min, "--min");
1580
+ if (opts.max !== void 0) domain.max = parseNumericLiteral(opts.max, "--max");
1581
+ return domain;
1582
+ }
1509
1583
 
1510
1584
  // src/commands/deploy.ts
1511
1585
  import "commander";
@@ -3421,6 +3495,7 @@ function registerStatusTools(server, callApi) {
3421
3495
  "lexq_whoami",
3422
3496
  {
3423
3497
  title: "Who Am I",
3498
+ annotations: { readOnlyHint: true },
3424
3499
  description: "Show current authentication info (tenant ID, user ID, role).",
3425
3500
  inputSchema: z.object({})
3426
3501
  },
@@ -3435,6 +3510,7 @@ function registerGroupTools(server, callApi) {
3435
3510
  "lexq_groups_list",
3436
3511
  {
3437
3512
  title: "List Policy Groups",
3513
+ annotations: { readOnlyHint: true },
3438
3514
  description: "List all policy groups (tenant-wide, priority ASC).",
3439
3515
  inputSchema: z2.object({})
3440
3516
  },
@@ -3444,6 +3520,7 @@ function registerGroupTools(server, callApi) {
3444
3520
  "lexq_groups_get",
3445
3521
  {
3446
3522
  title: "Get Policy Group",
3523
+ annotations: { readOnlyHint: true },
3447
3524
  description: "Get a single policy group by ID.",
3448
3525
  inputSchema: z2.object({
3449
3526
  groupId: z2.string().uuid().describe("Policy group ID")
@@ -3455,6 +3532,7 @@ function registerGroupTools(server, callApi) {
3455
3532
  "lexq_groups_create",
3456
3533
  {
3457
3534
  title: "Create Policy Group",
3535
+ annotations: { readOnlyHint: false, destructiveHint: false },
3458
3536
  description: "Create a new policy group. Requires name. Priority is auto-assigned (appended last, tenant-wide); use lexq_groups_reorder to change order. Optionally set conflict resolution, activation group, and description. Policy groups that share an activationGroup form a cluster and must share the same activationMode / activationStrategy / executionLimit; executionLimit is how many of those groups run, not how many rules.",
3459
3537
  inputSchema: z2.object({
3460
3538
  name: z2.string().describe("Group name (unique among non-ARCHIVED)"),
@@ -3485,6 +3563,7 @@ function registerGroupTools(server, callApi) {
3485
3563
  "lexq_groups_update",
3486
3564
  {
3487
3565
  title: "Update Policy Group",
3566
+ annotations: { readOnlyHint: false, destructiveHint: true },
3488
3567
  description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged.",
3489
3568
  inputSchema: z2.object({
3490
3569
  groupId: z2.string().uuid().describe("Policy group ID"),
@@ -3507,6 +3586,7 @@ function registerGroupTools(server, callApi) {
3507
3586
  "lexq_groups_delete",
3508
3587
  {
3509
3588
  title: "Delete Policy Group",
3589
+ annotations: { readOnlyHint: false, destructiveHint: true },
3510
3590
  description: "Archive a policy group. Only non-live groups can be deleted. This is irreversible.",
3511
3591
  inputSchema: z2.object({
3512
3592
  groupId: z2.string().uuid().describe("Policy group ID")
@@ -3518,6 +3598,7 @@ function registerGroupTools(server, callApi) {
3518
3598
  "lexq_groups_reorder",
3519
3599
  {
3520
3600
  title: "Reorder Policy Groups",
3601
+ annotations: { readOnlyHint: false, destructiveHint: true },
3521
3602
  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.",
3522
3603
  inputSchema: z2.object({
3523
3604
  groupIds: z2.array(z2.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
@@ -3535,6 +3616,7 @@ function registerGroupTools(server, callApi) {
3535
3616
  "lexq_ab_test_start",
3536
3617
  {
3537
3618
  title: "Start A/B Test",
3619
+ annotations: { readOnlyHint: false, destructiveHint: false },
3538
3620
  description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. 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%.",
3539
3621
  inputSchema: z2.object({
3540
3622
  groupId: z2.string().uuid().describe("Policy group ID"),
@@ -3548,6 +3630,7 @@ function registerGroupTools(server, callApi) {
3548
3630
  "lexq_ab_test_stop",
3549
3631
  {
3550
3632
  title: "Stop A/B Test",
3633
+ annotations: { readOnlyHint: false, destructiveHint: true },
3551
3634
  description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
3552
3635
  inputSchema: z2.object({
3553
3636
  groupId: z2.string().uuid().describe("Policy group ID")
@@ -3559,6 +3642,7 @@ function registerGroupTools(server, callApi) {
3559
3642
  "lexq_ab_test_adjust",
3560
3643
  {
3561
3644
  title: "Adjust A/B Test",
3645
+ annotations: { readOnlyHint: false, destructiveHint: true },
3562
3646
  description: "Adjust traffic rate of a running A/B test.",
3563
3647
  inputSchema: z2.object({
3564
3648
  groupId: z2.string().uuid().describe("Policy group ID"),
@@ -3576,6 +3660,7 @@ function registerVersionTools(server, callApi) {
3576
3660
  "lexq_versions_list",
3577
3661
  {
3578
3662
  title: "List Policy Versions",
3663
+ annotations: { readOnlyHint: true },
3579
3664
  description: "List all versions of a policy group.",
3580
3665
  inputSchema: z3.object({
3581
3666
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3591,6 +3676,7 @@ function registerVersionTools(server, callApi) {
3591
3676
  "lexq_versions_get",
3592
3677
  {
3593
3678
  title: "Get Policy Version",
3679
+ annotations: { readOnlyHint: true },
3594
3680
  description: "Get a single version by ID, including its rules and fact requirements.",
3595
3681
  inputSchema: z3.object({
3596
3682
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3603,6 +3689,7 @@ function registerVersionTools(server, callApi) {
3603
3689
  "lexq_versions_create",
3604
3690
  {
3605
3691
  title: "Create Policy Version",
3692
+ annotations: { readOnlyHint: false, destructiveHint: false },
3606
3693
  description: "Create a new DRAFT version in a policy group. Optionally provide a commit message and effective date range.",
3607
3694
  inputSchema: z3.object({
3608
3695
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3617,6 +3704,7 @@ function registerVersionTools(server, callApi) {
3617
3704
  "lexq_versions_update",
3618
3705
  {
3619
3706
  title: "Update Policy Version",
3707
+ annotations: { readOnlyHint: false, destructiveHint: true },
3620
3708
  description: "Update a DRAFT version. Only DRAFT versions can be modified. Only provided fields are changed.",
3621
3709
  inputSchema: z3.object({
3622
3710
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3632,6 +3720,7 @@ function registerVersionTools(server, callApi) {
3632
3720
  "lexq_versions_delete",
3633
3721
  {
3634
3722
  title: "Delete Policy Version",
3723
+ annotations: { readOnlyHint: false, destructiveHint: true },
3635
3724
  description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
3636
3725
  inputSchema: z3.object({
3637
3726
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3644,6 +3733,7 @@ function registerVersionTools(server, callApi) {
3644
3733
  "lexq_versions_clone",
3645
3734
  {
3646
3735
  title: "Clone Policy Version",
3736
+ annotations: { readOnlyHint: false, destructiveHint: false },
3647
3737
  description: "Clone an existing version to create a new DRAFT. Useful when the source version is already published.",
3648
3738
  inputSchema: z3.object({
3649
3739
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3666,6 +3756,7 @@ function registerRuleTools(server, callApi) {
3666
3756
  "lexq_rules_list",
3667
3757
  {
3668
3758
  title: "List Rules",
3759
+ annotations: { readOnlyHint: true },
3669
3760
  description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
3670
3761
  inputSchema: z4.object({
3671
3762
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3678,6 +3769,7 @@ function registerRuleTools(server, callApi) {
3678
3769
  "lexq_rules_get",
3679
3770
  {
3680
3771
  title: "Get Rule Detail",
3772
+ annotations: { readOnlyHint: true },
3681
3773
  description: "Get full rule detail including condition tree and action definitions.",
3682
3774
  inputSchema: z4.object({
3683
3775
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3691,6 +3783,7 @@ function registerRuleTools(server, callApi) {
3691
3783
  "lexq_rules_create",
3692
3784
  {
3693
3785
  title: "Create Rule",
3786
+ annotations: { readOnlyHint: false, destructiveHint: false },
3694
3787
  description: dedent15`
3695
3788
  Create a rule in a DRAFT version. Requires name, condition tree, and actions array. priority is auto-assigned (appended last); use lexq_rules_reorder to change order.
3696
3789
 
@@ -3769,6 +3862,7 @@ function registerRuleTools(server, callApi) {
3769
3862
  "lexq_rules_update",
3770
3863
  {
3771
3864
  title: "Update Rule",
3865
+ annotations: { readOnlyHint: false, destructiveHint: true },
3772
3866
  description: "Update an existing rule in a DRAFT version. Only provided fields are changed.",
3773
3867
  inputSchema: z4.object({
3774
3868
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3790,6 +3884,7 @@ function registerRuleTools(server, callApi) {
3790
3884
  "lexq_rules_delete",
3791
3885
  {
3792
3886
  title: "Delete Rule",
3887
+ annotations: { readOnlyHint: false, destructiveHint: true },
3793
3888
  description: "Delete a rule from a DRAFT version.",
3794
3889
  inputSchema: z4.object({
3795
3890
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3810,6 +3905,7 @@ function registerRuleTools(server, callApi) {
3810
3905
  "lexq_rules_reorder",
3811
3906
  {
3812
3907
  title: "Reorder Rules",
3908
+ annotations: { readOnlyHint: false, destructiveHint: true },
3813
3909
  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).",
3814
3910
  inputSchema: z4.object({
3815
3911
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3831,6 +3927,7 @@ function registerRuleTools(server, callApi) {
3831
3927
  "lexq_rules_toggle",
3832
3928
  {
3833
3929
  title: "Toggle Rule",
3930
+ annotations: { readOnlyHint: false, destructiveHint: true },
3834
3931
  description: "Enable or disable a rule without deleting it.",
3835
3932
  inputSchema: z4.object({
3836
3933
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3858,6 +3955,7 @@ function registerFactTools(server, callApi) {
3858
3955
  "lexq_facts_list",
3859
3956
  {
3860
3957
  title: "List Fact Definitions",
3958
+ annotations: { readOnlyHint: true },
3861
3959
  description: "List all fact definitions (input variable schema). Shows key, type, required, and PII status. Always check this before creating rules.",
3862
3960
  inputSchema: z5.object({
3863
3961
  page: z5.number().int().min(0).default(0).describe("Page number"),
@@ -3875,6 +3973,7 @@ function registerFactTools(server, callApi) {
3875
3973
  "lexq_facts_create",
3876
3974
  {
3877
3975
  title: "Create Fact Definition",
3976
+ annotations: { readOnlyHint: false, destructiveHint: false },
3878
3977
  description: "Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER.",
3879
3978
  inputSchema: z5.object({
3880
3979
  key: z5.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
@@ -3884,6 +3983,15 @@ function registerFactTools(server, callApi) {
3884
3983
  isRequired: z5.boolean().default(false).describe("Whether this fact is required for rule evaluation"),
3885
3984
  isPii: z5.boolean().default(false).describe(
3886
3985
  "Mark as PII \u2014 masked on every read surface, revealable only in the console (audited)"
3986
+ ),
3987
+ valueDomain: z5.object({
3988
+ allowedValues: z5.array(z5.union([z5.string(), z5.number()])).optional().describe(
3989
+ "Accepted values. For list types the constraint applies to each element, not to the list as a whole."
3990
+ ),
3991
+ min: z5.number().optional().describe("Lower bound, inclusive. Numeric types only."),
3992
+ max: z5.number().optional().describe("Upper bound, inclusive. Numeric types only.")
3993
+ }).optional().describe(
3994
+ "Which values this fact accepts. Omit for no constraint. STRING and LIST_STRING take allowedValues only; NUMBER and LIST_NUMBER take all three; BOOLEAN takes none. Declaring it lets the rule editor offer a dropdown and rejects a rule literal that could never match."
3887
3995
  )
3888
3996
  })
3889
3997
  },
@@ -3893,6 +4001,7 @@ function registerFactTools(server, callApi) {
3893
4001
  "lexq_facts_update",
3894
4002
  {
3895
4003
  title: "Update Fact Definition",
4004
+ annotations: { readOnlyHint: false, destructiveHint: true },
3896
4005
  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. Only the fields you send are changed. System facts accept name, description, and PII only.",
3897
4006
  inputSchema: z5.object({
3898
4007
  factId: z5.string().uuid().describe("Fact definition ID"),
@@ -3902,7 +4011,16 @@ function registerFactTools(server, callApi) {
3902
4011
  "Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule references the fact."
3903
4012
  ),
3904
4013
  isRequired: z5.boolean().optional().describe("Required flag"),
3905
- isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)")
4014
+ isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
4015
+ valueDomain: z5.object({
4016
+ allowedValues: z5.array(z5.union([z5.string(), z5.number()])).optional().describe(
4017
+ "Accepted values. For list types the constraint applies to each element, not to the list as a whole."
4018
+ ),
4019
+ min: z5.number().optional().describe("Lower bound, inclusive. Numeric types only."),
4020
+ max: z5.number().optional().describe("Upper bound, inclusive. Numeric types only.")
4021
+ }).optional().describe(
4022
+ "Which values this fact accepts. Three states: omit the field to leave the constraint alone, send {} to remove it, or send a populated object to replace it. Narrowing it does not rewrite rules that already reference this fact \u2014 they keep working, and the next save is judged against the new constraint."
4023
+ )
3906
4024
  })
3907
4025
  },
3908
4026
  async ({ factId, ...body }) => callApi("PUT", `schema/facts/${factId}`, { body })
@@ -3911,6 +4029,7 @@ function registerFactTools(server, callApi) {
3911
4029
  "lexq_facts_delete",
3912
4030
  {
3913
4031
  title: "Delete Fact Definition",
4032
+ annotations: { readOnlyHint: false, destructiveHint: true },
3914
4033
  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. Remove the references first.",
3915
4034
  inputSchema: z5.object({
3916
4035
  factId: z5.string().uuid().describe("Fact definition ID")
@@ -3922,6 +4041,7 @@ function registerFactTools(server, callApi) {
3922
4041
  "lexq_facts_action_metadata",
3923
4042
  {
3924
4043
  title: "Get Action Runtime Fact Metadata",
4044
+ annotations: { readOnlyHint: true },
3925
4045
  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. Facts are supplied as input or written by a prior action in the same rule; only SET_FACT creates a fact from nothing. Static data, safe to cache in-session.",
3926
4046
  inputSchema: z5.object({})
3927
4047
  },
@@ -3931,6 +4051,7 @@ function registerFactTools(server, callApi) {
3931
4051
  "lexq_facts_unregistered",
3932
4052
  {
3933
4053
  title: "List Unregistered Facts",
4054
+ annotations: { readOnlyHint: true },
3934
4055
  description: "List facts referenced by a version's rules but not yet defined (read-only \u2014 does not block publish/deploy, INV-4). Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
3935
4056
  inputSchema: z5.object({
3936
4057
  groupId: z5.string().uuid().describe("Policy group ID"),
@@ -3943,6 +4064,7 @@ function registerFactTools(server, callApi) {
3943
4064
  "lexq_facts_export",
3944
4065
  {
3945
4066
  title: "Export Fact Catalog",
4067
+ annotations: { readOnlyHint: true },
3946
4068
  description: "Export the fact catalog. The two formats carry different things: CSV is the catalog as it stands, system facts included, for reading in a spreadsheet; JSON matches the shape that batch create accepts, so it can be fed straight back in, which is why it leaves out the fields that endpoint does not take. Returns the file contents as text.",
3947
4069
  inputSchema: z5.object({
3948
4070
  format: z5.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
@@ -3962,6 +4084,7 @@ function registerDeployTools(server, callApi) {
3962
4084
  "lexq_deploy_publish",
3963
4085
  {
3964
4086
  title: "Publish Version",
4087
+ annotations: { readOnlyHint: false, destructiveHint: false },
3965
4088
  description: "Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts referenced by rules do not block publishing (INV-4); call lexq_facts_unregistered first to review them.",
3966
4089
  inputSchema: z6.object({
3967
4090
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -3975,6 +4098,7 @@ function registerDeployTools(server, callApi) {
3975
4098
  "lexq_deploy_live",
3976
4099
  {
3977
4100
  title: "Deploy to Live",
4101
+ annotations: { readOnlyHint: false, destructiveHint: true },
3978
4102
  description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Versions whose effective start date has not arrived are rejected (P-037) \u2014 use lexq_deploy_schedule for those. Undefined facts do not block deployment (INV-4); use lexq_facts_unregistered to review what the version references but has not defined.",
3979
4103
  inputSchema: z6.object({
3980
4104
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -3990,6 +4114,7 @@ function registerDeployTools(server, callApi) {
3990
4114
  "lexq_deploy_rollback",
3991
4115
  {
3992
4116
  title: "Rollback Deployment",
4117
+ annotations: { readOnlyHint: false, destructiveHint: true },
3993
4118
  description: "Rollback to the previous deployed version. Only available if there is a previous version.",
3994
4119
  inputSchema: z6.object({
3995
4120
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -4004,6 +4129,7 @@ function registerDeployTools(server, callApi) {
4004
4129
  "lexq_deploy_undeploy",
4005
4130
  {
4006
4131
  title: "Undeploy",
4132
+ annotations: { readOnlyHint: false, destructiveHint: true },
4007
4133
  description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
4008
4134
  inputSchema: z6.object({
4009
4135
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -4018,6 +4144,7 @@ function registerDeployTools(server, callApi) {
4018
4144
  "lexq_deploy_schedule",
4019
4145
  {
4020
4146
  title: "Schedule Deployment",
4147
+ annotations: { readOnlyHint: false, destructiveHint: false },
4021
4148
  description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it. The snapshot hash is sealed at scheduling and re-verified at execution (fail-closed).",
4022
4149
  inputSchema: z6.object({
4023
4150
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -4033,6 +4160,7 @@ function registerDeployTools(server, callApi) {
4033
4160
  "lexq_deploy_unschedule",
4034
4161
  {
4035
4162
  title: "Cancel Scheduled Deployment",
4163
+ annotations: { readOnlyHint: false, destructiveHint: true },
4036
4164
  description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
4037
4165
  inputSchema: z6.object({
4038
4166
  groupId: z6.string().uuid().describe("Policy group ID")
@@ -4044,6 +4172,7 @@ function registerDeployTools(server, callApi) {
4044
4172
  "lexq_deploy_schedules",
4045
4173
  {
4046
4174
  title: "List Scheduled Deployments",
4175
+ annotations: { readOnlyHint: true },
4047
4176
  description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
4048
4177
  inputSchema: z6.object({
4049
4178
  page: z6.number().int().min(0).default(0).describe("Page number"),
@@ -4056,6 +4185,7 @@ function registerDeployTools(server, callApi) {
4056
4185
  "lexq_deploy_history",
4057
4186
  {
4058
4187
  title: "Deployment History",
4188
+ annotations: { readOnlyHint: true },
4059
4189
  description: "List deployment history across all groups.",
4060
4190
  inputSchema: z6.object({
4061
4191
  page: z6.number().int().min(0).default(0).describe("Page number"),
@@ -4079,6 +4209,7 @@ function registerDeployTools(server, callApi) {
4079
4209
  "lexq_deploy_detail",
4080
4210
  {
4081
4211
  title: "Deployment Detail",
4212
+ annotations: { readOnlyHint: true },
4082
4213
  description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
4083
4214
  inputSchema: z6.object({
4084
4215
  deploymentId: z6.string().uuid().describe("Deployment ID")
@@ -4090,6 +4221,7 @@ function registerDeployTools(server, callApi) {
4090
4221
  "lexq_deploy_overview",
4091
4222
  {
4092
4223
  title: "Deployment Overview",
4224
+ annotations: { readOnlyHint: true },
4093
4225
  description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer.",
4094
4226
  inputSchema: z6.object({})
4095
4227
  },
@@ -4099,6 +4231,7 @@ function registerDeployTools(server, callApi) {
4099
4231
  "lexq_deploy_deployable",
4100
4232
  {
4101
4233
  title: "List Deployable Versions",
4234
+ annotations: { readOnlyHint: true },
4102
4235
  description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
4103
4236
  inputSchema: z6.object({
4104
4237
  groupId: z6.string().uuid().describe("Policy group ID")
@@ -4110,6 +4243,7 @@ function registerDeployTools(server, callApi) {
4110
4243
  "lexq_deploy_diff",
4111
4244
  {
4112
4245
  title: "Deployment Diff",
4246
+ annotations: { readOnlyHint: true },
4113
4247
  description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
4114
4248
  inputSchema: z6.object({
4115
4249
  baseVersionId: z6.string().uuid().describe("Base version ID (typically the current live)"),
@@ -4130,6 +4264,7 @@ function registerAnalyticsTools(server, callApi) {
4130
4264
  "lexq_dry_run",
4131
4265
  {
4132
4266
  title: "Dry Run",
4267
+ annotations: { readOnlyHint: true },
4133
4268
  description: dedent16`
4134
4269
  Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
4135
4270
 
@@ -4159,6 +4294,7 @@ function registerAnalyticsTools(server, callApi) {
4159
4294
  "lexq_dry_run_compare",
4160
4295
  {
4161
4296
  title: "Dry Run Compare",
4297
+ annotations: { readOnlyHint: true },
4162
4298
  description: dedent16`
4163
4299
  Compare dry run results between two versions using the same input facts. Useful for validating changes.
4164
4300
 
@@ -4183,6 +4319,7 @@ function registerAnalyticsTools(server, callApi) {
4183
4319
  "lexq_requirements",
4184
4320
  {
4185
4321
  title: "Analyze Requirements",
4322
+ annotations: { readOnlyHint: true },
4186
4323
  description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
4187
4324
  inputSchema: z7.object({
4188
4325
  groupId: z7.string().uuid().describe("Policy group ID"),
@@ -4195,6 +4332,7 @@ function registerAnalyticsTools(server, callApi) {
4195
4332
  "lexq_simulation_start",
4196
4333
  {
4197
4334
  title: "Start Simulation",
4335
+ annotations: { readOnlyHint: false, destructiveHint: false },
4198
4336
  description: dedent16`
4199
4337
  Start an Impact Simulation against historical, uploaded, or inline data.
4200
4338
 
@@ -4236,6 +4374,7 @@ function registerAnalyticsTools(server, callApi) {
4236
4374
  "lexq_simulation_status",
4237
4375
  {
4238
4376
  title: "Simulation Status",
4377
+ annotations: { readOnlyHint: true },
4239
4378
  description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
4240
4379
  inputSchema: z7.object({
4241
4380
  simulationId: z7.string().uuid().describe("Simulation ID")
@@ -4247,6 +4386,7 @@ function registerAnalyticsTools(server, callApi) {
4247
4386
  "lexq_simulation_list",
4248
4387
  {
4249
4388
  title: "List Simulations",
4389
+ annotations: { readOnlyHint: true },
4250
4390
  description: "List simulation history with optional filters.",
4251
4391
  inputSchema: z7.object({
4252
4392
  page: z7.number().int().min(0).default(0).describe("Page number"),
@@ -4268,6 +4408,7 @@ function registerAnalyticsTools(server, callApi) {
4268
4408
  "lexq_simulation_cancel",
4269
4409
  {
4270
4410
  title: "Cancel Simulation",
4411
+ annotations: { readOnlyHint: false, destructiveHint: true },
4271
4412
  description: "Cancel a running or pending simulation.",
4272
4413
  inputSchema: z7.object({
4273
4414
  simulationId: z7.string().uuid().describe("Simulation ID")
@@ -4279,6 +4420,7 @@ function registerAnalyticsTools(server, callApi) {
4279
4420
  "lexq_simulation_export",
4280
4421
  {
4281
4422
  title: "Export Simulation",
4423
+ annotations: { readOnlyHint: true },
4282
4424
  description: "Export simulation results as JSON or CSV. Returns the raw data.",
4283
4425
  inputSchema: z7.object({
4284
4426
  simulationId: z7.string().uuid().describe("Simulation ID"),
@@ -4293,6 +4435,7 @@ function registerAnalyticsTools(server, callApi) {
4293
4435
  "lexq_dataset_upload",
4294
4436
  {
4295
4437
  title: "Upload Dataset",
4438
+ annotations: { readOnlyHint: false, destructiveHint: false },
4296
4439
  description: dedent16`
4297
4440
  Upload inline CSV or JSON content as a simulation dataset.
4298
4441
  The content is uploaded to S3 and a path is returned in the "path" field.
@@ -4336,6 +4479,7 @@ function registerAnalyticsTools(server, callApi) {
4336
4479
  "lexq_dataset_template",
4337
4480
  {
4338
4481
  title: "Download Dataset Template",
4482
+ annotations: { readOnlyHint: true },
4339
4483
  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.",
4340
4484
  inputSchema: z7.object({
4341
4485
  groupId: z7.string().uuid().describe("Policy group ID"),
@@ -4365,6 +4509,7 @@ function registerProfileTools(server, callApi) {
4365
4509
  "lexq_profile_overview",
4366
4510
  {
4367
4511
  title: "Group Latency Profile",
4512
+ annotations: { readOnlyHint: true },
4368
4513
  description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 6, p95 n \u2265 60, p99 n \u2265 300 \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
4369
4514
  inputSchema: z8.object({
4370
4515
  groupId: z8.string().uuid().describe("Policy group ID"),
@@ -4384,6 +4529,7 @@ function registerProfileTools(server, callApi) {
4384
4529
  "lexq_profile_rule",
4385
4530
  {
4386
4531
  title: "Rule Latency Detail",
4532
+ annotations: { readOnlyHint: true },
4387
4533
  description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265 6, p95 n \u2265 60, p99 n \u2265 300). ` + RELATIVE_THRESHOLD,
4388
4534
  inputSchema: z8.object({
4389
4535
  groupId: z8.string().uuid().describe("Policy group ID"),
@@ -4408,6 +4554,7 @@ function registerReplayTools(server, callApi) {
4408
4554
  "lexq_replay_decision",
4409
4555
  {
4410
4556
  title: "Replay a Decision",
4557
+ annotations: { readOnlyHint: true },
4411
4558
  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.",
4412
4559
  inputSchema: z9.object({
4413
4560
  traceId: z9.string().describe("Trace ID of the past execution to replay"),
@@ -4420,6 +4567,7 @@ function registerReplayTools(server, callApi) {
4420
4567
  "lexq_replay_start",
4421
4568
  {
4422
4569
  title: "Start Window Replay (Blast Radius)",
4570
+ annotations: { readOnlyHint: false, destructiveHint: false },
4423
4571
  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.",
4424
4572
  inputSchema: z9.object({
4425
4573
  candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against"),
@@ -4436,6 +4584,7 @@ function registerReplayTools(server, callApi) {
4436
4584
  "lexq_replay_status",
4437
4585
  {
4438
4586
  title: "Get Replay Job Status",
4587
+ annotations: { readOnlyHint: true },
4439
4588
  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.",
4440
4589
  inputSchema: z9.object({
4441
4590
  jobId: z9.string().describe("Replay job ID from lexq_replay_start")
@@ -4447,6 +4596,7 @@ function registerReplayTools(server, callApi) {
4447
4596
  "lexq_replay_list",
4448
4597
  {
4449
4598
  title: "List Replay Jobs",
4599
+ annotations: { readOnlyHint: true },
4450
4600
  description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
4451
4601
  inputSchema: z9.object({
4452
4602
  page: z9.number().int().min(0).default(0).describe("Page number"),
@@ -4459,6 +4609,7 @@ function registerReplayTools(server, callApi) {
4459
4609
  "lexq_replay_cancel",
4460
4610
  {
4461
4611
  title: "Cancel Replay Job",
4612
+ annotations: { readOnlyHint: false, destructiveHint: true },
4462
4613
  description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
4463
4614
  inputSchema: z9.object({
4464
4615
  jobId: z9.string().describe("Replay job ID")
@@ -4470,6 +4621,7 @@ function registerReplayTools(server, callApi) {
4470
4621
  "lexq_replay_export",
4471
4622
  {
4472
4623
  title: "Export Replay Result",
4624
+ annotations: { readOnlyHint: true },
4473
4625
  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.",
4474
4626
  inputSchema: z9.object({
4475
4627
  jobId: z9.string().describe("Replay job ID"),
@@ -4487,6 +4639,7 @@ function registerHistoryTools(server, callApi) {
4487
4639
  "lexq_history_list",
4488
4640
  {
4489
4641
  title: "List Execution History",
4642
+ annotations: { readOnlyHint: true },
4490
4643
  description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
4491
4644
  inputSchema: z10.object({
4492
4645
  page: z10.number().int().min(0).default(0).describe("Page number"),
@@ -4514,6 +4667,7 @@ function registerHistoryTools(server, callApi) {
4514
4667
  "lexq_history_get",
4515
4668
  {
4516
4669
  title: "Get Execution Detail",
4670
+ annotations: { readOnlyHint: true },
4517
4671
  description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
4518
4672
  inputSchema: z10.object({
4519
4673
  traceId: z10.string().describe("Trace ID from execution history")
@@ -4525,6 +4679,7 @@ function registerHistoryTools(server, callApi) {
4525
4679
  "lexq_history_stats",
4526
4680
  {
4527
4681
  title: "Execution Statistics",
4682
+ annotations: { readOnlyHint: true },
4528
4683
  description: "Get execution KPIs: total executions, success/failure counts, success rate, and average latency.",
4529
4684
  inputSchema: z10.object({
4530
4685
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
@@ -4549,6 +4704,7 @@ function registerProvenanceTools(server, callApi) {
4549
4704
  "lexq_provenance_get",
4550
4705
  {
4551
4706
  title: "Get Decision Provenance",
4707
+ annotations: { readOnlyHint: true },
4552
4708
  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.",
4553
4709
  inputSchema: z11.object({
4554
4710
  traceId: z11.string().describe("Trace ID of the execution")
@@ -4560,6 +4716,7 @@ function registerProvenanceTools(server, callApi) {
4560
4716
  "lexq_pii_reveals_list",
4561
4717
  {
4562
4718
  title: "List PII Reveal Audits",
4719
+ annotations: { readOnlyHint: true },
4563
4720
  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.",
4564
4721
  inputSchema: z11.object({
4565
4722
  page: z11.number().int().min(0).default(0).describe("Page number"),
@@ -4590,6 +4747,7 @@ function registerLogTools(server, callApi) {
4590
4747
  "lexq_logs_list",
4591
4748
  {
4592
4749
  title: "List Failure Logs",
4750
+ annotations: { readOnlyHint: true },
4593
4751
  description: "List system failure logs from background tasks (platform event webhooks, scheduled deployments).",
4594
4752
  inputSchema: z12.object({
4595
4753
  page: z12.number().int().min(0).default(0).describe("Page number"),
@@ -4615,6 +4773,7 @@ function registerLogTools(server, callApi) {
4615
4773
  "lexq_logs_get",
4616
4774
  {
4617
4775
  title: "Get Failure Log",
4776
+ annotations: { readOnlyHint: true },
4618
4777
  description: "Get failure log detail by ID.",
4619
4778
  inputSchema: z12.object({
4620
4779
  logId: z12.string().uuid().describe("Failure log ID")
@@ -4626,6 +4785,7 @@ function registerLogTools(server, callApi) {
4626
4785
  "lexq_logs_action",
4627
4786
  {
4628
4787
  title: "Process Failure Log",
4788
+ annotations: { readOnlyHint: false, destructiveHint: false },
4629
4789
  description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
4630
4790
  inputSchema: z12.object({
4631
4791
  logId: z12.string().uuid().describe("Failure log ID"),
@@ -4640,6 +4800,7 @@ function registerLogTools(server, callApi) {
4640
4800
  "lexq_logs_bulk_action",
4641
4801
  {
4642
4802
  title: "Bulk Process Failure Logs",
4803
+ annotations: { readOnlyHint: false, destructiveHint: false },
4643
4804
  description: "Process multiple failure logs at once. Provide an array of log IDs and the action.",
4644
4805
  inputSchema: z12.object({
4645
4806
  logIds: z12.array(z12.string().uuid()).describe("Array of failure log IDs"),
@@ -4659,6 +4820,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4659
4820
  "lexq_webhook_subscriptions_list",
4660
4821
  {
4661
4822
  title: "List Webhook Subscriptions",
4823
+ annotations: { readOnlyHint: true },
4662
4824
  description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications (publish, deploy, rollback, undeploy).",
4663
4825
  inputSchema: z13.object({
4664
4826
  page: z13.number().int().min(0).default(0).describe("Page number"),
@@ -4674,6 +4836,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4674
4836
  "lexq_webhook_subscriptions_get",
4675
4837
  {
4676
4838
  title: "Get Webhook Subscription",
4839
+ annotations: { readOnlyHint: true },
4677
4840
  description: "Get webhook subscription detail by ID.",
4678
4841
  inputSchema: z13.object({
4679
4842
  id: z13.string().uuid().describe("Webhook subscription ID")
@@ -4685,6 +4848,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4685
4848
  "lexq_webhook_subscriptions_save",
4686
4849
  {
4687
4850
  title: "Save Webhook Subscription",
4851
+ annotations: { readOnlyHint: false, destructiveHint: true },
4688
4852
  description: 'Create or update a webhook subscription. Omit id to create, provide id to update. Events: VERSION_PUBLISHED, DEPLOYED, ROLLED_BACK, UNDEPLOYED. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).',
4689
4853
  inputSchema: z13.object({
4690
4854
  id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
@@ -4702,6 +4866,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4702
4866
  "lexq_webhook_subscriptions_delete",
4703
4867
  {
4704
4868
  title: "Delete Webhook Subscription",
4869
+ annotations: { readOnlyHint: false, destructiveHint: true },
4705
4870
  description: "Delete a webhook subscription by ID.",
4706
4871
  inputSchema: z13.object({
4707
4872
  id: z13.string().uuid().describe("Webhook subscription ID")
@@ -4713,6 +4878,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4713
4878
  "lexq_webhook_subscriptions_test",
4714
4879
  {
4715
4880
  title: "Test Webhook Subscription",
4881
+ annotations: { readOnlyHint: false, destructiveHint: false },
4716
4882
  description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
4717
4883
  inputSchema: z13.object({
4718
4884
  id: z13.string().uuid().describe("Webhook subscription ID")
@@ -4729,6 +4895,7 @@ function registerDomainTemplateTools(server, callApi) {
4729
4895
  "lexq_domain_templates_list",
4730
4896
  {
4731
4897
  title: "List Domain Templates",
4898
+ annotations: { readOnlyHint: true },
4732
4899
  description: "List all domain templates. A domain template is a curated, industry-specific starter pack of fact definitions and sample rules (e.g. ECOMMERCE). Each entry reports its key, status (ACTIVE or COMING_SOON), and a summary of what it provisions. Call this before preview or apply to discover which templates can currently be applied.",
4733
4900
  inputSchema: z14.object({})
4734
4901
  },
@@ -4738,6 +4905,7 @@ function registerDomainTemplateTools(server, callApi) {
4738
4905
  "lexq_domain_templates_preview",
4739
4906
  {
4740
4907
  title: "Preview Domain Template",
4908
+ annotations: { readOnlyHint: true },
4741
4909
  description: "Preview exactly what a domain template will provision before applying it: the fact definitions it registers, the sample rules it creates, and an apply plan. This is a read-only dry run \u2014 nothing is created. Only ACTIVE templates can be previewed.",
4742
4910
  inputSchema: z14.object({
4743
4911
  template: z14.string().describe(
@@ -4751,6 +4919,7 @@ function registerDomainTemplateTools(server, callApi) {
4751
4919
  "lexq_domain_templates_apply",
4752
4920
  {
4753
4921
  title: "Apply Domain Template",
4922
+ annotations: { readOnlyHint: false, destructiveHint: false },
4754
4923
  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. Existing facts are skipped \u2014 apply is additive and never overwrites existing schema. Run lexq_domain_templates_preview first to review what will be created. Only ACTIVE templates can be applied.",
4755
4924
  inputSchema: z14.object({
4756
4925
  template: z14.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),