@lexq/cli 0.1.46 → 0.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +46 -29
- package/dist/mcp/register.d.ts +16 -1
- package/dist/mcp/register.js +26 -9
- package/package.json +3 -1
- package/skills/lexq-rules/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -48,6 +48,18 @@ function getConfigPath() {
|
|
|
48
48
|
return CONFIG_FILE;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// src/lib/lossless-json.ts
|
|
52
|
+
import {
|
|
53
|
+
LosslessNumber,
|
|
54
|
+
isSafeNumber,
|
|
55
|
+
parse as losslessParse,
|
|
56
|
+
stringify as losslessStringify
|
|
57
|
+
} from "lossless-json";
|
|
58
|
+
var survivesDoubleRoundTrip = (literal) => isSafeNumber(literal);
|
|
59
|
+
var parseNumber = (value) => survivesDoubleRoundTrip(value) ? parseFloat(value) : new LosslessNumber(value);
|
|
60
|
+
var parseJson = (text) => losslessParse(text, void 0, parseNumber);
|
|
61
|
+
var stringifyJson = (value, space) => losslessStringify(value, void 0, space);
|
|
62
|
+
|
|
51
63
|
// src/lib/api-client.ts
|
|
52
64
|
var ApiError = class extends Error {
|
|
53
65
|
constructor(statusCode, errorCode, message) {
|
|
@@ -82,7 +94,7 @@ async function doFetch(method, path, options) {
|
|
|
82
94
|
console.log(` Content-Type: application/json`);
|
|
83
95
|
if (options.body) {
|
|
84
96
|
console.log("Body:");
|
|
85
|
-
console.log(` ${
|
|
97
|
+
console.log(` ${stringifyJson(options.body, 2) ?? ""}`);
|
|
86
98
|
}
|
|
87
99
|
console.log("\n(Use without --dry-run to execute)");
|
|
88
100
|
process.exit(0);
|
|
@@ -92,7 +104,7 @@ async function doFetch(method, path, options) {
|
|
|
92
104
|
const response = await fetch(url.toString(), {
|
|
93
105
|
method,
|
|
94
106
|
headers,
|
|
95
|
-
body: options.body ?
|
|
107
|
+
body: options.body ? stringifyJson(options.body) : void 0
|
|
96
108
|
});
|
|
97
109
|
if (options.verbose) {
|
|
98
110
|
console.error(`\u2190 ${response.status} ${response.statusText} (${Date.now() - startTime}ms)`);
|
|
@@ -121,7 +133,7 @@ async function apiRequestWithMeta(method, path, options = {}) {
|
|
|
121
133
|
if (response.status === 204 || contentType === "") {
|
|
122
134
|
return { data: void 0, meta: null };
|
|
123
135
|
}
|
|
124
|
-
const json = await response.
|
|
136
|
+
const json = parseJson(await response.text());
|
|
125
137
|
assertOk(response, json);
|
|
126
138
|
return { data: json.data, meta: json.meta ?? null };
|
|
127
139
|
}
|
|
@@ -129,7 +141,7 @@ async function apiRequestWithMeta(method, path, options = {}) {
|
|
|
129
141
|
// src/lib/output.ts
|
|
130
142
|
import Table from "cli-table3";
|
|
131
143
|
function printJson(data) {
|
|
132
|
-
console.log(
|
|
144
|
+
console.log(stringifyJson(data, 2));
|
|
133
145
|
}
|
|
134
146
|
function printTable(headers, rows, options) {
|
|
135
147
|
const table = new Table({
|
|
@@ -976,7 +988,7 @@ ${data.length} total`);
|
|
|
976
988
|
).action(async (opts) => {
|
|
977
989
|
try {
|
|
978
990
|
const globalOpts = program.opts();
|
|
979
|
-
const body =
|
|
991
|
+
const body = parseJson(opts.json);
|
|
980
992
|
const { data, meta } = await apiRequestWithMeta(
|
|
981
993
|
"POST",
|
|
982
994
|
`policy-groups/${opts.groupId}/versions/${opts.versionId}/rules`,
|
|
@@ -1012,7 +1024,7 @@ ${data.length} total`);
|
|
|
1012
1024
|
).action(async (opts) => {
|
|
1013
1025
|
try {
|
|
1014
1026
|
const globalOpts = program.opts();
|
|
1015
|
-
const body =
|
|
1027
|
+
const body = parseJson(opts.json);
|
|
1016
1028
|
const { data, meta } = await apiRequestWithMeta(
|
|
1017
1029
|
"PUT",
|
|
1018
1030
|
`policy-groups/${opts.groupId}/versions/${opts.versionId}/rules/${opts.id}`,
|
|
@@ -1938,7 +1950,7 @@ function registerAnalyticsCommands(program) {
|
|
|
1938
1950
|
])
|
|
1939
1951
|
);
|
|
1940
1952
|
console.log("\nExample request:");
|
|
1941
|
-
console.log(
|
|
1953
|
+
console.log(stringifyJson(data.exampleRequest, 2));
|
|
1942
1954
|
} else {
|
|
1943
1955
|
printJson(data);
|
|
1944
1956
|
}
|
|
@@ -2043,10 +2055,10 @@ function registerAnalyticsCommands(program) {
|
|
|
2043
2055
|
`
|
|
2044
2056
|
\u2500\u2500 Metric: ${data.metricSummary.targetVariable} (${data.metricSummary.aggregationType}) \u2500\u2500`
|
|
2045
2057
|
);
|
|
2046
|
-
console.log(`Baseline: ${data.metricSummary.baselineValue
|
|
2047
|
-
console.log(`Simulated: ${data.metricSummary.simulatedValue
|
|
2058
|
+
console.log(`Baseline: ${String(data.metricSummary.baselineValue)}`);
|
|
2059
|
+
console.log(`Simulated: ${String(data.metricSummary.simulatedValue)}`);
|
|
2048
2060
|
console.log(
|
|
2049
|
-
`Delta: ${data.metricSummary.delta
|
|
2061
|
+
`Delta: ${signPrefix(data.metricSummary.delta)}${String(data.metricSummary.delta)}`
|
|
2050
2062
|
);
|
|
2051
2063
|
console.log(
|
|
2052
2064
|
`Change: ${data.metricSummary.deltaPercentage > 0 ? "+" : ""}${data.metricSummary.deltaPercentage.toFixed(1)}%`
|
|
@@ -2063,7 +2075,7 @@ function registerAnalyticsCommands(program) {
|
|
|
2063
2075
|
`Rate \u0394: ${diff.matchedRateDelta > 0 ? "+" : ""}${diff.matchedRateDelta.toFixed(1)}%`
|
|
2064
2076
|
);
|
|
2065
2077
|
console.log(
|
|
2066
|
-
`Metric \u0394: ${diff.metricValueDelta
|
|
2078
|
+
`Metric \u0394: ${signPrefix(diff.metricValueDelta)}${String(diff.metricValueDelta)}`
|
|
2067
2079
|
);
|
|
2068
2080
|
}
|
|
2069
2081
|
if (data.ruleStats?.length) {
|
|
@@ -2071,11 +2083,10 @@ function registerAnalyticsCommands(program) {
|
|
|
2071
2083
|
printTable(
|
|
2072
2084
|
["Rule", "Matched", "Metric"],
|
|
2073
2085
|
data.ruleStats.map((r) => [
|
|
2074
|
-
r.ruleName,
|
|
2086
|
+
r.ruleName.length > 30 ? r.ruleName.substring(0, 30) + "\u2026" : r.ruleName,
|
|
2075
2087
|
r.matchedCount.toLocaleString(),
|
|
2076
|
-
r.metricValue
|
|
2077
|
-
])
|
|
2078
|
-
{ truncate: 30 }
|
|
2088
|
+
String(r.metricValue)
|
|
2089
|
+
])
|
|
2079
2090
|
);
|
|
2080
2091
|
}
|
|
2081
2092
|
} else {
|
|
@@ -2186,7 +2197,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2186
2197
|
}
|
|
2187
2198
|
);
|
|
2188
2199
|
if (opts.output) {
|
|
2189
|
-
const text = typeof response === "string" ? response :
|
|
2200
|
+
const text = typeof response === "string" ? response : stringifyJson(response, 2) ?? "";
|
|
2190
2201
|
writeFileSync2(opts.output, text, "utf-8");
|
|
2191
2202
|
console.log(`\u2713 Exported to ${opts.output}`);
|
|
2192
2203
|
} else {
|
|
@@ -2306,13 +2317,19 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2306
2317
|
}
|
|
2307
2318
|
});
|
|
2308
2319
|
}
|
|
2320
|
+
function signPrefix(value) {
|
|
2321
|
+
const text = String(value);
|
|
2322
|
+
if (text.startsWith("-")) return "";
|
|
2323
|
+
const significand = text.split(/[eE]/)[0] ?? "";
|
|
2324
|
+
return /[1-9]/.test(significand) ? "+" : "";
|
|
2325
|
+
}
|
|
2309
2326
|
function resolveBody(opts) {
|
|
2310
2327
|
if (opts.file) {
|
|
2311
2328
|
const raw = readFileSync2(opts.file, "utf-8");
|
|
2312
|
-
return
|
|
2329
|
+
return parseJson(raw);
|
|
2313
2330
|
}
|
|
2314
2331
|
if (opts.json) {
|
|
2315
|
-
return
|
|
2332
|
+
return parseJson(opts.json);
|
|
2316
2333
|
}
|
|
2317
2334
|
return {};
|
|
2318
2335
|
}
|
|
@@ -3200,8 +3217,8 @@ function createCallApiFromConfig() {
|
|
|
3200
3217
|
headers: { "X-API-KEY": config.apiKey },
|
|
3201
3218
|
body: formData
|
|
3202
3219
|
});
|
|
3203
|
-
const data2 = await response.
|
|
3204
|
-
return { content: [{ type: "text", text:
|
|
3220
|
+
const data2 = parseJson(await response.text());
|
|
3221
|
+
return { content: [{ type: "text", text: stringifyJson(data2, 2) ?? "" }] };
|
|
3205
3222
|
}
|
|
3206
3223
|
const clientOpts = {
|
|
3207
3224
|
apiKey: config.apiKey,
|
|
@@ -3213,7 +3230,7 @@ function createCallApiFromConfig() {
|
|
|
3213
3230
|
params: opts?.params
|
|
3214
3231
|
});
|
|
3215
3232
|
const content = [
|
|
3216
|
-
{ type: "text", text:
|
|
3233
|
+
{ type: "text", text: stringifyJson(data, 2) ?? "" }
|
|
3217
3234
|
];
|
|
3218
3235
|
const warning = formatUnregisteredFactWarning(meta);
|
|
3219
3236
|
if (warning) content.push({ type: "text", text: warning });
|
|
@@ -3577,7 +3594,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3577
3594
|
subsequent actions and subsequent winning rules still run. Enforcement is the caller's
|
|
3578
3595
|
responsibility; the decision surfaces as the is_blocked fact.
|
|
3579
3596
|
|
|
3580
|
-
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..
|
|
3597
|
+
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..34), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
|
|
3581
3598
|
`,
|
|
3582
3599
|
inputSchema: {
|
|
3583
3600
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
@@ -3588,7 +3605,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3588
3605
|
}
|
|
3589
3606
|
},
|
|
3590
3607
|
async ({ groupId, versionId, rule }) => {
|
|
3591
|
-
const body =
|
|
3608
|
+
const body = parseJson(rule);
|
|
3592
3609
|
return callApi("POST", `policy-groups/${groupId}/versions/${versionId}/rules`, { body });
|
|
3593
3610
|
}
|
|
3594
3611
|
);
|
|
@@ -3607,7 +3624,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3607
3624
|
}
|
|
3608
3625
|
},
|
|
3609
3626
|
async ({ groupId, versionId, ruleId, rule }) => {
|
|
3610
|
-
const body =
|
|
3627
|
+
const body = parseJson(rule);
|
|
3611
3628
|
return callApi("PUT", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`, {
|
|
3612
3629
|
body
|
|
3613
3630
|
});
|
|
@@ -3953,7 +3970,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3953
3970
|
}
|
|
3954
3971
|
},
|
|
3955
3972
|
async ({ versionId, facts, includeDebugInfo }) => {
|
|
3956
|
-
const parsedFacts =
|
|
3973
|
+
const parsedFacts = parseJson(facts);
|
|
3957
3974
|
return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
|
|
3958
3975
|
body: { facts: parsedFacts, includeDebugInfo }
|
|
3959
3976
|
});
|
|
@@ -3977,7 +3994,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3977
3994
|
}
|
|
3978
3995
|
},
|
|
3979
3996
|
async ({ versionIdA, versionIdB, facts }) => {
|
|
3980
|
-
const parsedFacts =
|
|
3997
|
+
const parsedFacts = parseJson(facts);
|
|
3981
3998
|
return callApi("POST", "analytics/dry-run/compare", {
|
|
3982
3999
|
body: { versionIdA, versionIdB, facts: parsedFacts }
|
|
3983
4000
|
});
|
|
@@ -4032,7 +4049,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
4032
4049
|
}
|
|
4033
4050
|
},
|
|
4034
4051
|
async ({ body }) => {
|
|
4035
|
-
const parsed =
|
|
4052
|
+
const parsed = parseJson(body);
|
|
4036
4053
|
return callApi("POST", "analytics/simulations", { body: parsed });
|
|
4037
4054
|
}
|
|
4038
4055
|
);
|
|
@@ -4122,12 +4139,12 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
4122
4139
|
});
|
|
4123
4140
|
if (!result.isError) {
|
|
4124
4141
|
try {
|
|
4125
|
-
const uploaded =
|
|
4142
|
+
const uploaded = parseJson(result.content[0]?.text ?? "{}");
|
|
4126
4143
|
if (uploaded.path) {
|
|
4127
4144
|
const dataset = { type: "UPLOADED", source: "S3_BUCKET", path: uploaded.path };
|
|
4128
4145
|
result.content.push({
|
|
4129
4146
|
type: "text",
|
|
4130
|
-
text: "Ready-to-use dataset block for lexq_simulation_start:\n" +
|
|
4147
|
+
text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
|
|
4131
4148
|
});
|
|
4132
4149
|
}
|
|
4133
4150
|
} catch {
|
package/dist/mcp/register.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { LosslessNumber } from 'lossless-json';
|
|
3
|
+
export { LosslessNumber } from 'lossless-json';
|
|
2
4
|
|
|
3
5
|
interface McpToolResult {
|
|
4
6
|
[key: string]: unknown;
|
|
@@ -41,6 +43,19 @@ declare class ApiError extends Error {
|
|
|
41
43
|
constructor(statusCode: number, errorCode: string | null, message: string);
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/** Parses JSON text without rounding numbers that a double cannot hold. */
|
|
47
|
+
declare const parseJson: (text: string) => unknown;
|
|
48
|
+
/**
|
|
49
|
+
* Serializes to JSON, writing preserved numbers back as plain number literals.
|
|
50
|
+
*
|
|
51
|
+
* Use this rather than `JSON.stringify` anywhere a value may have come from {@link parseJson}.
|
|
52
|
+
* The native one turns a preserved number into an object, which breaks both the wire format and
|
|
53
|
+
* `lexq ... --json | jq`.
|
|
54
|
+
*/
|
|
55
|
+
declare const stringifyJson: (value: unknown, space?: number | string) => string | undefined;
|
|
56
|
+
/** Type guard for a preserved number. Check this before any generic object branch. */
|
|
57
|
+
declare const isLosslessNumber: (value: unknown) => value is LosslessNumber;
|
|
58
|
+
|
|
44
59
|
/**
|
|
45
60
|
* Registers all MCP tools on the given server.
|
|
46
61
|
*
|
|
@@ -49,4 +64,4 @@ declare class ApiError extends Error {
|
|
|
49
64
|
*/
|
|
50
65
|
declare function registerAllTools(server: McpServer, callApi: CallApi): void;
|
|
51
66
|
|
|
52
|
-
export { ApiError, type CallApi, type McpToolResult, formatUnregisteredFactWarning, paginationParams, registerAllTools };
|
|
67
|
+
export { ApiError, type CallApi, type McpToolResult, formatUnregisteredFactWarning, isLosslessNumber, paginationParams, parseJson, registerAllTools, stringifyJson };
|
package/dist/mcp/register.js
CHANGED
|
@@ -172,6 +172,19 @@ import { join } from "path";
|
|
|
172
172
|
var CONFIG_DIR = join(homedir(), ".lexq");
|
|
173
173
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
174
174
|
|
|
175
|
+
// src/lib/lossless-json.ts
|
|
176
|
+
import {
|
|
177
|
+
LosslessNumber,
|
|
178
|
+
isSafeNumber,
|
|
179
|
+
parse as losslessParse,
|
|
180
|
+
stringify as losslessStringify
|
|
181
|
+
} from "lossless-json";
|
|
182
|
+
var survivesDoubleRoundTrip = (literal) => isSafeNumber(literal);
|
|
183
|
+
var parseNumber = (value) => survivesDoubleRoundTrip(value) ? parseFloat(value) : new LosslessNumber(value);
|
|
184
|
+
var parseJson = (text) => losslessParse(text, void 0, parseNumber);
|
|
185
|
+
var stringifyJson = (value, space) => losslessStringify(value, void 0, space);
|
|
186
|
+
var isLosslessNumber = (value) => value instanceof LosslessNumber;
|
|
187
|
+
|
|
175
188
|
// src/lib/api-client.ts
|
|
176
189
|
var ApiError = class extends Error {
|
|
177
190
|
constructor(statusCode, errorCode, message) {
|
|
@@ -388,7 +401,7 @@ function registerRuleTools(server, callApi) {
|
|
|
388
401
|
subsequent actions and subsequent winning rules still run. Enforcement is the caller's
|
|
389
402
|
responsibility; the decision surfaces as the is_blocked fact.
|
|
390
403
|
|
|
391
|
-
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..
|
|
404
|
+
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..34), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
|
|
392
405
|
`,
|
|
393
406
|
inputSchema: {
|
|
394
407
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
@@ -399,7 +412,7 @@ function registerRuleTools(server, callApi) {
|
|
|
399
412
|
}
|
|
400
413
|
},
|
|
401
414
|
async ({ groupId, versionId, rule }) => {
|
|
402
|
-
const body =
|
|
415
|
+
const body = parseJson(rule);
|
|
403
416
|
return callApi("POST", `policy-groups/${groupId}/versions/${versionId}/rules`, { body });
|
|
404
417
|
}
|
|
405
418
|
);
|
|
@@ -418,7 +431,7 @@ function registerRuleTools(server, callApi) {
|
|
|
418
431
|
}
|
|
419
432
|
},
|
|
420
433
|
async ({ groupId, versionId, ruleId, rule }) => {
|
|
421
|
-
const body =
|
|
434
|
+
const body = parseJson(rule);
|
|
422
435
|
return callApi("PUT", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`, {
|
|
423
436
|
body
|
|
424
437
|
});
|
|
@@ -764,7 +777,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
764
777
|
}
|
|
765
778
|
},
|
|
766
779
|
async ({ versionId, facts, includeDebugInfo }) => {
|
|
767
|
-
const parsedFacts =
|
|
780
|
+
const parsedFacts = parseJson(facts);
|
|
768
781
|
return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
|
|
769
782
|
body: { facts: parsedFacts, includeDebugInfo }
|
|
770
783
|
});
|
|
@@ -788,7 +801,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
788
801
|
}
|
|
789
802
|
},
|
|
790
803
|
async ({ versionIdA, versionIdB, facts }) => {
|
|
791
|
-
const parsedFacts =
|
|
804
|
+
const parsedFacts = parseJson(facts);
|
|
792
805
|
return callApi("POST", "analytics/dry-run/compare", {
|
|
793
806
|
body: { versionIdA, versionIdB, facts: parsedFacts }
|
|
794
807
|
});
|
|
@@ -843,7 +856,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
843
856
|
}
|
|
844
857
|
},
|
|
845
858
|
async ({ body }) => {
|
|
846
|
-
const parsed =
|
|
859
|
+
const parsed = parseJson(body);
|
|
847
860
|
return callApi("POST", "analytics/simulations", { body: parsed });
|
|
848
861
|
}
|
|
849
862
|
);
|
|
@@ -933,12 +946,12 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
933
946
|
});
|
|
934
947
|
if (!result.isError) {
|
|
935
948
|
try {
|
|
936
|
-
const uploaded =
|
|
949
|
+
const uploaded = parseJson(result.content[0]?.text ?? "{}");
|
|
937
950
|
if (uploaded.path) {
|
|
938
951
|
const dataset = { type: "UPLOADED", source: "S3_BUCKET", path: uploaded.path };
|
|
939
952
|
result.content.push({
|
|
940
953
|
type: "text",
|
|
941
|
-
text: "Ready-to-use dataset block for lexq_simulation_start:\n" +
|
|
954
|
+
text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
|
|
942
955
|
});
|
|
943
956
|
}
|
|
944
957
|
} catch {
|
|
@@ -1383,7 +1396,11 @@ function registerAllTools(server, callApi) {
|
|
|
1383
1396
|
}
|
|
1384
1397
|
export {
|
|
1385
1398
|
ApiError,
|
|
1399
|
+
LosslessNumber,
|
|
1386
1400
|
formatUnregisteredFactWarning,
|
|
1401
|
+
isLosslessNumber,
|
|
1387
1402
|
paginationParams,
|
|
1388
|
-
|
|
1403
|
+
parseJson,
|
|
1404
|
+
registerAllTools,
|
|
1405
|
+
stringifyJson
|
|
1389
1406
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexq/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"description": "LexQ CLI — manage policies, simulate rules, and deploy from the terminal. Built for humans and AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"enums": "node scripts/gen-enums.mjs",
|
|
31
31
|
"enums:check": "node scripts/gen-enums.mjs --check",
|
|
32
32
|
"start": "node dist/index.js",
|
|
33
|
+
"test:decimals": "pnpm build && node tests/exact-decimals.mjs",
|
|
33
34
|
"prepublishOnly": "pnpm build",
|
|
34
35
|
"knip": "knip",
|
|
35
36
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"cli-table3": "^0.6.5",
|
|
62
63
|
"commander": "^13.1.0",
|
|
63
64
|
"dedent": "^1.7.2",
|
|
65
|
+
"lossless-json": "4.3.1",
|
|
64
66
|
"zod": "^3.25.76"
|
|
65
67
|
},
|
|
66
68
|
"devDependencies": {
|
|
@@ -193,7 +193,7 @@ Each rule can have multiple actions. Actions fire sequentially.
|
|
|
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. |
|
|
195
195
|
| `refVar` | optional | Base for percentage calculation. Omit to use `targetVar` itself. |
|
|
196
|
-
| `rounding` | optional | `{ scale: 0..
|
|
196
|
+
| `rounding` | optional | `{ scale: 0..34, mode?: HALF_UP \| ... }`. Omit for lossless full precision. |
|
|
197
197
|
|
|
198
198
|
**operator × method matrix**
|
|
199
199
|
|