@lexq/cli 3.0.0 → 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 +1 -3
- package/CONTEXT.md +5 -7
- package/README.md +3 -1
- package/dist/index.js +79 -33
- package/dist/mcp/register.d.ts +6 -5
- package/dist/mcp/register.js +18 -10
- package/package.json +2 -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 +6 -3
- package/skills/lexq-simulation/SKILL.md +2 -2
package/AGENTS.md
CHANGED
|
@@ -46,7 +46,7 @@ Detailed documentation lives in the `skills/` directory. **Read the relevant ski
|
|
|
46
46
|
2. **Always dry-run before publishing.** Run `lexq analytics dry-run` to validate.
|
|
47
47
|
3. **Never modify a non-DRAFT version.** Check version status first. Clone if needed.
|
|
48
48
|
4. **Always check facts before creating rules.** Run `lexq facts list` to know available keys and types.
|
|
49
|
-
5. **
|
|
49
|
+
5. **Leave `--format` at its default, `json`, and parse the output.** A command that writes a file chooses the file's format with `--as`, not `--format`.
|
|
50
50
|
6. **Copy full UUIDs from output.** Never guess or truncate IDs.
|
|
51
51
|
7. **Memo is required for all deploy operations.** `publish`, `live`, `rollback`, `undeploy` — every one needs `--memo`.
|
|
52
52
|
8. **Handle errors gracefully.** Check the error code and follow the action table in `lexq-shared/SKILL.md`.
|
|
@@ -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/README.md
CHANGED
|
@@ -224,13 +224,15 @@ lexq webhook-subscriptions list | get | save | delete | test
|
|
|
224
224
|
|
|
225
225
|
| Flag | Description |
|
|
226
226
|
|--------------------------|--------------------------------------------|
|
|
227
|
-
| `--format <json\|table>` |
|
|
227
|
+
| `--format <json\|table>` | How results are printed (default: `json`) |
|
|
228
228
|
| `--api-key <key>` | Override stored API key |
|
|
229
229
|
| `--base-url <url>` | Override API base URL |
|
|
230
230
|
| `--dry-run` | Preview the HTTP request without executing |
|
|
231
231
|
| `--verbose` | Show request/response details |
|
|
232
232
|
| `--no-color` | Disable colored output |
|
|
233
233
|
|
|
234
|
+
`--format` only changes how results are printed. A command that writes a file, such as `lexq facts export`, chooses the file's format with `--as csv` or `--as json`.
|
|
235
|
+
|
|
234
236
|
## Configuration
|
|
235
237
|
|
|
236
238
|
Config is stored at `~/.lexq/config.json`:
|
package/dist/index.js
CHANGED
|
@@ -184,6 +184,14 @@ async function apiRequestWithMeta(method, path, options = {}) {
|
|
|
184
184
|
|
|
185
185
|
// src/lib/output.ts
|
|
186
186
|
import Table from "cli-table3";
|
|
187
|
+
import { InvalidArgumentError } from "commander";
|
|
188
|
+
var OUTPUT_FORMATS = ["json", "table"];
|
|
189
|
+
function parseOutputFormat(value) {
|
|
190
|
+
if (OUTPUT_FORMATS.includes(value)) return value;
|
|
191
|
+
throw new InvalidArgumentError(
|
|
192
|
+
`Use ${OUTPUT_FORMATS.join(" or ")}. The format of an exported file is chosen with --as.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
187
195
|
function printJson(data) {
|
|
188
196
|
console.log(stringifyJson(data, 2));
|
|
189
197
|
}
|
|
@@ -394,6 +402,7 @@ var PURGE_AFTER_DAYS = 30;
|
|
|
394
402
|
var DESCRIPTION_MAX_LENGTH = 255;
|
|
395
403
|
var COMMIT_MESSAGE_MAX_LENGTH = 500;
|
|
396
404
|
var SCHEDULE_TICK_MAX_MILLIS = 6e4;
|
|
405
|
+
var MAX_DRY_RUN_EVALUATIONS_PER_MINUTE = 60;
|
|
397
406
|
|
|
398
407
|
// src/commands/groups.ts
|
|
399
408
|
import "commander";
|
|
@@ -784,7 +793,8 @@ function registerVersionCommands(program) {
|
|
|
784
793
|
|
|
785
794
|
A version is an immutable snapshot of rules within a policy group.
|
|
786
795
|
|
|
787
|
-
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.
|
|
788
798
|
|
|
789
799
|
Commands:
|
|
790
800
|
list List versions for a group
|
|
@@ -1346,6 +1356,7 @@ function escapeUnsafe(json) {
|
|
|
1346
1356
|
|
|
1347
1357
|
// src/lib/export.ts
|
|
1348
1358
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
1359
|
+
import { extname } from "path";
|
|
1349
1360
|
var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
|
|
1350
1361
|
function parseFormat(value) {
|
|
1351
1362
|
if (typeof value === "string" && EXPORT_FORMATS.includes(value)) {
|
|
@@ -1355,6 +1366,25 @@ function parseFormat(value) {
|
|
|
1355
1366
|
`Unsupported export format: ${String(value)}. Use ${EXPORT_FORMATS.join(" or ")}.`
|
|
1356
1367
|
);
|
|
1357
1368
|
}
|
|
1369
|
+
function fileFormat(command) {
|
|
1370
|
+
const opts = command.opts();
|
|
1371
|
+
const format = parseFormat(opts.as);
|
|
1372
|
+
const extension = typeof opts.output === "string" ? extname(opts.output).slice(1).toLowerCase() : "";
|
|
1373
|
+
let root = command;
|
|
1374
|
+
while (root.parent) root = root.parent;
|
|
1375
|
+
if (EXPORT_FORMATS.includes(extension)) {
|
|
1376
|
+
if (extension !== format) {
|
|
1377
|
+
console.error(
|
|
1378
|
+
`Note: ${opts.output} will hold ${format}. Pass --as ${extension} to write ${extension}.`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
} else if (root.getOptionValueSource("format") === "cli" && command.getOptionValueSource("as") !== "cli") {
|
|
1382
|
+
console.error(
|
|
1383
|
+
`Note: --format sets how results are printed, not the file. This file is ${format}; choose ${EXPORT_FORMATS.join(" or ")} with --as.`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
return format;
|
|
1387
|
+
}
|
|
1358
1388
|
async function runExport(path, format, options) {
|
|
1359
1389
|
const { output, params, ...clientOptions } = options;
|
|
1360
1390
|
const body = await apiRequest("GET", path, {
|
|
@@ -1510,7 +1540,7 @@ ${data.length} unregistered`);
|
|
|
1510
1540
|
STRING types take --allowed-values. NUMBER types take all three.
|
|
1511
1541
|
BOOLEAN takes none — it has only two values to begin with.
|
|
1512
1542
|
For list types the value domain applies to each element.
|
|
1513
|
-
Leave them out for no value domain
|
|
1543
|
+
Leave them out for no value domain.
|
|
1514
1544
|
`
|
|
1515
1545
|
).action(async (opts) => {
|
|
1516
1546
|
try {
|
|
@@ -1621,10 +1651,10 @@ ${warning}
|
|
|
1621
1651
|
$ lexq facts export --as json --output facts.json
|
|
1622
1652
|
$ lexq facts export --keyword user --as json
|
|
1623
1653
|
`
|
|
1624
|
-
).action(async (opts) => {
|
|
1654
|
+
).action(async (opts, command) => {
|
|
1625
1655
|
try {
|
|
1626
1656
|
const globalOpts = program.opts();
|
|
1627
|
-
await runExport("schema/facts/export",
|
|
1657
|
+
await runExport("schema/facts/export", fileFormat(command), {
|
|
1628
1658
|
apiKey: globalOpts.apiKey,
|
|
1629
1659
|
baseUrl: globalOpts.baseUrl,
|
|
1630
1660
|
dryRun: globalOpts.dryRun,
|
|
@@ -2072,8 +2102,8 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2072
2102
|
"after",
|
|
2073
2103
|
dedent7`
|
|
2074
2104
|
|
|
2075
|
-
The version must be ACTIVE with a future effective start date;
|
|
2076
|
-
|
|
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.
|
|
2077
2107
|
That time is the version's effectiveFrom, set on the DRAFT before publishing;
|
|
2078
2108
|
this command takes no time of its own.
|
|
2079
2109
|
One pending schedule per group. Manual deploy/rollback/undeploy, starting
|
|
@@ -2208,13 +2238,13 @@ import "commander";
|
|
|
2208
2238
|
import dedent8 from "dedent";
|
|
2209
2239
|
|
|
2210
2240
|
// src/lib/dataset-upload.ts
|
|
2211
|
-
import { extname } from "path";
|
|
2241
|
+
import { extname as extname2 } from "path";
|
|
2212
2242
|
var DATASET_CONTENT_TYPES = {
|
|
2213
2243
|
".csv": "text/csv",
|
|
2214
2244
|
".json": "application/json"
|
|
2215
2245
|
};
|
|
2216
2246
|
function datasetContentType(filename) {
|
|
2217
|
-
return DATASET_CONTENT_TYPES[
|
|
2247
|
+
return DATASET_CONTENT_TYPES[extname2(filename).toLowerCase()];
|
|
2218
2248
|
}
|
|
2219
2249
|
function uploadDataset(bytes, filename, options) {
|
|
2220
2250
|
const form = new FormData();
|
|
@@ -2226,6 +2256,9 @@ function uploadDataset(bytes, filename, options) {
|
|
|
2226
2256
|
});
|
|
2227
2257
|
}
|
|
2228
2258
|
|
|
2259
|
+
// src/lib/dry-run-limit.ts
|
|
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.`;
|
|
2261
|
+
|
|
2229
2262
|
// src/commands/analytics.ts
|
|
2230
2263
|
function registerAnalyticsCommands(program) {
|
|
2231
2264
|
const analytics = program.command("analytics").description("Dry run, simulation, and requirements").addHelpText(
|
|
@@ -2255,6 +2288,8 @@ function registerAnalyticsCommands(program) {
|
|
|
2255
2288
|
$ lexq analytics dry-run --version-id <vid> --file test-input.json
|
|
2256
2289
|
|
|
2257
2290
|
The request body must include a "facts" object. Use --debug for execution traces
|
|
2291
|
+
|
|
2292
|
+
${DRY_RUN_LIMIT_NOTE}
|
|
2258
2293
|
`
|
|
2259
2294
|
).action(async (opts) => {
|
|
2260
2295
|
try {
|
|
@@ -2293,6 +2328,8 @@ function registerAnalyticsCommands(program) {
|
|
|
2293
2328
|
}'
|
|
2294
2329
|
|
|
2295
2330
|
Shows side-by-side which rules matched and what actions fired for each version.
|
|
2331
|
+
|
|
2332
|
+
${DRY_RUN_LIMIT_NOTE}
|
|
2296
2333
|
`
|
|
2297
2334
|
).action(async (opts) => {
|
|
2298
2335
|
try {
|
|
@@ -2604,10 +2641,10 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2604
2641
|
$ lexq analytics simulation export --id <simId> --as csv --output results.csv
|
|
2605
2642
|
$ lexq analytics simulation export --id <simId> --as json
|
|
2606
2643
|
`
|
|
2607
|
-
).action(async (opts) => {
|
|
2644
|
+
).action(async (opts, command) => {
|
|
2608
2645
|
try {
|
|
2609
2646
|
const globalOpts = program.opts();
|
|
2610
|
-
await runExport(`analytics/simulations/${opts.id}/export`,
|
|
2647
|
+
await runExport(`analytics/simulations/${opts.id}/export`, fileFormat(command), {
|
|
2611
2648
|
apiKey: globalOpts.apiKey,
|
|
2612
2649
|
baseUrl: globalOpts.baseUrl,
|
|
2613
2650
|
dryRun: globalOpts.dryRun,
|
|
@@ -2666,7 +2703,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2666
2703
|
$ lexq analytics dataset template --group-id <gid> --version-id <vid> --output template.csv
|
|
2667
2704
|
$ lexq analytics dataset template --group-id <gid> --version-id <vid> --as json
|
|
2668
2705
|
`
|
|
2669
|
-
).action(async (opts) => {
|
|
2706
|
+
).action(async (opts, command) => {
|
|
2670
2707
|
try {
|
|
2671
2708
|
const globalOpts = program.opts();
|
|
2672
2709
|
const config = loadConfig();
|
|
@@ -2675,7 +2712,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2675
2712
|
if (!apiKey) {
|
|
2676
2713
|
throw new Error('Not authenticated. Run "lexq auth login" first.');
|
|
2677
2714
|
}
|
|
2678
|
-
const fmt =
|
|
2715
|
+
const fmt = fileFormat(command);
|
|
2679
2716
|
const url = new URL(
|
|
2680
2717
|
`analytics/groups/${opts.groupId}/versions/${opts.versionId}/dataset-template`,
|
|
2681
2718
|
baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"
|
|
@@ -2749,7 +2786,7 @@ function registerProfileCommands(program) {
|
|
|
2749
2786
|
per-rule p50s within the group. Absolute ms thresholds are intentionally
|
|
2750
2787
|
not supported. Each percentile is withheld (–) unless n×(1−q) ≥ ${TAIL_MIN_OBS} —
|
|
2751
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
|
|
2752
|
-
on every call, rule detail from a
|
|
2789
|
+
on every call, rule detail from a ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls.
|
|
2753
2790
|
|
|
2754
2791
|
Examples:
|
|
2755
2792
|
$ lexq profile <groupId>
|
|
@@ -2807,7 +2844,9 @@ function registerProfileCommands(program) {
|
|
|
2807
2844
|
console.log(`baseline ${b.phase.padEnd(9)}: ${base}`);
|
|
2808
2845
|
}
|
|
2809
2846
|
if (data.droppedRows > 0) {
|
|
2810
|
-
console.log(
|
|
2847
|
+
console.log(
|
|
2848
|
+
`\u26A0 droppedRows=${data.droppedRows} (some samples could not be read and are not included)`
|
|
2849
|
+
);
|
|
2811
2850
|
}
|
|
2812
2851
|
printTable(
|
|
2813
2852
|
["Rule", "Phase", "n", "p50(ms)", "p95(ms)", "p99(ms)", "\xD7base", "Flagged"],
|
|
@@ -3007,8 +3046,10 @@ function registerReplayCommands(program) {
|
|
|
3007
3046
|
"after",
|
|
3008
3047
|
dedent11`
|
|
3009
3048
|
|
|
3010
|
-
|
|
3011
|
-
|
|
3049
|
+
Returns decisionChanged, effect changes, fired rules on both sides,
|
|
3050
|
+
and a determinism verdict.
|
|
3051
|
+
|
|
3052
|
+
${DRY_RUN_LIMIT_NOTE}
|
|
3012
3053
|
|
|
3013
3054
|
Example:
|
|
3014
3055
|
$ lexq replay decision --trace-id <tid> --version-id <vid>
|
|
@@ -3129,8 +3170,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3129
3170
|
"after",
|
|
3130
3171
|
dedent11`
|
|
3131
3172
|
|
|
3132
|
-
Only COMPLETED jobs can be exported.
|
|
3133
|
-
partial one reads as the whole thing on the receiving end.
|
|
3173
|
+
Only COMPLETED jobs can be exported.
|
|
3134
3174
|
|
|
3135
3175
|
The two formats carry different things. CSV holds the effect blast radius, whose
|
|
3136
3176
|
columns are fixed. JSON holds the changed samples and action parameters, which
|
|
@@ -3140,10 +3180,10 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3140
3180
|
$ lexq replay export --id <jobId> --as csv --output blast-radius.csv
|
|
3141
3181
|
$ lexq replay export --id <jobId> --as json
|
|
3142
3182
|
`
|
|
3143
|
-
).action(async (opts) => {
|
|
3183
|
+
).action(async (opts, command) => {
|
|
3144
3184
|
try {
|
|
3145
3185
|
const globalOpts = program.opts();
|
|
3146
|
-
await runExport(`replay/jobs/${opts.id}/export`,
|
|
3186
|
+
await runExport(`replay/jobs/${opts.id}/export`, fileFormat(command), {
|
|
3147
3187
|
apiKey: globalOpts.apiKey,
|
|
3148
3188
|
baseUrl: globalOpts.baseUrl,
|
|
3149
3189
|
dryRun: globalOpts.dryRun,
|
|
@@ -3264,7 +3304,7 @@ function registerLogCommands(program) {
|
|
|
3264
3304
|
"after",
|
|
3265
3305
|
dedent13`
|
|
3266
3306
|
|
|
3267
|
-
|
|
3307
|
+
Failure logs for platform event webhooks and scheduled deployments that failed.
|
|
3268
3308
|
|
|
3269
3309
|
Commands:
|
|
3270
3310
|
list List failure logs with filters
|
|
@@ -3684,7 +3724,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3684
3724
|
title: "Dry Run",
|
|
3685
3725
|
annotations: READ_ONLY,
|
|
3686
3726
|
description: dedent15`
|
|
3687
|
-
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
|
|
3727
|
+
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects. ${DRY_RUN_LIMIT_NOTE}
|
|
3688
3728
|
|
|
3689
3729
|
Returns:
|
|
3690
3730
|
inputFacts — normalized input facts
|
|
@@ -3713,7 +3753,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3713
3753
|
title: "Dry Run Compare",
|
|
3714
3754
|
annotations: READ_ONLY,
|
|
3715
3755
|
description: dedent15`
|
|
3716
|
-
Compare dry run results between two versions using the same input facts. Useful for validating changes.
|
|
3756
|
+
Compare dry run results between two versions using the same input facts. Useful for validating changes. ${DRY_RUN_LIMIT_NOTE}
|
|
3717
3757
|
|
|
3718
3758
|
Returns:
|
|
3719
3759
|
resultA / resultB — full DryRunResponse for each version
|
|
@@ -3753,8 +3793,9 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3753
3793
|
description: dedent15`
|
|
3754
3794
|
Start an Impact Simulation against historical, uploaded, or inline data.
|
|
3755
3795
|
|
|
3756
|
-
|
|
3757
|
-
|
|
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.
|
|
3758
3799
|
|
|
3759
3800
|
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
3760
3801
|
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
@@ -3863,7 +3904,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3863
3904
|
annotations: ADDITIVE,
|
|
3864
3905
|
description: dedent15`
|
|
3865
3906
|
Upload inline CSV or JSON content as a simulation dataset.
|
|
3866
|
-
The content is
|
|
3907
|
+
The content is stored as a dataset, and its path is returned in the "path" field.
|
|
3867
3908
|
|
|
3868
3909
|
To use the returned path in lexq_simulation_start, set:
|
|
3869
3910
|
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
@@ -4657,7 +4698,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4657
4698
|
{
|
|
4658
4699
|
title: "Schedule Deployment",
|
|
4659
4700
|
annotations: DESTRUCTIVE,
|
|
4660
|
-
description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). The time is the version's effectiveFrom, set on the DRAFT before publishing; this call takes no time of its own. One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it.
|
|
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.",
|
|
4661
4702
|
inputSchema: z8.object({
|
|
4662
4703
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4663
4704
|
versionId: z8.string().uuid().describe("ACTIVE version ID with a future effective start date"),
|
|
@@ -4785,7 +4826,7 @@ function registerProfileTools(server, callApi) {
|
|
|
4785
4826
|
{
|
|
4786
4827
|
title: "Group Latency Profile",
|
|
4787
4828
|
annotations: READ_ONLY,
|
|
4788
|
-
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.`,
|
|
4789
4830
|
inputSchema: z9.object({
|
|
4790
4831
|
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
4791
4832
|
versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
@@ -4830,7 +4871,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4830
4871
|
{
|
|
4831
4872
|
title: "Replay a Decision",
|
|
4832
4873
|
annotations: READ_ONLY,
|
|
4833
|
-
description:
|
|
4874
|
+
description: `Re-evaluate a past execution (traceId) against a candidate version and return the decision diff (decisionChanged, effect changes, fired rules) plus a determinism verdict. Synchronous. ${DRY_RUN_LIMIT_NOTE} A replay sends no webhook, notification, or event: rule actions produce no outward effects.`,
|
|
4834
4875
|
inputSchema: z10.object({
|
|
4835
4876
|
traceId: z10.string().describe("Trace ID of the past execution to replay"),
|
|
4836
4877
|
candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against")
|
|
@@ -4885,7 +4926,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4885
4926
|
{
|
|
4886
4927
|
title: "Cancel Replay Job",
|
|
4887
4928
|
annotations: DESTRUCTIVE,
|
|
4888
|
-
description: "
|
|
4929
|
+
description: "Cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
|
|
4889
4930
|
inputSchema: z10.object({
|
|
4890
4931
|
jobId: z10.string().describe("Replay job ID")
|
|
4891
4932
|
})
|
|
@@ -4897,7 +4938,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4897
4938
|
{
|
|
4898
4939
|
title: "Export Replay Result",
|
|
4899
4940
|
annotations: READ_ONLY,
|
|
4900
|
-
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}`,
|
|
4901
4942
|
inputSchema: z10.object({
|
|
4902
4943
|
jobId: z10.string().describe("Replay job ID"),
|
|
4903
4944
|
format: z10.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
@@ -5428,9 +5469,14 @@ function createCli() {
|
|
|
5428
5469
|
const program = new Command();
|
|
5429
5470
|
program.name("lexq").description(
|
|
5430
5471
|
"Command-line interface for the LexQ Decision Operations Platform. Manage policies, run Impact Simulation, and deploy from your terminal."
|
|
5431
|
-
).version(getVersion2(), "-V, --version").option(
|
|
5472
|
+
).version(getVersion2(), "-V, --version").option(
|
|
5473
|
+
"--format <format>",
|
|
5474
|
+
`How results are printed: ${OUTPUT_FORMATS.join(" or ")}`,
|
|
5475
|
+
parseOutputFormat,
|
|
5476
|
+
"json"
|
|
5477
|
+
).option("--api-key <key>", "Override stored API key").option(
|
|
5432
5478
|
"--base-url <url>",
|
|
5433
|
-
"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"
|
|
5434
5480
|
).option("--dry-run", "Preview the HTTP request without executing").option("--verbose", "Show request/response details").option("--no-color", "Disable colored output");
|
|
5435
5481
|
registerAuthCommands(program);
|
|
5436
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
|
@@ -219,6 +219,7 @@ var STATS_DEFAULT_WINDOW_DAYS = 30;
|
|
|
219
219
|
var STATS_MAX_WINDOW_DAYS = 31;
|
|
220
220
|
var FAILURE_LOG_LOOKBACK_DAYS = 7;
|
|
221
221
|
var PURGE_AFTER_DAYS = 30;
|
|
222
|
+
var MAX_DRY_RUN_EVALUATIONS_PER_MINUTE = 60;
|
|
222
223
|
|
|
223
224
|
// src/mcp/tools/groups.ts
|
|
224
225
|
function registerGroupTools(server, callApi) {
|
|
@@ -652,6 +653,7 @@ var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
|
652
653
|
|
|
653
654
|
// src/lib/export.ts
|
|
654
655
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
656
|
+
import { extname } from "path";
|
|
655
657
|
var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
|
|
656
658
|
|
|
657
659
|
// src/mcp/tools/facts.ts
|
|
@@ -863,7 +865,7 @@ function registerDeployTools(server, callApi) {
|
|
|
863
865
|
{
|
|
864
866
|
title: "Schedule Deployment",
|
|
865
867
|
annotations: DESTRUCTIVE,
|
|
866
|
-
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.",
|
|
867
869
|
inputSchema: z6.object({
|
|
868
870
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
869
871
|
versionId: z6.string().uuid().describe("ACTIVE version ID with a future effective start date"),
|
|
@@ -977,6 +979,11 @@ function registerDeployTools(server, callApi) {
|
|
|
977
979
|
// src/mcp/tools/analytics.ts
|
|
978
980
|
import { z as z7 } from "zod";
|
|
979
981
|
import dedent3 from "dedent";
|
|
982
|
+
|
|
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); wait a minute before trying again.`;
|
|
985
|
+
|
|
986
|
+
// src/mcp/tools/analytics.ts
|
|
980
987
|
function datasetBlock(path) {
|
|
981
988
|
const dataset = { type: "UPLOADED", source: "S3_BUCKET", path };
|
|
982
989
|
return {
|
|
@@ -991,7 +998,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
991
998
|
title: "Dry Run",
|
|
992
999
|
annotations: READ_ONLY,
|
|
993
1000
|
description: dedent3`
|
|
994
|
-
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
|
|
1001
|
+
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects. ${DRY_RUN_LIMIT_NOTE}
|
|
995
1002
|
|
|
996
1003
|
Returns:
|
|
997
1004
|
inputFacts — normalized input facts
|
|
@@ -1020,7 +1027,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1020
1027
|
title: "Dry Run Compare",
|
|
1021
1028
|
annotations: READ_ONLY,
|
|
1022
1029
|
description: dedent3`
|
|
1023
|
-
Compare dry run results between two versions using the same input facts. Useful for validating changes.
|
|
1030
|
+
Compare dry run results between two versions using the same input facts. Useful for validating changes. ${DRY_RUN_LIMIT_NOTE}
|
|
1024
1031
|
|
|
1025
1032
|
Returns:
|
|
1026
1033
|
resultA / resultB — full DryRunResponse for each version
|
|
@@ -1060,8 +1067,9 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1060
1067
|
description: dedent3`
|
|
1061
1068
|
Start an Impact Simulation against historical, uploaded, or inline data.
|
|
1062
1069
|
|
|
1063
|
-
|
|
1064
|
-
|
|
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.
|
|
1065
1073
|
|
|
1066
1074
|
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
1067
1075
|
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
@@ -1170,7 +1178,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1170
1178
|
annotations: ADDITIVE,
|
|
1171
1179
|
description: dedent3`
|
|
1172
1180
|
Upload inline CSV or JSON content as a simulation dataset.
|
|
1173
|
-
The content is
|
|
1181
|
+
The content is stored as a dataset, and its path is returned in the "path" field.
|
|
1174
1182
|
|
|
1175
1183
|
To use the returned path in lexq_simulation_start, set:
|
|
1176
1184
|
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
@@ -1250,7 +1258,7 @@ function registerProfileTools(server, callApi) {
|
|
|
1250
1258
|
{
|
|
1251
1259
|
title: "Group Latency Profile",
|
|
1252
1260
|
annotations: READ_ONLY,
|
|
1253
|
-
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.`,
|
|
1254
1262
|
inputSchema: z8.object({
|
|
1255
1263
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
1256
1264
|
versionId: z8.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
@@ -1295,7 +1303,7 @@ function registerReplayTools(server, callApi) {
|
|
|
1295
1303
|
{
|
|
1296
1304
|
title: "Replay a Decision",
|
|
1297
1305
|
annotations: READ_ONLY,
|
|
1298
|
-
description:
|
|
1306
|
+
description: `Re-evaluate a past execution (traceId) against a candidate version and return the decision diff (decisionChanged, effect changes, fired rules) plus a determinism verdict. Synchronous. ${DRY_RUN_LIMIT_NOTE} A replay sends no webhook, notification, or event: rule actions produce no outward effects.`,
|
|
1299
1307
|
inputSchema: z9.object({
|
|
1300
1308
|
traceId: z9.string().describe("Trace ID of the past execution to replay"),
|
|
1301
1309
|
candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against")
|
|
@@ -1350,7 +1358,7 @@ function registerReplayTools(server, callApi) {
|
|
|
1350
1358
|
{
|
|
1351
1359
|
title: "Cancel Replay Job",
|
|
1352
1360
|
annotations: DESTRUCTIVE,
|
|
1353
|
-
description: "
|
|
1361
|
+
description: "Cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
|
|
1354
1362
|
inputSchema: z9.object({
|
|
1355
1363
|
jobId: z9.string().describe("Replay job ID")
|
|
1356
1364
|
})
|
|
@@ -1362,7 +1370,7 @@ function registerReplayTools(server, callApi) {
|
|
|
1362
1370
|
{
|
|
1363
1371
|
title: "Export Replay Result",
|
|
1364
1372
|
annotations: READ_ONLY,
|
|
1365
|
-
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}`,
|
|
1366
1374
|
inputSchema: z9.object({
|
|
1367
1375
|
jobId: z9.string().describe("Replay job ID"),
|
|
1368
1376
|
format: z9.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexq/cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"mcpName": "io.lexq/lexq",
|
|
5
5
|
"description": "The Decision Operations Platform for engineering teams",
|
|
6
6
|
"type": "module",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"test:rollback": "pnpm build && node tests/rollback.mjs",
|
|
48
48
|
"test:dataset-upload": "pnpm build && node tests/dataset-upload.mjs",
|
|
49
49
|
"test:export-limit": "pnpm build && node tests/export-limit.mjs",
|
|
50
|
+
"test:format-options": "pnpm build && node tests/format-options.mjs",
|
|
50
51
|
"prepublishOnly": "pnpm build",
|
|
51
52
|
"knip": "knip",
|
|
52
53
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
@@ -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
|
|
@@ -72,16 +72,19 @@ Every command accepts these flags:
|
|
|
72
72
|
|
|
73
73
|
| Flag | Description | Default |
|
|
74
74
|
|--------------------------|--------------------------------------------|---------------------------------------|
|
|
75
|
-
| `--format <json\|table>` |
|
|
75
|
+
| `--format <json\|table>` | How results are printed | `json` |
|
|
76
76
|
| `--api-key <key>` | Override stored API key | from config |
|
|
77
77
|
| `--base-url <url>` | Override API base URL | `https://api.lexq.io/api/v1/partners` |
|
|
78
78
|
| `--dry-run` | Preview the HTTP request without executing | off |
|
|
79
79
|
| `--verbose` | Show request/response details | off |
|
|
80
80
|
| `--no-color` | Disable colored output | off |
|
|
81
81
|
|
|
82
|
-
**Agent best practice:**
|
|
82
|
+
**Agent best practice:** Leave `--format` at its default, `json`, and parse the output programmatically. Use
|
|
83
83
|
`--format table` only when displaying to humans.
|
|
84
84
|
|
|
85
|
+
`--format` does not change a file. The commands that write one (`facts export`, `analytics simulation export`,
|
|
86
|
+
`replay export`, `analytics dataset template`) choose its format with `--as csv` or `--as json`.
|
|
87
|
+
|
|
85
88
|
## Command Groups
|
|
86
89
|
|
|
87
90
|
| Group | Commands | Description |
|
|
@@ -105,7 +108,7 @@ Every command accepts these flags:
|
|
|
105
108
|
|
|
106
109
|
## Pagination
|
|
107
110
|
|
|
108
|
-
All list endpoints return
|
|
111
|
+
All list endpoints return one page of results:
|
|
109
112
|
|
|
110
113
|
```json
|
|
111
114
|
{
|
|
@@ -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 '{
|