@lexq/cli 1.1.4 → 1.1.6
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 +80 -27
- package/dist/mcp/register.d.ts +25 -1
- package/dist/mcp/register.js +50 -9
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1234,6 +1234,46 @@ ${data.length} total`);
|
|
|
1234
1234
|
import "commander";
|
|
1235
1235
|
import dedent6 from "dedent";
|
|
1236
1236
|
|
|
1237
|
+
// src/lib/value-domain-warning.ts
|
|
1238
|
+
function formatValueDomainMismatchWarning(meta) {
|
|
1239
|
+
const mismatch = meta?.valueDomainMismatch;
|
|
1240
|
+
if (!mismatch || !Array.isArray(mismatch.versions) || mismatch.versions.length === 0) return null;
|
|
1241
|
+
const n = mismatch.totalRuleCount;
|
|
1242
|
+
const subject = n === 1 ? "1 rule compares" : `${n} rules compare`;
|
|
1243
|
+
const lines = [
|
|
1244
|
+
`\u26A0 ${subject} ${mismatch.factKey} with values the new value domain does not accept. The update was still saved.`
|
|
1245
|
+
];
|
|
1246
|
+
for (const version of mismatch.versions) {
|
|
1247
|
+
lines.push(...versionLines(mismatch.factKey, version));
|
|
1248
|
+
}
|
|
1249
|
+
return lines.join("\n");
|
|
1250
|
+
}
|
|
1251
|
+
function versionLines(factKey, version) {
|
|
1252
|
+
const draft = version.versionStatus === "DRAFT";
|
|
1253
|
+
const lines = [
|
|
1254
|
+
` ${quoted(version.groupName)} v${version.versionNo}, ${draft ? "draft" : "deployed"} (groupId ${version.groupId}, versionId ${version.versionId})`,
|
|
1255
|
+
draft ? " Dry runs of this draft already reject calls that send these values (P-052), and live calls will too once it is published and deployed. Saving one of these rules again is refused (P-051) until each listed condition is edited or removed; first check whether each one is still needed." : " Calls to it are checked against the fact definitions captured when it was published, so this change does not affect them. A version cloned from it carries these rules; fix them in the clone before publishing it."
|
|
1256
|
+
];
|
|
1257
|
+
for (const rule of version.rules) {
|
|
1258
|
+
const conditions = rule.conditions.map(
|
|
1259
|
+
(c) => `${factKey} ${c.operator} ${escapeUnsafe(stringifyJson(c.value) ?? String(c.value))}`
|
|
1260
|
+
).join("; ");
|
|
1261
|
+
lines.push(` \u2022 ${quoted(rule.ruleName)} (ruleId ${rule.ruleId}): ${conditions}`);
|
|
1262
|
+
}
|
|
1263
|
+
const hidden = version.ruleCount - version.rules.length;
|
|
1264
|
+
if (hidden > 0) {
|
|
1265
|
+
lines.push(` \u2026 and ${hidden} more rule${hidden === 1 ? "" : "s"} in this version.`);
|
|
1266
|
+
}
|
|
1267
|
+
return lines;
|
|
1268
|
+
}
|
|
1269
|
+
function quoted(name) {
|
|
1270
|
+
return escapeUnsafe(stringifyJson(String(name)) ?? '""');
|
|
1271
|
+
}
|
|
1272
|
+
var UNSAFE = /[\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
|
|
1273
|
+
function escapeUnsafe(json) {
|
|
1274
|
+
return json.replace(UNSAFE, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1237
1277
|
// src/lib/export.ts
|
|
1238
1278
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
1239
1279
|
|
|
@@ -1420,8 +1460,8 @@ ${data.length} unregistered`);
|
|
|
1420
1460
|
the rule editor into a dropdown instead of a free text box.
|
|
1421
1461
|
STRING types take --allowed-values. NUMBER types take all three.
|
|
1422
1462
|
BOOLEAN takes none — it has only two values to begin with.
|
|
1423
|
-
For list types the
|
|
1424
|
-
Leave them out for no
|
|
1463
|
+
For list types the value domain applies to each element.
|
|
1464
|
+
Leave them out for no value domain, which is how facts behaved before.
|
|
1425
1465
|
`
|
|
1426
1466
|
).action(async (opts) => {
|
|
1427
1467
|
try {
|
|
@@ -1440,7 +1480,7 @@ ${data.length} unregistered`);
|
|
|
1440
1480
|
process.exit(1);
|
|
1441
1481
|
}
|
|
1442
1482
|
});
|
|
1443
|
-
facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <n>", "Display name").option("--description <desc>",
|
|
1483
|
+
facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <n>", "Display name").option("--description <desc>", 'Description (pass "" to remove it)').option("--required", "Mark as required").option("--no-required", "Mark as not required").option("--pii", "Mark as PII (enables masking)").option("--no-pii", "Unmark as PII (disables masking \u2014 value becomes visible)").option("--type <type>", "Value type: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER").option("--allowed-values <csv>", "Comma-separated list of accepted values").option("--min <number>", "Lower bound, inclusive (numeric types only)").option("--max <number>", "Upper bound, inclusive (numeric types only)").option("--clear-value-domain", "Remove the value domain entirely").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
|
|
1444
1484
|
"after",
|
|
1445
1485
|
dedent6`
|
|
1446
1486
|
|
|
@@ -1453,10 +1493,11 @@ ${data.length} unregistered`);
|
|
|
1453
1493
|
$ lexq facts update --id <factId> --clear-value-domain
|
|
1454
1494
|
|
|
1455
1495
|
Only the flags you pass are changed. Leaving out --allowed-values, --min, and --max
|
|
1456
|
-
leaves the
|
|
1496
|
+
leaves the value domain alone; --clear-value-domain removes it.
|
|
1457
1497
|
|
|
1458
|
-
Narrowing
|
|
1459
|
-
|
|
1498
|
+
Narrowing the value domain never blocks the update and does not rewrite rules. When rules
|
|
1499
|
+
compare this fact with a value the new value domain excludes, a warning on stderr lists them
|
|
1500
|
+
by version: drafts and the versions currently deployed.
|
|
1460
1501
|
|
|
1461
1502
|
Pass --type when the accepted values are numbers and you are not also naming the type,
|
|
1462
1503
|
so "10" is sent as a number rather than a string. --json settles it either way.
|
|
@@ -1465,14 +1506,22 @@ ${data.length} unregistered`);
|
|
|
1465
1506
|
try {
|
|
1466
1507
|
const globalOpts = program.opts();
|
|
1467
1508
|
const body = opts.json ? JSON.parse(opts.json) : buildUpdateBody2(opts);
|
|
1468
|
-
const data = await
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1509
|
+
const { data, meta } = await apiRequestWithMeta(
|
|
1510
|
+
"PUT",
|
|
1511
|
+
`schema/facts/${opts.id}`,
|
|
1512
|
+
{
|
|
1513
|
+
apiKey: globalOpts.apiKey,
|
|
1514
|
+
baseUrl: globalOpts.baseUrl,
|
|
1515
|
+
dryRun: globalOpts.dryRun,
|
|
1516
|
+
verbose: globalOpts.verbose,
|
|
1517
|
+
body
|
|
1518
|
+
}
|
|
1519
|
+
);
|
|
1475
1520
|
printJson(data);
|
|
1521
|
+
const warning = formatValueDomainMismatchWarning(meta);
|
|
1522
|
+
if (warning) console.error(`
|
|
1523
|
+
${warning}
|
|
1524
|
+
`);
|
|
1476
1525
|
} catch (error) {
|
|
1477
1526
|
printError(error);
|
|
1478
1527
|
process.exit(1);
|
|
@@ -1583,7 +1632,7 @@ function buildUpdateBody2(opts) {
|
|
|
1583
1632
|
if (opts.description !== void 0) body.description = opts.description;
|
|
1584
1633
|
if (opts.type) body.type = opts.type;
|
|
1585
1634
|
if (typeof opts.required === "boolean") body.isRequired = opts.required;
|
|
1586
|
-
if (typeof opts.
|
|
1635
|
+
if (typeof opts.pii === "boolean") body.isPii = opts.pii;
|
|
1587
1636
|
if (opts.clearValueDomain === true) {
|
|
1588
1637
|
if (hasDomainFlag(opts)) {
|
|
1589
1638
|
throw new Error(
|
|
@@ -3531,6 +3580,8 @@ function createCallApiFromConfig() {
|
|
|
3531
3580
|
];
|
|
3532
3581
|
const warning = formatUnregisteredFactWarning(meta);
|
|
3533
3582
|
if (warning) content.push({ type: "text", text: warning });
|
|
3583
|
+
const domainWarning = formatValueDomainMismatchWarning(meta);
|
|
3584
|
+
if (domainWarning) content.push({ type: "text", text: domainWarning });
|
|
3534
3585
|
return { content };
|
|
3535
3586
|
} catch (error) {
|
|
3536
3587
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -4100,12 +4151,12 @@ function registerFactTools(server, callApi) {
|
|
|
4100
4151
|
),
|
|
4101
4152
|
valueDomain: z5.object({
|
|
4102
4153
|
allowedValues: z5.array(z5.union([z5.string(), z5.number()])).optional().describe(
|
|
4103
|
-
"Accepted values. For list types the
|
|
4154
|
+
"Accepted values. For list types the value domain applies to each element, not to the list as a whole."
|
|
4104
4155
|
),
|
|
4105
4156
|
min: z5.number().optional().describe("Lower bound, inclusive. Numeric types only."),
|
|
4106
4157
|
max: z5.number().optional().describe("Upper bound, inclusive. Numeric types only.")
|
|
4107
4158
|
}).optional().describe(
|
|
4108
|
-
"Which values this fact accepts. Omit for no
|
|
4159
|
+
"Which values this fact accepts. Omit for no value domain. STRING and LIST_STRING take allowedValues only; NUMBER and LIST_NUMBER take all three; BOOLEAN takes none. Declaring it lets the rule editor offer a dropdown and rejects a rule literal that could never match."
|
|
4109
4160
|
)
|
|
4110
4161
|
})
|
|
4111
4162
|
},
|
|
@@ -4120,7 +4171,7 @@ function registerFactTools(server, callApi) {
|
|
|
4120
4171
|
inputSchema: z5.object({
|
|
4121
4172
|
factId: z5.string().uuid().describe("Fact definition ID"),
|
|
4122
4173
|
name: z5.string().optional().describe("Display name"),
|
|
4123
|
-
description: z5.string().optional().describe(
|
|
4174
|
+
description: z5.string().optional().describe('Description. Send "" to remove it.'),
|
|
4124
4175
|
type: z5.enum(VALUE_TYPES).optional().describe(
|
|
4125
4176
|
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
|
|
4126
4177
|
),
|
|
@@ -4128,12 +4179,12 @@ function registerFactTools(server, callApi) {
|
|
|
4128
4179
|
isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
|
|
4129
4180
|
valueDomain: z5.object({
|
|
4130
4181
|
allowedValues: z5.array(z5.union([z5.string(), z5.number()])).optional().describe(
|
|
4131
|
-
"Accepted values. For list types the
|
|
4182
|
+
"Accepted values. For list types the value domain applies to each element, not to the list as a whole."
|
|
4132
4183
|
),
|
|
4133
4184
|
min: z5.number().optional().describe("Lower bound, inclusive. Numeric types only."),
|
|
4134
4185
|
max: z5.number().optional().describe("Upper bound, inclusive. Numeric types only.")
|
|
4135
4186
|
}).optional().describe(
|
|
4136
|
-
"Which values this fact accepts. Three states: omit the field to leave the
|
|
4187
|
+
"Which values this fact accepts. Three states: omit the field to leave the value domain alone, send {} to remove it, or send a populated object to replace it. Narrowing it never blocks the update and does not rewrite rules. When rules compare this fact with a value the new value domain excludes, the result ends with a warning that lists them by version: drafts and the versions currently deployed."
|
|
4137
4188
|
)
|
|
4138
4189
|
})
|
|
4139
4190
|
},
|
|
@@ -5012,7 +5063,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5012
5063
|
{
|
|
5013
5064
|
title: "List Domain Templates",
|
|
5014
5065
|
annotations: { readOnlyHint: true },
|
|
5015
|
-
description: "List all domain templates. A domain template is a
|
|
5066
|
+
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.",
|
|
5016
5067
|
inputSchema: z14.object({})
|
|
5017
5068
|
},
|
|
5018
5069
|
async () => callApi("GET", "domain-templates")
|
|
@@ -5022,10 +5073,10 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5022
5073
|
{
|
|
5023
5074
|
title: "Preview Domain Template",
|
|
5024
5075
|
annotations: { readOnlyHint: true },
|
|
5025
|
-
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
|
|
5076
|
+
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.",
|
|
5026
5077
|
inputSchema: z14.object({
|
|
5027
5078
|
template: z14.string().describe(
|
|
5028
|
-
"Domain template key (e.g. ECOMMERCE). Use lexq_domain_templates_list to see
|
|
5079
|
+
"Domain template key (e.g. ECOMMERCE). Use lexq_domain_templates_list to see the keys."
|
|
5029
5080
|
)
|
|
5030
5081
|
})
|
|
5031
5082
|
},
|
|
@@ -5036,7 +5087,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5036
5087
|
{
|
|
5037
5088
|
title: "Apply Domain Template",
|
|
5038
5089
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5039
|
-
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.
|
|
5090
|
+
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.",
|
|
5040
5091
|
inputSchema: z14.object({
|
|
5041
5092
|
template: z14.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
|
|
5042
5093
|
customName: z14.string().optional().describe(
|
|
@@ -5153,7 +5204,7 @@ function registerDomainTemplateCommands(program) {
|
|
|
5153
5204
|
apply Apply a template to the current tenant
|
|
5154
5205
|
|
|
5155
5206
|
Workflow: list → preview → apply
|
|
5156
|
-
|
|
5207
|
+
Run "list" to see every template and whether it can be applied.
|
|
5157
5208
|
`
|
|
5158
5209
|
);
|
|
5159
5210
|
templates.command("list").description("List available domain templates").action(async () => {
|
|
@@ -5220,10 +5271,12 @@ function registerDomainTemplateCommands(program) {
|
|
|
5220
5271
|
dedent18`
|
|
5221
5272
|
|
|
5222
5273
|
Creates the template's fact definitions and a new DRAFT policy group
|
|
5223
|
-
populated with its sample rules.
|
|
5224
|
-
|
|
5274
|
+
populated with its sample rules. A fact that already exists with the
|
|
5275
|
+
same definition is kept as it is, so apply never overwrites your schema.
|
|
5225
5276
|
|
|
5226
|
-
Run "preview" first to review what will be created.
|
|
5277
|
+
Run "preview" first to review what will be created. If it lists
|
|
5278
|
+
factConflicts, apply fails with P-061 and creates nothing: delete those
|
|
5279
|
+
facts or change them to match the template first.
|
|
5227
5280
|
|
|
5228
5281
|
Example:
|
|
5229
5282
|
$ lexq domain-templates apply --template ECOMMERCE
|
package/dist/mcp/register.d.ts
CHANGED
|
@@ -37,6 +37,30 @@ declare function paginationParams(page?: number, size?: number): Record<string,
|
|
|
37
37
|
*/
|
|
38
38
|
declare function formatUnregisteredFactWarning(meta: unknown): string | null;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Builds the warning text for meta.valueDomainMismatch, which a fact update returns when its new
|
|
42
|
+
* value domain excludes values that existing rules compare against. Returns null when the
|
|
43
|
+
* response carries none.
|
|
44
|
+
*
|
|
45
|
+
* The CLI and the MCP server print the same text: `lexq facts update` writes it to stderr and the
|
|
46
|
+
* stdio MCP server appends it to the tool result. It is exported from `@lexq/cli/mcp` so a server
|
|
47
|
+
* that hosts these tools can append it too. The ids are included so an agent can go straight to
|
|
48
|
+
* the rule.
|
|
49
|
+
*
|
|
50
|
+
* The advice depends on the version. A draft is checked against the new value domain already: its
|
|
51
|
+
* dry runs reject these values now, and live calls will too once it is published and deployed. A
|
|
52
|
+
* deployed version keeps the fact definitions captured when it was published, so calls to it are
|
|
53
|
+
* unaffected, but a version cloned from it carries the rule. Values are printed as the rule holds
|
|
54
|
+
* them; this text does not judge whether a condition became always true or always false, so it
|
|
55
|
+
* does not say whether to change a value or remove the condition.
|
|
56
|
+
*
|
|
57
|
+
* Group and rule names and condition values are written by users, so they are printed as JSON
|
|
58
|
+
* strings with control characters (C0 and C1), line and paragraph separators, and text-direction
|
|
59
|
+
* controls escaped. They cannot drive the terminal, break the layout with a new line, reorder the
|
|
60
|
+
* text around them, or pass as part of the warning an agent reads.
|
|
61
|
+
*/
|
|
62
|
+
declare function formatValueDomainMismatchWarning(meta: unknown): string | null;
|
|
63
|
+
|
|
40
64
|
declare class ApiError extends Error {
|
|
41
65
|
readonly statusCode: number;
|
|
42
66
|
readonly errorCode: string | null;
|
|
@@ -73,4 +97,4 @@ declare function registerAllTools(server: McpServer, callApi: CallApi): void;
|
|
|
73
97
|
*/
|
|
74
98
|
declare const SERVER_INFO: Omit<Implementation, 'version'>;
|
|
75
99
|
|
|
76
|
-
export { ApiError, type CallApi, type McpToolResult, SERVER_INFO, formatUnregisteredFactWarning, isLosslessNumber, paginationParams, parseJson, registerAllTools, stringifyJson };
|
|
100
|
+
export { ApiError, type CallApi, type McpToolResult, SERVER_INFO, formatUnregisteredFactWarning, formatValueDomainMismatchWarning, isLosslessNumber, paginationParams, parseJson, registerAllTools, stringifyJson };
|
package/dist/mcp/register.js
CHANGED
|
@@ -253,6 +253,46 @@ var ApiError = class extends Error {
|
|
|
253
253
|
errorCode;
|
|
254
254
|
};
|
|
255
255
|
|
|
256
|
+
// src/lib/value-domain-warning.ts
|
|
257
|
+
function formatValueDomainMismatchWarning(meta) {
|
|
258
|
+
const mismatch = meta?.valueDomainMismatch;
|
|
259
|
+
if (!mismatch || !Array.isArray(mismatch.versions) || mismatch.versions.length === 0) return null;
|
|
260
|
+
const n = mismatch.totalRuleCount;
|
|
261
|
+
const subject = n === 1 ? "1 rule compares" : `${n} rules compare`;
|
|
262
|
+
const lines = [
|
|
263
|
+
`\u26A0 ${subject} ${mismatch.factKey} with values the new value domain does not accept. The update was still saved.`
|
|
264
|
+
];
|
|
265
|
+
for (const version of mismatch.versions) {
|
|
266
|
+
lines.push(...versionLines(mismatch.factKey, version));
|
|
267
|
+
}
|
|
268
|
+
return lines.join("\n");
|
|
269
|
+
}
|
|
270
|
+
function versionLines(factKey, version) {
|
|
271
|
+
const draft = version.versionStatus === "DRAFT";
|
|
272
|
+
const lines = [
|
|
273
|
+
` ${quoted(version.groupName)} v${version.versionNo}, ${draft ? "draft" : "deployed"} (groupId ${version.groupId}, versionId ${version.versionId})`,
|
|
274
|
+
draft ? " Dry runs of this draft already reject calls that send these values (P-052), and live calls will too once it is published and deployed. Saving one of these rules again is refused (P-051) until each listed condition is edited or removed; first check whether each one is still needed." : " Calls to it are checked against the fact definitions captured when it was published, so this change does not affect them. A version cloned from it carries these rules; fix them in the clone before publishing it."
|
|
275
|
+
];
|
|
276
|
+
for (const rule of version.rules) {
|
|
277
|
+
const conditions = rule.conditions.map(
|
|
278
|
+
(c) => `${factKey} ${c.operator} ${escapeUnsafe(stringifyJson(c.value) ?? String(c.value))}`
|
|
279
|
+
).join("; ");
|
|
280
|
+
lines.push(` \u2022 ${quoted(rule.ruleName)} (ruleId ${rule.ruleId}): ${conditions}`);
|
|
281
|
+
}
|
|
282
|
+
const hidden = version.ruleCount - version.rules.length;
|
|
283
|
+
if (hidden > 0) {
|
|
284
|
+
lines.push(` \u2026 and ${hidden} more rule${hidden === 1 ? "" : "s"} in this version.`);
|
|
285
|
+
}
|
|
286
|
+
return lines;
|
|
287
|
+
}
|
|
288
|
+
function quoted(name) {
|
|
289
|
+
return escapeUnsafe(stringifyJson(String(name)) ?? '""');
|
|
290
|
+
}
|
|
291
|
+
var UNSAFE = /[\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
|
|
292
|
+
function escapeUnsafe(json) {
|
|
293
|
+
return json.replace(UNSAFE, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
256
296
|
// src/mcp/tools/_shared.ts
|
|
257
297
|
function paginationParams(page, size) {
|
|
258
298
|
const params = {};
|
|
@@ -646,12 +686,12 @@ function registerFactTools(server, callApi) {
|
|
|
646
686
|
),
|
|
647
687
|
valueDomain: z5.object({
|
|
648
688
|
allowedValues: z5.array(z5.union([z5.string(), z5.number()])).optional().describe(
|
|
649
|
-
"Accepted values. For list types the
|
|
689
|
+
"Accepted values. For list types the value domain applies to each element, not to the list as a whole."
|
|
650
690
|
),
|
|
651
691
|
min: z5.number().optional().describe("Lower bound, inclusive. Numeric types only."),
|
|
652
692
|
max: z5.number().optional().describe("Upper bound, inclusive. Numeric types only.")
|
|
653
693
|
}).optional().describe(
|
|
654
|
-
"Which values this fact accepts. Omit for no
|
|
694
|
+
"Which values this fact accepts. Omit for no value domain. STRING and LIST_STRING take allowedValues only; NUMBER and LIST_NUMBER take all three; BOOLEAN takes none. Declaring it lets the rule editor offer a dropdown and rejects a rule literal that could never match."
|
|
655
695
|
)
|
|
656
696
|
})
|
|
657
697
|
},
|
|
@@ -666,7 +706,7 @@ function registerFactTools(server, callApi) {
|
|
|
666
706
|
inputSchema: z5.object({
|
|
667
707
|
factId: z5.string().uuid().describe("Fact definition ID"),
|
|
668
708
|
name: z5.string().optional().describe("Display name"),
|
|
669
|
-
description: z5.string().optional().describe(
|
|
709
|
+
description: z5.string().optional().describe('Description. Send "" to remove it.'),
|
|
670
710
|
type: z5.enum(VALUE_TYPES).optional().describe(
|
|
671
711
|
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
|
|
672
712
|
),
|
|
@@ -674,12 +714,12 @@ function registerFactTools(server, callApi) {
|
|
|
674
714
|
isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
|
|
675
715
|
valueDomain: z5.object({
|
|
676
716
|
allowedValues: z5.array(z5.union([z5.string(), z5.number()])).optional().describe(
|
|
677
|
-
"Accepted values. For list types the
|
|
717
|
+
"Accepted values. For list types the value domain applies to each element, not to the list as a whole."
|
|
678
718
|
),
|
|
679
719
|
min: z5.number().optional().describe("Lower bound, inclusive. Numeric types only."),
|
|
680
720
|
max: z5.number().optional().describe("Upper bound, inclusive. Numeric types only.")
|
|
681
721
|
}).optional().describe(
|
|
682
|
-
"Which values this fact accepts. Three states: omit the field to leave the
|
|
722
|
+
"Which values this fact accepts. Three states: omit the field to leave the value domain alone, send {} to remove it, or send a populated object to replace it. Narrowing it never blocks the update and does not rewrite rules. When rules compare this fact with a value the new value domain excludes, the result ends with a warning that lists them by version: drafts and the versions currently deployed."
|
|
683
723
|
)
|
|
684
724
|
})
|
|
685
725
|
},
|
|
@@ -1558,7 +1598,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
1558
1598
|
{
|
|
1559
1599
|
title: "List Domain Templates",
|
|
1560
1600
|
annotations: { readOnlyHint: true },
|
|
1561
|
-
description: "List all domain templates. A domain template is a
|
|
1601
|
+
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.",
|
|
1562
1602
|
inputSchema: z14.object({})
|
|
1563
1603
|
},
|
|
1564
1604
|
async () => callApi("GET", "domain-templates")
|
|
@@ -1568,10 +1608,10 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
1568
1608
|
{
|
|
1569
1609
|
title: "Preview Domain Template",
|
|
1570
1610
|
annotations: { readOnlyHint: true },
|
|
1571
|
-
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
|
|
1611
|
+
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.",
|
|
1572
1612
|
inputSchema: z14.object({
|
|
1573
1613
|
template: z14.string().describe(
|
|
1574
|
-
"Domain template key (e.g. ECOMMERCE). Use lexq_domain_templates_list to see
|
|
1614
|
+
"Domain template key (e.g. ECOMMERCE). Use lexq_domain_templates_list to see the keys."
|
|
1575
1615
|
)
|
|
1576
1616
|
})
|
|
1577
1617
|
},
|
|
@@ -1582,7 +1622,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
1582
1622
|
{
|
|
1583
1623
|
title: "Apply Domain Template",
|
|
1584
1624
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
1585
|
-
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.
|
|
1625
|
+
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.",
|
|
1586
1626
|
inputSchema: z14.object({
|
|
1587
1627
|
template: z14.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
|
|
1588
1628
|
customName: z14.string().optional().describe(
|
|
@@ -1621,6 +1661,7 @@ export {
|
|
|
1621
1661
|
LosslessNumber,
|
|
1622
1662
|
SERVER_INFO,
|
|
1623
1663
|
formatUnregisteredFactWarning,
|
|
1664
|
+
formatValueDomainMismatchWarning,
|
|
1624
1665
|
isLosslessNumber,
|
|
1625
1666
|
paginationParams,
|
|
1626
1667
|
parseJson,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexq/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"mcpName": "io.lexq/lexq",
|
|
5
5
|
"description": "LexQ CLI — manage policies, simulate rules, and deploy from the terminal. Built for humans and AI agents.",
|
|
6
6
|
"type": "module",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"test:export-format": "node tests/export-format.mjs",
|
|
44
44
|
"test:command-options": "node tests/command-options.mjs",
|
|
45
45
|
"test:value-domain": "pnpm build && node tests/value-domain.mjs",
|
|
46
|
+
"test:fact-update": "pnpm build && node tests/fact-update.mjs",
|
|
46
47
|
"prepublishOnly": "pnpm build",
|
|
47
48
|
"knip": "knip",
|
|
48
49
|
"format": "prettier --write \"src/**/*.ts\"",
|