bpmnlint-plugin-camunda-compat 2.59.1 → 2.59.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bpmnlint-plugin-camunda-compat",
3
- "version": "2.59.1",
3
+ "version": "2.59.2",
4
4
  "description": "A bpmnlint plug-in for Camunda compatibility",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -3,7 +3,7 @@ const { is } = require('bpmnlint-utils');
3
3
  const { getPath, pathConcat } = require('@bpmn-io/moddle-utils');
4
4
 
5
5
  const { findExtensionElement, findAncestorAdHocSubProcess, isAgenticAdHocSubProcess } = require('../utils/element');
6
- const { CORRECT_NAME, NAME_ALIASES, findFunctionInvocations, getPositionalArgs } = require('./utils/feel');
6
+ const { CORRECT_NAME, NAME_ALIASES, findFunctionInvocations, getArgs } = require('./utils/feel');
7
7
  const { reportErrors } = require('../utils/reporter');
8
8
  const { ERROR_TYPES } = require('../utils/error-types');
9
9
  const { skipInNonExecutableProcess } = require('../utils/rule');
@@ -34,6 +34,10 @@ const { annotateRule } = require('../helper');
34
34
  *
35
35
  * A description that is simply absent (no argument, or an empty string) is
36
36
  * valid: the fromAi() description is optional, so neither rule reports it.
37
+ *
38
+ * Positional and named calls are equivalent, because
39
+ * FromAiTaggedParameterExtractor funnels both into the same five-argument
40
+ * call. Every check below therefore applies to both forms.
37
41
  */
38
42
  // ─── Constraint validators ────────────────────────────────────────────────────
39
43
 
@@ -214,9 +218,9 @@ module.exports = skipInNonExecutableProcess(function(config = {}) {
214
218
  continue;
215
219
  }
216
220
 
217
- const args = getPositionalArgs(inv.node, expr);
221
+ const args = getArgs(inv.node, expr);
218
222
 
219
- if (args.length === 0) {
223
+ if (!args[0]) {
220
224
  errors.push({
221
225
  message: 'fromAi() requires a key argument: a FEEL path like toolCall.url.',
222
226
  data: { type: ERROR_TYPES.AGENT_FEEL_KEY_MISSING },
@@ -229,7 +233,7 @@ module.exports = skipInNonExecutableProcess(function(config = {}) {
229
233
  errors.push(keyError);
230
234
  }
231
235
 
232
- if (args.length >= 2) {
236
+ if (args[1]) {
233
237
  const descriptionError = validateDescriptionTypeInvalid(args[1]);
234
238
  if (descriptionError) {
235
239
  errors.push(descriptionError);
@@ -284,7 +288,7 @@ module.exports = skipInNonExecutableProcess(function(config = {}) {
284
288
 
285
289
  const expr = source.substring(1).trim();
286
290
  for (const inv of findFunctionInvocations(expr)) {
287
- const args = getPositionalArgs(inv.node, expr);
291
+ const args = getArgs(inv.node, expr);
288
292
  const key = args[ 0 ];
289
293
  if (key && key.type === 'PathExpression' && key.text.startsWith('toolCall.')) {
290
294
  (keyOccurrences[ key.text ] = keyOccurrences[ key.text ] || []).push(input);
@@ -34,11 +34,18 @@ const isFeelProperty = (node, propertyName, value) => {
34
34
 
35
35
  // ─── fromAi() lezer helpers ─────────────────────────────────────────────────
36
36
  // Used by agent-fromai-contract to locate fromAi() calls and read their
37
- // positional arguments.
37
+ // arguments, written either positionally or as named parameters.
38
38
 
39
39
  const CORRECT_NAME = 'fromAi';
40
40
  const NAME_ALIASES = [ 'fromai', 'fromAI' ];
41
41
 
42
+ // fromAi() signature, mirroring FromAiTaggedParameterExtractor in
43
+ // camunda/camunda: it normalizes positional and named calls onto these same
44
+ // five slots, so every downstream check reads one index for both call forms.
45
+ // The list is also what tells a name the engine recognizes from a typo like
46
+ // "valu", which the engine drops.
47
+ const PARAMETER_NAMES = [ 'value', 'description', 'type', 'schema', 'options' ];
48
+
42
49
  function findFunctionInvocations(expr) {
43
50
  const tree = parser.parse(expr);
44
51
  const result = [];
@@ -65,18 +72,64 @@ function findFunctionInvocations(expr) {
65
72
  return result;
66
73
  }
67
74
 
68
- function getPositionalArgs(invocationNode, expr) {
75
+ // Comments and error nodes are siblings of the real arguments, so reading them
76
+ // as arguments would report a comment as the wrong key type, or an incomplete
77
+ // call as a "⚠" one.
78
+ const nextArgNode = node => {
79
+ while (node && node.type.isSkipped) {
80
+ node = node.nextSibling;
81
+ }
82
+ return node;
83
+ };
84
+
85
+ /**
86
+ * Reads a fromAi() call's arguments into an array indexed by PARAMETER_NAMES.
87
+ *
88
+ * A named call fills only the slots it mentions, so the array is sparse: for
89
+ * `fromAi(type: "number")` slot 2 is set while slot 0 is undefined. Callers
90
+ * must test individual slots for presence rather than read `args.length`.
91
+ */
92
+ function getArgs(invocationNode, expr) {
93
+ const toArg = node => ({ type: node.type.name, text: expr.slice(node.from, node.to) });
94
+
69
95
  let child = invocationNode.firstChild;
70
96
  while (child) {
71
97
  if (child.type.name === 'PositionalParameters') {
72
98
  const args = [];
73
- let arg = child.firstChild;
99
+ let arg = nextArgNode(child.firstChild);
74
100
  while (arg) {
75
- args.push({ type: arg.type.name, text: expr.slice(arg.from, arg.to) });
76
- arg = arg.nextSibling;
101
+ args.push(toArg(arg));
102
+ arg = nextArgNode(arg.nextSibling);
77
103
  }
78
104
  return args;
79
105
  }
106
+
107
+ if (child.type.name === 'NamedParameters') {
108
+
109
+ // Named arguments resolve by name, and their order in the call carries no
110
+ // meaning, so each one goes to its own slot. A name outside the signature
111
+ // is dropped, matching the engine, which reads the parameter map by name
112
+ // and never sees it.
113
+ const args = [];
114
+ let param = child.firstChild;
115
+ while (param) {
116
+ if (param.type.name === 'NamedParameter') {
117
+ const nameNode = param.firstChild;
118
+ const valueNode = nameNode && nextArgNode(nameNode.nextSibling);
119
+
120
+ if (nameNode && nameNode.type.name === 'ParameterName' && valueNode) {
121
+ const index = PARAMETER_NAMES.indexOf(expr.slice(nameNode.from, nameNode.to));
122
+
123
+ if (index !== -1) {
124
+ args[ index ] = toArg(valueNode);
125
+ }
126
+ }
127
+ }
128
+ param = param.nextSibling;
129
+ }
130
+ return args;
131
+ }
132
+
80
133
  child = child.nextSibling;
81
134
  }
82
135
  return [];
@@ -87,5 +140,5 @@ module.exports = {
87
140
  CORRECT_NAME,
88
141
  NAME_ALIASES,
89
142
  findFunctionInvocations,
90
- getPositionalArgs
143
+ getArgs
91
144
  };