@lexq/cli 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -2
- package/dist/index.js +49 -26
- package/dist/mcp/register.js +24 -14
- package/package.json +2 -1
- package/skills/lexq-execution/SKILL.md +1 -1
- package/skills/lexq-recipes/SKILL.md +4 -2
- package/skills/lexq-rules/SKILL.md +11 -4
package/AGENTS.md
CHANGED
|
@@ -180,8 +180,12 @@ The MCP toolset mirrors the CLI command inventory one-to-one.
|
|
|
180
180
|
- Releasing is two steps, in this order. Raise `version` in package.json inside the pull request, so a reviewer
|
|
181
181
|
sees the number beside the change that justifies it. After it merges, push the matching `v<version>` tag — that
|
|
182
182
|
tag is what publishes. A tag that disagrees with package.json is rejected before anything reaches npm.
|
|
183
|
-
- Pick the number by what breaks.
|
|
184
|
-
|
|
183
|
+
- Pick the number by what breaks. A change is breaking when a call that used to work now fails or does something
|
|
184
|
+
else. That includes removing or renaming a tool, command, option or parameter, changing a parameter's type,
|
|
185
|
+
adding a required parameter or making an optional one required, and narrowing the values a parameter accepts.
|
|
186
|
+
Raising the Node floor or moving a peer dependency to a new major version is breaking too. A breaking change
|
|
187
|
+
raises the major number. A new tool, command, option or optional parameter raises the minor, and anything else,
|
|
188
|
+
including a changed description, raises the patch.
|
|
185
189
|
- Gates before commit: `pnpm typecheck` (zero errors), `pnpm lint` (zero warnings), `pnpm test:fact-key`,
|
|
186
190
|
`pnpm test:decimals`.
|
|
187
191
|
- Multi-line CLI help and MCP tool descriptions use `dedent` — template-literal indentation leaks into LLM context.
|
package/dist/index.js
CHANGED
|
@@ -804,18 +804,18 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
804
804
|
process.exit(1);
|
|
805
805
|
}
|
|
806
806
|
});
|
|
807
|
-
versions.command("update").description("Update a draft version metadata").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--id <versionId>", "Policy version ID").option("--commit-message <message>", "Commit message").option("--effective-from <instant>", `Effective start, ${INSTANT}`).option("--effective-to <instant>", `Effective end, ${INSTANT}`).option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
|
|
807
|
+
versions.command("update").description("Update a draft version metadata").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--id <versionId>", "Policy version ID").option("--commit-message <message>", "Commit message").option("--effective-from <instant>", `Effective start, ${INSTANT}`).option("--effective-to <instant>", `Effective end, ${INSTANT}`).option("--clear-effective-from", "Remove the effective start").option("--clear-effective-to", "Remove the effective end").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
|
|
808
808
|
"after",
|
|
809
809
|
dedent4`
|
|
810
810
|
|
|
811
811
|
Only DRAFT versions can be updated. Published (ACTIVE) versions are immutable.
|
|
812
812
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
both current values (read them with lexq versions get).
|
|
813
|
+
Omitted options keep their current values. To remove an end of the effective
|
|
814
|
+
window, pass --clear-effective-from or --clear-effective-to instead of a value.
|
|
816
815
|
|
|
817
|
-
|
|
816
|
+
Examples:
|
|
818
817
|
$ lexq versions update --group-id <gid> --id <vid> --commit-message "Updated rules"
|
|
818
|
+
$ lexq versions update --group-id <gid> --id <vid> --clear-effective-to
|
|
819
819
|
`
|
|
820
820
|
).action(async (opts) => {
|
|
821
821
|
try {
|
|
@@ -911,6 +911,8 @@ function buildUpdateBody(opts) {
|
|
|
911
911
|
if (opts.commitMessage) body.commitMessage = opts.commitMessage;
|
|
912
912
|
if (opts.effectiveFrom) body.effectiveFrom = opts.effectiveFrom;
|
|
913
913
|
if (opts.effectiveTo) body.effectiveTo = opts.effectiveTo;
|
|
914
|
+
if (opts.clearEffectiveFrom) body.clearEffectiveFrom = true;
|
|
915
|
+
if (opts.clearEffectiveTo) body.clearEffectiveTo = true;
|
|
914
916
|
return body;
|
|
915
917
|
}
|
|
916
918
|
|
|
@@ -1326,7 +1328,9 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
1326
1328
|
dedent6`
|
|
1327
1329
|
|
|
1328
1330
|
Facts used in a version's rules that have no definition yet (read-only).
|
|
1329
|
-
|
|
1331
|
+
Keys a rule only reads do not block publish/deploy; register them to enable
|
|
1332
|
+
validation. A SET_FACT or MUTATE_FACT targetVar must already be registered:
|
|
1333
|
+
saving or publishing fails with ACT-032 otherwise.
|
|
1330
1334
|
|
|
1331
1335
|
Example:
|
|
1332
1336
|
$ lexq facts unregistered --group-id <gid> --version-id <vid> --format table
|
|
@@ -1642,6 +1646,7 @@ function registerDeployCommands(program) {
|
|
|
1642
1646
|
|
|
1643
1647
|
Locks the version permanently. Rules cannot be modified after publishing.
|
|
1644
1648
|
A snapshot hash is generated for integrity verification.
|
|
1649
|
+
A version whose effective end has already passed cannot be published (P-058).
|
|
1645
1650
|
|
|
1646
1651
|
Example:
|
|
1647
1652
|
$ lexq deploy publish --group-id <gid> --version-id <vid> --memo "Validated via dry-run"
|
|
@@ -1672,6 +1677,8 @@ function registerDeployCommands(program) {
|
|
|
1672
1677
|
dedent7`
|
|
1673
1678
|
|
|
1674
1679
|
Takes effect immediately. The version starts receiving production traffic.
|
|
1680
|
+
A version whose effective start has not arrived (P-037) or whose effective end
|
|
1681
|
+
has passed (P-058) is rejected.
|
|
1675
1682
|
|
|
1676
1683
|
Example:
|
|
1677
1684
|
$ lexq deploy live --group-id <gid> --version-id <vid> --memo "Go live — v3"
|
|
@@ -1698,7 +1705,8 @@ function registerDeployCommands(program) {
|
|
|
1698
1705
|
dedent7`
|
|
1699
1706
|
|
|
1700
1707
|
Reverts to the version that was live before the current one.
|
|
1701
|
-
Only available if the previous version is still ACTIVE
|
|
1708
|
+
Only available if the previous version is still ACTIVE and its effective end
|
|
1709
|
+
has not passed (P-058).
|
|
1702
1710
|
|
|
1703
1711
|
Example:
|
|
1704
1712
|
$ lexq deploy rollback --group-id <gid> --memo "High error rate" --force
|
|
@@ -2201,7 +2209,7 @@ function registerAnalyticsCommands(program) {
|
|
|
2201
2209
|
Use --format table for summary view.
|
|
2202
2210
|
`
|
|
2203
2211
|
);
|
|
2204
|
-
sim.command("start").description("Start a new batch simulation").
|
|
2212
|
+
sim.command("start").description("Start a new batch simulation").option("--json <body>", "Simulation request body as JSON").option("--file <path>", "Read request body from a JSON file").addHelpText(
|
|
2205
2213
|
"after",
|
|
2206
2214
|
dedent8`
|
|
2207
2215
|
|
|
@@ -2220,7 +2228,9 @@ function registerAnalyticsCommands(program) {
|
|
|
2220
2228
|
"maxRecords": 1000
|
|
2221
2229
|
}
|
|
2222
2230
|
}'
|
|
2223
|
-
|
|
2231
|
+
|
|
2232
|
+
$ lexq analytics simulation start --file simulation.json
|
|
2233
|
+
|
|
2224
2234
|
Dataset types: HISTORICAL, UPLOADED, MANUAL
|
|
2225
2235
|
Dataset sources: EXECUTION_LOGS, S3_BUCKET, REQUEST_BODY
|
|
2226
2236
|
HISTORICAL from / to are yyyy-MM-dd, whole days in the organization time zone, both inclusive.
|
|
@@ -2228,6 +2238,9 @@ function registerAnalyticsCommands(program) {
|
|
|
2228
2238
|
).action(async (opts) => {
|
|
2229
2239
|
try {
|
|
2230
2240
|
const globalOpts = program.opts();
|
|
2241
|
+
if (!opts.json && !opts.file) {
|
|
2242
|
+
throw new Error("Request body is required. Use --json or --file.");
|
|
2243
|
+
}
|
|
2231
2244
|
const body = resolveBody(opts);
|
|
2232
2245
|
const data = await apiRequest("POST", "analytics/simulations", {
|
|
2233
2246
|
apiKey: globalOpts.apiKey,
|
|
@@ -2578,7 +2591,7 @@ function registerProfileCommands(program) {
|
|
|
2578
2591
|
program.command("profile <groupId>").description("Per-rule latency profile with relative slow-rule flags").option(
|
|
2579
2592
|
"--rule <ruleId>",
|
|
2580
2593
|
`Single-rule detail (distributions + ${LATENCY_WINDOW_MILLIS / 1e3}s window series)`
|
|
2581
|
-
).option("--version <versionId>", "Version to inspect (default: live version)").option(
|
|
2594
|
+
).option("--version-id <versionId>", "Version to inspect (default: live version)").option(
|
|
2582
2595
|
"--from <instant>",
|
|
2583
2596
|
`Window start, ISO-8601 instant (default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago)`
|
|
2584
2597
|
).option("--to <instant>", "Window end, ISO-8601 instant (default: now)").option("--cache <state>", "Cache dimension for the rule table: HIT | MISS (default: HIT)").addHelpText(
|
|
@@ -2594,7 +2607,7 @@ function registerProfileCommands(program) {
|
|
|
2594
2607
|
Examples:
|
|
2595
2608
|
$ lexq profile <groupId>
|
|
2596
2609
|
$ lexq profile <groupId> --cache MISS --from 2026-07-01T00:00:00Z
|
|
2597
|
-
$ lexq profile <groupId> --rule <ruleId> --version <versionId>
|
|
2610
|
+
$ lexq profile <groupId> --rule <ruleId> --version-id <versionId>
|
|
2598
2611
|
`
|
|
2599
2612
|
).action(async (groupId, opts) => {
|
|
2600
2613
|
try {
|
|
@@ -2604,7 +2617,7 @@ function registerProfileCommands(program) {
|
|
|
2604
2617
|
throw new Error(`--cache must be one of: ${ProfileCacheState.join(" | ")}`);
|
|
2605
2618
|
}
|
|
2606
2619
|
const params = {};
|
|
2607
|
-
if (opts.
|
|
2620
|
+
if (opts.versionId) params.versionId = opts.versionId;
|
|
2608
2621
|
if (opts.from) params.from = opts.from;
|
|
2609
2622
|
if (opts.to) params.to = opts.to;
|
|
2610
2623
|
if (opts.cache) params.cacheState = opts.cache;
|
|
@@ -3743,13 +3756,15 @@ function registerVersionTools(server, callApi) {
|
|
|
3743
3756
|
{
|
|
3744
3757
|
title: "Update Policy Version",
|
|
3745
3758
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3746
|
-
description: "Update a DRAFT version. Only DRAFT versions can be modified.
|
|
3759
|
+
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.",
|
|
3747
3760
|
inputSchema: z3.object({
|
|
3748
3761
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
3749
3762
|
versionId: z3.string().uuid().describe("Version ID"),
|
|
3750
3763
|
commitMessage: z3.string().optional().describe("New commit message"),
|
|
3751
|
-
effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT} Omit it
|
|
3752
|
-
effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT} Omit it
|
|
3764
|
+
effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
|
|
3765
|
+
effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
|
|
3766
|
+
clearEffectiveFrom: z3.boolean().optional().describe("Set to true to remove the effective start. Do not send effectiveFrom with it."),
|
|
3767
|
+
clearEffectiveTo: z3.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
|
|
3753
3768
|
})
|
|
3754
3769
|
},
|
|
3755
3770
|
async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
|
|
@@ -3829,7 +3844,12 @@ function registerRuleTools(server, callApi) {
|
|
|
3829
3844
|
If a required key is missing, ask the user to confirm the type, isRequired, and description
|
|
3830
3845
|
before calling lexq_facts_create — registering facts enables type validation, Console UI
|
|
3831
3846
|
autocomplete, and the dry-run requirements analyzer.
|
|
3832
|
-
|
|
3847
|
+
|
|
3848
|
+
An action's targetVar (SET_FACT, MUTATE_FACT) must be a registered fact. Saving or
|
|
3849
|
+
publishing a rule whose targetVar is not registered fails with ACT-032, so register it
|
|
3850
|
+
with lexq_facts_create first. Keys a rule only reads (condition fields, refVar) may stay
|
|
3851
|
+
unregistered.
|
|
3852
|
+
|
|
3833
3853
|
After saving, lexq_facts_unregistered lists any keys this version references but has not
|
|
3834
3854
|
defined (non-blocking, version-wide) — use it to decide what to register.
|
|
3835
3855
|
|
|
@@ -3858,8 +3878,9 @@ function registerRuleTools(server, callApi) {
|
|
|
3858
3878
|
|
|
3859
3879
|
Action parameter schemas:
|
|
3860
3880
|
- MUTATE_FACT: { targetVar: string, operator: "ASSIGN"|"ADD"|"SUB"|"MUL"|"DIV", method: "PERCENTAGE"|"AMOUNT", operand: number, refVar?: string, rounding?: RoundingOption }
|
|
3861
|
-
targetVar is the fact this action reads and writes. It must
|
|
3862
|
-
|
|
3881
|
+
targetVar is the fact this action reads and writes. It must be a registered fact
|
|
3882
|
+
(ACT-032 otherwise), and the request must send it (P-015 otherwise). Its value must be
|
|
3883
|
+
a number when the action runs.
|
|
3863
3884
|
A missing required fact throws (no 0 default).
|
|
3864
3885
|
operand is the arithmetic operand; the unit is dictated by method (percent when
|
|
3865
3886
|
PERCENTAGE, absolute amount when AMOUNT). Ranges are not constrained — negative values
|
|
@@ -3877,8 +3898,10 @@ function registerRuleTools(server, callApi) {
|
|
|
3877
3898
|
DIV targetVar /= operand | invalid
|
|
3878
3899
|
Constraints: DIV + PERCENTAGE is invalid (use MUL with the inverse). DIV + AMOUNT
|
|
3879
3900
|
requires operand !== 0.
|
|
3880
|
-
- SET_FACT: { targetVar: string, value: string|number|boolean }
|
|
3881
|
-
|
|
3901
|
+
- SET_FACT: { targetVar: string, value: string|number|boolean } targetVar must be a
|
|
3902
|
+
registered fact (ACT-032 otherwise). The request does not have to send it: SET_FACT is
|
|
3903
|
+
the only action that can write a key absent from the input facts. MUTATE_FACT requires
|
|
3904
|
+
its target to be present in the input facts.
|
|
3882
3905
|
- BLOCK: { reason: string } Records a rejection decision. It does NOT halt rule execution —
|
|
3883
3906
|
subsequent actions and subsequent winning rules still run. Enforcement is the caller's
|
|
3884
3907
|
responsibility; the decision surfaces as the isBlocked fact.
|
|
@@ -3903,7 +3926,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3903
3926
|
{
|
|
3904
3927
|
title: "Update Rule",
|
|
3905
3928
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3906
|
-
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.`,
|
|
3929
|
+
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.`,
|
|
3907
3930
|
inputSchema: z4.object({
|
|
3908
3931
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
3909
3932
|
versionId: z4.string().uuid().describe("Version ID"),
|
|
@@ -4082,7 +4105,7 @@ function registerFactTools(server, callApi) {
|
|
|
4082
4105
|
{
|
|
4083
4106
|
title: "Get Action Runtime Fact Metadata",
|
|
4084
4107
|
annotations: { readOnlyHint: true },
|
|
4085
|
-
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.
|
|
4108
|
+
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 that can write a key the request did not send, and its targetVar must be a registered fact (ACT-032). Static data, safe to cache in-session.",
|
|
4086
4109
|
inputSchema: z5.object({})
|
|
4087
4110
|
},
|
|
4088
4111
|
async () => callApi("GET", "schema/action-metadata")
|
|
@@ -4092,7 +4115,7 @@ function registerFactTools(server, callApi) {
|
|
|
4092
4115
|
{
|
|
4093
4116
|
title: "List Unregistered Facts",
|
|
4094
4117
|
annotations: { readOnlyHint: true },
|
|
4095
|
-
description: "List facts referenced by a version's rules but not yet defined (read-only
|
|
4118
|
+
description: "List facts referenced by a version's rules but not yet defined (read-only). Keys that rules only read do not block publish/deploy; a rule whose action targetVar is not a registered fact is rejected at save and publish with ACT-032. Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
|
|
4096
4119
|
inputSchema: z5.object({
|
|
4097
4120
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4098
4121
|
versionId: z5.string().uuid().describe("Version ID")
|
|
@@ -4125,7 +4148,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4125
4148
|
{
|
|
4126
4149
|
title: "Publish Version",
|
|
4127
4150
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4128
|
-
description: `Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts
|
|
4151
|
+
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.`,
|
|
4129
4152
|
inputSchema: z6.object({
|
|
4130
4153
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4131
4154
|
versionId: z6.string().uuid().describe("Version ID to publish"),
|
|
@@ -4139,7 +4162,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4139
4162
|
{
|
|
4140
4163
|
title: "Deploy to Live",
|
|
4141
4164
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4142
|
-
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
|
|
4165
|
+
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.",
|
|
4143
4166
|
inputSchema: z6.object({
|
|
4144
4167
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4145
4168
|
versionId: z6.string().uuid().describe("Version ID to deploy"),
|
|
@@ -4155,7 +4178,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4155
4178
|
{
|
|
4156
4179
|
title: "Rollback Deployment",
|
|
4157
4180
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4158
|
-
description: "Rollback to the previous deployed version. Only available if there is a previous version.",
|
|
4181
|
+
description: "Rollback to the previous deployed version. Only available if there is a previous version that is still ACTIVE and whose effective end has not passed (P-058).",
|
|
4159
4182
|
inputSchema: z6.object({
|
|
4160
4183
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4161
4184
|
memo: z6.string().min(1).describe("Rollback reason (required)")
|
package/dist/mcp/register.js
CHANGED
|
@@ -321,13 +321,15 @@ function registerVersionTools(server, callApi) {
|
|
|
321
321
|
{
|
|
322
322
|
title: "Update Policy Version",
|
|
323
323
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
324
|
-
description: "Update a DRAFT version. Only DRAFT versions can be modified.
|
|
324
|
+
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.",
|
|
325
325
|
inputSchema: z3.object({
|
|
326
326
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
327
327
|
versionId: z3.string().uuid().describe("Version ID"),
|
|
328
328
|
commitMessage: z3.string().optional().describe("New commit message"),
|
|
329
|
-
effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT} Omit it
|
|
330
|
-
effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT} Omit it
|
|
329
|
+
effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
|
|
330
|
+
effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
|
|
331
|
+
clearEffectiveFrom: z3.boolean().optional().describe("Set to true to remove the effective start. Do not send effectiveFrom with it."),
|
|
332
|
+
clearEffectiveTo: z3.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
|
|
331
333
|
})
|
|
332
334
|
},
|
|
333
335
|
async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
|
|
@@ -407,7 +409,12 @@ function registerRuleTools(server, callApi) {
|
|
|
407
409
|
If a required key is missing, ask the user to confirm the type, isRequired, and description
|
|
408
410
|
before calling lexq_facts_create — registering facts enables type validation, Console UI
|
|
409
411
|
autocomplete, and the dry-run requirements analyzer.
|
|
410
|
-
|
|
412
|
+
|
|
413
|
+
An action's targetVar (SET_FACT, MUTATE_FACT) must be a registered fact. Saving or
|
|
414
|
+
publishing a rule whose targetVar is not registered fails with ACT-032, so register it
|
|
415
|
+
with lexq_facts_create first. Keys a rule only reads (condition fields, refVar) may stay
|
|
416
|
+
unregistered.
|
|
417
|
+
|
|
411
418
|
After saving, lexq_facts_unregistered lists any keys this version references but has not
|
|
412
419
|
defined (non-blocking, version-wide) — use it to decide what to register.
|
|
413
420
|
|
|
@@ -436,8 +443,9 @@ function registerRuleTools(server, callApi) {
|
|
|
436
443
|
|
|
437
444
|
Action parameter schemas:
|
|
438
445
|
- MUTATE_FACT: { targetVar: string, operator: "ASSIGN"|"ADD"|"SUB"|"MUL"|"DIV", method: "PERCENTAGE"|"AMOUNT", operand: number, refVar?: string, rounding?: RoundingOption }
|
|
439
|
-
targetVar is the fact this action reads and writes. It must
|
|
440
|
-
|
|
446
|
+
targetVar is the fact this action reads and writes. It must be a registered fact
|
|
447
|
+
(ACT-032 otherwise), and the request must send it (P-015 otherwise). Its value must be
|
|
448
|
+
a number when the action runs.
|
|
441
449
|
A missing required fact throws (no 0 default).
|
|
442
450
|
operand is the arithmetic operand; the unit is dictated by method (percent when
|
|
443
451
|
PERCENTAGE, absolute amount when AMOUNT). Ranges are not constrained — negative values
|
|
@@ -455,8 +463,10 @@ function registerRuleTools(server, callApi) {
|
|
|
455
463
|
DIV targetVar /= operand | invalid
|
|
456
464
|
Constraints: DIV + PERCENTAGE is invalid (use MUL with the inverse). DIV + AMOUNT
|
|
457
465
|
requires operand !== 0.
|
|
458
|
-
- SET_FACT: { targetVar: string, value: string|number|boolean }
|
|
459
|
-
|
|
466
|
+
- SET_FACT: { targetVar: string, value: string|number|boolean } targetVar must be a
|
|
467
|
+
registered fact (ACT-032 otherwise). The request does not have to send it: SET_FACT is
|
|
468
|
+
the only action that can write a key absent from the input facts. MUTATE_FACT requires
|
|
469
|
+
its target to be present in the input facts.
|
|
460
470
|
- BLOCK: { reason: string } Records a rejection decision. It does NOT halt rule execution —
|
|
461
471
|
subsequent actions and subsequent winning rules still run. Enforcement is the caller's
|
|
462
472
|
responsibility; the decision surfaces as the isBlocked fact.
|
|
@@ -481,7 +491,7 @@ function registerRuleTools(server, callApi) {
|
|
|
481
491
|
{
|
|
482
492
|
title: "Update Rule",
|
|
483
493
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
484
|
-
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.`,
|
|
494
|
+
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.`,
|
|
485
495
|
inputSchema: z4.object({
|
|
486
496
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
487
497
|
versionId: z4.string().uuid().describe("Version ID"),
|
|
@@ -664,7 +674,7 @@ function registerFactTools(server, callApi) {
|
|
|
664
674
|
{
|
|
665
675
|
title: "Get Action Runtime Fact Metadata",
|
|
666
676
|
annotations: { readOnlyHint: true },
|
|
667
|
-
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.
|
|
677
|
+
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 that can write a key the request did not send, and its targetVar must be a registered fact (ACT-032). Static data, safe to cache in-session.",
|
|
668
678
|
inputSchema: z5.object({})
|
|
669
679
|
},
|
|
670
680
|
async () => callApi("GET", "schema/action-metadata")
|
|
@@ -674,7 +684,7 @@ function registerFactTools(server, callApi) {
|
|
|
674
684
|
{
|
|
675
685
|
title: "List Unregistered Facts",
|
|
676
686
|
annotations: { readOnlyHint: true },
|
|
677
|
-
description: "List facts referenced by a version's rules but not yet defined (read-only
|
|
687
|
+
description: "List facts referenced by a version's rules but not yet defined (read-only). Keys that rules only read do not block publish/deploy; a rule whose action targetVar is not a registered fact is rejected at save and publish with ACT-032. Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
|
|
678
688
|
inputSchema: z5.object({
|
|
679
689
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
680
690
|
versionId: z5.string().uuid().describe("Version ID")
|
|
@@ -707,7 +717,7 @@ function registerDeployTools(server, callApi) {
|
|
|
707
717
|
{
|
|
708
718
|
title: "Publish Version",
|
|
709
719
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
710
|
-
description: `Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts
|
|
720
|
+
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.`,
|
|
711
721
|
inputSchema: z6.object({
|
|
712
722
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
713
723
|
versionId: z6.string().uuid().describe("Version ID to publish"),
|
|
@@ -721,7 +731,7 @@ function registerDeployTools(server, callApi) {
|
|
|
721
731
|
{
|
|
722
732
|
title: "Deploy to Live",
|
|
723
733
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
724
|
-
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
|
|
734
|
+
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.",
|
|
725
735
|
inputSchema: z6.object({
|
|
726
736
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
727
737
|
versionId: z6.string().uuid().describe("Version ID to deploy"),
|
|
@@ -737,7 +747,7 @@ function registerDeployTools(server, callApi) {
|
|
|
737
747
|
{
|
|
738
748
|
title: "Rollback Deployment",
|
|
739
749
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
740
|
-
description: "Rollback to the previous deployed version. Only available if there is a previous version.",
|
|
750
|
+
description: "Rollback to the previous deployed version. Only available if there is a previous version that is still ACTIVE and whose effective end has not passed (P-058).",
|
|
741
751
|
inputSchema: z6.object({
|
|
742
752
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
743
753
|
memo: z6.string().min(1).describe("Rollback reason (required)")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexq/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"test:decimals": "pnpm build && node tests/exact-decimals.mjs",
|
|
42
42
|
"test:fact-key": "node tests/fact-key.mjs",
|
|
43
43
|
"test:export-format": "node tests/export-format.mjs",
|
|
44
|
+
"test:command-options": "node tests/command-options.mjs",
|
|
44
45
|
"test:value-domain": "pnpm build && node tests/value-domain.mjs",
|
|
45
46
|
"prepublishOnly": "pnpm build",
|
|
46
47
|
"knip": "knip",
|
|
@@ -253,7 +253,7 @@ lexq profile <groupId>
|
|
|
253
253
|
lexq profile <groupId> --rule <ruleId>
|
|
254
254
|
|
|
255
255
|
# Defaults: live version, last 24h, cache HIT
|
|
256
|
-
lexq profile <groupId> --version <vid> --cache MISS \
|
|
256
|
+
lexq profile <groupId> --version-id <vid> --cache MISS \
|
|
257
257
|
--from 2026-07-01T00:00:00Z --to 2026-08-01T00:00:00Z
|
|
258
258
|
```
|
|
259
259
|
|
|
@@ -480,8 +480,10 @@ lexq analytics dry-run --version-id <vid> --debug --json '{
|
|
|
480
480
|
|
|
481
481
|
**Notes**
|
|
482
482
|
|
|
483
|
-
- `SET_FACT`
|
|
484
|
-
|
|
483
|
+
- Register a `SET_FACT` target before saving the rule, as the step above does. A rule whose
|
|
484
|
+
`targetVar` is not a registered fact is rejected with `ACT-032`.
|
|
485
|
+
- At execution, `SET_FACT` writes the key even when the request did not send it. `MUTATE_FACT`
|
|
486
|
+
requires the request to send its target. That is the division of labor between them.
|
|
485
487
|
- Flags appear in `generatedVariables` in the response. `SET_FACT` produces no `__delta` because
|
|
486
488
|
it is an assignment, not an arithmetic change.
|
|
487
489
|
- Segment on a `LIST_STRING` fact with `HAS_ANY` / `HAS_ALL` / `HAS_NONE`, not `CONTAINS`.
|
|
@@ -181,14 +181,14 @@ Each rule can have multiple actions. Actions fire sequentially.
|
|
|
181
181
|
| Type | Description | Key Parameters |
|
|
182
182
|
|---------------|--------------------------------------------|----------------------------------------------------------------------|
|
|
183
183
|
| `MUTATE_FACT` | Arithmetic change to a numeric fact | `targetVar`, `operator`, `method`, `operand`, `refVar?`, `rounding?` |
|
|
184
|
-
| `SET_FACT` | Assign a literal value
|
|
184
|
+
| `SET_FACT` | Assign a literal value to a registered fact | `targetVar`, `value` |
|
|
185
185
|
| `BLOCK` | Record a rejection decision | `reason` |
|
|
186
186
|
|
|
187
187
|
### `MUTATE_FACT` parameters
|
|
188
188
|
|
|
189
189
|
| Parameter | Required | Meaning |
|
|
190
190
|
|-------------|----------|-------------------------------------------------------------------------------|
|
|
191
|
-
| `targetVar` | always | The fact this action **reads and writes**. Must
|
|
191
|
+
| `targetVar` | always | The fact this action **reads and writes**. Must be registered and present as a number at execution. |
|
|
192
192
|
| `operator` | always | `ASSIGN` \| `ADD` \| `SUB` \| `MUL` \| `DIV` |
|
|
193
193
|
| `method` | always | `PERCENTAGE` \| `AMOUNT` — dictates the unit of `operand` |
|
|
194
194
|
| `operand` | always | The arithmetic operand. Percent when PERCENTAGE, absolute amount when AMOUNT. |
|
|
@@ -235,8 +235,15 @@ Use `refVar` when the base differs from the target:
|
|
|
235
235
|
|
|
236
236
|
### `SET_FACT` vs `MUTATE_FACT`
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
Both actions write to `targetVar`, and it must be a **registered fact**. Saving or publishing a
|
|
239
|
+
rule whose `targetVar` is not registered fails with `ACT-032`; register it with
|
|
240
|
+
`lexq facts create` first. Keys a rule only reads (condition fields, `refVar`) may stay
|
|
241
|
+
unregistered.
|
|
242
|
+
|
|
243
|
+
At execution, they differ in what the request must send. `SET_FACT` writes the value even when
|
|
244
|
+
the request did not send the key. `MUTATE_FACT` requires the request to send the target (`P-015`
|
|
245
|
+
otherwise), and the target's value must be a number when the action runs. "Write something the
|
|
246
|
+
request didn't send" is `SET_FACT`'s job.
|
|
240
247
|
|
|
241
248
|
### `BLOCK` does not halt execution
|
|
242
249
|
|