@telorun/templating 0.18.0 → 0.20.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.
@@ -1 +1 @@
1
- {"version":3,"file":"diagnose.d.ts","sourceRoot":"","sources":["../../src/cel/diagnose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,KAAK,EAAE,QAAQ,EAAiB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAE9E;;;;;;;;;;;;;;;4DAe4D;AAE5D,sEAAsE;AACtE,UAAU,OAAO;IACf,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IACrC,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC;CAC1D;AAWD;uDACuD;AACvD,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAiB7D;AAuJD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAClD,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC;;6BAEyB;IACzB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED;;;yBAGyB;AACzB,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,GAAG,SAAS,CA0DpF;AAED;;6BAE6B;AAC7B,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,WAAW,GAAG,MAAM,CAIpF"}
1
+ {"version":3,"file":"diagnose.d.ts","sourceRoot":"","sources":["../../src/cel/diagnose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,KAAK,EAAE,QAAQ,EAAiB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAE9E;;;;;;;;;;;;;;;4DAe4D;AAE5D,sEAAsE;AACtE,UAAU,OAAO;IACf,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IACrC,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC;CAC1D;AAWD;uDACuD;AACvD,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAiB7D;AA4KD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAClD,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC;;;2CAGuC;IACvC,QAAQ,CAAC,cAAc,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACrD;;6BAEyB;IACzB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED;;;yBAGyB;AACzB,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,GAAG,SAAS,CA0EpF;AAED;;6BAE6B;AAC7B,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,WAAW,GAAG,MAAM,CAIpF"}
@@ -33,7 +33,7 @@ export function functionIndex(env) {
33
33
  * missing from here degrades to "no extra explanation" rather than to a false
34
34
  * error on valid CEL. That is what keeps a cel-js upgrade from turning a new
35
35
  * macro into a manifest this analyzer refuses. */
36
- const MACROS = new Set(["optMap", "optFlatMap"]);
36
+ const MACROS = new Set(["optMap", "optFlatMap", "bind"]);
37
37
  function isNode(v) {
38
38
  return typeof v === "object" && v !== null && "op" in v;
39
39
  }
@@ -159,6 +159,26 @@ function signaturesOf(name, index) {
159
159
  /** Spread-friendly optional `fix`, so an undecidable rewrite simply omits the
160
160
  * field rather than carrying `undefined` into the diagnostic. */
161
161
  const withFix = (fix) => (fix ? { fix } : {});
162
+ /** The literal value of an argument, or `undefined` when it is an expression
163
+ * whose value is not statically known. A negated numeric literal is one node
164
+ * out (`-1` parses as unary minus over a value), and reading it is what lets a
165
+ * bound like "0–10" catch the below-range case as well as the above. */
166
+ function literalOf(node) {
167
+ if (node.op === "value")
168
+ return node.args;
169
+ if (node.op === "-_") {
170
+ const inner = node.args;
171
+ if (isNode(inner) && inner.op === "value") {
172
+ const v = inner.args;
173
+ if (typeof v === "number")
174
+ return -v;
175
+ if (typeof v === "bigint")
176
+ return -v;
177
+ }
178
+ }
179
+ return undefined;
180
+ }
181
+ const ARG_CHECKS = new Map(CEL_FUNCTIONS.flatMap((f) => (f.checkArgs ? [[f.name, f.checkArgs]] : [])));
162
182
  /** Classify every call in `ast`. Runs unconditionally rather than only after a
163
183
  * failed type-check, because a type-check reports its first error and stops:
164
184
  * an expression with two bad calls would otherwise fix one, re-run, and
@@ -207,10 +227,26 @@ export function auditCalls(source, ast, env) {
207
227
  }
208
228
  unresolved.push(call.name);
209
229
  }
230
+ const argumentIssues = [];
231
+ for (const call of calls) {
232
+ const check = ARG_CHECKS.get(call.name);
233
+ if (!check)
234
+ continue;
235
+ const message = check(call.args.map(literalOf));
236
+ // No span on an EngineDiagnostic, so the written call goes in the message —
237
+ // an expression with two calls to the same function is otherwise ambiguous.
238
+ if (message) {
239
+ argumentIssues.push({
240
+ code: "CEL_INVALID_ARGUMENT",
241
+ message: `${message} (in \`${source.slice(call.start, call.end)}\`)`,
242
+ });
243
+ }
244
+ }
210
245
  return {
211
246
  diagnostics,
212
247
  calls: calls.map(({ receiver: _receiver, args: _args, ...site }) => site),
213
248
  unresolved,
249
+ argumentIssues,
214
250
  };
215
251
  }
216
252
  /** Registered signatures for names a type-check failure mentions, so the
@@ -1 +1 @@
1
- {"version":3,"file":"cel.d.ts","sourceRoot":"","sources":["../../src/engines/cel.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAoB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAElG;;;;;;;;;;2BAU2B;AAC3B,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,aAAa,CAqGnF;AAYD;yCACyC;AACzC,eAAO,MAAM,SAAS,EAAE,gBAWvB,CAAC"}
1
+ {"version":3,"file":"cel.d.ts","sourceRoot":"","sources":["../../src/engines/cel.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAoB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAElG;;;;;;;;;;2BAU2B;AAC3B,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,aAAa,CA2GnF;AAYD;yCACyC;AACzC,eAAO,MAAM,SAAS,EAAE,gBAWvB,CAAC"}
@@ -25,6 +25,11 @@ export function analyzeCelExpression(source, env) {
25
25
  };
26
26
  }
27
27
  const audit = auditCalls(source, parsed.ast, env.celEnv);
28
+ // Reported whatever the type-checker concludes. A literal argument a guard
29
+ // will refuse is a defect the manifest states outright, and leaving it to the
30
+ // run is the thing static analysis exists to prevent — so it is not gated on
31
+ // `checkError` the way the call audit is.
32
+ out.push(...audit.argumentIssues);
28
33
  let type;
29
34
  let checkError;
30
35
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/templating",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Telo Templating - Engine registry and shared CEL core for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -36,15 +36,17 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "@marcbachmann/cel-js": "^7.6.1",
39
+ "d3-format": "3.1.2",
39
40
  "re2js": "^2.8.3",
40
41
  "uuid": "^14.0.1",
41
42
  "yaml": "^2.8.3"
42
43
  },
43
44
  "devDependencies": {
45
+ "@types/d3-format": "^3.0.4",
44
46
  "@types/node": "^20.0.0",
45
47
  "typescript": "^5.0.0",
46
48
  "vitest": "^2.1.8",
47
- "@telorun/sdk": "0.83.0"
49
+ "@telorun/sdk": "0.87.0"
48
50
  },
49
51
  "peerDependencies": {
50
52
  "@telorun/sdk": "*"
@@ -15,6 +15,25 @@ export function extractAccessChains(node: ASTNode): string[][] {
15
15
 
16
16
  const COMPREHENSION_METHODS = new Set(["filter", "map", "exists", "all", "exists_one"]);
17
17
 
18
+ /** `cel.bind(name, init, body)` — CEL's only binding form, and the one the
19
+ * parser expands rather than dispatching, so it appears as a receiver call on a
20
+ * bare `cel` identifier that is in no scope and never will be. Both walks below
21
+ * need the same three facts out of it, so the shape is read once here.
22
+ *
23
+ * The receiver is deliberately not returned: it contributes no chain, and
24
+ * descending into it is what produced a `CEL_UNKNOWN_FIELD` for `cel` itself. */
25
+ function bindCall(node: ASTNode): { name: string; init: ASTNode; body: ASTNode } | null {
26
+ if (node.op !== "rcall" || !Array.isArray(node.args)) return null;
27
+ const [method, receiver, callArgs] = node.args as [unknown, unknown, unknown];
28
+ if (method !== "bind") return null;
29
+ if (!isASTNode(receiver) || receiver.op !== "id" || receiver.args !== "cel") return null;
30
+ if (!Array.isArray(callArgs) || callArgs.length !== 3) return null;
31
+ const [nameNode, init, body] = callArgs as [unknown, unknown, unknown];
32
+ if (!isASTNode(nameNode) || nameNode.op !== "id") return null;
33
+ if (!isASTNode(init) || !isASTNode(body)) return null;
34
+ return { name: nameNode.args as string, init, body };
35
+ }
36
+
18
37
  function visitNode(node: ASTNode, chains: string[][], boundVars: Set<string>): void {
19
38
  const chain = extractChain(node, boundVars);
20
39
  if (chain !== null) {
@@ -22,6 +41,15 @@ function visitNode(node: ASTNode, chains: string[][], boundVars: Set<string>): v
22
41
  return;
23
42
  }
24
43
 
44
+ // The bound name is in scope for the body ONLY; `init` is evaluated in the
45
+ // enclosing scope, so a name used there still has to resolve there.
46
+ const bind = bindCall(node);
47
+ if (bind) {
48
+ visitNode(bind.init, chains, boundVars);
49
+ visitNode(bind.body, chains, new Set(boundVars).add(bind.name));
50
+ return;
51
+ }
52
+
25
53
  if (
26
54
  node.op === "rcall" &&
27
55
  Array.isArray(node.args) &&
@@ -246,6 +274,15 @@ function walkNullable(
246
274
  return;
247
275
  }
248
276
 
277
+ // `cel.bind` binds a name for its body; mirror extractAccessChains so a bound
278
+ // name is never read as a nullable context field.
279
+ const bind = bindCall(node);
280
+ if (bind) {
281
+ walkNullable(bind.init, nonNull, boundVars, issues, schema);
282
+ walkNullable(bind.body, nonNull, new Set(boundVars).add(bind.name), issues, schema);
283
+ return;
284
+ }
285
+
249
286
  // Comprehension macros bind a loop variable; mirror extractAccessChains so a
250
287
  // bound var is never mistaken for a nullable context field.
251
288
  if (