@lexq/cli 3.0.1 → 3.0.2
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 +0 -2
- package/CONTEXT.md +5 -7
- package/dist/index.js +21 -18
- package/dist/mcp/register.d.ts +6 -5
- package/dist/mcp/register.js +9 -8
- package/package.json +1 -1
- package/skills/lexq-execution/SKILL.md +11 -12
- package/skills/lexq-groups/SKILL.md +5 -12
- package/skills/lexq-recipes/SKILL.md +4 -5
- package/skills/lexq-shared/SKILL.md +1 -1
- package/skills/lexq-simulation/SKILL.md +2 -2
package/AGENTS.md
CHANGED
|
@@ -175,8 +175,6 @@ The MCP toolset mirrors the CLI command inventory one-to-one.
|
|
|
175
175
|
|
|
176
176
|
- CLI commands and MCP tools stay in lock-step — a new command lands with its MCP tool in the same PR, sharing
|
|
177
177
|
the single api-client layer.
|
|
178
|
-
- The hosted MCP server consumes this package from npm. Publish (`v*` tag) **before** redeploying it — CI cannot
|
|
179
|
-
catch that ordering.
|
|
180
178
|
- Releasing is two steps, in this order. Raise `version` in package.json inside the pull request, so a reviewer
|
|
181
179
|
sees the number beside the change that justifies it. After it merges, push the matching `v<version>` tag — that
|
|
182
180
|
tag is what publishes. A tag that disagrees with package.json is rejected before anything reaches npm.
|
package/CONTEXT.md
CHANGED
|
@@ -26,7 +26,7 @@ simulation, deploy to production, and execute via REST API — all without modif
|
|
|
26
26
|
└─────────────────────────────────────────────────────┘
|
|
27
27
|
│
|
|
28
28
|
▼
|
|
29
|
-
|
|
29
|
+
api.lexq.io
|
|
30
30
|
│
|
|
31
31
|
┌──────────────┴──────────────┐
|
|
32
32
|
▼ ▼
|
|
@@ -38,8 +38,7 @@ simulation, deploy to production, and execute via REST API — all without modif
|
|
|
38
38
|
LexQ Policy Engine
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
The CLI
|
|
42
|
-
endpoint.
|
|
41
|
+
The CLI calls only the Partner API. Your service evaluates rules through the Execution API.
|
|
43
42
|
|
|
44
43
|
**Partner Base URL:** `https://api.lexq.io/api/v1/partners`
|
|
45
44
|
**Execution Base URL:** `https://api.lexq.io/api/v1/execution`
|
|
@@ -77,7 +76,7 @@ Tenant
|
|
|
77
76
|
| `DRAFT` | Editable. Rules can be added/modified. |
|
|
78
77
|
| `ACTIVE` | Published. Locked — no modifications allowed. |
|
|
79
78
|
| `ARCHIVED` | Superseded by a newer version. |
|
|
80
|
-
| `EXPIRED` | Past `effectiveTo` date.
|
|
79
|
+
| `EXPIRED` | Past its `effectiveTo` date. |
|
|
81
80
|
|
|
82
81
|
### Condition Operators
|
|
83
82
|
|
|
@@ -123,8 +122,7 @@ Publishing (DRAFT → ACTIVE) is a qualification event, not a deployment — it
|
|
|
123
122
|
`BLOCKED` means the rule matched but lost conflict resolution — the round and reason are in `reasonCode` (
|
|
124
123
|
`MUTEX_PRIORITY_LOST`, `GROUP_LIMIT_REACHED`, …). It is unrelated to the `BLOCK` action, whose rule stays `SELECTED`.
|
|
125
124
|
|
|
126
|
-
`NOT_SELECTED`
|
|
127
|
-
implemented).
|
|
125
|
+
`NOT_SELECTED` is part of the enum but is not returned today.
|
|
128
126
|
|
|
129
127
|
### Platform Event Types (for webhook subscriptions)
|
|
130
128
|
|
|
@@ -153,7 +151,7 @@ implemented).
|
|
|
153
151
|
| **A/B Test** | Splitting traffic between the current live version and a test version by percentage. |
|
|
154
152
|
| **Execution Trace** | Per-rule evaluation result showing whether the condition matched and what actions were generated. |
|
|
155
153
|
| **Decision Trace** | Final disposition of a rule after conflict resolution (`SELECTED`, `BLOCKED`, …), with a `reasonCode` explaining why. |
|
|
156
|
-
| **Snapshot Hash** |
|
|
154
|
+
| **Snapshot Hash** | A fingerprint of a version's rules, recorded at deployment and used to check that the deployed rules did not change. |
|
|
157
155
|
| **Traffic Rate** | Percentage (1–99) of traffic routed to the A/B test version. |
|
|
158
156
|
| **Webhook Subscription** | A platform event listener — receives notifications on deployment lifecycle events. Tenant-level. |
|
|
159
157
|
| **Platform Event** | A lifecycle event emitted by the engine itself (`VERSION_PUBLISHED`, `DEPLOYED`, `ROLLED_BACK`, `UNDEPLOYED`, `DEPLOY_SCHEDULED`, `DEPLOY_SCHEDULE_CANCELED`). |
|
package/dist/index.js
CHANGED
|
@@ -793,7 +793,8 @@ function registerVersionCommands(program) {
|
|
|
793
793
|
|
|
794
794
|
A version is an immutable snapshot of rules within a policy group.
|
|
795
795
|
|
|
796
|
-
Lifecycle: DRAFT → ACTIVE (publish) →
|
|
796
|
+
Lifecycle: DRAFT → ACTIVE (publish) → EXPIRED (past effectiveTo)
|
|
797
|
+
A version stays ACTIVE after a newer version goes live, so you can roll back to it.
|
|
797
798
|
|
|
798
799
|
Commands:
|
|
799
800
|
list List versions for a group
|
|
@@ -1539,7 +1540,7 @@ ${data.length} unregistered`);
|
|
|
1539
1540
|
STRING types take --allowed-values. NUMBER types take all three.
|
|
1540
1541
|
BOOLEAN takes none — it has only two values to begin with.
|
|
1541
1542
|
For list types the value domain applies to each element.
|
|
1542
|
-
Leave them out for no value domain
|
|
1543
|
+
Leave them out for no value domain.
|
|
1543
1544
|
`
|
|
1544
1545
|
).action(async (opts) => {
|
|
1545
1546
|
try {
|
|
@@ -2101,8 +2102,8 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2101
2102
|
"after",
|
|
2102
2103
|
dedent7`
|
|
2103
2104
|
|
|
2104
|
-
The version must be ACTIVE with a future effective start date;
|
|
2105
|
-
|
|
2105
|
+
The version must be ACTIVE with a future effective start date; it is
|
|
2106
|
+
deployed automatically within ${SCHEDULE_TICK_MAX_MILLIS / 1e3} seconds after that time.
|
|
2106
2107
|
That time is the version's effectiveFrom, set on the DRAFT before publishing;
|
|
2107
2108
|
this command takes no time of its own.
|
|
2108
2109
|
One pending schedule per group. Manual deploy/rollback/undeploy, starting
|
|
@@ -2256,7 +2257,7 @@ function uploadDataset(bytes, filename, options) {
|
|
|
2256
2257
|
}
|
|
2257
2258
|
|
|
2258
2259
|
// src/lib/dry-run-limit.ts
|
|
2259
|
-
var DRY_RUN_LIMIT_NOTE = `Not billed and not counted toward the plan's usage. Dry runs, dry run compares, and single-decision replays share a limit of ${MAX_DRY_RUN_EVALUATIONS_PER_MINUTE} evaluations per minute for each organization; a compare counts as two. Past the limit, a call fails with 429 (C-007)
|
|
2260
|
+
var DRY_RUN_LIMIT_NOTE = `Not billed and not counted toward the plan's usage. Dry runs, dry run compares, and single-decision replays share a limit of ${MAX_DRY_RUN_EVALUATIONS_PER_MINUTE} evaluations per minute for each organization; a compare counts as two. Past the limit, a call fails with 429 (C-007); wait a minute before trying again.`;
|
|
2260
2261
|
|
|
2261
2262
|
// src/commands/analytics.ts
|
|
2262
2263
|
function registerAnalyticsCommands(program) {
|
|
@@ -2785,7 +2786,7 @@ function registerProfileCommands(program) {
|
|
|
2785
2786
|
per-rule p50s within the group. Absolute ms thresholds are intentionally
|
|
2786
2787
|
not supported. Each percentile is withheld (–) unless n×(1−q) ≥ ${TAIL_MIN_OBS} —
|
|
2787
2788
|
p50 from n ≥ ${PERCENTILE_MIN_N.p50}, p95 from n ≥ ${PERCENTILE_MIN_N.p95}, p99 from n ≥ ${PERCENTILE_MIN_N.p99}. TOTAL is recorded
|
|
2788
|
-
on every call, rule detail from a
|
|
2789
|
+
on every call, rule detail from a ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls.
|
|
2789
2790
|
|
|
2790
2791
|
Examples:
|
|
2791
2792
|
$ lexq profile <groupId>
|
|
@@ -2843,7 +2844,9 @@ function registerProfileCommands(program) {
|
|
|
2843
2844
|
console.log(`baseline ${b.phase.padEnd(9)}: ${base}`);
|
|
2844
2845
|
}
|
|
2845
2846
|
if (data.droppedRows > 0) {
|
|
2846
|
-
console.log(
|
|
2847
|
+
console.log(
|
|
2848
|
+
`\u26A0 droppedRows=${data.droppedRows} (some samples could not be read and are not included)`
|
|
2849
|
+
);
|
|
2847
2850
|
}
|
|
2848
2851
|
printTable(
|
|
2849
2852
|
["Rule", "Phase", "n", "p50(ms)", "p95(ms)", "p99(ms)", "\xD7base", "Flagged"],
|
|
@@ -3167,8 +3170,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3167
3170
|
"after",
|
|
3168
3171
|
dedent11`
|
|
3169
3172
|
|
|
3170
|
-
Only COMPLETED jobs can be exported.
|
|
3171
|
-
partial one reads as the whole thing on the receiving end.
|
|
3173
|
+
Only COMPLETED jobs can be exported.
|
|
3172
3174
|
|
|
3173
3175
|
The two formats carry different things. CSV holds the effect blast radius, whose
|
|
3174
3176
|
columns are fixed. JSON holds the changed samples and action parameters, which
|
|
@@ -3302,7 +3304,7 @@ function registerLogCommands(program) {
|
|
|
3302
3304
|
"after",
|
|
3303
3305
|
dedent13`
|
|
3304
3306
|
|
|
3305
|
-
|
|
3307
|
+
Failure logs for platform event webhooks and scheduled deployments that failed.
|
|
3306
3308
|
|
|
3307
3309
|
Commands:
|
|
3308
3310
|
list List failure logs with filters
|
|
@@ -3791,8 +3793,9 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3791
3793
|
description: dedent15`
|
|
3792
3794
|
Start an Impact Simulation against historical, uploaded, or inline data.
|
|
3793
3795
|
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
+
Every record a simulation processes counts toward the plan's simulation usage, including
|
|
3797
|
+
records processed before a cancel. When the plan's limit is reached, a new simulation
|
|
3798
|
+
cannot start.
|
|
3796
3799
|
|
|
3797
3800
|
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
3798
3801
|
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
@@ -3901,7 +3904,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3901
3904
|
annotations: ADDITIVE,
|
|
3902
3905
|
description: dedent15`
|
|
3903
3906
|
Upload inline CSV or JSON content as a simulation dataset.
|
|
3904
|
-
The content is
|
|
3907
|
+
The content is stored as a dataset, and its path is returned in the "path" field.
|
|
3905
3908
|
|
|
3906
3909
|
To use the returned path in lexq_simulation_start, set:
|
|
3907
3910
|
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
@@ -4695,7 +4698,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4695
4698
|
{
|
|
4696
4699
|
title: "Schedule Deployment",
|
|
4697
4700
|
annotations: DESTRUCTIVE,
|
|
4698
|
-
description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). The time is the version's effectiveFrom, set on the DRAFT before publishing; this call takes no time of its own. One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it.
|
|
4701
|
+
description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). The time is the version's effectiveFrom, set on the DRAFT before publishing; this call takes no time of its own. One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it. A scheduled deployment uses exactly the rules that were scheduled; if they do not match when it runs, it fails with HASH_MISMATCH and nothing is deployed.",
|
|
4699
4702
|
inputSchema: z8.object({
|
|
4700
4703
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4701
4704
|
versionId: z8.string().uuid().describe("ACTIVE version ID with a future effective start date"),
|
|
@@ -4823,7 +4826,7 @@ function registerProfileTools(server, callApi) {
|
|
|
4823
4826
|
{
|
|
4824
4827
|
title: "Group Latency Profile",
|
|
4825
4828
|
annotations: READ_ONLY,
|
|
4826
|
-
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by
|
|
4829
|
+
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cacheState (HIT = calls on a version already prepared for execution, MISS = calls that had to prepare the version first and run slower), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99} \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
|
|
4827
4830
|
inputSchema: z9.object({
|
|
4828
4831
|
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
4829
4832
|
versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
@@ -4923,7 +4926,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4923
4926
|
{
|
|
4924
4927
|
title: "Cancel Replay Job",
|
|
4925
4928
|
annotations: DESTRUCTIVE,
|
|
4926
|
-
description: "
|
|
4929
|
+
description: "Cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
|
|
4927
4930
|
inputSchema: z10.object({
|
|
4928
4931
|
jobId: z10.string().describe("Replay job ID")
|
|
4929
4932
|
})
|
|
@@ -4935,7 +4938,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4935
4938
|
{
|
|
4936
4939
|
title: "Export Replay Result",
|
|
4937
4940
|
annotations: READ_ONLY,
|
|
4938
|
-
description: `Export a COMPLETED window replay job
|
|
4941
|
+
description: `Export a COMPLETED window replay job; a job that has not completed is rejected. The two formats carry different things: CSV holds the effect blast radius, whose columns are fixed; JSON holds the changed samples and action parameters, which nest and whose keys differ per tenant. JSON is not a superset of CSV. Returns the file contents as text. ${EXPORT_LIMIT_NOTE}`,
|
|
4939
4942
|
inputSchema: z10.object({
|
|
4940
4943
|
jobId: z10.string().describe("Replay job ID"),
|
|
4941
4944
|
format: z10.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
@@ -5473,7 +5476,7 @@ function createCli() {
|
|
|
5473
5476
|
"json"
|
|
5474
5477
|
).option("--api-key <key>", "Override stored API key").option(
|
|
5475
5478
|
"--base-url <url>",
|
|
5476
|
-
"Override API base URL \u2014 must include the API prefix, e.g.
|
|
5479
|
+
"Override API base URL \u2014 must include the API prefix, e.g. https://api.lexq.io/api/v1/partners"
|
|
5477
5480
|
).option("--dry-run", "Preview the HTTP request without executing").option("--verbose", "Show request/response details").option("--no-color", "Disable colored output");
|
|
5478
5481
|
registerAuthCommands(program);
|
|
5479
5482
|
registerStatusCommand(program);
|
package/dist/mcp/register.d.ts
CHANGED
|
@@ -20,8 +20,8 @@ interface McpToolResult {
|
|
|
20
20
|
* Function signature for making API calls.
|
|
21
21
|
* Injected into tool registration functions for testability and reuse.
|
|
22
22
|
*
|
|
23
|
-
* - lexq
|
|
24
|
-
* -
|
|
23
|
+
* - lexq serve --mcp (stdio): createCallApiFromConfig() reads ~/.lexq/config.json.
|
|
24
|
+
* - A host serving these tools over HTTP passes its own CallApi.
|
|
25
25
|
*/
|
|
26
26
|
type CallApi = (method: string, path: string, opts?: {
|
|
27
27
|
body?: unknown;
|
|
@@ -37,8 +37,8 @@ declare function paginationParams(page?: number, size?: number): Record<string,
|
|
|
37
37
|
* Builds a salient, actionable warning from a response's meta when it reports facts that
|
|
38
38
|
* rules reference but the schema does not define. Returns null when there is nothing to warn.
|
|
39
39
|
*
|
|
40
|
-
* Shared by
|
|
41
|
-
*
|
|
40
|
+
* Shared by every CallApi (the stdio server here, and any host through @lexq/cli/mcp) so the
|
|
41
|
+
* agent-facing text is identical on every surface. The API only populates
|
|
42
42
|
* meta.unregisteredFacts on rule create/update, so this only ever fires there.
|
|
43
43
|
*/
|
|
44
44
|
declare function formatUnregisteredFactWarning(meta: unknown): string | null;
|
|
@@ -84,7 +84,8 @@ declare const isLosslessNumber: (value: unknown) => value is LosslessNumber;
|
|
|
84
84
|
* Registers all MCP tools on the given server.
|
|
85
85
|
*
|
|
86
86
|
* @param server - McpServer instance
|
|
87
|
-
* @param callApi -
|
|
87
|
+
* @param callApi - how tool calls reach the LexQ API. The stdio server builds one from
|
|
88
|
+
* ~/.lexq/config.json; a host passes its own.
|
|
88
89
|
*/
|
|
89
90
|
declare function registerAllTools(server: McpServer, callApi: CallApi): void;
|
|
90
91
|
/**
|
package/dist/mcp/register.js
CHANGED
|
@@ -865,7 +865,7 @@ function registerDeployTools(server, callApi) {
|
|
|
865
865
|
{
|
|
866
866
|
title: "Schedule Deployment",
|
|
867
867
|
annotations: DESTRUCTIVE,
|
|
868
|
-
description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). The time is the version's effectiveFrom, set on the DRAFT before publishing; this call takes no time of its own. One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it.
|
|
868
|
+
description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). The time is the version's effectiveFrom, set on the DRAFT before publishing; this call takes no time of its own. One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it. A scheduled deployment uses exactly the rules that were scheduled; if they do not match when it runs, it fails with HASH_MISMATCH and nothing is deployed.",
|
|
869
869
|
inputSchema: z6.object({
|
|
870
870
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
871
871
|
versionId: z6.string().uuid().describe("ACTIVE version ID with a future effective start date"),
|
|
@@ -981,7 +981,7 @@ import { z as z7 } from "zod";
|
|
|
981
981
|
import dedent3 from "dedent";
|
|
982
982
|
|
|
983
983
|
// src/lib/dry-run-limit.ts
|
|
984
|
-
var DRY_RUN_LIMIT_NOTE = `Not billed and not counted toward the plan's usage. Dry runs, dry run compares, and single-decision replays share a limit of ${MAX_DRY_RUN_EVALUATIONS_PER_MINUTE} evaluations per minute for each organization; a compare counts as two. Past the limit, a call fails with 429 (C-007)
|
|
984
|
+
var DRY_RUN_LIMIT_NOTE = `Not billed and not counted toward the plan's usage. Dry runs, dry run compares, and single-decision replays share a limit of ${MAX_DRY_RUN_EVALUATIONS_PER_MINUTE} evaluations per minute for each organization; a compare counts as two. Past the limit, a call fails with 429 (C-007); wait a minute before trying again.`;
|
|
985
985
|
|
|
986
986
|
// src/mcp/tools/analytics.ts
|
|
987
987
|
function datasetBlock(path) {
|
|
@@ -1067,8 +1067,9 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1067
1067
|
description: dedent3`
|
|
1068
1068
|
Start an Impact Simulation against historical, uploaded, or inline data.
|
|
1069
1069
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1070
|
+
Every record a simulation processes counts toward the plan's simulation usage, including
|
|
1071
|
+
records processed before a cancel. When the plan's limit is reached, a new simulation
|
|
1072
|
+
cannot start.
|
|
1072
1073
|
|
|
1073
1074
|
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
1074
1075
|
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
@@ -1177,7 +1178,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1177
1178
|
annotations: ADDITIVE,
|
|
1178
1179
|
description: dedent3`
|
|
1179
1180
|
Upload inline CSV or JSON content as a simulation dataset.
|
|
1180
|
-
The content is
|
|
1181
|
+
The content is stored as a dataset, and its path is returned in the "path" field.
|
|
1181
1182
|
|
|
1182
1183
|
To use the returned path in lexq_simulation_start, set:
|
|
1183
1184
|
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
@@ -1257,7 +1258,7 @@ function registerProfileTools(server, callApi) {
|
|
|
1257
1258
|
{
|
|
1258
1259
|
title: "Group Latency Profile",
|
|
1259
1260
|
annotations: READ_ONLY,
|
|
1260
|
-
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by
|
|
1261
|
+
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cacheState (HIT = calls on a version already prepared for execution, MISS = calls that had to prepare the version first and run slower), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99} \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
|
|
1261
1262
|
inputSchema: z8.object({
|
|
1262
1263
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
1263
1264
|
versionId: z8.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
@@ -1357,7 +1358,7 @@ function registerReplayTools(server, callApi) {
|
|
|
1357
1358
|
{
|
|
1358
1359
|
title: "Cancel Replay Job",
|
|
1359
1360
|
annotations: DESTRUCTIVE,
|
|
1360
|
-
description: "
|
|
1361
|
+
description: "Cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
|
|
1361
1362
|
inputSchema: z9.object({
|
|
1362
1363
|
jobId: z9.string().describe("Replay job ID")
|
|
1363
1364
|
})
|
|
@@ -1369,7 +1370,7 @@ function registerReplayTools(server, callApi) {
|
|
|
1369
1370
|
{
|
|
1370
1371
|
title: "Export Replay Result",
|
|
1371
1372
|
annotations: READ_ONLY,
|
|
1372
|
-
description: `Export a COMPLETED window replay job
|
|
1373
|
+
description: `Export a COMPLETED window replay job; a job that has not completed is rejected. The two formats carry different things: CSV holds the effect blast radius, whose columns are fixed; JSON holds the changed samples and action parameters, which nest and whose keys differ per tenant. JSON is not a superset of CSV. Returns the file contents as text. ${EXPORT_LIMIT_NOTE}`,
|
|
1373
1374
|
inputSchema: z9.object({
|
|
1374
1375
|
jobId: z9.string().describe("Replay job ID"),
|
|
1375
1376
|
format: z9.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
package/package.json
CHANGED
|
@@ -81,8 +81,7 @@ Use stats to monitor:
|
|
|
81
81
|
|
|
82
82
|
## 2. Failure Logs
|
|
83
83
|
|
|
84
|
-
Failure logs
|
|
85
|
-
Two task types exist:
|
|
84
|
+
Failure logs record background tasks that failed. Two task types exist:
|
|
86
85
|
|
|
87
86
|
| Task type | What failed |
|
|
88
87
|
|------------------------|------------------------------------------------------------------|
|
|
@@ -171,12 +170,12 @@ lexq deploy schedules --format table
|
|
|
171
170
|
lexq deploy unschedule --group-id <gid> --force
|
|
172
171
|
```
|
|
173
172
|
|
|
174
|
-
One pending schedule per group.
|
|
175
|
-
start. A manual deploy, rollback, undeploy, A/B test start, or group archive **cancels**
|
|
176
|
-
pending schedule
|
|
173
|
+
One pending schedule per group. A scheduled deployment goes live within 60 seconds after its
|
|
174
|
+
effective start. A manual deploy, rollback, undeploy, A/B test start, or group archive **cancels**
|
|
175
|
+
the pending schedule, and the schedule's `canceledReason` says which one.
|
|
177
176
|
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
A scheduled deployment uses exactly the rules that were scheduled. If they do not match when it
|
|
178
|
+
runs, it fails with `HASH_MISMATCH` and nothing is deployed.
|
|
180
179
|
|
|
181
180
|
## 4. Decision Provenance
|
|
182
181
|
|
|
@@ -226,9 +225,8 @@ lexq replay export --id <jobId> --as csv --output blast-radius.csv
|
|
|
226
225
|
Replay reports whether the outcome is `DETERMINISTIC` or `REPLAY_MAY_DIFFER` — the latter when the
|
|
227
226
|
original execution depended on values the replay cannot reproduce.
|
|
228
227
|
|
|
229
|
-
Only a `COMPLETED` job can be exported
|
|
230
|
-
|
|
231
|
-
effect blast radius, whose columns are fixed, and JSON carries the changed samples and action
|
|
228
|
+
Only a `COMPLETED` job can be exported. The two formats answer different questions — CSV carries
|
|
229
|
+
the effect blast radius, whose columns are fixed, and JSON carries the changed samples and action
|
|
232
230
|
parameters, which nest and whose keys differ per tenant.
|
|
233
231
|
|
|
234
232
|
## 6. Latency Profile
|
|
@@ -249,8 +247,9 @@ lexq profile <groupId> --version-id <vid> --cache MISS \
|
|
|
249
247
|
`--from` / `--to` are ISO-8601 **instants** here (`2026-07-01T00:00:00Z`), unlike `replay start`
|
|
250
248
|
which takes `yyyy-MM-dd` days in the organization time zone.
|
|
251
249
|
|
|
252
|
-
`p50` / `p95` / `p99` are `null`
|
|
253
|
-
|
|
250
|
+
`p50` / `p95` / `p99` are `null` until the sample is large enough to show them, and the engine
|
|
251
|
+
never estimates a withheld one. `lexq profile --help` lists the thresholds, and `n` is always
|
|
252
|
+
present. A rule whose sample is under 100 is never flagged.
|
|
254
253
|
|
|
255
254
|
## Agent Monitoring Workflow
|
|
256
255
|
|
|
@@ -152,12 +152,7 @@ tables of `lexq deploy history`, `deploy schedules`, `history list`, and
|
|
|
152
152
|
**Constraint:** All groups sharing the same `activationGroup` **must** use identical
|
|
153
153
|
`activationMode` and `activationStrategy`. The API rejects mismatches with a `P-` error.
|
|
154
154
|
|
|
155
|
-
Groups are selected by the order you set, not by comparing action values.
|
|
156
|
-
`MAX_BENEFIT` were removed on 2026-08-01 — the former resolved identically to
|
|
157
|
-
`HIGHEST_PRIORITY` (candidates are evaluated in priority order, so "first match" *is* "lowest
|
|
158
|
-
priority number"), and the latter compared post-action values as scores, which made a 20%
|
|
159
|
-
discount lose to a 10% discount. The engine cannot know which direction is "better" for your
|
|
160
|
-
domain.
|
|
155
|
+
Groups are selected by the priority order you set, not by comparing action values.
|
|
161
156
|
|
|
162
157
|
### Example: Exclusive Discount Groups
|
|
163
158
|
|
|
@@ -187,9 +182,8 @@ Split traffic between the current live version and a test version.
|
|
|
187
182
|
|
|
188
183
|
### Traffic Identity Key
|
|
189
184
|
|
|
190
|
-
**Every execution request has to carry `context.trafficKey`.** That value
|
|
191
|
-
|
|
192
|
-
bucket, so the same key value always reaches the same version for that group.
|
|
185
|
+
**Every execution request has to carry `context.trafficKey`.** That value decides the split: the
|
|
186
|
+
same key value always reaches the same version for that group.
|
|
193
187
|
|
|
194
188
|
Send it in `context`, not in `facts`. Routing never reads `facts`.
|
|
195
189
|
|
|
@@ -202,11 +196,10 @@ Send it in `context`, not in `facts`. Routing never reads `facts`.
|
|
|
202
196
|
|
|
203
197
|
If the key is missing or empty, no request is ever assigned to the test version. The test still
|
|
204
198
|
reports as running and execution logs keep accumulating, so **the failure does not surface in the
|
|
205
|
-
response
|
|
206
|
-
at 0%.
|
|
199
|
+
response**. This is the most common reason a test sits at 0%.
|
|
207
200
|
|
|
208
201
|
Pick a value that is stable per experiment unit (a customer ID, a merchant ID, a session ID) and
|
|
209
|
-
opaque — the whole `context` object is stored
|
|
202
|
+
opaque — the whole `context` object is stored in execution history, so send a hash or a
|
|
210
203
|
surrogate key rather than an email address. A value that changes every request, such as a request
|
|
211
204
|
ID, scatters one user across both versions and destroys the comparison.
|
|
212
205
|
|
|
@@ -376,11 +376,10 @@ live but from a newer deployment. Start again from step 1.
|
|
|
376
376
|
|
|
377
377
|
**Goal:** Check for pending failures and find their cause.
|
|
378
378
|
|
|
379
|
-
Failure logs
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
Management → Failure Logs (https://console.lexq.io/management/failure-logs).
|
|
379
|
+
Failure logs record platform event webhooks and scheduled deployments that failed. There is no
|
|
380
|
+
retry action; fix the cause at the source (webhook endpoint, version state). The CLI can read
|
|
381
|
+
failure logs but cannot resolve or ignore them: an organization admin does that in the console,
|
|
382
|
+
under Management → Failure Logs (https://console.lexq.io/management/failure-logs).
|
|
384
383
|
|
|
385
384
|
```bash
|
|
386
385
|
# 1. List pending failures
|
|
@@ -197,7 +197,7 @@ Each code says **who fixes it**.
|
|
|
197
197
|
|
|
198
198
|
`CONDITION_VALUE_TYPE_INVALID` is worth escalating. The right-hand side of a condition belongs
|
|
199
199
|
to the rule, not to your request, so changing the payload will not help — the rule fails the
|
|
200
|
-
same way for every caller.
|
|
200
|
+
same way for every caller.
|
|
201
201
|
|
|
202
202
|
A rule from another group referencing facts you did not send yields `FACT_NOT_PROVIDED` — this
|
|
203
203
|
is normal, not an error.
|
|
@@ -263,7 +263,7 @@ lexq analytics dataset template \
|
|
|
263
263
|
|
|
264
264
|
# 2. Upload dataset
|
|
265
265
|
lexq analytics dataset upload --file ./my-data.csv
|
|
266
|
-
# → path
|
|
266
|
+
# → returns a path. Pass it to simulation start unchanged.
|
|
267
267
|
|
|
268
268
|
# 3. Start simulation with uploaded path
|
|
269
269
|
lexq analytics simulation start --json '{
|