@smartmemory/stratum 0.3.0

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.
Files changed (81) hide show
  1. package/dist/cli/guard.js +91 -0
  2. package/dist/cli/guard.js.map +1 -0
  3. package/dist/cli/mcp_install.js +302 -0
  4. package/dist/cli/mcp_install.js.map +1 -0
  5. package/dist/cli/query_gate.js +316 -0
  6. package/dist/cli/query_gate.js.map +1 -0
  7. package/dist/cli/stratum.js +291 -0
  8. package/dist/cli/stratum.js.map +1 -0
  9. package/dist/connectors/background.js +531 -0
  10. package/dist/connectors/background.js.map +1 -0
  11. package/dist/connectors/base.js +12 -0
  12. package/dist/connectors/base.js.map +1 -0
  13. package/dist/connectors/claude-bg-worker.js +99 -0
  14. package/dist/connectors/claude-bg-worker.js.map +1 -0
  15. package/dist/connectors/claude.js +170 -0
  16. package/dist/connectors/claude.js.map +1 -0
  17. package/dist/connectors/codex.js +319 -0
  18. package/dist/connectors/codex.js.map +1 -0
  19. package/dist/connectors/index.js +7 -0
  20. package/dist/connectors/index.js.map +1 -0
  21. package/dist/connectors/proc_identity.js +90 -0
  22. package/dist/connectors/proc_identity.js.map +1 -0
  23. package/dist/connectors/runner.js +63 -0
  24. package/dist/connectors/runner.js.map +1 -0
  25. package/dist/contracts/events.json +65 -0
  26. package/dist/contracts/mcp-surface.json +215 -0
  27. package/dist/engine/checkpoint.js +50 -0
  28. package/dist/engine/checkpoint.js.map +1 -0
  29. package/dist/engine/engine.js +2356 -0
  30. package/dist/engine/engine.js.map +1 -0
  31. package/dist/engine/ledger.js +36 -0
  32. package/dist/engine/ledger.js.map +1 -0
  33. package/dist/engine/state.js +42 -0
  34. package/dist/engine/state.js.map +1 -0
  35. package/dist/eval/expr.js +679 -0
  36. package/dist/eval/expr.js.map +1 -0
  37. package/dist/eval/files.js +130 -0
  38. package/dist/eval/files.js.map +1 -0
  39. package/dist/guard/canonical.js +116 -0
  40. package/dist/guard/canonical.js.map +1 -0
  41. package/dist/guard/errors.js +80 -0
  42. package/dist/guard/errors.js.map +1 -0
  43. package/dist/guard/evidence.js +475 -0
  44. package/dist/guard/evidence.js.map +1 -0
  45. package/dist/guard/fingerprint.js +13 -0
  46. package/dist/guard/fingerprint.js.map +1 -0
  47. package/dist/guard/lock.js +360 -0
  48. package/dist/guard/lock.js.map +1 -0
  49. package/dist/guard/store.js +396 -0
  50. package/dist/guard/store.js.map +1 -0
  51. package/dist/guard/transition.js +477 -0
  52. package/dist/guard/transition.js.map +1 -0
  53. package/dist/ir/refs.js +69 -0
  54. package/dist/ir/refs.js.map +1 -0
  55. package/dist/ir/schema.js +123 -0
  56. package/dist/ir/schema.js.map +1 -0
  57. package/dist/ir/validate.js +595 -0
  58. package/dist/ir/validate.js.map +1 -0
  59. package/dist/judge/codex_judged.js +85 -0
  60. package/dist/judge/codex_judged.js.map +1 -0
  61. package/dist/judge/fixture_judged.js +62 -0
  62. package/dist/judge/fixture_judged.js.map +1 -0
  63. package/dist/judge/judged.js +89 -0
  64. package/dist/judge/judged.js.map +1 -0
  65. package/dist/judge/pricing.js +22 -0
  66. package/dist/judge/pricing.js.map +1 -0
  67. package/dist/mcp/contracts.js +162 -0
  68. package/dist/mcp/contracts.js.map +1 -0
  69. package/dist/mcp/main.js +7 -0
  70. package/dist/mcp/main.js.map +1 -0
  71. package/dist/mcp/server.js +370 -0
  72. package/dist/mcp/server.js.map +1 -0
  73. package/dist/migrate/check.js +164 -0
  74. package/dist/migrate/check.js.map +1 -0
  75. package/dist/parallel/certificate.js +37 -0
  76. package/dist/parallel/certificate.js.map +1 -0
  77. package/dist/parallel/evaluate.js +73 -0
  78. package/dist/parallel/evaluate.js.map +1 -0
  79. package/dist/speckit/compiler.js +162 -0
  80. package/dist/speckit/compiler.js.map +1 -0
  81. package/package.json +47 -0
@@ -0,0 +1,679 @@
1
+ import { createFileHelpers, FileValidationError } from "./files.js";
2
+ const BLOCKED_MEMBERS = new Set(["__proto__", "constructor", "prototype"]);
3
+ const IDENTIFIERS = new Set(["result", "input", "item", "prev"]);
4
+ const FUNCTIONS = new Set(["len", "any", "all", "max", "min", "str", "int", "bool", "matches", "file_exists", "file_contains"]);
5
+ const MAX_PARSE_DEPTH = 128;
6
+ const MAX_AST_NODES = 4096;
7
+ const MAX_REGEX_LENGTH = 256;
8
+ const MAX_REGEX_INPUT = 4096;
9
+ class ExpressionError extends Error {
10
+ code;
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.code = code;
14
+ this.name = "ExpressionError";
15
+ }
16
+ }
17
+ class Lexer {
18
+ position = 0;
19
+ source;
20
+ constructor(source) {
21
+ this.source = source;
22
+ }
23
+ next() {
24
+ while (/\s/u.test(this.source[this.position] ?? ""))
25
+ this.position += 1;
26
+ const start = this.position;
27
+ const char = this.source[this.position];
28
+ if (char === undefined)
29
+ return { kind: "eof", text: "", position: start };
30
+ if (char === "\"" || char === "'")
31
+ return this.string(char, start);
32
+ if (/[0-9]/u.test(char))
33
+ return this.number(start);
34
+ if (/[A-Za-z_]/u.test(char))
35
+ return this.identifier(start);
36
+ for (const operator of ["==", "!=", "<=", ">=", "&&", "||", "<", ">", "+", "-", "*", "/", "!"]) {
37
+ if (this.source.startsWith(operator, start)) {
38
+ this.position += operator.length;
39
+ return { kind: "operator", text: operator, position: start };
40
+ }
41
+ }
42
+ if ("().[],".includes(char)) {
43
+ this.position += 1;
44
+ return { kind: "punctuation", text: char, position: start };
45
+ }
46
+ throw new ExpressionError("parse_error", `unexpected character ${JSON.stringify(char)} at position ${start}`);
47
+ }
48
+ identifier(start) {
49
+ this.position += 1;
50
+ while (/[A-Za-z0-9_]/u.test(this.source[this.position] ?? ""))
51
+ this.position += 1;
52
+ const text = this.source.slice(start, this.position);
53
+ return text === "in"
54
+ ? { kind: "operator", text, position: start }
55
+ : { kind: "identifier", text, position: start };
56
+ }
57
+ number(start) {
58
+ const match = /^(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u.exec(this.source.slice(start));
59
+ if (!match)
60
+ throw new ExpressionError("parse_error", `invalid number at position ${start}`);
61
+ this.position += match[0].length;
62
+ const value = Number(match[0]);
63
+ if (!Number.isFinite(value))
64
+ throw new ExpressionError("parse_error", `number must be finite at position ${start}`);
65
+ return { kind: "number", text: match[0], value, position: start };
66
+ }
67
+ string(quote, start) {
68
+ this.position += 1;
69
+ let value = "";
70
+ while (this.position < this.source.length) {
71
+ const char = this.source[this.position++];
72
+ if (char === quote)
73
+ return { kind: "string", text: this.source.slice(start, this.position), value, position: start };
74
+ if (char === "\n" || char === "\r" || char.charCodeAt(0) < 0x20) {
75
+ throw new ExpressionError("parse_error", `unescaped control character in string at position ${this.position - 1}`);
76
+ }
77
+ if (char !== "\\") {
78
+ value += char;
79
+ continue;
80
+ }
81
+ const escaped = this.source[this.position++];
82
+ if (escaped === undefined)
83
+ break;
84
+ const simple = { "\"": "\"", "'": "'", "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: "\t" };
85
+ if (Object.hasOwn(simple, escaped)) {
86
+ value += simple[escaped];
87
+ continue;
88
+ }
89
+ if (escaped === "u") {
90
+ const hex = this.source.slice(this.position, this.position + 4);
91
+ if (!/^[0-9A-Fa-f]{4}$/u.test(hex))
92
+ throw new ExpressionError("parse_error", `invalid unicode escape at position ${this.position - 2}`);
93
+ value += String.fromCharCode(Number.parseInt(hex, 16));
94
+ this.position += 4;
95
+ continue;
96
+ }
97
+ throw new ExpressionError("parse_error", `invalid escape \\${escaped} at position ${this.position - 2}`);
98
+ }
99
+ throw new ExpressionError("parse_error", `unterminated string at position ${start}`);
100
+ }
101
+ }
102
+ class Parser {
103
+ current;
104
+ depth = 0;
105
+ nodes = 0;
106
+ lexer;
107
+ constructor(lexer) {
108
+ this.lexer = lexer;
109
+ this.current = lexer.next();
110
+ }
111
+ parse() {
112
+ const node = this.parseOr();
113
+ if (this.current.kind !== "eof")
114
+ this.fail(`unexpected token ${JSON.stringify(this.current.text)}`);
115
+ return node;
116
+ }
117
+ parseOr() { return this.binary(() => this.parseAnd(), ["||"]); }
118
+ parseAnd() { return this.binary(() => this.parseComparison(), ["&&"]); }
119
+ parseComparison() { return this.binary(() => this.parseAdditive(), ["==", "!=", "<", "<=", ">", ">=", "in"]); }
120
+ parseAdditive() { return this.binary(() => this.parseMultiplicative(), ["+", "-"]); }
121
+ parseMultiplicative() { return this.binary(() => this.parseUnary(), ["*", "/"]); }
122
+ binary(next, operators) {
123
+ let left = next();
124
+ while (this.current.kind === "operator" && operators.includes(this.current.text)) {
125
+ const operator = this.current.text;
126
+ this.advance();
127
+ left = this.node({ kind: "binary", operator, left, right: next() });
128
+ }
129
+ return left;
130
+ }
131
+ parseUnary() {
132
+ if (this.current.kind === "operator" && (this.current.text === "!" || this.current.text === "-")) {
133
+ const operator = this.current.text;
134
+ this.advance();
135
+ return this.nested(() => this.node({ kind: "unary", operator, operand: this.parseUnary() }));
136
+ }
137
+ return this.parsePostfix();
138
+ }
139
+ parsePostfix() {
140
+ let target = this.parsePrimary();
141
+ while (this.current.text === "." || this.current.text === "[") {
142
+ if (this.current.text === ".") {
143
+ this.advance();
144
+ if (this.current.kind !== "identifier")
145
+ this.fail("member access requires a property name");
146
+ const property = this.current.text;
147
+ if (BLOCKED_MEMBERS.has(property))
148
+ this.fail(`member ${JSON.stringify(property)} is forbidden`);
149
+ this.advance();
150
+ target = this.node({ kind: "member", target, property });
151
+ continue;
152
+ }
153
+ this.advance();
154
+ if (this.current.kind !== "number" || !Number.isInteger(this.current.value) || this.current.value < 0) {
155
+ this.fail("index access requires a non-negative integer literal");
156
+ }
157
+ const index = this.current.value;
158
+ this.advance();
159
+ this.expect("]");
160
+ target = this.node({ kind: "index", target, index });
161
+ }
162
+ return target;
163
+ }
164
+ parsePrimary() {
165
+ if (this.current.kind === "string" || this.current.kind === "number") {
166
+ const value = this.current.value;
167
+ this.advance();
168
+ return this.node({ kind: "literal", value });
169
+ }
170
+ if (this.current.kind === "identifier") {
171
+ const name = this.current.text;
172
+ this.advance();
173
+ if (name === "true" || name === "false" || name === "null") {
174
+ return this.node({ kind: "literal", value: name === "null" ? null : name === "true" });
175
+ }
176
+ if (!this.at("("))
177
+ return this.node({ kind: "identifier", name });
178
+ this.advance();
179
+ const args = [];
180
+ if (!this.at(")")) {
181
+ while (true) {
182
+ args.push(this.nested(() => this.parseOr()));
183
+ if (!this.at(","))
184
+ break;
185
+ this.advance();
186
+ }
187
+ }
188
+ this.expect(")");
189
+ return this.node({ kind: "call", name, args });
190
+ }
191
+ if (this.current.text === "(") {
192
+ this.advance();
193
+ const value = this.nested(() => this.parseOr());
194
+ this.expect(")");
195
+ return value;
196
+ }
197
+ this.fail(`expected a literal, identifier, function call, or parenthesized expression`);
198
+ }
199
+ nested(action) {
200
+ this.depth += 1;
201
+ if (this.depth > MAX_PARSE_DEPTH)
202
+ throw new ExpressionError("resource_limit", `expression nesting exceeds ${MAX_PARSE_DEPTH}`);
203
+ try {
204
+ return action();
205
+ }
206
+ finally {
207
+ this.depth -= 1;
208
+ }
209
+ }
210
+ node(node) {
211
+ this.nodes += 1;
212
+ if (this.nodes > MAX_AST_NODES)
213
+ throw new ExpressionError("resource_limit", `expression exceeds ${MAX_AST_NODES} syntax nodes`);
214
+ return node;
215
+ }
216
+ expect(text) {
217
+ if (this.current.text !== text)
218
+ this.fail(`expected ${JSON.stringify(text)}`);
219
+ this.advance();
220
+ }
221
+ advance() { this.current = this.lexer.next(); }
222
+ at(text) { return this.current.text === text; }
223
+ fail(message) { throw new ExpressionError("parse_error", `${message} at position ${this.current.position}`); }
224
+ }
225
+ export function evaluateExpression(expression, bindings, options = {}) {
226
+ try {
227
+ const ast = new Parser(new Lexer(expression)).parse();
228
+ return { ok: true, value: evaluateNode(ast, bindings, options) };
229
+ }
230
+ catch (error) {
231
+ if (error instanceof ExpressionError)
232
+ return { ok: false, reason: `${error.code}: ${error.message}` };
233
+ if (error instanceof FileValidationError)
234
+ return { ok: false, reason: `${error.code}: ${error.message}` };
235
+ return { ok: false, reason: `validation_error: ${errorMessage(error)}` };
236
+ }
237
+ }
238
+ export function evaluatePredicate(expression, bindings, options = {}) {
239
+ const result = evaluateExpression(expression, bindings, options);
240
+ if (!result.ok)
241
+ return { holds: false, reason: result.reason };
242
+ if (typeof result.value !== "boolean") {
243
+ return { holds: false, reason: `type_error: predicate must evaluate to boolean, received ${typeName(result.value)}` };
244
+ }
245
+ return { holds: result.value, reason: `predicate evaluated to ${result.value}` };
246
+ }
247
+ /** Static policy helper: parse once, then inspect every nested AST node. */
248
+ export function expressionUsesFilePredicate(expression) {
249
+ let ast;
250
+ try {
251
+ ast = new Parser(new Lexer(expression)).parse();
252
+ }
253
+ catch {
254
+ return false;
255
+ }
256
+ const visit = (node) => {
257
+ switch (node.kind) {
258
+ case "literal":
259
+ case "identifier": return false;
260
+ case "member": return visit(node.target);
261
+ case "index": return visit(node.target);
262
+ case "call": return node.name === "file_exists" || node.name === "file_contains" || node.args.some(visit);
263
+ case "unary": return visit(node.operand);
264
+ case "binary": return visit(node.left) || visit(node.right);
265
+ }
266
+ };
267
+ return visit(ast);
268
+ }
269
+ /**
270
+ * Engine adapter. During ensure evaluation (context.result present) `result` is the
271
+ * step output under test; for when/set it falls back to the own-property map of
272
+ * completed step outputs. A context workspaceRoot overrides the constructor option.
273
+ */
274
+ export class ExpressionEvaluator {
275
+ options;
276
+ constructor(options = {}) {
277
+ this.options = options;
278
+ }
279
+ evaluate(expression, context) {
280
+ const result = evaluateExpression(expression, this.bindings(context), this.merged(context));
281
+ return result.ok ? result.value : false;
282
+ }
283
+ evaluatePredicate(expression, context) {
284
+ return evaluatePredicate(expression, this.bindings(context), this.merged(context));
285
+ }
286
+ bindings(context) {
287
+ return {
288
+ input: context.input,
289
+ result: Object.hasOwn(context, "result") ? context.result : context.steps,
290
+ ...(Object.hasOwn(context, "item") ? { item: context.item } : {}),
291
+ ...(Object.hasOwn(context, "prev") ? { prev: context.prev } : {}),
292
+ };
293
+ }
294
+ merged(context) {
295
+ return { ...this.options, ...(context.workspaceRoot !== undefined ? { workspaceRoot: context.workspaceRoot } : {}) };
296
+ }
297
+ }
298
+ export function createEvaluator(options = {}) {
299
+ return new ExpressionEvaluator(options);
300
+ }
301
+ function evaluateNode(node, bindings, options) {
302
+ switch (node.kind) {
303
+ case "literal": return node.value;
304
+ case "identifier": return identifier(node.name, bindings);
305
+ case "member": return member(evaluateNode(node.target, bindings, options), node.property);
306
+ case "index": return index(evaluateNode(node.target, bindings, options), node.index);
307
+ case "call": return call(node.name, node.args.map((arg) => evaluateNode(arg, bindings, options)), options);
308
+ case "unary": return unary(node.operator, evaluateNode(node.operand, bindings, options));
309
+ case "binary": return binary(node, bindings, options);
310
+ }
311
+ }
312
+ function identifier(name, bindings) {
313
+ if (!IDENTIFIERS.has(name) || !Object.hasOwn(bindings, name)) {
314
+ throw new ExpressionError("unknown_identifier", `unknown identifier ${JSON.stringify(name)}`);
315
+ }
316
+ return assertJsonValue(bindings[name], `identifier ${name}`);
317
+ }
318
+ function member(target, property) {
319
+ if (BLOCKED_MEMBERS.has(property))
320
+ throw new ExpressionError("validation_error", `member ${JSON.stringify(property)} is forbidden`);
321
+ const object = plainObject(target, "member access target");
322
+ if (!Object.hasOwn(object, property))
323
+ throw new ExpressionError("type_error", `member ${JSON.stringify(property)} does not exist`);
324
+ return object[property];
325
+ }
326
+ function index(target, position) {
327
+ if (!Array.isArray(target))
328
+ throw new ExpressionError("type_error", `index access requires an array, received ${typeName(target)}`);
329
+ if (position >= target.length)
330
+ throw new ExpressionError("type_error", `array index ${position} is out of bounds`);
331
+ return target[position];
332
+ }
333
+ function unary(operator, value) {
334
+ if (operator === "!")
335
+ return !booleanValue(value, "operator !");
336
+ return finiteNumber(-numberValue(value, "unary operator -"), "unary operator -");
337
+ }
338
+ function binary(node, bindings, options) {
339
+ const left = evaluateNode(node.left, bindings, options);
340
+ if (node.operator === "&&") {
341
+ const holds = booleanValue(left, "left operand of &&");
342
+ return holds ? booleanValue(evaluateNode(node.right, bindings, options), "right operand of &&") : false;
343
+ }
344
+ if (node.operator === "||") {
345
+ const holds = booleanValue(left, "left operand of ||");
346
+ return holds ? true : booleanValue(evaluateNode(node.right, bindings, options), "right operand of ||");
347
+ }
348
+ const right = evaluateNode(node.right, bindings, options);
349
+ switch (node.operator) {
350
+ case "==": return jsonEqual(left, right);
351
+ case "!=": return !jsonEqual(left, right);
352
+ case "<": return compare(left, right, "<") < 0;
353
+ case "<=": return compare(left, right, "<=") <= 0;
354
+ case ">": return compare(left, right, ">") > 0;
355
+ case ">=": return compare(left, right, ">=") >= 0;
356
+ case "+":
357
+ if (typeof left === "number" && typeof right === "number")
358
+ return finiteNumber(left + right, "operator +");
359
+ if (typeof left === "string" && typeof right === "string")
360
+ return left + right;
361
+ throw new ExpressionError("type_error", "operator + requires two numbers or two strings");
362
+ case "-": return finiteNumber(numberValue(left, "left operand of -") - numberValue(right, "right operand of -"), "operator -");
363
+ case "*": return finiteNumber(numberValue(left, "left operand of *") * numberValue(right, "right operand of *"), "operator *");
364
+ case "/": {
365
+ const divisor = numberValue(right, "right operand of /");
366
+ if (divisor === 0)
367
+ throw new ExpressionError("type_error", "division by zero");
368
+ return finiteNumber(numberValue(left, "left operand of /") / divisor, "operator /");
369
+ }
370
+ case "in": return contains(left, right);
371
+ default: throw new ExpressionError("parse_error", `unknown operator ${JSON.stringify(node.operator)}`);
372
+ }
373
+ }
374
+ function call(name, args, options) {
375
+ if (!FUNCTIONS.has(name))
376
+ throw new ExpressionError("unknown_function", `unknown function ${JSON.stringify(name)}`);
377
+ switch (name) {
378
+ case "len": {
379
+ arity(name, args, 1);
380
+ const value = args[0];
381
+ if (typeof value === "string" || Array.isArray(value))
382
+ return value.length;
383
+ return Object.keys(plainObject(value, "len argument")).length;
384
+ }
385
+ case "any":
386
+ case "all": {
387
+ arity(name, args, 1);
388
+ const values = arrayValue(args[0], `${name} argument`);
389
+ return name === "any" ? values.some(truthy) : values.every(truthy);
390
+ }
391
+ case "max":
392
+ case "min": {
393
+ arity(name, args, 1);
394
+ const values = arrayValue(args[0], `${name} argument`);
395
+ if (values.length === 0)
396
+ throw new ExpressionError("type_error", `${name} requires a non-empty array`);
397
+ if (values.every((value) => typeof value === "number"))
398
+ return name === "max" ? Math.max(...values) : Math.min(...values);
399
+ if (values.every((value) => typeof value === "string")) {
400
+ return values.slice(1).reduce((best, value) => name === "max" ? (value > best ? value : best) : (value < best ? value : best), values[0]);
401
+ }
402
+ throw new ExpressionError("type_error", `${name} requires an array containing only numbers or only strings`);
403
+ }
404
+ case "str":
405
+ arity(name, args, 1);
406
+ return stringConversion(args[0]);
407
+ case "int":
408
+ arity(name, args, 1);
409
+ return integerConversion(args[0]);
410
+ case "bool":
411
+ arity(name, args, 1);
412
+ return truthy(args[0]);
413
+ case "matches": {
414
+ arity(name, args, 2);
415
+ const input = stringValue(args[0], "first matches argument");
416
+ const pattern = stringValue(args[1], "second matches argument");
417
+ return safeMatches(input, pattern);
418
+ }
419
+ case "file_exists": {
420
+ arity(name, args, 1);
421
+ return helpers(options).fileExists(stringValue(args[0], "file_exists path"));
422
+ }
423
+ case "file_contains": {
424
+ arity(name, args, 2);
425
+ return helpers(options).fileContains(stringValue(args[0], "file_contains path"), stringValue(args[1], "file_contains substring"));
426
+ }
427
+ }
428
+ throw new ExpressionError("unknown_function", `unknown function ${JSON.stringify(name)}`);
429
+ }
430
+ function helpers(options) {
431
+ if (options.workspaceRoot === undefined)
432
+ throw new ExpressionError("validation_error", "file function requires a workspace root");
433
+ return createFileHelpers(options.workspaceRoot);
434
+ }
435
+ function contains(needle, haystack) {
436
+ if (Array.isArray(haystack))
437
+ return haystack.some((value) => jsonEqual(needle, value));
438
+ if (typeof haystack === "string")
439
+ return haystack.includes(stringValue(needle, "left operand of in"));
440
+ const object = plainObject(haystack, "right operand of in");
441
+ const key = stringValue(needle, "left operand of in");
442
+ if (BLOCKED_MEMBERS.has(key))
443
+ throw new ExpressionError("validation_error", `member ${JSON.stringify(key)} is forbidden`);
444
+ return Object.hasOwn(object, key);
445
+ }
446
+ function compare(left, right, operator) {
447
+ if (typeof left === "number" && typeof right === "number")
448
+ return left - right;
449
+ if (typeof left === "string" && typeof right === "string")
450
+ return left < right ? -1 : left > right ? 1 : 0;
451
+ throw new ExpressionError("type_error", `operator ${operator} requires two numbers or two strings`);
452
+ }
453
+ function jsonEqual(left, right) {
454
+ if (left === right)
455
+ return true;
456
+ if (Array.isArray(left) && Array.isArray(right))
457
+ return left.length === right.length && left.every((value, indexValue) => jsonEqual(value, right[indexValue]));
458
+ if (isPlainObject(left) && isPlainObject(right)) {
459
+ const leftKeys = Object.keys(left);
460
+ const rightKeys = Object.keys(right);
461
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && jsonEqual(left[key], right[key]));
462
+ }
463
+ return false;
464
+ }
465
+ function assertJsonValue(value, label, seen = new WeakSet(), depth = 0) {
466
+ if (depth > MAX_PARSE_DEPTH)
467
+ throw new ExpressionError("resource_limit", `${label} exceeds JSON nesting limit ${MAX_PARSE_DEPTH}`);
468
+ if (value === null || typeof value === "boolean" || typeof value === "string")
469
+ return value;
470
+ if (typeof value === "number")
471
+ return finiteNumber(value, label);
472
+ if (typeof value !== "object")
473
+ throw new ExpressionError("type_error", `${label} is not a JSON value`);
474
+ if (seen.has(value))
475
+ throw new ExpressionError("type_error", `${label} contains a cycle`);
476
+ seen.add(value);
477
+ try {
478
+ if (Array.isArray(value))
479
+ return value.map((item) => assertJsonValue(item, label, seen, depth + 1));
480
+ if (!isPlainObject(value))
481
+ throw new ExpressionError("type_error", `${label} must contain plain JSON objects only`);
482
+ const copy = Object.create(null);
483
+ for (const [key, item] of Object.entries(value))
484
+ copy[key] = assertJsonValue(item, label, seen, depth + 1);
485
+ return copy;
486
+ }
487
+ finally {
488
+ seen.delete(value);
489
+ }
490
+ }
491
+ function isPlainObject(value) {
492
+ if (typeof value !== "object" || value === null || Array.isArray(value))
493
+ return false;
494
+ const prototype = Object.getPrototypeOf(value);
495
+ return prototype === Object.prototype || prototype === null;
496
+ }
497
+ function plainObject(value, label) {
498
+ if (!isPlainObject(value))
499
+ throw new ExpressionError("type_error", `${label} requires an object, received ${typeName(value)}`);
500
+ return value;
501
+ }
502
+ function arrayValue(value, label) {
503
+ if (!Array.isArray(value))
504
+ throw new ExpressionError("type_error", `${label} requires an array, received ${typeName(value)}`);
505
+ return value;
506
+ }
507
+ function booleanValue(value, label) {
508
+ if (typeof value !== "boolean")
509
+ throw new ExpressionError("type_error", `${label} requires a boolean, received ${typeName(value)}`);
510
+ return value;
511
+ }
512
+ function numberValue(value, label) {
513
+ if (typeof value !== "number")
514
+ throw new ExpressionError("type_error", `${label} requires a number, received ${typeName(value)}`);
515
+ return value;
516
+ }
517
+ function stringValue(value, label) {
518
+ if (typeof value !== "string")
519
+ throw new ExpressionError("type_error", `${label} requires a string, received ${typeName(value)}`);
520
+ return value;
521
+ }
522
+ function finiteNumber(value, label) {
523
+ if (!Number.isFinite(value))
524
+ throw new ExpressionError("type_error", `${label} must be a finite number`);
525
+ return value;
526
+ }
527
+ function arity(name, args, expected) {
528
+ if (args.length !== expected)
529
+ throw new ExpressionError("type_error", `${name} expects ${expected} argument${expected === 1 ? "" : "s"}, received ${args.length}`);
530
+ }
531
+ function truthy(value) {
532
+ if (value === null)
533
+ return false;
534
+ if (typeof value === "boolean")
535
+ return value;
536
+ if (typeof value === "number")
537
+ return value !== 0;
538
+ if (typeof value === "string" || Array.isArray(value))
539
+ return value.length > 0;
540
+ return Object.keys(value).length > 0;
541
+ }
542
+ function stringConversion(value) {
543
+ if (typeof value === "string")
544
+ return value;
545
+ if (value === null || typeof value === "boolean" || typeof value === "number")
546
+ return String(value);
547
+ return JSON.stringify(value);
548
+ }
549
+ function integerConversion(value) {
550
+ if (typeof value === "boolean")
551
+ return value ? 1 : 0;
552
+ if (typeof value === "number")
553
+ return finiteNumber(Math.trunc(value), "int result");
554
+ if (typeof value === "string" && /^[+-]?\d+$/u.test(value))
555
+ return finiteNumber(Number.parseInt(value, 10), "int result");
556
+ throw new ExpressionError("type_error", `int requires a boolean, finite number, or integer string, received ${typeName(value)}`);
557
+ }
558
+ function safeMatches(input, pattern) {
559
+ if (pattern.length > MAX_REGEX_LENGTH)
560
+ throw new ExpressionError("resource_limit", `regex exceeds ${MAX_REGEX_LENGTH} characters`);
561
+ if (input.length > MAX_REGEX_INPUT)
562
+ throw new ExpressionError("resource_limit", `regex input exceeds ${MAX_REGEX_INPUT} characters`);
563
+ if (!safeRegexShape(pattern))
564
+ throw new ExpressionError("validation_error", "regex contains a potentially unsafe construct");
565
+ try {
566
+ return new RegExp(pattern, "u").test(input);
567
+ }
568
+ catch (error) {
569
+ throw new ExpressionError("validation_error", `invalid regex: ${errorMessage(error)}`);
570
+ }
571
+ }
572
+ /**
573
+ * Conservative shape filter: rejects backreferences, `(?...)` constructs, nested
574
+ * quantifiers, and ADJACENT quantified atoms (`a*a*`, `[ab]+a?`, `(x)*(y)*`) —
575
+ * sequential overlapping quantifiers backtrack combinatorially just like nested
576
+ * ones. False positives are acceptable; a rejected pattern fails the predicate
577
+ * with a structured reason, never hangs the evaluator.
578
+ */
579
+ function safeRegexShape(pattern) {
580
+ if (/\\[1-9]/u.test(pattern) || pattern.includes("(?"))
581
+ return false;
582
+ // Parentheses are TRANSPARENT to adjacency: `(a*)(b*)` backtracks like `a*b*`,
583
+ // so quantified-atom state flows into a group's first atom and out of its last,
584
+ // and a quantified group checks the atom that preceded its `(`.
585
+ const stack = [];
586
+ let quantifiers = 0;
587
+ let lastAtomQuantified = false;
588
+ let index = 0;
589
+ while (index < pattern.length) {
590
+ const char = pattern[index];
591
+ if (char === "(") {
592
+ stack.push({ quantified: false, alternation: false, precededByQuantified: lastAtomQuantified });
593
+ index += 1;
594
+ continue;
595
+ }
596
+ if (char === "|") {
597
+ if (stack.length > 0)
598
+ stack[stack.length - 1].alternation = true;
599
+ lastAtomQuantified = false; // branches do not concatenate
600
+ index += 1;
601
+ continue;
602
+ }
603
+ // Consume exactly one atom.
604
+ let atomEnd;
605
+ let group;
606
+ if (char === ")") {
607
+ group = stack.pop();
608
+ if (!group)
609
+ return false; // unbalanced
610
+ atomEnd = index + 1;
611
+ }
612
+ else if (char === "[") {
613
+ let cursor = index + 1;
614
+ while (cursor < pattern.length && pattern[cursor] !== "]")
615
+ cursor += pattern[cursor] === "\\" ? 2 : 1;
616
+ if (cursor >= pattern.length)
617
+ return false; // unterminated class
618
+ atomEnd = cursor + 1;
619
+ }
620
+ else if (char === "\\") {
621
+ if (index + 1 >= pattern.length)
622
+ return false; // dangling escape
623
+ atomEnd = index + 2;
624
+ }
625
+ else if (isQuantifierStart(char)) {
626
+ return false; // dangling quantifier (nothing to repeat)
627
+ }
628
+ else {
629
+ atomEnd = index + 1; // plain character (anchors included)
630
+ }
631
+ const contentTrailingQuantified = lastAtomQuantified;
632
+ const quantifierTail = quantifierEnd(pattern, atomEnd);
633
+ const quantified = quantifierTail !== atomEnd;
634
+ if (quantified) {
635
+ quantifiers += 1;
636
+ if (quantifiers > 32)
637
+ return false;
638
+ // Adjacency: for a quantified group the relevant neighbor is the atom before
639
+ // its `(`; for any other atom it is the previous atom (incl. through `)`).
640
+ if (group ? group.precededByQuantified : lastAtomQuantified)
641
+ return false;
642
+ if (group && (group.quantified || group.alternation))
643
+ return false; // nested quantifier
644
+ if (stack.length > 0)
645
+ stack[stack.length - 1].quantified = true;
646
+ }
647
+ if (group && (group.quantified || group.alternation) && stack.length > 0) {
648
+ stack[stack.length - 1].quantified = true;
649
+ }
650
+ // An unquantified `)` stays transparent: its trailing content state flows onward.
651
+ lastAtomQuantified = quantified || (group !== undefined && !quantified && contentTrailingQuantified);
652
+ index = quantifierTail;
653
+ }
654
+ return stack.length === 0;
655
+ }
656
+ /** Returns the index just past a quantifier at `at` (incl. `{m,n}` and a lazy `?` suffix), or `at` if none. */
657
+ function quantifierEnd(pattern, at) {
658
+ const char = pattern[at] ?? "";
659
+ let end = at;
660
+ if (char === "*" || char === "+" || char === "?") {
661
+ end = at + 1;
662
+ }
663
+ else if (char === "{") {
664
+ const close = pattern.indexOf("}", at + 1);
665
+ if (close === -1 || !/^\{\d+(,\d*)?\}$/u.test(pattern.slice(at, close + 1)))
666
+ return at; // literal brace
667
+ end = close + 1;
668
+ }
669
+ else {
670
+ return at;
671
+ }
672
+ if (pattern[end] === "?")
673
+ end += 1; // lazy modifier
674
+ return end;
675
+ }
676
+ function isQuantifierStart(value) { return value === "*" || value === "+" || value === "?" || value === "{"; }
677
+ function typeName(value) { return value === null ? "null" : Array.isArray(value) ? "array" : typeof value; }
678
+ function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
679
+ //# sourceMappingURL=expr.js.map